voyage/commands/trekendsession.md
Kjell Tore Guttormsen 8d39e1d4a5 fix(trekendsession): release v5.9.1 - eager-exec blocks crashed command load
Phase 3 (atomic-write) and Phase 4 (validator) used !`...` eager-exec
with unresolved runtime placeholders; the harness executes those at
command LOAD time, so zsh parsed <project-dir> as input redirection and
/trekendsession aborted before the model saw a single instruction.
Both blocks are now plain runtime Bash fences with {curly} placeholders
(shell-inert, trekplan.md convention) and absolute ${CLAUDE_PLUGIN_ROOT}
paths (cwd-relative plugin paths were a latent ERR_MODULE_NOT_FOUND in
any user repo). Phase 1 discovery block keeps its legitimate eager-exec.

Regression guard: new tests/commands/trekendsession.test.mjs flags any
!`-block in commands/*.md containing <angle>/{curly} placeholders, and
pins Phase 3/4 as runtime Bash. Suite 828 -> 832 (830/0/2). E2E: fixed
blocks run with real values write both state files, validator valid:true;
trekcontinue.md:147 runtime-verified self-contained.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013NXzF3F2zAM8S7FCBXCqAb
2026-07-03 01:11:02 +02:00

7.2 KiB

name description argument-hint
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

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

If $ARGUMENTS contains --help or -h, print the usage block below and exit cleanly. Do NOT proceed to any further phase.

/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 $ARGUMENTS. Both <next-brief-path> and <next-label> 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.
  • Idempotent within a session. Running twice with the same args overwrites cleanly (atomic rename); does not double-advance.