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
|
@@ -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/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
|
@@ -35,7 +35,7 @@ import { shouldRunHook } from './_lib/profile-gate.mjs';
|
|
|
35
35
|
if (!shouldRunHook('on-session-end')) process.exit(0);
|
|
36
36
|
|
|
37
37
|
import { emitEvent } from '../scripts/lib/events.mjs';
|
|
38
|
-
import {
|
|
38
|
+
import { getProjectDir } from '../scripts/lib/platform.mjs';
|
|
39
39
|
import {
|
|
40
40
|
backfillAbandonedSession,
|
|
41
41
|
backfillCompletedFromStateMd,
|
|
@@ -551,7 +551,7 @@ async function emitFinalWaveCompleted(
|
|
|
551
551
|
|
|
552
552
|
async function main() {
|
|
553
553
|
const input = await readStdinJson();
|
|
554
|
-
const projectRoot =
|
|
554
|
+
const projectRoot = getProjectDir();
|
|
555
555
|
|
|
556
556
|
const reason =
|
|
557
557
|
typeof input?.reason === 'string' && input.reason.length > 0 ? input.reason : 'other';
|
|
@@ -42,7 +42,7 @@ if (!shouldRunHook('on-session-start')) process.exit(0);
|
|
|
42
42
|
import { emitEvent, eventsFilePath } from '../scripts/lib/events.mjs';
|
|
43
43
|
import { maybeRotate } from '../scripts/lib/events-rotation.mjs';
|
|
44
44
|
import { readConfigFile, parseSessionConfig } from '../scripts/lib/config.mjs';
|
|
45
|
-
import {
|
|
45
|
+
import { getPlatform, resolveProjectDir } from '../scripts/lib/platform.mjs';
|
|
46
46
|
import {
|
|
47
47
|
registerSelf,
|
|
48
48
|
detectPeers,
|
|
@@ -410,6 +410,13 @@ async function resolveSessionId(input, projectRoot) {
|
|
|
410
410
|
if (Object.prototype.hasOwnProperty.call(prev, 'last_wave_completed')) {
|
|
411
411
|
preserved.last_wave_completed = prev.last_wave_completed;
|
|
412
412
|
}
|
|
413
|
+
// #980 — the OPEN half of the wave-diff pair, written by
|
|
414
|
+
// post-tool-batch-wave-signal.mjs. Dropping it across a /clear leaves
|
|
415
|
+
// the running wave with no start point, so the next
|
|
416
|
+
// `orchestrator.wave.completed` silently omits `files_changed`.
|
|
417
|
+
if (Object.prototype.hasOwnProperty.call(prev, 'wave_start_sha')) {
|
|
418
|
+
preserved.wave_start_sha = prev.wave_start_sha;
|
|
419
|
+
}
|
|
413
420
|
}
|
|
414
421
|
} catch { /* absent / unparseable → no preservation (reset) */ }
|
|
415
422
|
}
|
|
@@ -705,7 +712,15 @@ async function main() {
|
|
|
705
712
|
// must never break the hook, which is informational-only.
|
|
706
713
|
const input = await stdinPromise;
|
|
707
714
|
const { sessionId, semanticSessionId, mode } = await resolveSessionId(input, projectRoot);
|
|
708
|
-
|
|
715
|
+
// getPlatform() ALREADY implements the SO_PLATFORM override as step 1 of its
|
|
716
|
+
// precedence — and, unlike a bare `??`, it validates the value against the
|
|
717
|
+
// four-platform allowlist and trims it. The former `process.env.SO_PLATFORM ??
|
|
718
|
+
// getPlatform()` therefore added nothing except the empty-string trap
|
|
719
|
+
// (`.claude/rules/development.md` § Env-var fallback whitespace trap): a key
|
|
720
|
+
// exported as `SO_PLATFORM=` is not nullish, so `??` short-circuited and this
|
|
721
|
+
// hook stamped `platform: ''` on the session event and handed '' down to the
|
|
722
|
+
// update probe. Measured 2026-09-06 by the codex test below.
|
|
723
|
+
const platform = getPlatform();
|
|
709
724
|
|
|
710
725
|
// Epic #583 P3 — mechanical session.lock writer (closes D1+D2+D4 gaps).
|
|
711
726
|
// Bootstrap the session-lock so discoverActiveSessions() picks us up even
|
|
@@ -947,6 +962,40 @@ async function main() {
|
|
|
947
962
|
} catch { /* hook must remain non-blocking */ }
|
|
948
963
|
}
|
|
949
964
|
|
|
965
|
+
// Plugin-update probe (#nnn, d7 R2) — "is the plugin that is RUNNING behind
|
|
966
|
+
// the plugin that is PUBLISHED?".
|
|
967
|
+
//
|
|
968
|
+
// MEASURED 2026-09-06: this host ran the marketplace-cache copy at 3.19.0
|
|
969
|
+
// (installed 2026-08-09) while repo and npm stood at the 3.24 line — five minors,
|
|
970
|
+
// four weeks, zero warnings, because NO code anywhere compared installed
|
|
971
|
+
// against available (`grep -rln "registry.npmjs.org\|dist-tags" scripts/
|
|
972
|
+
// hooks/ skills/` → only scripts/release.mjs, the publisher).
|
|
973
|
+
//
|
|
974
|
+
// Placement mirrors the probe block above: BEFORE the payload (the event
|
|
975
|
+
// carries both versions) and BEFORE flushBanner(), so the line rides the ONE
|
|
976
|
+
// systemMessage envelope. A second stdout object would be silently discarded
|
|
977
|
+
// (HR-106) — the failure mode the bannerLines docstring describes.
|
|
978
|
+
//
|
|
979
|
+
// Same measurement/display split as the probes: the request and the event
|
|
980
|
+
// fields happen regardless, `pushBanner` is gated on `bannerData`. Network,
|
|
981
|
+
// cache and kill-switch behaviour live in the module.
|
|
982
|
+
let pluginUpdate = null;
|
|
983
|
+
try {
|
|
984
|
+
const { checkPluginUpdate } = await import('../scripts/lib/plugin-update-banner.mjs');
|
|
985
|
+
pluginUpdate = await checkPluginUpdate({
|
|
986
|
+
cacheDir: path.join(projectRoot, '.orchestrator', 'runtime'),
|
|
987
|
+
// The probe's own fallback is env-only (SO_PLATFORM, then each harness's
|
|
988
|
+
// compat var) and cannot run detectPlatform()'s third step, the marker-
|
|
989
|
+
// directory walk from cwd. This hook already resolved the platform at the
|
|
990
|
+
// top of run() through exactly that full precedence, so handing it over
|
|
991
|
+
// makes the remedy instruction MEASURED rather than guessed — a Codex or
|
|
992
|
+
// Cursor consumer with no compat var set would otherwise be told to run
|
|
993
|
+
// `/plugin update`, a command its harness does not have.
|
|
994
|
+
platform,
|
|
995
|
+
});
|
|
996
|
+
if (pluginUpdate && bannerData) pushBanner(pluginUpdate.message);
|
|
997
|
+
} catch { /* hook must remain non-blocking */ }
|
|
998
|
+
|
|
950
999
|
const payload = {
|
|
951
1000
|
platform,
|
|
952
1001
|
project: projectName,
|
|
@@ -958,6 +1007,34 @@ async function main() {
|
|
|
958
1007
|
// makes the supersession rate measurable instead of inferred (HR-105).
|
|
959
1008
|
peers_superseded: mechanicalPeersSuperseded,
|
|
960
1009
|
};
|
|
1010
|
+
|
|
1011
|
+
// #nnn — installed/latest plugin version on the session record.
|
|
1012
|
+
//
|
|
1013
|
+
// HR-105: a verdict nothing records is unfalsifiable. `installed` is stamped
|
|
1014
|
+
// on EVERY start (it is a local file read that cannot fail for network
|
|
1015
|
+
// reasons), and `latest` whenever it is known — including the runs where
|
|
1016
|
+
// checkPluginUpdate() stayed silent because the versions matched. Those runs
|
|
1017
|
+
// are the DENOMINATOR of the probe's firing rate; without them the fleet can
|
|
1018
|
+
// only ever see how often it warned, never how often it could have.
|
|
1019
|
+
//
|
|
1020
|
+
// Both keys are OMITTED rather than nulled when unknown (docs/events-schema.md
|
|
1021
|
+
// optional-field convention: absent ≠ zero) — a kill-switched or offline
|
|
1022
|
+
// session must not read as "latest is 0/unknown-but-measured".
|
|
1023
|
+
try {
|
|
1024
|
+
const { readInstalledPluginVersion, readCachedLatest, isUpdateCheckDisabled } =
|
|
1025
|
+
await import('../scripts/lib/plugin-update-banner.mjs');
|
|
1026
|
+
const installedVersion = readInstalledPluginVersion();
|
|
1027
|
+
if (installedVersion) payload.plugin_version_installed = installedVersion;
|
|
1028
|
+
// `latest` is gated on the kill switch, `installed` is not: reading this
|
|
1029
|
+
// repo's own package.json is not the thing an operator switched off.
|
|
1030
|
+
if (!isUpdateCheckDisabled()) {
|
|
1031
|
+
const latestVersion =
|
|
1032
|
+
pluginUpdate?.latest
|
|
1033
|
+
?? readCachedLatest({ cacheDir: path.join(projectRoot, '.orchestrator', 'runtime') })?.version
|
|
1034
|
+
?? null;
|
|
1035
|
+
if (latestVersion) payload.plugin_version_latest = latestVersion;
|
|
1036
|
+
}
|
|
1037
|
+
} catch { /* telemetry never blocks the hook */ }
|
|
961
1038
|
if (bannerData) {
|
|
962
1039
|
payload.host_class = bannerData.host.host_class;
|
|
963
1040
|
payload.ram_free_gb = bannerData.resources.ram_free_gb;
|
|
@@ -993,6 +1070,30 @@ async function main() {
|
|
|
993
1070
|
// only stdout write in the hook.
|
|
994
1071
|
flushBanner();
|
|
995
1072
|
|
|
1073
|
+
// #1229 — refresh the session-lock heartbeat at the END of this hook's run.
|
|
1074
|
+
//
|
|
1075
|
+
// MEASURED (STATE.md deviation 2026-09-05T06:08:46Z): session-20's lock was
|
|
1076
|
+
// reaped as stale at 06:06Z by an UNRELATED session's SessionEnd, its
|
|
1077
|
+
// heartbeat 11.3 h old against a 4 h TTL. The session was not idle — it was
|
|
1078
|
+
// still inside session-start / session-plan, where no heartbeat writer
|
|
1079
|
+
// existed: `updateHeartbeat()` was called only from the wave loop, from
|
|
1080
|
+
// on-stop and from session-end. A session that never reached /go therefore
|
|
1081
|
+
// aged out of its own lock while working.
|
|
1082
|
+
//
|
|
1083
|
+
// bootstrapLock() above stamps `last_heartbeat = started_at` at LOCK GENESIS,
|
|
1084
|
+
// i.e. before the peer scan, the backfill and the Phase-4 probes have run.
|
|
1085
|
+
// Stamping again here moves the clock to the end of the hook and — more
|
|
1086
|
+
// importantly — makes session-start a heartbeat CALL SITE at all, so the
|
|
1087
|
+
// cadence no longer begins only when the first wave starts.
|
|
1088
|
+
//
|
|
1089
|
+
// `updateHeartbeat()` already carries the same-session guard this needs (it
|
|
1090
|
+
// returns false without writing when the lock belongs to another session),
|
|
1091
|
+
// so no `touchIfOwn()` wrapper is warranted — see scripts/lib/session-lock.mjs.
|
|
1092
|
+
try {
|
|
1093
|
+
const { updateHeartbeat } = await import('../scripts/lib/session-lock.mjs');
|
|
1094
|
+
updateHeartbeat({ repoRoot: projectRoot, sessionId });
|
|
1095
|
+
} catch { /* hook must remain non-blocking */ }
|
|
1096
|
+
|
|
996
1097
|
// Size-based rotation of events.jsonl (#251). Session-start is the single
|
|
997
1098
|
// rotation trigger — per-append overhead is rejected design. Any failure
|
|
998
1099
|
// is swallowed: rotation must NEVER block the hook.
|
package/hooks/on-stop.mjs
CHANGED
|
@@ -16,7 +16,9 @@
|
|
|
16
16
|
* JSONL format (`.orchestrator/metrics/events.jsonl`) — emitted via the canonical
|
|
17
17
|
* `emitEvent()` so the JSONL record and the optional Clank webhook always carry the
|
|
18
18
|
* SAME dotted event name (was: bare `stop`/`subagent_stop` in JSONL vs dotted in webhook):
|
|
19
|
-
* Stop: {"timestamp":<ISO>,"event":"orchestrator.
|
|
19
|
+
* Stop: {"timestamp":<ISO>,"event":"orchestrator.turn.stopped","session_id":"...","semantic_session_id":"...","wave":<int>,"branch":"...","commit":"...","duration_ms":<int>,"duration_source":"session-lock"}
|
|
20
|
+
* + the SAME payload under the DEPRECATED legacy name (one generation, removal 2027-03-06):
|
|
21
|
+
* {"timestamp":<ISO>,"event":"orchestrator.session.stopped",…same fields…,"deprecated":true}
|
|
20
22
|
* (`session_id` / `semantic_session_id` are omitted when unresolvable — #1068 AC1.
|
|
21
23
|
* `duration_ms` + `duration_source` are omitted TOGETHER when no OWNED
|
|
22
24
|
* session.lock is readable — never a fabricated 0, see K5 below.)
|
|
@@ -46,7 +48,7 @@ import { AGENT_ID_RE, resolveSubagentSidecar } from './_lib/subagent-paths.mjs';
|
|
|
46
48
|
if (!shouldRunHook('on-stop')) process.exit(0);
|
|
47
49
|
|
|
48
50
|
import { emitEvent } from '../scripts/lib/events.mjs';
|
|
49
|
-
import {
|
|
51
|
+
import { getProjectDir } from '../scripts/lib/platform.mjs';
|
|
50
52
|
import { parseSessionId } from '../scripts/lib/session-id.mjs';
|
|
51
53
|
import { heartbeat, logSweepEvent } from '../scripts/lib/session-registry.mjs';
|
|
52
54
|
import { readLock, updateHeartbeat } from '../scripts/lib/session-lock.mjs';
|
|
@@ -366,7 +368,7 @@ function resolveStopDuration(input, projectRoot) {
|
|
|
366
368
|
* @param {object|null} input
|
|
367
369
|
*/
|
|
368
370
|
async function handleStop(input) {
|
|
369
|
-
const projectRoot =
|
|
371
|
+
const projectRoot = getProjectDir();
|
|
370
372
|
|
|
371
373
|
const wave = await readWaveNumber(projectRoot);
|
|
372
374
|
const { commit, branch } = await gitInfo(projectRoot);
|
|
@@ -457,15 +459,38 @@ async function handleStop(input) {
|
|
|
457
459
|
// #1183 — a malformed record throws EventValidationError BEFORE any side
|
|
458
460
|
// effect (scripts/lib/events.mjs); this hook must never abort on that, so
|
|
459
461
|
// the emit is wrapped rather than left to propagate.
|
|
462
|
+
//
|
|
463
|
+
// ── THE RENAME (GitLab #1234) ──────────────────────────────────────────────
|
|
464
|
+
// `orchestrator.session.stopped` is a TURN counter with a session's name. The
|
|
465
|
+
// Stop hook fires once per ASSISTANT TURN, not once per session: measured
|
|
466
|
+
// 2026-09-06 over 90 days, 15.538 records against 2.016 distinct
|
|
467
|
+
// `session.started` ids = 7,7 per session, with 184 for a single id. Six
|
|
468
|
+
// consumers read it as a session-lifecycle signal and are wrong by that factor.
|
|
469
|
+
//
|
|
470
|
+
// So the same payload now leaves under BOTH names for one generation:
|
|
471
|
+
// - `orchestrator.turn.stopped` — the correct name, what new readers use.
|
|
472
|
+
// - `orchestrator.session.stopped` — unchanged shape PLUS `deprecated: true`,
|
|
473
|
+
// so an existing reader keeps working and a NEW reader can tell at a glance
|
|
474
|
+
// that it picked the legacy name.
|
|
475
|
+
// Removal of the legacy name: 2027-03-06 (docs/events-schema.md).
|
|
476
|
+
//
|
|
477
|
+
// The SubagentStop branch (`orchestrator.agent.stopped`) is deliberately NOT
|
|
478
|
+
// renamed — it is a different event and its per-agent cardinality is correct.
|
|
479
|
+
const stoppedPayload = {
|
|
480
|
+
...(sessionId !== null ? { session_id: sessionId } : {}),
|
|
481
|
+
...(semanticSessionId !== null ? { semantic_session_id: semanticSessionId } : {}),
|
|
482
|
+
wave,
|
|
483
|
+
...(branch !== null ? { branch } : {}),
|
|
484
|
+
...(commit !== null ? { commit } : {}),
|
|
485
|
+
...duration,
|
|
486
|
+
};
|
|
487
|
+
|
|
460
488
|
try {
|
|
461
|
-
await emitEvent('orchestrator.
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
...(commit !== null ? { commit } : {}),
|
|
467
|
-
...duration,
|
|
468
|
-
});
|
|
489
|
+
await emitEvent('orchestrator.turn.stopped', stoppedPayload);
|
|
490
|
+
} catch { /* telemetry never blocks the hook (#1183) */ }
|
|
491
|
+
|
|
492
|
+
try {
|
|
493
|
+
await emitEvent('orchestrator.session.stopped', { ...stoppedPayload, deprecated: true });
|
|
469
494
|
} catch { /* telemetry never blocks the hook (#1183) */ }
|
|
470
495
|
}
|
|
471
496
|
|
package/hooks/operator-steer.mjs
CHANGED
|
@@ -37,14 +37,14 @@ import { shouldRunHook } from './_lib/profile-gate.mjs';
|
|
|
37
37
|
// Exit 0 immediately when disabled via SO_HOOK_PROFILE / SO_DISABLED_HOOKS.
|
|
38
38
|
if (!shouldRunHook('operator-steer')) process.exit(0);
|
|
39
39
|
|
|
40
|
-
import {
|
|
40
|
+
import { getProjectDir } from '../scripts/lib/platform.mjs';
|
|
41
41
|
|
|
42
42
|
// ---------------------------------------------------------------------------
|
|
43
43
|
// Main
|
|
44
44
|
// ---------------------------------------------------------------------------
|
|
45
45
|
|
|
46
46
|
async function main() {
|
|
47
|
-
const steerPath = path.join(
|
|
47
|
+
const steerPath = path.join(getProjectDir(), '.orchestrator', 'STEER.md');
|
|
48
48
|
|
|
49
49
|
// File absent — nothing to do.
|
|
50
50
|
if (!existsSync(steerPath)) return;
|
|
@@ -163,6 +163,13 @@ import { findScopeFile, pathMatchesPattern } from '../scripts/lib/hardening.mjs'
|
|
|
163
163
|
// is unchanged. Two byte-identical copies of a clock is exactly the one-fact-two-
|
|
164
164
|
// copies class this repo keeps paying for.
|
|
165
165
|
import { sessionAgeMs, PEER_RECORD_PREFIX, isPeerRecordId } from '../scripts/lib/scope-gate.mjs';
|
|
166
|
+
// #1153 P1 — the same process-local ownership check hooks/enforce-scope.mjs
|
|
167
|
+
// applies at Gate 3b. This hook reads the SAME working-copy `wave-scope.json`,
|
|
168
|
+
// so without it a peer session's manifest drives this session's advisories.
|
|
169
|
+
import {
|
|
170
|
+
readProcessLocalSessionIds,
|
|
171
|
+
classifyManifestSession,
|
|
172
|
+
} from '../scripts/lib/session-identity/own-session.mjs';
|
|
166
173
|
|
|
167
174
|
// ---------------------------------------------------------------------------
|
|
168
175
|
// Constants
|
|
@@ -854,6 +861,17 @@ async function main() {
|
|
|
854
861
|
}
|
|
855
862
|
if (raw === null) return; // file vanished between findScopeFile and read
|
|
856
863
|
|
|
864
|
+
// The control-file identity of THIS call, computed BEFORE the G3b
|
|
865
|
+
// stand-down (W4/F7). Ordering is the whole fix: G3b used to `return` above
|
|
866
|
+
// this, so a peer or agent that REBOUND `wave-scope.json` to a fabricated
|
|
867
|
+
// `session_id` with one `cat >` redirect disarmed this session's gates AND
|
|
868
|
+
// suppressed the #938 control-file notice at the same time. Deleting the file
|
|
869
|
+
// was reported (G3 above); rebinding it was not — the louder half of the
|
|
870
|
+
// tamper trail was the one behind the gate the tamper opens.
|
|
871
|
+
//
|
|
872
|
+
// Nothing but the hash/enforcement snapshot is derived here: no path
|
|
873
|
+
// evaluation, no allowedPaths read, no report — those still wait for the
|
|
874
|
+
// classification below to judge the manifest ours.
|
|
857
875
|
const relScopePath = path.relative(repoRoot, scopePath) || scopePath;
|
|
858
876
|
const currentScopeState = {
|
|
859
877
|
hash: createHash('sha1').update(raw).digest('hex').slice(0, 16),
|
|
@@ -863,6 +881,73 @@ async function main() {
|
|
|
863
881
|
gateOn: scope ? scope?.gates?.['bash-write-verify'] !== false : true,
|
|
864
882
|
};
|
|
865
883
|
|
|
884
|
+
// G3b (#1153 P1) — is this manifest even MINE?
|
|
885
|
+
//
|
|
886
|
+
// `wave-scope.json` is a WORKING-COPY artefact; two live sessions in one
|
|
887
|
+
// checkout read the same bytes. Everything below this point — the path
|
|
888
|
+
// report, the control-file notice, the snapshot — describes THIS session's
|
|
889
|
+
// relationship to a wave plan. When the manifest provably names another
|
|
890
|
+
// session, none of it is about us, and the advisories were a peer's
|
|
891
|
+
// `allowedPaths` scolding an unrelated session (#1082 / #1123).
|
|
892
|
+
//
|
|
893
|
+
// Deliberately AFTER the parse: an unparseable manifest leaves `scope === null`,
|
|
894
|
+
// which classifies as `'unknown'` and keeps every pre-#1153 behaviour —
|
|
895
|
+
// including the #938 tamper notices, which must never be reachable via a
|
|
896
|
+
// manifest a Bash call just corrupted. `'own'` and `'unknown'` (legacy,
|
|
897
|
+
// unbound, or no resolvable own identity) fall through unchanged.
|
|
898
|
+
//
|
|
899
|
+
// CEILING (BV-004): on a harness that supplies no session id at all (Codex
|
|
900
|
+
// CLI, Cursor today) the own-id set is empty, so this gate is permanently
|
|
901
|
+
// `'unknown'` = full pre-#1153 behaviour there.
|
|
902
|
+
{
|
|
903
|
+
const ownIds = new Set(readProcessLocalSessionIds({ hookInput: input }));
|
|
904
|
+
const { verdict, manifestIds } = classifyManifestSession(scope ?? {}, ownIds);
|
|
905
|
+
if (verdict === 'foreign') {
|
|
906
|
+
// Observability only — one event per stand-down decision, awaited so the
|
|
907
|
+
// append cannot be lost to the process exiting.
|
|
908
|
+
try {
|
|
909
|
+
const { emitEvent } = await import('../scripts/lib/events.mjs');
|
|
910
|
+
await emitEvent(
|
|
911
|
+
'orchestrator.scope.foreign_session_ignored',
|
|
912
|
+
{
|
|
913
|
+
hook: 'post-bash-write-verify',
|
|
914
|
+
manifest: scopePath,
|
|
915
|
+
manifest_session: manifestIds,
|
|
916
|
+
own_session: [...ownIds],
|
|
917
|
+
wave: scope?.wave,
|
|
918
|
+
},
|
|
919
|
+
{ repoRoot },
|
|
920
|
+
);
|
|
921
|
+
} catch { /* observability is best-effort — never blocks the decision */ }
|
|
922
|
+
|
|
923
|
+
// The stand-down itself is a control-file event when the binding CHANGED
|
|
924
|
+
// since the last call: the manifest this session was baselined against is
|
|
925
|
+
// now bound to someone else, which switches every scope gate off. Reported
|
|
926
|
+
// on the visible channel exactly once — the snapshot below records the new
|
|
927
|
+
// hash, so a stable foreign manifest (an ordinary peer wave) is silent from
|
|
928
|
+
// the next call on, and a rebind back to us reports again.
|
|
929
|
+
if (prevScopeState
|
|
930
|
+
&& prevScopeState.hash !== 'absent'
|
|
931
|
+
&& prevScopeState.hash !== currentScopeState.hash) {
|
|
932
|
+
writeSnapshot(snapFile, { ...carriedRecord, scopeState: currentScopeState });
|
|
933
|
+
emitMessages([
|
|
934
|
+
`bash-write-verify: control file ${relScopePath} changed its SESSION BINDING `
|
|
935
|
+
+ `since the last Bash call (now names ${manifestIds.join(', ') || 'an unnamed session'}) — `
|
|
936
|
+
+ 'this session\'s scope gates are standing down. '
|
|
937
|
+
+ 'Scope-control changes are never exempt from reporting (#938).',
|
|
938
|
+
], true);
|
|
939
|
+
}
|
|
940
|
+
return;
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
// ADVERSARIAL CEILING: `wave-scope.json` is a file ANY process in this working
|
|
945
|
+
// copy can write, so writing a foreign id into it is a per-file kill switch for
|
|
946
|
+
// this session's own gate. Accepted because "no manifest" already means allow
|
|
947
|
+
// (G3), so a foreign manifest grants nothing an `rm` would not — but the change
|
|
948
|
+
// MUST stay visible: see post-bash-write-verify's control-file notice (#938),
|
|
949
|
+
// which is the only thing that distinguishes this from a legitimate peer wave.
|
|
950
|
+
|
|
866
951
|
let scopeMtimeMs = null;
|
|
867
952
|
try {
|
|
868
953
|
scopeMtimeMs = statSync(scopePath).mtimeMs;
|