voyage/commands/trekcontinue.md
2026-10-26 15:55:21 +01:00

16 KiB
Raw Blame History

name description argument-hint
trekcontinue Resume the next session in a multi-session trekplan project. Reads .session-state.local.json and immediately begins the next session. [<project-dir> | --help]

Ultracontinue Local v1.0

Zero-friction multi-session resumption. In a fresh Claude Code session, type /trekcontinue — 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 (/trekexecute Phase 8 / Phase 2.55 / Phase 4, the /trekendsession helper, or — in the future — graceful-handoff). This command only reads.

Pipeline position:

/trekplan           →  plan.md
/trekexecute        →  progress.json + .session-state.local.json
... session boundary, fresh chat ...
/trekcontinue             →  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 the shared arg-parser. Run first, exactly like this:

node ${CLAUDE_PLUGIN_ROOT}/lib/parsers/arg-parser.mjs --command trekcontinue --stdin <<'VOYAGE_ARGS_EOF_7F3C9E'
$ARGUMENTS
VOYAGE_ARGS_EOF_7F3C9E

The arguments reach the parser as the one line inside the quoted heredoc, on stdin, never as shell words. It prints the parse result as JSON. On exit 1, print its errors and stop. When it prints no JSON, say that the parser did not run and stop.

Read flags and positional from the JSON it prints. 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.

/trekcontinue — Resume the next session in a multi-session trekplan project.

Usage:
  /trekcontinue                                       # auto-discover state file under cwd
  /trekcontinue <project-dir>                         # explicit project directory
  /trekcontinue --cleanup <project-dir>               # dry-run: list stale files
  /trekcontinue --cleanup --confirm <project-dir>     # actually delete (requires status: completed)
  /trekcontinue --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:
  /trekbrief                # produces brief.md
  /trekplan --project ...   # produces plan.md
  /trekexecute --project .. # writes session-state on session-end
  ... (fresh Claude chat) ...
  /trekcontinue                   # reads session-state, runs next session

Phase 0.5 — Cleanup mode dispatch

After the parser has printed its parse of $ARGUMENTS, check the parsed flags object directly (NOT a string contains-check on raw $ARGUMENTS — that substring pattern was the root cause of Bug 1).

If flags['--cleanup'] === true, switch into the terminal cleanup flow and do NOT proceed to Phase 1 or any later phase.

Required positional: an explicit <project-dir> (positional[0]). There is no "clean all" mode — accidental wholesale deletion would be irreversible. If positional[0] is missing, empty, or starts with -, print this usage block to stderr and exit non-zero:

Error: /trekcontinue --cleanup requires <project-dir>.
Usage:
  /trekcontinue --cleanup <project-dir>            # dry-run: list stale files
  /trekcontinue --cleanup --confirm <project-dir>  # actually delete (status: completed)

Compute mode from parsed flags:

dryRun  = (flags['--confirm'] !== true)
confirm = (flags['--confirm'] === true)

Invoke cleanup inline. Emit the concrete project-dir path as a literal token in the Bash command — never a template placeholder — same anti-substitution rule as Phase 2:

node --input-type=module -e "import {cleanupProject} from '${CLAUDE_PLUGIN_ROOT}/lib/util/cleanup.mjs'; const [, dir, mode] = process.argv; const r = cleanupProject(dir, {dryRun: mode !== 'confirm', confirm: mode === 'confirm'}); console.log(JSON.stringify(r, null, 2)); process.exit(r.valid ? 0 : 1)" '<RESOLVED-PROJECT-DIR>' '<MODE>'

Substitute <RESOLVED-PROJECT-DIR> with the literal positional[0] value you have in your working context, and <MODE> with either the literal string dryrun or the literal string confirm based on the booleans above. The validator emits a {valid, errors, warnings, parsed} JSON record. Print it to stdout. Exit with the validator's exit code.

Cleanup is a terminal mode. It must not fall through to Phase 1/2/3/4. Operators who want to resume after cleanup must invoke /trekcontinue again without --cleanup.

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: /trekcontinue <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 /trekbrief or /trekplan.
    
  • 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 /trekcontinue <project-dir>.)

Phase 1.5 — Frontmatter consistency check

Bug 3 contract: producers (/trekexecute, /trekendsession) write NEXT-SESSION-PROMPT.local.md with YAML frontmatter (produced_by:, produced_at:). Multiple producers may have written candidates in different locations; this phase refuses ambiguity before validating the state file.

