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 0.6: Skill-Invocation Self-Report (#724, C4)
Emit an L1 skill-invocation record for
session-enditself. The PreToolUseSkill-matcher hook only captures skills dispatched via theSkilltool — a prose-invoked skill like this one is invisible to it (verified gap: zerosession-endrows inskill-invocations.jsonldespite many closed sessions). This self-report closes that gap so L2/L3 skill-health has asession-endselection signal. Best-effort, try/catch-silent — it never blocks the close.
try {
const { appendSkillInvocation, DEFAULT_SKILL_INVOCATIONS_PATH } =
await import('${PLUGIN_ROOT}/scripts/lib/skill-invocations-schema.mjs');
const nodePath = await import('node:path');
await appendSkillInvocation(nodePath.join(process.cwd(), DEFAULT_SKILL_INVOCATIONS_PATH), {
timestamp: new Date().toISOString(),
event: 'selected',
skill: 'session-orchestrator:session-end',
session_id: sessionId ?? null, // from session.lock `session_id`, when available
phase: null,
});
} catch { /* self-report is advisory — never block the close */ }
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
- Do NOT file the carryover issue here (#769). Collect a carryover candidate instead — append it to the in-memory candidate list that the Phase 1.65 Handover Alignment Gate consumes. The issue is filed (only if the gate confirms it) in Phase 5 Step 3. Candidate record (JS keys as
routeCandidates/normalizeCandidateread them —source-phase→sourcePhase,origin-issue→originIssue; seeplan-verification.md § Candidate Record Format):{ task: '<original task description>', sourcePhase: '1.2', originIssue: <IID or null>, priority: '<original>', bucket: 'partially-done' }
- The eventual issue keeps the source-specific
[Carryover]template — Title[Carryover] <original task description>, Labelspriority::<original>+status:ready, Description = what's done / what's left / context for next session. - Link to the original issue when applicable (record its IID as
originIssue; a candidate with no origin issue auto-carries per the gate's routing, so nothing planned is silently forgotten).
1.3 Not Started Items
- Document WHY (blocked? de-scoped? out of time?)
- If no longer relevant: close the original issue with a comment explaining why. This is a pre-gate disposition — it files nothing and adds no candidate.
- If still relevant: do NOT touch the original issue here. Append a carryover candidate so the Phase 1.65 gate surfaces it —
{ task: '<item>', sourcePhase: '1.3', originIssue: <original IID>, priority: '<original>', bucket: 'not-started' }. Phase 1.3 files no NEW[Carryover]issue; the candidate's disposition IS the keep-vs-carry decision on the ORIGINAL issue. If the gate carries it → ensure the original remainsstatus:ready; a dropped middle-band 1.3 candidate leaves the original issue unchanged and open (no auto-close in v1).
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)
- Completed emergent work (finished, or already dispositioned into an issue): document and attribute to the relevant issues exactly as today — this path is NOT gated. If a completed emergent fix warrants a follow-up/doc issue, create it immediately (unchanged behavior).
- Unfinished / undispositioned emergent work (at close, neither finished nor already filed as an issue): do NOT file it here. Append a carryover candidate —
{ task: '<emergent item>', sourcePhase: '1.4', originIssue: <IID or null>, priority: '<assessed>', bucket: 'emergent' }. The Phase 1.65 gate decides whether it is filed; a confirmed 1.4 candidate is filed in Phase 5 Step 3 as a normal issue (NOT the[Carryover]template).
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 a carryover candidate is collected for each (they auto-carry; filed via the Phase 1.65 gate → Phase 5 Step 3 — cross-reference with Phase 1.2)Carryover validation fallback (#261) — collect, do NOT file yet (#769): 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). Do NOT callcreateSpiralCarryoverIssuehere — since #769 its firing moves behind the Phase 1.65 Handover Alignment Gate so that NO[Carryover]issue is created before the gate. Instead append an auto-carry candidate (SPIRAL/FAILED is a non-deselectable auto-carry class — the gate only surfaces it in the status count, never as a deselectable option; consistent with the Critical Rule atSKILL.md:853), carrying the payload the deferredcreateSpiralCarryoverIssuecall will need:// #769: collect, don't file. The actual createSpiralCarryoverIssue() call // fires in Phase 5 Step 3 (behind the gate). bucket 'spiral-failed' → auto-carry, // so it is ALWAYS carried; the operator never sees it as a triage option. // For each SPIRAL/FAILED agent missing the "→ issue #NNN" suffix: candidates.push({ task: '<agent task from Wave History>', sourcePhase: '1.6', originIssue: null, // SPIRAL/FAILED safety-net items carry no origin issue priority: 'high', bucket: 'spiral-failed', // Filing payload retained on the coordinator's original candidate object, // consumed in Phase 5 Step 3 (routeCandidates only classifies — it returns // normalized copies and does not carry this annotation): _spiral: { kind: 'SPIRAL' /* or 'FAILED' */, context: '<Deviations / error context from STATE.md>' }, });The deferred Phase-5.3 call imports
createSpiralCarryoverIssuefrom${PLUGIN_ROOT}/scripts/lib/spiral-carryover.mjs; it 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> (evidence: <file:line or path>)',
session_id: sessionId,
date: today,
});
why_failed MUST cite at least one concrete file (and line, if applicable) that grounds the failure — a bare narrative reason without a file reference is not acceptable.
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.65 Handover Alignment Gate (#769)
Opt-in-by-default interactive gate. Reads
handover-gate.enabled(defaulttrue) andhandover-gate.max-open-questions(default3) from parsed Session Config (cfg['handover-gate'], produced byscripts/lib/config.mjs→scripts/lib/config/handover-gate.mjs). Position is load-bearing: it runs AFTER Phase 1.6.6 — so all four candidate sources (1.2 Partially Done, 1.3 Not Started still-relevant, 1.4 unfinished Emergent, 1.6 SPIRAL/FAILED walk) are computed and NOTHING has been filed yet — and BEFORE Phase 1.7, so the gate's carry/drop decision feeds the Phase 1.7 carryover count. This is the ONLY place[Carryover]filing is authorized to originate; Phase 5 Step 3 merely executes the gate's carry-list.
Skill-prose-first, minimal mechanical core — same pattern as Phase 3.6.3 memory-proposals: the coordinator runs the
AskUserQuestioninteraction (per.claude/rules/ask-via-tool.mdAUQ-003); the purescripts/lib/handover-gate.mjslib does only the deterministic classification. No hook, no agent, no new event schema.
Fail-open skip (FA5 — the load-bearing safety decision)
Skip the gate entirely — treat EVERY candidate as carry (byte-identical to the pre-#769 status quo), emitting a single stderr WARN — when ANY of:
cfg['handover-gate'].enabled === false.- session-end runs in an embedded / autopilot context OR headless
claude -p(no operator at the keyboard;AskUserQuestionis unavailable per AUQ-004 — the same embedded-mode precedent as discovery suppressing its AUQ). AskUserQuestionis unavailable or throws at call time (wrap the calls; on error, fail-open — never surface a half-rendered gate).- The candidate list is empty AND STATE.md
## Open Questionshas no unanswered entry — Zero-Friction clean close: emit NO AUQ and continue unchanged.
Fail-open NEVER hangs the close on an unanswerable AUQ and NEVER loses data — it degrades exactly to today's silent-carryover behavior. Log e.g. ⚠ handover-gate: skipped (<reason>) — all candidates carry (status quo).
Telemetry on skip (#773): even when the gate is skipped, emit the orchestrator.handover.gated event ONCE with path: "fail_open" so this never-interactive path is still measurable (the carryover=0 blind spot #773 closed was invisible precisely because skipped closes emitted nothing). Every candidate carries, so auto_carry = candidates_total, asked = 0, dropped = 0, and the three question counts are 0:
node scripts/emit-event.mjs --type orchestrator.handover.gated --payload \
"$(node -e "process.stdout.write(JSON.stringify({candidates_total: CT, auto_carry: CT, asked: 0, dropped: 0, questions_asked: 0, questions_answered: 0, questions_deferred: 0, path: 'fail_open'}))")"
(CT = the in-memory candidate-list length. The Zero-Friction clean-close variant — empty candidates AND no open questions — emits with all counts 0 and path: "fail_open" too, so even the quietest close leaves a breadcrumb.)
Step 1 — Assemble candidates + open questions
The in-memory candidate list is the union of the candidates appended by Phases 1.2 / 1.3 (still-relevant) / 1.4 (unfinished emergent) / 1.6 (SPIRAL/FAILED). Each candidate object carries
{ task, sourcePhase, originIssue, priority, bucket }(plus any filing payload, e.g. the SPIRAL/FAILED_spiralkind/context). Seeplan-verification.md § Candidate Record Format.Classify via the pure helper:
import { routeCandidates } from '${PLUGIN_ROOT}/scripts/lib/handover-gate.mjs'; const { autoCarry, ask } = routeCandidates(candidates);autoCarry=priority::critical|highORbucket === 'spiral-failed'ORoriginIssue === null— non-deselectable (dropping any of these would be real forgetting; consistent with the Critical Rule atSKILL.md:853).ask= the middle-band (prioritymedium/low/none WITH an origin issue, buckets not-started/emergent/partially-done) plus anymalformedrecord.routeCandidatesreturns NORMALIZED copies for gate rendering; the coordinator retains its ORIGINAL candidate objects (with filing payloads) for Phase 5 Step 3.Read STATE.md contents and extract the open questions via the sibling helper:
import { readOpenQuestions } from '${PLUGIN_ROOT}/scripts/lib/state-md.mjs'; const openQuestions = readOpenQuestions(stateMdContents); // Array<{question, source, priority, answered, answer?}> const unanswered = openQuestions.filter((q) => !q.answered);Zero-Friction check: if
autoCarry.length === 0 && ask.length === 0 && unanswered.length === 0, skip per Fail-open above (no AUQ, no WARN needed beyond an info log — clean close).
Step 2 — AUQ Call 1 (Status-Gate)
Render ONE AskUserQuestion. The question text NAMES the candidate counts by class and the open-question count, e.g. "<A> auto-carry + <M> triage candidate(s), <U> open question(s). Close and triage now?". Options (Recommendation first, AUQ-003):
"Closen + Triage (Recommended)" — proceed to AUQ Call 2 (triage the middle-band + answer the top open questions), then file the resulting carry-list in Phase 5 Step 3.
"Alle carryoven (ohne Triage)" — fast-path: carry ALL candidates (
autoCarry ∪ ask) with no triage; SKIP AUQ Call 2; unanswered questions stay- [ ]and roundtrip to the next session. Equivalent to the status quo for filing, minus the friction."Weiterarbeiten (Close abbrechen)" — abort session-end cleanly: NO commit, NO lock-release, NO issue creation; STATE.md stays
status: active; the session remains open and the coordinator continues working the open points. Before stopping, emitorchestrator.handover.gatedONCE withpath: "weiterarbeiten"(#773) — the gate WAS rendered (AUQ Call 1 happened) and the operator chose to keep working, which is a distinct, previously-unmeasured outcome. Nothing is filed, so reportauto_carry = autoCarry.length,asked = ask.length,dropped = 0, and all three question counts0:node scripts/emit-event.mjs --type orchestrator.handover.gated --payload \ "$(node -e "process.stdout.write(JSON.stringify({candidates_total: CT, auto_carry: AC, asked: ASK, dropped: 0, questions_asked: 0, questions_answered: 0, questions_deferred: 0, path: 'weiterarbeiten'}))")"Then print
session-end aborted at Phase 1.65 by user choice (Weiterarbeiten). Session stays open.and STOP the close (do not fall through to Phase 1.7).
(Codex CLI / Cursor IDE: same three options as a numbered Markdown list.)
Step 3 — AUQ Call 2 (Triage + Open Questions) — only after "Closen + Triage"
Combine the Middle-Band triage multiSelect AND up to max-open-questions open-question single-questions, honoring AUQ-003 (≤4 questions/call, ≤4 options/multiSelect):
Middle-Band multiSelect — one multiSelect over the
askcandidates, EVERY option preselected (= carry; one Enter keeps the sensible default). Option label:[<bucket>] <task-truncated> — <priority|—> (origin #<IID|none>).multiSelect: true. Deselected = drop.Batching (Phase 3.6.3 precedent):
0→ no multiSelect;1–4→ a single multiSelect that rides in the SAME first call alongside the open questions;5+→ sequentialBatch N of MmultiSelects in FIFO batches of 4 (header: "Handover — Triage Middle-Band (Batch N of M)").const BATCH_SIZE = 4; const batches = []; for (let i = 0; i < ask.length; i += BATCH_SIZE) batches.push(ask.slice(i, i + BATCH_SIZE));When
ask.length ≤ 4: the single triage multiSelect + up tomax-open-questionsopen-question single-questions all ride in ONE call (1 + 3 = 4 questions max — AUQ-003-safe). Whenask.length > 4: emit the open questions in the FIRST call and the middle-band as ⌈M/4⌉ dedicatedBatch N of Mcalls.
Open questions — up to
max-open-questions(default 3; effectively capped at 3 in the first call = the 4-question limit minus the 1 triage multiSelect) highest-priorityunansweredquestions, each a single-select with 2–4 options (Recommendation first). Derive options from the agent-supplied answer-candidates when present; otherwise offerAnswer: <A> / Answer: <B> / Defer (keep open). Questions beyond the cap stay untouched (- [ ]) and roundtrip (FA3-Semantik).
Step 4 — Apply the gate outcome
carry-list =
autoCarry(always) ∪ the middle-bandaskitems the operator LEFT SELECTED. drop-list = the middle-bandaskitems the operator DESELECTED. ("Alle carryoven"→ carry-list =autoCarry ∪ ask, drop-list = ∅.) Store both for Phase 5 Step 3 (filing) and Phase 6 (report). NOTHING is filed in this phase.Answered open questions — decide + enqueue in-memory only; do NOT mark
[x]yet (#769): for each open question answered in AUQ Call 2, capture the outcome in an in-memoryansweredQuestionslist — one record per answered question:{ question, answer, impliesWork: <bool> }. Do NOT callmarkOpenQuestionAnsweredOnDiskin this phase.The durable STATE.md
- [x]write is deliberately deferred to Phase 5 Step 3 so that it lands on the SAME side of the Quality Gate (Phase 3) as the carryover-issue filing — either a completed close marks the question[x]AND files its implied work, or a Quality-Gate abort does neither. Marking[x]here (at gate time) would silently forget the answer if the Quality Gate later aborts the close: the now-[x]question no longer re-surfaces viareadOpenQuestions().filter(!answered)on re-close, so any implied work would be dropped ticketless — exactly the silent-forget this feature exists to prevent.If the chosen answer implies NEW work (
impliesWork: true), ALSO enqueue it now onto the carry-list as a carry-candidate (originIssue: null→ auto-carry), carrying the answer as body context, so Phase 5 Step 3 files the issue AND marks the question[x]atomically. Pure decisions with no to-do (impliesWork: false) carry no candidate; they are recorded only by the Phase 5.3 STATE.md[x]mark + the Final Report. Unanswered / over-cap questions stay- [ ]and roundtrip to the next session (FA4).The gate's carry/drop split feeds the Phase 1.7 carryover count.
Step 5 — Emit gate telemetry (#773)
After the carry/drop split is settled, emit orchestrator.handover.gated exactly once for the interactive path taken. This is the mechanical producer that makes the gate observable — before #773 the gate decided carry/drop entirely in coordinator prose, so effectiveness.carryover had no mechanical anchor and 41/41 records read carryover: 0 despite real filtering. Derive the payload from the in-memory gate state:
candidates_total=autoCarry.length + ask.lengthauto_carry=autoCarry.length(non-deselectable)asked=ask.length(middle-band candidates surfaced for triage)dropped= drop-list length (middle-band items the operator DESELECTED;0on the"Alle carryoven"fast-path since AUQ Call 2 is skipped)questions_asked/questions_answered/questions_deferred= the open-question counts from AUQ Call 2 (surfaced / answered / left- [ ]and roundtripped). All0on the fast-path.path="triage"(after "Closen + Triage") or"fast_path"(after "Alle carryoven ohne Triage")
node scripts/emit-event.mjs --type orchestrator.handover.gated --payload \
"$(node -e "process.stdout.write(JSON.stringify({candidates_total: CT, auto_carry: AC, asked: ASK, dropped: DROP, questions_asked: QA, questions_answered: QAN, questions_deferred: QD, path: PATH}))")"
The questions_asked / questions_answered / questions_deferred values here are the SAME three counts recorded as the top-level open_questions_asked / open_questions_answered / open_questions_deferred session fields in Phase 1.7 (see metrics-collection.md). Emit the event with the exact scripts/emit-event.mjs --type … --payload … flag signature (NOT a positional argument — see the CLI header).
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) Route as a carryover candidate per Phase 1.2 → the Phase 1.65 gate files it. Never forgotten: a no-origin/critical/high item auto-carries as a [Carryover]issue; a middle-band item with an origin issue is preselected=carry (and its origin issue stays open even if dropped).SPIRAL / FAILED agent carryover Route as an auto-carry candidate per Phase 1.6 → filed via createSpiralCarryoverIssuein Phase 5 Step 3 (non-deselectable; exempt from theissue-budgetcap — the[Carryover] [SPIRAL|FAILED]title and thetype::carryoverlabel bypass it, so a full budget can never swallow this filing)
Override-ratio telemetry (#730/H5): whenever one or more MED/LOW review findings are routed to "Unresolved Review Findings" (rather than fixed), additionally emit a single event capturing how many findings were absorbed rather than resolved — feeding the override_ratio metric:
node scripts/emit-event.mjs --type orchestrator.finding.overridden --payload '{"phase":"1.8","kind":"med-low-review-finding","count":N}'
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: do NOT block the close. Present the findings list and surface an AskUserQuestion whose Recommended default is warn + carryover + continue:- On Claude Code: AskUserQuestion with options:
- "Warn + carryover and close (Recommended)" — file a carryover issue (labels
carryover,priority::high) titled[Carryover] Vault staleness (strict) — <count> findingsdocumenting the stale projects/narratives for a follow-up session, log a Deviation entry in STATE.md## Deviations, then continue the close:- [<ISO timestamp>] Phase 2.3: Vault staleness strict-mode findings carried over. Findings: <count> (projects: <N>, narratives: <M>) → issue #<IID>. - "Override and close" — proceed without a carryover issue, 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>).In addition to the Deviation entry, emit an override-ratio event so the override feeds theoverride_ratiometric (#730/H5):node scripts/emit-event.mjs --type orchestrator.finding.overridden --payload '{"phase":"2.3","kind":"vault-staleness-strict","count":N}'.
- "Warn + carryover and close (Recommended)" — file a carryover issue (labels
- 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.mode: hardhere is an operator-declared repo contract (the repo deliberately chosemode: hard), so the block semantics are preserved — but the AUQ now ALSO offers a warn + carryover escape hatch. Present the phase name + captured summary and offer:- On Claude Code: AskUserQuestion with options:
- "Fix and retry Phase 2.5" (Recommended) — exit close, let the user investigate.
- "Warn + carryover and close" — file a carryover issue (labels
carryover,priority::high) titled[Carryover] custom-phase '<name>' (mode=hard) exited <code>capturing the phase name + captured summary for a follow-up session, log the Deviation entry, then continue the close. - "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.In addition to the Deviation entry, emit an override-ratio event so the override feeds theoverride_ratiometric (#730/H5):node scripts/emit-event.mjs --type orchestrator.finding.overridden --payload '{"phase":"2.5","kind":"custom-phase-hard","count":N}'. - "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 2.6: Broken-Window Budget (#730/H5)
Opt-in via
broken-window-budget.enabledin Session Config (defaultfalse). Skip silently when disabled.
Assemble the in-memory "knowingly-broken shipment" list from THIS session's already-computed results — no new detection logic, only aggregation:
- Phase 2.0a stub findings (
result.stubbed) that shipped anyway underenforcement: warn. - Phase 2.3 / 2.5 "Override and close" choices (reuse each entry's Deviation-log payload verbatim).
- Phase 1.8 MED/LOW findings routed to "Unresolved Review Findings" (#617).
- Wave-level reviewer findings overridden without a fix task (
## Deviationsentries matchingreviewer finding overridden— written by wave-executor §5/5a).
For EACH item: file a hard-terminated closure issue via createBrokenWindowIssue()
from scripts/lib/spiral-carryover.mjs — labels broken-window + priority::high,
due-date = today + broken-window-budget.due-days (default 7; glab native
--due-date, gh fallback: Due: <date> as first body line — GitHub has no
native due-date
…(truncated)