9.5 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), exactly as written: the arguments reach the parser as the one
line inside the quoted heredoc, on stdin, never as shell words:
setopt nonomatch 2>/dev/null || true
node ${CLAUDE_PLUGIN_ROOT}/lib/parsers/arg-parser.mjs --command trekendsession --stdin <<'VOYAGE_ARGS_EOF_7F3C9E'
$ARGUMENTS
VOYAGE_ARGS_EOF_7F3C9E
The setopt nonomatch line is no longer needed for the arguments (the quoted
heredoc expands no glob); it stays harmless.
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.
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, ORpositional[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.mdfound 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
cdinto 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 the parser's tail (the rest of the text as typed after
the brief path, one outer quote pair removed, so a label with spaces stays
whole). 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).
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, exactly as written: put no
value from Phase 1 or Phase 2 into it. This is NOT an eager-exec block — the
files do not exist at command-load time. The block takes nothing on its shell
line: it re-reads the arguments as the one line inside the quoted heredoc, on
stdin, never as shell words (the heredoc is the last thing in the command),
parses them with the same parseArgs as Phase 0 (the brief path is
positional[0], the label is tail), and re-derives the project directory by
Phase 1's enumeration: exactly one .claude/projects/<dir>/brief.md under the
cwd, else it prints why and exits 1. The import paths 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 { existsSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
import { atomicWriteJson } from '${CLAUDE_PLUGIN_ROOT}/lib/util/atomic-write.mjs';
import { parseArgs } from '${CLAUDE_PLUGIN_ROOT}/lib/parsers/arg-parser.mjs';
let text = readFileSync(0, 'utf8');
if (text.endsWith('\\n')) text = text.slice(0, -1);
if (text.endsWith('\\r')) text = text.slice(0, -1);
const args = parseArgs(text, 'trekendsession');
const brief = args.positional[0] || '';
const label = args.tail;
if (brief === '' || label === '') { console.error('Error: missing required args.'); process.exit(1); }
const root = '.claude/projects';
const found = existsSync(root) ? readdirSync(root).filter((d) => existsSync(path.join(root, d, 'brief.md'))).map((d) => path.join(root, d)) : [];
if (found.length === 0) { console.error('no .claude/projects/ directory with a brief.md found under cwd; cannot determine project directory'); process.exit(1); }
if (found.length > 1) { console.error('more than one project directory holds a brief.md; cd into the intended one and retry:\\n' + found.join('\\n')); process.exit(1); }
const dir = found[0];
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);
" <<'VOYAGE_ARGS_EOF_7F3C9E'
$ARGUMENTS
VOYAGE_ARGS_EOF_7F3C9E
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.