Skip to content

feat(cli): add ark context command#5

Merged
Anekoique merged 2 commits into
mainfrom
feat/ark-context
Apr 27, 2026
Merged

feat(cli): add ark context command#5
Anekoique merged 2 commits into
mainfrom
feat/ark-context

Conversation

@Anekoique
Copy link
Copy Markdown
Owner

@Anekoique Anekoique commented Apr 27, 2026

This pull request updates the Ark tool's installed version and feature specifications, and clarifies the behavior of the spec extract operation. The most important changes are grouped below:

Ark version and feature registration updates:

  • Bump Ark tool version from 0.1.1 to 0.1.2 in .ark/.installed.json.
  • Register the new ark-context feature in .ark/specs/features/INDEX.md.

Specification and documentation clarifications:

  • Clarify in .ark/specs/features/ark-agent-namespace/SPEC.md that spec extract copies the latest ## Spec section verbatim from the PLAN and does not merge across iterations; the PLAN must keep ## Spec self-contained.

File hash updates (as a result of the above changes):

  • Update file hashes in .ark/.installed.json to reflect new or changed files and specifications.

Summary by CodeRabbit

  • New Features

    • Added ark context — read-only snapshot of git + workflow state (JSON/text) with session/phase scoping and phase-specific --for filters.
    • ark init now installs a SessionStart hook that auto-invokes ark context at session start.
  • Behavior Changes

    • ark load restores and ark unload captures the SessionStart hook; ark remove deletes the Ark hook; ark upgrade re-applies the hook idempotently.
  • Documentation

    • Updated workflow and command guides to use ark context for phase-based state queries and tightened SPEC iteration rules.

@Anekoique Anekoique requested a review from Copilot April 27, 2026 08:29
@claude
Copy link
Copy Markdown

claude Bot commented Apr 27, 2026

Claude encountered an error —— View job


I'll analyze this and get back to you.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 27, 2026

📝 Walkthrough

Walkthrough

Adds a new read-only ark context command producing deterministic git + .ark/ workflow snapshots with session/phase scopes and JSON/text formats; integrates a SessionStart hook lifecycle (install/capture/restore/remove/reapply), extends snapshots to include hook bodies, introduces discovery-based layout resolution, and updates docs and templates to use context snapshots. (49 words)

Changes

