| name | session-close |
| description | Orchestrate session close — Phase 2 issue close + Final Report. Replaces CLAUDE.md WF-CODE-12. Handles both ENFORCE_WORKTREE on (worktree path) and off (branch/main path). |
| user-invocable | true |
Session close orchestrator. Drives issue-close-finalize (when applicable),
collects the outcome JSON written by Step L, and emits the Final Report by
substituting the skeleton from hooks/lib/final-report-schema.renderSkeleton.
Replaces the legacy "Step 7" emit inside /worktree-end so the Final Report
reflects every terminal action.
Pre-flight
AGENTS_CONFIG_DIR must be set.
- Caller context (under
ENFORCE_WORKTREE=on): /worktree-end Steps 1–6i have
already completed (worktree merged and removed; <PLANS_DIR>/<session-id>-final-report-env.json exists).
- Caller context (under
ENFORCE_WORKTREE=off): the PR is merged. No worktree-end
ran; the env file does not yet exist.
Step SC-0 — Resolve PLANS_DIR and session id
PLANS_DIR=$(bash "$AGENTS_CONFIG_DIR/bin/workflow-plans-dir" 2>/dev/null \
|| printf '%s\n' "${WORKFLOW_PLANS_DIR:-$HOME/.workflow-plans}")
printf 'PLANS_DIR=%s\n' "$PLANS_DIR"
Substitute the absolute path for <PLANS_DIR> in every subsequent step.
Resolve <session-id> from $CLAUDE_ENV_FILE (CLAUDE_SESSION_ID) with the
fallback chain used by --from-session. If unresolvable, abort:
session id unresolved — cannot render Final Report.
<PLANS_DIR> and <session-id> are LLM-substituted literals — shell variables
do not persist between Bash tool calls.
Step SC-1a — Detect WF-META session
Run:
node "$AGENTS_CONFIG_DIR/bin/session-close-detect-wf-meta.js" ""
- stdout
yes → WF-META session. Record IS_WF_META=yes as an LLM-tracked state literal (not a shell variable — every Bash call is self-contained). Proceed to SC-2C.
- stdout
no → proceed to SC-1b (ENFORCE_WORKTREE detection).
Step SC-1b — Detect ENFORCE_WORKTREE mode
Check via Bash:
bash -c 'cd "$AGENTS_CONFIG_DIR" && bash "$AGENTS_CONFIG_DIR/bin/confirm-off" ENFORCE_WORKTREE on'
- stdout
ON or ERROR → worktree path (SC-2A).
- stdout
OFF → branch/main path (SC-2B).
Step SC-2A — Worktree path: reuse existing env JSON
test -f "<PLANS_DIR>/<session-id>-final-report-env.json" \
|| { echo "ERROR: env JSON missing — /worktree-end must run first" >&2; exit 1; }
Then write the late-finding alert eligibility flag (#997):
node "$AGENTS_CONFIG_DIR/bin/supervisor-write-alert" --session-id "" --set-alert-eligible-phase post_final_report_window
Proceed to SC-3.
Step SC-2B — Branch/main path: build minimal env JSON
node "$AGENTS_CONFIG_DIR/bin/session-close-build-env.js" "<PLANS_DIR>/<session-id>-final-report-env.json"
Exit 0 → write the late-finding alert eligibility flag (#997):
node "$AGENTS_CONFIG_DIR/bin/supervisor-write-alert" --session-id "" --set-alert-eligible-phase post_final_report_window
Then proceed to SC-3. Non-zero → abort (PR unresolvable).
Step SC-2C — WF-META path: write PR-less env JSON
node "$AGENTS_CONFIG_DIR"/bin/session-close-build-env.js --wf-meta "<PLANS_DIR>/-final-report-env.json"
Exit 0 → write the late-finding alert eligibility flag:
node "$AGENTS_CONFIG_DIR/bin/supervisor-write-alert" --session-id "" --set-alert-eligible-phase post_final_report_window
Record supervisor notice:
node "$AGENTS_CONFIG_DIR/bin/supervisor-report" --categories workflow --severity notice --detail "WF-META session: SC-2C path taken — env JSON written with empty PR fields" --reporter session-close
Retain IS_WF_META=yes and proceed to SC-3.
Step SC-3 — Non-GitHub pre-flight + issue close dispatch
If IS_WF_META=yes (set in SC-2C): write skipped_wf_meta outcomes directly:
node "$AGENTS_CONFIG_DIR"/bin/issue-close-write-outcome.js --wf-meta '<ISSUES_JSON_ARRAY>' "<PLANS_DIR>/-issue-close-outcome.json"
<ISSUES_JSON_ARRAY> is the JSON number array the LLM parses from intent.md via hooks/lib/parse-closes-issues.js, inlined as a literal. When intent.md is absent, use '[]'.
Then skip directly to SC-6. WF-META skips is-github-dotcom-remote and never invokes /issue-close-finalize — skipped_wf_meta is written regardless of remote type.
If IS_WF_META is unset or no: proceed with the is-github-dotcom-remote check below.
bash "$AGENTS_CONFIG_DIR/bin/is-github-dotcom-remote"; echo "NON_GITHUB_RC=$?"
- Non-zero → non-GitHub remote. Write skipped outcomes (pass
'[]' when
closes_issues is empty), then skip to SC-6:
node "$AGENTS_CONFIG_DIR/bin/issue-close-write-outcome.js" \
--non-github '<ISSUES_JSON_ARRAY>' \
"<PLANS_DIR>/<session-id>-issue-close-outcome.json"
<ISSUES_JSON_ARRAY> is the JSON number array the LLM parses from intent.md
via hooks/lib/parse-closes-issues.js, inlined as a literal at substitution time.
- Zero → GitHub remote. Parse
closes_issues from
<PLANS_DIR>/<session-id>-intent.md via the canonical parser.
[] → write empty outcome, skip to SC-6:
printf '{"issues":[]}\n' > "<PLANS_DIR>/<session-id>-issue-close-outcome.json"
Step SC-3a — Invoke /issue-close-finalize via the Skill tool
Invoke /issue-close-finalize --from-session. The sub-skill writes
<PLANS_DIR>/<session-id>-issue-close-outcome.json as its Step L.
If it terminates without writing that file, write a synthetic fallback:
node "$AGENTS_CONFIG_DIR/bin/issue-close-write-outcome.js" \
--fallback "<PLANS_DIR>/<session-id>-intent.md" \
"<PLANS_DIR>/<session-id>-issue-close-outcome.json"
Steps SC-4+SC-5 — Retrospective scan + Pre-Final-Report gate
Dispatch the session-close-gate worker per skills/_shared/worker-dispatch.md. Payload:
session_id: current session ID (resolved from $CLAUDE_ENV_FILE / fallback chain per SC-0)
plans_dir: the PLANS_DIR from WD-1 — do NOT reuse the <PLANS_DIR> literal from SC-0, which was resolved with a fallback
artifact_dir: same value as plans_dir
outcome_json_path: absolute path to <PLANS_DIR>/<session-id>-issue-close-outcome.json
On status: failed: emit supervisor-report warning and STOP. Do NOT proceed to SC-6. User must manually re-run /session-close. This path is fail-closed — SC-6 never runs on worker failure.
On status: complete:
- Read
gate_action from artifact_path (gate JSON).
- Always emit
echo "<<WORKFLOW_MARK_STEP_pre_final_report_gate_complete>>".
gate_action: yield → STOP after sentinel. SC-6 does not run. Supervisor review runs later.
gate_action: proceed → continue to SC-6.
Step SC-6 — Emit Final Report directly into assistant text
Run: node "$AGENTS_CONFIG_DIR/bin/render-final-report.js" "" "<PLANS_DIR>/-final-report-env.json" "<PLANS_DIR>/-issue-close-outcome.json" "<PLANS_DIR>/-intent.md" "<PLANS_DIR>/-supervisor-state.json"
Emit the stdout per skills/_shared/final-report-emission.md — verbatim scope and CONV_LANG scope are defined there.
SC-6a. Mark session title complete: node "$AGENTS_CONFIG_DIR/bin/cc-session-title" mark-complete "$(pwd)". Fail-open.
After emitting, mark completion:
node "$AGENTS_CONFIG_DIR/bin/supervisor-write-alert" --session-id "" --set-alert-phase closed
WSID=$(awk '/^Session-ID:/{sub(/^Session-ID:[[:space:]]*/,""); sub(/\r/,""); print; exit}' "<NOTES_BACKUP_PATH>" 2>/dev/null || true)
if [ -n "$WSID" ] && [ "$WSID" != "" ]; then
node "$AGENTS_CONFIG_DIR/bin/supervisor-write-alert" --session-id "$WSID" --set-alert-phase closed --clear-alert-armed-at
fi
node "$AGENTS_CONFIG_DIR/bin/supervisor-write-audit" --clear-audit-phase --session-id ""
echo "<<WORKFLOW_MARK_STEP_final_report_complete>>"
stop-final-report-guard.js blocks (exit 2) when any of the 13 headings or any unsubstituted <TOKEN> is missing/present after ## Final Report — <session-id>.
Step SC-7 — Surface alert findings (post-Final-Report)
Read <PLANS_DIR>/<session-id>-supervisor-state.json (Read tool). If absent, or alert.findings is empty, or alert.findings_surfaced_at is already set, skip to the sentinel and return.
Run:
node "$AGENTS_CONFIG_DIR/bin/session-close-render-sc7.js" "<PLANS_DIR>/-supervisor-state.json" ""
When the render is non-empty: emit the text verbatim into the assistant reply (no preamble, no wrapping).
Mark surfaced and complete:
node "$AGENTS_CONFIG_DIR/bin/supervisor-write-alert" --session-id "" --mark-findings-surfaced
echo "<<WORKFLOW_MARK_STEP_l2_findings_surfaced_complete>>"
Step SC-8 — Promote residual WORKTREE_NOTES entries (post-Final-Report)
- Runs after the Final Report is emitted, only when unpromoted entries remain (
/worktree-end WE-11 normally clears them): resolve the notes path via node "$AGENTS_CONFIG_DIR/bin/worktree-notes-triage.js" resolve --caller session-close --session-id "<session-id>"; on action: skip return, otherwise run skills/_shared/notes-promotion.md (NP-1..NP-11) against the returned notesPath.
Rules
- Orchestrates only — never modifies workflow state directly.
/issue-close-finalize is invoked via the Skill tool only (never bash/spawnSync).
- Non-GitHub remotes never invoke
/issue-close-finalize; outcomes written by SC-3.
- Empty
closes_issues → skip /issue-close-finalize, write {"issues":[]}, emit Final Report.
/issue-close-finalize failures surface in outcome JSON; renderer still runs (non-blocking).
- Every Bash call is self-contained — no shell variable crosses call boundaries.
- On fallback or step degradation (synthetic outcome fallback, non-GitHub skip path): run
node "$AGENTS_CONFIG_DIR/bin/supervisor-report" --categories workflow --severity warning --detail "<describe fallback>" --reporter session-close (session-id auto-resolves).
- Report observations via /supervisor-report (trigger conditions: rules/supervisor-reporting.md).
- WF-META session (
workflow_type: wf-meta) → never invoke /issue-close-finalize; SC-3 writes skipped_wf_meta outcome directly and skips to SC-6.