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
|
@@ -29,19 +29,55 @@
|
|
|
29
29
|
*
|
|
30
30
|
* Exit codes: 0 always (informational, never blocking).
|
|
31
31
|
*
|
|
32
|
+
* OWNERSHIP RULE for the wave keys (#1193 W4c Q1-MED). `.orchestrator/` is
|
|
33
|
+
* repo-global, so `current-session.json` describes whichever session most
|
|
34
|
+
* recently ran SessionStart — routinely a DIFFERENT, still-live session when
|
|
35
|
+
* two windows share this working copy. `last_wave` and `last_wave_completed`
|
|
36
|
+
* are therefore written ONLY when this batch's RAW stdin `session_id` equals
|
|
37
|
+
* the file's `session_id` (a file without one is legacy and allowed). Writing
|
|
38
|
+
* them unguarded was reproduced both ways with the real hook binaries: session
|
|
39
|
+
* A's batch stamped `last_wave_completed` into B's record, so B's own attested
|
|
40
|
+
* SessionEnd stayed silent (the #1193 gap preserved on B), and A's `last_wave`
|
|
41
|
+
* landed in B's record, so B's SessionEnd emitted `wave.completed` for A's wave
|
|
42
|
+
* under B's identity. `last_batch` stays UNGATED — it is a batch-resolution
|
|
43
|
+
* breadcrumb, not a claim about whose wave lifecycle this is.
|
|
44
|
+
*
|
|
45
|
+
* DIFF-SIZE MEASUREMENT (#980). The mechanical fallback below persists
|
|
46
|
+
* `wave_start_sha` (`git rev-parse HEAD`, best-effort) at every wave-OPEN
|
|
47
|
+
* transition and attaches `files_changed` + `files_changed_source:
|
|
48
|
+
* 'worktree-vs-wave-start-sha'` to the `orchestrator.wave.completed` it emits
|
|
49
|
+
* for the wave that sha opened. `files_changed` counts the DEDUPED union of
|
|
50
|
+
* `git diff --name-only <wave_start_sha>` (working tree vs the wave's start
|
|
51
|
+
* commit — the coordinator normally commits only at close, so uncommitted
|
|
52
|
+
* edits must count) and `git ls-files --others --exclude-standard` (untracked).
|
|
53
|
+
* Every git call is best-effort with a 1.5 s timeout; any failure, or a missing
|
|
54
|
+
* `wave_start_sha`, OMITS the key — the convergence-monitor reader treats an
|
|
55
|
+
* absent key as null and the `shrinking_diff` signal simply does not fire.
|
|
56
|
+
* `wave_start_sha` is written under the SAME ownership gate as the wave keys.
|
|
57
|
+
* OUT OF SCOPE: `hooks/on-session-end.mjs`'s final-wave `.completed` carries no
|
|
58
|
+
* `files_changed` — no wave-open transition runs there, so it has no start sha.
|
|
59
|
+
*
|
|
60
|
+
* MONOTONICITY RULE (#1193 W4c Q3-MED-3). Three writers touch
|
|
61
|
+
* `last_wave_completed` (this hook's explicit branch, this hook's fallback,
|
|
62
|
+
* `hooks/on-session-end.mjs`). Every one of them writes through `maxWave()`,
|
|
63
|
+
* so the mark can only ever rise — an explicit `wave-complete{5}` arriving
|
|
64
|
+
* while `last_wave` is 4 must not be walked BACKWARDS to 4 by a later writer.
|
|
65
|
+
*
|
|
32
66
|
* hooks.json wiring is managed separately (W3-C4 scope).
|
|
33
67
|
*/
|
|
34
68
|
|
|
35
|
-
import {
|
|
69
|
+
import { execFileSync } from 'node:child_process';
|
|
70
|
+
import { readFile } from 'node:fs/promises';
|
|
36
71
|
import path from 'node:path';
|
|
37
72
|
|
|
38
73
|
import { shouldRunHook } from './_lib/profile-gate.mjs';
|
|
39
74
|
// Exit 0 immediately when disabled via SO_HOOK_PROFILE / SO_DISABLED_HOOKS.
|
|
40
75
|
if (!shouldRunHook('post-tool-batch-wave-signal')) process.exit(0);
|
|
41
76
|
|
|
42
|
-
import {
|
|
77
|
+
import { getProjectDir } from '../scripts/lib/platform.mjs';
|
|
43
78
|
import { emitEvent } from '../scripts/lib/events.mjs';
|
|
44
79
|
import { findScopeFile } from '../scripts/lib/scope-gate.mjs';
|
|
80
|
+
import { atomicMutateJson } from './_lib/atomic-json.mjs';
|
|
45
81
|
|
|
46
82
|
// ---------------------------------------------------------------------------
|
|
47
83
|
// Helpers
|
|
@@ -74,32 +110,6 @@ function readStdinJson() {
|
|
|
74
110
|
});
|
|
75
111
|
}
|
|
76
112
|
|
|
77
|
-
/**
|
|
78
|
-
* Atomic read-modify-write of a JSON file via temp + rename.
|
|
79
|
-
* Reads the existing file (or starts with `defaultValue` when absent),
|
|
80
|
-
* applies `mutate`, writes to a tmp file, then renames over the original.
|
|
81
|
-
* Atomic on POSIX (same-filesystem rename). Best-effort on Windows.
|
|
82
|
-
*
|
|
83
|
-
* @param {string} filePath
|
|
84
|
-
* @param {object} defaultValue — used when the file does not exist or is unparseable
|
|
85
|
-
* @param {function(object): object} mutate — synchronous pure transformer
|
|
86
|
-
*/
|
|
87
|
-
async function atomicMutateJson(filePath, defaultValue, mutate) {
|
|
88
|
-
let current = defaultValue;
|
|
89
|
-
try {
|
|
90
|
-
const raw = await readFile(filePath, 'utf8');
|
|
91
|
-
current = JSON.parse(raw);
|
|
92
|
-
} catch {
|
|
93
|
-
// File absent or unparseable — start from defaultValue.
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
const updated = mutate(current);
|
|
97
|
-
const tmp = `${filePath}.tmp-ptb-${process.pid}-${Date.now()}`;
|
|
98
|
-
await mkdir(path.dirname(filePath), { recursive: true });
|
|
99
|
-
await writeFile(tmp, JSON.stringify(updated, null, 2) + '\n', 'utf8');
|
|
100
|
-
await rename(tmp, filePath);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
113
|
/**
|
|
104
114
|
* Resolve the session-id for the heartbeat refresh.
|
|
105
115
|
* Precedence:
|
|
@@ -135,6 +145,54 @@ async function resolveSessionIdForHeartbeat(input, sessionFile) {
|
|
|
135
145
|
return null;
|
|
136
146
|
}
|
|
137
147
|
|
|
148
|
+
/**
|
|
149
|
+
* Monotone high-water helper — see the MONOTONICITY RULE in the module
|
|
150
|
+
* docblock. A non-integer `existing` (absent, `null`, or the string `'3'` a
|
|
151
|
+
* hand-edited file can carry) counts as ABSENT, never as a competing value.
|
|
152
|
+
*
|
|
153
|
+
* @param {unknown} existing
|
|
154
|
+
* @param {number} n
|
|
155
|
+
* @returns {number}
|
|
156
|
+
*/
|
|
157
|
+
function maxWave(existing, n) {
|
|
158
|
+
return Number.isInteger(existing) && existing > n ? existing : n;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Does THIS batch own `.orchestrator/current-session.json`?
|
|
163
|
+
* See the OWNERSHIP RULE in the module docblock for why the wave keys need it.
|
|
164
|
+
*
|
|
165
|
+
* Decided on the RAW stdin id only — deliberately NOT via
|
|
166
|
+
* `resolveSessionIdForHeartbeat()`, whose current-session.json fallback would
|
|
167
|
+
* make the compare self-fulfilling (the same F-A defect fixed in
|
|
168
|
+
* `hooks/on-session-end.mjs`: an assertion may never be derived from the value
|
|
169
|
+
* it asserts about). A file carrying no `session_id` is a legacy/pre-#587
|
|
170
|
+
* record with no owner to contend with → allowed.
|
|
171
|
+
*
|
|
172
|
+
* @param {object|null} input Parsed stdin payload.
|
|
173
|
+
* @param {string} sessionFile Absolute path to current-session.json.
|
|
174
|
+
* @returns {Promise<boolean>}
|
|
175
|
+
*/
|
|
176
|
+
async function ownsSessionFile(input, sessionFile) {
|
|
177
|
+
let rawStdinId = null;
|
|
178
|
+
if (typeof input?.session_id === 'string' && input.session_id.length > 0) {
|
|
179
|
+
rawStdinId = input.session_id;
|
|
180
|
+
} else if (typeof input?.sessionId === 'string' && input.sessionId.length > 0) {
|
|
181
|
+
rawStdinId = input.sessionId;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
let recordedId = null;
|
|
185
|
+
try {
|
|
186
|
+
const parsed = JSON.parse(await readFile(sessionFile, 'utf8'));
|
|
187
|
+
if (typeof parsed?.session_id === 'string' && parsed.session_id.length > 0) {
|
|
188
|
+
recordedId = parsed.session_id;
|
|
189
|
+
}
|
|
190
|
+
} catch { /* absent or unparseable → no recorded owner */ }
|
|
191
|
+
|
|
192
|
+
if (recordedId === null) return true;
|
|
193
|
+
return rawStdinId !== null && rawStdinId === recordedId;
|
|
194
|
+
}
|
|
195
|
+
|
|
138
196
|
/**
|
|
139
197
|
* Resolve the current wave number from the wave-scope manifest's `.wave`.
|
|
140
198
|
* Returns 0 when the file is absent or unparseable, mirroring the
|
|
@@ -166,6 +224,87 @@ async function resolveWaveNumber(projectDir) {
|
|
|
166
224
|
}
|
|
167
225
|
}
|
|
168
226
|
|
|
227
|
+
/**
|
|
228
|
+
* The only environment variables forwarded to the `git` children below.
|
|
229
|
+
* Everything else — `GIT_DIR`/`GIT_WORK_TREE`/`GIT_COMMON_DIR` above all — is
|
|
230
|
+
* dropped, so an ambient redirect cannot make these probes measure a FOREIGN
|
|
231
|
+
* repository while `cwd` points at this one (`GIT_DIR` outranks `cwd`; that is
|
|
232
|
+
* exactly the 2026-08-19 redirect incident class).
|
|
233
|
+
*
|
|
234
|
+
* Same 6-entry allowlist as `scripts/lib/git-config-drift.mjs:170` and
|
|
235
|
+
* `check-banner-parity.mjs:63`, COPIED rather than imported on purpose: this is
|
|
236
|
+
* a live hook, and importing a scripts/lib module for six strings would add an
|
|
237
|
+
* edge to the hook import graph (`hooks/_lib/hook-import-set.json`) for no
|
|
238
|
+
* behavioural gain — the constant is cheaper to copy than a shared module is to
|
|
239
|
+
* maintain (BV-001, the same call git-config-drift.mjs documents).
|
|
240
|
+
*/
|
|
241
|
+
const GIT_ENV_ALLOWLIST = Object.freeze(['PATH', 'HOME', 'LANG', 'LC_ALL', 'TMPDIR', 'TZ']);
|
|
242
|
+
|
|
243
|
+
/** @returns {Record<string,string>} the allowlisted subset of `process.env`. */
|
|
244
|
+
function filteredGitEnv() {
|
|
245
|
+
/** @type {Record<string,string>} */
|
|
246
|
+
const out = {};
|
|
247
|
+
for (const key of GIT_ENV_ALLOWLIST) {
|
|
248
|
+
const value = process.env[key];
|
|
249
|
+
if (typeof value === 'string') out[key] = value;
|
|
250
|
+
}
|
|
251
|
+
return out;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** Shared spawn options for the best-effort git probes below (#980). */
|
|
255
|
+
const GIT_OPTS = (projectDir) => ({
|
|
256
|
+
cwd: projectDir,
|
|
257
|
+
encoding: 'utf8',
|
|
258
|
+
timeout: 1_500,
|
|
259
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
260
|
+
env: filteredGitEnv(),
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* `git rev-parse HEAD` in `projectDir`, best-effort (#980).
|
|
265
|
+
* Returns null on ANY failure — not a git repo, detached/empty HEAD, timeout,
|
|
266
|
+
* git absent. A null is persisted as null so a STALE sha from the previous wave
|
|
267
|
+
* can never be mistaken for this wave's start point.
|
|
268
|
+
*
|
|
269
|
+
* @param {string} projectDir
|
|
270
|
+
* @returns {string|null}
|
|
271
|
+
*/
|
|
272
|
+
function readHeadSha(projectDir) {
|
|
273
|
+
try {
|
|
274
|
+
const out = execFileSync('git', ['rev-parse', 'HEAD'], GIT_OPTS(projectDir)).trim();
|
|
275
|
+
return /^[0-9a-f]{40}$/i.test(out) ? out : null;
|
|
276
|
+
} catch {
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Count the files this wave touched: the DEDUPED union of the working tree's
|
|
283
|
+
* diff against `sha` and the untracked (non-ignored) files (#980).
|
|
284
|
+
*
|
|
285
|
+
* Working-tree-vs-sha rather than `sha..HEAD` on purpose: the coordinator
|
|
286
|
+
* commits at session close, not per wave, so a commit-only diff reads 0 for
|
|
287
|
+
* every wave of a normal session.
|
|
288
|
+
*
|
|
289
|
+
* @param {string} projectDir
|
|
290
|
+
* @param {unknown} sha Persisted `wave_start_sha` (may be null/absent).
|
|
291
|
+
* @returns {number|null} null when unmeasurable — caller OMITS the key.
|
|
292
|
+
*/
|
|
293
|
+
function countFilesChangedSince(projectDir, sha) {
|
|
294
|
+
if (typeof sha !== 'string' || !/^[0-9a-f]{40}$/i.test(sha)) return null;
|
|
295
|
+
try {
|
|
296
|
+
const opts = GIT_OPTS(projectDir);
|
|
297
|
+
const tracked = execFileSync('git', ['diff', '--name-only', sha], opts);
|
|
298
|
+
const untracked = execFileSync('git', ['ls-files', '--others', '--exclude-standard'], opts);
|
|
299
|
+
const paths = new Set(
|
|
300
|
+
`${tracked}\n${untracked}`.split('\n').map((l) => l.trim()).filter((l) => l.length > 0),
|
|
301
|
+
);
|
|
302
|
+
return paths.size;
|
|
303
|
+
} catch {
|
|
304
|
+
return null;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
169
308
|
// ---------------------------------------------------------------------------
|
|
170
309
|
// Main
|
|
171
310
|
// ---------------------------------------------------------------------------
|
|
@@ -206,12 +345,18 @@ async function main() {
|
|
|
206
345
|
...(waveSignal !== null ? { wave_signal: waveSignal } : {}),
|
|
207
346
|
};
|
|
208
347
|
|
|
209
|
-
const sessionFile = path.join(
|
|
348
|
+
const sessionFile = path.join(getProjectDir(), '.orchestrator', 'current-session.json');
|
|
210
349
|
|
|
211
|
-
await atomicMutateJson(sessionFile, {}, (current) => ({
|
|
350
|
+
const lastBatchResult = await atomicMutateJson(sessionFile, {}, (current) => ({
|
|
212
351
|
...current,
|
|
213
352
|
last_batch: lastBatch,
|
|
214
|
-
}));
|
|
353
|
+
}), 'ptb');
|
|
354
|
+
// Non-ENOENT failure only drops this turn's last_batch signal — the
|
|
355
|
+
// heartbeat refresh and wave-lifecycle logic below are independent and
|
|
356
|
+
// must still run (a throw here would silently skip both).
|
|
357
|
+
if (!lastBatchResult.ok) {
|
|
358
|
+
console.error(`post-tool-batch-wave-signal: last_batch write skipped (${lastBatchResult.reason})`);
|
|
359
|
+
}
|
|
215
360
|
|
|
216
361
|
// ----------------------------------------------------------------------
|
|
217
362
|
// Heartbeat refresh (Epic #583 W3-P3, wires W2-I3 OQ2).
|
|
@@ -229,7 +374,7 @@ async function main() {
|
|
|
229
374
|
const sessionId = await resolveSessionIdForHeartbeat(input, sessionFile);
|
|
230
375
|
if (typeof sessionId === 'string' && sessionId.length > 0) {
|
|
231
376
|
const { updateHeartbeat } = await import('../scripts/lib/session-lock.mjs');
|
|
232
|
-
updateHeartbeat({ repoRoot:
|
|
377
|
+
updateHeartbeat({ repoRoot: getProjectDir(), sessionId });
|
|
233
378
|
}
|
|
234
379
|
} catch { /* best effort — hook must remain non-blocking */ }
|
|
235
380
|
|
|
@@ -256,6 +401,32 @@ async function main() {
|
|
|
256
401
|
...(batchSize !== null ? { batch_size: batchSize } : {}),
|
|
257
402
|
},
|
|
258
403
|
);
|
|
404
|
+
// #1193 review F2 — this branch is a SECOND emitter of
|
|
405
|
+
// `wave.completed`, so it must feed the same high-water mark the other
|
|
406
|
+
// two read; otherwise SessionEnd (and the fallback below) would close the
|
|
407
|
+
// very wave this signal just closed. Only a numbered completion can be
|
|
408
|
+
// recorded — an unnumbered signal marks nothing.
|
|
409
|
+
//
|
|
410
|
+
// W4c Q2-F1 — this branch also advances `last_wave`. Without it an
|
|
411
|
+
// explicit `wave-complete{5}` left the marker AHEAD of `last_wave: 4`,
|
|
412
|
+
// and SessionEnd then read `4 !== 5`, emitted a duplicate completed(4)
|
|
413
|
+
// and walked the marker BACKWARDS to 4. Both keys go through maxWave().
|
|
414
|
+
// W4c Q1-MED — and neither is written into a PEER's record.
|
|
415
|
+
if (
|
|
416
|
+
waveSignal === 'wave-complete'
|
|
417
|
+
&& typeof waveNumber === 'number'
|
|
418
|
+
&& waveNumber > 0
|
|
419
|
+
&& await ownsSessionFile(input, sessionFile)
|
|
420
|
+
) {
|
|
421
|
+
const waveResult = await atomicMutateJson(sessionFile, {}, (current) => ({
|
|
422
|
+
...current,
|
|
423
|
+
last_wave: maxWave(current?.last_wave, waveNumber),
|
|
424
|
+
last_wave_completed: maxWave(current?.last_wave_completed, waveNumber),
|
|
425
|
+
}), 'ptb');
|
|
426
|
+
if (!waveResult.ok) {
|
|
427
|
+
console.error(`post-tool-batch-wave-signal: last_wave write skipped (${waveResult.reason})`);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
259
430
|
} catch { /* best-effort — hook must remain non-blocking */ }
|
|
260
431
|
} else if (waveSignal === null) {
|
|
261
432
|
// ------------------------------------------------------------------
|
|
@@ -274,26 +445,55 @@ async function main() {
|
|
|
274
445
|
// those windows — a drop to 0 (or any non-increase) is NOT a wave change
|
|
275
446
|
// and is ignored, preventing spurious emissions on every batch.
|
|
276
447
|
//
|
|
277
|
-
// Final
|
|
278
|
-
//
|
|
279
|
-
//
|
|
448
|
+
// Final wave (#1193): no N+1 transition exists for the LAST wave, so its
|
|
449
|
+
// `completed` is emitted by `hooks/on-session-end.mjs` at SessionEnd, not
|
|
450
|
+
// here — and not by the coordinator, which never did (the prose claim this
|
|
451
|
+
// comment used to make was false from #612 until #1193, costing exactly one
|
|
452
|
+
// missing completion per wave run fleet-wide). The two emitters are made
|
|
453
|
+
// idempotent by the `last_wave_completed` high-water mark persisted below:
|
|
454
|
+
// when an N+1 transition already closed wave N, SessionEnd sees
|
|
455
|
+
// last_wave_completed === last_wave and stays silent.
|
|
280
456
|
try {
|
|
281
|
-
const wave = await resolveWaveNumber(
|
|
457
|
+
const wave = await resolveWaveNumber(getProjectDir());
|
|
282
458
|
if (wave > 0) {
|
|
283
459
|
// Read last_wave from the just-written session file (after the
|
|
284
460
|
// last_batch RMW above, so we observe the latest persisted value).
|
|
285
461
|
let lastWave = 0;
|
|
462
|
+
let lastWaveCompleted = 0;
|
|
463
|
+
let waveStartSha = null;
|
|
286
464
|
try {
|
|
287
465
|
const raw = await readFile(sessionFile, 'utf8');
|
|
288
466
|
const parsed = JSON.parse(raw);
|
|
289
467
|
if (typeof parsed.last_wave === 'number') lastWave = parsed.last_wave;
|
|
290
|
-
|
|
468
|
+
// #980 — the sha persisted when THIS `last_wave` was opened.
|
|
469
|
+
if (typeof parsed.wave_start_sha === 'string') waveStartSha = parsed.wave_start_sha;
|
|
470
|
+
// #1193 review F2 — read in the SAME read as `last_wave`: a `/clear`
|
|
471
|
+
// mid-wave already emitted `completed(lastWave)` via SessionEnd, and
|
|
472
|
+
// `on-session-start.mjs` preserves that marker across the restart, so
|
|
473
|
+
// guarding on `wave > lastWave` alone would emit it a second time.
|
|
474
|
+
if (Number.isInteger(parsed.last_wave_completed)) {
|
|
475
|
+
lastWaveCompleted = parsed.last_wave_completed;
|
|
476
|
+
}
|
|
477
|
+
} catch { /* absent/unparseable → both stay 0 */ }
|
|
291
478
|
|
|
292
479
|
if (wave > lastWave) {
|
|
293
|
-
// Close the prior wave first
|
|
294
|
-
|
|
480
|
+
// Close the prior wave first — unless it was already closed. The
|
|
481
|
+
// compare is `>` and not `!==` (W4c Q3-MED-3): a marker AHEAD of
|
|
482
|
+
// `last_wave` (an explicit wave-complete for a higher wave) means the
|
|
483
|
+
// prior wave is already closed too, and a non-integer marker counts
|
|
484
|
+
// as absent rather than as "different".
|
|
485
|
+
if (lastWave > lastWaveCompleted) {
|
|
486
|
+
// #980 — measure the closing wave's diff size BEFORE the new wave's
|
|
487
|
+
// start sha is persisted below. Unmeasurable → both keys omitted.
|
|
488
|
+
const filesChanged = countFilesChangedSince(getProjectDir(), waveStartSha);
|
|
295
489
|
await emitEvent('orchestrator.wave.completed', {
|
|
296
490
|
wave_number: lastWave,
|
|
491
|
+
...(filesChanged !== null
|
|
492
|
+
? {
|
|
493
|
+
files_changed: filesChanged,
|
|
494
|
+
files_changed_source: 'worktree-vs-wave-start-sha',
|
|
495
|
+
}
|
|
496
|
+
: {}),
|
|
297
497
|
...(batchId !== null ? { batch_id: batchId } : {}),
|
|
298
498
|
...(batchSize !== null ? { batch_size: batchSize } : {}),
|
|
299
499
|
});
|
|
@@ -305,11 +505,39 @@ async function main() {
|
|
|
305
505
|
...(batchId !== null ? { batch_id: batchId } : {}),
|
|
306
506
|
...(batchSize !== null ? { batch_size: batchSize } : {}),
|
|
307
507
|
});
|
|
308
|
-
// Persist the high-water
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
508
|
+
// Persist the high-water marks so the next batch does not re-emit,
|
|
509
|
+
// and so SessionEnd can tell whether the prior wave was already
|
|
510
|
+
// closed here (#1193). `last_wave_completed` is only advanced when a
|
|
511
|
+
// `completed` was actually emitted above (lastWave > 0).
|
|
512
|
+
// W4c Q1-MED — never stamp these into a PEER session's record. The
|
|
513
|
+
// events above are still emitted (they carry THIS session's
|
|
514
|
+
// attribution via emitEvent); only the shared-file claim is withheld.
|
|
515
|
+
// NAMED CEILING: for a non-owning session the marks therefore never
|
|
516
|
+
// advance, so this branch re-emits started{wave} once per batch until
|
|
517
|
+
// that session's own SessionStart takes over the file. Revisit if the
|
|
518
|
+
// ledger shows repeated started{N} for one wave in a shared checkout.
|
|
519
|
+
if (await ownsSessionFile(input, sessionFile)) {
|
|
520
|
+
// #980 — the OPEN half: stamp the sha this new wave starts from.
|
|
521
|
+
// Written as null (not omitted) when git is unreadable, so the
|
|
522
|
+
// PREVIOUS wave's sha can never linger and inflate the next count.
|
|
523
|
+
const startSha = readHeadSha(getProjectDir());
|
|
524
|
+
const markResult = await atomicMutateJson(sessionFile, {}, (current) => ({
|
|
525
|
+
...current,
|
|
526
|
+
wave_start_sha: startSha,
|
|
527
|
+
last_wave: maxWave(current?.last_wave, wave),
|
|
528
|
+
...(Math.max(lastWave, lastWaveCompleted) > 0
|
|
529
|
+
? {
|
|
530
|
+
last_wave_completed: maxWave(
|
|
531
|
+
current?.last_wave_completed,
|
|
532
|
+
Math.max(lastWave, lastWaveCompleted),
|
|
533
|
+
),
|
|
534
|
+
}
|
|
535
|
+
: {}),
|
|
536
|
+
}), 'ptb');
|
|
537
|
+
if (!markResult.ok) {
|
|
538
|
+
console.error(`post-tool-batch-wave-signal: last_wave mark skipped (${markResult.reason})`);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
313
541
|
}
|
|
314
542
|
}
|
|
315
543
|
} catch { /* best-effort — hook must remain non-blocking */ }
|
|
@@ -24,14 +24,14 @@
|
|
|
24
24
|
* hooks.json wiring is managed separately (W3-C4 scope).
|
|
25
25
|
*/
|
|
26
26
|
|
|
27
|
-
import { readFile, writeFile, mkdir, rename } from 'node:fs/promises';
|
|
28
27
|
import path from 'node:path';
|
|
29
28
|
|
|
30
29
|
import { shouldRunHook } from './_lib/profile-gate.mjs';
|
|
31
30
|
// Exit 0 immediately when disabled via SO_HOOK_PROFILE / SO_DISABLED_HOOKS.
|
|
32
31
|
if (!shouldRunHook('post-tool-failure-corrective-context')) process.exit(0);
|
|
33
32
|
|
|
34
|
-
import {
|
|
33
|
+
import { getProjectDir } from '../scripts/lib/platform.mjs';
|
|
34
|
+
import { atomicMutateJson } from './_lib/atomic-json.mjs';
|
|
35
35
|
|
|
36
36
|
// ---------------------------------------------------------------------------
|
|
37
37
|
// Constants
|
|
@@ -74,35 +74,6 @@ function readStdinJson() {
|
|
|
74
74
|
});
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
/**
|
|
78
|
-
* Atomic read-modify-write of a JSON file.
|
|
79
|
-
* Reads the existing file (or starts with `defaultValue` when absent),
|
|
80
|
-
* applies `mutate`, writes to a tmp file, renames over the original.
|
|
81
|
-
*
|
|
82
|
-
* @param {string} filePath
|
|
83
|
-
* @param {object} defaultValue — used when the file does not exist
|
|
84
|
-
* @param {function(object): object} mutate — pure transformer
|
|
85
|
-
*/
|
|
86
|
-
async function atomicMutateJson(filePath, defaultValue, mutate) {
|
|
87
|
-
let current = defaultValue;
|
|
88
|
-
try {
|
|
89
|
-
const raw = await readFile(filePath, 'utf8');
|
|
90
|
-
current = JSON.parse(raw);
|
|
91
|
-
} catch {
|
|
92
|
-
// File absent or unparseable — start from defaultValue.
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
const updated = mutate(current);
|
|
96
|
-
const tmp = `${filePath}.tmp-ptf-${process.pid}-${Date.now()}`;
|
|
97
|
-
await mkdir(path.dirname(filePath), { recursive: true });
|
|
98
|
-
await writeFile(tmp, JSON.stringify(updated, null, 2) + '\n', 'utf8');
|
|
99
|
-
|
|
100
|
-
// Rename is atomic on POSIX (same-filesystem). On Windows this is best-effort
|
|
101
|
-
// via the fs.rename syscall (may fail if target is locked — swallowed by
|
|
102
|
-
// the catch in main()).
|
|
103
|
-
await rename(tmp, filePath);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
77
|
// ---------------------------------------------------------------------------
|
|
107
78
|
// Main
|
|
108
79
|
// ---------------------------------------------------------------------------
|
|
@@ -208,16 +179,22 @@ async function main() {
|
|
|
208
179
|
exit_code: exitCode,
|
|
209
180
|
};
|
|
210
181
|
|
|
211
|
-
const sessionFile = path.join(
|
|
182
|
+
const sessionFile = path.join(getProjectDir(), '.orchestrator', 'current-session.json');
|
|
212
183
|
|
|
213
|
-
await atomicMutateJson(sessionFile, {}, (current) => {
|
|
184
|
+
const result = await atomicMutateJson(sessionFile, {}, (current) => {
|
|
214
185
|
const existing = Array.isArray(current.corrective_context)
|
|
215
186
|
? current.corrective_context
|
|
216
187
|
: [];
|
|
217
188
|
// Append the new note and cap at MAX_ENTRIES (keep most-recent).
|
|
218
189
|
const updated = [...existing, note].slice(-MAX_ENTRIES);
|
|
219
190
|
return { ...current, corrective_context: updated };
|
|
220
|
-
});
|
|
191
|
+
}, 'ptf');
|
|
192
|
+
// additionalContext below is derived from `note` in-memory, independent of
|
|
193
|
+
// whether the write landed — a non-ENOENT read/parse failure only means
|
|
194
|
+
// this turn's note is not persisted to current-session.json.
|
|
195
|
+
if (!result.ok) {
|
|
196
|
+
console.error(`post-tool-failure-corrective-context: atomicMutateJson skipped write (${result.reason})`);
|
|
197
|
+
}
|
|
221
198
|
|
|
222
199
|
// Surface corrective context to Claude via additionalContext on the next turn.
|
|
223
200
|
// PostToolUseFailure hookSpecificOutput shape per CC docs:
|
|
@@ -29,7 +29,7 @@ if (!shouldRunHook('frontend-slop-hook')) process.exit(0);
|
|
|
29
29
|
|
|
30
30
|
import path from 'node:path';
|
|
31
31
|
|
|
32
|
-
import {
|
|
32
|
+
import { getProjectDir } from '../scripts/lib/platform.mjs';
|
|
33
33
|
import { emitEvent } from '../scripts/lib/events.mjs';
|
|
34
34
|
import { detectFiles, SCANNABLE_EXTS } from '../scripts/lib/frontend-detect/detect.mjs';
|
|
35
35
|
import { loadFrontendSlopHookConfig } from '../scripts/lib/config/frontend-slop-hook.mjs';
|
|
@@ -98,7 +98,7 @@ function resolveSessionId(input) {
|
|
|
98
98
|
*/
|
|
99
99
|
function toRelPath(filePath) {
|
|
100
100
|
try {
|
|
101
|
-
const rel = path.relative(
|
|
101
|
+
const rel = path.relative(getProjectDir(), filePath);
|
|
102
102
|
if (rel && !rel.startsWith('..') && !path.isAbsolute(rel)) return rel;
|
|
103
103
|
} catch { /* fall through */ }
|
|
104
104
|
return path.basename(filePath);
|
|
@@ -147,7 +147,7 @@ async function main() {
|
|
|
147
147
|
if (!SCANNABLE_EXTS.has(ext)) return;
|
|
148
148
|
|
|
149
149
|
// Session Config gate — OPT-IN (default off).
|
|
150
|
-
const config = await loadFrontendSlopHookConfig({ repoRoot:
|
|
150
|
+
const config = await loadFrontendSlopHookConfig({ repoRoot: getProjectDir() });
|
|
151
151
|
if (config.enabled !== true) return;
|
|
152
152
|
|
|
153
153
|
// Run the deterministic detector (fail-soft: unreadable files → []).
|
|
@@ -95,6 +95,8 @@ let blocker = null;
|
|
|
95
95
|
let readConfigFile;
|
|
96
96
|
let loadEffectivePolicy;
|
|
97
97
|
let isSessionConfigHeading;
|
|
98
|
+
let preprocessBlockLinesNoDash;
|
|
99
|
+
let findUnterminatedComment;
|
|
98
100
|
let emitEvent;
|
|
99
101
|
|
|
100
102
|
/**
|
|
@@ -164,6 +166,7 @@ async function bootstrap() {
|
|
|
164
166
|
config: { specifier: lib('config.mjs') },
|
|
165
167
|
blockedCommandsPolicy: { specifier: lib('blocked-commands-policy.mjs') },
|
|
166
168
|
sectionExtractor: { specifier: lib('config', 'section-extractor.mjs') },
|
|
169
|
+
blockPreprocess: { specifier: lib('config', 'block-preprocess.mjs') },
|
|
167
170
|
events: { specifier: lib('events.mjs') },
|
|
168
171
|
blocker: {
|
|
169
172
|
specifier: lib('command-blocker.mjs'),
|
|
@@ -192,6 +195,7 @@ async function bootstrap() {
|
|
|
192
195
|
({ readConfigFile } = modules.config);
|
|
193
196
|
({ loadEffectivePolicy } = modules.blockedCommandsPolicy);
|
|
194
197
|
({ isSessionConfigHeading } = modules.sectionExtractor);
|
|
198
|
+
({ findUnterminatedComment, preprocessBlockLinesNoDash } = modules.blockPreprocess);
|
|
195
199
|
({ emitEvent } = modules.events);
|
|
196
200
|
blocker = modules.blocker;
|
|
197
201
|
degradedLabels = degraded;
|
|
@@ -760,19 +764,41 @@ async function main() {
|
|
|
760
764
|
const mdContent = await readConfigFile(projectDir);
|
|
761
765
|
// Look for "allow-destructive-ops: true" in the Session Config block.
|
|
762
766
|
// Simple line-based scan: find lines under ## Session Config, stop at next ##.
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
767
|
+
// W4/F1b — HTML-comment stripping is MANDATORY here (same class as #1162a).
|
|
768
|
+
// With a raw `split(/\r?\n/)` a COMMENTED-OUT
|
|
769
|
+
// `<!--\nallow-destructive-ops: true\n-->` under `## Session Config` DISABLED
|
|
770
|
+
// the whole 14-rule guard for the session: commenting a key out is the most
|
|
771
|
+
// ordinary way to turn it off and must never turn it ON. `NoDash` (not the
|
|
772
|
+
// full `preprocessBlockLines`) on purpose — bold-subkey normalisation is not
|
|
773
|
+
// wanted; the regex below already tolerates `- **key:**` and stays as-is.
|
|
774
|
+
//
|
|
775
|
+
// UNTERMINATED `<!--` — the fail-closed direction INVERTS for a bypass scan.
|
|
776
|
+
// `stripHtmlCommentBlocks` returns the lines UNFILTERED when a comment is
|
|
777
|
+
// never closed (right for a block PARSER, nothing may silently vanish), which
|
|
778
|
+
// here would hand the commented-out bypass line back and re-arm the guard's
|
|
779
|
+
// own off switch. For this scan "I cannot tell where the comments end" must
|
|
780
|
+
// mean NOT ARMED: the bypass is opt-in, so refusing to grant it is the safe
|
|
781
|
+
// reading of an ambiguous document. Said out loud on stderr, because a
|
|
782
|
+
// silently-ignored bypass would look identical to a typo the operator meant.
|
|
783
|
+
if (findUnterminatedComment(mdContent.split(/\r?\n/)) !== null) {
|
|
784
|
+
process.stderr.write(
|
|
785
|
+
'ℹ destructive-guard: unterminated <!-- in CLAUDE.md — bypass ignored\n'
|
|
786
|
+
);
|
|
787
|
+
} else {
|
|
788
|
+
const lines = preprocessBlockLinesNoDash(mdContent);
|
|
789
|
+
let inConfig = false;
|
|
790
|
+
for (const line of lines) {
|
|
791
|
+
// SSOT predicate (#968) — never re-derive this comparison. A local copy
|
|
792
|
+
// that drifts LOOSER than the extractor silently disagrees with the
|
|
793
|
+
// runtime about where the config block starts.
|
|
794
|
+
if (isSessionConfigHeading(line)) { inConfig = true; continue; }
|
|
795
|
+
if (inConfig && /^## /.test(line)) break;
|
|
796
|
+
if (inConfig) {
|
|
797
|
+
const m = line.match(/^\s*(?:-\s+\*\*)?allow-destructive-ops(?::\*\*)?\s*:\s*(\S+)/);
|
|
798
|
+
if (m && m[1].toLowerCase() === 'true') {
|
|
799
|
+
process.stderr.write('ℹ destructive-guard bypassed\n');
|
|
800
|
+
return emitAllow();
|
|
801
|
+
}
|
|
776
802
|
}
|
|
777
803
|
}
|
|
778
804
|
}
|
|
@@ -185,13 +185,19 @@ async function main() {
|
|
|
185
185
|
|
|
186
186
|
// G7 — emit canonical event via emitEvent (single emission path: schema + webhook,
|
|
187
187
|
// replacing the local hand-rolled appendFileSync bypass).
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
188
|
+
// #1183 — a malformed record throws EventValidationError BEFORE any side
|
|
189
|
+
// effect (scripts/lib/events.mjs); this PreToolUse hook must never abort on
|
|
190
|
+
// that (it is deny-capable via emitAllow() below), so the emit is wrapped
|
|
191
|
+
// rather than left to propagate.
|
|
192
|
+
try {
|
|
193
|
+
await emitEvent('orchestrator.memory.propose_invoked', {
|
|
194
|
+
session_id: sessionId,
|
|
195
|
+
wave,
|
|
196
|
+
argv_truncated: argvRedacted.slice(0, 512),
|
|
197
|
+
cwd: process.cwd(),
|
|
198
|
+
exit_code: null,
|
|
199
|
+
});
|
|
200
|
+
} catch { /* telemetry never blocks the hook (#1183) */ }
|
|
195
201
|
|
|
196
202
|
// Always allow — this is an observe-only audit hook
|
|
197
203
|
return emitAllow();
|