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

9 KiB
Raw Blame History

name description argument-hint model
ultracontinue-local Resume the next session in a multi-session ultraplan project. Reads .session-state.local.json and immediately begins the next session. [<project-dir> | --help] 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:

!`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.

{"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.