diff --git a/CHANGELOG.md b/CHANGELOG.md index 5718f30..7c49356 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,10 @@ Please choose versions by [Semantic Versioning](http://semver.org/). * MINOR version when you add functionality in a backwards-compatible manner, and * PATCH version when you make backwards-compatible bug fixes. +## Unreleased + +- refactor(plugin): rename orchestration-mode flag `--tool` → `--non-interactive` and internal label `MODE=tool` → `MODE=non_interactive` across create-task, create-goal, complete-task, complete-goal, defer-task, defer-goal commands and task-creator, goal-creator, goal-manager-agent agents; the command/agent instructions still accept `--tool` as a deprecated alias for one release. These flags are Claude Code slash-command arguments interpreted by the agent instructions — not vault-cli binary flags. + ## v0.96.0 - feat: Config loading follows XDG Base Directory spec — new FindConfigDir prefers ~/.config/vault-cli/ over legacy ~/.vault-cli/ with no forced migration diff --git a/agents/goal-creator.md b/agents/goal-creator.md index 4ccea3d..32c3d41 100644 --- a/agents/goal-creator.md +++ b/agents/goal-creator.md @@ -29,9 +29,9 @@ You create one goal file in the configured vault. You read the vault config via ## 1. Parse arguments -Input form: `[goal description] [--tool] [--vault NAME] [--objective NAME]`. +Input form: `[goal description] [--non-interactive] [--vault NAME] [--objective NAME]`. -- `--tool` → MODE = `tool` (machine-readable JSON output, no AskUserQuestion calls, no audit, hard-fail on collision) +- `--non-interactive` (deprecated alias: `--tool`) → MODE = `non_interactive` (machine-readable JSON output, no AskUserQuestion calls, no audit, hard-fail on collision) - `--vault NAME` → target vault override - `--objective NAME` → parent objective name (string match against objectives_dir) - Remaining tokens → goal description / title @@ -52,7 +52,7 @@ Find the vault entry matching the requested vault name (or `default_vault` from - `objectives_dir` — parent objective lookup folder (or empty) - `themes_dir` — theme folder (or empty) -If the vault is not found, report the error and stop. In MODE=tool, return `{"success": false, "error": "..."}`. +If the vault is not found, report the error and stop. In MODE=non_interactive, return `{"success": false, "error": "..."}`. ## 3. Compose the title @@ -68,8 +68,8 @@ Final filename: `{Title}.md` (no Jira prefix; goals are higher-level than tasks) If `--objective NAME` was provided: - Glob `{vault.path}/{objectives_dir}/*.md` and find a name match (case-insensitive, partial OK) -- If 0 matches → MODE=tool fails with `objective not found`; MODE=interactive AskUserQuestion to pick from list or skip -- If >1 matches → MODE=tool fails; MODE=interactive AskUserQuestion to pick +- If 0 matches → MODE=non_interactive fails with `objective not found`; MODE=interactive AskUserQuestion to pick from list or skip +- If >1 matches → MODE=non_interactive fails; MODE=interactive AskUserQuestion to pick - Store as `OBJECTIVE_LINK` (e.g. `[[Objective Name]]`) If `--objective` not provided, leave `OBJECTIVE_LINK` empty. Do not auto-detect. @@ -86,14 +86,14 @@ In MODE=interactive, AskUserQuestion for timeline: If the user picks a duration, compute `start_date = today` and `end_date = today + N weeks`. Store as `TIMELINE = "{start_date} to {end_date}"`. -In MODE=tool, leave timeline empty. +In MODE=non_interactive, leave timeline empty. ## 6. Determine category and priority - **Category**: infer from keywords or description (e.g. `trading`, `health`, `learning`, `personal`); leave empty if unclear - **Priority**: default 3; raise to 2 if user description signals importance; 1 reserved for top-priority goals -In MODE=tool, default to category=`""` and priority=3, do not block. +In MODE=non_interactive, default to category=`""` and priority=3, do not block. ## 7. Resolve template body @@ -129,7 +129,7 @@ If a template body was loaded in step 7, use it. Otherwise, generate the standar 1. `Tags: [[Goal]]` (plus theme tags if set) 2. `---` separator -3. Short summary paragraph (1–2 sentences). **First sentence MUST state the outcome** — what's true when the goal is done, not what work happens. E.g. "Reduce X from Y to Z" not "Build Z by doing W". **Before writing, rephrase any mechanism-phrased input into outcome form** — this rule applies in both interactive and tool mode; do NOT rely on step 12 audit to catch it (audit only runs in interactive mode). Avoid `via X` / `by doing Y` / `through Z` / `Split X / Build Y / Refactor Z / Set up Z` openings — these are mechanism leaks. Mechanism details belong in `# Impact` as an "Approach" lead paragraph. See `docs/goal-writing.md#summary-first-sentence` for the sniff test. +3. Short summary paragraph (1–2 sentences). **First sentence MUST state the outcome** — what's true when the goal is done, not what work happens. E.g. "Reduce X from Y to Z" not "Build Z by doing W". **Before writing, rephrase any mechanism-phrased input into outcome form** — this rule applies in both interactive and non_interactive mode; do NOT rely on step 12 audit to catch it (audit only runs in interactive mode). Avoid `via X` / `by doing Y` / `through Z` / `Split X / Build Y / Refactor Z / Set up Z` openings — these are mechanism leaks. Mechanism details belong in `# Impact` as an "Approach" lead paragraph. See `docs/goal-writing.md#summary-first-sentence` for the sniff test. 4. `# Impact` — why this goal matters strategically. If a brief mechanism description is needed, lead with an "**Approach**:" paragraph before the strategic bullets. 5. `# Status Summary` — Progress / Current / Next / Blockers placeholders 6. `# Success Criteria` — measurable outcomes as checkboxes @@ -145,7 +145,7 @@ Glob: {vault.path}/{goals_dir}/{filename} If the file already exists: -- MODE=tool → return `{"success": false, "error": "goal file already exists: ..."}` +- MODE=non_interactive → return `{"success": false, "error": "goal file already exists: ..."}` - MODE=interactive → AskUserQuestion: 1. Pick a different name 2. Cancel ## 11. Write the file @@ -170,7 +170,7 @@ Run a light self-audit against the file: - If `timeline` is set, validate it is ≤ 4 weeks - No accidental empty sections -Skip in MODE=tool. +Skip in MODE=non_interactive. ## 13. Return @@ -187,13 +187,13 @@ Next steps: 3. Verify: /vault-cli:verify-goal "{title}" ``` -MODE=tool output (single JSON object on stdout, nothing else): +MODE=non_interactive output (single JSON object on stdout, nothing else): ```json {"success": true, "path": "{absolute_path}", "filename": "{filename}"} ``` -On error in MODE=tool: +On error in MODE=non_interactive: ```json {"success": false, "error": ""} @@ -205,7 +205,7 @@ On error in MODE=tool: - Vault not found → fail with the requested vault name in the error - `goals_dir` directory does not exist on disk → create it (`mkdir -p`) before writing - `goal_template` configured but file missing → fail with "template not found: {path}" -- `--objective` not resolvable → MODE=tool fails; MODE=interactive prompts +- `--objective` not resolvable → MODE=non_interactive fails; MODE=interactive prompts - Filename collision → see step 10 - Timeline > 4 weeks → fail with "goals must be ≤ 4 weeks; use an objective for longer horizons" diff --git a/agents/goal-manager-agent.md b/agents/goal-manager-agent.md index 216136e..e01b19f 100644 --- a/agents/goal-manager-agent.md +++ b/agents/goal-manager-agent.md @@ -83,7 +83,7 @@ Show goal status. Accepts an explicit goal name/path, or detects from conversati - MODE=interactive: parse conversation for file paths, wiki links, goal mentions - 0 matches → error "No active goal detected; pass a goal name explicitly" - >1 matches → AskUserQuestion to select - - MODE=tool: return `{"success": false, "error": "goal name required in tool mode"}` and STOP + - MODE=non_interactive: return `{"success": false, "error": "goal name required in non_interactive mode"}` and STOP 2. **Find goal file:** ``` diff --git a/agents/task-creator.md b/agents/task-creator.md index b97f4fa..4954608 100644 --- a/agents/task-creator.md +++ b/agents/task-creator.md @@ -30,9 +30,9 @@ You create one task file in the configured vault. You read the vault config via ## 1. Parse arguments -Input form: `[task description] [--tool] [--vault NAME]`. +Input form: `[task description] [--non-interactive] [--vault NAME]`. -- `--tool` → MODE = `tool` (machine-readable JSON output, no AskUserQuestion calls, no audit, hard-fail on collision) +- `--non-interactive` (deprecated alias: `--tool`) → MODE = `non_interactive` (machine-readable JSON output, no AskUserQuestion calls, no audit, hard-fail on collision) - `--vault NAME` → target vault override - Remaining tokens → task description / title @@ -51,7 +51,7 @@ Find the vault entry matching the requested vault name (or `default_vault` from - `task_template` — absolute path to the task template (or empty/absent) - `goals_dir` — for optional parent-goal linking (or empty) -If the vault is not found, report the error and stop. In MODE=tool, return `{"success": false, "error": "..."}`. +If the vault is not found, report the error and stop. In MODE=non_interactive, return `{"success": false, "error": "..."}`. ## 3. Detect Jira / issue key in the description @@ -60,7 +60,7 @@ Scan the description for patterns like `BRO-18665`, `TRADE-4304`, `OC-2042`, etc - If found, store as `JIRA_KEY` and remove from the title text. - If a single key is found, prepend it to the final filename: `{JIRA_KEY} {Title}.md`. -In MODE=interactive, optionally fetch the Jira issue details via `mcp__atlassian__getJiraIssue` to enrich the title and description (only if a Jira key was detected and the MCP tool is available). Skip in MODE=tool. +In MODE=interactive, optionally fetch the Jira issue details via `mcp__atlassian__getJiraIssue` to enrich the title and description (only if a Jira key was detected and the MCP tool is available). Skip in MODE=non_interactive. ## 4. Detect incident-shaped task (interactive only) @@ -77,7 +77,7 @@ If matched, AskUserQuestion with a SEV-1..SEV-4 picker: If the user picks a severity, store as `SEVERITY` for inclusion in frontmatter. -Skip this step entirely in MODE=tool. +Skip this step entirely in MODE=non_interactive. ## 5. Compose the title @@ -90,7 +90,7 @@ Final filename: `{JIRA_KEY }{Title}.md` (Jira key prefix only when detected). ## 6. Determine category and priority -- **Category**: infer from keywords or task content (e.g. `octopus`, `trading`, `personal`); if unclear, leave empty in tool mode or AskUserQuestion in interactive mode +- **Category**: infer from keywords or task content (e.g. `octopus`, `trading`, `personal`); if unclear, leave empty in non_interactive mode or AskUserQuestion in interactive mode - **Priority**: default 3 (low/normal); raise to 2 if SEV-2/3 incident or if user description signals urgency; 1 only on SEV-1 ## 7. Resolve template body @@ -145,7 +145,7 @@ Glob: {vault.path}/{tasks_dir}/{filename} If the file already exists: -- MODE=tool → return `{"success": false, "error": "task file already exists: ..."}` +- MODE=non_interactive → return `{"success": false, "error": "task file already exists: ..."}` - MODE=interactive → AskUserQuestion: 1. Pick a different name 2. Cancel ## 11. Write the file @@ -165,7 +165,7 @@ Run a light self-audit against the file: - Body has Success Criteria + Tasks sections (or template body) - No accidental empty sections -Skip in MODE=tool. +Skip in MODE=non_interactive. ## 13. Return @@ -182,13 +182,13 @@ Next steps: 3. Defer: /defer-task "{title}" ``` -MODE=tool output (single JSON object on stdout, nothing else): +MODE=non_interactive output (single JSON object on stdout, nothing else): ```json {"success": true, "path": "{absolute_path}", "filename": "{filename}"} ``` -On error in MODE=tool: +On error in MODE=non_interactive: ```json {"success": false, "error": ""} @@ -201,5 +201,5 @@ On error in MODE=tool: - `tasks_dir` directory does not exist on disk → create it (`mkdir -p`) before writing - `task_template` configured but file missing → fail with "template not found: {path}" - Filename collision → see step 10 -- Unable to detect category/priority and MODE=tool → use category=`""` and priority=3, do not block +- Unable to detect category/priority and MODE=non_interactive → use category=`""` and priority=3, do not block diff --git a/commands/complete-goal.md b/commands/complete-goal.md index 901c26f..b175189 100644 --- a/commands/complete-goal.md +++ b/commands/complete-goal.md @@ -1,6 +1,6 @@ --- description: Mark goal as complete (checks subtasks and success criteria) -argument-hint: " [--tool] [--force]" +argument-hint: " [--non-interactive] [--force]" --- @@ -9,7 +9,7 @@ Mark goal as complete using vault-cli. Verifies success criteria and linked subt 1. Parse arguments: - - If contains `--tool` → MODE=tool, remove flag from args + - If contains `--non-interactive` (or deprecated `--tool`) → MODE=non_interactive, remove flag from args - If contains `--force` → FORCE=true, remove flag from args - Otherwise → MODE=interactive - Extract goal name from remaining args @@ -41,7 +41,7 @@ Mark goal as complete using vault-cli. Verifies success criteria and linked subt ``` - If warnings in output, show them -3. **MODE=tool (--tool flag):** +3. **MODE=non_interactive (--non-interactive flag):** a. Read goal file to check completion state b. If incomplete success criteria or open subtasks (and FORCE not set): @@ -60,7 +60,7 @@ Mark goal as complete using vault-cli. Verifies success criteria and linked subt - vault-cli goal complete invoked (NOT Edit tool for frontmatter) -- **MODE=tool**: Returns JSON only, never forces unless `--force` explicitly passed +- **MODE=non_interactive**: Returns JSON only, never forces unless `--force` explicitly passed - **MODE=interactive**: Shows progress, asks if incomplete, reports result - Parent objective updated (by vault-cli) - `--force` only used when user explicitly approves or passes the flag diff --git a/commands/complete-task.md b/commands/complete-task.md index 675e7f4..ca6930a 100644 --- a/commands/complete-task.md +++ b/commands/complete-task.md @@ -1,6 +1,6 @@ --- description: Mark task as complete (normal or recurring) -argument-hint: " [--tool] [--force]" +argument-hint: " [--non-interactive] [--force]" allowed-tools: - Bash(vault-cli:*) - Read @@ -12,9 +12,9 @@ Mark task as complete using vault-cli. Handles normal and recurring tasks approp 1. Parse arguments: - - If contains `--tool` → MODE=tool, remove flag from args + - If contains `--non-interactive` (or deprecated `--tool`) → MODE=non_interactive, remove flag from args - Otherwise → MODE=interactive - - If contains `--force` → FORCE=true, remove flag from args (interactive mode only — `--tool` overrides as defined in step 3) + - If contains `--force` → FORCE=true, remove flag from args (interactive mode only — `--non-interactive` overrides as defined in step 3) - Extract task name from remaining args 2. **MODE=interactive (default):** @@ -49,9 +49,9 @@ Mark task as complete using vault-cli. Handles normal and recurring tasks approp Rationale for this closer (one-task-per-session contract, why never `/vault-cli:next-task` here, why the "no end-of-day suggestions" global rule does NOT override): see [`sync-progress.md` Phase 6](sync-progress.md) — same contract applies. Single source of truth lives there to prevent drift. - MODE=tool MUST NOT emit this panel (see step 3 — JSON only). + MODE=non_interactive MUST NOT emit this panel (see step 3 — JSON only). -3. **MODE=tool (--tool flag):** +3. **MODE=non_interactive (--non-interactive flag):** a. Read task file to check completion state b. If incomplete items: @@ -70,7 +70,7 @@ Mark task as complete using vault-cli. Handles normal and recurring tasks approp d. Never ask questions, never prompt. - e. **Never emit the `⚪ DONE` closer panel** — MODE=tool output is JSON only. The closer panel from step 2e is interactive-mode only. + e. **Never emit the `⚪ DONE` closer panel** — MODE=non_interactive output is JSON only. The closer panel from step 2e is interactive-mode only. 4. Task types (handled by vault-cli internally): - Normal tasks: status→completed, goals updated, daily note checked @@ -79,7 +79,7 @@ Mark task as complete using vault-cli. Handles normal and recurring tasks approp - vault-cli task complete invoked (NOT Edit tool for frontmatter) -- **MODE=tool**: Returns JSON only, sets phase=human_review if incomplete +- **MODE=non_interactive**: Returns JSON only, sets phase=human_review if incomplete - **MODE=interactive**: Shows completion %, aborts with `--force` hint if incomplete (no prompts), reports result on success - Goal files updated (by vault-cli) - Daily note updated (by vault-cli) diff --git a/commands/create-goal.md b/commands/create-goal.md index a423501..5271132 100644 --- a/commands/create-goal.md +++ b/commands/create-goal.md @@ -1,13 +1,13 @@ --- description: Create a new goal in the configured vault with guided prompts -argument-hint: "[goal description] [--tool] [--vault NAME] [--objective NAME]" +argument-hint: "[goal description] [--non-interactive] [--vault NAME] [--objective NAME]" allowed-tools: [Task, Read, Write, Glob, Grep, Bash, AskUserQuestion] --- Invoke the goal-creator agent to create a goal file in the vault. Parse `$ARGUMENTS`: -- `--tool` → MODE=tool (orchestration mode, JSON output, no prompts) +- `--non-interactive` (deprecated alias: `--tool`) → MODE=non_interactive (orchestration mode, JSON output, no prompts) - `--vault NAME` → target a specific vault (otherwise use the default vault from `~/.vault-cli/config.yaml`) - `--objective NAME` → link to parent objective by name (interactive mode resolves via Glob/AskUserQuestion if ambiguous) - Remaining text → goal description / title diff --git a/commands/create-task.md b/commands/create-task.md index 7d0d446..3b7cdbf 100644 --- a/commands/create-task.md +++ b/commands/create-task.md @@ -1,13 +1,13 @@ --- description: Create a new task in the configured vault with guided prompts -argument-hint: "[task description] [--tool] [--vault NAME]" +argument-hint: "[task description] [--non-interactive] [--vault NAME]" allowed-tools: [Task, Read, Write, Glob, Grep, Bash, AskUserQuestion] --- Invoke the task-creator agent to create a task file in the vault. Parse `$ARGUMENTS`: -- `--tool` → MODE=tool (orchestration mode, JSON output, no prompts) +- `--non-interactive` (deprecated alias: `--tool`) → MODE=non_interactive (orchestration mode, JSON output, no prompts) - `--vault NAME` → target a specific vault (otherwise use the default vault from `~/.vault-cli/config.yaml`) - Remaining text → task description / title diff --git a/commands/defer-goal.md b/commands/defer-goal.md index 01080ae..dddd3b5 100644 --- a/commands/defer-goal.md +++ b/commands/defer-goal.md @@ -1,6 +1,6 @@ --- description: Defer goal to specific date -argument-hint: " [--tool]" +argument-hint: " [--non-interactive]" allowed-tools: Bash(vault-cli goal defer:*) --- @@ -10,7 +10,7 @@ Defer goal to specific date using vault-cli. Supports absolute (YYYY-MM-DD), rel 1. Parse arguments: - - If contains `--tool` → MODE=tool, remove flag from args + - If contains `--non-interactive` (or deprecated `--tool`) → MODE=non_interactive, remove flag from args - Otherwise → MODE=interactive - Split remaining args: goal name = all but last, date = last word @@ -30,7 +30,7 @@ Defer goal to specific date using vault-cli. Supports absolute (YYYY-MM-DD), rel ``` - If warnings in output, show them -3. **MODE=tool (--tool flag):** +3. **MODE=non_interactive (--non-interactive flag):** a. Run vault-cli: ```bash @@ -50,6 +50,6 @@ Defer goal to specific date using vault-cli. Supports absolute (YYYY-MM-DD), rel - vault-cli goal defer invoked (NOT Edit tool for frontmatter) - Date parsed and validated (by vault-cli) - Goal defer_date updated (by vault-cli) -- MODE=tool: Returns JSON only +- MODE=non_interactive: Returns JSON only - MODE=interactive: Full report diff --git a/commands/defer-task.md b/commands/defer-task.md index 1a20e1a..f8cd69f 100644 --- a/commands/defer-task.md +++ b/commands/defer-task.md @@ -1,6 +1,6 @@ --- description: Defer task to specific date -argument-hint: " [--tool]" +argument-hint: " [--non-interactive]" allowed-tools: Bash(vault-cli task defer:*) --- @@ -10,7 +10,7 @@ Defer task to specific date using vault-cli. Supports absolute (YYYY-MM-DD), rel 1. Parse arguments: - - If contains `--tool` → MODE=tool, remove flag from args + - If contains `--non-interactive` (or deprecated `--tool`) → MODE=non_interactive, remove flag from args - Otherwise → MODE=interactive - Split remaining args: task name = all but last, date = last word @@ -30,7 +30,7 @@ Defer task to specific date using vault-cli. Supports absolute (YYYY-MM-DD), rel ``` - If warnings in output, show them -3. **MODE=tool (--tool flag):** +3. **MODE=non_interactive (--non-interactive flag):** a. Run vault-cli: ```bash @@ -53,6 +53,6 @@ Defer task to specific date using vault-cli. Supports absolute (YYYY-MM-DD), rel - Date parsed and validated (by vault-cli) - Task defer_date updated (by vault-cli) - Daily notes updated (by vault-cli) -- MODE=tool: Returns JSON only +- MODE=non_interactive: Returns JSON only - MODE=interactive: Full report diff --git a/prompts/work-on-headless-non-interactive.md b/prompts/work-on-headless-non-interactive.md new file mode 100644 index 0000000..102e946 --- /dev/null +++ b/prompts/work-on-headless-non-interactive.md @@ -0,0 +1,65 @@ +--- +status: draft +created: "2026-07-02T18:15:00Z" +--- + + +- Clicking "Start" on a task in the Vault UI can hang for five minutes and then fail with a "claude session start timed out" error whenever the task needs any input to get going. +- Root cause: the background step that opens the work session runs Claude in headless mode, where it cannot answer questions. If the work-on command stops to ask something, it waits for an answer that never comes until the timeout kills it. +- Fix: tell that headless step to run the work-on command in non-interactive mode, so it takes safe defaults instead of stopping to ask. The interactive part still happens later, when the session is resumed in a real terminal. +- Add a unit test proving the headless bootstrap now runs the command in non-interactive mode. +- After this change, starting a task from the Vault UI no longer hangs, and `make precommit` passes. + + + +Stop the headless `work-on` Claude bootstrap from hanging on interactive prompts by appending `--non-interactive` to the bootstrap slash-command invocation in `pkg/ops/workon.go`, and add a unit test. No changes outside `pkg/ops/`; no interface or constructor signature changes. + + + +Read CLAUDE.md for project conventions. +Read `/home/node/.claude/plugins/marketplaces/coding/docs/go-testing-guide.md` — Ginkgo v2 / Gomega, Counterfeiter mocks, external `_test` package convention. + +Read these files before implementing: +- `pkg/ops/workon.go`: + - `handleClaudeSession` (around line 179) — builds the bootstrap prompt at line 191 as `fmt.Sprintf(`%s "%s"`, vault.GetWorkOnCommand(), task.FilePath)` and calls `w.starter.StartSession(ctx, prompt, vaultPath, task.Name)`. This is the ONLY place the bootstrap prompt is built. +- `pkg/ops/claude_session.go`: + - `StartSession` (line 69) — always shells out to `claude --print -p --output-format json`, a headless turn that cannot answer `AskUserQuestion`. Read for understanding only; do NOT modify. +- `pkg/ops/workon_test.go` (around line 194) — the existing assertion on the bootstrap prompt uses `MatchRegexp(`^/custom-cmd "`)` (a prefix match), which still passes after a suffix is appended. Add a new assertion for the `--non-interactive` suffix here. +- `mocks/claude-session-starter.go` — Counterfeiter fake `ClaudeSessionStarter`; `StartSessionArgsForCall(0)` returns `(ctx, prompt, cwd, name)` — the prompt is the 2nd value. + +Design note (resolved for this prompt): +- The append is UNCONDITIONAL. `StartSession` always runs headless `claude --print` to CREATE the session; that turn cannot answer `AskUserQuestion` even when vault-cli is in interactive mode (interactive sharpening happens afterward in `ResumeSession`, the sibling path in `pkg/ops/claude_resume.go`, which is correctly left alone). So the bootstrap must always be non-interactive. +- The default work-on command `/vault-cli:work-on-task` honors `--non-interactive`; passing it to a custom `work_on_command` that ignores it is harmless (extra prompt text). + + + +1. In `pkg/ops/workon.go` `handleClaudeSession`, append ` --non-interactive` to the bootstrap prompt: + ```go + // The bootstrap always runs headless `claude --print`, which cannot answer + // AskUserQuestion; --non-interactive tells the work-on command to take safe + // defaults instead of prompting (prevents the 5m headless hang). + prompt := fmt.Sprintf(`%s "%s" --non-interactive`, vault.GetWorkOnCommand(), task.FilePath) + ``` + +2. In `pkg/ops/workon_test.go`, add an assertion that the prompt handed to the fake starter ends with `--non-interactive` and still contains the task file path — via `starter.StartSessionArgsForCall(0)` (the 2nd return value is the prompt). Keep the existing prefix assertion. + +3. Add a CHANGELOG entry under `## Unreleased`. Do NOT bump the four version strings — the autoRelease bot versions `## Unreleased` on merge. + + + +- Error handling: `github.com/bborbe/errors` wrapping with `ctx`; never `fmt.Errorf`; never `context.Background()` in `pkg/`. +- Do NOT change the `ClaudeSessionStarter` interface signature or any constructor. +- The append is unconditional (bootstrap is always headless) — do NOT thread `isInteractive` into `handleClaudeSession`. +- Do NOT modify `pkg/ops/claude_session.go` or the resume path (`pkg/ops/claude_resume.go`). +- Do NOT commit — dark-factory handles git. +- Existing tests must still pass. + +Non-goals: making the hardcoded 5-minute session-start timeout configurable is intentionally OUT OF SCOPE. Fix 1 above resolves the hang; a tunable timeout is a separate concern and, if ever needed, belongs in its own prompt with a named consumer. + + + +Run `make precommit` — must pass. +Run `make test` — unit suite passes. +Run `grep -n "non-interactive" pkg/ops/workon.go` — ≥1 line. +Run `grep -n "non-interactive" pkg/ops/workon_test.go` — ≥1 line (new assertion). +