session-orchestrator 5.2.0 → 5.3.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 +3 -1
- package/.agents/skills/autopilot/SKILL.md +5 -1
- package/.agents/skills/autopilot/agents/openai.yaml +5 -0
- package/.agents/skills/bootstrap/SKILL.md +5 -1
- package/.agents/skills/bootstrap/agents/openai.yaml +5 -0
- package/.agents/skills/brainstorm/SKILL.md +5 -1
- package/.agents/skills/brainstorm/agents/openai.yaml +5 -0
- package/.agents/skills/claude-md-drift-check/SKILL.md +3 -1
- package/.agents/skills/close/SKILL.md +5 -1
- package/.agents/skills/close/agents/openai.yaml +5 -0
- package/.agents/skills/convergence-monitoring/SKILL.md +4 -2
- package/.agents/skills/debug/SKILL.md +5 -1
- package/.agents/skills/debug/agents/openai.yaml +5 -0
- package/.agents/skills/discovery/SKILL.md +5 -1
- package/.agents/skills/discovery/agents/openai.yaml +5 -0
- package/.agents/skills/dispatcher/SKILL.md +5 -1
- package/.agents/skills/dispatcher/agents/openai.yaml +5 -0
- package/.agents/skills/docs-orchestrator/SKILL.md +3 -1
- package/.agents/skills/ecosystem-health/SKILL.md +3 -1
- package/.agents/skills/eli5/SKILL.md +5 -1
- package/.agents/skills/eli5/agents/openai.yaml +5 -0
- package/.agents/skills/eval/SKILL.md +6 -2
- package/.agents/skills/eval/agents/openai.yaml +5 -0
- package/.agents/skills/evolve/SKILL.md +6 -2
- package/.agents/skills/evolve/agents/openai.yaml +5 -0
- package/.agents/skills/frontmatter-guard/SKILL.md +3 -1
- package/.agents/skills/gitlab-ops/SKILL.md +3 -1
- package/.agents/skills/gitlab-portfolio/SKILL.md +3 -1
- package/.agents/skills/go/SKILL.md +5 -1
- package/.agents/skills/go/agents/openai.yaml +5 -0
- package/.agents/skills/grill/SKILL.md +5 -1
- package/.agents/skills/grill/agents/openai.yaml +5 -0
- package/.agents/skills/harness-audit/SKILL.md +5 -1
- package/.agents/skills/harness-audit/agents/openai.yaml +5 -0
- package/.agents/skills/hook-development/SKILL.md +3 -1
- package/.agents/skills/mcp-builder/SKILL.md +3 -1
- package/.agents/skills/memory-cleanup/SKILL.md +5 -1
- package/.agents/skills/memory-cleanup/agents/openai.yaml +5 -0
- package/.agents/skills/mode-selector/SKILL.md +3 -1
- package/.agents/skills/npm-publish/SKILL.md +4 -2
- package/.agents/skills/peekaboo-driver/SKILL.md +3 -1
- package/.agents/skills/persona-panel/SKILL.md +5 -1
- package/.agents/skills/persona-panel/agents/openai.yaml +5 -0
- package/.agents/skills/plan/SKILL.md +5 -1
- package/.agents/skills/plan/agents/openai.yaml +5 -0
- package/.agents/skills/playwright-driver/SKILL.md +3 -1
- package/.agents/skills/portfolio/SKILL.md +5 -1
- package/.agents/skills/portfolio/agents/openai.yaml +5 -0
- package/.agents/skills/quality-gates/SKILL.md +3 -1
- package/.agents/skills/reconcile/SKILL.md +5 -1
- package/.agents/skills/reconcile/agents/openai.yaml +5 -0
- package/.agents/skills/release/SKILL.md +5 -1
- package/.agents/skills/release/agents/openai.yaml +5 -0
- package/.agents/skills/remote-offload/SKILL.md +3 -1
- package/.agents/skills/repo-audit/SKILL.md +5 -1
- package/.agents/skills/repo-audit/agents/openai.yaml +5 -0
- package/.agents/skills/session/SKILL.md +21 -0
- package/.agents/skills/session/agents/openai.yaml +5 -0
- package/.agents/skills/session-end/SKILL.md +3 -1
- package/.agents/skills/session-plan/SKILL.md +3 -1
- package/.agents/skills/session-start/SKILL.md +3 -1
- package/.agents/skills/spinout/SKILL.md +5 -1
- package/.agents/skills/spinout/agents/openai.yaml +5 -0
- package/.agents/skills/sunset-review/SKILL.md +5 -1
- package/.agents/skills/sunset-review/agents/openai.yaml +5 -0
- package/.agents/skills/templates-ack/SKILL.md +21 -0
- package/.agents/skills/templates-ack/agents/openai.yaml +5 -0
- package/.agents/skills/test/SKILL.md +5 -1
- package/.agents/skills/test/agents/openai.yaml +5 -0
- package/.agents/skills/test-runner/SKILL.md +3 -1
- package/.agents/skills/tmux-layout/SKILL.md +3 -1
- package/.agents/skills/using-orchestrator/SKILL.md +3 -1
- package/.agents/skills/ux-grill/SKILL.md +5 -1
- package/.agents/skills/ux-grill/agents/openai.yaml +5 -0
- package/.agents/skills/vault-mirror/SKILL.md +3 -1
- package/.agents/skills/vault-sync/SKILL.md +3 -1
- package/.agents/skills/wave-executor/SKILL.md +3 -1
- package/.agents/skills/write-executable-plan/SKILL.md +3 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +4 -4
- package/.codex-plugin/skills/convergence-monitoring/SKILL.md +1 -3
- package/.codex-plugin/skills/eval/SKILL.md +1 -1
- package/.codex-plugin/skills/evolve/SKILL.md +1 -1
- package/.codex-plugin/skills/npm-publish/SKILL.md +1 -3
- package/.codex-plugin/skills/session/SKILL.md +1 -1
- package/.cursor/commands/eval.md +1 -1
- package/.cursor/commands/session.md +1 -1
- package/.cursor/rules/000-session-orchestrator.mdc +0 -2
- package/.cursor/rules/050-plan.mdc +1 -1
- package/.cursor/skills/convergence-monitoring/SKILL.md +1 -0
- package/.cursor/skills/eval/SKILL.md +1 -1
- package/.cursor/skills/npm-publish/SKILL.md +1 -0
- package/.cursor-plugin/plugin.json +1 -1
- package/.orchestrator/policy/blocked-commands.json +12 -3
- package/AGENTS.md +3 -2
- package/CHANGELOG.md +136 -0
- package/README.md +9 -9
- package/SECURITY.md +12 -0
- package/agents/dialectic-deriver.md +13 -10
- package/agents/eval-judge.md +67 -45
- package/agents/skill-applied-judge.md +34 -19
- package/commands/session.md +7 -3
- package/docs/baseline.md +12 -6
- package/docs/codex-setup.md +14 -2
- package/docs/components.md +7 -5
- package/docs/events-schema.md +56 -9
- package/docs/rule-authoring.md +58 -6
- package/docs/session-config-reference.md +100 -7
- package/docs/session-config-template.md +31 -2
- package/docs/telemetry.md +2 -0
- package/hooks/_lib/hook-import-set.json +85 -8
- package/hooks/_lib/subagent-transcript.mjs +582 -31
- package/hooks/config-protection.mjs +11 -3
- package/hooks/cwd-change-restore.mjs +11 -3
- package/hooks/enforce-commands.mjs +70 -23
- package/hooks/enforce-scope.mjs +143 -33
- package/hooks/hooks-codex.json +1 -1
- package/hooks/hooks.json +1 -1
- package/hooks/loop-guard.mjs +11 -3
- package/hooks/on-session-end.mjs +58 -23
- package/hooks/on-session-start.mjs +48 -11
- package/hooks/on-stop.mjs +168 -22
- package/hooks/operator-steer.mjs +11 -3
- package/hooks/post-bash-issue-budget-refund.mjs +18 -8
- package/hooks/post-bash-write-verify.mjs +3 -2
- package/hooks/post-edit-import-probe.mjs +17 -9
- package/hooks/post-edit-validate.mjs +13 -5
- package/hooks/post-subagent-discovery-validator.mjs +98 -13
- package/hooks/post-tool-batch-wave-signal.mjs +200 -38
- package/hooks/post-tool-failure-corrective-context.mjs +11 -5
- package/hooks/post-tooluse-frontend-slop.mjs +10 -4
- package/hooks/pre-auq-clarity.mjs +15 -2
- package/hooks/pre-bash-destructive-guard.mjs +80 -9
- package/hooks/pre-bash-issue-budget.mjs +16 -11
- package/hooks/pre-bash-memory-propose-audit.mjs +86 -54
- package/hooks/pre-bash-sessions-ledger-guard.mjs +391 -20
- package/hooks/pre-bash-staging-fence.mjs +335 -31
- package/hooks/pre-bash-templates-first.mjs +19 -14
- package/hooks/pre-task-scope-disjoint.mjs +233 -2
- package/hooks/subagent-telemetry.mjs +15 -19
- package/hooks/wave-scope-commit-guard.mjs +197 -100
- package/monitors/monitors.json +1 -1
- package/output-styles/wave-summary.md +1 -1
- package/package.json +1 -1
- package/pi/prompts/eval.md +1 -1
- package/pi/prompts/session.md +1 -1
- package/rules/README.md +1 -1
- package/rules/opt-in-domain/prompt-caching.md +1 -1
- package/rules/opt-in-stack/backend-data.md +1 -1
- package/rules/opt-in-stack/backend.md +3 -3
- package/rules/opt-in-stack/frontend.md +1 -1
- package/rules/opt-in-stack/security-web.md +3 -3
- package/rules/opt-in-stack/swift.md +1 -1
- package/scripts/autopilot.mjs +23 -2
- package/scripts/backfill-abandoned-sessions.mjs +117 -15
- package/scripts/check-sessions-integrity.mjs +300 -0
- package/scripts/dialectic-deriver.mjs +50 -13
- package/scripts/emit-session.mjs +75 -29
- package/scripts/eval-session.mjs +65 -3
- package/scripts/generate-agents-skills.mjs +102 -29
- package/scripts/generate-cursor-adapter.mjs +61 -16
- package/scripts/lib/agent-status.mjs +2 -31
- package/scripts/lib/auq/clarity.mjs +10 -2
- package/scripts/lib/auq/parse.mjs +12 -31
- package/scripts/lib/auq/schema.mjs +56 -41
- package/scripts/lib/auto-dialectic.mjs +304 -15
- package/scripts/lib/autopilot/flags.mjs +12 -1
- package/scripts/lib/autopilot/kill-switches.mjs +6 -3
- package/scripts/lib/autopilot/loop.mjs +14 -1
- package/scripts/lib/autopilot/stall-sampler.mjs +80 -23
- package/scripts/lib/ci-status-banner.mjs +376 -16
- package/scripts/lib/command-blocker.mjs +275 -28
- package/scripts/lib/config/dialectic.mjs +12 -3
- package/scripts/lib/config/gate.mjs +74 -0
- package/scripts/lib/config/reaper.mjs +162 -0
- package/scripts/lib/config.mjs +14 -0
- package/scripts/lib/convergence-monitor.mjs +74 -11
- package/scripts/lib/ecosystem-health.mjs +11 -0
- package/scripts/lib/eval/engine.mjs +421 -53
- package/scripts/lib/eval/judge.mjs +463 -40
- package/scripts/lib/eval/schema.mjs +10 -1
- package/scripts/lib/events-rotation.mjs +221 -25
- package/scripts/lib/events-schema.mjs +114 -0
- package/scripts/lib/events.mjs +524 -5
- package/scripts/lib/frontmatter-guard.mjs +21 -10
- package/scripts/lib/gates/gate-baseline.mjs +27 -2
- package/scripts/lib/gates/gate-full.mjs +28 -3
- package/scripts/lib/gates/gate-helpers.mjs +243 -21
- package/scripts/lib/gates/gate-incremental.mjs +28 -3
- package/scripts/lib/gates/gate-per-file.mjs +27 -2
- package/scripts/lib/gitlab-portfolio/markdown-writer.mjs +6 -1
- package/scripts/lib/instruction-budget-guard.mjs +146 -4
- package/scripts/lib/io.mjs +42 -8
- package/scripts/lib/issue-close-strip-labels.mjs +207 -49
- package/scripts/lib/js-mask.mjs +197 -0
- package/scripts/lib/learnings/evolve-telemetry.mjs +11 -7
- package/scripts/lib/maintenance-due-banner.mjs +53 -88
- package/scripts/lib/orphan-reaper.mjs +1588 -0
- package/scripts/lib/peer-cards/merger.mjs +48 -10
- package/scripts/lib/peer-cards/reader.mjs +78 -2
- package/scripts/lib/process-group.mjs +899 -0
- package/scripts/lib/quality-gate.mjs +107 -28
- package/scripts/lib/reconcile/backlog.mjs +368 -0
- package/scripts/lib/reconcile/engine.mjs +55 -188
- package/scripts/lib/reconcile/rule-expiry-sweep.mjs +302 -60
- package/scripts/lib/reconcile/sanitize.mjs +69 -3
- package/scripts/lib/reconcile-nudge-banner.mjs +138 -45
- package/scripts/lib/resource-probe/parsers.mjs +31 -0
- package/scripts/lib/rule-loader.mjs +41 -12
- package/scripts/lib/scope-echo.mjs +39 -2
- package/scripts/lib/scope-gate.mjs +605 -1
- package/scripts/lib/session-close-backfill.mjs +33 -6
- package/scripts/lib/session-id.mjs +9 -20
- package/scripts/lib/session-invocation.mjs +20 -0
- package/scripts/lib/session-schema/constants.mjs +30 -2
- package/scripts/lib/session-schema/normalizer.mjs +56 -4
- package/scripts/lib/session-schema.mjs +8 -3
- package/scripts/lib/session-start-probes.mjs +95 -10
- package/scripts/lib/sessions-canonical.mjs +23 -0
- package/scripts/lib/sessions-integrity-banner.mjs +7 -1
- package/scripts/lib/sessions-staleness-banner.mjs +193 -51
- package/scripts/lib/skill-evidence-window.mjs +891 -0
- package/scripts/lib/skill-evolution/candidate-intake.mjs +133 -12
- package/scripts/lib/skill-evolution/engine.mjs +18 -9
- package/scripts/lib/skill-judge.mjs +45 -3
- package/scripts/lib/tail-window.mjs +56 -0
- package/scripts/lib/telemetry/schema.mjs +30 -0
- package/scripts/lib/telemetry/sync.mjs +61 -6
- package/scripts/lib/telemetry-flush-health-banner.mjs +4 -22
- package/scripts/lib/test-runner/issue-reconcile.mjs +48 -16
- package/scripts/lib/tmux-layout/telemetry-stats.mjs +72 -13
- package/scripts/lib/user-invocable-skills.mjs +23 -3
- package/scripts/lib/ux-grill/reconcile.mjs +48 -22
- package/scripts/lib/validate/check-agents-skills.mjs +26 -15
- package/scripts/lib/validate/check-cursor-adapter.mjs +1 -0
- package/scripts/lib/validate/check-entry-guard.mjs +13 -50
- package/scripts/lib/validate/check-hook-entry-guards.mjs +636 -0
- package/scripts/lib/validate/check-pi-prompts.mjs +1 -0
- package/scripts/lib/validate/check-rules.mjs +7 -5
- package/scripts/lib/validate/check-skill-links.mjs +9 -1
- package/scripts/lib/validate/check-skill-script-paths.mjs +239 -27
- package/scripts/lib/validate/check-test-git-config-target.mjs +24 -34
- package/scripts/lib/validate/check-untracked-test-deps.mjs +7 -102
- package/scripts/lib/validate/check-unwired-features.mjs +130 -27
- package/scripts/lib/validate/check-validator-registration.mjs +34 -10
- package/scripts/lib/validate/confidential-names.mjs +10 -0
- package/scripts/lib/validate-vendored-rules.mjs +4 -3
- package/scripts/lib/vault-mirror/namespace.mjs +46 -8
- package/scripts/lib/vault-mirror/process.mjs +10 -3
- package/scripts/lib/vault-mirror/render-sessions.mjs +12 -2
- package/scripts/lib/vault-status/narrative-mirror.mjs +31 -7
- package/scripts/lib/vault-yaml.mjs +118 -0
- package/scripts/lib/worktree/lifecycle.mjs +153 -1
- package/scripts/release-session-lock.mjs +305 -0
- package/scripts/release.mjs +30 -5
- package/scripts/resolve-session-invocation.mjs +59 -0
- package/scripts/run-quality-gate.mjs +156 -17
- package/scripts/sweep-expired-rules.mjs +14 -3
- package/scripts/validate-plugin.mjs +12 -0
- package/scripts/validate-wave-scope.mjs +32 -105
- package/scripts/vault-mirror.mjs +9 -1
- package/skills/_shared/platform-tools.md +23 -11
- package/skills/autopilot/SKILL.md +22 -7
- package/skills/claude-md-drift-check/SKILL.md +1 -1
- package/skills/convergence-monitoring/README.md +8 -1
- package/skills/convergence-monitoring/SIGNALS.md +50 -6
- package/skills/convergence-monitoring/SKILL.md +15 -6
- package/skills/eval/SKILL.md +39 -24
- package/skills/eval/rubric-v1.md +1 -0
- package/skills/eval/rubric-v2.md +457 -0
- package/skills/evolve/SKILL.md +1 -1
- package/skills/evolve/references/evolve-dialectic-mode.md +42 -25
- package/skills/gitlab-ops/SKILL.md +3 -2
- package/skills/npm-publish/SKILL.md +1 -1
- package/skills/reconcile/SKILL.md +11 -0
- package/skills/session-end/SKILL.md +13 -16
- package/skills/session-end/discovery-scan.md +1 -1
- package/skills/session-end/phase-3-6-tail.md +55 -9
- package/skills/session-end/references/phase-5-issue-cleanup.md +9 -14
- package/skills/session-end/session-metrics-write.md +10 -0
- package/skills/session-plan/SKILL.md +17 -5
- package/skills/session-plan/references/session-plan-task-classification.md +2 -2
- package/skills/session-start/references/phase-4-ssot-environment-check.md +2 -1
- package/skills/ux-grill/SKILL.md +1 -1
- package/skills/wave-executor/SKILL.md +8 -4
- package/skills/wave-executor/circuit-breaker.md +2 -0
- package/skills/wave-executor/references/wave-executor-state-init.md +5 -3
- package/skills/wave-executor/references/wave-loop-dispatch.md +2 -1
- package/.codex-plugin/skills/convergence-monitoring/agents/openai.yaml +0 -5
- package/.codex-plugin/skills/npm-publish/agents/openai.yaml +0 -5
- package/.cursor/commands/convergence-monitoring.md +0 -13
- package/.cursor/commands/npm-publish.md +0 -13
- package/pi/prompts/convergence-monitoring.md +0 -11
- package/pi/prompts/npm-publish.md +0 -11
|
@@ -36,8 +36,6 @@ import { randomUUID } from 'node:crypto';
|
|
|
36
36
|
import path from 'node:path';
|
|
37
37
|
|
|
38
38
|
import { shouldRunHook } from './_lib/profile-gate.mjs';
|
|
39
|
-
// #211: exit 0 immediately (silent allow) when this hook is disabled via profile/env
|
|
40
|
-
if (!shouldRunHook('on-session-start')) process.exit(0);
|
|
41
39
|
|
|
42
40
|
import { emitEvent, eventsFilePath } from '../scripts/lib/events.mjs';
|
|
43
41
|
import { maybeRotate } from '../scripts/lib/events-rotation.mjs';
|
|
@@ -54,6 +52,7 @@ import { detectColdStart, consumeMarker } from '../scripts/lib/cold-start-detect
|
|
|
54
52
|
import { parseSessionId } from '../scripts/lib/session-id.mjs';
|
|
55
53
|
import { readTelemetryState, resolveConsent, isCiEnv } from '../scripts/lib/telemetry/consent.mjs';
|
|
56
54
|
import { loadOwnerConfig } from '../scripts/lib/owner-yaml.mjs';
|
|
55
|
+
import { isMainModule } from '../scripts/lib/is-main-module.mjs';
|
|
57
56
|
// SSOT for the "is this a re-entry into the same logical session?" question
|
|
58
57
|
// (#1091). Defined in the lock-bootstrap leaf module, which this hook already
|
|
59
58
|
// loads, so the preservation branch below and the lock force-refresh gate can
|
|
@@ -453,8 +452,10 @@ async function resolveSessionId(input, projectRoot) {
|
|
|
453
452
|
// observe. A naive full overwrite of current-session.json drops the
|
|
454
453
|
// `last_wave` / `last_batch` markers written mid-session by
|
|
455
454
|
// post-tool-batch-wave-signal.mjs, which makes the next PostToolBatch
|
|
456
|
-
// re-read last_wave as absent→0 and
|
|
457
|
-
//
|
|
455
|
+
// re-read last_wave as absent→0 and RE-OPEN wave N mid-wave, re-stamping
|
|
456
|
+
// wave_start_sha and so moving the #980 files_changed start point. (Until
|
|
457
|
+
// 2026-09-19 it also re-emitted a duplicate wave.started{N}; that event
|
|
458
|
+
// has since been removed.)
|
|
458
459
|
//
|
|
459
460
|
// Three cases, exactly one of which preserves nothing:
|
|
460
461
|
// 'raw-id' — same-logical-session source AND the recorded raw
|
|
@@ -764,8 +765,12 @@ async function main() {
|
|
|
764
765
|
// v3.1.0 env-aware banner (opt-out via enable-host-banner: false in Session Config).
|
|
765
766
|
// The ask-via-tool nudge rides the same opt-out flag — both are coordinator
|
|
766
767
|
// reminders shown at session start; users who silence one expect silence.
|
|
768
|
+
// Hoisted because the cold-start DISPLAY below rides the same opt-out (#1133)
|
|
769
|
+
// and `bannerData` is not a usable proxy for it: `emitHostBanner()` returns
|
|
770
|
+
// null on its own error paths, which would silently re-open the nudge.
|
|
771
|
+
const hostBannerEnabled = await isHostBannerEnabled(projectRoot);
|
|
767
772
|
let bannerData = null;
|
|
768
|
-
if (
|
|
773
|
+
if (hostBannerEnabled) {
|
|
769
774
|
bannerData = await emitHostBanner(projectRoot);
|
|
770
775
|
// Always-on nudge: a user decision has three legitimate forms and AUQ-001
|
|
771
776
|
// routes between them in order — operator verb first (nothing is blocked
|
|
@@ -795,7 +800,14 @@ async function main() {
|
|
|
795
800
|
enabled: coldStartCfg.enabled !== false,
|
|
796
801
|
});
|
|
797
802
|
if (decision.shouldEmit) {
|
|
798
|
-
|
|
803
|
+
// #1133 — the DISPLAY rides the enable-host-banner opt-out like every
|
|
804
|
+
// other start-of-session emitter ("users who silence one expect
|
|
805
|
+
// silence"). The one-shot marker consumption below does NOT: the
|
|
806
|
+
// opt-out silences the display, never the measurement or the state
|
|
807
|
+
// transition. Consuming it unconditionally is what keeps the nudge
|
|
808
|
+
// one-shot for an operator who has banners off — otherwise it re-arms
|
|
809
|
+
// on every start forever.
|
|
810
|
+
if (hostBannerEnabled) pushBanner(decision.bannerLines.join('\n'));
|
|
799
811
|
if (decision.markerPath) {
|
|
800
812
|
await consumeMarker(decision.markerPath).catch(() => false);
|
|
801
813
|
}
|
|
@@ -1219,6 +1231,14 @@ async function main() {
|
|
|
1219
1231
|
// Size-based rotation of events.jsonl (#251). Session-start is the single
|
|
1220
1232
|
// rotation trigger — per-append overhead is rejected design. Any failure
|
|
1221
1233
|
// is swallowed: rotation must NEVER block the hook.
|
|
1234
|
+
//
|
|
1235
|
+
// #1401 — the DURABLE record of a rotation is no longer this block. Until
|
|
1236
|
+
// 2026-09-19 the only evidence a rotation had happened was the `console.error`
|
|
1237
|
+
// below, and this hook's stderr is discarded by the harness: a rotation and a
|
|
1238
|
+
// DELETED archive were byte-identical from outside. `maybeRotate()` now writes
|
|
1239
|
+
// an `orchestrator.events.rotated` record as the first line of the new active
|
|
1240
|
+
// file, so the ledger carries its own break whatever happens to this stderr
|
|
1241
|
+
// line. The line stays as an operator convenience only — never as the record.
|
|
1222
1242
|
try {
|
|
1223
1243
|
let rotCfg = { enabled: true, 'max-size-mb': 10, 'max-backups': 5 };
|
|
1224
1244
|
try {
|
|
@@ -1234,7 +1254,13 @@ async function main() {
|
|
|
1234
1254
|
enabled: rotCfg.enabled !== false,
|
|
1235
1255
|
});
|
|
1236
1256
|
if (result.rotated) {
|
|
1237
|
-
|
|
1257
|
+
const pruned = result.pruned?.length ? `, pruned ${result.pruned.length}` : '';
|
|
1258
|
+
const ledger = result.recordWritten ? '' : ' [WARN: rotation record NOT written]';
|
|
1259
|
+
console.error(
|
|
1260
|
+
`events-rotation: archived ${result.archivedAs} ` +
|
|
1261
|
+
`(${result.sizeBefore} bytes, ${result.lines ?? '?'} lines, ` +
|
|
1262
|
+
`${result.firstTs ?? 'unknown'} → ${result.lastTs ?? 'unknown'}${pruned})${ledger}`,
|
|
1263
|
+
);
|
|
1238
1264
|
}
|
|
1239
1265
|
} catch (err) {
|
|
1240
1266
|
console.error(`events-rotation: skipped (${err?.message ?? err})`);
|
|
@@ -1245,7 +1271,18 @@ async function main() {
|
|
|
1245
1271
|
// flushBanner() runs here too so a throw partway through main() still surfaces
|
|
1246
1272
|
// whatever was already collected; it is idempotent, so the normal path (which
|
|
1247
1273
|
// flushes at the end of main) does not double-emit.
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1274
|
+
//
|
|
1275
|
+
// Entry guard (#1298 P7): every harness execs this file as the node script
|
|
1276
|
+
// (`sh run-node.sh <this file>` → `exec node "$@"`), so argv[1] IS this module
|
|
1277
|
+
// on every real path. A bare `import()` — a probe, a test, a curious agent —
|
|
1278
|
+
// must not run main() against the live repo: incident W4-FX1 overwrote
|
|
1279
|
+
// `.orchestrator/current-session.json` exactly that way.
|
|
1280
|
+
if (isMainModule(import.meta.url)) {
|
|
1281
|
+
// #1393: the profile gate sits INSIDE the entry guard — at module top level
|
|
1282
|
+
// its `process.exit(0)` exited every process that merely imported this hook.
|
|
1283
|
+
if (!shouldRunHook('on-session-start')) process.exit(0);
|
|
1284
|
+
main().catch(() => {}).finally(() => {
|
|
1285
|
+
flushBanner();
|
|
1286
|
+
process.exit(0);
|
|
1287
|
+
});
|
|
1288
|
+
}
|
package/hooks/on-stop.mjs
CHANGED
|
@@ -30,28 +30,28 @@
|
|
|
30
30
|
*/
|
|
31
31
|
|
|
32
32
|
import path from 'node:path';
|
|
33
|
+
import { spawn } from 'node:child_process';
|
|
34
|
+
import { performance } from 'node:perf_hooks';
|
|
33
35
|
import {
|
|
34
36
|
promises as fs,
|
|
35
|
-
closeSync,
|
|
36
37
|
existsSync,
|
|
37
|
-
fstatSync,
|
|
38
|
-
openSync,
|
|
39
38
|
readFileSync,
|
|
40
|
-
readSync,
|
|
41
39
|
statSync,
|
|
42
40
|
writeFileSync,
|
|
43
41
|
} from 'node:fs';
|
|
42
|
+
import { fileURLToPath } from 'node:url';
|
|
44
43
|
|
|
45
44
|
import { shouldRunHook } from './_lib/profile-gate.mjs';
|
|
45
|
+
import { readTailWindow } from '../scripts/lib/tail-window.mjs';
|
|
46
46
|
import { AGENT_ID_RE, resolveSubagentSidecar } from './_lib/subagent-paths.mjs';
|
|
47
|
-
// #211: exit 0 immediately (silent allow) when this hook is disabled via profile/env
|
|
48
|
-
if (!shouldRunHook('on-stop')) process.exit(0);
|
|
49
47
|
|
|
50
48
|
import { emitEvent } from '../scripts/lib/events.mjs';
|
|
51
49
|
import { detectPlatform, getProjectDir } from '../scripts/lib/platform.mjs';
|
|
52
50
|
import { parseSessionId } from '../scripts/lib/session-id.mjs';
|
|
51
|
+
import { isMainModule } from '../scripts/lib/is-main-module.mjs';
|
|
53
52
|
import { heartbeat, logSweepEvent } from '../scripts/lib/session-registry.mjs';
|
|
54
53
|
import { readLock, updateHeartbeat } from '../scripts/lib/session-lock.mjs';
|
|
54
|
+
import { _parseReaper } from '../scripts/lib/config/reaper.mjs';
|
|
55
55
|
|
|
56
56
|
// ---------------------------------------------------------------------------
|
|
57
57
|
// stdin reading (inline — no io.mjs because Stop hooks exit 0 always, never deny)
|
|
@@ -743,17 +743,11 @@ function firstNonEmptyString(input, keys) {
|
|
|
743
743
|
* @returns {string|null}
|
|
744
744
|
*/
|
|
745
745
|
function readStatusFromTranscriptTail(transcriptPath) {
|
|
746
|
-
let fd = null;
|
|
747
746
|
try {
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
const length = Math.min(size, STATUS_TAIL_BYTES);
|
|
751
|
-
const start = size - length;
|
|
752
|
-
const buf = Buffer.alloc(length);
|
|
753
|
-
const read = readSync(fd, buf, 0, length, start);
|
|
754
|
-
let lines = buf.subarray(0, read).toString('utf8').split('\n');
|
|
747
|
+
const window = readTailWindow(transcriptPath, STATUS_TAIL_BYTES);
|
|
748
|
+
let lines = window.text.split('\n');
|
|
755
749
|
// Drop the leading partial line when the window did not start at byte 0.
|
|
756
|
-
if (
|
|
750
|
+
if (window.cut) lines = lines.slice(1);
|
|
757
751
|
|
|
758
752
|
for (let i = lines.length - 1; i >= 0; i -= 1) {
|
|
759
753
|
const line = lines[i].trim();
|
|
@@ -774,10 +768,6 @@ function readStatusFromTranscriptTail(transcriptPath) {
|
|
|
774
768
|
return null;
|
|
775
769
|
} catch {
|
|
776
770
|
return null;
|
|
777
|
-
} finally {
|
|
778
|
-
if (fd !== null) {
|
|
779
|
-
try { closeSync(fd); } catch { /* best-effort */ }
|
|
780
|
-
}
|
|
781
771
|
}
|
|
782
772
|
}
|
|
783
773
|
|
|
@@ -785,10 +775,158 @@ function readStatusFromTranscriptTail(transcriptPath) {
|
|
|
785
775
|
// main
|
|
786
776
|
// ---------------------------------------------------------------------------
|
|
787
777
|
|
|
778
|
+
// ---------------------------------------------------------------------------
|
|
779
|
+
// orphan-reaper trigger (#1432 B4)
|
|
780
|
+
// ---------------------------------------------------------------------------
|
|
781
|
+
//
|
|
782
|
+
// DUPLICATED VERBATIM between the two trigger hooks —
|
|
783
|
+
// hooks/post-tool-batch-wave-signal.mjs <-> hooks/on-stop.mjs. Deliberate, with
|
|
784
|
+
// a named revisit trigger (BV-004): they are the only two trigger points the
|
|
785
|
+
// PRD declares, and a shared `hooks/_lib/reaper-trigger.mjs` would be a third
|
|
786
|
+
// file in the hook import graph. SIZE, measured 2026-09-22 by diffing the two
|
|
787
|
+
// blocks: 138 identical lines — this note said "~40 lines of glue" until then,
|
|
788
|
+
// which is what made the duplicate look cheaper than it is. Extract it the
|
|
789
|
+
// moment a THIRD hook needs the trigger, or the moment the copies must differ.
|
|
790
|
+
|
|
791
|
+
/**
|
|
792
|
+
* Filesystem path of the scan CLI, spawned as a PLAIN argv call.
|
|
793
|
+
*
|
|
794
|
+
* It used to be a `file://` URL handed to `node --input-type=module -e
|
|
795
|
+
* <program>`, because `scripts/lib/orphan-reaper.mjs` had no entry guard. It
|
|
796
|
+
* has one now (`parseReaperCliArgs` + the `isMainModule` tail), so the child's
|
|
797
|
+
* contract lives in that module instead of as source text duplicated here —
|
|
798
|
+
* and nothing this hook builds is a program any more: every variable part is
|
|
799
|
+
* an argv value, which cannot become code whatever the checkout path contains.
|
|
800
|
+
*
|
|
801
|
+
* `fileURLToPath`, not `new URL(...).pathname`: the latter leaves a
|
|
802
|
+
* percent-encoded path for any checkout directory containing a space.
|
|
803
|
+
*/
|
|
804
|
+
const ORPHAN_REAPER_SCRIPT = fileURLToPath(
|
|
805
|
+
new URL('../scripts/lib/orphan-reaper.mjs', import.meta.url),
|
|
806
|
+
);
|
|
807
|
+
|
|
808
|
+
/**
|
|
809
|
+
* Read the `reaper:` block from the project's Session Config host file.
|
|
810
|
+
* Sync + inline, mirroring `hooks/loop-guard.mjs` `loadConfig()` — a hot hook
|
|
811
|
+
* path must not import the full config orchestrator. A missing or unreadable
|
|
812
|
+
* file yields the parser defaults, i.e. DISABLED.
|
|
813
|
+
*
|
|
814
|
+
* @param {string} projectDir
|
|
815
|
+
* @returns {ReturnType<typeof _parseReaper>}
|
|
816
|
+
*/
|
|
817
|
+
function loadReaperConfig(projectDir) {
|
|
818
|
+
for (const name of ['CLAUDE.md', 'AGENTS.md']) {
|
|
819
|
+
try {
|
|
820
|
+
return _parseReaper(readFileSync(path.join(projectDir, name), 'utf8'));
|
|
821
|
+
} catch {
|
|
822
|
+
// missing or unreadable — try the next candidate
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
return _parseReaper('');
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
/**
|
|
829
|
+
* Trigger the orphan scan, throttled and NON-BLOCKING (PRD FA4).
|
|
830
|
+
*
|
|
831
|
+
* The hook itself does exactly two pieces of I/O — one config read and one
|
|
832
|
+
* `stat` of the throttle marker — and then hands the work to a DETACHED,
|
|
833
|
+
* unref'd child. The scan is never run inline: one `ps` round-trip out of Node
|
|
834
|
+
* was measured at ~47 ms over 287 KB of output, which alone would blow the
|
|
835
|
+
* 50 ms `reaper.max-hook-latency-ms` budget this hook has to stay inside.
|
|
836
|
+
*
|
|
837
|
+
* The marker is stamped BEFORE the spawn, so a spawn that fails still consumes
|
|
838
|
+
* the throttle window — otherwise a broken spawn would be retried on every
|
|
839
|
+
* single tool batch.
|
|
840
|
+
*
|
|
841
|
+
* `reaper.max-hook-latency-ms` is the ceiling on the work above — measured here
|
|
842
|
+
* with `performance.now()` and reported as ONE stderr WARN line when exceeded.
|
|
843
|
+
* A WARN and not an event: this fires from a `PostToolBatch`-class hook, and a
|
|
844
|
+
* per-fire telemetry record is exactly the always-on signal
|
|
845
|
+
* `.claude/rules/host-resources.md` HR-101 calls a broken instrument. The
|
|
846
|
+
* measurement covers preparation only — the scan itself runs detached, which is
|
|
847
|
+
* the whole reason the budget can be held.
|
|
848
|
+
*
|
|
849
|
+
* Never throws: any failure degrades silently (PRD FA4 "lautlos degradieren").
|
|
850
|
+
*
|
|
851
|
+
* @param {object} [opts]
|
|
852
|
+
* @param {string} [opts.projectDir] Repo root; defaults to `getProjectDir()`.
|
|
853
|
+
* @param {number} [opts.now] Injected clock (ms).
|
|
854
|
+
* @param {Function} [opts.spawnFn] Injected `spawn` (tests).
|
|
855
|
+
* @param {Function} [opts.statFn] Injected `statSync` (tests).
|
|
856
|
+
* @param {Function} [opts.writeFn] Injected marker writer (tests).
|
|
857
|
+
* @param {() => number} [opts.clockFn] Injected monotonic clock for the latency
|
|
858
|
+
* budget (tests); defaults to `performance.now`.
|
|
859
|
+
* @returns {Promise<{spawned: boolean, reason: string}>}
|
|
860
|
+
*/
|
|
861
|
+
export async function maybeTriggerOrphanScan({
|
|
862
|
+
projectDir,
|
|
863
|
+
now,
|
|
864
|
+
spawnFn = spawn,
|
|
865
|
+
statFn,
|
|
866
|
+
writeFn,
|
|
867
|
+
clockFn = () => performance.now(),
|
|
868
|
+
} = {}) {
|
|
869
|
+
const startedAt = clockFn();
|
|
870
|
+
/** One WARN line when the preparation overran `reaper.max-hook-latency-ms`. */
|
|
871
|
+
const checkLatency = (budgetMs) => {
|
|
872
|
+
const elapsed = clockFn() - startedAt;
|
|
873
|
+
if (Number.isFinite(budgetMs) && budgetMs > 0 && elapsed > budgetMs) {
|
|
874
|
+
process.stderr.write(
|
|
875
|
+
`orphan-reaper trigger: hook latency ${elapsed.toFixed(1)} ms exceeded `
|
|
876
|
+
+ `reaper.max-hook-latency-ms (${budgetMs} ms)\n`,
|
|
877
|
+
);
|
|
878
|
+
}
|
|
879
|
+
};
|
|
880
|
+
try {
|
|
881
|
+
const root = typeof projectDir === 'string' && projectDir ? projectDir : getProjectDir();
|
|
882
|
+
const cfg = loadReaperConfig(root);
|
|
883
|
+
// Cheapest gate first: disabled means no stat, no spawn, no module load.
|
|
884
|
+
if (cfg.enabled !== true) return { spawned: false, reason: 'disabled' };
|
|
885
|
+
|
|
886
|
+
const reaper = await import('../scripts/lib/orphan-reaper.mjs');
|
|
887
|
+
const markerPath = reaper.scanMarkerPath(root);
|
|
888
|
+
const nowMs = typeof now === 'number' ? now : Date.now();
|
|
889
|
+
|
|
890
|
+
if (!reaper.shouldScanNow(markerPath, nowMs, cfg['min-scan-interval-seconds'], { statFn })) {
|
|
891
|
+
checkLatency(cfg['max-hook-latency-ms']);
|
|
892
|
+
return { spawned: false, reason: 'throttled' };
|
|
893
|
+
}
|
|
894
|
+
reaper.touchScanMarker(markerPath, { writeFn });
|
|
895
|
+
|
|
896
|
+
const child = spawnFn(
|
|
897
|
+
process.execPath,
|
|
898
|
+
[
|
|
899
|
+
ORPHAN_REAPER_SCRIPT,
|
|
900
|
+
'--repo-root', root,
|
|
901
|
+
'--mode', cfg.mode,
|
|
902
|
+
'--min-age-seconds', String(cfg['min-age-seconds']),
|
|
903
|
+
'--kill-grace-ms', String(cfg['kill-grace-ms']),
|
|
904
|
+
'--verify-wait-ms', String(cfg['verify-wait-ms']),
|
|
905
|
+
'--false-alarm-window', String(cfg['false-alarm-window']),
|
|
906
|
+
],
|
|
907
|
+
{ detached: true, stdio: 'ignore' },
|
|
908
|
+
);
|
|
909
|
+
if (child && typeof child.unref === 'function') child.unref();
|
|
910
|
+
checkLatency(cfg['max-hook-latency-ms']);
|
|
911
|
+
return { spawned: true, reason: 'spawned' };
|
|
912
|
+
} catch {
|
|
913
|
+
return { spawned: false, reason: 'error' };
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
|
|
788
917
|
async function main() {
|
|
789
918
|
const input = await readStdinJson();
|
|
790
919
|
const eventType = discriminate(input);
|
|
791
920
|
|
|
921
|
+
// Orphan-reaper trigger (#1432 B4) — SubagentStop is the PRD's second trigger
|
|
922
|
+
// point: it lands exactly when gate children have just finished and their PIDs
|
|
923
|
+
// are still known. Placed BEFORE the branch dispatch (unlike the PostToolBatch
|
|
924
|
+
// hook, which triggers last) because both branches below may throw — a
|
|
925
|
+
// `handleStop` failure must not silently disarm the watchdog. The cost is one
|
|
926
|
+
// config read plus one stat, bounded by `reaper.max-hook-latency-ms` (50 ms),
|
|
927
|
+
// and the scan itself runs in a detached child.
|
|
928
|
+
await maybeTriggerOrphanScan();
|
|
929
|
+
|
|
792
930
|
if (eventType === 'subagent_stop') {
|
|
793
931
|
const additionalContext = await handleSubagentStop(input);
|
|
794
932
|
// v2.1.163+: emit hookSpecificOutput for SubagentStop path.
|
|
@@ -860,6 +998,14 @@ function buildTerminalSequenceJson(platform) {
|
|
|
860
998
|
}
|
|
861
999
|
|
|
862
1000
|
// Exit 0 always — informational hook must never block Claude.
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
1001
|
+
// Entry guard (#1298 P7): run only as the node script every harness execs
|
|
1002
|
+
// (`sh run-node.sh <this file>`); a bare `import()` must not run the Stop
|
|
1003
|
+
// handler against the live repo.
|
|
1004
|
+
if (isMainModule(import.meta.url)) {
|
|
1005
|
+
// #1393: the profile gate sits INSIDE the entry guard — at module top level
|
|
1006
|
+
// its `process.exit(0)` exited every process that merely imported this hook.
|
|
1007
|
+
if (!shouldRunHook('on-stop')) process.exit(0);
|
|
1008
|
+
main()
|
|
1009
|
+
.catch(() => {})
|
|
1010
|
+
.finally(() => process.exit(0));
|
|
1011
|
+
}
|
package/hooks/operator-steer.mjs
CHANGED
|
@@ -34,8 +34,7 @@ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
|
34
34
|
import path from 'node:path';
|
|
35
35
|
|
|
36
36
|
import { shouldRunHook } from './_lib/profile-gate.mjs';
|
|
37
|
-
|
|
38
|
-
if (!shouldRunHook('operator-steer')) process.exit(0);
|
|
37
|
+
import { isMainModule } from '../scripts/lib/is-main-module.mjs';
|
|
39
38
|
|
|
40
39
|
import { getProjectDir } from '../scripts/lib/platform.mjs';
|
|
41
40
|
|
|
@@ -61,4 +60,13 @@ async function main() {
|
|
|
61
60
|
}
|
|
62
61
|
|
|
63
62
|
// Exit 0 always — informational hook must never block Claude.
|
|
64
|
-
|
|
63
|
+
// Entry guard (#1393): run only when this file IS the script node was invoked
|
|
64
|
+
// with — every harness path execs it (`sh run-node.sh <this file>`). A bare
|
|
65
|
+
// `import()` (a probe, a test, a curious agent) must neither run main() nor
|
|
66
|
+
// tear the importing process down. The profile gate sits INSIDE the guard for
|
|
67
|
+
// that second reason: at module top level its `process.exit(0)` exited every
|
|
68
|
+
// process that merely imported this hook.
|
|
69
|
+
if (isMainModule(import.meta.url)) {
|
|
70
|
+
if (!shouldRunHook('operator-steer')) process.exit(0);
|
|
71
|
+
main().catch(() => {}).finally(() => process.exit(0));
|
|
72
|
+
}
|
|
@@ -68,12 +68,13 @@ import {
|
|
|
68
68
|
} from '../scripts/lib/issue-budget.mjs';
|
|
69
69
|
|
|
70
70
|
import { shouldRunHook } from './_lib/profile-gate.mjs';
|
|
71
|
+
import { isMainModule } from '../scripts/lib/is-main-module.mjs';
|
|
71
72
|
import { existsSync } from 'node:fs';
|
|
72
73
|
import path from 'node:path';
|
|
73
74
|
|
|
74
75
|
// Opt-out per session via SO_DISABLED_HOOKS=post-bash-issue-budget-refund; the
|
|
75
|
-
// "minimal"/"off" profiles disable it like every other non-core hook
|
|
76
|
-
|
|
76
|
+
// "minimal"/"off" profiles disable it like every other non-core hook — the gate
|
|
77
|
+
// itself runs inside the entry guard at the bottom of this file (#1393).
|
|
77
78
|
|
|
78
79
|
// ---------------------------------------------------------------------------
|
|
79
80
|
// Helpers
|
|
@@ -367,9 +368,18 @@ async function main() {
|
|
|
367
368
|
}
|
|
368
369
|
|
|
369
370
|
// Top-level error handler — fail open, same posture as the sibling hooks.
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
371
|
+
// Entry guard (#1393): run only when this file IS the script node was invoked
|
|
372
|
+
// with — every harness path execs it (`sh run-node.sh <this file>`). A bare
|
|
373
|
+
// `import()` (a probe, a test, a curious agent) must neither run main() nor
|
|
374
|
+
// tear the importing process down. The profile gate sits INSIDE the guard for
|
|
375
|
+
// that second reason: at module top level its `process.exit(0)` exited every
|
|
376
|
+
// process that merely imported this hook.
|
|
377
|
+
if (isMainModule(import.meta.url)) {
|
|
378
|
+
if (!shouldRunHook('post-bash-issue-budget-refund')) process.exit(0);
|
|
379
|
+
main().catch((e) => {
|
|
380
|
+
process.stderr.write(
|
|
381
|
+
`⚠ post-bash-issue-budget-refund: internal error — ${e?.message || e}\n`,
|
|
382
|
+
);
|
|
383
|
+
process.exit(0);
|
|
384
|
+
});
|
|
385
|
+
}
|
|
@@ -143,8 +143,6 @@
|
|
|
143
143
|
*/
|
|
144
144
|
|
|
145
145
|
import { shouldRunHook } from './_lib/profile-gate.mjs';
|
|
146
|
-
// Exit 0 immediately (silent no-op) when disabled via profile/env (#211).
|
|
147
|
-
if (!shouldRunHook('post-bash-write-verify')) process.exit(0);
|
|
148
146
|
|
|
149
147
|
import path from 'node:path';
|
|
150
148
|
import os from 'node:os';
|
|
@@ -1136,6 +1134,9 @@ function invokedAsScript() {
|
|
|
1136
1134
|
}
|
|
1137
1135
|
const isMain = invokedAsScript();
|
|
1138
1136
|
if (isMain) {
|
|
1137
|
+
// #1393: the profile gate sits INSIDE the entry guard — at module top level
|
|
1138
|
+
// its `process.exit(0)` exited every process that merely imported this hook.
|
|
1139
|
+
if (!shouldRunHook('post-bash-write-verify')) process.exit(0);
|
|
1139
1140
|
// Advisory hook: never block, never surface an error to the tool call.
|
|
1140
1141
|
main().catch(() => {}).finally(() => process.exit(0));
|
|
1141
1142
|
}
|
|
@@ -37,8 +37,7 @@
|
|
|
37
37
|
*/
|
|
38
38
|
|
|
39
39
|
import { shouldRunHook } from './_lib/profile-gate.mjs';
|
|
40
|
-
|
|
41
|
-
if (!shouldRunHook('post-edit-import-probe')) process.exit(0);
|
|
40
|
+
import { isMainModule } from '../scripts/lib/is-main-module.mjs';
|
|
42
41
|
|
|
43
42
|
import path from 'node:path';
|
|
44
43
|
import { readFileSync, existsSync } from 'node:fs';
|
|
@@ -335,10 +334,19 @@ async function main() {
|
|
|
335
334
|
// Exit 0 always — a hook that guards against hook breakage must never block.
|
|
336
335
|
// The catch stays silent towards the TOOL CALL but not towards the operator:
|
|
337
336
|
// a probe whose own breakage is invisible is a probe nobody can trust.
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
337
|
+
// Entry guard (#1393): run only when this file IS the script node was invoked
|
|
338
|
+
// with — every harness path execs it (`sh run-node.sh <this file>`). A bare
|
|
339
|
+
// `import()` (a probe, a test, a curious agent) must neither run main() nor
|
|
340
|
+
// tear the importing process down. The profile gate sits INSIDE the guard for
|
|
341
|
+
// that second reason: at module top level its `process.exit(0)` exited every
|
|
342
|
+
// process that merely imported this hook.
|
|
343
|
+
if (isMainModule(import.meta.url)) {
|
|
344
|
+
if (!shouldRunHook('post-edit-import-probe')) process.exit(0);
|
|
345
|
+
main()
|
|
346
|
+
.catch((err) => {
|
|
347
|
+
try {
|
|
348
|
+
process.stderr.write(`post-edit-import-probe: ${err?.message ?? String(err)}\n`);
|
|
349
|
+
} catch { /* stderr gone — nothing left to report to */ }
|
|
350
|
+
})
|
|
351
|
+
.finally(() => process.exit(0));
|
|
352
|
+
}
|
|
@@ -32,8 +32,7 @@ import { spawnSync } from 'node:child_process';
|
|
|
32
32
|
import { performance } from 'node:perf_hooks';
|
|
33
33
|
|
|
34
34
|
import { shouldRunHook } from './_lib/profile-gate.mjs';
|
|
35
|
-
|
|
36
|
-
if (!shouldRunHook('post-edit-validate')) process.exit(0);
|
|
35
|
+
import { isMainModule } from '../scripts/lib/is-main-module.mjs';
|
|
37
36
|
|
|
38
37
|
import { readStdin } from '../scripts/lib/io.mjs';
|
|
39
38
|
import { resolveProjectDir } from '../scripts/lib/platform.mjs';
|
|
@@ -220,6 +219,15 @@ async function main() {
|
|
|
220
219
|
}
|
|
221
220
|
|
|
222
221
|
// REQ-01: top-level catch — PostToolUse must never block; always exit 0
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
222
|
+
// Entry guard (#1393): run only when this file IS the script node was invoked
|
|
223
|
+
// with — every harness path execs it (`sh run-node.sh <this file>`). A bare
|
|
224
|
+
// `import()` (a probe, a test, a curious agent) must neither run main() nor
|
|
225
|
+
// tear the importing process down. The profile gate sits INSIDE the guard for
|
|
226
|
+
// that second reason: at module top level its `process.exit(0)` exited every
|
|
227
|
+
// process that merely imported this hook.
|
|
228
|
+
if (isMainModule(import.meta.url)) {
|
|
229
|
+
if (!shouldRunHook('post-edit-validate')) process.exit(0);
|
|
230
|
+
main().catch(() => {
|
|
231
|
+
process.exit(0);
|
|
232
|
+
});
|
|
233
|
+
}
|