session-orchestrator 3.23.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 +13 -0
- 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 +1401 -0
- package/NOTICE +11 -6
- package/README.md +127 -92
- package/agents/db-specialist.md +0 -1
- 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 +249 -48
- package/docs/codex-setup.md +66 -22
- package/docs/components.md +37 -16
- package/docs/cursor-setup.md +6 -2
- package/docs/events-schema.md +51 -10
- 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 +8 -8
- package/docs/session-config-reference.md +120 -61
- package/docs/session-config-template.md +40 -33
- package/docs/telemetry/telemetry-claims.md +11 -10
- package/docs/telemetry.md +187 -4
- package/docs/vault-docs-architecture.md +50 -11
- package/hooks/_lib/atomic-json.mjs +111 -0
- package/hooks/_lib/hook-import-set.json +1487 -0
- package/hooks/_lib/subagent-paths.mjs +143 -0
- package/hooks/_lib/subagent-transcript.mjs +562 -0
- package/hooks/config-protection.mjs +2 -2
- package/hooks/cwd-change-restore.mjs +11 -31
- package/hooks/enforce-commands.mjs +69 -0
- package/hooks/enforce-scope.mjs +35 -6
- 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 +280 -14
- package/hooks/on-session-start.mjs +153 -4
- package/hooks/on-stop.mjs +371 -17
- package/hooks/operator-steer.mjs +2 -2
- package/hooks/post-bash-write-verify.mjs +189 -4
- package/hooks/post-edit-import-probe.mjs +344 -0
- package/hooks/post-subagent-discovery-validator.mjs +278 -392
- package/hooks/post-tool-batch-wave-signal.mjs +272 -44
- package/hooks/post-tool-failure-corrective-context.mjs +11 -34
- package/hooks/post-tooluse-frontend-slop.mjs +3 -3
- package/hooks/pre-bash-destructive-guard.mjs +39 -13
- package/hooks/pre-bash-memory-propose-audit.mjs +13 -7
- package/hooks/skill-invocation-telemetry.mjs +17 -5
- package/hooks/subagent-telemetry.mjs +24 -30
- 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/autopilot.mjs +26 -12
- package/scripts/backfill-abandoned-sessions.mjs +130 -15
- package/scripts/backfill-learnings-from-vault.mjs +9 -3
- package/scripts/dialectic-deriver.mjs +73 -8
- package/scripts/emit-event.mjs +10 -2
- 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/auq/parse.mjs +5 -29
- package/scripts/lib/auto-dialectic.mjs +68 -0
- 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/autopilot/worktree-pipeline.mjs +82 -6
- package/scripts/lib/build-live-signals.mjs +25 -22
- package/scripts/lib/ci-status-banner.mjs +220 -75
- package/scripts/lib/codex/plugin-contract.mjs +82 -6
- package/scripts/lib/cold-start-detector.mjs +23 -14
- package/scripts/lib/config/auto-dream.mjs +2 -1
- package/scripts/lib/config/block-header.mjs +63 -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 +9 -3
- 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 +388 -0
- 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 +234 -0
- 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/config.mjs +31 -3
- package/scripts/lib/convergence-monitor.mjs +82 -16
- package/scripts/lib/dispatcher/enumerate.mjs +2 -17
- 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-schema.mjs +48 -0
- package/scripts/lib/events.mjs +256 -7
- package/scripts/lib/evolve/autonomy-verdict.mjs +9 -4
- package/scripts/lib/evolve/autopilot-effectiveness.mjs +18 -1
- 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/gitlab-portfolio/cli.mjs +3 -15
- package/scripts/lib/hardware-pattern-detector.mjs +18 -1
- package/scripts/lib/harness-audit/categories/category1.mjs +17 -6
- 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-banner.mjs +20 -8
- 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/peer-discovery.mjs +20 -2
- 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 +249 -9
- package/scripts/lib/reconcile/idempotency.mjs +37 -4
- package/scripts/lib/reconcile/writer.mjs +40 -18
- package/scripts/lib/scope-gate.mjs +36 -0
- package/scripts/lib/session-close-backfill.mjs +125 -18
- package/scripts/lib/session-discovery.mjs +57 -3
- package/scripts/lib/session-end/phase-skip.mjs +2 -2
- package/scripts/lib/session-id.mjs +12 -23
- package/scripts/lib/session-identity/own-session.mjs +187 -11
- 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/session-transition.mjs +1 -1
- package/scripts/lib/sessions-canonical.mjs +446 -0
- 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 +255 -17
- package/scripts/lib/telemetry/sync.mjs +417 -24
- package/scripts/lib/tmux-layout/telemetry.mjs +14 -2
- 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-doc-cli-commands.mjs +9 -33
- package/scripts/lib/validate/check-hooks-emit-event-guard.mjs +370 -0
- 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 +455 -0
- package/scripts/lib/validate/check-untracked-test-deps.mjs +10 -0
- package/scripts/lib/validate/check-unwired-features.mjs +0 -9
- package/scripts/lib/validate/check-validator-registration.mjs +254 -0
- package/scripts/lib/validate/check-vcs-repo-flag.mjs +6 -28
- package/scripts/lib/validate/enumerate-repo-files.mjs +317 -0
- package/scripts/lib/validate/markdown-fences.mjs +196 -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/board-lock.mjs +185 -0
- package/scripts/lib/vault-status/board-writer.mjs +174 -135
- package/scripts/lib/vault-status/narrative-mirror.mjs +129 -37
- 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 +502 -0
- package/scripts/lib/wave-resource-gate.mjs +133 -7
- package/scripts/lib/wave-sizing.mjs +4 -1
- package/scripts/lib/wave-transcript-tail.mjs +142 -8
- package/scripts/materialize-wave-scope.mjs +32 -9
- package/scripts/memory-propose.mjs +146 -8
- package/scripts/migrate-cold-start-seed.mjs +4 -1
- package/scripts/parse-config.mjs +60 -3
- package/scripts/promote-vault-strict.mjs +4 -15
- package/scripts/release.mjs +337 -29
- package/scripts/repair-invalid-sessions.mjs +3 -3
- package/scripts/run-quality-gate.mjs +128 -11
- package/scripts/site-numbers.mjs +36 -4
- 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 +187 -0
- package/scripts/validate-wave-scope.mjs +28 -8
- package/scripts/vault-consolidate.mjs +3 -11
- package/scripts/vault-integration-watcher.mjs +2 -4
- package/scripts/vault-mirror.mjs +111 -26
- package/scripts/wave-scope-binding.mjs +215 -0
- package/skills/_shared/instruction-file-resolution.md +10 -0
- package/skills/_shared/parallel-aware-auq.md +31 -2
- package/skills/_shared/parallel-aware-preamble.md +18 -4
- 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/ecosystem-health/SKILL.md +4 -1
- package/skills/ecosystem-health/wizard.md +5 -0
- package/skills/evolve/SKILL.md +87 -11
- package/skills/frontmatter-guard/SKILL.md +11 -5
- package/skills/npm-publish/SKILL.md +1 -1
- package/skills/reconcile/SKILL.md +38 -2
- package/skills/remote-offload/SKILL.md +89 -0
- package/skills/session-end/SKILL.md +18 -905
- package/skills/session-end/phase-3-6-tail.md +19 -9
- 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 +41 -900
- 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 +16 -2
- 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 -1271
- 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 -269
- 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
|
@@ -65,6 +65,10 @@ let readJson;
|
|
|
65
65
|
let findScopeFile;
|
|
66
66
|
let extractBashWriteTargets;
|
|
67
67
|
let pathMatchesPattern;
|
|
68
|
+
/** @type {typeof import('../scripts/lib/session-identity/own-session.mjs').readProcessLocalSessionIds} */
|
|
69
|
+
let readProcessLocalSessionIds;
|
|
70
|
+
/** @type {typeof import('../scripts/lib/session-identity/own-session.mjs').classifyManifestSession} */
|
|
71
|
+
let classifyManifestSession;
|
|
68
72
|
/**
|
|
69
73
|
* The `command-blocker.mjs` namespace, imported DIRECTLY (not via the
|
|
70
74
|
* hardening.mjs barrel — which does not carry the `headFallback` recovery, and
|
|
@@ -150,6 +154,10 @@ async function bootstrap() {
|
|
|
150
154
|
headFallback: true,
|
|
151
155
|
requires: ['commandMatchesBlocked', 'suggestForCommandBlock'],
|
|
152
156
|
},
|
|
157
|
+
// #1153 P1. Bound through the same loader as every other repo dependency,
|
|
158
|
+
// so a SyntaxError in the identity module banners GUARD INACTIVE instead
|
|
159
|
+
// of link-crashing this hook into a silent fail-open (#993).
|
|
160
|
+
sessionIdentity: { specifier: lib('session-identity/own-session.mjs') },
|
|
153
161
|
},
|
|
154
162
|
{
|
|
155
163
|
hookName: HOOK_NAME,
|
|
@@ -163,6 +171,7 @@ async function bootstrap() {
|
|
|
163
171
|
({ resolveProjectDir } = modules.platform);
|
|
164
172
|
({ readJson } = modules.common);
|
|
165
173
|
({ findScopeFile, extractBashWriteTargets, pathMatchesPattern } = modules.hardening);
|
|
174
|
+
({ readProcessLocalSessionIds, classifyManifestSession } = modules.sessionIdentity);
|
|
166
175
|
blocker = modules.blocker;
|
|
167
176
|
degradedLabels = degraded;
|
|
168
177
|
}
|
|
@@ -245,6 +254,66 @@ async function main() {
|
|
|
245
254
|
} catch {
|
|
246
255
|
scope = {};
|
|
247
256
|
}
|
|
257
|
+
// G3b (#1153 P1) — is this manifest even MINE?
|
|
258
|
+
//
|
|
259
|
+
// `wave-scope.json` lives in the WORKING COPY, not in the session, so two live
|
|
260
|
+
// sessions sharing one checkout read the SAME file. Before this gate, session
|
|
261
|
+
// A's `blockedCommands` decided session B's Bash calls, with a deny reason
|
|
262
|
+
// pointing at a wave plan B does not own (#1082, the sibling defect
|
|
263
|
+
// hooks/enforce-scope.mjs Gate 3b closed for Edit/Write under #1123).
|
|
264
|
+
//
|
|
265
|
+
// Deliberately AFTER the parse block: a corrupt manifest folds to `{}`, hence
|
|
266
|
+
// no ids, hence `'unknown'` — and keeps being enforced. `'own'` and
|
|
267
|
+
// `'unknown'` fall through completely unchanged; a legacy manifest without a
|
|
268
|
+
// `session` field stays enforced. Only what is PROVABLY foreign stands down.
|
|
269
|
+
//
|
|
270
|
+
// IDENTITY TIER: `readProcessLocalSessionIds` (hook payload +
|
|
271
|
+
// `CLAUDE_CODE_SESSION_ID`) and deliberately NOT `readOwnSessionIds`, whose
|
|
272
|
+
// third tier is the repo-global `session.lock` — one file shared by every
|
|
273
|
+
// session in the checkout, which made a peer's manifest classify as `'own'`
|
|
274
|
+
// (#1194).
|
|
275
|
+
//
|
|
276
|
+
// CEILING (BV-004): on a harness that exports no session env var and puts no
|
|
277
|
+
// `session_id` in the hook payload (Codex CLI, Cursor today) both tiers are
|
|
278
|
+
// empty, so this gate is permanently `'unknown'` = enforce = pre-#1153
|
|
279
|
+
// behaviour there. Revisit when those hook payloads carry a session id.
|
|
280
|
+
{
|
|
281
|
+
// `new Set(...)` is load-bearing: `readProcessLocalSessionIds` returns a
|
|
282
|
+
// string[], and `classifyManifestSession` does `ownIds instanceof Set ?
|
|
283
|
+
// ownIds : new Set()` — a bare array would silently become EMPTY.
|
|
284
|
+
const ownIds = new Set(readProcessLocalSessionIds({ hookInput: input }));
|
|
285
|
+
const { verdict, manifestIds } = classifyManifestSession(scope, ownIds);
|
|
286
|
+
if (verdict === 'foreign') {
|
|
287
|
+
// Observability only, and deliberately NOT emitWarn: this branch is hit on
|
|
288
|
+
// every Bash call of the non-owning session, so a stderr line per call
|
|
289
|
+
// would be noise. Awaited before the allow — flushNotices() exits the
|
|
290
|
+
// process, which would discard a pending append.
|
|
291
|
+
try {
|
|
292
|
+
const { emitEvent } = await import('../scripts/lib/events.mjs');
|
|
293
|
+
await emitEvent(
|
|
294
|
+
'orchestrator.scope.foreign_session_ignored',
|
|
295
|
+
{
|
|
296
|
+
hook: HOOK_NAME,
|
|
297
|
+
manifest: scopePath,
|
|
298
|
+
manifest_session: manifestIds,
|
|
299
|
+
own_session: [...ownIds],
|
|
300
|
+
wave: scope.wave,
|
|
301
|
+
command,
|
|
302
|
+
},
|
|
303
|
+
{ repoRoot: projectRoot },
|
|
304
|
+
);
|
|
305
|
+
} catch { /* observability is best-effort — never blocks the decision */ }
|
|
306
|
+
return flushNotices(notices);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// ADVERSARIAL CEILING: `wave-scope.json` is a file ANY process in this working
|
|
311
|
+
// copy can write, so writing a foreign id into it is a per-file kill switch for
|
|
312
|
+
// this session's own gate. Accepted because "no manifest" already means allow
|
|
313
|
+
// (G3), so a foreign manifest grants nothing an `rm` would not — but the change
|
|
314
|
+
// MUST stay visible: see post-bash-write-verify's control-file notice (#938),
|
|
315
|
+
// which is the only thing that distinguishes this from a legitimate peer wave.
|
|
316
|
+
|
|
248
317
|
const enforcement = scope.enforcement || 'strict';
|
|
249
318
|
const blockedCommands = Array.isArray(scope.blockedCommands)
|
|
250
319
|
? scope.blockedCommands
|
package/hooks/enforce-scope.mjs
CHANGED
|
@@ -9,10 +9,12 @@
|
|
|
9
9
|
* G1 tool filter — only Edit/Write/MultiEdit are gated
|
|
10
10
|
* G2 file_path present + string
|
|
11
11
|
* G3 wave-scope.json exists
|
|
12
|
-
* G3b (#1123) the manifest belongs to THIS session — a manifest that
|
|
12
|
+
* G3b (#1123, #1194) the manifest belongs to THIS session — a manifest that
|
|
13
13
|
* PROVABLY names another live session in this shared working copy is not
|
|
14
14
|
* ours to enforce; allow + emit one event. Runs AFTER the parse so a
|
|
15
|
-
* corrupt manifest still fails closed.
|
|
15
|
+
* corrupt manifest still fails closed. Identity is PROCESS-LOCAL only
|
|
16
|
+
* (hook payload + CLAUDE_CODE_SESSION_ID), never the repo-global
|
|
17
|
+
* `session.lock` — see the gate's own block for why.
|
|
16
18
|
* G4 path-guard gate enabled
|
|
17
19
|
* G5 enforcement != "off"
|
|
18
20
|
* G5b (#792) allowlist-first: an EXPLICIT absolute allowedPaths entry that
|
|
@@ -102,8 +104,10 @@ let readJson;
|
|
|
102
104
|
let classifyEmptyScope;
|
|
103
105
|
let suggestForEmptyScope;
|
|
104
106
|
let sessionStartedAtMs;
|
|
105
|
-
// #1123 — "is this manifest even mine?" (G3b).
|
|
106
|
-
|
|
107
|
+
// #1123 — "is this manifest even mine?" (G3b). Process-local identity only
|
|
108
|
+
// (#1194): the repo-global `session.lock` tier is shared by every session in the
|
|
109
|
+
// checkout and would classify a peer's manifest as ours.
|
|
110
|
+
let readProcessLocalSessionIds;
|
|
107
111
|
let classifyManifestSession;
|
|
108
112
|
|
|
109
113
|
const PLUGIN_ROOT = path.resolve(import.meta.dirname, '..');
|
|
@@ -180,7 +184,7 @@ async function bootstrap() {
|
|
|
180
184
|
({ findScopeFile, pathMatchesPattern, suggestForScopeViolation } = modules.hardening);
|
|
181
185
|
({ readJson } = modules.common);
|
|
182
186
|
({ classifyEmptyScope, suggestForEmptyScope, sessionStartedAtMs } = modules.scopeGate);
|
|
183
|
-
({
|
|
187
|
+
({ readProcessLocalSessionIds, classifyManifestSession } = modules.sessionIdentity);
|
|
184
188
|
}
|
|
185
189
|
|
|
186
190
|
async function main() {
|
|
@@ -251,13 +255,38 @@ async function main() {
|
|
|
251
255
|
// without a `session` field is `'unknown'` and stays enforced: only what is
|
|
252
256
|
// PROVABLY foreign is treated as foreign.
|
|
253
257
|
//
|
|
258
|
+
// IDENTITY TIER (#1194): `readProcessLocalSessionIds` — the hook payload and
|
|
259
|
+
// `CLAUDE_CODE_SESSION_ID` — and deliberately NOT `readOwnSessionIds`, whose
|
|
260
|
+
// third tier is the repo-global `session.lock`. That lock is ONE file shared
|
|
261
|
+
// by every session in the checkout, so unioning it made a peer's manifest
|
|
262
|
+
// match a peer-written lock id and classify as `'own'`: Gate 7 then denied the
|
|
263
|
+
// second session's legitimate writes — the exact lockout G3b exists to end.
|
|
264
|
+
// A better signal REPLACES a worse one (`host-resources.md` § HR-102).
|
|
265
|
+
//
|
|
266
|
+
// The trade this buys, accepted rather than hidden: a session that wrote a
|
|
267
|
+
// PEER's id into its OWN manifest (it lost the `bootstrapLock()` race, so
|
|
268
|
+
// `sessionAttribution()` handed it the peer's lock id) now reads `'foreign'`
|
|
269
|
+
// and its own guard stands down. The defense is on the WRITER side — the
|
|
270
|
+
// manifest writer must omit the `session` keys when the lock does not name it
|
|
271
|
+
// (`skills/wave-executor/wave-loop.md` § Scope Manifest, "Verify the binding
|
|
272
|
+
// names YOU before you write it") — not on this reader.
|
|
273
|
+
//
|
|
274
|
+
// CEILING (BV-004): on a harness that exports no session env var and puts no
|
|
275
|
+
// `session_id` in the hook payload (Codex CLI, Cursor today), both tiers are
|
|
276
|
+
// empty, so G3b is permanently `'unknown'` = enforce = pre-#1123 behaviour
|
|
277
|
+
// there. Revisit when Codex/Cursor hook payloads carry a session id.
|
|
278
|
+
//
|
|
254
279
|
// ACCEPTED RESIDUAL, named rather than hidden: `session` is a plain field in a
|
|
255
280
|
// file any process in this working copy can write, so writing a foreign id
|
|
256
281
|
// into it switches this guard off for that manifest. That is the SAME power
|
|
257
282
|
// `enforcement: "off"` already grants in the same file — this gate adds no new
|
|
258
283
|
// authority, and the manifest is the coordinator's own artefact either way.
|
|
259
284
|
{
|
|
260
|
-
|
|
285
|
+
// `new Set(...)` is load-bearing: `readProcessLocalSessionIds` returns a
|
|
286
|
+
// string[], and `classifyManifestSession` does `ownIds instanceof Set ?
|
|
287
|
+
// ownIds : new Set()` — a bare array would silently become EMPTY, making
|
|
288
|
+
// every manifest read `unknown`.
|
|
289
|
+
const ownIds = new Set(readProcessLocalSessionIds({ hookInput: input }));
|
|
261
290
|
const { verdict, manifestIds } = classifyManifestSession(scope, ownIds);
|
|
262
291
|
if (verdict === 'foreign') {
|
|
263
292
|
// Observability only, and deliberately NOT emitWarn: this branch is hit on
|
package/hooks/hooks-codex.json
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"hooks": [
|
|
8
8
|
{
|
|
9
9
|
"type": "command",
|
|
10
|
-
"command": "echo '🎯 Session Orchestrator
|
|
10
|
+
"command": "echo '🎯 Session Orchestrator v4.0.0 — /session [housekeeping|feature|deep] | /plan [new|feature|retro] | /discovery [scope] | /evolve [analyze|review|list]'",
|
|
11
11
|
"async": false
|
|
12
12
|
},
|
|
13
13
|
{
|
package/hooks/hooks-cursor.json
CHANGED
|
@@ -112,6 +112,11 @@
|
|
|
112
112
|
"type": "command",
|
|
113
113
|
"command": "sh \"$CURSOR_PLUGIN_ROOT/hooks/run-node.sh\" \"$CURSOR_PLUGIN_ROOT/hooks/post-tooluse-frontend-slop.mjs\"",
|
|
114
114
|
"timeout": 5
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
"type": "command",
|
|
118
|
+
"command": "sh \"$CURSOR_PLUGIN_ROOT/hooks/run-node.sh\" \"$CURSOR_PLUGIN_ROOT/hooks/post-edit-import-probe.mjs\"",
|
|
119
|
+
"timeout": 5
|
|
115
120
|
}
|
|
116
121
|
]
|
|
117
122
|
},
|
|
@@ -139,6 +144,11 @@
|
|
|
139
144
|
"type": "command",
|
|
140
145
|
"command": "sh \"$CURSOR_PLUGIN_ROOT/hooks/run-node.sh\" \"$CURSOR_PLUGIN_ROOT/hooks/post-tooluse-frontend-slop.mjs\"",
|
|
141
146
|
"timeout": 5
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
"type": "command",
|
|
150
|
+
"command": "sh \"$CURSOR_PLUGIN_ROOT/hooks/run-node.sh\" \"$CURSOR_PLUGIN_ROOT/hooks/post-edit-import-probe.mjs\"",
|
|
151
|
+
"timeout": 5
|
|
142
152
|
}
|
|
143
153
|
]
|
|
144
154
|
}
|
package/hooks/hooks-pi.json
CHANGED
|
@@ -85,6 +85,11 @@
|
|
|
85
85
|
"type": "command",
|
|
86
86
|
"command": "sh \"$PI_PLUGIN_ROOT/hooks/run-node.sh\" \"$PI_PLUGIN_ROOT/hooks/post-tooluse-frontend-slop.mjs\"",
|
|
87
87
|
"timeout": 5
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
"type": "command",
|
|
91
|
+
"command": "sh \"$PI_PLUGIN_ROOT/hooks/run-node.sh\" \"$PI_PLUGIN_ROOT/hooks/post-edit-import-probe.mjs\"",
|
|
92
|
+
"timeout": 5
|
|
88
93
|
}
|
|
89
94
|
]
|
|
90
95
|
},
|
package/hooks/hooks.json
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
"hooks": [
|
|
7
7
|
{
|
|
8
8
|
"type": "command",
|
|
9
|
-
"command": "echo '🎯 Session Orchestrator
|
|
9
|
+
"command": "echo '🎯 Session Orchestrator v4.0.0 — /session [housekeeping|feature|deep] | /plan [new|feature|retro] | /discovery [scope] | /evolve [analyze|review|list]'",
|
|
10
10
|
"async": false
|
|
11
11
|
},
|
|
12
12
|
{
|
|
@@ -135,6 +135,11 @@
|
|
|
135
135
|
"type": "command",
|
|
136
136
|
"command": "sh \"$CLAUDE_PLUGIN_ROOT/hooks/run-node.sh\" \"$CLAUDE_PLUGIN_ROOT/hooks/post-tooluse-frontend-slop.mjs\"",
|
|
137
137
|
"timeout": 5
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
"type": "command",
|
|
141
|
+
"command": "sh \"$CLAUDE_PLUGIN_ROOT/hooks/run-node.sh\" \"$CLAUDE_PLUGIN_ROOT/hooks/post-edit-import-probe.mjs\"",
|
|
142
|
+
"timeout": 5
|
|
138
143
|
}
|
|
139
144
|
]
|
|
140
145
|
},
|
package/hooks/loop-guard.mjs
CHANGED
|
@@ -33,7 +33,7 @@ import os from 'node:os';
|
|
|
33
33
|
import path from 'node:path';
|
|
34
34
|
import crypto from 'node:crypto';
|
|
35
35
|
|
|
36
|
-
import {
|
|
36
|
+
import { getProjectDir } from '../scripts/lib/platform.mjs';
|
|
37
37
|
import { emitEvent } from '../scripts/lib/events.mjs';
|
|
38
38
|
import { writeJsonAtomicSync } from '../scripts/lib/io.mjs';
|
|
39
39
|
import { _parseLoopGuard } from '../scripts/lib/config/loop-guard.mjs';
|
|
@@ -84,8 +84,8 @@ function readStdinJson() {
|
|
|
84
84
|
*/
|
|
85
85
|
async function loadConfig() {
|
|
86
86
|
const candidates = [
|
|
87
|
-
path.join(
|
|
88
|
-
path.join(
|
|
87
|
+
path.join(getProjectDir(), 'CLAUDE.md'),
|
|
88
|
+
path.join(getProjectDir(), 'AGENTS.md'),
|
|
89
89
|
];
|
|
90
90
|
for (const file of candidates) {
|
|
91
91
|
try {
|
package/hooks/on-session-end.mjs
CHANGED
|
@@ -11,7 +11,12 @@
|
|
|
11
11
|
*
|
|
12
12
|
* JSONL format (`.orchestrator/metrics/events.jsonl`):
|
|
13
13
|
* {"timestamp":<ISO>,"event":"orchestrator.session.ended","session_id":"...","semantic_session_id":"...","reason":"<reason>","duration_ms":<int>}
|
|
14
|
-
* (`session_id` / `semantic_session_id` are omitted when unresolvable — #1068 AC1.
|
|
14
|
+
* (`session_id` / `semantic_session_id` are omitted when unresolvable — #1068 AC1.
|
|
15
|
+
* `duration_ms` likewise: it is written ONLY when the ending session IS the
|
|
16
|
+
* one `current-session.json` records AND a start timestamp parsed. A
|
|
17
|
+
* fabricated `0` reads as a measured zero-length session and is
|
|
18
|
+
* indistinguishable from one — 1082 of 1498 fleet records (72,2 %) carried
|
|
19
|
+
* exactly that zero, measured 2026-09-02. Omit, never fabricate.)
|
|
15
20
|
*
|
|
16
21
|
* Exit codes: 0 always (informational hook — must never block session teardown).
|
|
17
22
|
* stdin: optional JSON { hook_event_name:"SessionEnd", session_id?, reason?, cwd? }.
|
|
@@ -30,7 +35,7 @@ import { shouldRunHook } from './_lib/profile-gate.mjs';
|
|
|
30
35
|
if (!shouldRunHook('on-session-end')) process.exit(0);
|
|
31
36
|
|
|
32
37
|
import { emitEvent } from '../scripts/lib/events.mjs';
|
|
33
|
-
import {
|
|
38
|
+
import { getProjectDir } from '../scripts/lib/platform.mjs';
|
|
34
39
|
import {
|
|
35
40
|
backfillAbandonedSession,
|
|
36
41
|
backfillCompletedFromStateMd,
|
|
@@ -46,6 +51,7 @@ import { deregisterSelf, logSweepEvent } from '../scripts/lib/session-registry.m
|
|
|
46
51
|
import { readConfigFile, parseSessionConfig } from '../scripts/lib/config.mjs';
|
|
47
52
|
import { flush } from '../scripts/lib/telemetry/sync.mjs';
|
|
48
53
|
import { attemptLockReconciliation } from './_lib/lock-reconcile.mjs';
|
|
54
|
+
import { atomicMutateJson } from './_lib/atomic-json.mjs';
|
|
49
55
|
|
|
50
56
|
// ---------------------------------------------------------------------------
|
|
51
57
|
// stdin reading (inline — SessionEnd hooks exit 0 always, never deny)
|
|
@@ -84,6 +90,9 @@ async function readStdinJson() {
|
|
|
84
90
|
* `.orchestrator/current-session.json` (written by on-session-start.mjs).
|
|
85
91
|
* duration_ms is only computed when the ENDING session is the one recorded in
|
|
86
92
|
* current-session.json — never fabricated for a mismatched / unknown session.
|
|
93
|
+
* Not measurable ⇒ `durationMs: null`, and the caller then OMITS the key
|
|
94
|
+
* entirely (#1193 W5 F1, the last site of the omit-never-fabricate class this
|
|
95
|
+
* session removed from `session.stopped` and `agent.stopped`).
|
|
87
96
|
*
|
|
88
97
|
* #1091 / Kanevry#66 — WRITER/READER SYMMETRY. `on-session-start.mjs`
|
|
89
98
|
* (`resolveSessionId`, :316-317) accepts a stdin raw id only when
|
|
@@ -114,14 +123,42 @@ async function readStdinJson() {
|
|
|
114
123
|
* session therefore resolves `semanticSessionId: null` rather than inheriting
|
|
115
124
|
* another live session's backfill identity.
|
|
116
125
|
*
|
|
126
|
+
* #1193 W4a review F-A — `isRecordedSession` is computed from the RAW stdin
|
|
127
|
+
* UUID, NEVER from the resolved `sessionId`. The old order was
|
|
128
|
+
* `if (sessionId === null) sessionId = recordedId;` followed by
|
|
129
|
+
* `sessionId === recordedId`, which is SELF-FULFILLING: whenever stdin carried
|
|
130
|
+
* no session_id (or a non-UUID one), the fallback assigned the file's own id
|
|
131
|
+
* and the compare then trivially succeeded. Reproduced twice 2026-09-02 —
|
|
132
|
+
* peer-owned `current-session.json` (`last_wave: 3`) plus stdin
|
|
133
|
+
* `{"reason":"other"}` emitted `wave.completed` for the PEER's wave and wrote
|
|
134
|
+
* `last_wave_completed: 3` into the PEER's file, after which the peer's own
|
|
135
|
+
* SessionEnd stayed silent. The same vacuous predicate had always gated
|
|
136
|
+
* `durationMs` and `semanticSessionId`, so the #863 guard was hollow on that
|
|
137
|
+
* path too. One root fix (BV-003) for all three consumers: a `null` raw id
|
|
138
|
+
* means "not attestable" ⇒ `false`.
|
|
139
|
+
*
|
|
140
|
+
* WHY `sessionId` STILL FALLS BACK while the predicate does not. The two are
|
|
141
|
+
* deliberately asymmetric. `sessionId` is the hook's ACTOR identity — the id
|
|
142
|
+
* `deregisterSelf()` and the lock-release ownership compare use, and its
|
|
143
|
+
* current-session.json fallback is a named contract pinned by
|
|
144
|
+
* `tests/hooks/on-session-end.test.mjs` ("falls back to current-session.json
|
|
145
|
+
* session_id when stdin omits it"). `isRecordedSession` is an OWNERSHIP
|
|
146
|
+
* ASSERTION about a repo-global file; an assertion may never be derived from
|
|
147
|
+
* the very value it is asserting about. So the fallback stays for the emitted
|
|
148
|
+
* `session_id` field, and every claim that speaks FOR the recorded session
|
|
149
|
+
* (`duration_ms`, `semantic_session_id`, the final `wave.completed`) is gated
|
|
150
|
+
* on the strict raw compare instead. Precedent: `hooks/on-stop.mjs`
|
|
151
|
+
* `resolveStopDuration()`, which refuses the resolved id for the same reason.
|
|
152
|
+
*
|
|
117
153
|
* @param {object|null} input
|
|
118
154
|
* @param {string} projectRoot
|
|
119
|
-
* @returns {Promise<{sessionId: string|null, semanticSessionId: string|null, durationMs: number}>}
|
|
155
|
+
* @returns {Promise<{sessionId: string|null, semanticSessionId: string|null, durationMs: number|null, isRecordedSession: boolean, rawStdinId: string|null}>}
|
|
120
156
|
*/
|
|
121
157
|
async function resolveSession(input, projectRoot) {
|
|
122
158
|
const fromStdin = input?.session_id ?? input?.sessionId ?? null;
|
|
123
159
|
// UUID-only, exactly as the writer decides it (see the docblock above).
|
|
124
|
-
|
|
160
|
+
const rawStdinId = parseSessionId(fromStdin)?.format === 'uuid' ? fromStdin : null;
|
|
161
|
+
let sessionId = rawStdinId;
|
|
125
162
|
|
|
126
163
|
let recordedId = null;
|
|
127
164
|
let semanticSessionId = null;
|
|
@@ -144,13 +181,17 @@ async function resolveSession(input, projectRoot) {
|
|
|
144
181
|
}
|
|
145
182
|
} catch { /* missing or unparseable is fine */ }
|
|
146
183
|
|
|
184
|
+
// Actor-identity fallback ONLY — see the docblock's asymmetry note. This
|
|
185
|
+
// value must NEVER feed the ownership predicate below.
|
|
147
186
|
if (sessionId === null) sessionId = recordedId;
|
|
148
187
|
|
|
149
|
-
// Only trust the recorded start time when the ending session IS the recorded
|
|
150
|
-
|
|
188
|
+
// Only trust the recorded start time when the ending session IS the recorded
|
|
189
|
+
// one — decided on the RAW stdin id, so an absent/non-UUID id is `false`
|
|
190
|
+
// rather than self-fulfilling (F-A).
|
|
191
|
+
const isRecordedSession = rawStdinId !== null && rawStdinId === recordedId;
|
|
151
192
|
const durationMs = startedAtMs !== null && isRecordedSession
|
|
152
193
|
? Math.max(0, Date.now() - startedAtMs)
|
|
153
|
-
:
|
|
194
|
+
: null;
|
|
154
195
|
|
|
155
196
|
// #863 defect (c) — same guard as durationMs above: only surface the
|
|
156
197
|
// recorded semantic id when THIS ending session is genuinely the one
|
|
@@ -158,7 +199,16 @@ async function resolveSession(input, projectRoot) {
|
|
|
158
199
|
// contamination scenario this closes.
|
|
159
200
|
const resolvedSemanticSessionId = isRecordedSession ? semanticSessionId : null;
|
|
160
201
|
|
|
161
|
-
|
|
202
|
+
// `rawStdinId` is returned so a LATER read of current-session.json can
|
|
203
|
+
// re-verify ownership against the same identity (W4c Q1-LOW-TOCTOU) instead
|
|
204
|
+
// of trusting an attestation made against an earlier read of the file.
|
|
205
|
+
return {
|
|
206
|
+
sessionId,
|
|
207
|
+
semanticSessionId: resolvedSemanticSessionId,
|
|
208
|
+
durationMs,
|
|
209
|
+
isRecordedSession,
|
|
210
|
+
rawStdinId,
|
|
211
|
+
};
|
|
162
212
|
}
|
|
163
213
|
|
|
164
214
|
/**
|
|
@@ -307,17 +357,206 @@ async function emitBackfillOutcome(kind, result, { sessionId, semanticSessionId
|
|
|
307
357
|
} catch { /* observability is best-effort */ }
|
|
308
358
|
}
|
|
309
359
|
|
|
360
|
+
/**
|
|
361
|
+
* Emit `orchestrator.wave.final_refused` — the SIBLING event to
|
|
362
|
+
* `orchestrator.wave.completed` for every refusal path inside
|
|
363
|
+
* {@link emitFinalWaveCompleted} (#1201 Part B / Discovery D8). Deliberately
|
|
364
|
+
* a separate event name rather than `orchestrator.wave.completed` carrying
|
|
365
|
+
* `emitted:false`: existing consumers of `.completed` treat every row as a
|
|
366
|
+
* finished wave, and overloading it would silently corrupt that count.
|
|
367
|
+
*
|
|
368
|
+
* Wrapped in its OWN try/catch — independent of the caller's outer
|
|
369
|
+
* try/catch — so a telemetry failure on ONE refusal path can never surface
|
|
370
|
+
* as a failure of teardown, matching the best-effort contract every other
|
|
371
|
+
* emit in this SessionEnd hook already carries.
|
|
372
|
+
*
|
|
373
|
+
* @param {{sessionId: string|null, semanticSessionId: string|null}} ids
|
|
374
|
+
* @param {'not-recorded'|'clear'|'resume'|'unreadable'|'session-id-mismatch'|'no-wave'|'already-completed'|'exception'} reason
|
|
375
|
+
* @param {number} [waveNumber] - only when `last_wave` was resolved to a
|
|
376
|
+
* positive number before the refusal (currently only `already-completed`).
|
|
377
|
+
* @returns {Promise<void>}
|
|
378
|
+
*/
|
|
379
|
+
async function emitFinalRefused({ sessionId, semanticSessionId }, reason, waveNumber) {
|
|
380
|
+
try {
|
|
381
|
+
await emitEvent('orchestrator.wave.final_refused', {
|
|
382
|
+
...(sessionId !== null ? { session_id: sessionId } : {}),
|
|
383
|
+
...(semanticSessionId !== null ? { semantic_session_id: semanticSessionId } : {}),
|
|
384
|
+
reason,
|
|
385
|
+
...(typeof waveNumber === 'number' ? { wave_number: waveNumber } : {}),
|
|
386
|
+
emitted_by: 'on-session-end',
|
|
387
|
+
});
|
|
388
|
+
} catch { /* best-effort — a refusal record must never itself block teardown */ }
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Emit the FINAL `orchestrator.wave.completed` of the session (#1193).
|
|
393
|
+
*
|
|
394
|
+
* `hooks/post-tool-batch-wave-signal.mjs` closes wave N-1 only at an N-1→N
|
|
395
|
+
* transition, so the LAST wave of every session never received a completion —
|
|
396
|
+
* measured fleet-wide 2026-09-02 as 296 gaps over 296 wave runs (1018 started
|
|
397
|
+
* vs 722 completed), i.e. EXACTLY one missing final completion per run. The
|
|
398
|
+
* comment that claimed the coordinator emitted it at session close described a
|
|
399
|
+
* step that never existed. SessionEnd is that emitter.
|
|
400
|
+
*
|
|
401
|
+
* Deliberately SessionEnd-only: `on-stop.mjs` is not mirrored, so the ledger
|
|
402
|
+
* keeps the closed-vs-abandoned split measurable.
|
|
403
|
+
*
|
|
404
|
+
* Idempotent via the `last_wave_completed` high-water mark, written by both
|
|
405
|
+
* emitters and preserved across clear/compact by `on-session-start.mjs`.
|
|
406
|
+
* Emits nothing when `last_wave` is absent or 0 — an Express-Path or
|
|
407
|
+
* coordinator-direct session never batched, and zero waves is the correct
|
|
408
|
+
* reading there, not a gap.
|
|
409
|
+
*
|
|
410
|
+
* OWNERSHIP-GATED (#1193 review F1). `.orchestrator/current-session.json` is a
|
|
411
|
+
* single repo-global file describing whichever session most recently ran
|
|
412
|
+
* SessionStart — routinely a DIFFERENT, still-live session when two windows
|
|
413
|
+
* share this working copy. Emitting unguarded would (a) close a PEER's live
|
|
414
|
+
* wave with a completion the peer never reached, and (b) write
|
|
415
|
+
* `last_wave_completed` into the peer's file, so the peer's own SessionEnd then
|
|
416
|
+
* stays silent — preserving the very #1193 gap this closes, on the wrong
|
|
417
|
+
* session. So this reuses the SAME `isRecordedSession` predicate
|
|
418
|
+
* `resolveSession()` applies to `durationMs` and `semanticSessionId` (#863
|
|
419
|
+
* defect (c)); when it is false, nothing is emitted and nothing is written.
|
|
420
|
+
*
|
|
421
|
+
* REASON-GATED for `/clear` AND `resume` (#1193 review F2 + W4c Q3-MED-2). The
|
|
422
|
+
* SessionEnd matcher is empty, so `/clear` fires this hook mid-wave while the
|
|
423
|
+
* LOGICAL session continues (`hooks/on-session-start.mjs:383` preserves
|
|
424
|
+
* `last_wave` / `last_wave_completed` across exactly that). A `resume` is the
|
|
425
|
+
* SAME class — start preserves the marker across a resume of the same logical
|
|
426
|
+
* session just as it does across a clear — and resume is the MORE common of the
|
|
427
|
+
* two (fleet n = 1498 `session.ended`, 2026-09-02: 12 resume vs 9 clear).
|
|
428
|
+
* Closing the live wave on either is premature, and the preserved marker would
|
|
429
|
+
* then suppress the real completion later. Fleet `session.ended` reasons, re-measured
|
|
430
|
+
* 2026-09-02 over every repo's `.orchestrator/metrics/events.jsonl` under
|
|
431
|
+
* `~/Projects` (glob written as a path segment on purpose — a literal star
|
|
432
|
+
* followed by a slash would close this comment), except
|
|
433
|
+
* `EventDrop.at-deps-2026-09` — n = 1335: 1286 other, 27 completed, 12 resume,
|
|
434
|
+
* 8 clear, 1 error, 1 close. (Same denominator and date as the
|
|
435
|
+
* `session.ended` row in `docs/audits/2026-09-02-fleet-instruments.md`; the two
|
|
436
|
+
* disagreed by ~150 before W4a F-F because each counted a different repo set.)
|
|
437
|
+
*
|
|
438
|
+
* Strictly best-effort: never throws, never blocks teardown.
|
|
439
|
+
*
|
|
440
|
+
* SIX silent refusal paths (#1201 Part B / Discovery D8 — one more than the
|
|
441
|
+
* issue's original five) previously returned with no trace anywhere: per
|
|
442
|
+
* `.claude/rules/host-resources.md` § HR-105, a refusal that writes nothing
|
|
443
|
+
* is unfalsifiable. Every refusal now emits a SIBLING event,
|
|
444
|
+
* `orchestrator.wave.final_refused`, via {@link emitFinalRefused} — never
|
|
445
|
+
* `orchestrator.wave.completed` itself with an `emitted:false` flag, because
|
|
446
|
+
* that event's consumers treat every row as a finished wave (D8's explicit
|
|
447
|
+
* recommendation). Each emit is wrapped in its OWN try/catch inside
|
|
448
|
+
* `emitFinalRefused` so telemetry can never block teardown, on top of this
|
|
449
|
+
* function's own outer catch (which now also emits `reason: 'exception'`,
|
|
450
|
+
* best-effort).
|
|
451
|
+
*
|
|
452
|
+
* @param {string} projectRoot
|
|
453
|
+
* @param {{sessionId: string|null, semanticSessionId: string|null,
|
|
454
|
+
* isRecordedSession: boolean, reason: string,
|
|
455
|
+
* rawStdinId: string|null}} ctx
|
|
456
|
+
* @returns {Promise<{emitted: true, wave_number: number}|{emitted: false, reason: string}>}
|
|
457
|
+
* The return value is informational only — every branch has already
|
|
458
|
+
* persisted its own outcome via `orchestrator.wave.completed` or
|
|
459
|
+
* `orchestrator.wave.final_refused` by the time this resolves.
|
|
460
|
+
*/
|
|
461
|
+
async function emitFinalWaveCompleted(
|
|
462
|
+
projectRoot,
|
|
463
|
+
{ sessionId, semanticSessionId, isRecordedSession, reason, rawStdinId },
|
|
464
|
+
) {
|
|
465
|
+
const ids = { sessionId, semanticSessionId };
|
|
466
|
+
try {
|
|
467
|
+
// F1 — never speak for a session current-session.json does not describe.
|
|
468
|
+
if (!isRecordedSession) {
|
|
469
|
+
await emitFinalRefused(ids, 'not-recorded');
|
|
470
|
+
return { emitted: false, reason: 'not-recorded' };
|
|
471
|
+
}
|
|
472
|
+
// F2 — `/clear` ends the HARNESS session, not the logical one. `resume` is
|
|
473
|
+
// the SAME class (W4c Q3-MED-2): `on-session-start.mjs` preserves
|
|
474
|
+
// `last_wave` / `last_wave_completed` across a resume of the same logical
|
|
475
|
+
// session exactly as it does across a clear, and resume is the MORE common
|
|
476
|
+
// of the two (fleet n=1498, 2026-09-02: 12 resume vs 9 clear).
|
|
477
|
+
if (reason === 'clear' || reason === 'resume') {
|
|
478
|
+
await emitFinalRefused(ids, reason);
|
|
479
|
+
return { emitted: false, reason };
|
|
480
|
+
}
|
|
481
|
+
const sessionFile = path.join(projectRoot, '.orchestrator', 'current-session.json');
|
|
482
|
+
let parsed = null;
|
|
483
|
+
try {
|
|
484
|
+
parsed = JSON.parse(await fs.readFile(sessionFile, 'utf8'));
|
|
485
|
+
} catch {
|
|
486
|
+
// absent or malformed → nothing attestable to close
|
|
487
|
+
await emitFinalRefused(ids, 'unreadable');
|
|
488
|
+
return { emitted: false, reason: 'unreadable' };
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// W4c Q1-LOW-TOCTOU — `isRecordedSession` was attested against the FIRST
|
|
492
|
+
// read of this file (in `resolveSession()`); the values acted on below come
|
|
493
|
+
// from this SECOND read. Re-verify ownership here rather than inheriting a
|
|
494
|
+
// stale attestation. A genuine swap BETWEEN the two reads is not testable
|
|
495
|
+
// without a seam, and none is added for it — the peer-id case pins the
|
|
496
|
+
// re-check, and this predicate is what makes the window harmless.
|
|
497
|
+
if (rawStdinId === null || parsed?.session_id !== rawStdinId) {
|
|
498
|
+
await emitFinalRefused(ids, 'session-id-mismatch');
|
|
499
|
+
return { emitted: false, reason: 'session-id-mismatch' };
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const lastWave = parsed?.last_wave;
|
|
503
|
+
if (typeof lastWave !== 'number' || !(lastWave > 0)) {
|
|
504
|
+
await emitFinalRefused(ids, 'no-wave');
|
|
505
|
+
return { emitted: false, reason: 'no-wave' };
|
|
506
|
+
}
|
|
507
|
+
// W4c Q3-MED-3(iii) / Q3-LOW-4 — strictly ABOVE the high-water mark, not
|
|
508
|
+
// merely different from it: a marker AHEAD of `last_wave` (written by the
|
|
509
|
+
// batch hook's explicit `wave-complete{N}` branch) means this wave is
|
|
510
|
+
// already closed, and a non-integer marker (`'3'`, `null`) counts as
|
|
511
|
+
// ABSENT rather than as "different".
|
|
512
|
+
const marker = Number.isInteger(parsed?.last_wave_completed) ? parsed.last_wave_completed : 0;
|
|
513
|
+
if (!(lastWave > marker)) {
|
|
514
|
+
await emitFinalRefused(ids, 'already-completed', lastWave);
|
|
515
|
+
return { emitted: false, reason: 'already-completed' };
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
await emitEvent('orchestrator.wave.completed', {
|
|
519
|
+
...(sessionId !== null ? { session_id: sessionId } : {}),
|
|
520
|
+
...(semanticSessionId !== null ? { semantic_session_id: semanticSessionId } : {}),
|
|
521
|
+
wave_number: lastWave,
|
|
522
|
+
reason: 'session-end',
|
|
523
|
+
emitted_by: 'on-session-end',
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
// Monotone, exactly as the batch hook's `maxWave()` — the mark may only
|
|
527
|
+
// ever rise, whichever of the three writers gets here last (W4c Q3-MED-3).
|
|
528
|
+
const markResult = await atomicMutateJson(sessionFile, {}, (current) => ({
|
|
529
|
+
...current,
|
|
530
|
+
last_wave_completed: Number.isInteger(current?.last_wave_completed)
|
|
531
|
+
&& current.last_wave_completed > lastWave
|
|
532
|
+
? current.last_wave_completed
|
|
533
|
+
: lastWave,
|
|
534
|
+
}), 'ose');
|
|
535
|
+
// The wave.completed EVENT above already fired regardless — this only
|
|
536
|
+
// withholds the shared-file high-water mark on a non-ENOENT failure.
|
|
537
|
+
if (!markResult.ok) {
|
|
538
|
+
console.error(`on-session-end: last_wave_completed mark skipped (${markResult.reason})`);
|
|
539
|
+
}
|
|
540
|
+
return { emitted: true, wave_number: lastWave };
|
|
541
|
+
} catch {
|
|
542
|
+
// best-effort — a SessionEnd hook must never block teardown
|
|
543
|
+
await emitFinalRefused(ids, 'exception');
|
|
544
|
+
return { emitted: false, reason: 'exception' };
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
310
548
|
// ---------------------------------------------------------------------------
|
|
311
549
|
// main
|
|
312
550
|
// ---------------------------------------------------------------------------
|
|
313
551
|
|
|
314
552
|
async function main() {
|
|
315
553
|
const input = await readStdinJson();
|
|
316
|
-
const projectRoot =
|
|
554
|
+
const projectRoot = getProjectDir();
|
|
317
555
|
|
|
318
556
|
const reason =
|
|
319
557
|
typeof input?.reason === 'string' && input.reason.length > 0 ? input.reason : 'other';
|
|
320
|
-
const { sessionId, semanticSessionId, durationMs } =
|
|
558
|
+
const { sessionId, semanticSessionId, durationMs, isRecordedSession, rawStdinId } =
|
|
559
|
+
await resolveSession(input, projectRoot);
|
|
321
560
|
|
|
322
561
|
// Single emission path: emitEvent writes the canonical {timestamp, event, ...payload}
|
|
323
562
|
// JSONL record AND fires the optional Clank webhook with the SAME event name.
|
|
@@ -332,13 +571,40 @@ async function main() {
|
|
|
332
571
|
// attestation bar is the #863 defect (c) guard inside `resolveSession()`: an
|
|
333
572
|
// ending session that is NOT the one current-session.json describes resolves
|
|
334
573
|
// `semanticSessionId: null` and therefore emits no key here.
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
574
|
+
// #1193 — close the last wave BEFORE the terminal session event, so the
|
|
575
|
+
// ledger's wave lifecycle is balanced within the session's own window. Gated
|
|
576
|
+
// on the SAME `isRecordedSession` attestation as the identity keys above, and
|
|
577
|
+
// skipped for `reason === 'clear'` and `reason === 'resume'` alike — both end
|
|
578
|
+
// the HARNESS session while the LOGICAL one continues; see the emitter's docblock.
|
|
579
|
+
//
|
|
580
|
+
// #1201 Part B — the return is `{emitted:true, wave_number}` or
|
|
581
|
+
// `{emitted:false, reason}`, but it is intentionally NOT branched on here:
|
|
582
|
+
// both outcomes already persisted themselves (`orchestrator.wave.completed`
|
|
583
|
+
// or the sibling `orchestrator.wave.final_refused`) before this call
|
|
584
|
+
// resolves, so nothing downstream in this hook needs to react to it.
|
|
585
|
+
await emitFinalWaveCompleted(projectRoot, {
|
|
586
|
+
sessionId,
|
|
587
|
+
semanticSessionId,
|
|
588
|
+
isRecordedSession,
|
|
338
589
|
reason,
|
|
339
|
-
|
|
590
|
+
rawStdinId,
|
|
340
591
|
});
|
|
341
592
|
|
|
593
|
+
// #1183 — a malformed record throws EventValidationError BEFORE any side
|
|
594
|
+
// effect (scripts/lib/events.mjs); this hook must never abort on that, so
|
|
595
|
+
// the emit is wrapped rather than left to propagate.
|
|
596
|
+
try {
|
|
597
|
+
await emitEvent('orchestrator.session.ended', {
|
|
598
|
+
...(sessionId !== null ? { session_id: sessionId } : {}),
|
|
599
|
+
...(semanticSessionId !== null ? { semantic_session_id: semanticSessionId } : {}),
|
|
600
|
+
reason,
|
|
601
|
+
// Omit-never-fabricate: only a MEASURED span is written. `null` here means
|
|
602
|
+
// the ending session is not the recorded one (or no start time parsed) —
|
|
603
|
+
// absence must stay absence, not become a zero-length session.
|
|
604
|
+
...(Number.isFinite(durationMs) && durationMs >= 0 ? { duration_ms: durationMs } : {}),
|
|
605
|
+
});
|
|
606
|
+
} catch { /* telemetry never blocks the hook (#1183) */ }
|
|
607
|
+
|
|
342
608
|
// -------------------------------------------------------------------------
|
|
343
609
|
// C1 (#724) — Close-through backfill + deterministic lock release.
|
|
344
610
|
// Both are STRICTLY best-effort: a SessionEnd hook must never block teardown
|