After resolving the project directory and state-file path, look for two NEXT-SESSION-PROMPT.local.md candidates:

a. NEXT-SESSION-PROMPT.local.md at the repo root (git rev-parse --show-toplevel) — operator-managed master file, written by the operator or an external producer, never by Voyage b. <project-dir>/NEXT-SESSION-PROMPT.local.md — producer-written sibling

If both exist:

  • Read both via the Read tool (NOT Bash — same anti-substitution rule as Phase 2).

  • Invoke the consistency validator with the paths emitted as concrete literal tokens (no template substitution at the Bash boundary). When the project's .session-state.local.json exists, pass it with --state-file (else run the same line without the option):

    node ${CLAUDE_PLUGIN_ROOT}/lib/validators/next-session-prompt-validator.mjs --json --consistency <RESOLVED-PATH-A> <RESOLVED-PATH-B> --state-file <RESOLVED-PATH-STATE>
    

    Replace <RESOLVED-PATH-A> and <RESOLVED-PATH-B> with the two concrete filesystem paths you have in your working context, and <RESOLVED-PATH-STATE> with the concrete path of the project's .session-state.local.json. The validator emits {valid, errors, warnings} JSON on stdout.

  • If valid: false (typically NEXT_SESSION_PROMPT_PRODUCER_MISMATCH): print the structured errors[] (each [code] message on its own line), list both candidate paths, and exit non-zero. Do NOT proceed to Phase 2. Resolve the conflict by deleting the stale candidate (run /trekcontinue --cleanup --confirm <project-dir> after the current session closes, or remove by hand).

  • If valid: true with a NEXT_SESSION_PROMPT_WALL_CLOCK_DRIFT warning (one of the candidates is more than 24h old): print the warning to stderr but continue — long pauses (weekend, vacation) are not failures.

  • If valid: true with a NEXT_SESSION_PROMPT_STALE_IGNORED warning (one candidate is older than the state file's updated_at): print the warning and continue. The state-anchored check is the primary refusal signal; staleness simply rejects the older candidate.

If only one exists: continue to Phase 2. No comparison needed.

If neither exists: continue to Phase 2. Legacy projects and first-run flows have no NEXT-SESSION-PROMPT files.

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 ${CLAUDE_PLUGIN_ROOT}/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 ${CLAUDE_PLUGIN_ROOT}/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 trekplan pipeline. The user did not type a separate "start" command — /trekcontinue 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 one record to trekcontinue-stats.jsonl in the plugin data directory. Run it exactly like this. The record goes on stdin, and the writer resolves the directory: CLAUDE_PLUGIN_DATA, which the Bash tool env does not carry, else the directory scripts/yardstick.mjs reads.

node ${CLAUDE_PLUGIN_ROOT}/lib/stats/stats-append.mjs trekcontinue <<'JSON'
{"ts":"<iso-8601>","project":"<project>","next_session_label":"<label>","status":"<status>"}
JSON

The writer prints {"written": true, ...}. On "written": false (exit 1), print its reason in one line and continue. A tracking failure never blocks the main workflow, but it is reported, not skipped silently.

Profile (v4.1) — inheritance from plan-frontmatter

Accepts --profile <name> where <name> is economy, balanced, premium, or a custom profile under voyage-profiles/. Default: premium.

Unlike the other voyage commands, /trekcontinue defaults to inheritance from the resumed plan's frontmatter profile: field rather than to premium. This keeps a multi-session run consistent across resumptions.

Resolution order (per lib/profiles/resolver.mjs resolveTrekcontinueProfile):

  1. --profile flag (source: flag) — explicit override, emits stderr advisory [voyage] profile inheritance overridden by --profile flag: <plan-profile> → <flag-profile>
  2. VOYAGE_PROFILE env-var (source: env)
  3. Plan-frontmatter profile: field (source: inheritance) — typical case
  4. premium default for v4.0-style plans without profile: (source: default)

The inherited profile drives phase_models.continue for the next session's orchestration. Operators who explicitly want to switch profiles mid-run pass --profile <name> and accept the advisory (e.g. drop from premium to balanced mid-run to save cost on later sessions).

Examples:

/trekcontinue                                  # inherits from plan.md
/trekcontinue --profile balanced               # explicit override + advisory
VOYAGE_PROFILE=economy /trekcontinue           # env-var override

Hard rules

  • Idempotent. Running /trekcontinue 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 0–4 must run without AskUserQuestion. This keeps the command headless-safe.