voyage/commands/trekendsession.md

7.9 KiB

name description argument-hint disable-model-invocation
trekendsession Mark the current session as complete and write session-state pointing at the next session. Helper for informal multi-session flows. <next-brief-path> <next-label> | --help true

Voyage End-Session Local v1.0

Tiny helper for informal multi-session flows (no formal plan with Execution Strategy). Writes a .session-state.local.json pointing at the next session so /trekcontinue can resume in a fresh Claude chat.

For formal flows (a plan produced by /trekplan --project), /trekexecute Phase 8 already writes the state file — this helper is unnecessary there. Use this command for ad-hoc release runs, manual multi-session handovers, or any flow that does not run through /trekexecute.

Pipeline position:

... session N work ...
/trekendsession <brief> "<next-label>"   →  writes state
... session boundary, fresh chat ...
/trekcontinue                                         →  reads state, starts session N+1

See Handover 7 in docs/HANDOVER-CONTRACTS.md for the schema.

Phase 0 — --help handling

Parse $ARGUMENTS with the arg parser. Run this via the Bash tool at runtime (NOT eager-exec):

setopt nonomatch 2>/dev/null || true
node ${CLAUDE_PLUGIN_ROOT}/lib/parsers/arg-parser.mjs --command trekendsession -- $ARGUMENTS

It prints the parse result as JSON. On exit 1, print its errors and stop.

Print the usage block below and exit cleanly 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 declares no alias).

Then do NOT proceed to any further phase. In every other case — a slug or a label that holds -h or help too — go on to Phase 1.

/trekendsession — Mark current session done; point at next session.

Usage:
  /trekendsession <next-brief-path> <next-label>
  /trekendsession --help

Both arguments are REQUIRED. No interactive prompt — headless-safe.

Writes <project-dir>/.session-state.local.json with:
  schema_version           1
  project                  <auto-resolved from cwd>
  next_session_brief_path  <next-brief-path argument>
  next_session_label       <next-label argument>
  status                   in_progress
  updated_at               <now, ISO-8601>

Then validates via lib/validators/session-state-validator.mjs and prints
the same 3-line narration that /trekcontinue will show in the next session.

Example:
  /trekendsession .claude/projects/2026-05-01-feature/brief.md "Session 2 of 3"

Phase 1 — Resolve project directory

Resolve the nearest .claude/projects/*/brief.md from cwd (the current working directory). Use node -e enumeration (NOT shell glob — harness-mode safety):

!`node --input-type=module -e "import {existsSync, readdirSync} from 'node:fs'; import {join} from 'node:path'; const root='.claude/projects'; if(!existsSync(root)) process.exit(0); readdirSync(root).filter(d=>existsSync(join(root,d,'brief.md'))).forEach(d=>process.stdout.write(join(root,d)+'\\n'));"`

Decision tree:

  • 0 candidates: print error to stderr — "no .claude/projects/<dir>/brief.md found under cwd; cannot determine project directory" — and exit 1. Do NOT fall back to a synthesized path.
  • 1 candidate: use it as <project-dir>. Continue.
  • >1 candidates: print all paths and ask the operator to cd into the intended project directory before retrying. Exit 1.

Phase 2 — Required args check (headless-safe)

Read the parse result from Phase 0: <next-brief-path> is positional[0] and <next-label> is positional[1]. Both are required. If either is missing or empty:

Error: missing required args.
Usage: /trekendsession <next-brief-path> '<next-label>'

Print to stderr and exit 1. No interactive prompt — this keeps the helper headless-safe (per brief NFR; addresses adversarial-review major #11). If you want an interactive flow, use /trekcontinue --help to see the full pipeline.

Phase 3 — Atomically write .session-state.local.json + sibling NEXT-SESSION-PROMPT.local.md

Write {project_dir}/.session-state.local.json with the schema-v1 object:

{
  "schema_version": 1,
  "project": "{project_dir}",
  "next_session_brief_path": "{arg 1}",
  "next_session_label": "{arg 2}",
  "status": "in_progress",
  "updated_at": "{now, ISO-8601}"
}

Use the atomic-write util — write to <path>.tmp, then rename into place — to avoid partial-state on crash. The util is ESM, so invoke via node --input-type=module -e with an import statement (a CommonJS shim would throw ERR_REQUIRE_ESM on Node 18+ since atomic-write.mjs is ESM).

Under node --input-type=module -e "<script>" arg1 arg2 arg3, Node sets process.argv[0] to the node binary path and user args start at process.argv[1]. Adjust the destructure if your Node version differs.

This phase ALSO writes a sibling NEXT-SESSION-PROMPT.local.md in the project directory with YAML frontmatter (produced_by: trekendsession, produced_at: {ISO-8601}, project: {project_dir}). Both files are written in a single ESM block so the writes succeed or fail together.

Run the block below via the Bash tool at runtime, substituting the resolved values for the {curly} placeholders (Phase 1 gives {project_dir}, Phase 2 gives {next_brief_path} and {next_label}). This is NOT an eager-exec block — the values do not exist at command-load time. The import path must stay absolute via ${CLAUDE_PLUGIN_ROOT} — your Bash cwd is the user's repo, not the plugin root, so a cwd-relative import throws ERR_MODULE_NOT_FOUND:

node --input-type=module -e "
import path from 'node:path';
import { writeFileSync } from 'node:fs';
import { atomicWriteJson } from '${CLAUDE_PLUGIN_ROOT}/lib/util/atomic-write.mjs';
const [, dir, brief, label] = process.argv;
const now = new Date().toISOString();
const stateObj = { schema_version: 1, project: dir, next_session_brief_path: brief, next_session_label: label, status: 'in_progress', updated_at: now };
const stateFile = path.join(dir, '.session-state.local.json');
atomicWriteJson(stateFile, stateObj);
const promptFile = path.join(dir, 'NEXT-SESSION-PROMPT.local.md');
const promptBody = '---\\nproduced_by: trekendsession\\nproduced_at: ' + now + '\\nproject: ' + dir + '\\n---\\n\\n# ' + label + '\\n\\nResume via /trekcontinue.\\n';
writeFileSync(promptFile, promptBody);
console.log(stateFile);
console.log(promptFile);
" '{project_dir}' '{next_brief_path}' '{next_label}'

Phase 4 — Validate + narrate

Validate the freshly-written state file via the Bash tool at runtime, substituting the resolved {project_dir} (NOT eager-exec — the file does not exist at command-load time):

node ${CLAUDE_PLUGIN_ROOT}/lib/validators/session-state-validator.mjs --json {project_dir}/.session-state.local.json

If valid: true, print the success block matching /trekcontinue Phase 3 narration (SC-8 cross-project consistency — same template both sides):

Session state written: {project_dir}/.session-state.local.json

Project: {project_dir}
Next session: {next_label}
Brief: {next_brief_path}

In a fresh Claude session, run /trekcontinue to resume.

If valid: false, print the structured errors[] and exit 1. Investigate before retrying — usually means a bad path or label argument.

Hard rules

  • Required args, no defaults. Never invent a brief path or session label. If args are missing, fail loud.
  • Atomic write only. Tmp + rename — no partial state files on disk.
  • Zero secrets. Status, paths, labels — never API keys, never user content beyond filenames.
  • NEVER auto-invoke this command. It is operator-typed only at session-end (the frontmatter field keeps the model from starting it; the operator types it).
  • Idempotent within a session. Running twice with the same args overwrites cleanly (atomic rename); does not double-advance.