Session Start Skill
Project-instruction file resolution:
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.
Soul
Before anything else, read and internalize soul.md in this skill directory. It defines WHO you are — your communication style, decision-making philosophy, and values. Every interaction in this session should reflect this identity. You are not a generic assistant; you are a seasoned engineering lead who drives outcomes.
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, classifies the caller mode against the exclusivity-matrix, and fires the appropriate AUQ on conflict.
This runs BEFORE the local session-lock acquire in Phase 1.2 — the preamble's cross-worktree detection is broader than acquire()'s single-worktree check. When the preamble returns PROMOTION_OFFER and the user picks "Worktree anlegen + starten", Phase 1.2 will be skipped entirely (the new worktree's own session-start performs it).
Outcome handling:
PASS_THROUGH→ continue to Phase 1EXCLUSIVE_BLOCKED→ exit Phase 0 cleanly per the AUQ outcome (Warten/Andere Session beenden/Abbrechen— all three return without initializing STATE.md)PROMOTION_OFFERwith user picking "Worktree anlegen + starten" → callenterWorktree({ basePath, sessionId, branch, repoRoot })fromscripts/lib/autopilot/worktree-pipeline.mjs. Compute params:basePath = path.dirname(repoRoot),sessionIdfrom resolveSemanticSessionId(),branchfrom current HEAD,repoRoot = process.cwd(). On success, exit Phase 0 immediately — the new worktree's own session-start runs from scratch (Phase 1 onwards), Phase 1.2 session-lock-acquire is the new worktree's responsibility. On enterWorktree failure (WorktreeBoundaryErrororgit worktree addnon-zero exit), emit stderr WARNparallel-aware: enterWorktree failed: <err>; falling back to Manuelland proceed via the Manuell path.PROMOTION_OFFERwith user picking "Manuell — in-place daneben" → append Deviation, continue to Phase 1PROMOTION_OFFERwith user picking "Abbrechen" → exit cleanly
Implementation reference: skills/_shared/parallel-aware-preamble.md § Implementation.
AUQ reference: skills/_shared/parallel-aware-auq.md.
Phase 1: Read Session Config
Read and parse Session Config per skills/_shared/config-reading.md. Store result as $CONFIG.
Phase 1.2: Session Lock Acquire (#330)
See also Phase 0.5 (Parallel-Aware Preamble) — the cross-worktree detection runs first. This Phase 1.2 handles the single-worktree local-lock semantics that complement the preamble.
Skip this phase if
persistenceconfig isfalse.
Acquire a distributed session-lock to detect parallel sessions in the same repo before initializing STATE.md. This prevents two concurrent Claude/Codex sessions from stomping each other's wave state and metrics writes.
Mechanical wiring (Epic #583, 2026-05-27): The SessionStart hook (hooks/on-session-start.mjs → hooks/_lib/lock-bootstrap.mjs) now writes .orchestrator/session.lock mechanically BEFORE this skill's prose runs. The prose Phase 1.2 becomes confirmatory — it verifies the lock exists with the expected shape via readLock({ repoRoot: process.cwd() }). Re-call acquire() only if readLock() returns null (mechanical hook failed) OR the existing lock's session_id does not match the current session's id (a rare divergence — surface via AUQ before overwriting). The decision flow below still applies to all three outcomes (active / stale / fs-error) when the prose path needs to acquire.
import { acquire, forceAcquire } from 'scripts/lib/session-lock.mjs';
const result = acquire({ sessionId, mode: sessionType, ttlHours: 4, repoRoot: process.cwd() });
Where sessionId is the session identifier derived from the session type and timestamp (e.g. main-2026-05-08-deep-1), and sessionType is the session mode (housekeeping, feature, or deep).
Decision flow
result.ok === true→ lock is held. Continue to Phase 1.5 (Session Continuity). The lock must be released in session-end.result.ok === falsewith `reason === 'active'**:- Another Claude/Codex session holds an active lock in this repo.
- Present a choice via
AskUserQuestion:AskUserQuestion({ questions: [{ question: `Another session lock is active in this repo (started ${ageHours}h ago, mode=${existingLock.mode}, host=${existingLock.host}, pid=${existingLock.pid}). How should I proceed?`, header: "Session Lock Conflict", multiSelect: false, options: [ { label: "Abort (Recommended)", description: "Let the other session finish. Safe default — prevents metrics and wave-state corruption." }, { label: "Force-take the lock", description: "Overwrites the active lock. ONLY use if you are certain the other session is no longer running." }, ], }], }); - Codex CLI / Cursor IDE fallback (numbered Markdown list):
Session lock conflict — active lock detected (started <ageHours>h ago, mode=<mode>, host=<host>, pid=<pid>). 1. Abort (Recommended) — let the other session finish. 2. Force-take the lock — ONLY if the other session is known dead. Reply with the number of your choice. - On Abort: exit session-start cleanly with a brief stderr note (
session-lock: aborted — active lock held by session_id=<id>). Do NOT initialize STATE.md. - On Force-take: call
forceAcquire({ sessionId, mode: sessionType, ttlHours: 4, repoRoot: process.cwd() }). After Phase 1.5 initializes STATE.md, append a deviation viaappendDeviation():Force-took session lock from session_id=<existingLock.session_id>, age=<ageHours>h, mode=<existingLock.mode>, pid=<existingLock.pid>. Continue.
result.ok === falsewithreason === 'stale-pid-dead'or `'stale-pid-alive'**:- A stale lock was found (TTL expired). Likely left behind by a session that crashed or was force-killed.
- Present a choice via
AskUserQuestion:AskUserQuestion({ questions: [{ question: `Stale session lock found (started ${ageHours}h ago, ttl=${existingLock.ttl_hours}h). Process pid=${existingLock.pid} on host=${existingLock.host} is ${reason === 'stale-pid-dead' ? 'confirmed dead' : 'still running or status unknown'}. Reclaim the lock?`, header: "Stale Session Lock", multiSelect: false, options: [ { label: "Reclaim (Recommended)", description: "Overwrite the stale lock and continue. Safe when the previous session is no longer active." }, { label: "Abort — investigate manually", description: "Stop here. Inspect .orchestrator/session.lock before proceeding." }, ], }], }); - Codex CLI / Cursor IDE fallback (numbered Markdown list):
Stale session lock found (started <ageHours>h ago, ttl=<ttlHours>h, pid=<pid> on <host>). 1. Reclaim (Recommended) — overwrite stale lock and continue. 2. Abort — investigate .orchestrator/session.lock manually. Reply with the number of your choice. - On Reclaim: call
forceAcquire({ sessionId, mode: sessionType, ttlHours: 4, repoRoot: process.cwd() }). After Phase 1.5 initializes STATE.md, append a deviation:Stale-lock reclaim: replaced lock from session_id=<existingLock.session_id>, age=<ageHours>h, pid=<existingLock.pid>. Continue. - On Abort: exit cleanly.
result.ok === falsewith `reason === 'fs-error'**:- Filesystem error when writing the lock file. Log
⚠ session-lock: acquire failed — <error>. Continuing without lock (degraded mode).and proceed without a lock. Do NOT block the session for a transient FS error.
- Filesystem error when writing the lock file. Log
New reasons from P1.2 #570: When called with the optional
activeSessionsargument,acquire()can also returnactive-incompatible-exclusive,active-compatible-parallel, oractive-readonly-bypass. Session-start invokesacquire()WITHOUTactiveSessions(the preamble in Phase 0.5 already handled cross-worktree detection); these new reasons surface only in callers that bypass the preamble. Other entry-points (autopilot, session-plan, wave-executor, session-end) follow the same pattern.
Cross-host behaviour
When existingLock.host !== os.hostname(), PID liveness cannot be checked (pidAlive: null). In this case:
- For
reason === 'active': the recommendation is Abort — cross-host locks cannot be verified as dead. - For stale reasons: the recommendation is still Reclaim only if TTL is clearly expired (>2× ttl_hours). Otherwise default to Abort.
- Never auto-reclaim cross-host locks under any circumstance — always present the AUQ and let the user decide.
- The AUQ question text for cross-host cases should note:
"(cross-host — PID liveness cannot be verified)".
Phase 1.2.1: Peer-Guard (Epic #583 defense-in-depth)
Skip this phase if
persistenceconfig isfalse.
After Phase 1.2 acquires (or confirms) the lock, call checkPeerStateMd(repoRoot, sessionId) from scripts/lib/state-md-peer-guard.mjs. This catches the rare case where lock-based detection missed an active peer (e.g., the peer's session.lock was force-deleted by an out-of-band sweep but STATE.md is still status: active, OR the peer's registry write succeeded but the lock-bootstrap hook crashed before the lock landed).
import { findPeers } from '$PLUGIN_ROOT/scripts/lib/peer-discovery.mjs';
const { peers } = await findPeers(process.cwd(), { mySessionId: sessionId });
const peer = peers.find((p) => p.source === 'state-md') ?? null;
// Phase 1.2.1 consumes only the 'state-md' subset (STATE.md surface only).
if (peer) {
// STATE.md is owned by an active peer — do NOT overwrite.
// peer.sessionId, peer.mode, peer.currentWave, peer.ageHours are populated.
// Fire the Worktree-Promotion AUQ from parallel-aware-auq.md.
}
Decision flow
peer === null→ no active peer owns STATE.md. Continue to Phase 1.5.peer !== null→ STATE.md is owned by a live peer session. Do NOT proceed with the default Phase 1.5/1b STATE.md overwrite. Fire the Worktree-Promotion AUQ fromskills/_shared/parallel-aware-auq.md(same options the Phase 0.5 preamble would emit onPROMOTION_OFFER).- User picks "Worktree anlegen + starten" → call
enterWorktree(...)and exit Phase 1 immediately (the new worktree's own session-start runs from scratch). - User picks "Manuell — in-place daneben" → append a Deviation describing the missed peer detection, continue to Phase 1.5. STATE.md WILL be overwritten — the user has explicitly accepted that risk.
- User picks "Abbrechen" → exit cleanly.
- User picks "Worktree anlegen + starten" → call
Soft-gate semantics
This is a SOFT-GATE — the operator can override via the AUQ — but the warning is mandatory and must not be silenced. Treat any checkPeerStateMd failure (read error, malformed STATE.md, etc.) as peer === null (fail-open: do not block the session for a corrupted STATE.md file; the rest of the parallel-aware machinery still applies).
Why this complements Phase 1.2
Phase 1.2 owns the .orchestrator/session.lock file; Phase 1.2.1 owns the STATE.md frontmatter. The two surfaces can disagree (briefly, during a crash; durably, if a sweep deleted one but not the other). The Peer-Guard treats STATE.md as a second, independent source of truth — if EITHER source says a peer is active, the coordinator must pause before stomping shared state.
Phase 1.5: Session Continuity
Skip this phase if
persistenceconfig isfalse.
Check for <state-dir>/STATE.md in the project root:
Where
<state-dir>is.claude/under Claude Code or.codex/under Codex CLI. Seeskills/_shared/platform-tools.mdfor details.
Ownership Reference: See
skills/_shared/state-ownership.mdfor the STATE.md ownership contract, schema, and guards.
Before reading STATE.md contents, validate the branch field:
- If STATE.md's
branchdoes not matchgit rev-parse --abbrev-ref HEAD, log: "⚠ STATE.md from branch [X], current branch is [Y] — treating as stale." Skip to step 2 (treat as if STATE.md does not exist).
- STATE.md exists — read it and inspect the
statusfield:status: active— previous session crashed or was interrupted. Use the AskUserQuestion tool to present: "Found unfinished session from [started_at]. [N] waves completed. Resume or start fresh?" with options to resume the previous plan or start a new session. After a resume choice, proceed to Snapshot Recovery subsection below.status: paused— session was intentionally paused. Use AskUserQuestion to offer resuming from the pause point or starting fresh. After a resume choice, proceed to Snapshot Recovery subsection below.status: completed— previous session ended cleanly. Note the summary for context (what was done, what was deferred), then render the Recommendations Banner (see subsection below) and reset STATE.md to idle before any new session state is written (see "Idle Reset" below). Continue with normal initialization.
- STATE.md does not exist — first session or persistence was previously off. Continue normally.
Recommendations Banner (Epic #271 Phase A)
Runs on the
status: completedbranch only, BEFORE Idle Reset archives the fields. Silent no-op on other branches.
Read the 5 optional v1.1 Recommendation fields from STATE.md frontmatter via parseRecommendations (from scripts/lib/state-md.mjs). The writer is session-end Phase 3.7a (see skills/session-end/SKILL.md).
node --input-type=module -e "
import {readFileSync} from 'node:fs';
import {parseStateMd, parseRecommendations} from '${PLUGIN_ROOT}/scripts/lib/state-md.mjs';
import {isValidMode} from '${PLUGIN_ROOT}/scripts/lib/recommendations-v0.mjs';
import {appendFileSync, mkdirSync} from 'node:fs';
const SWEEP_LOG = '.orchestrator/metrics/sweep.log';
function logWarn(event, detail) {
try {
mkdirSync('.orchestrator/metrics', {recursive: true});
appendFileSync(SWEEP_LOG, JSON.stringify({timestamp: new Date().toISOString(), event, detail}) + '\n');
} catch {}
}
const parsed = parseStateMd(readFileSync('<state-dir>/STATE.md', 'utf8'));
if (!parsed) process.exit(0);
const rec = parseRecommendations(parsed.frontmatter);
if (!rec) process.exit(0); // pre-v1.1 STATE.md — graceful silent no-banner (AC3)
// AC4: type-mismatch in top-priorities — field-level null from parser; still render other fields
if (rec.priorities === null && Object.prototype.hasOwnProperty.call(parsed.frontmatter, 'top-priorities')) {
logWarn('state-md-type-mismatch', {field: 'top-priorities', got: typeof parsed.frontmatter['top-priorities']});
}
// AC4: partial fields — warn but still render available ones
const missingCount = [rec.mode, rec.priorities, rec.carryoverRatio, rec.completionRate, rec.rationale].filter((x) => x === null).length;
if (missingCount > 0 && missingCount < 5) {
logWarn('state-md-partial-recommendation', {missing: missingCount});
}
const modeOk = rec.mode && isValidMode(rec.mode);
const mode = modeOk ? rec.mode : '(unknown-mode)';
const rationale = rec.rationale || '(no rationale)';
const pct = (x) => (x === null ? '—' : Math.round(x * 100) + '%');
console.log('📋 Previous session recommended: ' + mode + ' — ' + rationale + ' (completion: ' + pct(rec.completionRate) + ', carryover: ' + pct(rec.carryoverRatio) + ')');
if (Array.isArray(rec.priorities) && rec.priorities.length > 0) {
console.log(' Suggested issues: ' + rec.priorities.map((id) => '#' + id).join(', '));
}
"
Behavior matrix (AC1/AC3/AC4):
- All 5 fields present + valid → banner line + suggested-issues line (if priorities non-empty).
- Field(s) absent entirely → no banner (graceful no-op, no WARN).
- 1–4 fields present (partial) → banner renders with
—for missing, WARNstate-md-partial-recommendationto sweep.log. top-prioritiesis not an array (type-mismatch) → treated as null, WARNstate-md-type-mismatchto sweep.log, other fields still render.- Unknown
recommended-modevalue → banner shows(unknown-mode)instead of the string.
The reader does NOT mutate STATE.md — it is a pure observer. Idle Reset (subsection below) is the only code path that modifies the file on the completed branch.
Idle Reset (completed-branch only)
When (and only when) the prior status is completed, rewrite STATE.md to a clean idle state before Phase 1b (Initialize STATE.md) runs. This prevents the next agent from reading a stale "completed" banner at session-start, while preserving the prior session's record in a demoted archive block.
Reset rules — applies ONLY on the completed branch. Do NOT perform this reset on active or paused; those paths stay user-interactive via AskUserQuestion.
Set frontmatter
status: idle.Clear
current-wave(set to0).Move the existing
## Wave Historybody into a new## Previous Sessionarchive section (retain the record, but demote it below the new session's live state). Remove the original## Wave Historysection — wave-executor will recreate it on the next wave.Clear
## Deviations(leave the heading with an empty body so the schema is preserved).Leave other frontmatter fields (
schema-version,session-type,branch,issues,started_at,total-waves) intact until Phase 1b overwrites them with the new session's values.v1.1 Recommendation-field archival (Epic #271 Phase A, AC2): If ANY of the 5 Recommendation fields (
recommended-mode,top-priorities,carryover-ratio,completion-rate,rationale) is present in the frontmatter, remove them from the frontmatter viaupdateFrontmatterFields(contents, {field: null, ...})(null value deletes the key). Then prepend a readable block (NOT YAML) to the## Previous Sessionbody:### Recommendations (archived from v1.1 frontmatter) - **Recommended mode:** <mode> - **Rationale:** <rationale> - **Completion rate:** <XX%> - **Carryover ratio:** <XX%> - **Top priorities:** #<id>, #<id>, … _(or "none")_Omit individual bullets for null-valued fields. If all 5 are null (i.e.,
parseRecommendationsreturned non-null but every field is null after type-coercion), skip the archival block entirely.
Rationale: /close intentionally keeps STATE.md as a record so the next session-start can read it. This reset completes that contract by demoting the record before new session state is written, so a fresh session never appears "already completed". The Recommendation archival (rule 6) preserves the session-to-session handoff in a human-readable form after the Recommendations Banner has rendered — Phase B's Mode-Selector will read the LIVE frontmatter of the current session and does not need the archived copy, so this is purely informational for humans browsing STATE.md history.
Snapshot Recovery (#196)
Applies ONLY after the user chose to resume from the active/paused branch above. Skip entirely on the completed branch (snapshots for completed sessions are GC'd by session-end, not offered for recovery) and on the "start fresh" path of an active/paused prompt (starting fresh implies abandoning any snapshot).
import { listSnapshots, deleteSnapshot } from '$PLUGIN_ROOT/scripts/lib/coordinator-snapshot.mjs';
const snaps = await listSnapshots({ sessionId: '<sessionId from STATE.md>' });
If snaps.length === 0 → no snapshots to recover; continue to the Current-Task Banner.
If snaps.length >= 1 → present the following choice:
Claude Code (AskUserQuestion):
AskUserQuestion({
questions: [{
question: `Found ${snaps.length} coordinator snapshot(s) from the resumed session (latest from ${humanAgeOf(snaps[0].createdAt)}). Recover, keep as backup, or discard?`,
header: "Snapshot",
multiSelect: false,
options: [
{ label: "Recover (diff vs current tree) (Recommended)", description: "Apply the latest snapshot back onto the working tree. You will see a diff and can unstage unwanted changes before committing." },
{ label: "Keep as backup", description: "Leave refs/so-snapshots/* in place untouched. You can recover manually later via `git stash apply $(git rev-parse <ref>)`." },
{ label: "Discard all", description: "Delete all refs/so-snapshots/<sessionId>/* immediately via deleteSnapshot." },
],
}],
});
Codex CLI / Cursor IDE fallback (numbered Markdown list):
Snapshot recovery options:
1. **Recover (Recommended)** — Apply the latest snapshot back onto the working tree. You will see a diff and can unstage unwanted changes before committing.
2. **Keep as backup** — Leave the refs in place untouched. You can recover manually later.
3. **Discard all** — Delete all refs/so-snapshots/<sessionId>/* immediately.
Reply with the number of your choice.
On user choice:
- Recover →
git stash apply <snaps[0].sha>(use apply, not pop — leaves the ref intact in case the user changes their mind). Then show the resultinggit diff --statso the user sees what landed. - Keep as backup → no-op. Log in the Session Overview:
Snapshot(s) retained: <N>. Recover manually with \git stash apply `.` - Discard all → for each snapshot in
snaps, calldeleteSnapshot({refName: snap.ref}). Log count.
Snapshot age (humanAgeOf) is derived from snap.createdAt (ISO 8601 from git for-each-ref --format='%(committerdate:iso8601)'). A simple inline helper:
function humanAgeOf(iso) {
const mins = Math.floor((Date.now() - new Date(iso).getTime()) / 60000);
if (mins < 60) return `${mins}m ago`;
const hrs = Math.floor(mins / 60);
if (hrs < 24) return `${hrs}h ago`;
return `${Math.floor(hrs / 24)}d ago`;
}
Current-Task Banner (#184)
After the continuity checks above, render a one-line banner showing the current task from STATE.md. This gives the user an immediate "where am I" signal before the rest of the session overview loads.
node --input-type=module -e "
import {readFileSync} from 'node:fs';
import {readCurrentTask} from '${PLUGIN_ROOT}/scripts/lib/state-md.mjs';
try {
const t = readCurrentTask(readFileSync('<state-dir>/STATE.md', 'utf8'));
if (t) console.log('Current task: ' + t.description);
} catch {}
"
Skip silently when STATE.md is absent or unreadable. The banner is informational, not load-bearing.
Also read <state-dir>/STATUS.md if it exists for additional project-level context.
Phase 1.6: Metrics Initialization
Skip if
persistenceconfig isfalse.
- Ensure '.orchestrator/metrics/' directory exists in the project root (create if missing). For backward compatibility with pre-v2.0 sessions, also check the platform's legacy metrics directory (
<state-dir>/metrics/where<state-dir>is.claude/,.codex/, or.cursor/per platform). - If '.orchestrator/metrics/sessions.jsonl' exists, count lines to determine number of previous sessions. If not found, check
<state-dir>/metrics/sessions.jsonlas a platform-specific legacy fallback. - Store the count for display in Phase 7 — this feeds the Historical Trends section
Phase 2: Git Analysis (parallel)
Run these checks as ONE parallel Bash block — background the independent git ops with & and wait:
# Independent ops — launch in parallel, collect output via tmpfiles
git branch -a > /tmp/so-branches.$$ &
git log --oneline -N > /tmp/so-commits.$$ & # N from Session Config `recent-commits` (default 20)
git status --short > /tmp/so-status.$$ &
git log origin/main..HEAD --oneline > /tmp/so-ahead.$$ &
wait
# Then read the 4 tmpfiles in a single step and derive: branch state, recent commits,
# unpushed/uncommitted, open branches. Clean up tmpfiles once derivations are done:
rm -f /tmp/so-branches.$$ /tmp/so-commits.$$ /tmp/so-status.$$ /tmp/so-ahead.$$
Checks to run (derived from the collected output):
- Branch state: current branch (from
branch -a), ahead/behind origin (fromaheadtmpfile) - Recent commits: parse
commitstmpfile — identify last session's work by commit patterns - Unpushed/uncommitted:
statustmpfile +aheadtmpfile combined - Open branches: parse
branch -atmpfile, identify which are mergeable to develop/main - Stale branches: run AFTER the parallel block — requires iterating over branches (depends on
branch -aoutput). Usegit log -1 --format=%ct <branch>per branch; flag those with no commits in more thanstale-branch-days(default: 7) days.
Rationale: The 4 independent ops are I/O-bound — running them in parallel cuts Phase 2 wall-clock from ~500ms to ~150ms. The stale-branches check depends on the branch list, so it runs after wait.
Phase 2.5: Docs Planning (Docs-Orchestrator Integration)
Skip this phase if
docs-orchestrator.enabledconfig is nottrue(default:false).
Reads the docs-orchestrator config fields, auto-detects which audiences (user/dev/vault) are affected by the current scope using signals from Phases 2–5, confirms the selection with the user via AskUserQuestion, and emits a ### Docs Planning Result (Phase 2.5) block into the conversation context. That block is the MANDATORY contract consumed by session-plan Step 1.8 to seed Docs-role tasks. Audience → file-pattern mapping is the authoritative source at skills/docs-orchestrator/audience-mapping.md. Contains non-overlap discipline rules (paths owned by vault-mirror and daily are off-limits).
See phase-2-5-docs-planning.md for full details.
Phase 2.6: Steering Docs Loading
Skip this phase silently when
.orchestrator/steering/does not exist in the project root. This mirrors Phase 2.5's silent-no-op pattern — backward compatibility with repos that have not yet scaffolded steering docs.
Check for the steering directory and load all three docs if present:
STEERING_DIR=".orchestrator/steering"
if [ -d "$STEERING_DIR" ]; then
PRODUCT_MD=""
TECH_MD=""
STRUCTURE_MD=""
[ -f "$STEERING_DIR/product.md" ] && PRODUCT_MD=$(cat "$STEERING_DIR/product.md")
[ -f "$STEERING_DIR/tech.md" ] && TECH_MD=$(cat "$STEERING_DIR/tech.md")
[ -f "$STEERING_DIR/structure.md" ] && STRUCTURE_MD=$(cat "$STEERING_DIR/structure.md")
fi
When at least one file is non-empty, inject the following Steering Context banner into the conversation context before Phase 3. This gives Phase 3 (VCS Deep Dive) and subsequent phases stable product/tech/structure facts without re-reading CLAUDE.md:
--- Steering Context ---
[product.md contents — mission, target users, in-scope, out-of-scope]
[tech.md contents — stack, commands, constraints]
[structure.md contents — directory map, inventory, key skills]
--- End Steering Context ---
If .orchestrator/steering/ is absent or all three files are empty, proceed directly to Phase 3 with no banner and no warning. Do not treat missing steering docs as an error.
See .orchestrator/steering/{product,tech,structure}.md for file contents.
Phase 2.7: GitLab Portfolio Snapshot (#41)
Skip this phase if
gitlab-portfolio.enabledis nottruein Session Config (default:false). Also skip silently whenvault-integration.enabledisfalseorvault-integration.vault-diris absent.
When active, this phase surfaces a compact portfolio health banner at session-start without writing any file. It runs in dry-run mode only — the full write path is reserved for the /portfolio command.
Config check
PORTFOLIO_ENABLED=$(echo "$CONFIG" | jq -r '."gitlab-portfolio".enabled // false')
VAULT_ENABLED=$(echo "$CONFIG" | jq -r '."vault-integration".enabled // false')
VAULT_DIR=$(echo "$CONFIG" | jq -r '."vault-integration"."vault-dir" // empty')
PORTFOLIO_MODE=$(echo "$CONFIG" | jq -r '."gitlab-portfolio".mode // "warn"')
if [ "$PORTFOLIO_ENABLED" != "true" ] || [ "$VAULT_ENABLED" != "true" ] || [ -z "$VAULT_DIR" ]; then
exit 0 # silent no-op
fi
if [ "$PORTFOLIO_MODE" = "off" ]; then
exit 0 # silent no-op
fi
Dispatch
Invoke scripts/lib/gitlab-portfolio/cli.mjs in dry-run mode (same orchestrator used by /portfolio):
node scripts/lib/gitlab-portfolio/cli.mjs \
--vault-dir "$VAULT_DIR" \
--dry-run \
--session-start-snapshot # instructs cli.mjs to emit the compact JSON summary for banner rendering
The CLI emits a single-line JSON to stdout:
{ "repos": 16, "openIssues": 42, "critical": 3, "stale": 5, "lastRefresh": "2026-05-16T08:00:00Z" }
Banner rendering
Parse the JSON and render the banner into the Session Overview:
📊 Portfolio: 16 repos · 42 open issues · 3 critical · 5 stale (>30d)
Last refresh: 2026-05-16 08:00 UTC
Run /portfolio to refresh.
Failure behavior
Governed by the mode field from gitlab-portfolio: config:
warn(default): if the CLI exits non-zero or emits invalid JSON, append⚠ partial (<X>/<N> repos failed)to the banner and continue session-start normally. Do NOT halt.strict: if the CLI fails, emit a single-line banner❌ portfolio snapshot failed — run /portfolio for detailsinto the Session Overview. Do NOT halt session-start — session-start must never be blocked by portfolio failures.off: silent no-op (already handled by the config check above).
Performance budget
Must complete within 8 seconds for portfolios of ≤16 repos (matches the D3 timeout used by vault-staleness and CI-status probes). If the CLI has not exited after 8 seconds, terminate it, skip banner rendering, and emit a single WARN line to .orchestrator/metrics/sweep.log:
{"timestamp":"<ISO>","event":"portfolio-snapshot-timeout","detail":{"timeout_ms":8000}}
Proceed to Phase 3 without blocking.
Cross-reference
See commands/portfolio.md for the /portfolio command (full write path, --dry-run, --repo single-repo testing).
Phase 3: VCS Deep Dive (parallel)
VCS Reference: Detect the VCS platform per the "VCS Auto-Detection" section of the gitlab-ops skill. Use CLI commands per the "Common CLI Commands" section. For cross-project queries, see "Dynamic Project Resolution."
Using the detected VCS CLI, query (reading issue-limit from Session Config, default: 50):
- Open issues — categorize by priority and status labels
- Recently closed — what was done since last session
- Milestones — active sprint status
- Open MRs/PRs — anything waiting for review/merge
- Pipeline/CI status — is CI green?
Group issues by:
priority:critical/priority:high— must-addressstatus:ready— ready to work on- Session-type relevance (housekeeping tasks vs feature tasks vs deep-work tasks)
Phase 4: SSOT & Environment Check
SSOT freshness: for each file in
ssot-filesconfig, check last modified date. Flag if older thanssot-freshness-days(default: 5) days.Quality baseline: Run Baseline quality checks per the quality-gates skill. Commands are resolved in this order (issue #183): a.
.orchestrator/policy/quality-gates.json— preferred source when present. b. Session Configtest-command/typecheck-command/lint-command— fallback. c. Hardcoded defaults:pnpm test --run,tsgo --noEmit,pnpm lint. Before running, perform a command-availability check: for each resolved command, extract the binary (first token) and runcommand -v <binary>. If absent, skip that check and log⚠ Quality baseline: <binary> not found — skipping <variant>. Report results but do not block the session.Pencil design status: if
pencilis configured, verify the.penfile exists at the configured path. Report: "Pencil design configured at [path] — design-code alignment reviews will run after Impl-Core and Impl-Polish waves." If file not found, warn: "Pencil path configured but file not found at [path]."Plugin freshness: Determine the session-orchestrator plugin directory (navigate up from this skill's base directory to the plugin root). Run
git -C <plugin-dir> log -1 --format="%ci"to get the last commit date. If older thanplugin-freshness-days(default: 30) days, flag a warning in the Session Overview:"⚠ Session Orchestrator plugin last updated [N] days ago — consider pulling the latest version."Non-blocking — present in overview, don't halt.Additionally, if
.orchestrator/bootstrap.lockexists in the current repo, invoke the bootstrap-lock-freshness probe (scripts/lib/bootstrap-lock-freshness.mjs) to check lock age and plugin-version drift. PasscurrentPluginVersionread from$PLUGIN_ROOT/package.jsonso version comparison is live. When severity iswarnoralert, render an additional banner alongside the plugin-freshness warning:- warn (age 30–89d or non-parseable version mismatch):
"⚠ bootstrap.lock: age=<N>d, plugin-version=<lock-ver> (current=<plugin-ver>) — consider re-running /bootstrap --retroactive to refresh." - alert (age ≥90d, unparseable, missing, or major plugin-version mismatch):
"⚠ bootstrap.lock: <message> — re-run /bootstrap --retroactive is strongly recommended." - info-only version mismatch (patch or minor version only):
"ℹ bootstrap.lock: plugin-version=<lock-ver> (current=<plugin-ver>) — minor drift only, no action required." - legacy lock without plugin-version (soft signal only):
"ℹ bootstrap.lock: lock predates plugin-version field; consider /bootstrap --retroactive to refresh."
Additionally, if
.orchestrator/metrics/vault-staleness.jsonlexists in the current repo (vault-integration enabled), read the most recent line viascripts/lib/vault-staleness-banner.mjs(checkVaultStaleness({repoRoot})). Whenstale_count > 0, render a banner alongside the bootstrap-lock warning:- warn (
stale_count > 0, maxdelta_hours <= 48):"⚠ vault-staleness: <N> projects stale (max delta: <X>h) — last run <timestamp>." - alert (
stale_count > 0, maxdelta_hours > 48):"⚠ vault-staleness: <N> projects stale (max delta: <X>h) — Clank-Vault-Sync cron likely broken, see agents/vault#70 fix pattern."
The helper returns
null(silent no-op) when the JSONL is absent, malformed, orstale_count === 0. Skip silently in those cases — do not block the session.Additionally, if the current repo has a configured
originremote andglab(GitLab) orgh(GitHub) is available, invoke the CI-status probe (scripts/lib/ci-status-banner.mjs) viacheckCiStatus({ repoRoot: process.cwd() }). The helper returnsnull(silent no-op) when no VCS remote, no CLI tool, parse failure, or CLI timeout (8s default). Whenresult.status === 'red', render a banner alongside the bootstrap-lock and vault-staleness warnings:- Red (
status === 'red'):"🚨 CI RED on HEAD (pipeline #<currentPipelineId>) — last green: #<lastGreen.pipelineId> (commit <SHA-7>, <redCount> pipelines ago). Failing job: <failingJobName>" - Green or unknown: silent (no banner) — informational only.
The banner is non-blocking — display in the Session Overview, do not halt the session. If
ci-status-banner.mjsis absent (pre-#369 plugin install), skip silently.Additionally, invoke the QG-command-drift probe (
scripts/lib/qg-command-drift-banner.mjs) viaawait checkQgCommandDrift({ repoRoot }). The helper returnsnull(silent no-op) when no drift or when Session Config load fails. When a non-null result is returned, renderresult.messagealongside the bootstrap-lock-freshness, vault-staleness, and CI-status banners:- Drift detected (
{ severity: 'warn', message: ... }): renderresult.message. The message has the shape"⚠ Session Config drift (*-command keys): <details>. Verify the overrides are intentional. See .claude/rules/quality-gates-autofix.md § Session Config Command Injection for the RCE-equivalent trust-model." - No drift: silent (no banner).
The banner is non-blocking — display in the Session Overview, do not halt the session. Cross-reference:
.claude/rules/quality-gates-autofix.md§ Session Config Command Injection — the banner exists because*-commandkeys are RCE-equivalent under the VCS trust-anchor model.Additionally, invoke the peer-cards-staleness probe (
scripts/lib/peer-cards/staleness-banner.mjs) viaawait checkPeerCardsStaleness({ repoRoot }). The helper returnsnull(silent no-op) when.orchestrator/peers/is absent, neither USER.md nor AGENT.md is present, no card is stale, or the reader fails. When a non-null result is returned ({ severity: 'warn', message, stale }), renderresult.messagealongside the bootstrap-lock-freshness, vault-staleness, CI-status, and QG-command-drift banners:- Stale (>30d):
"⚠ peer-cards: USER.md (Nd), AGENT.md (Nd) stale (>30 days) — consider running /evolve --dialectic to refresh."(one or both targets, whichever are stale). - Fresh / absent / malformed frontmatter: silent (no banner).
Cross-reference:
.claude/rules/owner-persona.md(host-wideowner.yamloperator identity) andskills/vault-sync/SKILL.md(type: peer-cardvalue in the vault-frontmatter enum). Peer cards complementowner.yamlwith per-repo behavioural identity for the operator (USER.md) and agent (AGENT.md).All banners are non-blocking — display in the Session Overview, do not halt the session. If
bootstrap-lock-freshness.mjsis absent (pre-#186 plugin install) orpeer-cards/staleness-banner.mjsis absent (pre-#503 plugin install), skip silently.- warn (age 30–89d or non-parseable version mismatch):
Phase 4.5: Resource Health (v3.1.0)
Skip this phase if
resource-awareness: falsein Session Config.
Reads .orchestrator/host.json and runs a live resource snapshot via resource-probe.mjs. Computes a green/warn/critical verdict against configurable thresholds (RAM, CPU, concurrent Claude processes, SSH). On warn/critical, presents an AskUserQuestion prompt to apply the recommended agents-per-wave cap or proceed at the user's own risk. The cap is forwarded to session-plan as an in-session override.
See phase-4-5-resource-health.md for full details.
Phase 5: Cross-Repo Status (if configured)
For each repo in cross-repos:
cd ~/Projects/<repo> && git log --oneline -5 && git status --short- Check for open issues that reference this repo
- Note any branches that should be merged
Phase 6: Pattern Recognition
Look across the gathered data for:
- Recurring patterns: same types of issues appearing repeatedly → suggest standardization
- Blocking chains: issues blocked by other issues across repos
- Quick wins: low-effort issues that could be closed alongside main work
- Staleness: issues open longer than
stale-issue-days(default: 30) days without progress → flag for triage - Synergies: issues that share code paths and can be combined
Phase 6.5: Memory Recall
Skip this phase if
persistenceconfig isfalse.
Platform Note: Session memory files at
~/.claude/projects/are a Claude Code feature. On Codex CLI and Cursor IDE, skip this phase —
…(truncated)