---
name: ultraplan-end-session-local
description: Mark the current session as complete and write session-state pointing at the next session. Helper for informal multi-session flows.
argument-hint: "<next-brief-path> <next-label> | --help"
model: 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):

```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/<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: /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:

```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 `<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:

```bash
!`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:

```bash
!`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.
