session-orchestrator 3.24.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/architecture/SKILL.md +18 -0
- package/.agents/skills/autopilot/SKILL.md +17 -0
- package/.agents/skills/bootstrap/SKILL.md +20 -0
- package/.agents/skills/brainstorm/SKILL.md +22 -0
- package/.agents/skills/claude-md-drift-check/SKILL.md +15 -0
- package/.agents/skills/convergence-monitoring/SKILL.md +22 -0
- package/.agents/skills/debug/SKILL.md +22 -0
- package/.agents/skills/discovery/SKILL.md +20 -0
- package/.agents/skills/dispatcher/SKILL.md +15 -0
- package/.agents/skills/docs-orchestrator/SKILL.md +18 -0
- package/.agents/skills/ecosystem-health/SKILL.md +20 -0
- package/.agents/skills/eli5/SKILL.md +20 -0
- package/.agents/skills/eval/SKILL.md +21 -0
- package/.agents/skills/evolve/SKILL.md +21 -0
- package/.agents/skills/frontmatter-guard/SKILL.md +15 -0
- package/.agents/skills/gitlab-ops/SKILL.md +20 -0
- package/.agents/skills/gitlab-portfolio/SKILL.md +15 -0
- package/.agents/skills/grill/SKILL.md +22 -0
- package/.agents/skills/hook-development/SKILL.md +15 -0
- package/.agents/skills/mcp-builder/SKILL.md +15 -0
- package/.agents/skills/memory-cleanup/SKILL.md +21 -0
- package/.agents/skills/mode-selector/SKILL.md +17 -0
- package/.agents/skills/npm-publish/SKILL.md +16 -0
- package/.agents/skills/peekaboo-driver/SKILL.md +18 -0
- package/.agents/skills/persona-panel/SKILL.md +17 -0
- package/.agents/skills/plan/SKILL.md +20 -0
- package/.agents/skills/playwright-driver/SKILL.md +20 -0
- package/.agents/skills/quality-gates/SKILL.md +20 -0
- package/.agents/skills/reconcile/SKILL.md +21 -0
- package/.agents/skills/remote-offload/SKILL.md +20 -0
- package/.agents/skills/repo-audit/SKILL.md +16 -0
- package/.agents/skills/session-end/SKILL.md +20 -0
- package/.agents/skills/session-plan/SKILL.md +20 -0
- package/.agents/skills/session-start/SKILL.md +20 -0
- package/.agents/skills/spinout/SKILL.md +16 -0
- package/.agents/skills/sunset-review/SKILL.md +16 -0
- package/.agents/skills/test-runner/SKILL.md +20 -0
- package/.agents/skills/tmux-layout/SKILL.md +21 -0
- package/.agents/skills/using-orchestrator/SKILL.md +17 -0
- package/.agents/skills/vault-mirror/SKILL.md +15 -0
- package/.agents/skills/vault-sync/SKILL.md +15 -0
- package/.agents/skills/wave-executor/SKILL.md +20 -0
- package/.agents/skills/write-executable-plan/SKILL.md +22 -0
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor/commands/autopilot.md +2 -2
- package/.cursor/commands/bootstrap.md +1 -1
- package/.cursor/commands/brainstorm.md +1 -1
- package/.cursor/commands/debug.md +1 -1
- package/.cursor/commands/discovery.md +1 -1
- package/.cursor/commands/dispatcher.md +2 -2
- package/.cursor/commands/eli5.md +2 -2
- package/.cursor/commands/eval.md +2 -2
- package/.cursor/commands/evolve.md +1 -1
- package/.cursor/commands/go.md +1 -1
- package/.cursor/commands/grill.md +2 -2
- package/.cursor/commands/memory-cleanup.md +2 -2
- package/.cursor/commands/persona-panel.md +1 -1
- package/.cursor/commands/plan.md +1 -1
- package/.cursor/commands/portfolio.md +1 -1
- package/.cursor/commands/reconcile.md +2 -2
- package/.cursor/commands/release.md +2 -2
- package/.cursor/commands/session.md +2 -2
- package/.cursor/commands/spinout.md +2 -2
- package/.cursor/commands/sunset-review.md +2 -2
- package/.cursor/commands/templates-ack.md +2 -2
- package/.cursor/commands/test.md +2 -2
- package/.cursor/skills/brainstorm/SKILL.md +1 -1
- package/.cursor/skills/eval/SKILL.md +1 -1
- package/.cursor/skills/quality-gates/SKILL.md +1 -1
- package/.cursor/skills/remote-offload/SKILL.md +1 -1
- package/.orchestrator/policy/blocked-commands.json +121 -0
- package/.orchestrator/policy/ecosystem.schema.json +66 -0
- package/.orchestrator/policy/quality-gates.example.json +16 -0
- package/.orchestrator/policy/quality-gates.schema.json +38 -0
- package/.orchestrator/policy/templates-policy.json +27 -0
- package/.orchestrator/policy/test-profiles.json +47 -0
- package/AGENTS.md +225 -0
- package/CHANGELOG.md +1125 -2
- package/NOTICE +11 -6
- package/README.md +127 -94
- package/agents/eval-judge.md +1 -1
- package/agents/skill-applied-judge.md +1 -1
- package/assets/wave-lifecycle.svg +98 -0
- package/commands/release.md +6 -3
- package/commands/session.md +18 -3
- package/docs/README.md +4 -0
- package/{agents/AGENTS.md → docs/agent-authoring.md} +19 -26
- package/docs/baseline.md +67 -0
- package/docs/ci-setup.md +108 -62
- package/docs/codex-setup.md +65 -21
- package/docs/components.md +36 -15
- package/docs/cursor-setup.md +6 -2
- package/docs/events-schema.md +9 -6
- package/docs/instruction-delivery.md +62 -0
- package/{agents/memory-proposal-collector.md → docs/memory-proposal-flow.md} +1 -8
- package/docs/migration-v4.md +341 -0
- package/docs/pi-setup.md +6 -1
- package/docs/plugin-architecture-v3.md +1 -1
- package/docs/rule-authoring.md +85 -19
- package/docs/scope-collision-guard.md +5 -5
- package/docs/session-config-reference.md +57 -56
- package/docs/session-config-template.md +6 -29
- package/docs/telemetry.md +157 -3
- package/docs/vault-docs-architecture.md +50 -11
- package/hooks/_lib/hook-import-set.json +1487 -0
- package/hooks/_lib/subagent-transcript.mjs +562 -0
- package/hooks/config-protection.mjs +2 -2
- package/hooks/cwd-change-restore.mjs +2 -2
- package/hooks/enforce-commands.mjs +69 -0
- package/hooks/hooks-codex.json +1 -1
- package/hooks/hooks-cursor.json +10 -0
- package/hooks/hooks-pi.json +5 -0
- package/hooks/hooks.json +6 -1
- package/hooks/loop-guard.mjs +3 -3
- package/hooks/on-session-end.mjs +2 -2
- package/hooks/on-session-start.mjs +103 -2
- package/hooks/on-stop.mjs +36 -11
- package/hooks/operator-steer.mjs +2 -2
- package/hooks/post-bash-write-verify.mjs +85 -0
- package/hooks/post-edit-import-probe.mjs +344 -0
- package/hooks/post-subagent-discovery-validator.mjs +187 -431
- package/hooks/post-tool-batch-wave-signal.mjs +118 -4
- package/hooks/post-tool-failure-corrective-context.mjs +2 -2
- package/hooks/post-tooluse-frontend-slop.mjs +3 -3
- package/hooks/pre-bash-destructive-guard.mjs +39 -13
- package/hooks/skill-invocation-telemetry.mjs +17 -5
- package/hooks/subagent-telemetry.mjs +13 -4
- package/monitors/monitors.json +3 -3
- package/package.json +9 -1
- package/pi/prompts/session.md +2 -2
- package/plugin.json +27 -0
- package/scripts/backfill-abandoned-sessions.mjs +50 -4
- package/scripts/backfill-learnings-from-vault.mjs +9 -3
- package/scripts/dialectic-deriver.mjs +73 -8
- package/scripts/export-hw-learnings.mjs +113 -1
- package/scripts/generate-agents-skills.mjs +378 -0
- package/scripts/generate-cursor-adapter.mjs +45 -8
- package/scripts/generate-hook-import-set.mjs +249 -0
- package/scripts/lib/agent-status.mjs +13 -2
- package/scripts/lib/auto-dream.mjs +38 -36
- package/scripts/lib/autonomy/suitability.mjs +6 -0
- package/scripts/lib/autopilot/loop.mjs +2 -2
- package/scripts/lib/ci-status-banner.mjs +220 -75
- package/scripts/lib/codex/plugin-contract.mjs +82 -6
- package/scripts/lib/config/auto-dream.mjs +2 -1
- package/scripts/lib/config/block-header.mjs +8 -0
- package/scripts/lib/config/block-preprocess.mjs +177 -0
- package/scripts/lib/config/broken-window.mjs +2 -1
- package/scripts/lib/config/cold-start.mjs +2 -1
- package/scripts/lib/config/config-protection.mjs +22 -2
- package/scripts/lib/config/context-coverage.mjs +2 -1
- package/scripts/lib/config/cross-repo.mjs +2 -1
- package/scripts/lib/config/custom-phases.mjs +2 -1
- package/scripts/lib/config/dialectic.mjs +2 -1
- package/scripts/lib/config/discovery-validator.mjs +2 -1
- package/scripts/lib/config/dispatcher-autonomy-capture.mjs +24 -1
- package/scripts/lib/config/dispatcher-autonomy.mjs +2 -1
- package/scripts/lib/config/docs-orchestrator.mjs +2 -1
- package/scripts/lib/config/docs-staleness.mjs +2 -1
- package/scripts/lib/config/drift-check.mjs +2 -1
- package/scripts/lib/config/eval.mjs +2 -1
- package/scripts/lib/config/events-rotation.mjs +2 -1
- package/scripts/lib/config/evolve.mjs +8 -2
- package/scripts/lib/config/frontend-slop-hook.mjs +7 -3
- package/scripts/lib/config/gitlab-portfolio.mjs +2 -1
- package/scripts/lib/config/handover-gate.mjs +2 -1
- package/scripts/lib/config/health-endpoints.mjs +7 -2
- package/scripts/lib/config/issue-budget.mjs +2 -1
- package/scripts/lib/config/loop-guard.mjs +2 -1
- package/scripts/lib/config/memory.mjs +2 -1
- package/scripts/lib/config/moc-staleness.mjs +2 -1
- package/scripts/lib/config/persona-gate-wave.mjs +2 -1
- package/scripts/lib/config/private-config-dir.mjs +67 -0
- package/scripts/lib/config/reconcile.mjs +2 -1
- package/scripts/lib/config/remote-hosts.mjs +2 -1
- package/scripts/lib/config/section-extractor.mjs +7 -1
- package/scripts/lib/config/skill-evolution.mjs +2 -1
- package/scripts/lib/config/slopcheck.mjs +2 -1
- package/scripts/lib/config/state-md-lock.mjs +2 -1
- package/scripts/lib/config/templates-first.mjs +2 -1
- package/scripts/lib/config/test.mjs +2 -1
- package/scripts/lib/config/vault-integration.mjs +7 -1
- package/scripts/lib/config/vault-mirror-quality.mjs +2 -1
- package/scripts/lib/config/vault-staleness.mjs +2 -1
- package/scripts/lib/config/vault-sync.mjs +2 -1
- package/scripts/lib/config/verification-auto-fix.mjs +2 -1
- package/scripts/lib/config/wave-reviewers.mjs +2 -1
- package/scripts/lib/config/worktree-orphans.mjs +2 -1
- package/scripts/lib/convergence-monitor.mjs +82 -16
- package/scripts/lib/dispatcher/rank.mjs +124 -48
- package/scripts/lib/ecosystem-health.mjs +16 -2
- package/scripts/lib/eval/engine.mjs +9 -1
- package/scripts/lib/eval/session-resolve.mjs +23 -4
- package/scripts/lib/events.mjs +22 -6
- package/scripts/lib/frontmatter-guard.mjs +131 -13
- package/scripts/lib/gates/gate-full.mjs +26 -0
- package/scripts/lib/gates/gate-helpers.mjs +76 -0
- package/scripts/lib/hardware-pattern-detector.mjs +18 -1
- package/scripts/lib/harness-audit/categories/category4.mjs +31 -11
- package/scripts/lib/host-identity.mjs +50 -11
- package/scripts/lib/instruction-budget-guard.mjs +171 -5
- package/scripts/lib/learnings/evolve-telemetry.mjs +178 -0
- package/scripts/lib/learnings/io.mjs +60 -6
- package/scripts/lib/memory-proposals/store.mjs +30 -22
- package/scripts/lib/owner-config-banner.mjs +43 -6
- package/scripts/lib/owner-config-loader.mjs +21 -10
- package/scripts/lib/owner-interview.mjs +3 -3
- package/scripts/lib/owner-yaml.mjs +207 -14
- package/scripts/lib/platform.mjs +108 -15
- package/scripts/lib/plugin-update-banner.mjs +406 -0
- package/scripts/lib/project-hygiene.mjs +38 -2
- package/scripts/lib/qg-command-drift-banner.mjs +50 -12
- package/scripts/lib/quality-gate.mjs +133 -44
- package/scripts/lib/reconcile/emitter.mjs +68 -6
- package/scripts/lib/reconcile/engine.mjs +13 -4
- package/scripts/lib/reconcile/idempotency.mjs +37 -4
- package/scripts/lib/reconcile/writer.mjs +40 -18
- package/scripts/lib/session-close-backfill.mjs +67 -9
- package/scripts/lib/session-id.mjs +12 -23
- package/scripts/lib/session-identity/own-session.mjs +125 -10
- package/scripts/lib/session-lock-shape.mjs +43 -0
- package/scripts/lib/session-lock.mjs +5 -10
- package/scripts/lib/session-registry.mjs +25 -9
- package/scripts/lib/session-schema/constants.mjs +36 -2
- package/scripts/lib/session-schema/validator.mjs +38 -4
- package/scripts/lib/session-start-probes.mjs +18 -1
- package/scripts/lib/sessions-staleness-banner.mjs +18 -11
- package/scripts/lib/skill-health/join.mjs +17 -4
- package/scripts/lib/state-md.mjs +78 -0
- package/scripts/lib/sunset/walker.mjs +6 -0
- package/scripts/lib/telemetry/schema.mjs +181 -9
- package/scripts/lib/telemetry/sync.mjs +368 -12
- package/scripts/lib/validate/check-agents-skills.mjs +327 -0
- package/scripts/lib/validate/check-agents.mjs +3 -3
- package/scripts/lib/validate/check-cursor-adapter.mjs +234 -72
- package/scripts/lib/validate/check-hooks-symmetry.mjs +45 -16
- package/scripts/lib/validate/check-owner-leakage.mjs +281 -20
- package/scripts/lib/validate/check-skill-links.mjs +163 -0
- package/scripts/lib/validate/check-skill-script-paths.mjs +47 -28
- package/scripts/lib/validate/check-unwired-features.mjs +0 -2
- package/scripts/lib/validate/check-validator-registration.mjs +10 -4
- package/scripts/lib/validate/enumerate-repo-files.mjs +317 -0
- package/scripts/lib/vault-backfill/template.mjs +63 -6
- package/scripts/lib/vault-mirror/process.mjs +165 -42
- package/scripts/lib/vault-mirror/telemetry.mjs +2 -2
- package/scripts/lib/vault-status/narrative-mirror.mjs +127 -18
- package/scripts/lib/wave-executor/dispatch-common.mjs +164 -0
- package/scripts/lib/wave-executor/foreign-dispatch.mjs +7 -142
- package/scripts/lib/wave-executor/remote-dispatch.mjs +5 -7
- package/scripts/lib/wave-resource-gate.mjs +8 -2
- package/scripts/lib/wave-sizing.mjs +4 -1
- package/scripts/lib/wave-transcript-tail.mjs +118 -4
- package/scripts/materialize-wave-scope.mjs +12 -5
- package/scripts/memory-propose.mjs +19 -5
- package/scripts/migrate-cold-start-seed.mjs +4 -1
- package/scripts/parse-config.mjs +60 -3
- package/scripts/release.mjs +337 -29
- package/scripts/repair-invalid-sessions.mjs +3 -3
- package/scripts/run-quality-gate.mjs +128 -11
- package/scripts/sweep-expired-learnings.mjs +90 -0
- package/scripts/sync-vault-schema.mjs +3 -1
- package/scripts/telemetry.mjs +2 -2
- package/scripts/validate-plugin.mjs +161 -0
- package/scripts/validate-wave-scope.mjs +28 -8
- package/scripts/wave-scope-binding.mjs +215 -0
- package/skills/_shared/instruction-file-resolution.md +10 -0
- package/skills/_shared/parallel-aware-preamble.md +1 -0
- package/skills/_shared/platform-tools.md +1 -1
- package/skills/_shared/state-ownership.md +1 -1
- package/skills/architecture/SKILL.md +7 -5
- package/skills/{domain-model/SKILL.md → architecture/references/domain-model.md} +9 -9
- package/skills/autopilot/SKILL.md +4 -18
- package/skills/claude-md-drift-check/SKILL.md +5 -1
- package/skills/claude-md-drift-check/checker.mjs +62 -2
- package/skills/convergence-monitoring/SIGNALS.md +55 -0
- package/skills/discovery/probes/vault-staleness.mjs +37 -13
- package/skills/discovery/probes-arch.md +20 -18
- package/skills/dispatcher/SKILL.md +3 -2
- package/skills/evolve/SKILL.md +65 -26
- package/skills/frontmatter-guard/SKILL.md +11 -5
- package/skills/npm-publish/SKILL.md +1 -1
- package/skills/reconcile/SKILL.md +33 -0
- package/skills/remote-offload/SKILL.md +1 -1
- package/skills/session-end/SKILL.md +18 -905
- package/skills/session-end/phase-3-6-tail.md +10 -3
- package/skills/session-end/plan-verification.md +221 -155
- package/skills/session-end/references/phase-2-quality-gate.md +93 -0
- package/skills/session-end/references/phase-3-documentation-updates.md +229 -0
- package/skills/session-end/references/phase-4a-worktree-cleanup.md +120 -0
- package/skills/session-end/references/phase-4b-worktree-orphan-sweep.md +58 -0
- package/skills/session-end/references/phase-5-issue-cleanup.md +104 -0
- package/skills/session-end/references/session-summary-template.md +62 -0
- package/skills/session-plan/SKILL.md +49 -0
- package/skills/session-start/SKILL.md +22 -904
- package/skills/session-start/phase-8-5-express-path.md +1 -1
- package/skills/session-start/references/phase-1-1-dispatcher-autonomy-capture.md +55 -0
- package/skills/session-start/references/phase-1-2-session-lock.md +140 -0
- package/skills/session-start/references/phase-1-5-session-continuity.md +254 -0
- package/skills/session-start/references/phase-1-7-vault-status-board.md +53 -0
- package/skills/session-start/references/phase-2-7-portfolio-snapshot.md +75 -0
- package/skills/session-start/references/phase-4-ssot-environment-check.md +155 -0
- package/skills/session-start/references/phase-6-5-forced-reads.md +75 -0
- package/skills/session-start/references/phase-6-6-project-intelligence.md +81 -0
- package/skills/session-start/references/phase-6-7-memory-banner-telemetry-consent.md +103 -0
- package/skills/vault-sync/validator.mjs +21 -27
- package/skills/wave-executor/SKILL.md +15 -1
- package/skills/wave-executor/references/wave-loop-dispatch.md +612 -0
- package/skills/wave-executor/references/wave-loop-review.md +570 -0
- package/skills/wave-executor/references/wave-loop-scope-manifest.md +162 -0
- package/skills/wave-executor/wave-loop.md +14 -1309
- package/templates/_shared/journey-manifest.md +10 -6
- package/.cursor/commands/autopilot-multi.md +0 -14
- package/.cursor/commands/contract-version-bump.md +0 -14
- package/.cursor/commands/journey-audit.md +0 -14
- package/.cursor/skills/contract-version-bump/SKILL.md +0 -12
- package/.cursor/skills/daily/SKILL.md +0 -12
- package/.cursor/skills/domain-model/SKILL.md +0 -13
- package/.cursor/skills/journey-audit/SKILL.md +0 -13
- package/.cursor/skills/skill-creator/SKILL.md +0 -13
- package/.cursor/skills/ubiquitous-language/SKILL.md +0 -13
- package/commands/autopilot-multi.md +0 -74
- package/commands/contract-version-bump.md +0 -28
- package/commands/journey-audit.md +0 -43
- package/pi/prompts/autopilot-multi.md +0 -12
- package/pi/prompts/contract-version-bump.md +0 -12
- package/pi/prompts/journey-audit.md +0 -12
- package/scripts/autopilot-multi.mjs +0 -885
- package/scripts/backfill-learnings-expires.mjs +0 -196
- package/scripts/backfill-learnings.mjs +0 -203
- package/scripts/fleet-instruction-scan.mjs +0 -141
- package/scripts/lib/autopilot/dep-graph.mjs +0 -417
- package/scripts/lib/autopilot/multi-killswitch.mjs +0 -184
- package/scripts/lib/webhook-url.mjs +0 -105
- package/scripts/lifecycle-sim-v6.mjs +0 -347
- package/scripts/migrate-learnings-jsonl.mjs +0 -189
- package/scripts/migrate-subagents-jsonl.mjs +0 -196
- package/scripts/upload-social-preview.mjs +0 -316
- package/skills/_shared/model-selection.md +0 -64
- package/skills/contract-version-bump/SKILL.md +0 -219
- package/skills/daily/SKILL.md +0 -222
- package/skills/daily/generate.sh +0 -92
- package/skills/daily/templates/daily.md.tpl +0 -36
- package/skills/journey-audit/SKILL.md +0 -270
- package/skills/skill-creator/SKILL.md +0 -168
- package/skills/ubiquitous-language/SKILL.md +0 -97
- package/skills/vault-sync/package-lock.json +0 -40
- /package/skills/{domain-model → architecture/references}/ADR-FORMAT.md +0 -0
- /package/skills/{domain-model → architecture/references}/CONTEXT-FORMAT.md +0 -0
|
@@ -21,7 +21,7 @@ invalid `--session-type` / `--task-count` / `--repo-root`), exit `2` an unreadab
|
|
|
21
21
|
`--parallel-agents` is optional and tri-state — omitting it asserts nothing and leaves the field
|
|
22
22
|
out of the record, which is NOT the same claim as `false`.
|
|
23
23
|
|
|
24
|
-
`--repo-root` is required and is never defaulted from `
|
|
24
|
+
`--repo-root` is required and is never defaulted from `getProjectDir()` (#941): an ambient
|
|
25
25
|
destination once put a synthetic record into the operator's real fleet ledger.
|
|
26
26
|
|
|
27
27
|
Until #1146 this block was a fenced `js` snippet with an `import` — prose no process runs. Measured
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Phase 1.1: Dispatcher-Autonomy Migration Capture
|
|
2
|
+
|
|
3
|
+
> Sub-file of the session-start skill (#1157 — agentskills.io: SKILL.md core < 500 lines, procedure in `references/`). Extracted VERBATIM from `skills/session-start/SKILL.md`; `SKILL.md` keeps a one-line stub naming this phase and its gate condition.
|
|
4
|
+
|
|
5
|
+
## Phase 1.1: Dispatcher-Autonomy Migration Capture (one-time, per-repo)
|
|
6
|
+
|
|
7
|
+
> Closes session-orchestrator issue #681 (Epic #673 P3 — one-time per-repo dispatcher-autonomy capture). Migration trigger: the first session-start after this feature ships on a repo whose committed `dispatcher-autonomy:` block is still absent. Cross-reference `.claude/rules/ask-via-tool.md` (AUQ via tool, not prose).
|
|
8
|
+
|
|
9
|
+
**WHEN:** Runs after Phase 1 (config read) and BEFORE Phase 1.2 (session-lock acquire). Fires **exactly once per repo** — the write makes the committed block present, so every subsequent session skips it.
|
|
10
|
+
|
|
11
|
+
**WHY (one-time guard):** The committed `## Dispatcher Autonomy` block is the never-re-ask marker. Detect "block absent" via `isDispatcherAutonomyBlockPresent($CLAUDE_MD_CONTENT)` — a raw `/^dispatcher-autonomy:\s*$/m` presence check on the file content. Do NOT use the resolved autonomy value from `$CONFIG`: it returns `'off'` for BOTH "block absent" AND "block present with `autonomy: off`", so it cannot distinguish a first-run migration from a deliberate `off`. Only the raw presence check distinguishes them.
|
|
12
|
+
|
|
13
|
+
> **The guard is gated purely on committed-block PRESENCE, never on the resolved value.** A machine whose effective autonomy differs from the committed default — because `SO_DISPATCHER_AUTONOMY` or `owner.yaml` `dispatcher.autonomy` overrides it — STILL counts as captured the moment the committed block exists, and is never re-asked. Conversely a host with `owner.yaml` `dispatcher.autonomy` set but NO committed block is still asked once at this migration: a host-local override does NOT satisfy the migration guard; only the committed CLAUDE.md / AGENTS.md block does. Even a header-present-but-body-malformed block counts as PRESENT (a malformed block is the operator's to fix, not a re-prompt trigger).
|
|
14
|
+
|
|
15
|
+
**WHAT:** When the block is absent, the coordinator dispatches ONE `AskUserQuestion` using the definition from `scripts/lib/config/dispatcher-autonomy-capture.mjs`:
|
|
16
|
+
|
|
17
|
+
- **Dispatcher autonomy** — `off` (Recommended, fail-closed) | `advisory` | `autonomous-gated`
|
|
18
|
+
|
|
19
|
+
On **any** answer (including `off`) the committed block is written, presented, and never re-asked. The writer persists ONLY the committed default — host-local overrides (`SO_DISPATCHER_AUTONOMY` env, `owner.yaml` `dispatcher.autonomy`) stay host-local and NEVER land in CLAUDE.md.
|
|
20
|
+
|
|
21
|
+
> **Capture writes the committed default; the runtime value flows through `resolveDispatcherAutonomy`.** This phase only persists the operator's one-time choice as the committed baseline. The EFFECTIVE autonomy at run time is resolved separately by `resolveDispatcherAutonomy()` in `scripts/lib/config/dispatcher-autonomy.mjs` with host-local precedence `SO_DISPATCHER_AUTONOMY` env > `owner.yaml` `dispatcher.autonomy` > committed > `off` (#653 pattern). Migration capture never reads or writes those override tiers — it writes the committed tier only, so a machine with an active override differs from the committed default WITHOUT re-triggering this capture.
|
|
22
|
+
|
|
23
|
+
**AUQ (mandatory — use the tool, not prose):** On Claude Code / Cursor IDE, dispatch this via the **`AskUserQuestion` tool** per `.claude/rules/ask-via-tool.md` (AUQ-001) — never an inline markdown "choose 1/2/3" list. Option 1 (`off`) is the recommended, fail-closed default. Only Codex CLI (no `AskUserQuestion`) falls back to a numbered-list prose prompt (AUQ-004 exception 1).
|
|
24
|
+
|
|
25
|
+
**HOW (coordinator steps):**
|
|
26
|
+
|
|
27
|
+
```js
|
|
28
|
+
import {
|
|
29
|
+
getDispatcherAutonomyQuestion,
|
|
30
|
+
isDispatcherAutonomyBlockPresent,
|
|
31
|
+
writeDispatcherAutonomyBlock,
|
|
32
|
+
} from '$PLUGIN_ROOT/scripts/lib/config/dispatcher-autonomy-capture.mjs';
|
|
33
|
+
import { readFileSync } from 'node:fs';
|
|
34
|
+
|
|
35
|
+
const claudeMdPath = `${process.cwd()}/CLAUDE.md`;
|
|
36
|
+
let content = '';
|
|
37
|
+
try { content = readFileSync(claudeMdPath, 'utf8'); } catch { /* no CLAUDE.md — skip */ }
|
|
38
|
+
if (content && !isDispatcherAutonomyBlockPresent(content)) {
|
|
39
|
+
const q = getDispatcherAutonomyQuestion(); // option 1 = 'off' (Recommended, fail-closed)
|
|
40
|
+
// Claude Code / Cursor: dispatch AskUserQuestion([q]) (the TOOL — AUQ-001); collect the
|
|
41
|
+
// selected label (the `autonomy` enum). Never an inline numbered-list prose question here.
|
|
42
|
+
// Codex CLI fallback only (no AskUserQuestion — AUQ-004 exception 1): print q.question +
|
|
43
|
+
// numbered q.options list, read the operator's pick, map it to the option label.
|
|
44
|
+
const autonomy = /* selected option label: 'off' | 'advisory' | 'autonomous-gated' */;
|
|
45
|
+
const result = writeDispatcherAutonomyBlock({ claudeMdPath, autonomy });
|
|
46
|
+
// result: { written: true, path } on first write; { written: false, reason: 'already-present' } if a
|
|
47
|
+
// parallel session already wrote it OR a malformed block already exists (defensive
|
|
48
|
+
// double-write guard re-checks absence against freshly-read content before writing).
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
> Skip silently when no committed `CLAUDE.md` exists (e.g. a not-yet-bootstrapped repo) — the read failure is non-fatal. The capture then runs at bootstrap (Phase 3.5.1) instead.
|
|
53
|
+
|
|
54
|
+
**WHERE:** Appended as a standalone `## Dispatcher Autonomy` H2 in the repo's committed `CLAUDE.md` (NOT a key inside `## Session Config` — the standalone-H2 placement keeps `claude-md-drift-check` Check-6 parity green).
|
|
55
|
+
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# Phase 1.2 + 1.2.1: Session Lock Acquire and Peer-Guard
|
|
2
|
+
|
|
3
|
+
> Sub-file of the session-start skill (#1157 — agentskills.io: SKILL.md core < 500 lines, procedure in `references/`). Extracted VERBATIM from `skills/session-start/SKILL.md`; `SKILL.md` keeps a one-line stub naming this phase and its gate condition.
|
|
4
|
+
|
|
5
|
+
## Phase 1.2: Session Lock Acquire (#330)
|
|
6
|
+
|
|
7
|
+
> **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.
|
|
8
|
+
|
|
9
|
+
> Skip this phase if `persistence` config is `false`.
|
|
10
|
+
|
|
11
|
+
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.
|
|
12
|
+
|
|
13
|
+
**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 raw `session_id` does not exactly match the current session's raw id (a rare divergence — surface via AUQ before overwriting). A matching `semantic_session_id`, STATE.md `session`, or owner proof cannot repair that mismatch. The decision flow below still applies to all three outcomes (active / stale / fs-error) when the prose path needs to acquire.
|
|
14
|
+
|
|
15
|
+
```javascript
|
|
16
|
+
import { acquire, forceAcquire } from 'scripts/lib/session-lock.mjs';
|
|
17
|
+
const result = acquire({ sessionId, mode: sessionType, ttlHours: 4, repoRoot: process.cwd() });
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Where `sessionId` is the physical raw identity for this invocation: the native harness-provided raw id, or a generated UUID when no trustworthy raw id exists. It is the only value passed to `acquire()` and the only live lock/registry ownership key. `semanticSessionId` may be recorded separately as an attribution/history label and may populate STATE.md `session`; neither label is a substitute for `sessionId`. `sessionType` is the session mode (`housekeeping`, `feature`, or `deep`).
|
|
21
|
+
|
|
22
|
+
### Decision flow
|
|
23
|
+
|
|
24
|
+
1. **`result.ok === true`** → lock is held. Continue to Phase 1.5 (Session Continuity). The lock must be released in session-end.
|
|
25
|
+
|
|
26
|
+
2. **`result.ok === false`** with `reason === 'active'**:
|
|
27
|
+
- Another Claude/Codex session holds an active lock in this repo.
|
|
28
|
+
- Present a choice via `AskUserQuestion`:
|
|
29
|
+
```js
|
|
30
|
+
AskUserQuestion({
|
|
31
|
+
questions: [{
|
|
32
|
+
question: `Another session holds the lock here — started ${ageHours}h ago, mode=${existingLock.mode}, host=${existingLock.host}, pid=${existingLock.pid}. Wait, or take the lock?`,
|
|
33
|
+
header: "Session lock",
|
|
34
|
+
multiSelect: false,
|
|
35
|
+
options: [
|
|
36
|
+
{ label: "Abort (Recommended)", description: "Stop here and let the other session finish, then start again. Nothing is written until it releases the lock, and two sessions sharing one wave state overwrite each other's metrics." },
|
|
37
|
+
{ label: "Force-take the lock", description: "Overwrites the active lock and starts anyway. Only when that session is certainly gone — otherwise both keep writing the same wave state and one of them loses everything." },
|
|
38
|
+
],
|
|
39
|
+
}],
|
|
40
|
+
});
|
|
41
|
+
```
|
|
42
|
+
- **Codex CLI / Cursor IDE fallback (numbered Markdown list):**
|
|
43
|
+
```
|
|
44
|
+
Another session holds the lock here — started <ageHours>h ago, mode=<mode>, host=<host>, pid=<pid>. Wait, or take the lock?
|
|
45
|
+
1. Abort (Recommended) — stop here and let the other session finish, then start again; nothing is written until it releases the lock.
|
|
46
|
+
2. Force-take the lock — overwrites the active lock. Only when that session is certainly gone, otherwise both keep writing the same wave state and one loses everything.
|
|
47
|
+
Reply with the number of your choice.
|
|
48
|
+
```
|
|
49
|
+
- 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.
|
|
50
|
+
- On **Force-take**: call `forceAcquire({ sessionId, mode: sessionType, ttlHours: 4, repoRoot: process.cwd() })`. After Phase 1.5 initializes STATE.md, append a deviation via `appendDeviation()`:
|
|
51
|
+
`Force-took session lock from session_id=<existingLock.session_id>, age=<ageHours>h, mode=<existingLock.mode>, pid=<existingLock.pid>`. Continue.
|
|
52
|
+
|
|
53
|
+
3. **`result.ok === false`** with `reason === 'stale-heartbeat'`:
|
|
54
|
+
- A stale lock was found (its last heartbeat is older than its ttl). Likely left behind by a session that crashed or was force-killed. The lock's recorded `pid` is NOT consulted — it belongs to the ephemeral hook subprocess that wrote the lock, never to the session; measured 2026-08-23: 7 of 7 recorded pids were dead, including the live heartbeating session's own (#1137).
|
|
55
|
+
- Present a choice via `AskUserQuestion`:
|
|
56
|
+
```js
|
|
57
|
+
// `heartbeatAgeMinutes` and `ageHours` come straight off the acquire() result (#1137);
|
|
58
|
+
// `sameHost` is not on the result — compute it first. Use hostnamesMatch, NEVER a raw
|
|
59
|
+
// `===` against os.hostname(): the hostname flips spelling on a single machine
|
|
60
|
+
// (measured 2026-08-24: `Mac.home` and `Ferdinands-MacBook-Pro.local` ten minutes apart),
|
|
61
|
+
// so a raw comparison labels this machine's OWN lock "another machine" (#1072).
|
|
62
|
+
// `||`, not `??` — an EMPTY-STRING host_id must fall back to `host`, or
|
|
63
|
+
// hostnamesMatch('', …) is false and this machine reads its own lock as
|
|
64
|
+
// cross-host. Production uses `lockHostCandidate()` from host-identity.mjs.
|
|
65
|
+
const sameHost = hostnamesMatch(existingLock.host_id || existingLock.host, os.hostname());
|
|
66
|
+
AskUserQuestion({
|
|
67
|
+
questions: [{
|
|
68
|
+
question: `A stale session lock is in the way — started ${ageHours}h ago on host=${existingLock.host}${sameHost ? '' : ' (another machine)'}, its ttl=${existingLock.ttl_hours}h has expired, and its last heartbeat was ${Math.round(heartbeatAgeMinutes)} minutes ago. Reclaim it?`,
|
|
69
|
+
header: "Stale lock",
|
|
70
|
+
multiSelect: false,
|
|
71
|
+
options: [
|
|
72
|
+
{ label: "Reclaim (Recommended)", description: "Overwrites the stale lock and continues, because its time-to-live has run out. When that process is really dead, nothing of the old session is lost." },
|
|
73
|
+
{ label: "Abort — investigate manually", description: "Stops here and writes nothing. The lock file `.orchestrator/session.lock` (it names the process that wrote it) tells you whether that session is still alive." },
|
|
74
|
+
],
|
|
75
|
+
}],
|
|
76
|
+
});
|
|
77
|
+
```
|
|
78
|
+
- **Codex CLI / Cursor IDE fallback (numbered Markdown list):**
|
|
79
|
+
```
|
|
80
|
+
A stale session lock is in the way — started <ageHours>h ago on <host>, ttl=<ttlHours>h expired, last heartbeat <heartbeatAgeMinutes> minutes ago. Reclaim it?
|
|
81
|
+
1. Reclaim (Recommended) — overwrites the stale lock and continues, because its time-to-live has run out and that process is no longer holding anything.
|
|
82
|
+
2. Abort — stops here and writes nothing. The lock file `.orchestrator/session.lock` (it names the process that wrote it) tells you whether that session is still alive.
|
|
83
|
+
Reply with the number of your choice.
|
|
84
|
+
```
|
|
85
|
+
- On **Reclaim**: call `forceAcquire({ sessionId, mode: sessionType, ttlHours: 4, repoRoot: process.cwd() })`. After Phase 1.5 initializes STATE.md, append a deviation:
|
|
86
|
+
`Stale-lock reclaim: replaced lock from session_id=<existingLock.session_id>, age=<ageHours>h, pid=<existingLock.pid>`. Continue.
|
|
87
|
+
- On **Abort**: exit cleanly.
|
|
88
|
+
|
|
89
|
+
4. **`result.ok === false`** with `reason === 'fs-error'**:
|
|
90
|
+
- 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.
|
|
91
|
+
|
|
92
|
+
> **New reasons from P1.2 #570:** When called with the optional `activeSessions` argument, `acquire()` can also return `active-incompatible-exclusive`, `active-compatible-parallel`, or `active-readonly-bypass`. Session-start invokes `acquire()` WITHOUT `activeSessions` (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.
|
|
93
|
+
|
|
94
|
+
### Cross-host behaviour
|
|
95
|
+
|
|
96
|
+
When `hostnamesMatch(existingLock.host_id || existingLock.host, os.hostname())` is **false** — never a raw `existingLock.host !== os.hostname()`, which labels this machine's own lock "another machine" the moment the hostname flips spelling (#1072; mirror the Phase-1.2 snippet above) — the lock was written on another machine and nothing local can corroborate its heartbeat. `checkStale()` carries no `pidAlive` field at all (REMOVED in #1151; #1137 had left it as an always-`null` stub) — `heartbeatAgeMinutes` is the magnitude to reason from, and `isLive` the verdict. In this case:
|
|
97
|
+
- For `reason === 'active'`: the recommendation is **Abort** — cross-host locks cannot be verified as dead.
|
|
98
|
+
- For stale reasons: the recommendation is still **Reclaim** only if TTL is clearly expired (>2× ttl_hours). Otherwise default to **Abort**.
|
|
99
|
+
- **Never auto-reclaim cross-host locks** under any circumstance — always present the AUQ and let the user decide.
|
|
100
|
+
- The AUQ question text for cross-host cases should note: `"(cross-host — the heartbeat cannot be corroborated locally)"`. Do NOT phrase it as PID liveness: the pid on a lock belongs to the ephemeral writer subprocess, not the session, and is never probed (#1137/#1151).
|
|
101
|
+
|
|
102
|
+
## Phase 1.2.1: Peer-Guard (Epic #583 defense-in-depth)
|
|
103
|
+
|
|
104
|
+
> Skip this phase if `persistence` config is `false`.
|
|
105
|
+
|
|
106
|
+
After Phase 1.2 acquires (or confirms) the lock, use `findPeers(repoRoot, { mySessionId: callerSessionHint })` for the STATE.md peer guard. `callerSessionHint` is the original semantic attribution label when one exists, otherwise the raw `sessionId`: `findPeers` may translate the semantic hint for the discovered lock/registry surface only after the exact raw binding check in `parallel-aware-preamble.md`, while keeping the original hint for STATE.md. 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).
|
|
107
|
+
|
|
108
|
+
```javascript
|
|
109
|
+
import { findPeers } from '$PLUGIN_ROOT/scripts/lib/peer-discovery.mjs';
|
|
110
|
+
// Keep the STATE.md comparison in its original attribution-label space.
|
|
111
|
+
// findPeers performs the guarded semantic→raw translation only for discovered peers.
|
|
112
|
+
const callerSessionHint = semanticSessionId ?? sessionId;
|
|
113
|
+
const { peers } = await findPeers(process.cwd(), { mySessionId: callerSessionHint });
|
|
114
|
+
const peer = peers.find((p) => p.source === 'state-md') ?? null;
|
|
115
|
+
// Phase 1.2.1 consumes only the 'state-md' subset (STATE.md surface only).
|
|
116
|
+
if (peer) {
|
|
117
|
+
// STATE.md is owned by an active peer — do NOT overwrite.
|
|
118
|
+
// peer.sessionId, peer.mode, peer.currentWave, peer.ageHours are populated.
|
|
119
|
+
// Fire the Worktree-Promotion AUQ from parallel-aware-auq.md.
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
GH#67 note: the `lockSuperseded` advisory-downgrade described in Phase 0.5's outcome handling applies only to the `discovered` peer subset — this phase's `peer` is always `source: 'state-md'`, so a `discovered`-side `lockSuperseded: true` never suppresses this guard; the Worktree-Promotion AUQ still fires exactly as below whenever a live STATE.md peer is found.
|
|
124
|
+
|
|
125
|
+
### Decision flow
|
|
126
|
+
|
|
127
|
+
1. **`peer === null`** → no active peer owns STATE.md. Continue to Phase 1.5.
|
|
128
|
+
2. **`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 from `skills/_shared/parallel-aware-auq.md` (same options the Phase 0.5 preamble would emit on `PROMOTION_OFFER`).
|
|
129
|
+
- User picks "Worktree anlegen + starten" → call `enterWorktree({ ..., rawSessionId, reason: 'worktree-promotion' })` from `scripts/lib/autopilot/worktree-pipeline.mjs` — since #1170 this ONE call also releases the source root: it calls `leaveSourceRoot({ repoRoot, sessionId: rawSessionId, semanticSessionId, reason })` from `scripts/lib/session-transition.mjs` internally, on BOTH success exits, so no separate `leaveSourceRoot` call is made at this site. `rawSessionId` is the RAW physical `session_id` read from this root's `.orchestrator/session.lock` via `readLock({ repoRoot })`, never the semantic label and never `current-session.json` (which may describe a peer, #863) — (#1069 process boundary: this site runs AFTER Phase 1.2 already acquired the lock, so the old root MUST be deregistered and its lock released here, or the new worktree's own Phase 1.2 finds a phantom owner). The return value's `left` field carries `leaveSourceRoot()`'s result; it never throws, so on `left.ok !== true` `enterWorktree` itself emits the stderr WARN `enterWorktree: leaveSourceRoot: <reason>` and the promotion continues regardless — the destination worktree already exists, so aborting here would leave exactly the two-live-roots state the call prevents. Then exit Phase 1 immediately (the new worktree's own session-start runs from scratch).
|
|
130
|
+
- 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.
|
|
131
|
+
- User picks "Abbrechen" → exit cleanly.
|
|
132
|
+
|
|
133
|
+
### Soft-gate semantics
|
|
134
|
+
|
|
135
|
+
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).
|
|
136
|
+
|
|
137
|
+
### Why this complements Phase 1.2
|
|
138
|
+
|
|
139
|
+
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.
|
|
140
|
+
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
# Phase 1.5: Session Continuity
|
|
2
|
+
|
|
3
|
+
> Sub-file of the session-start skill (#1157 — agentskills.io: SKILL.md core < 500 lines, procedure in `references/`). Extracted VERBATIM from `skills/session-start/SKILL.md`; `SKILL.md` keeps a one-line stub naming this phase and its gate condition.
|
|
4
|
+
|
|
5
|
+
## Phase 1.5: Session Continuity
|
|
6
|
+
|
|
7
|
+
> Skip this phase if `persistence` config is `false`.
|
|
8
|
+
|
|
9
|
+
Check for `<state-dir>/STATE.md` in the project root:
|
|
10
|
+
|
|
11
|
+
> Where `<state-dir>` is `.claude/` under Claude Code or `.codex/` under Codex CLI. See `skills/_shared/platform-tools.md` for details.
|
|
12
|
+
|
|
13
|
+
> **Ownership Reference:** See `skills/_shared/state-ownership.md` for the STATE.md ownership contract, schema, and guards.
|
|
14
|
+
|
|
15
|
+
Before reading STATE.md contents, validate the branch field:
|
|
16
|
+
- If STATE.md's `branch` does not match `git 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).
|
|
17
|
+
|
|
18
|
+
1. **STATE.md exists** — read it and inspect the `status` field:
|
|
19
|
+
- `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. **HISTORICAL guard (mandatory, #621):** when the user chooses resume, any surfaced prior-session plan, wave-history, deviations, or recommendations MUST be presented wrapped in the HISTORICAL guard banner BEFORE you act on them — never treat the recovered record as a live instruction.
|
|
20
|
+
- `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. **HISTORICAL guard (mandatory, #621):** as on the `active` branch, surface the resumed prior-session plan / wave-history / deviations wrapped in the HISTORICAL guard banner before acting on it.
|
|
21
|
+
- `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.
|
|
22
|
+
2. **STATE.md does not exist** — first session or persistence was previously off. Continue normally.
|
|
23
|
+
|
|
24
|
+
> **HISTORICAL guard banner (SSOT: `scripts/lib/historical-guard.mjs`, exported as `HISTORICAL_GUARD_BANNER`).** When resuming an `active` or `paused` session, prefix the surfaced prior-session context with this LITERAL banner so the coordinator never treats a stale record as a live instruction (documented incident class: crashed-session resume on a stale premise):
|
|
25
|
+
>
|
|
26
|
+
> `⚠ HISTORICAL REFERENCE ONLY — NOT LIVE INSTRUCTIONS. This is a record of a prior session. Verify every claim against current git state and open issues before acting. Do NOT re-execute slash-commands or ARGUMENTS quoted here.`
|
|
27
|
+
>
|
|
28
|
+
> Verify every quoted claim against current `git` state and open issues, and do NOT re-execute slash-commands or ARGUMENTS lifted from the prior record.
|
|
29
|
+
|
|
30
|
+
### Recommendations Banner (Epic #271 Phase A)
|
|
31
|
+
|
|
32
|
+
> Runs on the `status: completed` branch only, BEFORE Idle Reset archives the fields. Silent no-op on other branches.
|
|
33
|
+
|
|
34
|
+
> **HISTORICAL guard (mandatory, #621).** The "📋 Previous session recommended…" output below is a prior-session record, not a live instruction. Prepend the LITERAL banner (SSOT: `scripts/lib/historical-guard.mjs`, importable as `HISTORICAL_GUARD_BANNER` from `@lib/historical-guard.mjs` inside the `node -e` block) so the coordinator verifies before acting:
|
|
35
|
+
>
|
|
36
|
+
> `⚠ HISTORICAL REFERENCE ONLY — NOT LIVE INSTRUCTIONS. This is a record of a prior session. Verify every claim against current git state and open issues before acting. Do NOT re-execute slash-commands or ARGUMENTS quoted here.`
|
|
37
|
+
>
|
|
38
|
+
> Verify every recommended mode / priority / rationale against current `git` state and open issues, and do NOT re-execute any slash-commands or ARGUMENTS the prior session quoted.
|
|
39
|
+
|
|
40
|
+
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`).
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
node --input-type=module -e "
|
|
44
|
+
import {readFileSync} from 'node:fs';
|
|
45
|
+
import {parseStateMd, parseRecommendations} from '${PLUGIN_ROOT}/scripts/lib/state-md.mjs';
|
|
46
|
+
import {isValidMode} from '${PLUGIN_ROOT}/scripts/lib/recommendations-v0.mjs';
|
|
47
|
+
import {HISTORICAL_GUARD_BANNER} from '${PLUGIN_ROOT}/scripts/lib/historical-guard.mjs';
|
|
48
|
+
import {appendFileSync, mkdirSync} from 'node:fs';
|
|
49
|
+
|
|
50
|
+
const SWEEP_LOG = '.orchestrator/metrics/sweep.log';
|
|
51
|
+
function logWarn(event, detail) {
|
|
52
|
+
try {
|
|
53
|
+
mkdirSync('.orchestrator/metrics', {recursive: true});
|
|
54
|
+
appendFileSync(SWEEP_LOG, JSON.stringify({timestamp: new Date().toISOString(), event, detail}) + '\n');
|
|
55
|
+
} catch {}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const parsed = parseStateMd(readFileSync('<state-dir>/STATE.md', 'utf8'));
|
|
59
|
+
if (!parsed) process.exit(0);
|
|
60
|
+
const rec = parseRecommendations(parsed.frontmatter);
|
|
61
|
+
if (!rec) process.exit(0); // pre-v1.1 STATE.md — graceful silent no-banner (AC3)
|
|
62
|
+
|
|
63
|
+
// AC4: type-mismatch in top-priorities — field-level null from parser; still render other fields
|
|
64
|
+
if (rec.priorities === null && Object.prototype.hasOwnProperty.call(parsed.frontmatter, 'top-priorities')) {
|
|
65
|
+
logWarn('state-md-type-mismatch', {field: 'top-priorities', got: typeof parsed.frontmatter['top-priorities']});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// AC4: partial fields — warn but still render available ones
|
|
69
|
+
const missingCount = [rec.mode, rec.priorities, rec.carryoverRatio, rec.completionRate, rec.rationale].filter((x) => x === null).length;
|
|
70
|
+
if (missingCount > 0 && missingCount < 5) {
|
|
71
|
+
logWarn('state-md-partial-recommendation', {missing: missingCount});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const modeOk = rec.mode && isValidMode(rec.mode);
|
|
75
|
+
const mode = modeOk ? rec.mode : '(unknown-mode)';
|
|
76
|
+
const rationale = rec.rationale || '(no rationale)';
|
|
77
|
+
const pct = (x) => (x === null ? '—' : Math.round(x * 100) + '%');
|
|
78
|
+
console.log(HISTORICAL_GUARD_BANNER); // #621 — prior-session record, verify before acting; do NOT re-execute quoted commands/ARGUMENTS
|
|
79
|
+
console.log('📋 Previous session recommended: ' + mode + ' — ' + rationale + ' (completion: ' + pct(rec.completionRate) + ', carryover: ' + pct(rec.carryoverRatio) + ')');
|
|
80
|
+
if (Array.isArray(rec.priorities) && rec.priorities.length > 0) {
|
|
81
|
+
console.log(' Suggested issues: ' + rec.priorities.map((id) => '#' + id).join(', '));
|
|
82
|
+
}
|
|
83
|
+
"
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
**Behavior matrix (AC1/AC3/AC4):**
|
|
87
|
+
- All 5 fields present + valid → banner line + suggested-issues line (if priorities non-empty).
|
|
88
|
+
- Field(s) absent entirely → no banner (graceful no-op, no WARN).
|
|
89
|
+
- 1–4 fields present (partial) → banner renders with `—` for missing, WARN `state-md-partial-recommendation` to sweep.log.
|
|
90
|
+
- `top-priorities` is not an array (type-mismatch) → treated as null, WARN `state-md-type-mismatch` to sweep.log, other fields still render.
|
|
91
|
+
- Unknown `recommended-mode` value → banner shows `(unknown-mode)` instead of the string.
|
|
92
|
+
|
|
93
|
+
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.
|
|
94
|
+
|
|
95
|
+
### Idle Reset (completed-branch only)
|
|
96
|
+
|
|
97
|
+
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.
|
|
98
|
+
|
|
99
|
+
Reset rules — applies ONLY on the `completed` branch. Do NOT perform this reset on `active` or `paused`; those paths stay user-interactive via AskUserQuestion.
|
|
100
|
+
|
|
101
|
+
1. Set frontmatter `status: idle`.
|
|
102
|
+
2. Clear `current-wave` (set to `0`).
|
|
103
|
+
3. Move the existing `## Wave History` body into a new `## Previous Session` archive section (retain the record, but demote it below the new session's live state). Remove the original `## Wave History` section — wave-executor will recreate it on the next wave.
|
|
104
|
+
4. Clear `## Deviations` (leave the heading with an empty body so the schema is preserved).
|
|
105
|
+
- **PRESERVE `## What Not To Retry` (#623):** do NOT clear, demote, or drop this section during the Idle Reset. Unlike `## Deviations` (per-session, emptied above) and `## Wave History` (demoted into `## Previous Session`), `## What Not To Retry` is a **cross-session continuity slot** — its entries must survive into the next session so session-start Phase 6.5.1 can surface them. Leave the section, its heading, and all entries byte-for-byte intact.
|
|
106
|
+
- **PRESERVE `## Open Questions` (#772):** do NOT clear, demote, or drop this section during the Idle Reset. Unlike `## Deviations` (per-session, emptied above) and `## Wave History` (demoted into `## Previous Session`), `## Open Questions` is a **cross-session continuity slot** — unanswered entries must survive into the next session so session-start Phase 6.5.2 can surface them as a forced-read. Leave the section, its heading, and all entries (answered and unanswered) byte-for-byte intact.
|
|
107
|
+
5. 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.
|
|
108
|
+
6. **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 via `updateFrontmatterFields(contents, {field: null, ...})` (null value deletes the key). Then prepend a readable block (NOT YAML) to the `## Previous Session` body:
|
|
109
|
+
|
|
110
|
+
```markdown
|
|
111
|
+
### Recommendations (archived from v1.1 frontmatter)
|
|
112
|
+
- **Recommended mode:** <mode>
|
|
113
|
+
- **Rationale:** <rationale>
|
|
114
|
+
- **Completion rate:** <XX%>
|
|
115
|
+
- **Carryover ratio:** <XX%>
|
|
116
|
+
- **Top priorities:** #<id>, #<id>, … _(or "none")_
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Omit individual bullets for null-valued fields. If all 5 are null (i.e., `parseRecommendations` returned non-null but every field is null after type-coercion), skip the archival block entirely.
|
|
120
|
+
7. **Scope-baseline key deletion (Epic #894 S5, #898):** If ANY of the 5 `scope-baseline-*` frontmatter keys (`scope-baseline-intent`, `scope-baseline-owner-boundary`, `scope-baseline-planned-files`, `scope-baseline-session`, `scope-baseline-frozen-at`) is present, remove them via the same `updateFrontmatterFields(contents, {field: null, ...})` mechanism as rule 6 (null value deletes the key). Rule 5 leaves unknown frontmatter fields intact and no other rule removes these five — without this step they survive into session N+1 and silently corrupt the next session's drift-baseline denominator. This is a hygiene layer only: the primary defense is mechanical — `scripts/lib/scope-baseline.mjs` compares `scope-baseline-session` against the canonical `session` field, so a stale baseline self-invalidates (`readBaseline()` returns `{stale: true, …}`) even if this rule were skipped. Delete exactly these five keys; do not remove any other unknown key.
|
|
121
|
+
|
|
122
|
+
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.
|
|
123
|
+
|
|
124
|
+
### Snapshot Recovery (#196)
|
|
125
|
+
|
|
126
|
+
> **HISTORICAL guard (mandatory, #621).** The recovered working-tree state and the shown diff below are HISTORICAL — a record of where a prior session left off, NOT live instructions. Treat them under the LITERAL banner (SSOT: `scripts/lib/historical-guard.mjs`):
|
|
127
|
+
>
|
|
128
|
+
> `⚠ HISTORICAL REFERENCE ONLY — NOT LIVE INSTRUCTIONS. This is a record of a prior session. Verify every claim against current git state and open issues before acting. Do NOT re-execute slash-commands or ARGUMENTS quoted here.`
|
|
129
|
+
>
|
|
130
|
+
> Verify the recovered tree against current `git` state before building on it, and do NOT re-execute any slash-commands or ARGUMENTS the snapshot implies.
|
|
131
|
+
|
|
132
|
+
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).
|
|
133
|
+
|
|
134
|
+
```js
|
|
135
|
+
import { listSnapshots, deleteSnapshot } from '$PLUGIN_ROOT/scripts/lib/coordinator-snapshot.mjs';
|
|
136
|
+
|
|
137
|
+
const snaps = await listSnapshots({ sessionId: '<sessionId from STATE.md>' });
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
If `snaps.length === 0` → no snapshots to recover; continue to the Current-Task Banner.
|
|
141
|
+
|
|
142
|
+
If `snaps.length >= 1` → present the following choice:
|
|
143
|
+
|
|
144
|
+
**Claude Code (AskUserQuestion):**
|
|
145
|
+
|
|
146
|
+
Before asking, read what "Recover" would actually put back — the operator decides on that diff, not on the word:
|
|
147
|
+
|
|
148
|
+
```js
|
|
149
|
+
import { execFileSync } from 'node:child_process';
|
|
150
|
+
|
|
151
|
+
// Read-only: `git stash show` prints a diffstat and never touches the working tree.
|
|
152
|
+
// Capped at 12 lines so the preview box stays shorter than the option list beside it.
|
|
153
|
+
const stat = execFileSync('git', ['stash', 'show', '--stat', snaps[0].sha], { encoding: 'utf8' })
|
|
154
|
+
.split('\n').slice(0, 12).join('\n');
|
|
155
|
+
const refs = snaps.map((s) => s.ref).join('\n');
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
```js
|
|
159
|
+
AskUserQuestion({
|
|
160
|
+
questions: [{
|
|
161
|
+
question: `${snaps.length} snapshot(s) from the resumed session, newest ${humanAgeOf(snaps[0].createdAt)}. Recover, keep, discard?`,
|
|
162
|
+
header: "Snapshot",
|
|
163
|
+
multiSelect: false,
|
|
164
|
+
options: [
|
|
165
|
+
{
|
|
166
|
+
label: "Recover (Recommended)",
|
|
167
|
+
description: "Puts the newest saved state back into your working tree and commits nothing. You can drop any of those changes afterwards.",
|
|
168
|
+
preview: `These files come back:\n\n\`\`\`\n${stat}\n\`\`\``,
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
label: "Keep as backup",
|
|
172
|
+
description: "Nothing happens now: `refs/so-snapshots/*` (the saved states) stay, and `git stash apply $(git rev-parse <ref>)` (this puts one back) works later.",
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
label: "Discard all",
|
|
176
|
+
description: "Deletes every saved state of this session for good: `refs/so-snapshots/<sessionId>/*` (all of them) is gone, and there is no second copy.",
|
|
177
|
+
preview: `Deleted for good:\n\n\`\`\`\n${refs}\n\`\`\``,
|
|
178
|
+
},
|
|
179
|
+
],
|
|
180
|
+
}],
|
|
181
|
+
});
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
`preview` renders beside the option list and only works with `multiSelect: false`. It is used here because the answer decides which literal text lands in the working tree — "Recover" is a diff, "Discard all" is a list of refs that stop existing. "Keep as backup" carries none: keeping is exactly the state the operator already sees.
|
|
185
|
+
|
|
186
|
+
**Codex CLI / Cursor IDE fallback (numbered Markdown list):**
|
|
187
|
+
|
|
188
|
+
These harnesses have no preview box, so the same diffstat is printed inline — it is the only place the operator ever sees it:
|
|
189
|
+
|
|
190
|
+
```markdown
|
|
191
|
+
"Recover" would put these files back:
|
|
192
|
+
|
|
193
|
+
<git stash show --stat <snaps[0].sha>, capped at 12 lines>
|
|
194
|
+
|
|
195
|
+
<N> snapshot(s) from the resumed session, newest <age>. Recover, keep, discard?
|
|
196
|
+
|
|
197
|
+
1. **Recover (Recommended)** — puts the newest saved state back into your working tree and commits nothing. You can drop any of those changes afterwards.
|
|
198
|
+
2. **Keep as backup** — nothing happens now: `refs/so-snapshots/*` (the saved states) stay, and `git stash apply $(git rev-parse <ref>)` (this puts one back) works later.
|
|
199
|
+
3. **Discard all** — deletes every saved state of this session for good: `refs/so-snapshots/<sessionId>/*` (all of them) is gone, and there is no second copy.
|
|
200
|
+
|
|
201
|
+
Reply with the number of your choice.
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
On user choice:
|
|
205
|
+
- **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 resulting `git diff --stat` so the user sees what landed.
|
|
206
|
+
- **Keep as backup** → no-op. Log in the Session Overview: `Snapshot(s) retained: <N>. Recover manually with \`git stash apply <sha>\`.`
|
|
207
|
+
- **Discard all** → for each snapshot in `snaps`, call `deleteSnapshot({refName: snap.ref})`. Log count.
|
|
208
|
+
|
|
209
|
+
Snapshot age (`humanAgeOf`) is derived from `snap.createdAt` (ISO 8601 from `git for-each-ref --format='%(committerdate:iso8601)'`). A simple inline helper:
|
|
210
|
+
|
|
211
|
+
```js
|
|
212
|
+
function humanAgeOf(iso) {
|
|
213
|
+
const mins = Math.floor((Date.now() - new Date(iso).getTime()) / 60000);
|
|
214
|
+
if (mins < 60) return `${mins}m ago`;
|
|
215
|
+
const hrs = Math.floor(mins / 60);
|
|
216
|
+
if (hrs < 24) return `${hrs}h ago`;
|
|
217
|
+
return `${Math.floor(hrs / 24)}d ago`;
|
|
218
|
+
}
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### Current-Task Banner (#184)
|
|
222
|
+
|
|
223
|
+
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.
|
|
224
|
+
|
|
225
|
+
```bash
|
|
226
|
+
node --input-type=module -e "
|
|
227
|
+
import {readFileSync} from 'node:fs';
|
|
228
|
+
import {readCurrentTask} from '${PLUGIN_ROOT}/scripts/lib/state-md.mjs';
|
|
229
|
+
try {
|
|
230
|
+
const t = readCurrentTask(readFileSync('<state-dir>/STATE.md', 'utf8'));
|
|
231
|
+
if (t) console.log('Current task: ' + t.description);
|
|
232
|
+
} catch {}
|
|
233
|
+
"
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
Skip silently when STATE.md is absent or unreadable. The banner is informational, not load-bearing.
|
|
237
|
+
|
|
238
|
+
Also read `<state-dir>/STATUS.md` if it exists for additional project-level context.
|
|
239
|
+
|
|
240
|
+
### Session-Profile Persistence (`ultradeep` alias)
|
|
241
|
+
|
|
242
|
+
> **CONTENT ADDITION — not part of the #1157 move.** Applies to the STATE.md initialization write (Phase 1b), the same write that sets `session-type`. Rationale, wave shape and budgets: `docs/prd/2026-09-06-ultradeep-session-profile.md` — do not restate them here. Alias resolution: `commands/session.md` § Argument alias.
|
|
243
|
+
|
|
244
|
+
When the `/session` argument was the `ultradeep` ALIAS, persist the profile alongside the type:
|
|
245
|
+
|
|
246
|
+
```js
|
|
247
|
+
import { setSessionProfile } from '${PLUGIN_ROOT}/scripts/lib/state-md.mjs';
|
|
248
|
+
// `session-type` stays `deep` and must NEVER become `ultradeep` — an unknown type
|
|
249
|
+
// degrades SILENTLY in two places: `scripts/lib/telemetry/schema.mjs:83` maps it to
|
|
250
|
+
// `other`, `scripts/lib/session-close-backfill.mjs:71` labels it `housekeeping`.
|
|
251
|
+
contents = setSessionProfile(contents, 'ultradeep');
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
For every other argument, write **nothing** — absence is the contract, never `''`, `none` or `null` as a value. `setSessionProfile(contents, null)` deletes a stale key inherited from a previous session's record; it throws on an empty-string profile, so never pass one.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Phase 1.7: Vault Live-Status Board (#674)
|
|
2
|
+
|
|
3
|
+
> Sub-file of the session-start skill (#1157 — agentskills.io: SKILL.md core < 500 lines, procedure in `references/`). Extracted VERBATIM from `skills/session-start/SKILL.md`; `SKILL.md` keeps a one-line stub naming this phase and its gate condition.
|
|
4
|
+
|
|
5
|
+
## Phase 1.7: Vault Live-Status Board (#674)
|
|
6
|
+
|
|
7
|
+
> Skip this phase silently when `vault-integration.enabled` is not `true` in Session Config. Use the same `jq -r` idiom Phase 2.7 uses (`echo "$CONFIG" | jq -r '."vault-integration".enabled // false'`). When the value is anything other than `true`, do nothing and proceed to Phase 2 — no banner, no warning.
|
|
8
|
+
|
|
9
|
+
When active, this phase marks THIS repo as live on the cross-repo vault board (`<vault-dir>/01-projects/_active-sessions.md`) so an operator scanning the vault can see, at a glance, which repos have a session in flight. Epic #673 / PRD §FA-1.
|
|
10
|
+
|
|
11
|
+
### Config check
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
VAULT_ENABLED=$(echo "$CONFIG" | jq -r '."vault-integration".enabled // false')
|
|
15
|
+
if [ "$VAULT_ENABLED" != "true" ]; then
|
|
16
|
+
exit 0 # silent no-op — vault integration disabled
|
|
17
|
+
fi
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
### Dispatch
|
|
21
|
+
|
|
22
|
+
Call `sweepBoard` from `scripts/lib/vault-status/board-writer.mjs` — the host-wide sweep (issue #716):
|
|
23
|
+
|
|
24
|
+
```js
|
|
25
|
+
import { sweepBoard } from 'scripts/lib/vault-status/board-writer.mjs';
|
|
26
|
+
|
|
27
|
+
await sweepBoard({
|
|
28
|
+
repoRoot: process.cwd(),
|
|
29
|
+
});
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
`sweepBoard` enumerates candidate repos host-wide (`enumerateCandidates` — confinement root `~/Projects` plus any `cross-repo.projects` config-declared repos, issue #676), re-derives the board status for every BUSY repo it finds (`in-progress` or `force-closed`, never `frei`), unions in THIS repo so its own row is always re-derived, and writes the board in one idempotent merge. **A crashed session in ANY repo now renders `force-closed` on the board from THIS repo's session-start** — not only from that repo's own next session-start/-end.
|
|
33
|
+
|
|
34
|
+
> **Call-site contract:** `sweepBoard` is now the primary call. `explicitStatus` is **inert for `'in-progress'`** — `collectRows` only honors an explicit per-repo `status: 'closed'` override; THIS repo's `in-progress` row is always rendered from its own **live `session.lock` lease** (already written/heartbeated by Phase 1.2's `acquire()`), never from a passed-in status string. If constructing `repos` manually for a narrower sweep, `collectRows` requires `{ repoRoot }` object descriptors and silently skips bare path strings (`board-writer.mjs` `collectRows` guard) — `sweepBoard`/`buildSweepRepos` already produce the correct shape, so this only matters for a hand-rolled `mirrorBoard({ repos })` call.
|
|
35
|
+
|
|
36
|
+
This single call does three things:
|
|
37
|
+
|
|
38
|
+
1. **Sets THIS repo's board row to `in-progress`** with the current semantic-session-id **attribution label** (never a lock/registry ownership key), branch, mode, and heartbeat (read off this repo's `session.lock` v2 lease + the host-wide registry — both already written by Phase 1.2's `acquire()`).
|
|
39
|
+
2. **Re-derives THIS repo's status from its live lease**, so a stale lease left by a prior crashed session in this same repo renders as `force-closed` (heartbeat older than the v2 ttl, default 4h — `DEFAULT_TTL_HOURS` in `scripts/lib/session-lock.mjs`, evaluated via `isLockLive`) and is **never silently dropped** — its fields are read straight off the dead lock.
|
|
40
|
+
3. **Re-derives every OTHER busy repo's status host-wide** via `enumerateCandidates` — a dead lease in repo B renders `force-closed` on the board the next time ANY repo's session-start runs `sweepBoard`, closing the #676→#716 gap. `frei` (lock-less) repos are excluded from re-derivation to avoid board noise; their prior rows, and the prior rows of any repo `enumerateCandidates` did not surface, are preserved unchanged via the idempotent merge — never dropped.
|
|
41
|
+
|
|
42
|
+
`sweepBoard` internally calls `mirrorBoard`, which re-reads Session Config, resolves the host-local vault-dir, and **silently no-ops** (returning `{ action: 'skipped-vault-disabled' }`) when `vault-integration.enabled` is not `true`, the vault-dir is absent, the vault resolves outside `$HOME`, or the config is unreadable. The Bash gate above is the fast-path skip; this internal guard is the defense-in-depth backstop — both agree on the same condition.
|
|
43
|
+
|
|
44
|
+
### Safety invariants
|
|
45
|
+
|
|
46
|
+
- **Generator-marked + idempotent.** The board carries the `_generator: session-orchestrator-active-sessions@1` frontmatter sentinel; repeated writes that produce identical content are no-ops, so re-running this phase never churns the file.
|
|
47
|
+
- **Host-local + git-ignorable.** The board lives under the operator's vault tree (under `$HOME`), never inside any repo — it is never committed.
|
|
48
|
+
- **NEVER touches the human-authored `_overview.md`.** The writer hard-refuses any path whose basename is `_overview.md` (returns `{ action: 'skipped-handwritten' }`), and only ever overwrites files it owns (frontmatter `_generator` matches the marker). The handwritten overview is structurally safe.
|
|
49
|
+
|
|
50
|
+
### Non-blocking behavior
|
|
51
|
+
|
|
52
|
+
This is **best-effort**, exactly like the Phase 4 banners: a board-write failure (I/O error, thrown exception, malformed lease, or a failed host-wide enumeration) MUST NOT halt session-start. `sweepBoard` already degrades internally — if `enumerateCandidates` throws for any reason, it falls back to the pre-#716 single-repo write (`mirrorBoard({ repoRoot, explicitStatus: 'in-progress' })`) so the board write still happens. On top of that internal fallback, the coordinator MUST STILL wrap the `sweepBoard` call so any remaining error is swallowed and logged as a single WARN line, then continue to Phase 2. Session-start is never blocked by a vault-board failure.
|
|
53
|
+
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# Phase 2.7: GitLab Portfolio Snapshot (#41)
|
|
2
|
+
|
|
3
|
+
> Sub-file of the session-start skill (#1157 — agentskills.io: SKILL.md core < 500 lines, procedure in `references/`). Extracted VERBATIM from `skills/session-start/SKILL.md`; `SKILL.md` keeps a one-line stub naming this phase and its gate condition.
|
|
4
|
+
|
|
5
|
+
## Phase 2.7: GitLab Portfolio Snapshot (#41)
|
|
6
|
+
|
|
7
|
+
> Skip this phase if `gitlab-portfolio.enabled` is not `true` in Session Config (default: `false`). Also skip silently when `vault-integration.enabled` is `false` or `vault-integration.vault-dir` is absent.
|
|
8
|
+
|
|
9
|
+
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.
|
|
10
|
+
|
|
11
|
+
### Config check
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
PORTFOLIO_ENABLED=$(echo "$CONFIG" | jq -r '."gitlab-portfolio".enabled // false')
|
|
15
|
+
VAULT_ENABLED=$(echo "$CONFIG" | jq -r '."vault-integration".enabled // false')
|
|
16
|
+
VAULT_DIR=$(echo "$CONFIG" | jq -r '."vault-integration"."vault-dir" // empty')
|
|
17
|
+
PORTFOLIO_MODE=$(echo "$CONFIG" | jq -r '."gitlab-portfolio".mode // "warn"')
|
|
18
|
+
|
|
19
|
+
if [ "$PORTFOLIO_ENABLED" != "true" ] || [ "$VAULT_ENABLED" != "true" ] || [ -z "$VAULT_DIR" ]; then
|
|
20
|
+
exit 0 # silent no-op
|
|
21
|
+
fi
|
|
22
|
+
if [ "$PORTFOLIO_MODE" = "off" ]; then
|
|
23
|
+
exit 0 # silent no-op
|
|
24
|
+
fi
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### Dispatch
|
|
28
|
+
|
|
29
|
+
Invoke `scripts/lib/gitlab-portfolio/cli.mjs` in dry-run mode (same orchestrator used by `/portfolio`):
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
node scripts/lib/gitlab-portfolio/cli.mjs \
|
|
33
|
+
--vault-dir "$VAULT_DIR" \
|
|
34
|
+
--dry-run \
|
|
35
|
+
--session-start-snapshot # instructs cli.mjs to emit the compact JSON summary for banner rendering
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
The CLI emits a single-line JSON to stdout:
|
|
39
|
+
|
|
40
|
+
```json
|
|
41
|
+
{ "repos": 16, "openIssues": 42, "critical": 3, "stale": 5, "lastRefresh": "2026-05-16T08:00:00Z" }
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Banner rendering
|
|
45
|
+
|
|
46
|
+
Parse the JSON and render the banner into the Session Overview:
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
📊 Portfolio: 16 repos · 42 open issues · 3 critical · 5 stale (>30d)
|
|
50
|
+
Last refresh: 2026-05-16 08:00 UTC
|
|
51
|
+
Run /portfolio to refresh.
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Failure behavior
|
|
55
|
+
|
|
56
|
+
Governed by the `mode` field from `gitlab-portfolio:` config:
|
|
57
|
+
|
|
58
|
+
- `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.
|
|
59
|
+
- `strict`: if the CLI fails, emit a single-line banner `❌ portfolio snapshot failed — run /portfolio for details` into the Session Overview. Do NOT halt session-start — session-start must never be blocked by portfolio failures.
|
|
60
|
+
- `off`: silent no-op (already handled by the config check above).
|
|
61
|
+
|
|
62
|
+
### Performance budget
|
|
63
|
+
|
|
64
|
+
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`:
|
|
65
|
+
|
|
66
|
+
```json
|
|
67
|
+
{"timestamp":"<ISO>","event":"portfolio-snapshot-timeout","detail":{"timeout_ms":8000}}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Proceed to Phase 3 without blocking.
|
|
71
|
+
|
|
72
|
+
### Cross-reference
|
|
73
|
+
|
|
74
|
+
See `commands/portfolio.md` for the `/portfolio` command (full write path, `--dry-run`, `--repo` single-repo testing).
|
|
75
|
+
|