--- name: trekendsession description: Mark the current session as complete and write session-state pointing at the next session. Helper for informal multi-session flows. argument-hint: " | --help" disable-model-invocation: 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 "" → 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: ```bash 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`, 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 /trekendsession --help Both arguments are REQUIRED. No interactive prompt — headless-safe. Writes /.session-state.local.json with: schema_version 1 project next_session_brief_path next_session_label status in_progress updated_at 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): ```bash !`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//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 ``. 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: `` is `positional[0]` and `` 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 '' ``` 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: ```json { "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 `.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//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`: ```bash 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): ```bash 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.