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
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
3
|
* post-bash-write-verify.mjs — PostToolUse hook (matcher `Bash`): report
|
|
4
|
-
* working-tree changes a Bash call made OUTSIDE the wave's `allowedPaths
|
|
4
|
+
* working-tree changes a Bash call made OUTSIDE the wave's `allowedPaths` —
|
|
5
|
+
* except a path a PEER session declared through a `peer-session-*` record in
|
|
6
|
+
* the wave's aggregate scope sidecar (#1195), which is named as a peer write.
|
|
5
7
|
*
|
|
6
8
|
* ## Why this hook exists (#915, follow-up to #906 / #800)
|
|
7
9
|
*
|
|
@@ -160,7 +162,14 @@ import { findScopeFile, pathMatchesPattern } from '../scripts/lib/hardening.mjs'
|
|
|
160
162
|
// tests/hooks/post-bash-write-verify.test.mjs, which imports the named export —
|
|
161
163
|
// is unchanged. Two byte-identical copies of a clock is exactly the one-fact-two-
|
|
162
164
|
// copies class this repo keeps paying for.
|
|
163
|
-
import { sessionAgeMs } from '../scripts/lib/scope-gate.mjs';
|
|
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';
|
|
164
173
|
|
|
165
174
|
// ---------------------------------------------------------------------------
|
|
166
175
|
// Constants
|
|
@@ -322,6 +331,90 @@ export function isInScope(relPath, allowedPaths) {
|
|
|
322
331
|
return allowedPaths.some((p) => typeof p === 'string' && pathMatchesPattern(relPath, p));
|
|
323
332
|
}
|
|
324
333
|
|
|
334
|
+
/**
|
|
335
|
+
* Re-exported for readers of this hook: the prefix that marks an
|
|
336
|
+
* aggregate-sidecar record as a PEER SESSION's declared scope rather than one
|
|
337
|
+
* of this wave's own agents (#1195). The DEFINITION lives in
|
|
338
|
+
* `scripts/lib/scope-gate.mjs`, next to `unionFileScopes`, which must exclude
|
|
339
|
+
* exactly these records from `allowedPaths` for the peer branch below to be
|
|
340
|
+
* reachable at all.
|
|
341
|
+
*/
|
|
342
|
+
export { PEER_RECORD_PREFIX };
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* The peer-session records of a wave's aggregate scope sidecar.
|
|
346
|
+
*
|
|
347
|
+
* Shape, as written by `scripts/materialize-wave-scope.mjs`
|
|
348
|
+
* (`<state-dir>/filescopes/wave-<N>.scopes.json`): ONE JSON array of
|
|
349
|
+
* `{ id, files }` records — the per-agent files under
|
|
350
|
+
* `<state-dir>/filescopes/wave-<N>/<id>.json` are path STRINGS and are a
|
|
351
|
+
* different artefact (see `docs/scope-collision-guard.md` § 2.2). Only records
|
|
352
|
+
* whose `id` starts with `peer-session-` are returned: those are paths a peer
|
|
353
|
+
* session announced and this session's coordinator carried into the manifest
|
|
354
|
+
* union (`skills/wave-executor/wave-loop.md` § Scope Manifest).
|
|
355
|
+
*
|
|
356
|
+
* Absent, unreadable or malformed sidecar ⇒ `[]`, which restores the exact
|
|
357
|
+
* pre-#1195 behaviour. Never throws.
|
|
358
|
+
*
|
|
359
|
+
* @param {string} stateDir absolute path of the harness state dir (the
|
|
360
|
+
* directory holding `wave-scope.json`)
|
|
361
|
+
* @param {unknown} wave the manifest's `wave` field
|
|
362
|
+
* @returns {Array<{ id: string, files: string[] }>}
|
|
363
|
+
*/
|
|
364
|
+
export function readPeerScopeRecords(stateDir, wave) {
|
|
365
|
+
if (typeof stateDir !== 'string' || stateDir === '') return [];
|
|
366
|
+
if (typeof wave !== 'number' || !Number.isInteger(wave) || wave <= 0) return [];
|
|
367
|
+
let parsed;
|
|
368
|
+
try {
|
|
369
|
+
parsed = JSON.parse(
|
|
370
|
+
readFileSync(path.join(stateDir, 'filescopes', `wave-${wave}.scopes.json`), 'utf8'),
|
|
371
|
+
);
|
|
372
|
+
} catch {
|
|
373
|
+
return [];
|
|
374
|
+
}
|
|
375
|
+
if (!Array.isArray(parsed)) return [];
|
|
376
|
+
return parsed
|
|
377
|
+
.filter(
|
|
378
|
+
(r) => r && typeof r === 'object'
|
|
379
|
+
&& isPeerRecordId(r.id)
|
|
380
|
+
&& Array.isArray(r.files),
|
|
381
|
+
)
|
|
382
|
+
.map((r) => ({ id: r.id, files: r.files.filter((f) => typeof f === 'string') }));
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Which peer record — if any — declared this path?
|
|
387
|
+
*
|
|
388
|
+
* Same matcher as `isInScope`, so a peer's declaration is read exactly the way
|
|
389
|
+
* the wave's own `allowedPaths` are.
|
|
390
|
+
*
|
|
391
|
+
* @param {string} relPath
|
|
392
|
+
* @param {Array<{ id: string, files: string[] }>} peerRecords
|
|
393
|
+
* @returns {string|null} the peer record id, or null
|
|
394
|
+
*/
|
|
395
|
+
export function peerRecordFor(relPath, peerRecords) {
|
|
396
|
+
if (!Array.isArray(peerRecords)) return null;
|
|
397
|
+
for (const record of peerRecords) {
|
|
398
|
+
if (record.files.some((p) => pathMatchesPattern(relPath, p))) return record.id;
|
|
399
|
+
}
|
|
400
|
+
return null;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Render the peer-write notice (#1195) — deliberately a NOTICE and not part of
|
|
405
|
+
* the violation report: a peer record is EXCLUDED from the union that computes
|
|
406
|
+
* `allowedPaths` (`unionFileScopes`, `scripts/lib/scope-gate.mjs`), so a peer
|
|
407
|
+
* path is outside `allowedPaths` and reaches this branch; naming it as a
|
|
408
|
+
* violation would re-file an agreed peer write as an alarm.
|
|
409
|
+
*
|
|
410
|
+
* @param {string} relPath
|
|
411
|
+
* @param {string} peerId the `peer-session-<id>` record id
|
|
412
|
+
* @returns {string}
|
|
413
|
+
*/
|
|
414
|
+
export function formatPeerWriteNotice(relPath, peerId) {
|
|
415
|
+
return `bash-write-verify: ${relPath} inside ${peerId} scope — peer write, not a violation`;
|
|
416
|
+
}
|
|
417
|
+
|
|
325
418
|
/**
|
|
326
419
|
* Stable signature of the wave's scope. A change means a new wave, which must
|
|
327
420
|
* re-baseline silently rather than blame the next Bash call for the previous
|
|
@@ -768,6 +861,17 @@ async function main() {
|
|
|
768
861
|
}
|
|
769
862
|
if (raw === null) return; // file vanished between findScopeFile and read
|
|
770
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.
|
|
771
875
|
const relScopePath = path.relative(repoRoot, scopePath) || scopePath;
|
|
772
876
|
const currentScopeState = {
|
|
773
877
|
hash: createHash('sha1').update(raw).digest('hex').slice(0, 16),
|
|
@@ -777,6 +881,73 @@ async function main() {
|
|
|
777
881
|
gateOn: scope ? scope?.gates?.['bash-write-verify'] !== false : true,
|
|
778
882
|
};
|
|
779
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
|
+
|
|
780
951
|
let scopeMtimeMs = null;
|
|
781
952
|
try {
|
|
782
953
|
scopeMtimeMs = statSync(scopePath).mtimeMs;
|
|
@@ -915,8 +1086,22 @@ async function main() {
|
|
|
915
1086
|
// first run is named, not silently trusted. Computed before the G4/G5 gate
|
|
916
1087
|
// above so the gate cannot swallow it.
|
|
917
1088
|
if (missingSnapshotNotice) messages.push(missingSnapshotNotice);
|
|
918
|
-
|
|
919
|
-
|
|
1089
|
+
// #1195 — a path a PEER session declared (a `peer-session-*` record in this
|
|
1090
|
+
// wave's aggregate sidecar) is outside `allowedPaths` because `--union`
|
|
1091
|
+
// EXCLUDES peer records when computing it (`unionFileScopes`,
|
|
1092
|
+
// `scripts/lib/scope-gate.mjs`) — that exclusion is what keeps this branch
|
|
1093
|
+
// reachable. Such a write is agreed, not a bypass. Split it out of the violation report and
|
|
1094
|
+
// name it, so the peer's file is countable without being an alarm. Sidecar
|
|
1095
|
+
// absent ⇒ `peerRecords` is empty ⇒ every path stays a violation, unchanged.
|
|
1096
|
+
const peerRecords = readPeerScopeRecords(path.dirname(scopePath), scope.wave);
|
|
1097
|
+
const violations = [];
|
|
1098
|
+
for (const relPath of report) {
|
|
1099
|
+
const peerId = peerRecordFor(relPath, peerRecords);
|
|
1100
|
+
if (peerId) messages.push(formatPeerWriteNotice(relPath, peerId));
|
|
1101
|
+
else violations.push(relPath);
|
|
1102
|
+
}
|
|
1103
|
+
if (violations.length > 0) {
|
|
1104
|
+
messages.push(formatMessage(violations, allowedPaths.length));
|
|
920
1105
|
warn = true;
|
|
921
1106
|
}
|
|
922
1107
|
if (messages.length === 0) return; // silence is the common case (0.91 % fire rate)
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* post-edit-import-probe.mjs — PostToolUse hook that catches a BROKEN
|
|
4
|
+
* hook-reachable module the moment it is saved (GitLab #1224).
|
|
5
|
+
*
|
|
6
|
+
* Incident 2026-09-04: `scripts/lib/session-identity/own-session.mjs` was saved
|
|
7
|
+
* with a call to a function that was written two edits later. Three hook entry
|
|
8
|
+
* files import that module on every tool call, so every Bash/Edit/Write call
|
|
9
|
+
* across the host — including two foreign sessions — got "Internal hook error —
|
|
10
|
+
* request blocked" for ~8 minutes. The defect was invisible to `node --check`
|
|
11
|
+
* (syntactically valid) and to a plain `await import()` (module-level
|
|
12
|
+
* evaluation succeeds; the ReferenceError is raised only when the exported
|
|
13
|
+
* function is CALLED). ESLint `no-undef` catches it — that is the primary check
|
|
14
|
+
* here; the import probe is the secondary net for load-time throws.
|
|
15
|
+
*
|
|
16
|
+
* Decision flow (each gate exits 0 silently):
|
|
17
|
+
* G1 profile gate — shouldRunHook('post-edit-import-probe')
|
|
18
|
+
* G2 stdin JSON (null-safe)
|
|
19
|
+
* G3 tool_name ∈ {Edit, Write, MultiEdit}
|
|
20
|
+
* G4 tool_input.file_path present
|
|
21
|
+
* G5 extension ∈ {.mjs, .js, .cjs}
|
|
22
|
+
* G6 membership in hooks/_lib/hook-import-set.json (the committed
|
|
23
|
+
* hook-reachable allowlist) — anything else is not this probe's business
|
|
24
|
+
* C1 PRIMARY: ESLint on the single file; only `no-undef` and fatal/parse
|
|
25
|
+
* errors count as a failure (other rules belong to `npm run lint`)
|
|
26
|
+
* C2 SECONDARY (scripts/lib/** only — NEVER hooks/*.mjs, half of which run
|
|
27
|
+
* main() at module bottom): `await import(file)` in a child process
|
|
28
|
+
*
|
|
29
|
+
* The PRIMARY check needs an ESLint install in EITHER the project's own
|
|
30
|
+
* `node_modules` (tried first — an npm-installed consumer repo has no plugin
|
|
31
|
+
* devDeps) or the plugin root's; with neither, C1 degrades to a silent no-op
|
|
32
|
+
* and only C2 remains. `SO_IMPORT_PROBE_ESLINT` overrides that resolution.
|
|
33
|
+
*
|
|
34
|
+
* Output on failure: an `additionalContext` roll-up + a `systemMessage`.
|
|
35
|
+
* Exit codes: 0 ALWAYS — a probe for hook breakage must never become one
|
|
36
|
+
* (the probe's own internal faults go to stderr, never to the tool call).
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
import { shouldRunHook } from './_lib/profile-gate.mjs';
|
|
40
|
+
// Exit 0 immediately when disabled via SO_HOOK_PROFILE / SO_DISABLED_HOOKS.
|
|
41
|
+
if (!shouldRunHook('post-edit-import-probe')) process.exit(0);
|
|
42
|
+
|
|
43
|
+
import path from 'node:path';
|
|
44
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
45
|
+
import { spawnSync } from 'node:child_process';
|
|
46
|
+
import { performance } from 'node:perf_hooks';
|
|
47
|
+
|
|
48
|
+
import { getProjectDir } from '../scripts/lib/platform.mjs';
|
|
49
|
+
|
|
50
|
+
/** Tool names whose payload carries an edited file path. */
|
|
51
|
+
const EDIT_TOOLS = new Set(['Edit', 'Write', 'MultiEdit']);
|
|
52
|
+
|
|
53
|
+
/** Extensions this probe can reason about (Node ESM/CJS modules). */
|
|
54
|
+
const MODULE_EXTS = new Set(['.mjs', '.js', '.cjs']);
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Total wall-clock budget for BOTH checks. The hooks.json ceiling is 5 s; the
|
|
58
|
+
* measured ESLint single-file run is 0.3–0.5 s, so 3 s leaves headroom for a
|
|
59
|
+
* cold start without ever approaching the harness timeout.
|
|
60
|
+
*/
|
|
61
|
+
const BUDGET_MS = 3_000;
|
|
62
|
+
|
|
63
|
+
/** The plugin's own root (…/hooks/.. ), used to locate a vendored ESLint. */
|
|
64
|
+
const PLUGIN_ROOT = path.resolve(import.meta.dirname, '..');
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Read stdin to EOF (best-effort). Returns parsed JSON or null on any failure.
|
|
68
|
+
*
|
|
69
|
+
* @returns {Promise<object|null>}
|
|
70
|
+
*/
|
|
71
|
+
function readStdinJson() {
|
|
72
|
+
return new Promise((resolve) => {
|
|
73
|
+
if (process.stdin.readableEnded || process.stdin.closed) { resolve(null); return; }
|
|
74
|
+
const chunks = [];
|
|
75
|
+
const timer = setTimeout(() => resolve(null), 5_000);
|
|
76
|
+
process.stdin.setEncoding('utf8');
|
|
77
|
+
process.stdin.on('data', (c) => chunks.push(c));
|
|
78
|
+
process.stdin.on('end', () => {
|
|
79
|
+
clearTimeout(timer);
|
|
80
|
+
const raw = chunks.join('').trim();
|
|
81
|
+
if (!raw) { resolve(null); return; }
|
|
82
|
+
try { resolve(JSON.parse(raw)); } catch { resolve(null); }
|
|
83
|
+
});
|
|
84
|
+
process.stdin.on('error', () => { clearTimeout(timer); resolve(null); });
|
|
85
|
+
process.stdin.resume();
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Load the committed hook-reachable allowlist.
|
|
91
|
+
* Missing / unreadable / malformed → empty map (silent no-op, never a crash).
|
|
92
|
+
*
|
|
93
|
+
* @param {string} projectDir
|
|
94
|
+
* @returns {Map<string, string[]>} repo-relative path → hook entry basenames
|
|
95
|
+
*/
|
|
96
|
+
export function loadImportSet(projectDir) {
|
|
97
|
+
const file = path.join(projectDir, 'hooks', '_lib', 'hook-import-set.json');
|
|
98
|
+
try {
|
|
99
|
+
if (!existsSync(file)) return new Map();
|
|
100
|
+
const parsed = JSON.parse(readFileSync(file, 'utf8'));
|
|
101
|
+
const entries = Array.isArray(parsed?.entries) ? parsed.entries : [];
|
|
102
|
+
const map = new Map();
|
|
103
|
+
for (const e of entries) {
|
|
104
|
+
if (e && typeof e.file === 'string') {
|
|
105
|
+
map.set(e.file, Array.isArray(e.reachable_from) ? e.reachable_from : []);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return map;
|
|
109
|
+
} catch {
|
|
110
|
+
return new Map();
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Repo-relative POSIX path, or null when the file lives outside the project.
|
|
116
|
+
*
|
|
117
|
+
* @param {string} projectDir
|
|
118
|
+
* @param {string} absFile
|
|
119
|
+
* @returns {string|null}
|
|
120
|
+
*/
|
|
121
|
+
export function toRepoRelative(projectDir, absFile) {
|
|
122
|
+
const rel = path.relative(projectDir, absFile);
|
|
123
|
+
if (!rel || rel.startsWith('..') || path.isAbsolute(rel)) return null;
|
|
124
|
+
return rel.split(path.sep).join('/');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Locate an ESLint entry script. The PROJECT's own install is tried first —
|
|
129
|
+
* in an npm-installed consumer repo the plugin root has no devDependencies at
|
|
130
|
+
* all, so a plugin-root-only lookup makes the primary check a silent no-op
|
|
131
|
+
* there. Both the `.bin` shim and the package's own entry script are probed at
|
|
132
|
+
* each root (a pnpm/npm layout may publish either).
|
|
133
|
+
*
|
|
134
|
+
* `SO_IMPORT_PROBE_ESLINT` overrides the search: an explicit path is used as
|
|
135
|
+
* given, and an EMPTY value suppresses the check (test seam for the
|
|
136
|
+
* eslint-unavailable path).
|
|
137
|
+
*
|
|
138
|
+
* Returns null when nothing is found → the check is skipped silently: a
|
|
139
|
+
* consumer repo without ESLint installed is not a defect.
|
|
140
|
+
*
|
|
141
|
+
* @param {string} projectDir
|
|
142
|
+
* @returns {string|null}
|
|
143
|
+
*/
|
|
144
|
+
export function resolveEslintBin(projectDir) {
|
|
145
|
+
const override = process.env.SO_IMPORT_PROBE_ESLINT;
|
|
146
|
+
if (typeof override === 'string') {
|
|
147
|
+
if (!override) return null;
|
|
148
|
+
try { return existsSync(override) ? override : null; } catch { return null; }
|
|
149
|
+
}
|
|
150
|
+
const candidates = [];
|
|
151
|
+
for (const root of [projectDir, PLUGIN_ROOT]) {
|
|
152
|
+
candidates.push(path.join(root, 'node_modules', '.bin', 'eslint'));
|
|
153
|
+
candidates.push(path.join(root, 'node_modules', 'eslint', 'bin', 'eslint.js'));
|
|
154
|
+
}
|
|
155
|
+
for (const c of candidates) {
|
|
156
|
+
try { if (existsSync(c)) return c; } catch { /* unreadable → next candidate */ }
|
|
157
|
+
}
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* PRIMARY check — run ESLint on the single file and keep only the message
|
|
163
|
+
* classes that indicate a module which will THROW when a hook calls into it:
|
|
164
|
+
* `no-undef` (the 2026-09-04 incident shape) and any fatal/parse error.
|
|
165
|
+
*
|
|
166
|
+
* @param {string} projectDir
|
|
167
|
+
* @param {string} absFile
|
|
168
|
+
* @param {number} timeoutMs
|
|
169
|
+
* @returns {string|null} first offending message, or null when clean/skipped
|
|
170
|
+
*/
|
|
171
|
+
export function runEslintCheck(projectDir, absFile, timeoutMs) {
|
|
172
|
+
const bin = resolveEslintBin(projectDir);
|
|
173
|
+
if (!bin) {
|
|
174
|
+
// ESLint unavailable → skip, never guess. Traced so a consumer repo can
|
|
175
|
+
// tell "primary check found nothing" from "primary check never ran".
|
|
176
|
+
if (process.env.SO_IMPORT_PROBE_TRACE) process.stderr.write('probe:eslint-unavailable\n');
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const res = spawnSync(
|
|
181
|
+
process.execPath,
|
|
182
|
+
[bin, '--no-warn-ignored', '--format', 'json', absFile],
|
|
183
|
+
{ cwd: projectDir, encoding: 'utf8', timeout: timeoutMs },
|
|
184
|
+
);
|
|
185
|
+
// A crashed/timed-out ESLint produces no parseable report — skip rather than
|
|
186
|
+
// invent a finding. Same for a repo whose flat config cannot be resolved.
|
|
187
|
+
let report;
|
|
188
|
+
try { report = JSON.parse(res.stdout ?? ''); } catch { return null; }
|
|
189
|
+
if (!Array.isArray(report)) return null;
|
|
190
|
+
|
|
191
|
+
for (const fileResult of report) {
|
|
192
|
+
for (const m of fileResult?.messages ?? []) {
|
|
193
|
+
const isFatal = m.fatal === true;
|
|
194
|
+
const isUndef = m.ruleId === 'no-undef';
|
|
195
|
+
if (isFatal || isUndef) {
|
|
196
|
+
return `L${m.line ?? '?'} ${m.ruleId ?? 'parse-error'}: ${m.message}`;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* SECONDARY check — actually import the module in a child process to catch a
|
|
205
|
+
* load-time throw. Restricted to `scripts/lib/**`: roughly half the
|
|
206
|
+
* `hooks/*.mjs` entry files call `main()` at module bottom, so importing one
|
|
207
|
+
* would EXECUTE it.
|
|
208
|
+
*
|
|
209
|
+
* @param {string} absFile
|
|
210
|
+
* @param {number} timeoutMs
|
|
211
|
+
* @returns {string|null} first error line, or null when clean
|
|
212
|
+
*/
|
|
213
|
+
export function runImportCheck(absFile, timeoutMs) {
|
|
214
|
+
const res = spawnSync(
|
|
215
|
+
process.execPath,
|
|
216
|
+
[
|
|
217
|
+
'--input-type=module',
|
|
218
|
+
'-e',
|
|
219
|
+
"const { pathToFileURL } = await import('node:url');"
|
|
220
|
+
+ ' await import(pathToFileURL(process.env.SO_IMPORT_PROBE_TARGET).href);',
|
|
221
|
+
],
|
|
222
|
+
{
|
|
223
|
+
encoding: 'utf8',
|
|
224
|
+
timeout: timeoutMs,
|
|
225
|
+
// NO positional argument: a target passed as argv[1] satisfies the
|
|
226
|
+
// `import.meta.url === file://${process.argv[1]}` main-guard that many
|
|
227
|
+
// modules carry, so the probe would EXECUTE their CLI main() (measured:
|
|
228
|
+
// scripts/lib/sunset/walker.mjs walks the repo for ~3.9 s and is then
|
|
229
|
+
// SIGTERMed at the budget). The path travels in the environment instead.
|
|
230
|
+
// The env is minimal and deliberately drops NODE_OPTIONS — an inherited
|
|
231
|
+
// --require/--import would run foreign code inside the probe.
|
|
232
|
+
env: {
|
|
233
|
+
PATH: process.env.PATH ?? '',
|
|
234
|
+
HOME: process.env.HOME ?? '',
|
|
235
|
+
SO_HOOK_PROFILE: 'off',
|
|
236
|
+
SO_IMPORT_PROBE_TARGET: absFile,
|
|
237
|
+
},
|
|
238
|
+
},
|
|
239
|
+
);
|
|
240
|
+
if (res.status === 0) return null;
|
|
241
|
+
// A timeout (killed child, status null) is not evidence of breakage.
|
|
242
|
+
if (res.status === null) return null;
|
|
243
|
+
// Node echoes the offending SOURCE line before the diagnostic, and that echo
|
|
244
|
+
// also contains the word "Error" — anchor on the diagnostic shape
|
|
245
|
+
// ("ReferenceError: x is not defined") so the report names the fault, not the
|
|
246
|
+
// source text that triggered it.
|
|
247
|
+
const lines = String(res.stderr ?? '').split('\n').map((s) => s.trim()).filter(Boolean);
|
|
248
|
+
const diagnostic = lines.find((s) => /^[A-Za-z]*Error(:|\b)/.test(s));
|
|
249
|
+
return diagnostic || lines.find((s) => /Error/.test(s)) || `import failed with exit ${res.status}`;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Build the operator-facing warning. It names the blast radius explicitly —
|
|
254
|
+
* the whole point of the probe is that a broken module here is not a local
|
|
255
|
+
* defect.
|
|
256
|
+
*
|
|
257
|
+
* @param {string} relPath
|
|
258
|
+
* @param {string} error
|
|
259
|
+
* @param {string[]} reachableFrom
|
|
260
|
+
* @returns {string}
|
|
261
|
+
*/
|
|
262
|
+
export function buildWarning(relPath, error, reachableFrom) {
|
|
263
|
+
const hooks = reachableFrom.length > 0 ? reachableFrom.join(', ') : '(unknown)';
|
|
264
|
+
return (
|
|
265
|
+
`⚠ import-probe: ${relPath} — ${error}. ` +
|
|
266
|
+
`This module is reached by hooks ${hooks} — every Edit/Write/Bash call may now be ` +
|
|
267
|
+
`blocked host-wide. Fix before continuing.`
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// ---------------------------------------------------------------------------
|
|
272
|
+
// main
|
|
273
|
+
// ---------------------------------------------------------------------------
|
|
274
|
+
|
|
275
|
+
async function main() {
|
|
276
|
+
const started = performance.now();
|
|
277
|
+
const input = await readStdinJson();
|
|
278
|
+
if (!input) return; // G2
|
|
279
|
+
|
|
280
|
+
const toolName = typeof input.tool_name === 'string' ? input.tool_name : null;
|
|
281
|
+
if (!toolName || !EDIT_TOOLS.has(toolName)) return; // G3
|
|
282
|
+
|
|
283
|
+
const toolInput = input.tool_input;
|
|
284
|
+
const filePath = toolInput && typeof toolInput.file_path === 'string'
|
|
285
|
+
? toolInput.file_path : null;
|
|
286
|
+
if (!filePath) return; // G4
|
|
287
|
+
|
|
288
|
+
if (!MODULE_EXTS.has(path.extname(filePath).toLowerCase())) return; // G5
|
|
289
|
+
|
|
290
|
+
const projectDir = getProjectDir();
|
|
291
|
+
const absFile = path.resolve(projectDir, filePath);
|
|
292
|
+
const rel = toRepoRelative(projectDir, absFile);
|
|
293
|
+
if (!rel) return;
|
|
294
|
+
|
|
295
|
+
const importSet = loadImportSet(projectDir);
|
|
296
|
+
if (!importSet.has(rel)) return; // G6
|
|
297
|
+
const reachableFrom = importSet.get(rel) ?? [];
|
|
298
|
+
|
|
299
|
+
// Trace hook for tests: proves the gates above short-circuit BEFORE any spawn.
|
|
300
|
+
if (process.env.SO_IMPORT_PROBE_TRACE) process.stderr.write('probe:checks-start\n');
|
|
301
|
+
|
|
302
|
+
let error = runEslintCheck(projectDir, absFile, BUDGET_MS); // C1
|
|
303
|
+
let check = 'eslint';
|
|
304
|
+
|
|
305
|
+
if (!error && rel.startsWith('scripts/lib/')) { // C2
|
|
306
|
+
// spawnSync rejects a fractional timeout (RangeError) — round it.
|
|
307
|
+
const remaining = Math.max(250, Math.round(BUDGET_MS - (performance.now() - started)));
|
|
308
|
+
error = runImportCheck(absFile, remaining);
|
|
309
|
+
if (error) check = 'import';
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (!error) return;
|
|
313
|
+
|
|
314
|
+
const durationMs = Math.round(performance.now() - started);
|
|
315
|
+
try {
|
|
316
|
+
const { emitEvent } = await import('../scripts/lib/events.mjs');
|
|
317
|
+
await emitEvent('orchestrator.hook.import_probe_failed', {
|
|
318
|
+
file: rel,
|
|
319
|
+
check,
|
|
320
|
+
error,
|
|
321
|
+
reachable_from: reachableFrom,
|
|
322
|
+
duration_ms: durationMs,
|
|
323
|
+
});
|
|
324
|
+
} catch { /* best-effort telemetry — never blocks the hook */ }
|
|
325
|
+
|
|
326
|
+
process.stdout.write(JSON.stringify({
|
|
327
|
+
hookSpecificOutput: {
|
|
328
|
+
hookEventName: 'PostToolUse',
|
|
329
|
+
additionalContext: buildWarning(rel, error, reachableFrom),
|
|
330
|
+
},
|
|
331
|
+
systemMessage: `⚠ import-probe FAILED: ${rel}`,
|
|
332
|
+
}));
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Exit 0 always — a hook that guards against hook breakage must never block.
|
|
336
|
+
// The catch stays silent towards the TOOL CALL but not towards the operator:
|
|
337
|
+
// a probe whose own breakage is invisible is a probe nobody can trust.
|
|
338
|
+
main()
|
|
339
|
+
.catch((err) => {
|
|
340
|
+
try {
|
|
341
|
+
process.stderr.write(`post-edit-import-probe: ${err?.message ?? String(err)}\n`);
|
|
342
|
+
} catch { /* stderr gone — nothing left to report to */ }
|
|
343
|
+
})
|
|
344
|
+
.finally(() => process.exit(0));
|