ktg-plugin-marketplace/plugins/ultraplan-local/commands/ultracontinue-local.md
Kjell Tore Guttormsen f58b892436 fix(ultraplan-local): Bug 2 — eliminate state-file-path template; Read tool + concrete arg
Step 5 of v3.4.1 hot-fix plan. Phase 2 of
commands/ultracontinue-local.md is rewritten to remove every curly-
brace template placeholder. The {state-file-path} substitution failure
caused the path-guard hook to crash on unresolved templates.

New Phase 2 structure:

  2.a — Read the file with the Read tool (no Bash). Deterministic and
        not subject to shell-substitution errors.
  2.b — Schema-validate via the existing CLI shim, with the resolved
        absolute path emitted as a literal string token by the model
        at the time of the Bash call. Anti-substitution invariant:
        STOP if about to emit any unresolved placeholder.
  2.c — Interpret validator result (preserved verbatim from the
        previous Phase 2 — three-way branch on valid + status).

Verification: grep -c "{state-file-path}" returns 0; full Phase 2
section contains no {lowercase-template} curly-brace placeholders.
Suite 322 -> 335 passing (+13: 7 from Step 1, 4 from Step 2, 2 from
Step 4).
2026-05-04 16:40:11 +02:00

210 lines
9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
name: ultracontinue-local
description: Resume the next session in a multi-session ultraplan project. Reads .session-state.local.json and immediately begins the next session.
argument-hint: "[<project-dir> | --help]"
model: opus
---
# Ultracontinue Local v1.0
Zero-friction multi-session resumption. In a fresh Claude Code session, type
`/ultracontinue` — the command reads the per-project state file
(`.claude/projects/<project>/.session-state.local.json`), shows a 3-line summary,
and immediately begins executing the next session.
The state file is the contract. Any session-end mechanism may write it
(`/ultraexecute-local` Phase 8 / Phase 2.55 / Phase 4, the
`/ultraplan-end-session-local` helper, or — in the future — `graceful-handoff`).
This command only reads.
Pipeline position:
```
/ultraplan-local → plan.md
/ultraexecute-local → progress.json + .session-state.local.json
... session boundary, fresh chat ...
/ultracontinue → reads .session-state.local.json, starts next session
```
See **Handover 7** in `docs/HANDOVER-CONTRACTS.md` for the full schema.
## Phase 0 — `--help` handling
Parse `$ARGUMENTS` with `parseArgs($ARGUMENTS, 'ultracontinue')` from
`lib/parsers/arg-parser.mjs`. Dispatch the usage block ONLY when one of these
two conditions equals exactly true (no substring search, no "contains" check):
- `flags['--help'] === true`, OR
- `positional[0] === '-h'` (single-dash short form — the parser keeps it as
positional because the schema does not declare an alias).
In every other case — including when `$ARGUMENTS` is empty, whitespace-only,
the literal empty string `""`, or a positional project-dir — fall through to
Phase 1. Do NOT print the usage block on empty args.
```
/ultracontinue — Resume the next session in a multi-session ultraplan project.
Usage:
/ultracontinue # auto-discover state file under cwd
/ultracontinue <project-dir> # explicit project directory
/ultracontinue --help # this message
Reads .claude/projects/<project>/.session-state.local.json (per-project,
gitignored). On a valid resumable state, prints a 3-line summary and begins
executing the next session immediately. No interactive confirmation prompt.
State-file schema (v1):
schema_version 1
project string
next_session_brief_path string (validator soft-checks file existence)
next_session_label string
status in_progress | partial | failed | stopped | completed
(completed → no further sessions to resume)
updated_at ISO-8601 timestamp
(unknown top-level keys are tolerated — forward-compat for graceful-handoff v2.2)
Typical flow:
/ultrabrief-local # produces brief.md
/ultraplan-local --project ... # produces plan.md
/ultraexecute-local --project .. # writes session-state on session-end
... (fresh Claude chat) ...
/ultracontinue # reads session-state, runs next session
```
## Phase 1 — Resolve project directory
The parsed `positional[0]` from Phase 0 is the explicit project-dir argument,
when present. Otherwise (empty `$ARGUMENTS` or whitespace-only) auto-discover.
### Step 1.a — Reject `.md` positional argument (SC-2)
If `positional[0]` is non-empty AND ends in `.md`, the user almost certainly
pasted a `NEXT-SESSION-PROMPT.local.md` path instead of a project directory.
Print the following diagnostic to stderr and exit non-zero. Do NOT proceed.
```
Error: expected <project-dir>, got a markdown file path: <positional[0]>
Did you mean to paste the file path as a project directory?
Usage: /ultracontinue-local <project-dir>
```
### Step 1.b — Auto-discover candidates
When no explicit project-dir was given, enumerate
`.claude/projects/*/.session-state.local.json` paths with `node -e`
(NOT shell glob — harness-mode safety) and emit each as one JSON line of
`{"path": ..., "updated_at": ...}` so Phase 1 can sort numerically:
```bash
!`node -e "const fs=require('fs'),path=require('path');const root='.claude/projects';if(!fs.existsSync(root))process.exit(0);for(const d of fs.readdirSync(root)){const p=path.join(root,d,'.session-state.local.json');if(!fs.existsSync(p))continue;let u='';try{u=(JSON.parse(fs.readFileSync(p,'utf8'))||{}).updated_at||''}catch(_){};process.stdout.write(JSON.stringify({path:p,updated_at:u})+'\\n');}"`
```
Sort the emitted candidates by `Date.parse(updated_at)` descending (newest
first) — numeric comparison, NOT lexicographic string compare. The newest
resumable state wins.
### Step 1.c — Decision tree
- **0 candidates and no explicit arg:** print SC-2 cold-start message and exit:
```
No active multi-session project here.
Start with /ultrabrief-local or /ultraplan-local.
```
- **1 candidate (or explicit non-`.md` arg):** continue to Phase 2 with that path.
- **>1 candidates and no explicit arg:** with the Date.parse sort applied, the
newest resumable state wins automatically and the command continues to Phase 2
with that path. (Operators who want a different candidate re-invoke as
`/ultracontinue <project-dir>`.)
## Phase 2 — Validate the state file
Phase 1 resolved a concrete state-file path. That path is a real string in
your working context — never a template. Phase 2 must read and validate the
state file without any placeholder substitution.
### Step 2.a — Read the file with the Read tool (no Bash)
Use the **Read tool** on the resolved state-file path from Phase 1. Do NOT use
Bash for the read. The Read tool is deterministic and not subject to
shell-substitution errors. Parse the returned JSON body programmatically.
### Step 2.b — Schema-validate the parsed object
Verify the schema by invoking the existing validator CLI shim. Emit the
resolved absolute path as a literal string token in the Bash command — the
exact same string you just passed to the Read tool. The validator accepts
`--json <path>` and prints a `{valid, errors, warnings}` JSON record:
```
node lib/validators/session-state-validator.mjs --json <RESOLVED-ABSOLUTE-PATH-FROM-PHASE-1>
```
Replace `<RESOLVED-ABSOLUTE-PATH-FROM-PHASE-1>` with the actual path string at
the time you issue the Bash call. There is no template engine; the string is
substituted by you, the model, before the Bash tool sees the command.
**Anti-substitution invariant.** If you ever find yourself about to emit a
literal angle-bracket placeholder, or any other unresolved variable name, to
the Bash tool — STOP. The resolved path is a concrete value you already have
from Phase 1; emit the value, not a placeholder for it.
### Step 2.c — Interpret the result
- **Validator exit code != 0 OR `valid: false` in JSON output:** print the
structured `errors[]` (each `[code] message` on its own line) and exit. Do
not proceed to narration. Suggest running the validator directly for
follow-up: `node lib/validators/session-state-validator.mjs <path>`.
- **`valid: true` AND any warning has `code: SESSION_STATE_NOT_RESUMABLE`**
(i.e. `status: completed`): print "no further sessions to resume; project
complete" and exit cleanly.
- **`valid: true` AND status is one of `in_progress | partial | failed | stopped`:**
proceed to Phase 3.
## Phase 3 — Narrate 3-line summary
Print this exact template (using values from the validated `parsed` object):
```
Project: {project}
Next session: {next_session_label}
Brief: {next_session_brief_path}
```
No interactive confirmation prompt — per the brief NFR ("ingen prompts, men la
informasjon synes"). The 3-line block is informational only.
## Phase 4 — Begin execution
Read the file at `next_session_brief_path` (it is the brief that the next
session is supposed to execute — typically the same `brief.md` for
single-brief multi-session plans, or a session-specific spec for parallel
session decomposition). Understand the task and begin executing per the
standard ultraplan-local pipeline. The user did not type a separate "start"
command — `/ultracontinue` is the start.
If the brief file does not exist (validator emits a warning but does not
fail), print: `Warning: next_session_brief_path "{path}" does not exist on
disk. Cannot continue automatically.` and exit. Do not guess.
## Phase 5 — Stats tracking
Append a one-line JSON record to `${CLAUDE_PLUGIN_DATA}/ultracontinue-stats.jsonl`
if the env var is set; silently skip otherwise.
```json
{"ts":"<iso-8601>","project":"<project>","next_session_label":"<label>","status":"<status>"}
```
## Hard rules
- **Idempotent.** Running `/ultracontinue` twice in the same Claude session
does not advance state — the writer (Phase 8 / hook / helper) advances state
only when a session completes.
- **Zero secrets in the state file.** Status, paths, labels — never API keys,
never user content beyond filenames.
- **NEVER auto-load via SessionStart.** The command is operator-invoked only.
Auto-loading would re-introduce the stale-file risk noted in
`feedback_next_session_prompt_manual.md`.
- **No interactive prompts.** Phases 04 must run without `AskUserQuestion`.
This keeps the command headless-safe.