Cohort / File(s) Summary
Context Command Core
crates/ark-core/src/commands/context/mod.rs, crates/ark-core/src/commands/context/gather.rs, crates/ark-core/src/commands/context/model.rs, crates/ark-core/src/commands/context/projection.rs, crates/ark-core/src/commands/context/related_specs.rs, crates/ark-core/src/commands/context/render.rs
Implements ark context: full gather (git, tasks, specs, archive), JSON schema (SCHEMA_VERSION = 1), projection by scope/phase, PRD related-specs extraction, and text renderer. Adds public APIs and unit tests.
CLI Integration & Root Discovery
crates/ark-cli/src/main.rs, crates/ark-core/src/layout.rs
Adds ark context flags (--scope/--for/--format), uses Layout::discover_from for discovery-aware commands via TargetArgs::resolve_with_discovery().
Git Subprocess & FS Settings Helpers
crates/ark-core/src/io/git.rs, crates/ark-core/src/io/fs.rs, crates/ark-core/src/io/mod.rs
Centralizes git spawning (run_gitGitOutput with GitSpawn error) and settings.json hook helpers (upsert/read/remove, idempotent writes, legacy-shape matching). Restricts ad-hoc git spawns.
Snapshot & State Extensions
crates/ark-core/src/state/snapshot.rs, crates/ark-core/src/state/mod.rs
Adds Snapshot.hook_bodies and SnapshotHookBody with #[serde(default)] for forward-compatible snapshot capture/restore; re-exports via state module.
Lifecycle Commands Hook Management
crates/ark-core/src/commands/init.rs, load.rs, unload.rs, remove.rs, upgrade.rs, crates/ark-core/src/commands/mod.rs
init writes canonical SessionStart hook; unload captures hook into snapshot; load restores captured hook or inserts canonical; remove deletes Ark hook entry and reports removal; upgrade re-applies the hook unconditionally. Adds tests and summary/report fields.
Public API & Errors
crates/ark-core/src/lib.rs, crates/ark-core/src/error.rs
Re-exports new context types (ContextOptions/ContextSummary/ContextScope etc.), and adds Error::GitSpawn variant.
Layout & Paths
crates/ark-core/src/layout.rs
Adds constants and accessors for .ark/specs/project/INDEX.md and .claude/settings.json, and Layout::discover_from(cwd) with tests.
Docs / Workflow / Templates
.ark/workflow.md, templates/ark/workflow.md, templates/claude/commands/ark/{design,quick,archive}.md, .claude/commands/ark/{design,quick,archive}.md, AGENTS.md, README.md, docs/ROADMAP.md
Refactors workflow to stage procedures using ark context snapshots, tightens ## Spec self-containment, updates templates to invoke ark context, and documents hook lifecycle and new commands.
Feature & Task Artifacts
.ark/specs/features/INDEX.md, .ark/specs/features/ark-context/SPEC.md, .ark/specs/features/ark-agent-namespace/SPEC.md, .ark/specs/features/ark-upgrade/SPEC.md, .ark/tasks/archive/2026-04/ark-context/*, .ark/tasks/archive/2026-04/spec-extract-self-contained/*
Adds and updates ark-context PLANS/REVIEWS/PRD/VERIFY, archives task iterations, adjusts ark-upgrade SPEC, and introduces spec-extract-self-contained PRD/task.
User-facing Hook Template
.claude/settings.json
Adds a .claude/settings.json file with canonical SessionStart hook calling ark context --scope session --format json.
Installer Metadata
.ark/.installed.json
Bumps installed metadata version/timestamp and refreshes hashes for updated docs/templates/commands.
Command docs (quick/archive/design)
.claude/commands/ark/quick.md, .claude/commands/ark/archive.md, .claude/commands/ark/design.md
Switches procedural guidance to use ark context JSON snapshots as the authoritative input at phase starts and updates pre-archive checks and verification guidance.

Sequence Diagram(s)

sequenceDiagram
    participant User as User
    participant CLI as ark context<br/>(crates/ark-cli)
    participant Discover as Layout::discover_from
    participant Gather as gather_context
    participant IO as io/git.rs + io/fs.rs
    participant Parse as PRD/Specs Parser
    participant Project as project()<br/>(projection)
    participant Render as JSON/Text Encoder

    User->>CLI: ark context --scope session --format json
    CLI->>Discover: discover_from(cwd)
    Discover->>IO: check ancestors for .ark/
    IO-->>Discover: .ark/ path
    Discover-->>CLI: Layout { root }
    CLI->>Gather: gather_context(&layout)
    Gather->>IO: run_git(...) & read .ark/tasks/, specs files
    IO-->>Gather: git output, file contents
    Gather->>Parse: parse INDEX.md / managed blocks / PRD related-specs
    Parse-->>Gather: spec rows, related-specs
    Gather-->>CLI: Context { git, tasks, specs, archive }
    CLI->>Project: project(ctx, Scope::Session)
    Project-->>CLI: ProjectedContext
    CLI->>Render: format as JSON (pretty) or Text
    Render-->>CLI: output string
    CLI-->>User: print stdout (SessionStart wrapper if session+json)
Loading
sequenceDiagram
    participant User as User
    participant Init as ark init
    participant FS as io/fs.rs
    participant Settings as .claude/settings.json

    User->>Init: ark init
    Init->>FS: update_settings_hook(path, canonical_entry)
    FS->>Settings: read existing hooks (or {})
    Settings-->>FS: current JSON
    FS->>FS: normalize hooks.SessionStart, upsert by command identity
    FS->>Settings: write pretty JSON if changed
    Settings-->>FS: OK
    FS-->>Init: updated = true/false
    Init-->>User: initialization completed (hook installed/skipped)
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Poem

🐰 I nibbled through git and specs so bright,

Collected hooks at dawn and night,
Scoped the session, trimmed the phase,
JSON twinkled in tidy arrays,
Ark context hops — a tidy sight!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(cli): add ark context command' directly and accurately summarizes the main change: introducing a new CLI subcommand. It is concise, specific, and clearly communicates the primary purpose of the changeset.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/ark-context

Comment @coderabbitai help to get the list of available commands and usage tips.

@claude
Copy link
Copy Markdown

claude Bot commented Apr 27, 2026

Claude finished @Anekoique's task in 4m 31s —— View job


PR Review — feat(cli): add ark context command

  • Read AGENTS.md and conventions
  • Read core Rust source files
  • Analyze error handling, filesystem safety, correctness
  • Post findings

Overall the implementation is clean, well-tested, and follows the project conventions well. Four findings below, ordered by severity.


1. Path traversal via .current slug — gather.rs:318

layout.task_dir(slug) is a bare join(slug) with no sanitization. slug comes from the user-written .current file. A .current containing ../../home would cause the subsequent reads (including TaskToml::load(&task_dir)) to traverse outside the project root.

In practice the tasks.active lookup at line 324 would return None for any traversal slug, so the blast radius is limited to a single task.toml read that fails gracefully. But resolve_safe exists in the codebase precisely for this class of issue (it's used in snapshot restore paths), and this should use it too.

// gather.rs:318 — current code
let task_dir = layout.task_dir(slug);

// suggested fix
let task_dir = layout.resolve_safe(format!(".ark/tasks/{slug}"))
    .unwrap_or_else(|_| return Ok(None));  // or propagate the error

Alternatively, validate slug doesn't contain / or .. before the join. Fix this →


2. {:?} (Debug) formatting for Tier/Phase in text output — render.rs:92-93, 131

Tier and Phase have no Display impl. The text renderer uses {:?}, which emits PascalCase (Quick, Deep, Plan…), while the JSON serializer uses #[serde(rename_all = "lowercase")] (quick, deep, plan…). A user reading text output sees:

tier: Deep
phase: Plan

while a machine reading JSON sees "tier": "deep", "phase": "plan". Introducing Display on both enums (delegating to the serde lowercase form) would make text and JSON consistent and satisfy the convention of impl Display for user-facing output.

// crates/ark-core/src/commands/agent/state.rs
impl fmt::Display for Tier {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(match self {
            Self::Quick => "quick",
            Self::Standard => "standard",
            Self::Deep => "deep",
        })
    }
}
// similarly for Phase

Then render.rs lines 92, 93, 131 change {:?}{}. Fix this →


3. is_section_header_line false positives — related_specs.rs:55-61

The section terminator check:

fn is_section_header_line(line: &str) -> bool {
    if !line.starts_with("[**") { return false; }
    line.contains("**]")
}

Any line starting with [** and containing **] — including e.g. [**foo**] and some trailing text or a bold link in a list item — terminates the scan. A PRD body that mentions another section in a list item (- see [**Outcome**] for details) would cut off spec extraction prematurely.

A tighter check would require **] to appear with no significant content before end-of-line:

fn is_section_header_line(line: &str) -> bool {
    let trimmed = line.trim_end_matches(['\n', '\r']).trim_end();
    trimmed.starts_with("[**") && trimmed.ends_with("**]")
}

This matches [**Related Specs**] and [**Outcome**] exactly while rejecting inline bold links. Fix this →


4. Git spawn failure hard-errors contextgather.rs:46

let branch = git::run_git(&["rev-parse", "--abbrev-ref", "HEAD"], project_root)?;

Non-zero exit (e.g., no commits yet, not a git repo) soft-fails to GitState::default() — correct. But if git isn't in PATH, run_git returns Err(Error::GitSpawn), which propagates up and causes context to return an error entirely. For a read-only "what's the project state?" command, a missing git binary should probably also soft-fail:

let branch = match git::run_git(&["rev-parse", "--abbrev-ref", "HEAD"], project_root) {
    Ok(o) => o,
    Err(Error::GitSpawn { .. }) => return Ok(GitState::default()),
    Err(e) => return Err(e),
};

Minor notes (no action required)

  • list_artifacts reads full file content for line count (gather.rs:356): text.lines().count() loads the whole file. Fine for typical task artifacts, but count_lines_in_file via byte-counting would be more efficient for large PLANs. Not an issue in practice.

  • #[cfg(test)] heuristic in commands_no_bare_command_new (mod.rs:258): The test correctly notes it strips everything after the first #[cfg(test)]. If live code ever uses a conditional compile attribute before a test block, the strip would be too aggressive. The heuristic is intentional and acceptable for its stated purpose.

  • gather_current_task loads task.toml twice (gather.rs:323): let _ = TaskToml::load(&task_dir)? re-reads a TOML that gather_tasks already parsed (and soft-failed on). The reload is intentional — it propagates corruption errors for the current task vs. silently skipping it when building the active list. The logic is correct; a comment cross-referencing the asymmetric skip/propagate design would help future readers.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new top-level ark context command (plus supporting snapshot/hook/git plumbing) and updates the Ark workflow/slash-command templates and specs to use ark context as the primary source of project/phase state.

Changes:

  • Introduce ark context (JSON/text output, session/phase projections) and wire it into ark-cli.
  • Add .claude/settings.json SessionStart hook management (install/upgrade/unload/load/remove) and snapshot support for capturing the Ark hook entry.
  • Update workflow docs/specs/templates to reference ark context, and bump installed version metadata/hashes.

Reviewed changes

Copilot reviewed 49 out of 49 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
templates/claude/commands/ark/quick.md Updates template quick command to use ark context for design-phase context.
templates/claude/commands/ark/design.md Updates design template to refresh context per phase and clarifies self-contained ## Spec rule.
templates/claude/commands/ark/archive.md Updates archive template to use verify-phase context snapshot instead of reading task.toml directly.
templates/ark/workflow.md Rewrites workflow doc to center ark context calls and clarifies PLAN/REVIEW/VERIFY gates.
docs/ROADMAP.md Marks ark context, ark unload, and ark upgrade items as shipped and updates syntax.
crates/ark-core/src/state/snapshot.rs Adds hook_bodies to snapshot + SnapshotHookBody model and tests for backward compat/round-trip.
crates/ark-core/src/state/mod.rs Re-exports new snapshot types.
crates/ark-core/src/lib.rs Re-exports context command API types and entrypoint.
crates/ark-core/src/layout.rs Adds project-spec paths, .claude/settings.json path, and Layout::discover_from with tests.
crates/ark-core/src/io/mod.rs Adds git module and exports settings-hook helpers/constants.
crates/ark-core/src/io/git.rs Introduces the single sanctioned git subprocess helper with tests.
crates/ark-core/src/io/fs.rs Adds settings-hook CRUD helpers (update/remove/read_settings_hook) and canonical hook entry builder + tests.
crates/ark-core/src/error.rs Adds Error::GitSpawn for subprocess spawn failures.
crates/ark-core/src/commands/upgrade.rs Re-applies SessionStart hook on every upgrade and adds upgrade idempotence/re-add tests.
crates/ark-core/src/commands/unload.rs Captures + removes Ark SessionStart hook entry into snapshot; reports hook entry count.
crates/ark-core/src/commands/remove.rs Removes the Ark SessionStart hook entry during ark remove.
crates/ark-core/src/commands/mod.rs Exposes context module and re-exports context-facing types.
crates/ark-core/src/commands/load.rs Restores settings hook entries from snapshot and ensures canonical hook for legacy snapshots; adds round-trip tests.
crates/ark-core/src/commands/init.rs Installs SessionStart hook on init and adds tests for hook install/re-add behavior.
crates/ark-core/src/commands/context/mod.rs Implements ark context entrypoint, JSON envelope wrapping for SessionStart, and enforcement test for Command::new locality.
crates/ark-core/src/commands/context/gather.rs Gathers git/tasks/specs/archive/current-task state; parses project/features spec indices.
crates/ark-core/src/commands/context/model.rs Defines the JSON/text data model and caps/constants for context output.
crates/ark-core/src/commands/context/projection.rs Projects full context into session/phase-scoped payloads and filters related feature specs.
crates/ark-core/src/commands/context/render.rs Text rendering for projections with locked section headings and omission rules.
crates/ark-core/src/commands/context/related_specs.rs Parses PRD [**Related Specs**] section into feature spec paths.
crates/ark-cli/src/main.rs Adds ark context subcommand + arg validation; adds target discovery logic (resolve_with_discovery).
README.md Documents ark context and updates lifecycle command list.
AGENTS.md Updates repository layout and workflow/lifecycle table to include context and hook behavior.
.claude/settings.json Adds repo’s Claude Code SessionStart hook invoking ark context --scope session --format json.
.claude/commands/ark/quick.md Syncs shipped slash command to use ark context (quick).
.claude/commands/ark/design.md Syncs shipped slash command to use ark context across phases (design).
.claude/commands/ark/archive.md Syncs shipped slash command to use ark context (archive).
.ark/workflow.md Syncs live workflow doc from template changes.
.ark/tasks/archive/2026-04/spec-extract-self-contained/task.toml Adds archived task artifact documenting spec-extract self-contained requirement.
.ark/tasks/archive/2026-04/spec-extract-self-contained/PRD.md Adds archived PRD artifact describing the self-contained ## Spec rule motivation.
.ark/tasks/archive/2026-04/ark-context/task.toml Adds archived ark-context task metadata.
.ark/tasks/archive/2026-04/ark-context/VERIFY.md Adds archived verify artifact documenting behavior and follow-ups.
.ark/tasks/archive/2026-04/ark-context/PRD.md Adds archived ark-context PRD describing goals/outcomes.
.ark/tasks/archive/2026-04/ark-context/02_REVIEW.md Adds archived review iteration 02 artifact.
.ark/tasks/archive/2026-04/ark-context/02_PLAN.md Adds archived plan iteration 02 artifact.
.ark/tasks/archive/2026-04/ark-context/01_REVIEW.md Adds archived review iteration 01 artifact.
.ark/tasks/archive/2026-04/ark-context/01_PLAN.md Adds archived plan iteration 01 artifact.
.ark/tasks/archive/2026-04/ark-context/00_REVIEW.md Adds archived review iteration 00 artifact.
.ark/specs/features/ark-upgrade/SPEC.md Extends upgrade spec C-8 to cover settings hook behavior and adds changelog entry.
.ark/specs/features/ark-context/SPEC.md Adds the extracted ark-context feature spec.
.ark/specs/features/ark-agent-namespace/SPEC.md Clarifies spec extract copies latest ## Spec verbatim and requires self-contained ## Spec.
.ark/specs/features/INDEX.md Registers ark-context in the managed features index.
.ark/.installed.json Bumps installed version to 0.1.2 and updates hashes for changed files.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +34 to +39
let header_pos = text.find(SECTION_HEADER)?;
// Skip past the header line itself (everything up to and including
// the first newline after it).
let body_start = match text[header_pos..].find('\n') {
Some(nl) => header_pos + nl + 1,
None => return Some(""),
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

locate_section uses text.find(SECTION_HEADER) which will match [**Related Specs**] anywhere in the document (including mid-line), but the contract in the module doc says “Locate the line starting with [**Related Specs**]”. This can cause false positives if the token appears in prose. Consider scanning line-by-line for a header line whose trimmed content equals SECTION_HEADER (or starts with it) before extracting the section body.

Suggested change
let header_pos = text.find(SECTION_HEADER)?;
// Skip past the header line itself (everything up to and including
// the first newline after it).
let body_start = match text[header_pos..].find('\n') {
Some(nl) => header_pos + nl + 1,
None => return Some(""),
let mut offset = 0usize;
let mut body_start = None;
for line in text.split_inclusive('\n') {
let trimmed = line.trim_start();
if trimmed.starts_with(SECTION_HEADER) {
body_start = Some(offset + line.len());
break;
}
offset += line.len();
}
let body_start = match body_start {
Some(start) => start,
None => {
let trimmed = text.trim_start();
if trimmed.starts_with(SECTION_HEADER) && !text.contains('\n') {
return Some("");
}
return None;
}

Copilot uses AI. Check for mistakes.
Comment thread crates/ark-core/src/state/snapshot.rs Outdated
Comment on lines +30 to +32
/// Per-entry hook bodies captured by `unload`, restored by `load` via
/// `update_settings_hook`. New in v0.1.3 — older `.ark.db` files lack
/// this key and deserialize to `vec![]`. Per ark-context C-27.
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The doc comment says hook_bodies is “New in v0.1.3”, but this PR bumps the workspace/package version to 0.1.2 and uses that version in snapshot fixtures/tests. Please correct the referenced version (or rephrase without pinning a specific version) to avoid confusion when reading older snapshots.

Copilot uses AI. Check for mistakes.
Comment thread crates/ark-core/src/commands/unload.rs Outdated
Comment on lines +87 to +90
// 3. Capture + remove the Ark-owned SessionStart hook entry. Per
// ark-context C-18: only the Ark entry is captured; user siblings are
// user-owned content outside Ark's owned dirs and do not survive a
// round-trip.
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment claims user sibling hooks in .claude/settings.json “do not survive a round-trip”, but the actual implementation only removes the Ark entry (via remove_settings_hook) and leaves the rest of the file intact. That means sibling hooks can persist on disk across unload → load (and your load.rs tests explicitly assert this). Please update this comment to reflect the real behavior (only Ark entry is captured into the snapshot; siblings are not captured but are preserved on disk because unload is a surgical edit).

Copilot uses AI. Check for mistakes.
Comment thread crates/ark-core/src/io/mod.rs Outdated
Comment on lines +7 to +12
//! - [`git`] — the only sanctioned `Command::new("git")` site (per
//! ark-context C-26). Kept module-private; callers route through
//! [`crate::commands::context::gather`] rather than the raw helper.

pub mod fs;
pub mod git;
Copy link

Copilot AI Apr 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

io::git is documented here as “Kept module-private”, but it’s declared as pub mod git;, making it public to external crates. Either make it pub(crate) mod git (or mod git + re-export only what’s needed internally) to match the stated constraint, or update the module docs to reflect that it’s a public module.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 41b469cdef

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/ark-cli/src/main.rs Outdated
let root = if a.force {
a.target.resolve()
} else {
a.target.resolve_with_discovery()?
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Resolve ark load target without requiring .ark

ark load now calls resolve_with_discovery() in the non---force path, which only succeeds when an ancestor already contains .ark/. That prevents the documented restore/scaffold flow in common cases: a fresh project (no .ark yet) and a project right after ark unload (only .ark.db exists). In both scenarios the CLI exits with NotLoaded before load() can restore from snapshot or fall back to init.

Useful? React with 👍 / 👎.

Comment thread crates/ark-cli/src/main.rs Outdated
}
Self::Remove(a) => {
let root = a.resolve();
let root = a.resolve_with_discovery()?;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Let ark remove run without .ark discovery

ark remove now resolves its root via resolve_with_discovery(), so it refuses to run unless .ark/ exists in the cwd ancestry. This breaks the unconditional cleanup behavior (notably deleting .ark.db after ark unload), because unload removes .ark/ first and remove now fails before reaching remove()'s no-op-safe cleanup logic.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

♻️ Duplicate comments (1)
templates/claude/commands/ark/quick.md (1)

20-34: ⚠️ Potential issue | 🟡 Minor

Same step numbering inconsistency as noted in .claude/commands/ark/quick.md.

This is the source template — applying the fix here will propagate to the generated file. Steps jump from 1 to 3.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@templates/claude/commands/ark/quick.md` around lines 20 - 34, The template
has a step-numbering gap: after the heading "### 1. Pull project context" the
next heading is "### 3. Create the task"; update the template so steps are
sequential (either renumber "### 3. Create the task" to "### 2. Create the task"
or insert the missing step 2 content if intended), ensuring the headings in
templates/claude/commands/ark/quick.md follow 1, 2, 3 order to propagate the
correct numbering to generated files.
🧹 Nitpick comments (4)
crates/ark-core/src/commands/remove.rs (1)

106-115: Consider adding assertion for removed_hook_entry in the test.

The test remove_after_init_wipes_everything verifies most summary fields but doesn't assert on the new removed_hook_entry flag. Since init now writes the SessionStart hook, this test should also verify that the hook was removed.

💡 Proposed addition
         let summary = remove(RemoveOptions::new(tmp.path())).unwrap();
         assert!(summary.removed_ark_dir);
         assert!(summary.removed_claude_commands);
         assert_eq!(summary.blocks_removed, 1);
+        assert!(summary.removed_hook_entry);
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/ark-core/src/commands/remove.rs` around lines 106 - 115, The test
remove_after_init_wipes_everything must assert the new removed_hook_entry flag;
after calling remove(RemoveOptions::new(tmp.path())) and obtaining summary, add
an assertion checking summary.removed_hook_entry (e.g.,
assert!(summary.removed_hook_entry)) so the test verifies that the SessionStart
hook written by init(...) was removed; update the test function
remove_after_init_wipes_everything accordingly, referencing the existing init,
remove, RemoveOptions, and the summary variable.
crates/ark-core/src/io/mod.rs (1)

7-12: Make io::git private to enforce the intended API boundary.

The docstring states this module is "module-private" with callers routing through [crate::commands::context::gather], but pub mod git; exposes it as part of ark-core's public API. No code outside crates/ark-core uses ark_core::io::git, and internal callers (like gather.rs) work identically with mod git;. Change to mod git; to match the documented design and keep the git-spawning boundary tight.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/ark-core/src/io/mod.rs` around lines 7 - 12, The io::git module is
publicly exported but should be module-private per the docstring; change the
declaration from a public export to a private module by replacing the `pub mod
git;` export with `mod git;` in the io module, leaving `pub mod fs;` as-is, and
verify internal callers such as the gather implementation
(`crate::commands::context::gather` / gather.rs) continue to reference
`crate::io::git` (they can access it because it's in the same crate) or import
the private module directly; no external API should reference
`ark_core::io::git`.
crates/ark-core/src/io/fs.rs (1)

293-300: Consider using actual path in error context instead of placeholder.

The error is constructed with a placeholder path "<settings.json>" which loses the actual file path context. Since upsert_session_start_entry doesn't receive the path, consider either:

  1. Passing the path as a parameter
  2. Using a more specific error variant

This is minor since the error message itself is descriptive.

💡 Alternative: propagate path from caller
 fn upsert_session_start_entry(
     root: &mut serde_json::Value,
     entry: serde_json::Value,
+    path: &Path,
 ) -> Result<()> {
     let identity = identity_command(&entry).ok_or_else(|| {
         Error::io(
-            std::path::PathBuf::from("<settings.json>"),
+            path.to_path_buf(),
             std::io::Error::other(
                 "hook entry missing inner `hooks[*].command` (or top-level `command`)",
             ),
         )
     })?;

Then update the caller at line 197.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/ark-core/src/io/fs.rs` around lines 293 - 300, The error uses a
placeholder PathBuf "<settings.json>" when identity_command(&entry) returns None
inside upsert_session_start_entry; change this by either adding a path parameter
to upsert_session_start_entry (propagate the real file path from its caller) and
use that PathBuf in the Error::io construction, or switch to a more specific
error variant that does not require a PathBuf (e.g., a custom Error::config or
Error::invalid_entry) and include the descriptive message; update the caller
that invokes upsert_session_start_entry to pass the real path if you choose the
first option so the error includes accurate file context.
crates/ark-core/src/commands/context/projection.rs (1)

221-243: Test plan_phase_filters_features_to_related passes due to exact-match coincidence.

The test uses related = ["specs/features/foo/SPEC.md"] and features with paths like .ark/specs/features/foo/SPEC.md. After normalization, both become specs/features/foo/SPEC.md, so r.ends_with(&f_norm) succeeds only because they're identical strings.

Consider adding a test case where related_specs contains a path like "./specs/features/foo/SPEC.md" or ".ark/specs/features/foo/SPEC.md" to exercise the normalization + matching logic more thoroughly and catch the predicate direction issue.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/ark-core/src/commands/context/projection.rs` around lines 221 - 243,
The test plan_phase_filters_features_to_related currently only exercises
exact-match normalization; add an additional assertion case that supplies a
CurrentTask.related_specs entry using a non-exact path (e.g.
"./specs/features/foo/SPEC.md" or ".ark/specs/features/foo/SPEC.md") when
building ctx via ctx_with and calling project(ctx,
Scope::Phase(PhaseFilter::Plan)), then verify the resulting pj.specs.features
still contains only "foo" and other projections (archive/tasks) remain None;
this ensures the normalization + matching logic used by project (and the
predicate comparing related_specs to feature paths) is exercised for
prefixed/relative paths as well as exact ones.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.ark/tasks/archive/2026-04/ark-context/02_PLAN.md:
- Around line 78-85: The archived "## Spec" section currently uses "carried
forward from `01_PLAN.md`" which leaves the spec non-self-contained and will
break `ark agent spec extract`; fix by replacing that delta form in the "##
Spec" block with the full, inlined final spec text (i.e., expand G-1..G-12,
NG-1..NG-11, and C-1..C-29 as they should appear) or, if you intend this file to
be historical only, add a clear machine-readable marker and human notice at the
top of the file (e.g., "ARCHIVED: DO NOT USE FOR EXTRACTION" and a brief note
that this file is historical evidence only) so tools like `ark agent spec
extract` will skip it. Ensure the change targets the "## Spec" section that
currently contains the carried-forward phrase and update any references to
C-27/C-28/C-29 to match the inlined content or archival marker.

In @.ark/tasks/archive/2026-04/ark-context/VERIFY.md:
- Line 26: The summary sentence that reads "All 12 Goals (G-1 through G-12) and
26 Constraints (C-1 through C-26 + C-27 through C-29)" is internally
inconsistent with the enumerated constraints C-1..C-29; update that sentence to
say "29 Constraints" (or rephrase to "Constraints C-1 through C-29") so the
count matches the enumeration, and make the same change anywhere else in
VERIFY.md where the old "26 Constraints" appears.

In @.claude/commands/ark/quick.md:
- Around line 20-34: The numbered headings in .claude/commands/ark/quick.md skip
from "### 1. Pull project context" to "### 3. Create the task"; update the
heading "### 3. Create the task" to "### 2. Create the task" and incrementally
renumber all subsequent step headings (e.g., 4→3, 5→4, etc.) so numbering is
sequential; also search the same file for any in-text references to those step
numbers and update them to match the new numbering (look for the heading text
"Create the task" and other subsequent step titles to locate all occurrences).

In `@AGENTS.md`:
- Around line 33-53: The Markdown module-map fenced block starting with triple
backticks lacks a language tag which triggers MD040; update the opening fence to
include a language identifier (e.g., change the block that contains the module
tree under the module-map comment to start with ```text) so the code block is
explicitly marked as plain text and the MD040 warning is resolved.

In `@crates/ark-core/src/commands/context/mod.rs`:
- Around line 89-94: Replace the call to layout.ark_dir().exists() with
try_exists() and map any I/O error into your crate error type instead of
treating it as “not loaded”; call layout.ark_dir().try_exists(), and on Err(e)
return Err(Error::io(opts.project_root.clone(), e)) (or equivalent using the
same path value), while only returning Err(Error::NotLoaded { path:
opts.project_root }) when try_exists() returns Ok(false); keep references to
Layout::new, layout.ark_dir(), Error::NotLoaded and Error::io to locate and
implement the change.
- Around line 99-123: Change the two serde_json::to_string_pretty calls to
return errors instead of panicking: add (or reuse) an Error variant for
serialization failures, change wrap_session_start_envelope(payload: &str) ->
String to wrap_session_start_envelope(payload: &str) -> Result<String, Error>
and use serde_json::to_string_pretty(...)? (instead of .expect), and change the
caller that builds ContextSummary::Json to propagate the error (use ? when
serializing projected and when calling wrap_session_start_envelope) so context()
(or the enclosing function) returns Result<ContextSummary, Error>; update any
downstream call sites accordingly.

In `@crates/ark-core/src/commands/context/projection.rs`:
- Around line 144-148: The filter predicate is inverted: inside the closure that
computes f_norm (from f.path) and uses normalized_related.iter().any(...),
change the any(...) test so it checks whether each normalized related path ends
with the feature normalized path (i.e., r.ends_with(&f_norm)) instead of
checking f_norm.ends_with(r); locate the closure where f_path_str, f_norm, and
normalized_related are used (the filter on the iterator over features) and swap
the arguments to ends_with to match the docstring/spec (or update the docstring
if the opposite behavior is intended).

In `@crates/ark-core/src/commands/context/related_specs.rs`:
- Around line 33-52: locate_section() currently uses text.find(SECTION_HEADER)
which treats inline mentions as headers; change the logic to scan the input
line-by-line to find an actual header line whose trimmed contents equal
SECTION_HEADER, then set body_start to the first byte after that header line
(same newline-skip behavior) and proceed as before; update the function using
the existing helpers (is_section_header_line) and keep the rest of the loop that
detects the next section intact so extract() and current_task.related_specs get
the correct block.

In `@crates/ark-core/src/commands/unload.rs`:
- Around line 87-102: Update the misleading comment that says "user siblings...
do not survive a round-trip" to correctly state that user siblings are not
captured into the snapshot but remain on disk and therefore survive the
round-trip; reference the relevant symbols to clarify behavior: the
capture/remove sequence in unload.rs (layout.claude_settings(),
read_settings_hook(), SnapshotHookBody, remove_settings_hook(),
ARK_CONTEXT_HOOK_COMMAND) only captures/removes Ark's entry for the snapshot
while user siblings remain and are preserved when load (see test
roundtrip_preserves_user_pretooluse_sibling in load.rs) re-applies Ark's entry
without deleting other sibling entries.

In `@templates/claude/commands/ark/archive.md`:
- Around line 30-35: The pre-archive precheck queries ark context without
scoping to the resolved slug and assumes current_task and
current_task.summary.phase exist; update the archive precheck to call the
context command bound to the slug resolved in Step 1 (so the context is for the
target slug), then defensively check that current_task exists and that
current_task.summary and summary.phase are present before reading phase; if
absent, halt with a clear error. Also ensure phase validation logic uses the
correct expected phase per tier (standard/deep => "verify", quick => "execute"),
and only attempt to read VERIFY.md and inspect the "Verdict" line when the
artifact exists; if Verdict is "Rejected" halt and report.

---

Duplicate comments:
In `@templates/claude/commands/ark/quick.md`:
- Around line 20-34: The template has a step-numbering gap: after the heading
"### 1. Pull project context" the next heading is "### 3. Create the task";
update the template so steps are sequential (either renumber "### 3. Create the
task" to "### 2. Create the task" or insert the missing step 2 content if
intended), ensuring the headings in templates/claude/commands/ark/quick.md
follow 1, 2, 3 order to propagate the correct numbering to generated files.

---

Nitpick comments:
In `@crates/ark-core/src/commands/context/projection.rs`:
- Around line 221-243: The test plan_phase_filters_features_to_related currently
only exercises exact-match normalization; add an additional assertion case that
supplies a CurrentTask.related_specs entry using a non-exact path (e.g.
"./specs/features/foo/SPEC.md" or ".ark/specs/features/foo/SPEC.md") when
building ctx via ctx_with and calling project(ctx,
Scope::Phase(PhaseFilter::Plan)), then verify the resulting pj.specs.features
still contains only "foo" and other projections (archive/tasks) remain None;
this ensures the normalization + matching logic used by project (and the
predicate comparing related_specs to feature paths) is exercised for
prefixed/relative paths as well as exact ones.

In `@crates/ark-core/src/commands/remove.rs`:
- Around line 106-115: The test remove_after_init_wipes_everything must assert
the new removed_hook_entry flag; after calling
remove(RemoveOptions::new(tmp.path())) and obtaining summary, add an assertion
checking summary.removed_hook_entry (e.g., assert!(summary.removed_hook_entry))
so the test verifies that the SessionStart hook written by init(...) was
removed; update the test function remove_after_init_wipes_everything
accordingly, referencing the existing init, remove, RemoveOptions, and the
summary variable.

In `@crates/ark-core/src/io/fs.rs`:
- Around line 293-300: The error uses a placeholder PathBuf "<settings.json>"
when identity_command(&entry) returns None inside upsert_session_start_entry;
change this by either adding a path parameter to upsert_session_start_entry
(propagate the real file path from its caller) and use that PathBuf in the
Error::io construction, or switch to a more specific error variant that does not
require a PathBuf (e.g., a custom Error::config or Error::invalid_entry) and
include the descriptive message; update the caller that invokes
upsert_session_start_entry to pass the real path if you choose the first option
so the error includes accurate file context.

In `@crates/ark-core/src/io/mod.rs`:
- Around line 7-12: The io::git module is publicly exported but should be
module-private per the docstring; change the declaration from a public export to
a private module by replacing the `pub mod git;` export with `mod git;` in the
io module, leaving `pub mod fs;` as-is, and verify internal callers such as the
gather implementation (`crate::commands::context::gather` / gather.rs) continue
to reference `crate::io::git` (they can access it because it's in the same
crate) or import the private module directly; no external API should reference
`ark_core::io::git`.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 34f65fed-cdcb-4dee-ba24-ae744e408c77

📥 Commits

Reviewing files that changed from the base of the PR and between d204466 and 41b469c.

📒 Files selected for processing (49)
  • .ark/.installed.json
  • .ark/specs/features/INDEX.md
  • .ark/specs/features/ark-agent-namespace/SPEC.md
  • .ark/specs/features/ark-context/SPEC.md
  • .ark/specs/features/ark-upgrade/SPEC.md
  • .ark/tasks/archive/2026-04/ark-context/00_PLAN.md
  • .ark/tasks/archive/2026-04/ark-context/00_REVIEW.md
  • .ark/tasks/archive/2026-04/ark-context/01_PLAN.md
  • .ark/tasks/archive/2026-04/ark-context/01_REVIEW.md
  • .ark/tasks/archive/2026-04/ark-context/02_PLAN.md
  • .ark/tasks/archive/2026-04/ark-context/02_REVIEW.md
  • .ark/tasks/archive/2026-04/ark-context/PRD.md
  • .ark/tasks/archive/2026-04/ark-context/VERIFY.md
  • .ark/tasks/archive/2026-04/ark-context/task.toml
  • .ark/tasks/archive/2026-04/spec-extract-self-contained/PRD.md
  • .ark/tasks/archive/2026-04/spec-extract-self-contained/task.toml
  • .ark/workflow.md
  • .claude/commands/ark/archive.md
  • .claude/commands/ark/design.md
  • .claude/commands/ark/quick.md
  • .claude/settings.json
  • AGENTS.md
  • README.md
  • crates/ark-cli/src/main.rs
  • crates/ark-core/src/commands/context/gather.rs
  • crates/ark-core/src/commands/context/mod.rs
  • crates/ark-core/src/commands/context/model.rs
  • crates/ark-core/src/commands/context/projection.rs
  • crates/ark-core/src/commands/context/related_specs.rs
  • crates/ark-core/src/commands/context/render.rs
  • crates/ark-core/src/commands/init.rs
  • crates/ark-core/src/commands/load.rs
  • crates/ark-core/src/commands/mod.rs
  • crates/ark-core/src/commands/remove.rs
  • crates/ark-core/src/commands/unload.rs
  • crates/ark-core/src/commands/upgrade.rs
  • crates/ark-core/src/error.rs
  • crates/ark-core/src/io/fs.rs
  • crates/ark-core/src/io/git.rs
  • crates/ark-core/src/io/mod.rs
  • crates/ark-core/src/layout.rs
  • crates/ark-core/src/lib.rs
  • crates/ark-core/src/state/mod.rs
  • crates/ark-core/src/state/snapshot.rs
  • docs/ROADMAP.md
  • templates/ark/workflow.md
  • templates/claude/commands/ark/archive.md
  • templates/claude/commands/ark/design.md
  • templates/claude/commands/ark/quick.md

Comment on lines +78 to +85
## Spec

*Carried forward from `01_PLAN.md` §Spec, with the following deltas:*

- **G-1 through G-12:** unchanged.
- **NG-1 through NG-11:** unchanged.
- *Constraints:* C-1 through C-26 are inherited from 01_PLAN (C-3, C-11, C-21, C-26 with their 01-revised wording, plus the C-21 narrowing in §Changed above). C-27, C-28, C-29 added in this iteration.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

The archived final ## Spec is still non-self-contained.

Lines 78-85 still use the “carried forward from 01_PLAN.md” delta form. That means rerunning ark agent spec extract on this task would reproduce the same unusable SPEC shape this PR is trying to prevent. Please either inline the full final spec here or clearly mark this file as pre-fix historical evidence that must not be used as extraction input.

🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 78-78: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.ark/tasks/archive/2026-04/ark-context/02_PLAN.md around lines 78 - 85, The
archived "## Spec" section currently uses "carried forward from `01_PLAN.md`"
which leaves the spec non-self-contained and will break `ark agent spec
extract`; fix by replacing that delta form in the "## Spec" block with the full,
inlined final spec text (i.e., expand G-1..G-12, NG-1..NG-11, and C-1..C-29 as
they should appear) or, if you intend this file to be historical only, add a
clear machine-readable marker and human notice at the top of the file (e.g.,
"ARCHIVED: DO NOT USE FOR EXTRACTION" and a brief note that this file is
historical evidence only) so tools like `ark agent spec extract` will skip it.
Ensure the change targets the "## Spec" section that currently contains the
carried-forward phrase and update any references to C-27/C-28/C-29 to match the
inlined content or archival marker.


## Summary

The implementation delivers what `02_PLAN.md` (carrying forward `01_PLAN.md`) promised. All 12 Goals (G-1 through G-12) and 26 Constraints (C-1 through C-26 + C-27 through C-29) are satisfied as specified, with one deliberate scope adjustment (V-IT-15 negative case became a positive case — see V-001 below). 191 unit/integration tests pass; the CLI smoke test (init → context → unload → load → remove) round-trips cleanly with the SessionStart hook captured and restored. Build, fmt, clippy, and test gates are all green. Three non-blocking follow-ups documented below: V-IT-15 plan/reality mismatch (better behavior than planned, plan should be amended), Phase 4 documentation is partial (READMAP marker added but the SPEC body amendment is in the live SPEC only — `templates/ark/specs/...` is unchanged because that file isn't templated), and the slash-command G-10 row remained "manual review" without a per-file mechanical check (carried forward from 01's Unresolved §1, no regression).
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix the constraint count in the summary.

Line 26 says “26 Constraints” but then enumerates C-1 through C-29. Please either update the count to 29 or simplify the sentence so the archive summary stays internally consistent.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.ark/tasks/archive/2026-04/ark-context/VERIFY.md at line 26, The summary
sentence that reads "All 12 Goals (G-1 through G-12) and 26 Constraints (C-1
through C-26 + C-27 through C-29)" is internally inconsistent with the
enumerated constraints C-1..C-29; update that sentence to say "29 Constraints"
(or rephrase to "Constraints C-1 through C-29") so the count matches the
enumeration, and make the same change anywhere else in VERIFY.md where the old
"26 Constraints" appears.

Comment thread .claude/commands/ark/quick.md Outdated
Comment thread AGENTS.md
Comment on lines +33 to +53
├── layout.rs # Layout + project-root constants + discover_from
├── templates.rs # include_dir!() trees + walker
├── io/
│ ├── path_ext.rs # PathExt trait wrapping std::fs
│ └── fs.rs # write_file, walk_files, managed-block ops
│ ├── fs.rs # write_file, walk_files, managed-block ops, settings-hook
│ └── git.rs # the only sanctioned Command::new("git") site
├── state/
│ ├── manifest.rs # .ark/.installed.json
│ └── snapshot.rs # .ark.db capture/restore
│ └── snapshot.rs # .ark.db capture/restore (incl. SnapshotHookBody)
└── commands/
├── init.rs # scaffold from templates
├── load.rs # restore from .ark.db OR scaffold
├── unload.rs # capture into .ark.db, remove live files
├── remove.rs # unconditional wipe
├── upgrade.rs # refresh embedded templates to current CLI version
├── context/ # `ark context` — read-only state snapshot
│ ├── model.rs # Context + sub-structs, schema=1
│ ├── gather.rs # one-pass collection (git + tasks + specs)
│ ├── projection.rs # Scope / PhaseFilter / project()
│ ├── render.rs # text-mode Display
│ └── related_specs.rs # PRD [**Related Specs**] parser
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add a language identifier to the module-map fenced block.

Line 29 opens a fenced code block without a language, which triggers MD040.

💡 Proposed fix
-```
+```text
 ark-core/src/
 ├── lib.rs              # public re-exports
 ...
         └── template.rs #   internal helper: extract embedded templates
</details>

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 markdownlint-cli2 (0.22.1)</summary>

[warning] 52-52: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

</details>

</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

Verify each finding against the current code and only fix it if needed.

In @AGENTS.md around lines 33 - 53, The Markdown module-map fenced block
starting with triple backticks lacks a language tag which triggers MD040; update
the opening fence to include a language identifier (e.g., change the block that
contains the module tree under the module-map comment to start with ```text) so
the code block is explicitly marked as plain text and the MD040 warning is
resolved.


</details>

<!-- fingerprinting:phantom:poseidon:hawk:9b3bb1b3-2d1d-47dc-83b0-4e8d9ea33fc3 -->

<!-- d98c2f50 -->

<!-- This is an auto-generated comment by CodeRabbit -->

Comment thread crates/ark-core/src/commands/context/mod.rs
Comment on lines +99 to +123
let raw =
serde_json::to_string_pretty(&projected).expect("ProjectedContext serializes");
let body = if matches!(opts.scope, Scope::Session) {
wrap_session_start_envelope(&raw)
} else {
raw
};
Ok(ContextSummary::Json(format!("{body}\n")))
}
Format::Text => Ok(ContextSummary::Text(Box::new(projected))),
}
}

/// Wrap a JSON payload in Claude Code's SessionStart envelope. The payload
/// is embedded as a stringified value of `additionalContext` because the
/// hook contract requires that field to be a string.
fn wrap_session_start_envelope(payload: &str) -> String {
serde_json::to_string_pretty(&serde_json::json!({
"hookSpecificOutput": {
"hookEventName": "SessionStart",
"additionalContext": payload,
}
}))
.expect("envelope serializes")
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify whether serde_json errors can be propagated with `?` in ark-core error type.
fd -i 'error*.rs' crates/ark-core/src
rg -n "enum Error|type Result|From<serde_json::Error>|serde_json::Error" crates/ark-core/src

Repository: Anekoique/Ark

Length of output: 285


🏁 Script executed:

cat -n crates/ark-core/src/error.rs

Repository: Anekoique/Ark

Length of output: 3768


🏁 Script executed:

sed -n '85,130p' crates/ark-core/src/commands/context/mod.rs

Repository: Anekoique/Ark

Length of output: 1685


🏁 Script executed:

rg -B 3 "fn wrap_session_start_envelope" crates/ark-core/src/commands/context/mod.rs

Repository: Anekoique/Ark

Length of output: 319


🏁 Script executed:

rg -n "serde_json::to_string" crates/ark-core/src --context=2

Repository: Anekoique/Ark

Length of output: 4771


🏁 Script executed:

rg -n "ManifestCorrupt|SerializationError|JsonError" crates/ark-core/src

Repository: Anekoique/Ark

Length of output: 224


Propagate serde_json serialization errors instead of panicking.

Lines 100 and 116 use expect() for fallible operations that can realistically fail (out of memory, I/O constraints). Per the coding guideline, "Never use expect() outside tests; reserve expect("…") for documented invariants only," these should propagate as Result errors.

Additionally, this pattern appears elsewhere (state/snapshot.rs:128, state/manifest.rs:105, io/fs.rs:368), suggesting a systematic issue. To fix this, the Error enum needs a variant for serialization failures (or reuse an existing variant), then update both wrap_session_start_envelope() to return Result<String> and propagate errors in context() with the ? operator.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/ark-core/src/commands/context/mod.rs` around lines 99 - 123, Change
the two serde_json::to_string_pretty calls to return errors instead of
panicking: add (or reuse) an Error variant for serialization failures, change
wrap_session_start_envelope(payload: &str) -> String to
wrap_session_start_envelope(payload: &str) -> Result<String, Error> and use
serde_json::to_string_pretty(...)? (instead of .expect), and change the caller
that builds ContextSummary::Json to propagate the error (use ? when serializing
projected and when calling wrap_session_start_envelope) so context() (or the
enclosing function) returns Result<ContextSummary, Error>; update any downstream
call sites accordingly.

Comment thread crates/ark-core/src/commands/context/projection.rs
Comment thread crates/ark-core/src/commands/context/related_specs.rs
Comment thread crates/ark-core/src/commands/unload.rs
Comment thread templates/claude/commands/ark/archive.md Outdated
@claude
Copy link
Copy Markdown

claude Bot commented Apr 27, 2026

Claude finished @Anekoique's task in 3m 42s —— View job


PR Review — feat(cli): add ark context command

  • Read AGENTS.md conventions
  • Read core Rust source files
  • Analyze error handling, filesystem safety, correctness
  • Post findings

Good overall implementation. Several issues from prior reviews were addressed (pub(crate) mod git, try_exists() in context(), load/remove not using discovery, locate_section line-by-line header scan, unload comment). Four items remain unfixed or new.


1. {:?} debug format for Tier/Phase in text output — render.rs:92-93, 131

Tier and Phase have no Display impl. render.rs uses {:?}, which emits PascalCase via the derived Debug:

// render.rs:92-93
writeln!(f, "tier: {:?}", ct.summary.tier)?;   // → "tier: Deep"
writeln!(f, "phase: {:?}", ct.summary.phase)?;  // → "phase: Plan"

// render.rs:131
writeln!(f, "  {} [{:?} {:?} iter={}] {}", t.slug, t.tier, t.phase, ...)?;

JSON output uses #[serde(rename_all = "lowercase")], emitting "deep", "plan". Text and JSON are inconsistent for the same fields.

Fix: add Display impls to Tier and Phase in commands/agent/state.rs that match the serde lowercase form, then change {:?}{} in render.rs. Fix this →


2. is_section_header_line terminates section body on any inline bold link — related_specs.rs:65

The header detection in locate_section was correctly fixed to scan line-by-line. The body-end detector still uses:

fn is_section_header_line(line: &str) -> bool {
    if !line.starts_with("[**") { return false; }
    line.contains("**]")   // ← too broad
}

Any PRD body line beginning with [** and containing **] — e.g. [**Note**]: see below or a list item starting with [**foo**] — will prematurely end the section, silently dropping any spec paths that follow. Fix: require the trimmed line ends with **]:

fn is_section_header_line(line: &str) -> bool {
    let t = line.trim_end_matches(['\r', '\n']).trim_end();
    t.starts_with("[**") && t.ends_with("**]")
}

Fix this →


3. gather_git hard-errors when git is not in PATH — gather.rs:46, 61, 70

The non-zero exit code path soft-fails correctly. But if git is absent from PATH, run_git returns Err(Error::GitSpawn), and three call sites propagate that error with ?:

let branch = git::run_git(&["rev-parse", "--abbrev-ref", "HEAD"], project_root)?;
// ...
let status = git::run_git(&["status", "--porcelain"], project_root)?;
let log = git::run_git(&["log", "--oneline", ...], project_root)?;

context is read-only; a missing git binary is not a user error. The first call already has the is_success() check that returns GitState::default() — the spawn error could be caught at the same level:

let branch = match git::run_git(&["rev-parse", "--abbrev-ref", "HEAD"], project_root) {
    Ok(o) if !o.is_success() => return Ok(GitState::default()),
    Err(Error::GitSpawn { .. }) => return Ok(GitState::default()),
    Ok(o) => o,
    Err(e) => return Err(e),
};

Fix this →


Minor notes (no action required)

exists() in gather_tasks and gather_archive (gather.rs:105, 148): Same pattern fixed in context/mod.rstasks_dir.exists() collapses EACCES into false. Low severity since these are internal dirs, but try_exists() is consistent with the fix already applied to context().

expect() in context/mod.rs (lines 101, 123): ProjectedContext and the static JSON envelope always serialize, so these qualify as documented invariants. The convention is met; this is a style note only.

Path traversal via .current slug (gather.rs:318): layout.task_dir(slug) joins an unsanitized slug. The subsequent is_dir() check limits blast radius to a benign Ok(None). layout.resolve_safe() would be cleaner but is not urgent.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a0acbe3faa

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

// targets the Ark entry by command, leaving the rest of the file
// intact for the next `load` to find.
let settings = layout.claude_settings();
if let Some(entry) = read_settings_hook(&settings, ARK_CONTEXT_HOOK_COMMAND)? {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Parse settings before mutating files in unload

If .claude/settings.json is malformed JSON, read_settings_hook(...)? returns an error here and unload exits before snapshot.write(). By this point step 2 has already removed managed blocks (remove_managed_block), so the command can fail after destructive edits without producing a restorable .ark.db, which breaks unload’s preservation guarantee and can drop user-managed block content.

Useful? React with 👍 / 👎.

// 4. Ark-owned hook entry in .claude/settings.json (sibling user
// entries are preserved).
summary.removed_hook_entry =
remove_settings_hook(layout.claude_settings(), ARK_CONTEXT_HOOK_COMMAND)?;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep remove unconditional when settings JSON is invalid

ark remove is documented as unconditional cleanup, but this new call hard-fails on malformed .claude/settings.json because JSON parse errors bubble through ?. In that case the command returns failure after already deleting .ark/ and .ark.db, leaving only a stale hook entry and forcing users to hand-fix settings before remove can succeed.

Useful? React with 👍 / 👎.

@Anekoique Anekoique merged commit 71f09b1 into main Apr 27, 2026
3 of 4 checks passed
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (3)
.ark/tasks/archive/2026-04/ark-context/02_PLAN.md (1)

78-85: ⚠️ Potential issue | 🟠 Major

## Spec is still non-self-contained and remains extraction-hostile.

This section is still delta-style (“carried forward”) rather than a standalone spec, so extracting from this plan cannot produce a complete artifact. Please either inline the full final ## Spec content here or add a clear machine-readable archival marker so extract tooling skips this file.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.ark/tasks/archive/2026-04/ark-context/02_PLAN.md around lines 78 - 85, The
"## Spec" section is currently delta-style and not self-contained; update the
section labeled "## Spec" so it is extraction-friendly by either inlining the
full, final spec text instead of "Carried forward ..." deltas or adding a clear
machine‑readable archival marker (e.g., JSON/YAML frontmatter or a skip token)
immediately under the "## Spec" header that signals extract tools to ignore this
file; ensure the change is applied to the "## Spec" block and that any
references to G-*, NG-*, or C-* constraints remain intact or are fully expanded
when you inline.
crates/ark-core/src/commands/context/gather.rs (1)

104-106: ⚠️ Potential issue | 🟠 Major

Don't collapse unreadable task directories into an "empty" snapshot.

Both guards use exists(), which turns I/O failures into false. If .ark/tasks/ or .ark/tasks/archive/ is unreadable, ark context will silently report no active/archive tasks instead of surfacing the permission problem. Use try_exists() and map failures with Error::io(...).

Suggested fix
 fn gather_tasks(layout: &Layout) -> Result<TasksState> {
     let tasks_dir = layout.tasks_dir();
-    if !tasks_dir.exists() {
+    if !tasks_dir
+        .try_exists()
+        .map_err(|source| crate::error::Error::io(&tasks_dir, source))?
+    {
         return Ok(TasksState::default());
     }
@@
 fn gather_archive(layout: &Layout) -> Result<ArchiveState> {
     let archive_root = layout.tasks_archive_dir();
-    if !archive_root.exists() {
+    if !archive_root
+        .try_exists()
+        .map_err(|source| crate::error::Error::io(&archive_root, source))?
+    {
         return Ok(ArchiveState::default());
     }

As per coding guidelines: "**/*.rs: Every fallible operation in Rust code must return crate::error::Result<T>; wrap std::io::Error via Error::io(path, source) and never use unwrap() outside tests; reserve expect(\"…\") for documented invariants only`."

Also applies to: 147-149

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/ark-core/src/commands/context/gather.rs` around lines 104 - 106, The
code currently treats unreadable task directories as non-existent by calling
exists(); change to use try_exists() on layout.tasks_dir() and
layout.tasks_archive() and map any std::io::Error into the crate error via
Error::io(path, source) so the function returns Err(...) instead of silently
returning an empty TasksState; update the early-return branches that reference
tasks_dir and archive_dir (variables from gather.rs) to propagate IO errors
using the crate::error::Result<T> convention rather than collapsing failures
into false.
crates/ark-core/src/commands/context/mod.rs (1)

99-107: ⚠️ Potential issue | 🟠 Major

Propagate JSON serialization failures instead of panicking.

Both serde_json::to_string_pretty calls still use expect() in production code. Return a Result<String> from wrap_session_start_envelope and bubble serialization failures through context() so ark context fails with a regular Error instead of aborting the process.

As per coding guidelines: "**/*.rs: Every fallible operation in Rust code must return crate::error::Result<T>; wrap std::io::Error via Error::io(path, source) and never use unwrap() outside tests; reserve expect(\"…\") for documented invariants only`."

Also applies to: 116-123

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@crates/ark-core/src/commands/context/mod.rs` around lines 99 - 107, The code
currently calls serde_json::to_string_pretty(...).expect(...) which will abort
on serialization failure; change both places (the Format::Json branch and the
other JSON call at 116-123) to propagate errors by returning
crate::error::Result<String> from wrap_session_start_envelope and by replacing
expect(...) with ? on serde_json::to_string_pretty(&projected) (or map_err to
crate::Error if needed), then pass the resulting String into
wrap_session_start_envelope? and propagate the Result up through context() so
context() returns crate::error::Result<ContextSummary> instead of panicking;
update function signatures (wrap_session_start_envelope, context and any
callers) to use the crate::error::Result type and convert serde_json::Error into
the crate error per project conventions.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.ark/specs/features/ark-context/SPEC.md:
- Line 40: Several fenced code blocks in SPEC.md are missing language
identifiers which triggers markdownlint MD040; update each bare triple-backtick
fence used for plain text to include a language tag (e.g., change ``` to
```text) for the three occurrences referenced (the fence at the snippet around
line 40 and the other two occurrences around the noted locations) so the blocks
become ```text ... ``` and satisfy the linter.

In @.ark/tasks/archive/2026-04/ark-context/01_PLAN.md:
- Around line 27-28: The document's contract is inconsistent about whether
sibling hook entries are preserved byte-for-byte versus only structurally;
reconcile this by making the behavior explicit and aligning code and docs:
update the Snapshot struct description to state that Snapshot.hook_bodies only
captures Ark-owned entries (matched via ARK_CONTEXT_HOOK_COMMAND) and that
unload/load (see unload and load command implementations) will remove/reapply
only Ark-owned entries using update_settings_hook which may reformat the JSON,
so sibling/user-owned hook entries are preserved semantically but not guaranteed
byte-identical; then adjust the text around the settings-hook preservation
(including the lines referencing hook_bodies and update_settings_hook) to
reflect this precise rule and ensure tests expect semantic preservation not
byte-for-byte identity.
- Line 350: The plan step referencing `.ark/workflow.md` is stale because the
Mechanics table was moved to "## 6"; update the instruction in this file to
target the current Mechanics section by changing the step to add the row `|
Session / phase orientation (read-only) | ark context [--scope …] [--for …] |`
into the Mechanics table now under "## 6", placing that row immediately above
the existing `ark agent` rows; ensure the file name `.ark/workflow.md`, section
header "## 6" (Mechanics), and the `ark agent` rows are used to locate the
correct insertion point.

---

Duplicate comments:
In @.ark/tasks/archive/2026-04/ark-context/02_PLAN.md:
- Around line 78-85: The "## Spec" section is currently delta-style and not
self-contained; update the section labeled "## Spec" so it is
extraction-friendly by either inlining the full, final spec text instead of
"Carried forward ..." deltas or adding a clear machine‑readable archival marker
(e.g., JSON/YAML frontmatter or a skip token) immediately under the "## Spec"
header that signals extract tools to ignore this file; ensure the change is
applied to the "## Spec" block and that any references to G-*, NG-*, or C-*
constraints remain intact or are fully expanded when you inline.

In `@crates/ark-core/src/commands/context/gather.rs`:
- Around line 104-106: The code currently treats unreadable task directories as
non-existent by calling exists(); change to use try_exists() on
layout.tasks_dir() and layout.tasks_archive() and map any std::io::Error into
the crate error via Error::io(path, source) so the function returns Err(...)
instead of silently returning an empty TasksState; update the early-return
branches that reference tasks_dir and archive_dir (variables from gather.rs) to
propagate IO errors using the crate::error::Result<T> convention rather than
collapsing failures into false.

In `@crates/ark-core/src/commands/context/mod.rs`:
- Around line 99-107: The code currently calls
serde_json::to_string_pretty(...).expect(...) which will abort on serialization
failure; change both places (the Format::Json branch and the other JSON call at
116-123) to propagate errors by returning crate::error::Result<String> from
wrap_session_start_envelope and by replacing expect(...) with ? on
serde_json::to_string_pretty(&projected) (or map_err to crate::Error if needed),
then pass the resulting String into wrap_session_start_envelope? and propagate
the Result up through context() so context() returns
crate::error::Result<ContextSummary> instead of panicking; update function
signatures (wrap_session_start_envelope, context and any callers) to use the
crate::error::Result type and convert serde_json::Error into the crate error per
project conventions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: e0c5716e-f277-416a-966a-929a0eaefd53

📥 Commits

Reviewing files that changed from the base of the PR and between 41b469c and a0acbe3.

📒 Files selected for processing (49)
  • .ark/.installed.json
  • .ark/specs/features/INDEX.md
  • .ark/specs/features/ark-agent-namespace/SPEC.md
  • .ark/specs/features/ark-context/SPEC.md
  • .ark/specs/features/ark-upgrade/SPEC.md
  • .ark/tasks/archive/2026-04/ark-context/00_PLAN.md
  • .ark/tasks/archive/2026-04/ark-context/00_REVIEW.md
  • .ark/tasks/archive/2026-04/ark-context/01_PLAN.md
  • .ark/tasks/archive/2026-04/ark-context/01_REVIEW.md
  • .ark/tasks/archive/2026-04/ark-context/02_PLAN.md
  • .ark/tasks/archive/2026-04/ark-context/02_REVIEW.md
  • .ark/tasks/archive/2026-04/ark-context/PRD.md
  • .ark/tasks/archive/2026-04/ark-context/VERIFY.md
  • .ark/tasks/archive/2026-04/ark-context/task.toml
  • .ark/tasks/archive/2026-04/spec-extract-self-contained/PRD.md
  • .ark/tasks/archive/2026-04/spec-extract-self-contained/task.toml
  • .ark/workflow.md
  • .claude/commands/ark/archive.md
  • .claude/commands/ark/design.md
  • .claude/commands/ark/quick.md
  • .claude/settings.json
  • AGENTS.md
  • README.md
  • crates/ark-cli/src/main.rs
  • crates/ark-core/src/commands/context/gather.rs
  • crates/ark-core/src/commands/context/mod.rs
  • crates/ark-core/src/commands/context/model.rs
  • crates/ark-core/src/commands/context/projection.rs
  • crates/ark-core/src/commands/context/related_specs.rs
  • crates/ark-core/src/commands/context/render.rs
  • crates/ark-core/src/commands/init.rs
  • crates/ark-core/src/commands/load.rs
  • crates/ark-core/src/commands/mod.rs
  • crates/ark-core/src/commands/remove.rs
  • crates/ark-core/src/commands/unload.rs
  • crates/ark-core/src/commands/upgrade.rs
  • crates/ark-core/src/error.rs
  • crates/ark-core/src/io/fs.rs
  • crates/ark-core/src/io/git.rs
  • crates/ark-core/src/io/mod.rs
  • crates/ark-core/src/layout.rs
  • crates/ark-core/src/lib.rs
  • crates/ark-core/src/state/mod.rs
  • crates/ark-core/src/state/snapshot.rs
  • docs/ROADMAP.md
  • templates/ark/workflow.md
  • templates/claude/commands/ark/archive.md
  • templates/claude/commands/ark/design.md
  • templates/claude/commands/ark/quick.md
✅ Files skipped from review due to trivial changes (15)
  • .claude/settings.json
  • .ark/tasks/archive/2026-04/spec-extract-self-contained/task.toml
  • crates/ark-core/src/state/mod.rs
  • .ark/specs/features/INDEX.md
  • .ark/tasks/archive/2026-04/ark-context/task.toml
  • README.md
  • .ark/tasks/archive/2026-04/ark-context/PRD.md
  • .ark/.installed.json
  • .ark/tasks/archive/2026-04/ark-context/VERIFY.md
  • .ark/tasks/archive/2026-04/ark-context/02_REVIEW.md
  • .ark/specs/features/ark-agent-namespace/SPEC.md
  • .claude/commands/ark/archive.md
  • crates/ark-core/src/lib.rs
  • crates/ark-core/src/commands/context/render.rs
  • .claude/commands/ark/design.md
🚧 Files skipped from review as they are similar to previous changes (13)
  • crates/ark-core/src/commands/context/projection.rs
  • crates/ark-core/src/commands/mod.rs
  • crates/ark-core/src/commands/remove.rs
  • crates/ark-core/src/io/git.rs
  • crates/ark-core/src/commands/context/model.rs
  • .claude/commands/ark/quick.md
  • .ark/specs/features/ark-upgrade/SPEC.md
  • AGENTS.md
  • crates/ark-core/src/io/fs.rs
  • docs/ROADMAP.md
  • crates/ark-cli/src/main.rs
  • crates/ark-core/src/commands/upgrade.rs
  • .ark/tasks/archive/2026-04/spec-extract-self-contained/PRD.md


[**Architecture**]

```
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add language tags to fenced blocks to satisfy markdownlint.

The three fences opened on these lines are missing a language identifier (MD040).

Suggested patch
-```
+```text
@@
-```
+```text
@@
-```
+```text

Also applies to: 91-91, 111-111

🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 40-40: Fenced code blocks should have a language specified

(MD040, fenced-code-language)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.ark/specs/features/ark-context/SPEC.md at line 40, Several fenced code
blocks in SPEC.md are missing language identifiers which triggers markdownlint
MD040; update each bare triple-backtick fence used for plain text to include a
language tag (e.g., change ``` to ```text) for the three occurrences referenced
(the fence at the snippet around line 40 and the other two occurrences around
the noted locations) so the blocks become ```text ... ``` and satisfy the
linter.

Comment on lines +27 to +28
- **C-17:** `.claude/settings.json` is **not hash-tracked.** The Ark-owned `SessionStart` hook entry is re-applied on every successful `init`, `load`, and `upgrade` via `update_settings_hook`, mirroring the `CLAUDE.md` managed-block treatment (upgrade SPEC C-8). The file is not listed in `manifest.files` and has no entry in `manifest.hashes`. User-owned siblings (other `hooks.*` keys, unrelated top-level keys) are preserved byte-identically where JSON object-key order is stable.
- **C-18:** `Snapshot` gains a `hook_bodies: Vec<SnapshotHookBody>` slot. `unload` captures the Ark-owned `SessionStart` entry (matched by `command == ARK_CONTEXT_HOOK_COMMAND`) into this slot. `load` restores it by re-applying `update_settings_hook` after scaffolding. User-edits to *unrelated* hook entries are NOT captured by `hook_bodies` and do NOT survive a round-trip — they are user-owned content outside Ark's owned dirs. Documented trade-off; matches the existing rule that `.claude/` (apart from `commands/ark/`) is user space.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

The settings-hook preservation contract is internally inconsistent.

These sections currently claim three different behaviors: sibling entries are preserved "byte-identically"/"verbatim", sibling entries do not survive unload/load, and update_settings_hook rewrites the file by parsing + pretty-serializing the whole JSON document. The unload/load flow shown in crates/ark-core/src/commands/unload.rs:70-130 and crates/ark-core/src/commands/load.rs:96-125 only removes/reapplies the Ark-owned entry, so sibling hooks survive structurally on disk, but they are neither captured in hook_bodies nor preserved byte-for-byte. Tighten this contract before the tests lock in the wrong behavior.

Also applies to: 42-44, 192-199

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.ark/tasks/archive/2026-04/ark-context/01_PLAN.md around lines 27 - 28, The
document's contract is inconsistent about whether sibling hook entries are
preserved byte-for-byte versus only structurally; reconcile this by making the
behavior explicit and aligning code and docs: update the Snapshot struct
description to state that Snapshot.hook_bodies only captures Ark-owned entries
(matched via ARK_CONTEXT_HOOK_COMMAND) and that unload/load (see unload and load
command implementations) will remove/reapply only Ark-owned entries using
update_settings_hook which may reformat the JSON, so sibling/user-owned hook
entries are preserved semantically but not guaranteed byte-identical; then
adjust the text around the settings-hook preservation (including the lines
referencing hook_bodies and update_settings_hook) to reflect this precise rule
and ensure tests expect semantic preservation not byte-for-byte identity.

1. Add `io::fs::update_settings_hook` + `remove_settings_hook` + `ARK_CONTEXT_HOOK_COMMAND`. V-UT-20, V-UT-21, V-UT-22.
2. Wire `init.rs` to call `update_settings_hook` after scaffold. Add `Snapshot::hook_bodies` capture in `unload.rs` + restore in `load.rs`. Wire `remove.rs` to call `remove_settings_hook`. Wire `upgrade.rs` to call `update_settings_hook` unconditionally after template writes.
3. Update `templates/claude/commands/ark/{quick,design,archive}.md` to prepend `ark context --scope phase --for <phase> --format json` recipe.
4. Update `.ark/workflow.md` §7 mechanics table to add: `| Session / phase orientation (read-only) | ark context [--scope …] [--for …] |` placed above the `ark agent` rows.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

This workflow reference is stale.

.ark/workflow.md in this PR moved Mechanics to ## 6 and removed the old mechanics table, so this step now points at a section that no longer exists.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.ark/tasks/archive/2026-04/ark-context/01_PLAN.md at line 350, The plan step
referencing `.ark/workflow.md` is stale because the Mechanics table was moved to
"## 6"; update the instruction in this file to target the current Mechanics
section by changing the step to add the row `| Session / phase orientation
(read-only) | ark context [--scope …] [--for …] |` into the Mechanics table now
under "## 6", placing that row immediately above the existing `ark agent` rows;
ensure the file name `.ark/workflow.md`, section header "## 6" (Mechanics), and
the `ark agent` rows are used to locate the correct insertion point.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants