Session End Skill
Platform Note: State files (STATE.md, wave-scope.json) live in the platform's native directory:
.claude/(Claude Code),.codex/(Codex CLI),.cursor/(Cursor IDE), or.pi/(Pi). All references to.claude/below should use the platform's state directory. Shared metrics live in.orchestrator/metrics/. Seeskills/_shared/platform-tools.md.
Project-instruction file:
CLAUDE.mdandAGENTS.md(Codex CLI) are transparent aliases — see skills/_shared/instruction-file-resolution.md. All references toCLAUDE.mdin this skill resolve via that precedence rule.
Phase 0: Bootstrap Gate
Read skills/_shared/bootstrap-gate.md and execute the gate check. If the gate is CLOSED, invoke skills/bootstrap/SKILL.md and wait for completion before proceeding. If the gate is OPEN, continue to Phase 1.
Phase 0.5: Parallel-Aware Preamble
Skip silently when
persistence: falsein Session Config.
Before Phase 1, run the parallel-aware preamble per skills/_shared/parallel-aware-preamble.md. The preamble detects other active sessions in the worktree-family via findPeers(repoRoot, { mySessionId }), classifies the caller's mode via classifyMode(callerMode) against the exclusivity-matrix, and fires the appropriate AUQ on conflict.
Outcome handling:
PASS_THROUGH→ continue to Phase 1EXCLUSIVE_BLOCKED→ exit Phase 0 cleanly per the AUQ outcomePROMOTION_OFFER→ user picks Worktree-Promotion (seeparallel-aware-auq.mdoutcome-handling — callsenterWorktree()), in-place + Deviation, or Abbrechen
For session-end specifically: the preamble is DETECTION-ONLY. The lock-release path in later phases keeps its current behavior — releasing the OWN session's lock requires no matrix consultation.
Implementation reference: skills/_shared/parallel-aware-preamble.md § Implementation.
AUQ reference: skills/_shared/parallel-aware-auq.md.
Phase 1: Plan Verification
Read back the session plan that was agreed at the start. For EACH planned item:
1.1 Done Items
- Verify with evidence: read the changed files, check git diff, run relevant test
- Confirm acceptance criteria are met
- Mark as completed
1.2 Partially Done Items
- Document what was completed and what remains
- Create a VCS issue for the remaining work with:
- Title:
[Carryover] <original task description> - Labels:
priority:<original>,status:ready - Description: what's done, what's left, context for next session
- Title:
- Link to original issue if applicable
1.3 Not Started Items
- Document WHY (blocked? de-scoped? out of time?)
- If still relevant: ensure original issue remains
status:ready - If no longer relevant: close with comment explaining why
1.3a Optional /goal Backlog-Drain (opt-in — #636)
Advisory-only continuation anchor at the session-end backlog seam. Never auto-invokes
/goal, never blocks the close./goalis a user slash-command; the operator decides whether to drain now or carry over.
Gate conditions — ALL must be true for this nudge to surface:
goal-integration.enabled: truein Session Config (default:false).session-end-backlogis listed ingoal-integration.seams.
When any gate condition is false, skip this step silently — no surfaced suggestion, no STATE.md write, no AUQ.
What it does — when the gate fires AND ≥1 still-relevant Not-Started (§1.3) or Partially-Done (§1.2) item exists AND the operator would rather drain the backlog now than carry it to a future session, surface ONE suggested /goal command as an advisory bullet. Example:
/goal Drain the remaining backlog items <list>; done when each item's acceptance check passes as shown by 'npm test' output in this turn AND 'npm run typecheck' prints 0 errors in this turn, or stop after 20 turns.
Advisory-only contract: this step never auto-invokes /goal, never blocks the close, raises no AskUserQuestion, and writes nothing to STATE.md. It is informational prose only — the operator copies the command if they want it. The deterministic Phase 2 Quality Gate of session-end remains the completion authority: /goal keeps the loop alive across turns, but npm test / npm run typecheck / npm run lint and their exit codes decide whether the drained work is correct.
The /goal evaluator reads the transcript only and runs NO tools — it anchors CONTINUATION, never JUDGMENT. The suggested condition therefore references freshly-run gate output "in this turn's output" and embeds a bound ("or stop after N turns"). Cross-reference .claude/rules/loop-and-monitor.md § LM-008 for the full /goal continuation-vs-judgment contract rather than restating it here.
One goal per session: only ONE /goal can be active at a time. This backlog seam and the inter-wave fix-loop seam (wave-loop.md § /goal Continuation Anchor) cannot both hold an active goal simultaneously — the operator picks one.
1.4 Emergent Work
- Tasks that were NOT in the plan but were done (fixes, discoveries)
- Document and attribute to relevant issues
- If new issues were identified: create them on the VCS platform
1.5 Discovery Scan (if enabled)
Read skills/session-end/discovery-scan.md for embedded discovery dispatch and findings triage.
1.6 Safety Review
Skip if
persistenceisfalsein Session Config (STATE.md won't exist).
Review safety metrics from the session. This is informational — it does NOT block the session close.
Read
<state-dir>/STATE.mdto extract:- Circuit breaker activations: agents that hit maxTurns (
PARTIAL), agents that spiraled (SPIRAL), agents that failed (FAILED) - Worktree status: which agents used worktree isolation, any fallbacks or merge conflicts
- Circuit breaker activations: agents that hit maxTurns (
Read enforcement hook logs from stderr (if captured): count of scope violations blocked/warned, command violations blocked/warned
Summarize:
Safety review: - Agents: [X] complete, [Y] partial (hit turn limit), [Z] spiral/failed - Enforcement: [N] scope violations, [M] command blocks - Isolation: [K] agents in worktrees, [J] fallbacksIf any agents were
SPIRALorFAILED, ensure carryover issues exist (cross-reference with Phase 1.2)Carryover validation fallback (#261): Walk each Wave History entry in STATE.md. For every agent whose status is
SPIRALorFAILED, check whether the line ends with a→ issue #NNNsuffix (or→ existing #NNN). If the suffix is absent, the auto-create call in wave-executor did not run (e.g. a consumer-project #251 V0.x.y-close incident where the session crashed before dispatch completed, or the CLI was offline at detection time). Retroactively file the carryover viacreateSpiralCarryoverIssue:import { createSpiralCarryoverIssue } from '${PLUGIN_ROOT}/scripts/lib/spiral-carryover.mjs'; // For each SPIRAL/FAILED agent missing the "→ issue #NNN" suffix: const result = await createSpiralCarryoverIssue({ taskDescription: '<agent task from Wave History>', kind: 'SPIRAL', // or 'FAILED' context: '<Deviations / error context from STATE.md>', priority: 'high', vcs: '<from Session Config>' }); // result.created → note new issue id in Final Report under "New Issues Created" // result.skipped === 'duplicate' → an earlier session already filed one; record the existing id // result.skipped === 'error' → log in Final Report as "⚠ carryover filing failed for <task>: <error>" and continue (do NOT block close)The module is idempotent via its task-hash dedup marker, so re-running the fallback across sessions will not create duplicates.
1.6.6 Record "What Not To Retry" entries (#623)
Skip if
persistenceisfalse(STATE.md won't exist).
For every SPIRAL or FAILED agent surfaced in the walk above, ALSO append a cross-session "What Not To Retry" entry to STATE.md. This is the durable, human-readable continuity slot that the NEXT session-start surfaces as a forced-read block (session-start Phase 6.5.1) so a future session does not re-attempt the same failed approach. Unlike a carryover issue (which captures unfinished work), this captures the approach that should not be repeated.
import { appendWhatNotToRetryOnDisk } from '${PLUGIN_ROOT}/scripts/lib/state-md.mjs';
// `parsed` = parseStateMd(STATE.md); session id from the `session:` frontmatter field.
const sessionId = parsed.frontmatter.session ?? 'unknown-session';
const today = new Date().toISOString().slice(0, 10); // YYYY-MM-DD
// For each SPIRAL/FAILED agent from the Wave History walk:
await appendWhatNotToRetryOnDisk(repoRoot, {
approach: '<agent task description from Wave History>',
why_failed: '<SPIRAL|FAILED> — <one-line context from Deviations / error>',
session_id: sessionId,
date: today,
});
The helper is lock-guarded (PSA-005) and prunes the section FIFO to the 10 most-recent entries on each append. Optional coordinator entry: if the session abandoned an approach for reasons NOT captured by a SPIRAL/FAILED agent (e.g. a design that proved unworkable mid-session), the coordinator MAY add a free-text entry through the SAME appendWhatNotToRetryOnDisk helper with a descriptive approach + why_failed. Recording is informational and does NOT block the close.
1.7 Metrics Collection
Read skills/session-end/metrics-collection.md for JSONL schema and conditional field rules.
1.8 Session Review
Dispatch the session-reviewer agent to verify implementation quality before the quality gate:
On Codex CLI, dispatch via the
session-revieweragent role defined in.codex-plugin/agents/session-reviewer.toml.
- Invoke
subagent_type: "session-orchestrator:session-reviewer"with:- Scope: all files changed this session (from
git diff --name-onlyagainst the base branch) - Context: the session plan (issues, acceptance criteria) and all wave results from STATE.md
- Scope: all files changed this session (from
- Wait for the reviewer's Verdict:
PROCEED — continue to Phase 2
FIX REQUIRED — disposition each listed item by severity:
Finding class Disposition HIGH+ / blocking review finding Fix inline if quick (<2 min); else create an issue ( priority:high,status:ready) and note it in the Final ReportMED / LOW review finding Fold in-session if quick; else record under "Unresolved Review Findings" in the Final Report — DO NOT create an issue (#617) Planned-carryover (item was in the plan, not finished) ALWAYS create a [Carryover]issue per Phase 1.2 — unchangedSPIRAL / FAILED agent carryover ALWAYS file via createSpiralCarryoverIssueper Phase 1.6 — unchanged
1.9 Mission-Status Classification (when mission-status present in STATE.md)
Skip if
persistenceisfalsein Session Config, or ifmission-status:is absent from STATE.md frontmatter. When absent, fall back to binary checkbox detection in 1.1–1.4 unchanged — full backward compat.
When STATE.md frontmatter contains a mission-status: array (set by session-plan + wave-executor per #340), use the enum values to classify items into the 1.1–1.4 buckets. Read the array via parseMissionStatus(frontmatter) from scripts/lib/state-md.mjs.
Classification mapping:
status: completed→ 1.1 Done Items (item finished; verify with evidence per 1.1)status: testingorstatus: in-dev→ 1.2 Partially Done (carryover; document what remains)status: validatedorstatus: brainstormed→ 1.3 Not Started (carryover; check if still relevant)- Items NOT present in the
mission-status:array → fall back to binary checkbox detection per 1.1–1.4 unchanged
Backward compat: When mission-status: is absent from STATE.md (pre-#340 STATE.md files, or sessions where session-plan did not emit the block), behave exactly as before — enum classification is skipped entirely and 1.1–1.4 binary checkbox logic runs as the sole classification mechanism.
1.10 Mission Status Breakdown (when mission-status present)
Skip if
mission-status:is absent from STATE.md frontmatter (backward compat — no breakdown emitted).
After classifying items in Phase 1.9, produce a Mission Status breakdown subsection as part of the closed/carryover summary output. Count the number of tasks at each enum value across ALL waves:
### Mission Status Breakdown
- completed: <N> tasks
- testing: <N> tasks
- in-dev: <N> tasks
- validated: <N> tasks
- brainstormed: <N> tasks
- Total: <N> tasks across <W> waves
Rules:
- Count each task-id entry from the
mission-status:frontmatter array by its currentstatusvalue. completedmaps to Phase 1.1 (Done).testing+in-devmap to Phase 1.2 (Partial).validated+brainstormedmap to Phase 1.3 (Not Started).- Include this block in the Phase 6 Final Report under
### Carried Overor as a standalone subsection immediately after the Completed/Carried Over/New Issues lists. - When all tasks are
completed, the breakdown still appears (confirms clean session state).
Phase 2: Quality Gate
Verification Reference: See
verification-checklist.mdin this skill directory for the full quality gate checklist.
Run ALL checks listed in the verification checklist. If any check fails: fix if quick (<2 min), otherwise create a priority:high issue. Do NOT commit broken code.
Phase 2.0a: Echo-Stub Detection (GH #42)
gate-full.mjs emits a top-level stubbed: {} map in its JSON result, keyed by check name (typecheck, test, lint); value is { kind: 'echo'|'noop' }. When any check was short-circuited as a stub, runCheck() already returned status: 'pass' — so the overall gate verdict is green, but the result is meaningless.
Detection: immediately after parsing the gate-full JSON result, evaluate:
const stubbedEntries = Object.entries(result.stubbed ?? {});
If stubbedEntries.length > 0, surface a HIGH WARN block in the close summary:
⚠ QUALITY GATE STUBBED — <N> command(s) are echo/noop stubs, not real checks:
- <check-name>: <kind> stub (configured: "<command string>")
Re-configure with a real test command in CLAUDE.md Session Config before /close,
OR document this exception in /close --reason.
Behavior by enforcement mode:
enforcement: strict— block /close. Treat as a Phase 2 failure. Present the WARN block and exit without committing.enforcement: warn(default) — continue, but writequality-gate-stubbed: trueto STATE.md Deviations so the metrics writer captures it.enforcement: off— silent. Emit a single-linestderrlog only (echo-stub detected: <check-name>).
Recipe: for container-based test runners (e.g. EspoCRM PHPUnit) where an echo-stub was the historical workaround, see docs/recipes/quality-gate-container-pattern.md.
Source issue: GH #42 (root cause: a consumer-project #251 V0.15.7-close incident — silent false-positive close-verdicts from echo-stub test commands).
2.1 Vault Validation (if configured)
Read skills/session-end/vault-operations.md for validator bash contract and reporting matrix.
2.2 CLAUDE.md (or AGENTS.md) Drift Check (if configured)
Read skills/session-end/drift-operations.md for checker bash contract and reporting matrix. Complements 2.1: vault-sync validates frontmatter inside the vault tree; drift-check validates narrative claims (paths, counts, issue refs, session-file refs) in top-level repo docs.
2.3 Vault Staleness Check (if configured)
Skip this subsection if
vault-staleness.enabledis nottrue(default:false).
Step 1 — Resolve mode
Read vault-staleness.mode from $CONFIG (default: warn). Valid values: off | warn | strict.
If mode === 'off', skip Phase 2.3 entirely.
Step 2 — Invoke staleness probes
Both probes already ship in skills/discovery/probes/. Invoke each via Node import (no shell-out):
import { runProbe as runStaleness } from '$REPO_ROOT/skills/discovery/probes/vault-staleness.mjs';
import { runProbe as runNarrative } from '$REPO_ROOT/skills/discovery/probes/vault-narrative-staleness.mjs';
const projectStaleness = await runStaleness(projectRoot, config);
const narrativeStaleness = await runNarrative(projectRoot, config);
Each probe returns { findings: Array, metrics: Object, duration_ms: Number } and auto-appends a JSONL summary record to its respective metrics file.
Step 3 — Aggregate and route by mode
totalFindings = projectStaleness.findings.length + narrativeStaleness.findings.length
mode === 'warn'(default): report findings to closing report Docs Health line. Never block close.mode === 'strict':- If
totalFindings === 0: continue, logVault staleness: clean (mode=strict). - If
totalFindings > 0: BLOCK the close. Present the findings list and offer override:- On Claude Code: AskUserQuestion with options:
- "Fix and retry Phase 2.3" (Recommended) — exit close, let user investigate
- "Override and close" — proceed, log a Deviation entry in STATE.md
## Deviations:- [<ISO timestamp>] Phase 2.3: Vault staleness strict-mode findings overridden by user. Findings: <count> (projects: <N>, narratives: <M>). - "Abort close" — exit close without writing
- On Codex CLI / Cursor IDE: same options as numbered Markdown list.
- On Claude Code: AskUserQuestion with options:
- If
Step 4 — Surface to closing report
Pass the aggregated counts and mode forward to Phase 6 Final Report (Docs Health line — see Phase 6 below).
Phase 2.5: Custom Phases (#637)
Opt-in. Skip this phase entirely if
custom-phasesin$CONFIGis absent or[](the default).
Repos declare deterministic close/housekeeping phases as a contract (not the freeform special: convention): each phase runs a command with exit-code gating and Final-Report reporting. The block is parsed by scripts/lib/config/custom-phases.mjs; each record is { name, when, command, mode, review } (already validated — unsafe records were dropped at parse time).
Step 1 — Read + filter by when
Read custom-phases from $CONFIG and the session-type from STATE.md frontmatter (feature | deep | housekeeping | none):
- If
session-type === 'housekeeping': keep phases withwhen ∈ {housekeeping, both}. - Otherwise (
feature/deep/any other): keep phases withwhen ∈ {session-end, both}.
If no phases remain after filtering, skip to Phase 3.
Step 2 — Run each phase in declaration order
For each kept phase:
mode === 'off'⇒ skip silently (do not run the command).- Otherwise run
commandvia Bash. Capture the exit code and the last ~10 lines of stdout (these become the report summary — do NOT inline the full output). - If
reviewis set, read that file after the command as the review step and note its path in the report.
Step 3 — Route by mode
mode === 'warn'(default): record the result (name, exit code, summary) for the Phase 6 Final Report "Custom Phases" line. Never block the close — even on a non-zero exit.mode === 'hard':- exit code
0⇒ continue; record<name>: pass (mode=hard). - exit code
≠ 0⇒ BLOCK the close using the same routing pattern as Phase 2.3 strict-mode. Present the phase name + captured summary and offer override:- On Claude Code: AskUserQuestion with options:
- "Fix and retry Phase 2.5" (Recommended) — exit close, let the user investigate.
- "Override and close" — proceed, log a Deviation entry in STATE.md
## Deviations:- [<ISO timestamp>] Phase 2.5: custom-phase '<name>' (mode=hard) exited <code>, overridden by user. - "Abort close" — exit close without writing.
- On Codex CLI / Cursor IDE: same options as a numbered Markdown list.
- On Claude Code: AskUserQuestion with options:
- exit code
A hard-fail (whether overridden or not) ALWAYS appends its result line to STATE.md ## Deviations; warn-mode results do not.
Step 4 — Surface to closing report
Pass each phase result (name, mode, exitCode, summary, review?) forward to the Phase 6 Final Report "Custom Phases" line (see Phase 6 below).
Phase 3: Documentation Updates
Final heartbeat (#590-3) — at Phase 3 entry, refresh the session-lock heartbeat BEFORE the multi-minute close-out chain (vault-mirror, dialectic, durable-commit, metrics). A long-idle deep session may not have had PostToolBatch activity for >4h; without a refresh the 4h-TTL lock would lapse mid-close and appear stale to a concurrent session. Place this call BEFORE Phase 3.8 Session Lock Release (which deletes the lock — refreshing a deleted lock is a no-op). Best-effort: a failure must NOT block the close.
// Final heartbeat (#590-3) — refresh before the multi-minute close-out (vault-mirror, dialectic, durable-commit) // so a long-idle deep session's 4h-TTL lock does not lapse mid-close. // BEFORE Phase 3.8 lock-release (which deletes the lock). import { updateHeartbeat } from 'scripts/lib/session-lock.mjs'; updateHeartbeat({ sessionId, repoRoot: process.cwd() });Skip silently if
persistence: falsein Session Config (no session.lock exists in that mode).
3.0 Defensive Cleanup
Delete <state-dir>/wave-scope.json if it still exists:
rm -f <state-dir>/wave-scope.json
This should have been cleaned up by wave-executor after the final wave, but crashed sessions or interrupted executions may leave it behind. A stale scope manifest from a previous session could incorrectly restrict the next session's enforcement hooks.
3.1 SSOT Files
- Update
STATUS.md/STATE.mdif they exist (metrics, dates, status) - Update
CLAUDE.md(orAGENTS.mdon Codex CLI) if patterns or conventions changed during this session - Check
<state-dir>/rules/— if a new pattern was established, suggest a new rule file
3.2 Docs Verification (docs-orchestrator integration)
Skip this subsection if
docs-orchestrator.enabledconfig is nottrue(default:false). Also skip entirely ifdocs-orchestrator.modeisoff.
Reads docs-tasks from STATE.md frontmatter (written by wave-executor Pre-Wave 1b), computes CHANGED_FILES via git diff --name-only "$SESSION_START_REF..HEAD", and runs a per-task verification loop (outcome: ok/partial/gap). In warn mode logs results non-blocking; in strict mode blocks on any gap and presents an AskUserQuestion override prompt. Emits a ### Documentation Coverage (docs-orchestrator) block for inclusion in the Phase 6 Final Report.
See phase-3-2-docs-verification.md for full details.
3.2a Session Handover (for significant sessions)
If this session made substantial changes, create or update:
<state-dir>/session-handover/doc with: tasks completed, resume point, metrics changed, issues opened/closed- Or update
<state-dir>/STATE.mdwith session digest
3.3 Claude Rules Freshness
Review <state-dir>/rules/ files that are relevant to this session's work:
- Are the rules still accurate after this session's changes?
- Should any rule be updated with new patterns?
- Should a new path-scoped rule be created?
- Suggest changes but DO NOT modify without user confirmation
3.4 Update STATE.md
Ownership Reference: See
skills/_shared/state-ownership.md. session-end is authorized to setstatus: completedplus the optionalupdatedtimestamp (#184), and — as of Phase A of Epic #271 — the 5 Recommendation fields written by Phase 3.7a. No other fields.
Runtime Ordering Note (Epic #271 Phase A): Phase 3.4's
status: completedwrite executes LAST in Phase 3, AFTER Phase 3.7 (sessions.jsonl) and Phase 3.7a (Compute and Write Recommendations). The ordinal position here (3.4) is kept for historical compatibility; the canonical runtime order is3.1 → 3.2 → 3.3 → 3.4a → 3.5 → 3.5a → 3.6 → 3.6.5 → 3.6.7 → 3.6.8 → 3.7 → 3.7a → 3.7b → 3.7c → 3.4. Rationale: Phase 3.7a reads in-memory session metrics and writes the 5 Recommendation fields viaupdateFrontmatterFields; that write must complete BEFORE the STATE.md frontmatter is finalized withstatus: completedso the Recommendation fields are visible to the next session-start while STATE.md is stillstatus: active. Crash-resilience: if/closeaborts between 3.7a and 3.4, STATE.md carriesstatus: active+ Recommendations; session-start Phase 1.5 offers resume (and the banner renders). If the reverse ordering were used (status: completed first), a crash would leavestatus: completedwithout Recommendations — the Reader would silently no-op the banner, losing the handoff.
Gate: Only run if
persistenceis enabled in Session Config and<state-dir>/STATE.mdexists.
- Set frontmatter
status: completed - Record final wave count and completion time in the frontmatter
- Touch
updated: <ISO 8601 UTC>in the frontmatter (issue #184). Usescripts/lib/state-md.mjs→touchUpdatedFieldfor safety:
Silent no-op if the file has no frontmatter.node --input-type=module -e " import {readFileSync, writeFileSync} from 'node:fs'; import {touchUpdatedField} from '${PLUGIN_ROOT}/scripts/lib/state-md.mjs'; const p = '<state-dir>/STATE.md'; writeFileSync(p, touchUpdatedField(readFileSync(p, 'utf8'), new Date().toISOString())); " - Keep the file as a record — do NOT delete it (next session-start reads it)
If STATE.md doesn't exist, skip this subsection.
3.4a Coordinator Snapshot Cleanup (#196)
Pre-dispatch snapshots (refs/so-snapshots/<sessionId>/wave-*) are created by wave-executor before each wave dispatch so that session-start can offer recovery if a session is interrupted mid-wave. On a clean close those snapshots are no longer needed and should be deleted. In addition, orphaned refs from older sessions that were never cleaned up (e.g. after a hard crash) are garbage-collected using an age-based policy (14 days).
Gate: Only run if
persistenceistruein Session Config. Skip entirely when persistence is off (snapshots are never written in that mode).
node --input-type=module -e "
import { listSnapshots, deleteSnapshot, gcSnapshots } from '${PLUGIN_ROOT}/scripts/lib/coordinator-snapshot.mjs';
// Step A: delete this session's snapshots (clean close → we don't need them)
const mine = await listSnapshots({ sessionId: '${SESSION_ID}' });
for (const s of mine) {
const r = await deleteSnapshot({ refName: s.ref });
if (!r.ok) console.error('snapshot cleanup:', r.error);
}
// Step B: GC orphans older than 14 days (non-fatal)
const gc = await gcSnapshots({ olderThanDays: 14 });
console.log(\`snapshot cleanup: deleted \${mine.length} from this session + \${gc.deletedCount} expired orphans (scanned \${gc.scanned}).\`);
"
Failures in either step are logged to stderr but do not block session close — a missed cleanup is self-healing via the 14-day GC on the next session.
This cleanup is the counterpart to the session-start Phase 1.5 recovery prompt: once a session closes cleanly, future sessions must not be offered recovery for its snapshots.
3.5 Session Memory
Gate: Only run if
persistenceis enabled in Session Config AND platform is Claude Code (session memory at~/.claude/projects/is Claude Code-only). Learnings (Phase 3.5a) and metrics (Phase 3.7) still write to.orchestrator/metrics/on all platforms.
- Create
~/.claude/projects/<project>/memory/session-<YYYY-MM-DD>.mdwith:- Frontmatter:
name,description(1-line summary),type: project ## Outcomes— per-issue status (completed / partial / not started) with evidence## Learnings— patterns discovered, architectural insights, gotchas## Next Session— priority recommendations, suggested session type, blockers
- Frontmatter:
- Update
~/.claude/projects/<project>/memory/MEMORY.md:- Under a
## Sessionsheading (create if missing), add:- [Session <date>](session-<date>.md) — <one-line summary>
- Under a
3.5a Learning Extraction + 3.6 Memory Cleanup & Learnings Write
Read skills/session-end/learning-patterns.md for extraction heuristics, confidence updates, passive decay, and JSONL write procedure.
3.6.3 Memory Proposals Collection (#501, F2.1)
Gate: Skip this phase entirely when ANY of:
persistenceisfalsein Session Configmemory.proposals.enabledisfalse(default:true).orchestrator/metrics/proposals.jsonldoes not exist OR contains zero entries
After learnings are written (Phase 3.6) and BEFORE auto-dream dispatch (Phase 3.6.5), collect agent-proposed memory entries written during this session and present them to the operator via AskUserQuestion multiSelect. Approved entries flow to learnings.jsonl with _provenance: agent-proposed@<wave-id>. Rejected entries are archived to .orchestrator/proposals.rejected.log.
The proposals queue is populated mid-session by wave-executor agents calling node scripts/memory-propose.mjs --type ... --subject ... --insight ... --evidence ... --confidence .... The CLI enforces:
- Quota per wave (default 5, configurable via
memory.proposals.quota-per-wave) - Confidence floor (default 0.5, configurable via
memory.proposals.confidence-floor) - Wrong-context guard (CLI exits non-zero when STATE.md
statusis notactive)
Coordinator-direct procedure
Read Session Config:
memory.proposals.enabled(defaulttrue),memory.proposals.quota-per-wave(default 5),memory.proposals.confidence-floor(default 0.5),auto-dream.min-confidence(default 0.5 — issue #566; SECOND gate above the write-timememory.proposals.confidence-floor).Invoke
collectProposalsfromscripts/lib/memory-proposals/collector.mjs, passing the collect-emit confidence floor from Session Config:import { collectProposals } from '${PLUGIN_ROOT}/scripts/lib/memory-proposals/collector.mjs'; const { queue, stats, perWaveSummaries } = await collectProposals({ repoRoot: process.cwd(), // Issue #566: collect-emit confidence floor. Records with // `record.confidence < minConfidence` are dropped from `queue` (but // counted in stats). When the key is absent, defaults to 0.5 via the // `_parseAutoDream` parser. minConfidence: config['auto-dream']?.['min-confidence'], });If
queue.length === 0: logmemory-proposals: queue empty (stats: ${JSON.stringify(stats)})and continue.AUQ pagination logic: partition the queue into FIFO batches of 4 inline:
- Empty queue → silent skip (no AUQ rendered).
- 1-4 items → single multiSelect call with all items as options.
- 5+ items → sequential multiSelect calls in batches of 4 (FIFO order; final batch may have < 4 items).
// Inlined from former scripts/lib/memory-proposals/auq-partition.mjs (PRD F2.2 #502 closed; see #558 M2). const BATCH_SIZE = 4; const batches = []; if (Array.isArray(queue) && queue.length > 0) { for (let i = 0; i < queue.length; i += BATCH_SIZE) { batches.push(queue.slice(i, i + BATCH_SIZE)); } }Then iterate
batchesand emit oneAskUserQuestionper batch withheader: "Memory — Confirm Proposals (Batch N of M)". Option label format:[<type-12>] | <subject-40> | conf=X.XX. Option description:evidence: <first 60 chars of insight>.multiSelect: true.After all batches answered, partition the queue into
approved(any option selected across all batches) andrejected(all unselected).Invoke
writeApprovedandarchiveRejectedfromscripts/lib/memory-proposals/sink.mjs:import { writeApproved, archiveRejected, clearProposalsJsonl } from '${PLUGIN_ROOT}/scripts/lib/memory-proposals/sink.mjs'; const writeResult = await writeApproved({ approved, repoRoot, sessionId }); const archiveResult = await archiveRejected({ rejected, repoRoot, reason: 'user-declined' }); await clearProposalsJsonl({ repoRoot });Log outcome for Phase 6 Final Report:
memory.proposals: <queued> queued → <approved> approved, <rejected> rejected (dropped: <dropped> quota, <below_floor> below-floor).
Failure modes
- If
collectProposalsfails (fs error): log warning⚠ memory-proposals: collect failed (${err}) — skipping, do not block session close. - If
writeApprovedreports errors per-record: log each, but continue (per-record fault isolation per sink contract). - If
clearProposalsJsonlfails: log warning; do not block. The file may be re-collected at the next session-end, idempotent.
Cross-references
- PRD:
docs/prd/2026-05-21-learning-memory-modernization.md§ F2.1 - Modules:
scripts/lib/memory-proposals/{schema,store,collector,sink}.mjs - CLI:
scripts/memory-propose.mjs(agents call this) - Hook:
hooks/pre-bash-memory-propose-audit.mjs(audit trail) - Coordinator AUQ spec:
agents/memory-proposal-collector.md(reference doc) - Sibling phases: 3.6.5 Auto-Dream (#502), 3.6.6 Skill-Applied Judge (#645 L3), 3.6.7 Auto-Dialectic (#506)
- Issue: #501
3.6.5 Auto-Dream Dispatch (#502, F2.2)
Skip this phase if
memory-cleanup-threshold: 0(kill-switch per PRD F2.2). Also skip on non-Claude-Code platforms (memory dir at~/.claude/projects/is Claude Code-only, mirrors Phase 3.5 gate).
After learnings are written (Phase 3.6), determine whether to emit a manual-cadence nudge to run /memory-cleanup --dry-run in the next session. The decision uses MEMORY.md line count and a sessions-since-last-cleanup signal. There is no memory-cleanup agent in the registry, so the historical auto-dream subagent dispatch never fired (see #614) — the nudge replaces it. A manually-run /memory-cleanup --dry-run writes a unified-diff proposal to .orchestrator/pending-dream.md for the session after that to apply via /memory-cleanup --apply-pending.
Read
memory-cleanup-threshold(default 5) andmemory-cleanup-soft-limit(default 180) from$CONFIG.Invoke
shouldDispatchAutoDreamfromscripts/lib/auto-dream.mjs:import { shouldDispatchAutoDream } from '${PLUGIN_ROOT}/scripts/lib/auto-dream.mjs'; import { resolveMemoryDir } from '${PLUGIN_ROOT}/scripts/lib/memory-paths.mjs'; const memoryDir = resolveMemoryDir(); const decision = await shouldDispatchAutoDream({ repoRoot: process.cwd(), memoryDir, threshold: config['memory-cleanup-threshold'] ?? 5, softLimit: config['memory-cleanup-soft-limit'] ?? 180, });If
decision.trigger === false: logauto-dream: not triggered (${decision.reason})and continue. Emit no nudge.If
decision.trigger === true: do not dispatch a subagent — there is nomemory-cleanupagent inagents/, so the historicalAgent({…})dispatch pointed at the agent namememory-cleanup(a subagent type that was never built) and never fired (see #614). Instead, emit a manual-cadence nudge and continue:auto-dream: cadence reached (${decision.reason}) — run /memory-cleanup --dry-run manually in the next session, then apply the proposal with /memory-cleanup --apply-pending.The
shouldDispatchAutoDreamdecision helper andscripts/lib/auto-dream.mjslib stay in use: they compute the signal that drives this nudge and back the manual/memory-cleanuppath (writePendingDream/readPendingDream/applyPendingDream).Record the outcome (skipped / nudge-emitted) so Phase 6 Final Report can surface a line:
auto-dream: manual /memory-cleanup --dry-run recommended (cadence reached) — apply with /memory-cleanup --apply-pending next session.
The pending-dream sidecar at .orchestrator/pending-dream.md is intentionally outside the vault tree — vault-mirror (Phase 3.7) must exclude it from its scope so the proposal survives the session close without being mirrored into 50-sessions/.
Cross-reference: PRD F2.2 acceptance criteria; scripts/lib/auto-dream.mjs API (shouldDispatchAutoDream, readDreamSignals, writePendingDream, readPendingDream, applyPendingDream).
3.6.6 Skill-Applied Judge (#645, L3)
Default OFF. Skip this phase — with NO module import and NO sidecar created — unless BOTH gates pass (evaluated in this order):
config['skill-evolution'].judge !== true→ skip (thejudge:key in the top-levelskill-evolution:block; defaultfalse).persistence === falsein Session Config → skip.When skipped, log
skill-judge: disabled (skill-evolution.judge=false)(orpersistence=false) and return. This is the disabled-path guarantee: with the judge off, only L1 (skill-invocations.jsonl, written by the PreToolUse hook) and L2 (scripts/lib/skill-health/join.mjs) records exist — no judgment, no error, zero L3 code executes. Do NOT importscripts/lib/skill-judge.mjson the disabled path.
After learnings are written (Phase 3.6) and the auto-dream decision is made (Phase 3.6.5), and when the judge is enabled, run a bounded, read-only LLM-judge over this session's selected skills to emit ADVISORY per-skill applied/completed judgments to .orchestrator/metrics/skill-judgments.jsonl.
The #614 distinction (the whole point of L3's Design A): unlike the 3.6.5 / 3.6.7 nudge-only paths — which cannot dispatch a live subagent because the target read-only agents (memory-cleanup, dialectic-deriver) cannot write their own sidecars — L3 performs a LIVE read-only dispatch. This is #614-safe because the read-only skill-applied-judge agent RETURNS JSON and the COORDINATOR writes the sidecar, not the agent. A read-only agent that returns judgments is allowed; a read-only agent that must write a file is the #614 trap.
Advisory-only: the judge output is written with advisory: true (schema-rejected otherwise) and NEVER feeds an auto-action gate — not a sunset decision, not a C2 repair (scripts/lib/skill-evolution/*), not a promotion. Per #645 R9(b) the C2 repair gate stays deterministic; L3 is a signal for humans/dashboards only.
Read
config['skill-evolution'].judge(defaultfalse),config['skill-evolution']['judge-budget-tokens'](default 8000), andpersistence. Apply the two skip gates above.Determine the judged set — only THIS session's selected skills. Read
.orchestrator/metrics/skill-invocations.jsonland collect the distinctskillvalues whosesession_idmatches the current session id. If the judged set is empty,runSkillJudgereturnsstatus: 'empty-input'(no dispatch) — log and continue.Invoke
runSkillJudgefromscripts/lib/skill-judge.mjs, wiring the real dispatch via the DI seam:import { runSkillJudge } from '${PLUGIN_ROOT}/scripts/lib/skill-judge.mjs'; import { appendSkillJudgment } from '${PLUGIN_ROOT}/scripts/lib/skill-judgments-schema.mjs'; import path from 'node:path'; const budgetTokens = config['skill-evolution']['judge-budget-tokens'] ?? 8000; const result = await runSkillJudge({ // Claude Code path: wire the real read-only haiku subagent as dispatchAgent. dispatchAgent: ({ model, prompt, maxTokens }) => Agent({ subagent_type: 'skill-applied-judge', model: 'haiku', prompt, max_tokens: maxTokens }), repoRoot: process.cwd(), sessionId, transcriptTail, // recent session transcript excerpt (UNTRUSTED — fenced by the lib) selectedSkills, // distinct skills from step 2 model: 'haiku', budget: { input: budgetTokens, output: 4000 }, });- Claude Code path:
dispatchAgentwraps the realAgent({ subagent_type: 'skill-applied-judge', model: 'haiku', … }). The agent issandbox-tier: read-onlyand RETURNS one fenced ```json block — it never writes files. - **Cod
- Claude Code path:
…(truncated)