session-orchestrator 3.24.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/architecture/SKILL.md +18 -0
- package/.agents/skills/autopilot/SKILL.md +17 -0
- package/.agents/skills/bootstrap/SKILL.md +20 -0
- package/.agents/skills/brainstorm/SKILL.md +22 -0
- package/.agents/skills/claude-md-drift-check/SKILL.md +15 -0
- package/.agents/skills/convergence-monitoring/SKILL.md +22 -0
- package/.agents/skills/debug/SKILL.md +22 -0
- package/.agents/skills/discovery/SKILL.md +20 -0
- package/.agents/skills/dispatcher/SKILL.md +15 -0
- package/.agents/skills/docs-orchestrator/SKILL.md +18 -0
- package/.agents/skills/ecosystem-health/SKILL.md +20 -0
- package/.agents/skills/eli5/SKILL.md +20 -0
- package/.agents/skills/eval/SKILL.md +21 -0
- package/.agents/skills/evolve/SKILL.md +21 -0
- package/.agents/skills/frontmatter-guard/SKILL.md +15 -0
- package/.agents/skills/gitlab-ops/SKILL.md +20 -0
- package/.agents/skills/gitlab-portfolio/SKILL.md +15 -0
- package/.agents/skills/grill/SKILL.md +22 -0
- package/.agents/skills/hook-development/SKILL.md +15 -0
- package/.agents/skills/mcp-builder/SKILL.md +15 -0
- package/.agents/skills/memory-cleanup/SKILL.md +21 -0
- package/.agents/skills/mode-selector/SKILL.md +17 -0
- package/.agents/skills/npm-publish/SKILL.md +16 -0
- package/.agents/skills/peekaboo-driver/SKILL.md +18 -0
- package/.agents/skills/persona-panel/SKILL.md +17 -0
- package/.agents/skills/plan/SKILL.md +20 -0
- package/.agents/skills/playwright-driver/SKILL.md +20 -0
- package/.agents/skills/quality-gates/SKILL.md +20 -0
- package/.agents/skills/reconcile/SKILL.md +21 -0
- package/.agents/skills/remote-offload/SKILL.md +20 -0
- package/.agents/skills/repo-audit/SKILL.md +16 -0
- package/.agents/skills/session-end/SKILL.md +20 -0
- package/.agents/skills/session-plan/SKILL.md +20 -0
- package/.agents/skills/session-start/SKILL.md +20 -0
- package/.agents/skills/spinout/SKILL.md +16 -0
- package/.agents/skills/sunset-review/SKILL.md +16 -0
- package/.agents/skills/test-runner/SKILL.md +20 -0
- package/.agents/skills/tmux-layout/SKILL.md +21 -0
- package/.agents/skills/using-orchestrator/SKILL.md +17 -0
- package/.agents/skills/vault-mirror/SKILL.md +15 -0
- package/.agents/skills/vault-sync/SKILL.md +15 -0
- package/.agents/skills/wave-executor/SKILL.md +20 -0
- package/.agents/skills/write-executable-plan/SKILL.md +22 -0
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor/commands/autopilot.md +2 -2
- package/.cursor/commands/bootstrap.md +1 -1
- package/.cursor/commands/brainstorm.md +1 -1
- package/.cursor/commands/debug.md +1 -1
- package/.cursor/commands/discovery.md +1 -1
- package/.cursor/commands/dispatcher.md +2 -2
- package/.cursor/commands/eli5.md +2 -2
- package/.cursor/commands/eval.md +2 -2
- package/.cursor/commands/evolve.md +1 -1
- package/.cursor/commands/go.md +1 -1
- package/.cursor/commands/grill.md +2 -2
- package/.cursor/commands/memory-cleanup.md +2 -2
- package/.cursor/commands/persona-panel.md +1 -1
- package/.cursor/commands/plan.md +1 -1
- package/.cursor/commands/portfolio.md +1 -1
- package/.cursor/commands/reconcile.md +2 -2
- package/.cursor/commands/release.md +2 -2
- package/.cursor/commands/session.md +2 -2
- package/.cursor/commands/spinout.md +2 -2
- package/.cursor/commands/sunset-review.md +2 -2
- package/.cursor/commands/templates-ack.md +2 -2
- package/.cursor/commands/test.md +2 -2
- package/.cursor/skills/brainstorm/SKILL.md +1 -1
- package/.cursor/skills/eval/SKILL.md +1 -1
- package/.cursor/skills/quality-gates/SKILL.md +1 -1
- package/.cursor/skills/remote-offload/SKILL.md +1 -1
- package/.orchestrator/policy/blocked-commands.json +121 -0
- package/.orchestrator/policy/ecosystem.schema.json +66 -0
- package/.orchestrator/policy/quality-gates.example.json +16 -0
- package/.orchestrator/policy/quality-gates.schema.json +38 -0
- package/.orchestrator/policy/templates-policy.json +27 -0
- package/.orchestrator/policy/test-profiles.json +47 -0
- package/AGENTS.md +225 -0
- package/CHANGELOG.md +1125 -2
- package/NOTICE +11 -6
- package/README.md +127 -94
- package/agents/eval-judge.md +1 -1
- package/agents/skill-applied-judge.md +1 -1
- package/assets/wave-lifecycle.svg +98 -0
- package/commands/release.md +6 -3
- package/commands/session.md +18 -3
- package/docs/README.md +4 -0
- package/{agents/AGENTS.md → docs/agent-authoring.md} +19 -26
- package/docs/baseline.md +67 -0
- package/docs/ci-setup.md +108 -62
- package/docs/codex-setup.md +65 -21
- package/docs/components.md +36 -15
- package/docs/cursor-setup.md +6 -2
- package/docs/events-schema.md +9 -6
- package/docs/instruction-delivery.md +62 -0
- package/{agents/memory-proposal-collector.md → docs/memory-proposal-flow.md} +1 -8
- package/docs/migration-v4.md +341 -0
- package/docs/pi-setup.md +6 -1
- package/docs/plugin-architecture-v3.md +1 -1
- package/docs/rule-authoring.md +85 -19
- package/docs/scope-collision-guard.md +5 -5
- package/docs/session-config-reference.md +57 -56
- package/docs/session-config-template.md +6 -29
- package/docs/telemetry.md +157 -3
- package/docs/vault-docs-architecture.md +50 -11
- package/hooks/_lib/hook-import-set.json +1487 -0
- package/hooks/_lib/subagent-transcript.mjs +562 -0
- package/hooks/config-protection.mjs +2 -2
- package/hooks/cwd-change-restore.mjs +2 -2
- package/hooks/enforce-commands.mjs +69 -0
- package/hooks/hooks-codex.json +1 -1
- package/hooks/hooks-cursor.json +10 -0
- package/hooks/hooks-pi.json +5 -0
- package/hooks/hooks.json +6 -1
- package/hooks/loop-guard.mjs +3 -3
- package/hooks/on-session-end.mjs +2 -2
- package/hooks/on-session-start.mjs +103 -2
- package/hooks/on-stop.mjs +36 -11
- package/hooks/operator-steer.mjs +2 -2
- package/hooks/post-bash-write-verify.mjs +85 -0
- package/hooks/post-edit-import-probe.mjs +344 -0
- package/hooks/post-subagent-discovery-validator.mjs +187 -431
- package/hooks/post-tool-batch-wave-signal.mjs +118 -4
- package/hooks/post-tool-failure-corrective-context.mjs +2 -2
- package/hooks/post-tooluse-frontend-slop.mjs +3 -3
- package/hooks/pre-bash-destructive-guard.mjs +39 -13
- package/hooks/skill-invocation-telemetry.mjs +17 -5
- package/hooks/subagent-telemetry.mjs +13 -4
- package/monitors/monitors.json +3 -3
- package/package.json +9 -1
- package/pi/prompts/session.md +2 -2
- package/plugin.json +27 -0
- package/scripts/backfill-abandoned-sessions.mjs +50 -4
- package/scripts/backfill-learnings-from-vault.mjs +9 -3
- package/scripts/dialectic-deriver.mjs +73 -8
- package/scripts/export-hw-learnings.mjs +113 -1
- package/scripts/generate-agents-skills.mjs +378 -0
- package/scripts/generate-cursor-adapter.mjs +45 -8
- package/scripts/generate-hook-import-set.mjs +249 -0
- package/scripts/lib/agent-status.mjs +13 -2
- package/scripts/lib/auto-dream.mjs +38 -36
- package/scripts/lib/autonomy/suitability.mjs +6 -0
- package/scripts/lib/autopilot/loop.mjs +2 -2
- package/scripts/lib/ci-status-banner.mjs +220 -75
- package/scripts/lib/codex/plugin-contract.mjs +82 -6
- package/scripts/lib/config/auto-dream.mjs +2 -1
- package/scripts/lib/config/block-header.mjs +8 -0
- package/scripts/lib/config/block-preprocess.mjs +177 -0
- package/scripts/lib/config/broken-window.mjs +2 -1
- package/scripts/lib/config/cold-start.mjs +2 -1
- package/scripts/lib/config/config-protection.mjs +22 -2
- package/scripts/lib/config/context-coverage.mjs +2 -1
- package/scripts/lib/config/cross-repo.mjs +2 -1
- package/scripts/lib/config/custom-phases.mjs +2 -1
- package/scripts/lib/config/dialectic.mjs +2 -1
- package/scripts/lib/config/discovery-validator.mjs +2 -1
- package/scripts/lib/config/dispatcher-autonomy-capture.mjs +24 -1
- package/scripts/lib/config/dispatcher-autonomy.mjs +2 -1
- package/scripts/lib/config/docs-orchestrator.mjs +2 -1
- package/scripts/lib/config/docs-staleness.mjs +2 -1
- package/scripts/lib/config/drift-check.mjs +2 -1
- package/scripts/lib/config/eval.mjs +2 -1
- package/scripts/lib/config/events-rotation.mjs +2 -1
- package/scripts/lib/config/evolve.mjs +8 -2
- package/scripts/lib/config/frontend-slop-hook.mjs +7 -3
- package/scripts/lib/config/gitlab-portfolio.mjs +2 -1
- package/scripts/lib/config/handover-gate.mjs +2 -1
- package/scripts/lib/config/health-endpoints.mjs +7 -2
- package/scripts/lib/config/issue-budget.mjs +2 -1
- package/scripts/lib/config/loop-guard.mjs +2 -1
- package/scripts/lib/config/memory.mjs +2 -1
- package/scripts/lib/config/moc-staleness.mjs +2 -1
- package/scripts/lib/config/persona-gate-wave.mjs +2 -1
- package/scripts/lib/config/private-config-dir.mjs +67 -0
- package/scripts/lib/config/reconcile.mjs +2 -1
- package/scripts/lib/config/remote-hosts.mjs +2 -1
- package/scripts/lib/config/section-extractor.mjs +7 -1
- package/scripts/lib/config/skill-evolution.mjs +2 -1
- package/scripts/lib/config/slopcheck.mjs +2 -1
- package/scripts/lib/config/state-md-lock.mjs +2 -1
- package/scripts/lib/config/templates-first.mjs +2 -1
- package/scripts/lib/config/test.mjs +2 -1
- package/scripts/lib/config/vault-integration.mjs +7 -1
- package/scripts/lib/config/vault-mirror-quality.mjs +2 -1
- package/scripts/lib/config/vault-staleness.mjs +2 -1
- package/scripts/lib/config/vault-sync.mjs +2 -1
- package/scripts/lib/config/verification-auto-fix.mjs +2 -1
- package/scripts/lib/config/wave-reviewers.mjs +2 -1
- package/scripts/lib/config/worktree-orphans.mjs +2 -1
- package/scripts/lib/convergence-monitor.mjs +82 -16
- package/scripts/lib/dispatcher/rank.mjs +124 -48
- package/scripts/lib/ecosystem-health.mjs +16 -2
- package/scripts/lib/eval/engine.mjs +9 -1
- package/scripts/lib/eval/session-resolve.mjs +23 -4
- package/scripts/lib/events.mjs +22 -6
- package/scripts/lib/frontmatter-guard.mjs +131 -13
- package/scripts/lib/gates/gate-full.mjs +26 -0
- package/scripts/lib/gates/gate-helpers.mjs +76 -0
- package/scripts/lib/hardware-pattern-detector.mjs +18 -1
- package/scripts/lib/harness-audit/categories/category4.mjs +31 -11
- package/scripts/lib/host-identity.mjs +50 -11
- package/scripts/lib/instruction-budget-guard.mjs +171 -5
- package/scripts/lib/learnings/evolve-telemetry.mjs +178 -0
- package/scripts/lib/learnings/io.mjs +60 -6
- package/scripts/lib/memory-proposals/store.mjs +30 -22
- package/scripts/lib/owner-config-banner.mjs +43 -6
- package/scripts/lib/owner-config-loader.mjs +21 -10
- package/scripts/lib/owner-interview.mjs +3 -3
- package/scripts/lib/owner-yaml.mjs +207 -14
- package/scripts/lib/platform.mjs +108 -15
- package/scripts/lib/plugin-update-banner.mjs +406 -0
- package/scripts/lib/project-hygiene.mjs +38 -2
- package/scripts/lib/qg-command-drift-banner.mjs +50 -12
- package/scripts/lib/quality-gate.mjs +133 -44
- package/scripts/lib/reconcile/emitter.mjs +68 -6
- package/scripts/lib/reconcile/engine.mjs +13 -4
- package/scripts/lib/reconcile/idempotency.mjs +37 -4
- package/scripts/lib/reconcile/writer.mjs +40 -18
- package/scripts/lib/session-close-backfill.mjs +67 -9
- package/scripts/lib/session-id.mjs +12 -23
- package/scripts/lib/session-identity/own-session.mjs +125 -10
- package/scripts/lib/session-lock-shape.mjs +43 -0
- package/scripts/lib/session-lock.mjs +5 -10
- package/scripts/lib/session-registry.mjs +25 -9
- package/scripts/lib/session-schema/constants.mjs +36 -2
- package/scripts/lib/session-schema/validator.mjs +38 -4
- package/scripts/lib/session-start-probes.mjs +18 -1
- package/scripts/lib/sessions-staleness-banner.mjs +18 -11
- package/scripts/lib/skill-health/join.mjs +17 -4
- package/scripts/lib/state-md.mjs +78 -0
- package/scripts/lib/sunset/walker.mjs +6 -0
- package/scripts/lib/telemetry/schema.mjs +181 -9
- package/scripts/lib/telemetry/sync.mjs +368 -12
- package/scripts/lib/validate/check-agents-skills.mjs +327 -0
- package/scripts/lib/validate/check-agents.mjs +3 -3
- package/scripts/lib/validate/check-cursor-adapter.mjs +234 -72
- package/scripts/lib/validate/check-hooks-symmetry.mjs +45 -16
- package/scripts/lib/validate/check-owner-leakage.mjs +281 -20
- package/scripts/lib/validate/check-skill-links.mjs +163 -0
- package/scripts/lib/validate/check-skill-script-paths.mjs +47 -28
- package/scripts/lib/validate/check-unwired-features.mjs +0 -2
- package/scripts/lib/validate/check-validator-registration.mjs +10 -4
- package/scripts/lib/validate/enumerate-repo-files.mjs +317 -0
- package/scripts/lib/vault-backfill/template.mjs +63 -6
- package/scripts/lib/vault-mirror/process.mjs +165 -42
- package/scripts/lib/vault-mirror/telemetry.mjs +2 -2
- package/scripts/lib/vault-status/narrative-mirror.mjs +127 -18
- package/scripts/lib/wave-executor/dispatch-common.mjs +164 -0
- package/scripts/lib/wave-executor/foreign-dispatch.mjs +7 -142
- package/scripts/lib/wave-executor/remote-dispatch.mjs +5 -7
- package/scripts/lib/wave-resource-gate.mjs +8 -2
- package/scripts/lib/wave-sizing.mjs +4 -1
- package/scripts/lib/wave-transcript-tail.mjs +118 -4
- package/scripts/materialize-wave-scope.mjs +12 -5
- package/scripts/memory-propose.mjs +19 -5
- package/scripts/migrate-cold-start-seed.mjs +4 -1
- package/scripts/parse-config.mjs +60 -3
- package/scripts/release.mjs +337 -29
- package/scripts/repair-invalid-sessions.mjs +3 -3
- package/scripts/run-quality-gate.mjs +128 -11
- package/scripts/sweep-expired-learnings.mjs +90 -0
- package/scripts/sync-vault-schema.mjs +3 -1
- package/scripts/telemetry.mjs +2 -2
- package/scripts/validate-plugin.mjs +161 -0
- package/scripts/validate-wave-scope.mjs +28 -8
- package/scripts/wave-scope-binding.mjs +215 -0
- package/skills/_shared/instruction-file-resolution.md +10 -0
- package/skills/_shared/parallel-aware-preamble.md +1 -0
- package/skills/_shared/platform-tools.md +1 -1
- package/skills/_shared/state-ownership.md +1 -1
- package/skills/architecture/SKILL.md +7 -5
- package/skills/{domain-model/SKILL.md → architecture/references/domain-model.md} +9 -9
- package/skills/autopilot/SKILL.md +4 -18
- package/skills/claude-md-drift-check/SKILL.md +5 -1
- package/skills/claude-md-drift-check/checker.mjs +62 -2
- package/skills/convergence-monitoring/SIGNALS.md +55 -0
- package/skills/discovery/probes/vault-staleness.mjs +37 -13
- package/skills/discovery/probes-arch.md +20 -18
- package/skills/dispatcher/SKILL.md +3 -2
- package/skills/evolve/SKILL.md +65 -26
- package/skills/frontmatter-guard/SKILL.md +11 -5
- package/skills/npm-publish/SKILL.md +1 -1
- package/skills/reconcile/SKILL.md +33 -0
- package/skills/remote-offload/SKILL.md +1 -1
- package/skills/session-end/SKILL.md +18 -905
- package/skills/session-end/phase-3-6-tail.md +10 -3
- package/skills/session-end/plan-verification.md +221 -155
- package/skills/session-end/references/phase-2-quality-gate.md +93 -0
- package/skills/session-end/references/phase-3-documentation-updates.md +229 -0
- package/skills/session-end/references/phase-4a-worktree-cleanup.md +120 -0
- package/skills/session-end/references/phase-4b-worktree-orphan-sweep.md +58 -0
- package/skills/session-end/references/phase-5-issue-cleanup.md +104 -0
- package/skills/session-end/references/session-summary-template.md +62 -0
- package/skills/session-plan/SKILL.md +49 -0
- package/skills/session-start/SKILL.md +22 -904
- package/skills/session-start/phase-8-5-express-path.md +1 -1
- package/skills/session-start/references/phase-1-1-dispatcher-autonomy-capture.md +55 -0
- package/skills/session-start/references/phase-1-2-session-lock.md +140 -0
- package/skills/session-start/references/phase-1-5-session-continuity.md +254 -0
- package/skills/session-start/references/phase-1-7-vault-status-board.md +53 -0
- package/skills/session-start/references/phase-2-7-portfolio-snapshot.md +75 -0
- package/skills/session-start/references/phase-4-ssot-environment-check.md +155 -0
- package/skills/session-start/references/phase-6-5-forced-reads.md +75 -0
- package/skills/session-start/references/phase-6-6-project-intelligence.md +81 -0
- package/skills/session-start/references/phase-6-7-memory-banner-telemetry-consent.md +103 -0
- package/skills/vault-sync/validator.mjs +21 -27
- package/skills/wave-executor/SKILL.md +15 -1
- package/skills/wave-executor/references/wave-loop-dispatch.md +612 -0
- package/skills/wave-executor/references/wave-loop-review.md +570 -0
- package/skills/wave-executor/references/wave-loop-scope-manifest.md +162 -0
- package/skills/wave-executor/wave-loop.md +14 -1309
- package/templates/_shared/journey-manifest.md +10 -6
- package/.cursor/commands/autopilot-multi.md +0 -14
- package/.cursor/commands/contract-version-bump.md +0 -14
- package/.cursor/commands/journey-audit.md +0 -14
- package/.cursor/skills/contract-version-bump/SKILL.md +0 -12
- package/.cursor/skills/daily/SKILL.md +0 -12
- package/.cursor/skills/domain-model/SKILL.md +0 -13
- package/.cursor/skills/journey-audit/SKILL.md +0 -13
- package/.cursor/skills/skill-creator/SKILL.md +0 -13
- package/.cursor/skills/ubiquitous-language/SKILL.md +0 -13
- package/commands/autopilot-multi.md +0 -74
- package/commands/contract-version-bump.md +0 -28
- package/commands/journey-audit.md +0 -43
- package/pi/prompts/autopilot-multi.md +0 -12
- package/pi/prompts/contract-version-bump.md +0 -12
- package/pi/prompts/journey-audit.md +0 -12
- package/scripts/autopilot-multi.mjs +0 -885
- package/scripts/backfill-learnings-expires.mjs +0 -196
- package/scripts/backfill-learnings.mjs +0 -203
- package/scripts/fleet-instruction-scan.mjs +0 -141
- package/scripts/lib/autopilot/dep-graph.mjs +0 -417
- package/scripts/lib/autopilot/multi-killswitch.mjs +0 -184
- package/scripts/lib/webhook-url.mjs +0 -105
- package/scripts/lifecycle-sim-v6.mjs +0 -347
- package/scripts/migrate-learnings-jsonl.mjs +0 -189
- package/scripts/migrate-subagents-jsonl.mjs +0 -196
- package/scripts/upload-social-preview.mjs +0 -316
- package/skills/_shared/model-selection.md +0 -64
- package/skills/contract-version-bump/SKILL.md +0 -219
- package/skills/daily/SKILL.md +0 -222
- package/skills/daily/generate.sh +0 -92
- package/skills/daily/templates/daily.md.tpl +0 -36
- package/skills/journey-audit/SKILL.md +0 -270
- package/skills/skill-creator/SKILL.md +0 -168
- package/skills/ubiquitous-language/SKILL.md +0 -97
- package/skills/vault-sync/package-lock.json +0 -40
- /package/skills/{domain-model → architecture/references}/ADR-FORMAT.md +0 -0
- /package/skills/{domain-model → architecture/references}/CONTEXT-FORMAT.md +0 -0
|
@@ -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));
|