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
|
@@ -22,7 +22,24 @@
|
|
|
22
22
|
* `findViolations()` — the whole matcher (claim patterns, negative-context
|
|
23
23
|
* guards, evidence proximity, normalisation, dedup) lives there so it can
|
|
24
24
|
* be measured against a claim corpus without spawning this hook. It
|
|
25
|
-
* returns DEDUPLICATED `{claim, normalized, occurrences}`
|
|
25
|
+
* returns DEDUPLICATED `{claim, normalized, occurrences, kind, detail?}`
|
|
26
|
+
* records in THREE classes: `distributional` (#567/#908 — "4 of 4
|
|
27
|
+
* callers"), `gate-verdict` (w4-1 — "STATUS: done" / "alles grün" with
|
|
28
|
+
* no run receipt anywhere in the report), and `claim-mismatch` (#1385 R1
|
|
29
|
+
* — a claimed test count that NO observed vitest run carries). `kind`
|
|
30
|
+
* reaches both the ledger record and the WARN; its ABSENCE on a record
|
|
31
|
+
* means `distributional`, since no pre-w4-1 record could be anything
|
|
32
|
+
* else.
|
|
33
|
+
* 5b. The `claim-mismatch` class needs the OTHER side of the transcript, so
|
|
34
|
+
* `readTranscriptObservations()` reads the vitest summaries out of the
|
|
35
|
+
* `tool_result` blocks in the same window and passes them in. It closes
|
|
36
|
+
* an INVERSION: `RUN_RECEIPT_RE` is report-wide and matches
|
|
37
|
+
* `\d+\s+passed`, so before R1 an INVENTED number was its own receipt
|
|
38
|
+
* while an honest report with no number was flagged (measured
|
|
39
|
+
* 2026-09-19: `'STATUS: done\nTests pass: 5129 passed / 0 failed.'` → no
|
|
40
|
+
* violation; `'STATUS: done\nAlles grün.'` → two). No observations in
|
|
41
|
+
* the window ⇒ no `claim-mismatch` finding: evidence ABSENCE is
|
|
42
|
+
* `gate-verdict`'s job.
|
|
26
43
|
* 6. Attribute the claim: `agent` + `agent_source` (`payload`|`meta`|`none`)
|
|
27
44
|
* + `agent_description`, and — on `none` — the sorted stdin `payload_keys`
|
|
28
45
|
* the harness DID send, so a gap is diagnosable from the ledger.
|
|
@@ -62,8 +79,7 @@
|
|
|
62
79
|
*/
|
|
63
80
|
|
|
64
81
|
import { shouldRunHook } from './_lib/profile-gate.mjs';
|
|
65
|
-
|
|
66
|
-
if (!shouldRunHook('post-subagent-discovery-validator')) process.exit(0);
|
|
82
|
+
import { isMainModule } from '../scripts/lib/is-main-module.mjs';
|
|
67
83
|
|
|
68
84
|
import { createHash } from 'node:crypto';
|
|
69
85
|
import { promises as fs } from 'node:fs';
|
|
@@ -71,7 +87,11 @@ import { tmpdir } from 'node:os';
|
|
|
71
87
|
import path from 'node:path';
|
|
72
88
|
|
|
73
89
|
import { resolveSubagentSidecar } from './_lib/subagent-paths.mjs';
|
|
74
|
-
import {
|
|
90
|
+
import {
|
|
91
|
+
findViolations,
|
|
92
|
+
readTranscriptObservations,
|
|
93
|
+
readTranscriptTail,
|
|
94
|
+
} from './_lib/subagent-transcript.mjs';
|
|
75
95
|
import { appendJsonl } from '../scripts/lib/common.mjs';
|
|
76
96
|
import { eventsFilePath } from '../scripts/lib/events.mjs';
|
|
77
97
|
import { getProjectDir } from '../scripts/lib/platform.mjs';
|
|
@@ -393,7 +413,11 @@ async function main() {
|
|
|
393
413
|
if (sidecar === null) return;
|
|
394
414
|
|
|
395
415
|
const text = await readTranscriptTail(sidecar.transcript);
|
|
396
|
-
|
|
416
|
+
// The `tool_result` side of the SAME window (#1385 R1) — what the agent
|
|
417
|
+
// actually RAN, against which its claimed test counts are compared. An
|
|
418
|
+
// empty list disables the `claim-mismatch` class; it never creates one.
|
|
419
|
+
const observations = await readTranscriptObservations(sidecar.transcript);
|
|
420
|
+
const { violations, undatedVerified } = findViolations(text, { observations });
|
|
397
421
|
if (violations.length === 0) return;
|
|
398
422
|
|
|
399
423
|
const attribution = await resolveAgentAttribution(input, sidecar.meta);
|
|
@@ -443,7 +467,15 @@ async function main() {
|
|
|
443
467
|
}
|
|
444
468
|
if (seen) continue;
|
|
445
469
|
}
|
|
446
|
-
|
|
470
|
+
// AWAITED **and** CAUGHT. A telemetry write added to a guard silently
|
|
471
|
+
// disarms it unless both hold: without the await the process can exit
|
|
472
|
+
// before the append lands, and without the catch one failed write aborts
|
|
473
|
+
// the remaining violations AND the stderr WARN below — the coordinator's
|
|
474
|
+
// only copy of the finding. This hook never denies, so the failure is a
|
|
475
|
+
// lost finding rather than a lost block; it is still a loss, and a full
|
|
476
|
+
// disk must not be able to silence the scanner.
|
|
477
|
+
try {
|
|
478
|
+
await appendJsonl(filePath, {
|
|
447
479
|
event: 'discovery_validator_violation',
|
|
448
480
|
timestamp: new Date().toISOString(),
|
|
449
481
|
agent,
|
|
@@ -457,8 +489,25 @@ async function main() {
|
|
|
457
489
|
...(sessionId !== null ? { session_id: sessionId } : {}),
|
|
458
490
|
claim_text: violation.claim,
|
|
459
491
|
occurrences: violation.occurrences,
|
|
460
|
-
|
|
461
|
-
|
|
492
|
+
// Claim CLASS (w4-1, #1385 R1): `distributional` (the #567/#908
|
|
493
|
+
// patterns), `gate-verdict` (a done/green assertion with no run
|
|
494
|
+
// receipt), or `claim-mismatch` (a test count no observed run carries).
|
|
495
|
+
// A new field rather than an overloaded old one — a ledger reader
|
|
496
|
+
// triaging 3,000 records must be able to separate them without
|
|
497
|
+
// re-parsing `claim_text`, and every pre-w4-1 record is
|
|
498
|
+
// `distributional` by construction (the field's absence means that).
|
|
499
|
+
kind: violation.kind ?? 'distributional',
|
|
500
|
+
// `claim-mismatch` only: `{mismatch, claimed, observed, observed_n}`.
|
|
501
|
+
// Numbers, never the command text that produced them (precedent
|
|
502
|
+
// `8f15f77b` — `command_hash` instead of the raw command); `observed`
|
|
503
|
+
// is capped at the last 3 runs by findViolations().
|
|
504
|
+
...(violation.detail ?? {}),
|
|
505
|
+
});
|
|
506
|
+
written++;
|
|
507
|
+
} catch {
|
|
508
|
+
// Write failed — keep going. The WARN below still reaches the
|
|
509
|
+
// coordinator, which is the channel PSA-006 actually depends on.
|
|
510
|
+
}
|
|
462
511
|
}
|
|
463
512
|
|
|
464
513
|
// Advisory only (#908 item 4) — never promoted to a violation in v1.
|
|
@@ -473,10 +522,39 @@ async function main() {
|
|
|
473
522
|
? ` ${violations.length - written} already recorded earlier in this session.`
|
|
474
523
|
: '';
|
|
475
524
|
|
|
525
|
+
// THREE claim CLASSES, counted separately in the WARN (w4-1, #1385 R1):
|
|
526
|
+
// they fail different rules and want different corrections — a
|
|
527
|
+
// distributional claim needs its grep transcript, a gate verdict needs a
|
|
528
|
+
// run receipt, and a count mismatch needs the run repeated or the number
|
|
529
|
+
// corrected. One aggregate count would leave the coordinator guessing which.
|
|
530
|
+
const gateCount = violations.filter((v) => v.kind === 'gate-verdict').length;
|
|
531
|
+
const mismatchCount = violations.filter((v) => v.kind === 'claim-mismatch').length;
|
|
532
|
+
const distCount = violations.length - gateCount - mismatchCount;
|
|
533
|
+
const classNote = gateCount === 0
|
|
534
|
+
? ''
|
|
535
|
+
: ` ${gateCount} of them ${gateCount === 1 ? 'is a' : 'are'} DONE/GATE verdict(s) with no run ` +
|
|
536
|
+
`receipt (no test count, no exit code) anywhere in the report; ` +
|
|
537
|
+
`${distCount} repo-state/distributional.`;
|
|
538
|
+
|
|
539
|
+
// The correction this class asks for is named, because it is NOT the one
|
|
540
|
+
// the other two ask for: no amount of added grep evidence fixes a number
|
|
541
|
+
// that contradicts every run in the window.
|
|
542
|
+
const mismatchNote = mismatchCount === 0
|
|
543
|
+
? ''
|
|
544
|
+
: ` ${mismatchCount} ${mismatchCount === 1 ? 'is a' : 'are'} COUNT MISMATCH: ` +
|
|
545
|
+
violations
|
|
546
|
+
.filter((v) => v.kind === 'claim-mismatch')
|
|
547
|
+
.map((v) => {
|
|
548
|
+
const observed = (v.detail?.observed ?? []).map((o) => `${o.passed} passed`).join(', ');
|
|
549
|
+
return `claimed ${v.detail?.claimed?.passed} passed, observed ${observed || 'none'}`;
|
|
550
|
+
})
|
|
551
|
+
.join('; ') +
|
|
552
|
+
` — re-run the suite or correct the number.`;
|
|
553
|
+
|
|
476
554
|
const warnText =
|
|
477
|
-
`⚠ PSA-006: ${violations.length} distinct
|
|
478
|
-
`"${agent}" (source: ${attribution.source}) lack
|
|
479
|
-
`(grep/rg/find/git/wc/jq/ls/node/npm) (non-blocking)
|
|
555
|
+
`⚠ PSA-006: ${violations.length} distinct claim(s) from agent ` +
|
|
556
|
+
`"${agent}" (source: ${attribution.source}) lack measurement evidence ` +
|
|
557
|
+
`(grep/rg/find/git/wc/jq/ls/node/npm) (non-blocking).${classNote}${mismatchNote}` +
|
|
480
558
|
`${suppressedNote}${undatedNote} ` +
|
|
481
559
|
`See .claude/rules/parallel-sessions.md § PSA-006.`;
|
|
482
560
|
process.stderr.write(warnText + '\n');
|
|
@@ -526,5 +604,12 @@ async function main() {
|
|
|
526
604
|
}));
|
|
527
605
|
}
|
|
528
606
|
|
|
529
|
-
//
|
|
530
|
-
|
|
607
|
+
// Entry guard (#1393): run only as the node script the harness execs — a bare
|
|
608
|
+
// `import()` must run no handler and must not exit the importing process.
|
|
609
|
+
if (isMainModule(import.meta.url)) {
|
|
610
|
+
// Exit 0 immediately when disabled via SO_HOOK_PROFILE / SO_DISABLED_HOOKS.
|
|
611
|
+
if (!shouldRunHook('post-subagent-discovery-validator')) process.exit(0);
|
|
612
|
+
|
|
613
|
+
// Exit 0 always — informational hook must never block Claude (#567 v1).
|
|
614
|
+
main().catch(() => {}).finally(() => process.exit(0));
|
|
615
|
+
}
|
|
@@ -66,18 +66,21 @@
|
|
|
66
66
|
* hooks.json wiring is managed separately (W3-C4 scope).
|
|
67
67
|
*/
|
|
68
68
|
|
|
69
|
-
import { execFileSync } from 'node:child_process';
|
|
69
|
+
import { execFileSync, spawn } from 'node:child_process';
|
|
70
|
+
import { readFileSync } from 'node:fs';
|
|
70
71
|
import { readFile } from 'node:fs/promises';
|
|
71
72
|
import path from 'node:path';
|
|
73
|
+
import { performance } from 'node:perf_hooks';
|
|
74
|
+
import { fileURLToPath } from 'node:url';
|
|
72
75
|
|
|
73
76
|
import { shouldRunHook } from './_lib/profile-gate.mjs';
|
|
74
|
-
|
|
75
|
-
if (!shouldRunHook('post-tool-batch-wave-signal')) process.exit(0);
|
|
77
|
+
import { isMainModule } from '../scripts/lib/is-main-module.mjs';
|
|
76
78
|
|
|
77
79
|
import { getProjectDir } from '../scripts/lib/platform.mjs';
|
|
78
80
|
import { emitEvent } from '../scripts/lib/events.mjs';
|
|
79
81
|
import { findScopeFile } from '../scripts/lib/scope-gate.mjs';
|
|
80
82
|
import { atomicMutateJson } from './_lib/atomic-json.mjs';
|
|
83
|
+
import { _parseReaper } from '../scripts/lib/config/reaper.mjs';
|
|
81
84
|
|
|
82
85
|
// ---------------------------------------------------------------------------
|
|
83
86
|
// Helpers
|
|
@@ -205,7 +208,7 @@ async function ownsSessionFile(input, sessionFile) {
|
|
|
205
208
|
* previous hard-coded `.claude/` path made this hook's whole wave-lifecycle
|
|
206
209
|
* fallback structurally dead on Codex CLI, Cursor and pi: the manifest exists,
|
|
207
210
|
* it just is not under `.claude/`, so every batch read 0 and no
|
|
208
|
-
*
|
|
211
|
+
* wave-lifecycle event was ever emitted there.
|
|
209
212
|
*
|
|
210
213
|
* @param {string} projectDir
|
|
211
214
|
* @returns {Promise<number>}
|
|
@@ -305,6 +308,145 @@ function countFilesChangedSince(projectDir, sha) {
|
|
|
305
308
|
}
|
|
306
309
|
}
|
|
307
310
|
|
|
311
|
+
// ---------------------------------------------------------------------------
|
|
312
|
+
// orphan-reaper trigger (#1432 B4)
|
|
313
|
+
// ---------------------------------------------------------------------------
|
|
314
|
+
//
|
|
315
|
+
// DUPLICATED VERBATIM between the two trigger hooks —
|
|
316
|
+
// hooks/post-tool-batch-wave-signal.mjs <-> hooks/on-stop.mjs. Deliberate, with
|
|
317
|
+
// a named revisit trigger (BV-004): they are the only two trigger points the
|
|
318
|
+
// PRD declares, and a shared `hooks/_lib/reaper-trigger.mjs` would be a third
|
|
319
|
+
// file in the hook import graph. SIZE, measured 2026-09-22 by diffing the two
|
|
320
|
+
// blocks: 138 identical lines — this note said "~40 lines of glue" until then,
|
|
321
|
+
// which is what made the duplicate look cheaper than it is. Extract it the
|
|
322
|
+
// moment a THIRD hook needs the trigger, or the moment the copies must differ.
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Filesystem path of the scan CLI, spawned as a PLAIN argv call.
|
|
326
|
+
*
|
|
327
|
+
* It used to be a `file://` URL handed to `node --input-type=module -e
|
|
328
|
+
* <program>`, because `scripts/lib/orphan-reaper.mjs` had no entry guard. It
|
|
329
|
+
* has one now (`parseReaperCliArgs` + the `isMainModule` tail), so the child's
|
|
330
|
+
* contract lives in that module instead of as source text duplicated here —
|
|
331
|
+
* and nothing this hook builds is a program any more: every variable part is
|
|
332
|
+
* an argv value, which cannot become code whatever the checkout path contains.
|
|
333
|
+
*
|
|
334
|
+
* `fileURLToPath`, not `new URL(...).pathname`: the latter leaves a
|
|
335
|
+
* percent-encoded path for any checkout directory containing a space.
|
|
336
|
+
*/
|
|
337
|
+
const ORPHAN_REAPER_SCRIPT = fileURLToPath(
|
|
338
|
+
new URL('../scripts/lib/orphan-reaper.mjs', import.meta.url),
|
|
339
|
+
);
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Read the `reaper:` block from the project's Session Config host file.
|
|
343
|
+
* Sync + inline, mirroring `hooks/loop-guard.mjs` `loadConfig()` — a hot hook
|
|
344
|
+
* path must not import the full config orchestrator. A missing or unreadable
|
|
345
|
+
* file yields the parser defaults, i.e. DISABLED.
|
|
346
|
+
*
|
|
347
|
+
* @param {string} projectDir
|
|
348
|
+
* @returns {ReturnType<typeof _parseReaper>}
|
|
349
|
+
*/
|
|
350
|
+
function loadReaperConfig(projectDir) {
|
|
351
|
+
for (const name of ['CLAUDE.md', 'AGENTS.md']) {
|
|
352
|
+
try {
|
|
353
|
+
return _parseReaper(readFileSync(path.join(projectDir, name), 'utf8'));
|
|
354
|
+
} catch {
|
|
355
|
+
// missing or unreadable — try the next candidate
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
return _parseReaper('');
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Trigger the orphan scan, throttled and NON-BLOCKING (PRD FA4).
|
|
363
|
+
*
|
|
364
|
+
* The hook itself does exactly two pieces of I/O — one config read and one
|
|
365
|
+
* `stat` of the throttle marker — and then hands the work to a DETACHED,
|
|
366
|
+
* unref'd child. The scan is never run inline: one `ps` round-trip out of Node
|
|
367
|
+
* was measured at ~47 ms over 287 KB of output, which alone would blow the
|
|
368
|
+
* 50 ms `reaper.max-hook-latency-ms` budget this hook has to stay inside.
|
|
369
|
+
*
|
|
370
|
+
* The marker is stamped BEFORE the spawn, so a spawn that fails still consumes
|
|
371
|
+
* the throttle window — otherwise a broken spawn would be retried on every
|
|
372
|
+
* single tool batch.
|
|
373
|
+
*
|
|
374
|
+
* `reaper.max-hook-latency-ms` is the ceiling on the work above — measured here
|
|
375
|
+
* with `performance.now()` and reported as ONE stderr WARN line when exceeded.
|
|
376
|
+
* A WARN and not an event: this fires from a `PostToolBatch`-class hook, and a
|
|
377
|
+
* per-fire telemetry record is exactly the always-on signal
|
|
378
|
+
* `.claude/rules/host-resources.md` HR-101 calls a broken instrument. The
|
|
379
|
+
* measurement covers preparation only — the scan itself runs detached, which is
|
|
380
|
+
* the whole reason the budget can be held.
|
|
381
|
+
*
|
|
382
|
+
* Never throws: any failure degrades silently (PRD FA4 "lautlos degradieren").
|
|
383
|
+
*
|
|
384
|
+
* @param {object} [opts]
|
|
385
|
+
* @param {string} [opts.projectDir] Repo root; defaults to `getProjectDir()`.
|
|
386
|
+
* @param {number} [opts.now] Injected clock (ms).
|
|
387
|
+
* @param {Function} [opts.spawnFn] Injected `spawn` (tests).
|
|
388
|
+
* @param {Function} [opts.statFn] Injected `statSync` (tests).
|
|
389
|
+
* @param {Function} [opts.writeFn] Injected marker writer (tests).
|
|
390
|
+
* @param {() => number} [opts.clockFn] Injected monotonic clock for the latency
|
|
391
|
+
* budget (tests); defaults to `performance.now`.
|
|
392
|
+
* @returns {Promise<{spawned: boolean, reason: string}>}
|
|
393
|
+
*/
|
|
394
|
+
export async function maybeTriggerOrphanScan({
|
|
395
|
+
projectDir,
|
|
396
|
+
now,
|
|
397
|
+
spawnFn = spawn,
|
|
398
|
+
statFn,
|
|
399
|
+
writeFn,
|
|
400
|
+
clockFn = () => performance.now(),
|
|
401
|
+
} = {}) {
|
|
402
|
+
const startedAt = clockFn();
|
|
403
|
+
/** One WARN line when the preparation overran `reaper.max-hook-latency-ms`. */
|
|
404
|
+
const checkLatency = (budgetMs) => {
|
|
405
|
+
const elapsed = clockFn() - startedAt;
|
|
406
|
+
if (Number.isFinite(budgetMs) && budgetMs > 0 && elapsed > budgetMs) {
|
|
407
|
+
process.stderr.write(
|
|
408
|
+
`orphan-reaper trigger: hook latency ${elapsed.toFixed(1)} ms exceeded `
|
|
409
|
+
+ `reaper.max-hook-latency-ms (${budgetMs} ms)\n`,
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
};
|
|
413
|
+
try {
|
|
414
|
+
const root = typeof projectDir === 'string' && projectDir ? projectDir : getProjectDir();
|
|
415
|
+
const cfg = loadReaperConfig(root);
|
|
416
|
+
// Cheapest gate first: disabled means no stat, no spawn, no module load.
|
|
417
|
+
if (cfg.enabled !== true) return { spawned: false, reason: 'disabled' };
|
|
418
|
+
|
|
419
|
+
const reaper = await import('../scripts/lib/orphan-reaper.mjs');
|
|
420
|
+
const markerPath = reaper.scanMarkerPath(root);
|
|
421
|
+
const nowMs = typeof now === 'number' ? now : Date.now();
|
|
422
|
+
|
|
423
|
+
if (!reaper.shouldScanNow(markerPath, nowMs, cfg['min-scan-interval-seconds'], { statFn })) {
|
|
424
|
+
checkLatency(cfg['max-hook-latency-ms']);
|
|
425
|
+
return { spawned: false, reason: 'throttled' };
|
|
426
|
+
}
|
|
427
|
+
reaper.touchScanMarker(markerPath, { writeFn });
|
|
428
|
+
|
|
429
|
+
const child = spawnFn(
|
|
430
|
+
process.execPath,
|
|
431
|
+
[
|
|
432
|
+
ORPHAN_REAPER_SCRIPT,
|
|
433
|
+
'--repo-root', root,
|
|
434
|
+
'--mode', cfg.mode,
|
|
435
|
+
'--min-age-seconds', String(cfg['min-age-seconds']),
|
|
436
|
+
'--kill-grace-ms', String(cfg['kill-grace-ms']),
|
|
437
|
+
'--verify-wait-ms', String(cfg['verify-wait-ms']),
|
|
438
|
+
'--false-alarm-window', String(cfg['false-alarm-window']),
|
|
439
|
+
],
|
|
440
|
+
{ detached: true, stdio: 'ignore' },
|
|
441
|
+
);
|
|
442
|
+
if (child && typeof child.unref === 'function') child.unref();
|
|
443
|
+
checkLatency(cfg['max-hook-latency-ms']);
|
|
444
|
+
return { spawned: true, reason: 'spawned' };
|
|
445
|
+
} catch {
|
|
446
|
+
return { spawned: false, reason: 'error' };
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
308
450
|
// ---------------------------------------------------------------------------
|
|
309
451
|
// Main
|
|
310
452
|
// ---------------------------------------------------------------------------
|
|
@@ -378,9 +520,13 @@ async function main() {
|
|
|
378
520
|
}
|
|
379
521
|
} catch { /* best effort — hook must remain non-blocking */ }
|
|
380
522
|
|
|
381
|
-
// Emit wave-
|
|
382
|
-
// populates wave_signal
|
|
383
|
-
//
|
|
523
|
+
// Emit the wave-completion event via the canonical stream when the
|
|
524
|
+
// orchestrator populates wave_signal 'wave-complete'. Mechanical seam — see
|
|
525
|
+
// docs/events-schema.md. Best-effort; never blocks the hook. A 'wave-start'
|
|
526
|
+
// signal is still ACCEPTED — it keeps suppressing the fallback below, as
|
|
527
|
+
// before — but emits nothing: the `started` sibling event was removed
|
|
528
|
+
// 2026-09-19, having no reader once the convergence monitor stopped
|
|
529
|
+
// admitting it.
|
|
384
530
|
//
|
|
385
531
|
// Live path today: the `wave_signal === null` branch below provides the
|
|
386
532
|
// mechanical wave-lifecycle fallback (#612). It fires live by diffing
|
|
@@ -388,19 +534,14 @@ async function main() {
|
|
|
388
534
|
// payload injection required. This explicit-signal branch remains as the
|
|
389
535
|
// preferred path for whenever the harness DOES inject `wave_signal` into the
|
|
390
536
|
// batch payload (it takes precedence over the fallback when present).
|
|
391
|
-
if (waveSignal === 'wave-
|
|
537
|
+
if (waveSignal === 'wave-complete') {
|
|
392
538
|
try {
|
|
393
|
-
await emitEvent(
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
{
|
|
398
|
-
|
|
399
|
-
...(nextWaveRole !== null ? { next_wave_role: nextWaveRole } : {}),
|
|
400
|
-
...(batchId !== null ? { batch_id: batchId } : {}),
|
|
401
|
-
...(batchSize !== null ? { batch_size: batchSize } : {}),
|
|
402
|
-
},
|
|
403
|
-
);
|
|
539
|
+
await emitEvent('orchestrator.wave.completed', {
|
|
540
|
+
...(waveNumber !== null ? { wave_number: waveNumber } : {}),
|
|
541
|
+
...(nextWaveRole !== null ? { next_wave_role: nextWaveRole } : {}),
|
|
542
|
+
...(batchId !== null ? { batch_id: batchId } : {}),
|
|
543
|
+
...(batchSize !== null ? { batch_size: batchSize } : {}),
|
|
544
|
+
});
|
|
404
545
|
// #1193 review F2 — this branch is a SECOND emitter of
|
|
405
546
|
// `wave.completed`, so it must feed the same high-water mark the other
|
|
406
547
|
// two read; otherwise SessionEnd (and the fallback below) would close the
|
|
@@ -413,8 +554,7 @@ async function main() {
|
|
|
413
554
|
// and walked the marker BACKWARDS to 4. Both keys go through maxWave().
|
|
414
555
|
// W4c Q1-MED — and neither is written into a PEER's record.
|
|
415
556
|
if (
|
|
416
|
-
|
|
417
|
-
&& typeof waveNumber === 'number'
|
|
557
|
+
typeof waveNumber === 'number'
|
|
418
558
|
&& waveNumber > 0
|
|
419
559
|
&& await ownsSessionFile(input, sessionFile)
|
|
420
560
|
) {
|
|
@@ -498,24 +638,31 @@ async function main() {
|
|
|
498
638
|
...(batchSize !== null ? { batch_size: batchSize } : {}),
|
|
499
639
|
});
|
|
500
640
|
}
|
|
501
|
-
// Open the new wave.
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
});
|
|
508
|
-
// Persist the high-water marks so the next batch does not re-emit,
|
|
509
|
-
// and so SessionEnd can tell whether the prior wave was already
|
|
510
|
-
// closed here (#1193). `last_wave_completed` is only advanced when a
|
|
511
|
-
// `completed` was actually emitted above (lastWave > 0).
|
|
641
|
+
// Open the new wave. There is no event for this (the `started`
|
|
642
|
+
// sibling was removed 2026-09-19); opening IS the high-water marks
|
|
643
|
+
// persisted below, so the next batch does not re-close the prior
|
|
644
|
+
// wave, and SessionEnd can tell whether it was already closed here
|
|
645
|
+
// (#1193). `last_wave_completed` is only advanced when a `completed`
|
|
646
|
+
// was actually emitted above (lastWave > 0).
|
|
512
647
|
// W4c Q1-MED — never stamp these into a PEER session's record. The
|
|
513
|
-
//
|
|
648
|
+
// event above is still emitted (it carries THIS session's
|
|
514
649
|
// attribution via emitEvent); only the shared-file claim is withheld.
|
|
515
650
|
// NAMED CEILING: for a non-owning session the marks therefore never
|
|
516
|
-
// advance, so
|
|
517
|
-
//
|
|
518
|
-
//
|
|
651
|
+
// advance, so each of its batches re-emits completed{lastWave} while
|
|
652
|
+
// the file's `last_wave` is ahead of its `last_wave_completed`, until
|
|
653
|
+
// that session's own SessionStart takes over the file. The revisit
|
|
654
|
+
// trigger (repeated emissions for one wave) HAS FIRED — measured
|
|
655
|
+
// 2026-09-19 @ 8f6ac022 over this repo's
|
|
656
|
+
// `.orchestrator/metrics/events.jsonl{.1,}`, timestamps >= 2026-09-12,
|
|
657
|
+
// grouped by (session_id, wave_number) with jq:
|
|
658
|
+
// started — 1936 emissions / 48 groups, 19 duplicated, max 274
|
|
659
|
+
// completed — 650 emissions / 28 groups, 2 duplicated, max 579
|
|
660
|
+
// The `started` half is GONE (#1202 §11): its only reader,
|
|
661
|
+
// `scripts/lib/convergence-monitor.mjs`, no longer admits it — it
|
|
662
|
+
// carried no measurement and, sharing a tail tick with completed{N},
|
|
663
|
+
// masked the (N-1, N) shrinking_diff pair. The `completed` half is
|
|
664
|
+
// OPEN: that event has readers, so the fix is a mark a non-owning
|
|
665
|
+
// session can keep for itself, not a removal.
|
|
519
666
|
if (await ownsSessionFile(input, sessionFile)) {
|
|
520
667
|
// #980 — the OPEN half: stamp the sha this new wave starts from.
|
|
521
668
|
// Written as null (not omitted) when git is unreadable, so the
|
|
@@ -559,7 +706,22 @@ async function main() {
|
|
|
559
706
|
},
|
|
560
707
|
}));
|
|
561
708
|
}
|
|
709
|
+
|
|
710
|
+
// Orphan-reaper trigger (#1432 B4). LAST, after this hook's own contract is
|
|
711
|
+
// fulfilled — the stdout envelope above must never wait on the watchdog.
|
|
712
|
+
// Awaited (not fired and forgotten) because the entry guard calls
|
|
713
|
+
// `process.exit(0)` in a `finally`: an un-awaited spawn would race the exit.
|
|
714
|
+
// The await is bounded by design — one config read, one stat, one detached
|
|
715
|
+
// spawn, all measured under `reaper.max-hook-latency-ms` (50 ms).
|
|
716
|
+
await maybeTriggerOrphanScan();
|
|
562
717
|
}
|
|
563
718
|
|
|
564
|
-
//
|
|
565
|
-
|
|
719
|
+
// Entry guard (#1393): run only as the node script the harness execs — a bare
|
|
720
|
+
// `import()` must run no handler and must not exit the importing process.
|
|
721
|
+
if (isMainModule(import.meta.url)) {
|
|
722
|
+
// Exit 0 immediately when disabled via SO_HOOK_PROFILE / SO_DISABLED_HOOKS.
|
|
723
|
+
if (!shouldRunHook('post-tool-batch-wave-signal')) process.exit(0);
|
|
724
|
+
|
|
725
|
+
// Exit 0 always — informational hook must never block Claude.
|
|
726
|
+
main().catch(() => {}).finally(() => process.exit(0));
|
|
727
|
+
}
|
|
@@ -27,8 +27,7 @@
|
|
|
27
27
|
import path from 'node:path';
|
|
28
28
|
|
|
29
29
|
import { shouldRunHook } from './_lib/profile-gate.mjs';
|
|
30
|
-
|
|
31
|
-
if (!shouldRunHook('post-tool-failure-corrective-context')) process.exit(0);
|
|
30
|
+
import { isMainModule } from '../scripts/lib/is-main-module.mjs';
|
|
32
31
|
|
|
33
32
|
import { getProjectDir } from '../scripts/lib/platform.mjs';
|
|
34
33
|
import { atomicMutateJson } from './_lib/atomic-json.mjs';
|
|
@@ -206,7 +205,7 @@ async function main() {
|
|
|
206
205
|
// Strip control chars (newlines, ANSI escapes) before forwarding to additionalContext —
|
|
207
206
|
// SEC-016 (Log Injection): tool errors may contain attacker-controlled bytes.
|
|
208
207
|
const safeSummary = errorSummary
|
|
209
|
-
? errorSummary.replace(/[\r\n]/g, ' ').split('
|
|
208
|
+
? errorSummary.replace(/[\r\n]/g, ' ').split('\x1b').join(' ').slice(0, 120)
|
|
210
209
|
: '';
|
|
211
210
|
const summaryLabel = safeSummary ? ` Error: ${safeSummary}` : '';
|
|
212
211
|
const additionalContext =
|
|
@@ -221,5 +220,12 @@ async function main() {
|
|
|
221
220
|
}));
|
|
222
221
|
}
|
|
223
222
|
|
|
224
|
-
//
|
|
225
|
-
|
|
223
|
+
// Entry guard (#1393): run only as the node script the harness execs — a bare
|
|
224
|
+
// `import()` must run no handler and must not exit the importing process.
|
|
225
|
+
if (isMainModule(import.meta.url)) {
|
|
226
|
+
// Exit 0 immediately when disabled via SO_HOOK_PROFILE / SO_DISABLED_HOOKS.
|
|
227
|
+
if (!shouldRunHook('post-tool-failure-corrective-context')) process.exit(0);
|
|
228
|
+
|
|
229
|
+
// Exit 0 always — informational hook must never block Claude.
|
|
230
|
+
main().catch(() => {}).finally(() => process.exit(0));
|
|
231
|
+
}
|
|
@@ -24,8 +24,7 @@
|
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
26
|
import { shouldRunHook } from './_lib/profile-gate.mjs';
|
|
27
|
-
|
|
28
|
-
if (!shouldRunHook('frontend-slop-hook')) process.exit(0);
|
|
27
|
+
import { isMainModule } from '../scripts/lib/is-main-module.mjs';
|
|
29
28
|
|
|
30
29
|
import path from 'node:path';
|
|
31
30
|
|
|
@@ -180,5 +179,12 @@ async function main() {
|
|
|
180
179
|
}));
|
|
181
180
|
}
|
|
182
181
|
|
|
183
|
-
//
|
|
184
|
-
|
|
182
|
+
// Entry guard (#1393): run only as the node script the harness execs — a bare
|
|
183
|
+
// `import()` must run no handler and must not exit the importing process.
|
|
184
|
+
if (isMainModule(import.meta.url)) {
|
|
185
|
+
// Exit 0 immediately when disabled via SO_HOOK_PROFILE / SO_DISABLED_HOOKS.
|
|
186
|
+
if (!shouldRunHook('frontend-slop-hook')) process.exit(0);
|
|
187
|
+
|
|
188
|
+
// Exit 0 always — informational hook must never block Claude (#684).
|
|
189
|
+
main().catch(() => {}).finally(() => process.exit(0));
|
|
190
|
+
}
|
|
@@ -659,6 +659,20 @@ export function decide(input, lib) {
|
|
|
659
659
|
}
|
|
660
660
|
const tail = omitted > 0 ? `\n(+${omitted} weitere Befund(e) nicht gezeigt)` : '';
|
|
661
661
|
|
|
662
|
+
// Die Optionsgrenze wird aus der Schwelle GERENDERT, nie danebengeschrieben.
|
|
663
|
+
// Ausgeschriebene Zahlwörter ("zwei bis vier") standen hier bis 2026-09-18
|
|
664
|
+
// und waren für jeden Zahlen-grep unsichtbar: verschiebt sich
|
|
665
|
+
// `THRESHOLDS.K6.optionsMax`, blockt der Hook nach der neuen Zahl und nennt
|
|
666
|
+
// dem Operator die alte. Fehlt die Schwelle (defektes Schema-Modul), entfällt
|
|
667
|
+
// der Satz — eine Lücke ist besser als eine erfundene Zahl.
|
|
668
|
+
const k6Limits = lib?.schema?.THRESHOLDS?.K6;
|
|
669
|
+
const limitSentence =
|
|
670
|
+
Number.isInteger(k6Limits?.optionsMin) && Number.isInteger(k6Limits?.optionsMax)
|
|
671
|
+
? `Das ist keine Stilfrage: das Tool selbst nimmt nur ${k6Limits.optionsMin} bis ` +
|
|
672
|
+
`${k6Limits.optionsMax} Optionen je Frage an, und mehr als ${k6Limits.optionsMax} kann ` +
|
|
673
|
+
'niemand gegeneinander abwägen.\n\n'
|
|
674
|
+
: '';
|
|
675
|
+
|
|
662
676
|
// The FIRST LINE names the concrete break, because `emitDeny` derives the
|
|
663
677
|
// operator-visible headline from it. A general preamble first would put a
|
|
664
678
|
// sentence that is identical for every deny in front of the human, and the
|
|
@@ -666,8 +680,7 @@ export function decide(input, lib) {
|
|
|
666
680
|
const reason =
|
|
667
681
|
`AskUserQuestion blockiert: harte Grenze ${groups.map(([id]) => id).join(' + ')} gerissen — ` +
|
|
668
682
|
'so erreicht die Frage den Operator nicht.\n\n' +
|
|
669
|
-
|
|
670
|
-
'und mehr als vier kann niemand gegeneinander abwägen.\n\n' +
|
|
683
|
+
limitSentence +
|
|
671
684
|
`${sections.join('\n\n')}${tail}`;
|
|
672
685
|
|
|
673
686
|
const suggestion =
|