Step 7 of v3.4.1 plan.
ultraplan-end-session-local Phase 3:
- Replace require()-of-ESM-module shim with node --input-type=module + import.
- Convert Phase 1 project enumeration to ESM as well so the file is uniformly
ESM (grep -c 'require(' commands/ultraplan-end-session-local.md → 0).
- Combined ESM block writes both .session-state.local.json (atomicWriteJson)
and sibling NEXT-SESSION-PROMPT.local.md (writeFileSync) so producers
succeed or fail together.
- Sibling markdown gets frontmatter: produced_by, produced_at, project.
ultraexecute-local Phases 8 / 2.55 / 4:
- Each phase that writes .session-state.local.json now also writes a sibling
NEXT-SESSION-PROMPT.local.md with frontmatter (produced_by:
ultraexecute-local, produced_at: ISO-8601, status). Phase 8 includes the
full ESM block; 2.55 / 4 reference the combined pattern.
- This is the producer side of the Bug 3 contract; consumer-side wire-up
(Phase 1.5 consistency check in /ultracontinue) lands in Step 8.
Tests: 346 green (no new tests this step — coverage comes via Step 8
integration test).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
6.7 KiB
| name | description | argument-hint | model |
|---|---|---|---|
| ultraplan-end-session-local | 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 | sonnet |
Ultraplan 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 /ultracontinue can resume in a fresh Claude chat.
For formal flows (a plan produced by /ultraplan-local --project),
/ultraexecute-local 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
/ultraexecute-local.
Pipeline position:
... session N work ...
/ultraplan-end-session-local <brief> "<next-label>" → writes state
... session boundary, fresh chat ...
/ultracontinue → 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.
/ultraplan-end-session-local — Mark current session done; point at next session.
Usage:
/ultraplan-end-session-local <next-brief-path> <next-label>
/ultraplan-end-session-local --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 /ultracontinue will show in the next session.
Example:
/ultraplan-end-session-local .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 $ARGUMENTS. Both <next-brief-path> and <next-label> are required.
If either is missing or empty:
Error: missing required args.
Usage: /ultraplan-end-session-local <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 /ultracontinue --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: ultraplan-end-session-local,
produced_at: <ISO-8601>, project: <project-dir>). Both files are written
in a single ESM block so the writes succeed or fail together:
!`node --input-type=module -e "
import path from 'node:path';
import { writeFileSync } from 'node:fs';
import { atomicWriteJson } from './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: ultraplan-end-session-local\\nproduced_at: ' + now + '\\nproject: ' + dir + '\\n---\\n\\n# ' + label + '\\n\\nResume via /ultracontinue.\\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:
!`node lib/validators/session-state-validator.mjs --json <project-dir>/.session-state.local.json`
If valid: true, print the success block matching /ultracontinue 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 /ultracontinue 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.