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
|
@@ -18,18 +18,18 @@
|
|
|
18
18
|
* TAIL (last ~8 `type:"assistant"` records), concat text blocks. When no
|
|
19
19
|
* `agent_id` is derivable (or the file is absent) the hook exits 0 and
|
|
20
20
|
* records NOTHING — see the scope note below.
|
|
21
|
-
* 5.
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
* 7.
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
21
|
+
* 5. Hand the concatenated text to `hooks/_lib/subagent-transcript.mjs`
|
|
22
|
+
* `findViolations()` — the whole matcher (claim patterns, negative-context
|
|
23
|
+
* guards, evidence proximity, normalisation, dedup) lives there so it can
|
|
24
|
+
* be measured against a claim corpus without spawning this hook. It
|
|
25
|
+
* returns DEDUPLICATED `{claim, normalized, occurrences}` records.
|
|
26
|
+
* 6. Attribute the claim: `agent` + `agent_source` (`payload`|`meta`|`none`)
|
|
27
|
+
* + `agent_description`, and — on `none` — the sorted stdin `payload_keys`
|
|
28
|
+
* the harness DID send, so a gap is diagnosable from the ledger.
|
|
29
|
+
* 7. Write ONE record per distinct claim per session (session-scoped tmp
|
|
30
|
+
* sentinel), plus a stderr WARN. ADVISORY (#908 Baustein 2 input): claims
|
|
31
|
+
* that ARE verified but carry no measurement TIMESTAMP are counted and
|
|
32
|
+
* reported in the warn text — never recorded as violations in v1.
|
|
33
33
|
*
|
|
34
34
|
* Why read a transcript at all: the SubagentStop stdin payload has NO
|
|
35
35
|
* output_text field — the agent's text only exists on disk.
|
|
@@ -71,215 +71,12 @@ import { tmpdir } from 'node:os';
|
|
|
71
71
|
import path from 'node:path';
|
|
72
72
|
|
|
73
73
|
import { resolveSubagentSidecar } from './_lib/subagent-paths.mjs';
|
|
74
|
+
import { findViolations, readTranscriptTail } from './_lib/subagent-transcript.mjs';
|
|
74
75
|
import { appendJsonl } from '../scripts/lib/common.mjs';
|
|
75
76
|
import { eventsFilePath } from '../scripts/lib/events.mjs';
|
|
76
|
-
import {
|
|
77
|
+
import { getProjectDir } from '../scripts/lib/platform.mjs';
|
|
77
78
|
import { _parseDiscoveryValidator } from '../scripts/lib/config/discovery-validator.mjs';
|
|
78
79
|
|
|
79
|
-
// ---------------------------------------------------------------------------
|
|
80
|
-
// Constants
|
|
81
|
-
// ---------------------------------------------------------------------------
|
|
82
|
-
|
|
83
|
-
/** Number of trailing assistant records to scan. */
|
|
84
|
-
const TAIL_RECORDS = 8;
|
|
85
|
-
/** Proximity window (in lines) for an adjacent grep transcript. */
|
|
86
|
-
const GREP_PROXIMITY_LINES = 5;
|
|
87
|
-
/** Max characters of claim text persisted to the event record. */
|
|
88
|
-
const CLAIM_TEXT_MAX = 200;
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Code-distribution context nouns (singular or plural). A distributional claim
|
|
92
|
-
* is only a PSA-006 *code* claim when one of these appears near the trigger —
|
|
93
|
-
* this is what separates "4 of 4 callers" (a real claim) from "Turn 3 of 25"
|
|
94
|
-
* (a turn counter) and "every caller imports X" from "every developer should
|
|
95
|
-
* test" (generic advice). Tightening per W2-review LOW finding (#567).
|
|
96
|
-
*
|
|
97
|
-
* Single optional `\s?` only (call site / callsite) — no nested quantifiers,
|
|
98
|
-
* so the alternation stays linear-time / ReDoS-safe.
|
|
99
|
-
*/
|
|
100
|
-
const CTX = '(?:call\\s?sites?|callers?|sites?|references?|instances?|files?|consumers?|imports?|matches|match|occurrences?|usages?|modules?|tests?|places?|functions?|dependenc(?:y|ies)|endpoints?|hooks?)';
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* Repo-STATE nouns (#908). The four documented #908 drift cases were counts of
|
|
104
|
-
* repository state, not of code locations: "14 commits", "92 learnings",
|
|
105
|
-
* "5 dirty files", "412 lines". None of them contains a CTX noun, so the
|
|
106
|
-
* original six patterns could not see them.
|
|
107
|
-
*
|
|
108
|
-
* Vocabulary taken from this repo's own artefacts (`.orchestrator/metrics/*.jsonl`
|
|
109
|
-
* record kinds, `.claude/rules/`, `skills/`, `agents/`, `hooks/`) rather than a
|
|
110
|
-
* generic English list — a noun that never names a countable repo artefact here
|
|
111
|
-
* only buys false positives.
|
|
112
|
-
*/
|
|
113
|
-
const STATE = '(?:commits?|learnings?|issues?|branches?|lines?|entries|records?|sessions?|rules?|skills?|probes?|waves?|proposals?|worktrees?)';
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* The noun class the BARE-CARDINAL pattern may use — deliberately a strict
|
|
117
|
-
* subset of STATE ∪ CTX, restricted to the artefact kinds the #908 drift was
|
|
118
|
-
* actually measured in (commit counts, learnings counts, open-issue counts,
|
|
119
|
-
* branch counts, line counts, dirty-file counts).
|
|
120
|
-
*
|
|
121
|
-
* Measured, not guessed: over 32 real agent-stop windows from this repo's own
|
|
122
|
-
* transcripts, admitting the full CTX ∪ STATE set fired 93 times (2.9 per
|
|
123
|
-
* stop — the "validator gets switched off" zone). Every noun below earns its
|
|
124
|
-
* place by naming one of the documented #908 facts; the ones that only cost
|
|
125
|
-
* false positives (`tests`, `references`, `matches`, `agents`, `files` without
|
|
126
|
-
* a state adjective) are excluded here and remain reachable through the six
|
|
127
|
-
* quantifier-triggered patterns above, which have a lexical anchor.
|
|
128
|
-
*
|
|
129
|
-
* `callers` (#918): PSA-006's own canonical noun — the rule text's worked
|
|
130
|
-
* examples ("4 of 4 callers", "100% of callers opt-in") all count callers, yet
|
|
131
|
-
* the cardinal pattern could not see a bare "14 callers". Re-measured with it
|
|
132
|
-
* admitted: 490 real SubagentStop transcripts (2026-07-31), +0 additional
|
|
133
|
-
* firings from the bare form — the noun is free on this corpus (all +3 delta
|
|
134
|
-
* firings came from the #918 slash pattern below).
|
|
135
|
-
*/
|
|
136
|
-
const CARDINAL_NOUN = '(?:commits?|learnings?|issues?|branches?|lines?|files?|callers?)';
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
* Wide noun class = code-distribution nouns ∪ repo-state nouns. Used by the six
|
|
140
|
-
* QUANTIFIER-triggered patterns ("N of M", "100% of", "all N", "no remaining",
|
|
141
|
-
* "none of") — each of those carries a strong lexical trigger, so widening the
|
|
142
|
-
* noun set there is low-risk.
|
|
143
|
-
*
|
|
144
|
-
* Deliberately NOT used by the `every <noun>` pattern: `every` has no numeric
|
|
145
|
-
* anchor, so `every commit must be signed` / `every rule is always-on` are
|
|
146
|
-
* ordinary prose, not measured claims. That pattern keeps the narrow CTX.
|
|
147
|
-
*/
|
|
148
|
-
const WIDE = `(?:${CTX}|${STATE})`;
|
|
149
|
-
|
|
150
|
-
/** Bounded same-line gap between a trigger and its context noun. */
|
|
151
|
-
const CTX_GAP = '[^\\n]{0,40}?';
|
|
152
|
-
|
|
153
|
-
/**
|
|
154
|
-
* Distributional-claim patterns (case-insensitive). A match is a PSA-006 claim
|
|
155
|
-
* that requires an adjacent measurement transcript.
|
|
156
|
-
*
|
|
157
|
-
* Each pattern requires a context noun within a small, bounded same-line window
|
|
158
|
-
* of the trigger. The `[^\n]{0,40}?` gap is a bounded lazy character class
|
|
159
|
-
* (linear-time — the ReDoS-safety the W2 reviewer verified is preserved). True
|
|
160
|
-
* claims ("4 of 4 callers opt-in", "every caller imports X", "no remaining
|
|
161
|
-
* references to Y") still flag; benign strings ("Turn 3 of 25 complete",
|
|
162
|
-
* "every developer should test", "100% of users love it") do not.
|
|
163
|
-
*/
|
|
164
|
-
const CLAIM_PATTERNS = [
|
|
165
|
-
new RegExp(`\\b\\d+ of \\d+\\b${CTX_GAP}\\b${WIDE}\\b`, 'i'),
|
|
166
|
-
new RegExp(`100% of\\b${CTX_GAP}\\b${WIDE}\\b`, 'i'),
|
|
167
|
-
new RegExp(`\\ball \\d+\\b${CTX_GAP}\\b${WIDE}\\b`, 'i'),
|
|
168
|
-
new RegExp(`no remaining\\b${CTX_GAP}\\b${WIDE}\\b`, 'i'),
|
|
169
|
-
new RegExp(`every ${CTX}\\b`, 'i'),
|
|
170
|
-
new RegExp(`none of\\b${CTX_GAP}\\b${WIDE}\\b`, 'i'),
|
|
171
|
-
];
|
|
172
|
-
|
|
173
|
-
/**
|
|
174
|
-
* Pattern 7 (#908) — the BARE CARDINAL repo-state fact. `14 commits` has no
|
|
175
|
-
* quantifier trigger at all, which is exactly why the #908 drift went unseen.
|
|
176
|
-
*
|
|
177
|
-
* A naive `\d+` would fire on every issue reference, version literal, date,
|
|
178
|
-
* line number and percentage in a normal report — and a validator that fires on
|
|
179
|
-
* every report gets switched off, which is strictly worse than no validator.
|
|
180
|
-
* Precision is therefore bought three ways:
|
|
181
|
-
*
|
|
182
|
-
* 1. TRIGGER: a digit run that is not glued to identifier punctuation.
|
|
183
|
-
* Lookbehind rejects `#906`, `v3`, `PSA-006`, `W2`, `foo.mjs:123`;
|
|
184
|
-
* lookahead rejects `3.17`, `70%`, `2026-07-29`, `12615/0`.
|
|
185
|
-
* 2. GAP: at most two intervening ADJECTIVE-like words, and never a
|
|
186
|
-
* preposition/article/copula. "5 dirty files" matches; "3 of 5 stars" and
|
|
187
|
-
* "2 sections below the rules" do not.
|
|
188
|
-
* 3. SCOPE: evaluated only on prose lines with inline-code spans masked out —
|
|
189
|
-
* fenced blocks are skipped entirely (see `findViolations`), because a
|
|
190
|
-
* number inside a fence is tool OUTPUT (the evidence itself), not an
|
|
191
|
-
* unverified assertion about it.
|
|
192
|
-
*
|
|
193
|
-
* All quantifiers are bounded ({1,9}, {0,2}) — linear-time, ReDoS-safe.
|
|
194
|
-
*/
|
|
195
|
-
const CARDINAL_TRIGGER = '(?<![\\w#$:/.-])\\d{1,9}(?![\\d.%:/-])';
|
|
196
|
-
const CARDINAL_STOPWORDS =
|
|
197
|
-
'of|in|on|at|for|to|the|a|an|and|or|is|are|was|were|from|with|by|that|than|per|out|over|into|onto|via|but|as';
|
|
198
|
-
const CARDINAL_GAP = `(?:\\s+(?!(?:${CARDINAL_STOPWORDS})\\b)[A-Za-z][\\w-]*){0,2}`;
|
|
199
|
-
const CARDINAL_PATTERN = new RegExp(`${CARDINAL_TRIGGER}${CARDINAL_GAP}\\s+${CARDINAL_NOUN}\\b`, 'i');
|
|
200
|
-
|
|
201
|
-
/**
|
|
202
|
-
* Pattern 8 (#918) — the PSA-006 CANONICAL numerator/denominator slash form:
|
|
203
|
-
* "12/14 files", "4/4 callers". PSA-006 literally demands "Quote the numerator
|
|
204
|
-
* AND denominator", and `N/M` is the notation that demand produces — yet the
|
|
205
|
-
* cardinal trigger's lookahead `(?![\d.%:/-])` excludes the slash, making the
|
|
206
|
-
* rule's own canon form structurally invisible (#918).
|
|
207
|
-
*
|
|
208
|
-
* Admitting the slash re-opens the `12615/0/11` gate-summary class the
|
|
209
|
-
* lookahead exists to suppress, so the slash form gets its OWN pattern with a
|
|
210
|
-
* STRICTER contract than the bare cardinal:
|
|
211
|
-
*
|
|
212
|
-
* 1. RATIO: exactly two slash-joined numbers. Both boundary guards stay:
|
|
213
|
-
* the lookbehind rejects a numerator glued to a path/id (`hooks/12/14`),
|
|
214
|
-
* the lookahead after the DENOMINATOR rejects a third slash-segment —
|
|
215
|
-
* `12615/0/11` fails twice over (denominator `0` is followed by `/`, and
|
|
216
|
-
* the trailing pair `0/11` has a `/`-glued numerator).
|
|
217
|
-
* 2. NOUN IMMEDIATELY AFTER — no CARDINAL_GAP. The noun-after-denominator is
|
|
218
|
-
* the discriminator that separates a measured ratio ("12/14 files") from
|
|
219
|
-
* a bare slash pair that is a date or score ("on 12/14 we shipped",
|
|
220
|
-
* "rated 3/5 overall"): those are followed by anything BUT an artefact
|
|
221
|
-
* noun. Widening to the gapped form would admit US-date + adjective +
|
|
222
|
-
* noun collisions with no PSA-006 payoff.
|
|
223
|
-
*
|
|
224
|
-
* FP re-measured on the CORRECT text sort (#918 requirement — the prior 1.07
|
|
225
|
-
* rate was measured on coordinator narration, not on what this hook reads):
|
|
226
|
-
* 490 real SubagentStop subagent transcripts (`~/.claude/projects/<slug>/<session>/
|
|
227
|
-
* subagents/*.jsonl`), spawning THIS hook per transcript, 2026-07-31.
|
|
228
|
-
* Baseline (pre-#918): 345 firings / 0.7041 per stop. With slash form +
|
|
229
|
-
* `callers` admitted: 348 / 0.7102 — +3 firings, of which 1 is a true positive
|
|
230
|
-
* ("0/49 Learnings mit Allow-List-Typ", an unverified canon-form claim), 1
|
|
231
|
-
* quotes the #918 example sentence itself (mention-not-use), and 1 is a
|
|
232
|
-
* before/after line-count pair ("858/857 lines", class-consistent with the
|
|
233
|
-
* bare "412 lines" behaviour of pattern 7).
|
|
234
|
-
*
|
|
235
|
-
* Bounded quantifiers only ({1,9}) — linear-time, ReDoS-safe.
|
|
236
|
-
*/
|
|
237
|
-
const CARDINAL_RATIO_PATTERN = new RegExp(
|
|
238
|
-
`(?<![\\w#$:/.-])\\d{1,9}/\\d{1,9}(?![\\d.%:/-])\\s+${CARDINAL_NOUN}\\b`,
|
|
239
|
-
'i'
|
|
240
|
-
);
|
|
241
|
-
|
|
242
|
-
/** Inline-code spans are masked before the cardinal pattern runs. */
|
|
243
|
-
const INLINE_CODE_RE = /`[^`\n]*`/g;
|
|
244
|
-
|
|
245
|
-
/**
|
|
246
|
-
* Gate-summary / STATUS-report lines are the HARNESS's own completion output —
|
|
247
|
-
* tool evidence, not an unverified assertion about the codebase — yet carried
|
|
248
|
-
* no exemption of their own: they were caught only incidentally, by whichever
|
|
249
|
-
* individual claim pattern happened not to fire on that exact phrasing. GitLab
|
|
250
|
-
* #1198 (Discovery D8, 2026-09-02): a 400-event sample of
|
|
251
|
-
* `discovery_validator_violation` records showed 186/400 (46.5%) firing on
|
|
252
|
-
* this exact class — the single largest false-positive source measured to
|
|
253
|
-
* date. Skipped at the LINE level, before ANY pattern runs, rather than by
|
|
254
|
-
* narrowing `CARDINAL_NOUN`: narrowing the noun set would ALSO silence a true
|
|
255
|
-
* "N files" claim appearing outside a gate-summary line, which is not this
|
|
256
|
-
* bug's fix.
|
|
257
|
-
*
|
|
258
|
-
* Four alternatives cover the measured shapes: an N-passed/M-failed count
|
|
259
|
-
* ("14904 passed / 0 failed"), a `STATUS:` report line (`skills/wave-executor`
|
|
260
|
-
* agent-report convention), a "Full Gate" heading, and a `Gate: <verdict>`
|
|
261
|
-
* summary line (English "typecheck" or German "grün"/"rot").
|
|
262
|
-
*/
|
|
263
|
-
const GATE_SUMMARY_LINE_RE =
|
|
264
|
-
/\b\d+\s+passed\s*\/\s*\d+\s+failed\b|^\s*STATUS:\s*(?:done|partial|failed)\b|\bFull Gate\b|\bGate:\s*(?:typecheck|grün|rot)\b/i;
|
|
265
|
-
|
|
266
|
-
/**
|
|
267
|
-
* Commands that count as a MEASUREMENT inside a fenced block. `grep|rg|find`
|
|
268
|
-
* (the #567 set) only covers text search; the #908 facts are measured with
|
|
269
|
-
* `git log --oneline | wc -l`, `jq` over a JSONL metrics file, `ls | wc -l`,
|
|
270
|
-
* or a `node`/`npm` script. Refusing to recognise those made the honest,
|
|
271
|
-
* evidence-quoting path fail verification.
|
|
272
|
-
*/
|
|
273
|
-
const MEASUREMENT_CMD_RE = /\b(grep|rg|find|git|wc|jq|ls|node|npm)\b/;
|
|
274
|
-
|
|
275
|
-
/**
|
|
276
|
-
* Markers that date a measurement (#908 Baustein 2 input). ADVISORY in v1:
|
|
277
|
-
* an undated-but-verified claim is counted and reported, never a violation —
|
|
278
|
-
* a hard contract without an established authoring habit buys friction, not
|
|
279
|
-
* accuracy. Baustein 2 can escalate this to a violation once the habit exists.
|
|
280
|
-
*/
|
|
281
|
-
const TIMESTAMP_MARKER_RE = /\b\d{4}-\d{2}-\d{2}\b|\bHEAD\b|\bas of\b|\bmeasured (?:at|on)\b|\brev-parse\b/i;
|
|
282
|
-
|
|
283
80
|
// ---------------------------------------------------------------------------
|
|
284
81
|
// stdin reading (inline — Stop-family hooks exit 0 always, never deny)
|
|
285
82
|
// ---------------------------------------------------------------------------
|
|
@@ -324,8 +121,8 @@ function readStdinJson() {
|
|
|
324
121
|
*/
|
|
325
122
|
async function isEnabled() {
|
|
326
123
|
const candidates = [
|
|
327
|
-
path.join(
|
|
328
|
-
path.join(
|
|
124
|
+
path.join(getProjectDir(), 'CLAUDE.md'),
|
|
125
|
+
path.join(getProjectDir(), 'AGENTS.md'),
|
|
329
126
|
];
|
|
330
127
|
for (const file of candidates) {
|
|
331
128
|
try {
|
|
@@ -338,194 +135,6 @@ async function isEnabled() {
|
|
|
338
135
|
return false;
|
|
339
136
|
}
|
|
340
137
|
|
|
341
|
-
// ---------------------------------------------------------------------------
|
|
342
|
-
// transcript reading
|
|
343
|
-
// ---------------------------------------------------------------------------
|
|
344
|
-
|
|
345
|
-
/**
|
|
346
|
-
* Read the transcript JSONL at `transcriptPath`, take the last TAIL_RECORDS
|
|
347
|
-
* `type:"assistant"` records, and concatenate their text content blocks.
|
|
348
|
-
* Returns '' on any failure (missing file, /dev/null, malformed lines).
|
|
349
|
-
*
|
|
350
|
-
* @param {string} transcriptPath
|
|
351
|
-
* @returns {Promise<string>}
|
|
352
|
-
*/
|
|
353
|
-
async function readTranscriptTail(transcriptPath) {
|
|
354
|
-
if (typeof transcriptPath !== 'string' || !transcriptPath) return '';
|
|
355
|
-
let raw;
|
|
356
|
-
try {
|
|
357
|
-
raw = await fs.readFile(transcriptPath, 'utf8');
|
|
358
|
-
} catch {
|
|
359
|
-
return '';
|
|
360
|
-
}
|
|
361
|
-
if (!raw.trim()) return '';
|
|
362
|
-
|
|
363
|
-
const assistantRecords = [];
|
|
364
|
-
for (const line of raw.split(/\r?\n/)) {
|
|
365
|
-
const trimmed = line.trim();
|
|
366
|
-
if (!trimmed) continue;
|
|
367
|
-
let rec;
|
|
368
|
-
try { rec = JSON.parse(trimmed); } catch { continue; }
|
|
369
|
-
if (rec && rec.type === 'assistant') assistantRecords.push(rec);
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
const tail = assistantRecords.slice(-TAIL_RECORDS);
|
|
373
|
-
const textBlocks = [];
|
|
374
|
-
for (const rec of tail) {
|
|
375
|
-
const content = rec?.message?.content;
|
|
376
|
-
if (!Array.isArray(content)) continue;
|
|
377
|
-
for (const block of content) {
|
|
378
|
-
if (block && block.type === 'text' && typeof block.text === 'string') {
|
|
379
|
-
textBlocks.push(block.text);
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
}
|
|
383
|
-
return textBlocks.join('\n');
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
// ---------------------------------------------------------------------------
|
|
387
|
-
// claim scanning
|
|
388
|
-
// ---------------------------------------------------------------------------
|
|
389
|
-
|
|
390
|
-
/**
|
|
391
|
-
* Single fence walk. Returns two disjoint-purpose index sets:
|
|
392
|
-
* - `measurementLines`: lines belonging to a fenced block whose body contains
|
|
393
|
-
* a MEASUREMENT_CMD_RE invocation (the evidence a claim can lean on).
|
|
394
|
-
* - `fencedLines`: lines belonging to ANY fenced block (evidence or not).
|
|
395
|
-
* Used only to keep the greedy #908 cardinal pattern out of tool output;
|
|
396
|
-
* the six quantifier patterns are unchanged and still scan fenced lines.
|
|
397
|
-
*
|
|
398
|
-
* An unterminated trailing fence is treated as fenced-to-EOF (conservative for
|
|
399
|
-
* false-positive suppression) but never as a measurement block (its body was
|
|
400
|
-
* never closed, so we cannot claim it verified anything).
|
|
401
|
-
*
|
|
402
|
-
* @param {string[]} lines
|
|
403
|
-
* @returns {{ measurementLines: Set<number>, fencedLines: Set<number> }}
|
|
404
|
-
*/
|
|
405
|
-
function scanFences(lines) {
|
|
406
|
-
const measurementLines = new Set();
|
|
407
|
-
const fencedLines = new Set();
|
|
408
|
-
let fenceStart = -1;
|
|
409
|
-
let buffer = [];
|
|
410
|
-
let inFence = false;
|
|
411
|
-
|
|
412
|
-
for (let i = 0; i < lines.length; i++) {
|
|
413
|
-
const isFence = /^\s*```/.test(lines[i]);
|
|
414
|
-
if (!inFence) {
|
|
415
|
-
if (isFence) { inFence = true; fenceStart = i; buffer = []; }
|
|
416
|
-
continue;
|
|
417
|
-
}
|
|
418
|
-
// inside a fence
|
|
419
|
-
if (isFence) {
|
|
420
|
-
// closing fence — evaluate the buffered body
|
|
421
|
-
const body = buffer.join('\n');
|
|
422
|
-
const isMeasurement = MEASUREMENT_CMD_RE.test(body);
|
|
423
|
-
for (let j = fenceStart; j <= i; j++) {
|
|
424
|
-
fencedLines.add(j);
|
|
425
|
-
if (isMeasurement) measurementLines.add(j);
|
|
426
|
-
}
|
|
427
|
-
inFence = false;
|
|
428
|
-
fenceStart = -1;
|
|
429
|
-
buffer = [];
|
|
430
|
-
} else {
|
|
431
|
-
buffer.push(lines[i]);
|
|
432
|
-
}
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
// Unterminated trailing fence — suppress cardinal matches, grant no evidence.
|
|
436
|
-
if (inFence) {
|
|
437
|
-
for (let j = fenceStart; j < lines.length; j++) fencedLines.add(j);
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
// INLINE-code evidence: PSA-006 asks for "the exact pattern executed" to be
|
|
441
|
-
// quoted — an inline `git log --oneline | wc -l` satisfies that exactly as
|
|
442
|
-
// well as a fenced block, and one-line findings are commonly written that
|
|
443
|
-
// way. Refusing to count it would penalise the honest path.
|
|
444
|
-
for (let i = 0; i < lines.length; i++) {
|
|
445
|
-
if (fencedLines.has(i)) continue;
|
|
446
|
-
for (const span of lines[i].match(INLINE_CODE_RE) ?? []) {
|
|
447
|
-
if (MEASUREMENT_CMD_RE.test(span)) { measurementLines.add(i); break; }
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
return { measurementLines, fencedLines };
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
/**
|
|
455
|
-
* True when any line in `indices` sits within ±GREP_PROXIMITY_LINES of `i`.
|
|
456
|
-
*
|
|
457
|
-
* @param {Set<number>} indices
|
|
458
|
-
* @param {number} i
|
|
459
|
-
* @returns {boolean}
|
|
460
|
-
*/
|
|
461
|
-
function nearIndex(indices, i) {
|
|
462
|
-
for (let j = i - GREP_PROXIMITY_LINES; j <= i + GREP_PROXIMITY_LINES; j++) {
|
|
463
|
-
if (indices.has(j)) return true;
|
|
464
|
-
}
|
|
465
|
-
return false;
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
/**
|
|
469
|
-
* True when a measurement TIMESTAMP marker appears within the same proximity
|
|
470
|
-
* window used for the measurement block itself.
|
|
471
|
-
*
|
|
472
|
-
* @param {string[]} lines
|
|
473
|
-
* @param {number} i
|
|
474
|
-
* @returns {boolean}
|
|
475
|
-
*/
|
|
476
|
-
function hasMeasurementTimestamp(lines, i) {
|
|
477
|
-
const from = Math.max(0, i - GREP_PROXIMITY_LINES);
|
|
478
|
-
const to = Math.min(lines.length - 1, i + GREP_PROXIMITY_LINES);
|
|
479
|
-
for (let j = from; j <= to; j++) {
|
|
480
|
-
if (TIMESTAMP_MARKER_RE.test(lines[j])) return true;
|
|
481
|
-
}
|
|
482
|
-
return false;
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
/**
|
|
486
|
-
* Scan concatenated transcript text for claims lacking an adjacent measurement
|
|
487
|
-
* block (within ±GREP_PROXIMITY_LINES).
|
|
488
|
-
*
|
|
489
|
-
* @param {string} text
|
|
490
|
-
* @returns {{ violations: string[], undatedVerified: number }}
|
|
491
|
-
* `violations` — truncated claim-text snippets; `undatedVerified` — count of
|
|
492
|
-
* claims that ARE verified but carry no measurement timestamp (advisory).
|
|
493
|
-
*/
|
|
494
|
-
function findViolations(text) {
|
|
495
|
-
if (!text) return { violations: [], undatedVerified: 0 };
|
|
496
|
-
const lines = text.split(/\r?\n/);
|
|
497
|
-
const { measurementLines, fencedLines } = scanFences(lines);
|
|
498
|
-
const violations = [];
|
|
499
|
-
let undatedVerified = 0;
|
|
500
|
-
|
|
501
|
-
for (let i = 0; i < lines.length; i++) {
|
|
502
|
-
const line = lines[i];
|
|
503
|
-
// #1198 FIX 2: gate-summary/STATUS lines are tool OUTPUT, not a claim —
|
|
504
|
-
// skipped before any pattern runs (see GATE_SUMMARY_LINE_RE header).
|
|
505
|
-
if (GATE_SUMMARY_LINE_RE.test(line)) continue;
|
|
506
|
-
|
|
507
|
-
// #1198 FIX 3 (masking-order bug): mask inline-code spans ONCE, then test
|
|
508
|
-
// BOTH the six CLAIM_PATTERNS and the cardinal/ratio patterns against the
|
|
509
|
-
// masked text. Previously only the cardinal branch masked — a claim
|
|
510
|
-
// quoted entirely inside backticks (evidence/example text, not an
|
|
511
|
-
// assertion) still tripped CLAIM_PATTERNS via the raw, unmasked line.
|
|
512
|
-
const masked = line.replace(INLINE_CODE_RE, ' ');
|
|
513
|
-
let matched = CLAIM_PATTERNS.some((re) => re.test(masked));
|
|
514
|
-
if (!matched && !fencedLines.has(i)) {
|
|
515
|
-
matched = CARDINAL_PATTERN.test(masked) || CARDINAL_RATIO_PATTERN.test(masked);
|
|
516
|
-
}
|
|
517
|
-
if (!matched) continue;
|
|
518
|
-
|
|
519
|
-
if (nearIndex(measurementLines, i)) {
|
|
520
|
-
if (!hasMeasurementTimestamp(lines, i)) undatedVerified++;
|
|
521
|
-
continue;
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
violations.push(line.trim().slice(0, CLAIM_TEXT_MAX));
|
|
525
|
-
}
|
|
526
|
-
return { violations, undatedVerified };
|
|
527
|
-
}
|
|
528
|
-
|
|
529
138
|
// ---------------------------------------------------------------------------
|
|
530
139
|
// payload helpers
|
|
531
140
|
// ---------------------------------------------------------------------------
|
|
@@ -578,27 +187,92 @@ function resolveAgentTranscriptPath(input, agentId) {
|
|
|
578
187
|
const AGENT_TYPE_META_RE = /^[A-Za-z0-9_.:-]{1,64}$/;
|
|
579
188
|
|
|
580
189
|
/**
|
|
581
|
-
*
|
|
582
|
-
*
|
|
583
|
-
*
|
|
190
|
+
* The sidecar `description` is free operator prose ("W1-d5 Scripts-Audit"), so
|
|
191
|
+
* it is clamped by LENGTH and stripped of control characters rather than
|
|
192
|
+
* charset-validated. 120 chars is the widest description measured on-disk
|
|
193
|
+
* (2026-09-06, 20 real `agent-*.meta.json` files under
|
|
194
|
+
* `~/.claude/projects/<slug>/<session>/subagents/`: max 34 chars).
|
|
195
|
+
*
|
|
196
|
+
* Exposure note: unlike `orchestrator.agent.stopped` — which deliberately
|
|
197
|
+
* omits `description` because that record travels the optional Clank webhook
|
|
198
|
+
* unredacted (see hooks/on-stop.mjs) — this record is appended straight to
|
|
199
|
+
* `.orchestrator/metrics/events.jsonl` via `appendJsonl`, never through
|
|
200
|
+
* `emitEvent`, and it ALREADY carries up to 200 characters of the agent's own
|
|
201
|
+
* prose in `claim_text`. Carrying the description adds no new exposure class.
|
|
202
|
+
*/
|
|
203
|
+
const AGENT_DESCRIPTION_MAX = 120;
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Resolve WHO made the claim, and say where the answer came from (#1218).
|
|
207
|
+
*
|
|
208
|
+
* Measured 2026-09-06 over the fleet's worst-affected repo
|
|
209
|
+
* (`extern/aiat-barrierefrei-engine`, 3,360 `discovery_validator_violation`
|
|
210
|
+
* records): `agent` was the literal string `"unknown"` on 3,026 of them
|
|
211
|
+
* (90.1%), and NOT ONE record carried an `agent_id`. Both the sidecar-meta
|
|
212
|
+
* fallback and the `agent_id` field landed in the same commit (936dae8a,
|
|
213
|
+
* 2026-09-02); the newest of those 3,360 records is 2026-08-25. The 90.1% is
|
|
214
|
+
* therefore a property of a PRE-FIX corpus, not of the code at HEAD — d3's
|
|
215
|
+
* open question 3 ("pre- or post-#1191?") resolves to *pre*.
|
|
216
|
+
*
|
|
217
|
+
* What the corpus does NOT excuse is the SHAPE of the answer. Two gaps remain
|
|
218
|
+
* at HEAD and this function closes them:
|
|
219
|
+
*
|
|
220
|
+
* 1. `"unknown"` was indistinguishable from a real agent type named
|
|
221
|
+
* "unknown", and carried no hint of WHY resolution failed. The record now
|
|
222
|
+
* always carries `agent_source` — `payload` | `meta` | `none` — and, on
|
|
223
|
+
* `none`, the sorted list of stdin keys that WERE present, so the next
|
|
224
|
+
* reader diagnoses the harness gap from the ledger instead of guessing.
|
|
225
|
+
* 2. `agentType` is frequently the useless class `general-purpose`: measured
|
|
226
|
+
* on the same day over 20 real sidecars in THIS repo's own session
|
|
227
|
+
* directory, 14 read `general-purpose` and only 6 a plugin-qualified type.
|
|
228
|
+
* The sidecar's `description` ("W1-d5 Scripts-Audit") is what actually
|
|
229
|
+
* identifies the agent, so it is carried alongside as
|
|
230
|
+
* `agent_description`.
|
|
584
231
|
*
|
|
232
|
+
* @param {object} input — SubagentStop stdin payload
|
|
585
233
|
* @param {string} metaPath — `resolveSubagentSidecar(...).meta`
|
|
586
|
-
* @returns {Promise<string|null>}
|
|
234
|
+
* @returns {Promise<{agent: string, source: 'payload'|'meta'|'none', description: string|null, payloadKeys: string[]}>}
|
|
587
235
|
*/
|
|
588
|
-
async function
|
|
236
|
+
async function resolveAgentAttribution(input, metaPath) {
|
|
237
|
+
const fromPayload = firstNonEmptyString(input, ['agent_type', 'subagent_type'], null);
|
|
238
|
+
// Clamped with the same shape hooks/on-stop.mjs applies to `agentType`
|
|
239
|
+
// (colon included — `session-orchestrator:code-implementer` is the real
|
|
240
|
+
// shape). This value reaches BOTH the ledger event and the model-visible
|
|
241
|
+
// `additionalContext` string, so a mismatch is OMITTED rather than
|
|
242
|
+
// truncated: an unmeasured type stays visibly unmeasured.
|
|
243
|
+
const payloadAgent =
|
|
244
|
+
fromPayload !== null && AGENT_TYPE_META_RE.test(fromPayload.trim()) ? fromPayload.trim() : null;
|
|
245
|
+
|
|
246
|
+
let metaAgent = null;
|
|
247
|
+
let description = null;
|
|
589
248
|
try {
|
|
590
249
|
const meta = JSON.parse(await fs.readFile(metaPath, 'utf8'));
|
|
591
250
|
const t = meta?.agentType;
|
|
592
|
-
if (typeof t
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
251
|
+
if (typeof t === 'string' && AGENT_TYPE_META_RE.test(t.trim())) metaAgent = t.trim();
|
|
252
|
+
const d = meta?.description;
|
|
253
|
+
if (typeof d === 'string' && d.trim()) {
|
|
254
|
+
// Control characters (a stray NUL above all: one NUL makes a text
|
|
255
|
+
// file invisible to every grep-based audit) are replaced, never
|
|
256
|
+
// carried. Filtered by code point rather than by a control-char
|
|
257
|
+
// regex so this file itself stays greppable.
|
|
258
|
+
const clean = [...d]
|
|
259
|
+
.map((ch) => { const c = ch.codePointAt(0); return c < 0x20 || c === 0x7f ? ' ' : ch; })
|
|
260
|
+
.join('')
|
|
261
|
+
.replace(/\s+/g, ' ')
|
|
262
|
+
.trim();
|
|
263
|
+
if (clean) description = clean.slice(0, AGENT_DESCRIPTION_MAX);
|
|
264
|
+
}
|
|
599
265
|
} catch {
|
|
600
|
-
|
|
266
|
+
// absent or corrupt sidecar meta — the payload branch still stands
|
|
601
267
|
}
|
|
268
|
+
|
|
269
|
+
const agent = payloadAgent ?? metaAgent ?? 'unknown';
|
|
270
|
+
const source = payloadAgent !== null ? 'payload' : metaAgent !== null ? 'meta' : 'none';
|
|
271
|
+
// Keys only, never values: the diagnostic question is "what DID the harness
|
|
272
|
+
// send?", and a value could carry a path or prose that has no business in a
|
|
273
|
+
// record whose whole point is attribution.
|
|
274
|
+
const payloadKeys = input && typeof input === 'object' ? Object.keys(input).sort() : [];
|
|
275
|
+
return { agent, source, description, payloadKeys };
|
|
602
276
|
}
|
|
603
277
|
|
|
604
278
|
/**
|
|
@@ -657,6 +331,50 @@ function dedupSentinelPath({ projectRoot, sessionId, agentId, claimText }) {
|
|
|
657
331
|
);
|
|
658
332
|
}
|
|
659
333
|
|
|
334
|
+
/**
|
|
335
|
+
* Build the SESSION-scoped LEDGER sentinel for one normalized claim (#1198).
|
|
336
|
+
*
|
|
337
|
+
* Distinct from `dedupSentinelPath` above in BOTH key and effect, and the
|
|
338
|
+
* difference is the point:
|
|
339
|
+
* - `dedupSentinelPath` is keyed on the individual AGENT and suppresses only
|
|
340
|
+
* the `additionalContext` echo, so each real agent still receives its own
|
|
341
|
+
* copy of the feedback.
|
|
342
|
+
* - this one is keyed on the SESSION and the NORMALIZED claim, and suppresses
|
|
343
|
+
* the events.jsonl WRITE. The ledger is a record of distinct findings; the
|
|
344
|
+
* same sentence re-asserted by a second agent of the same session is the
|
|
345
|
+
* same finding, and writing it twice is what produced a duplication factor
|
|
346
|
+
* of 16.4 (3,360 records over 205 distinct `claim_text` values, measured
|
|
347
|
+
* 2026-09-06 in `extern/aiat-barrierefrei-engine`).
|
|
348
|
+
*
|
|
349
|
+
* Returns null without a session id — a claim that cannot be bound to a session
|
|
350
|
+
* must not collide with an unrelated one under a global "unknown" key, so it is
|
|
351
|
+
* always written (the pre-#1198 behaviour, preserved for that case only).
|
|
352
|
+
*
|
|
353
|
+
* NAMED CEILING (BV-004): one zero-byte tmp file per distinct claim per
|
|
354
|
+
* session, never cleaned up by this hook — the same shape (and the same
|
|
355
|
+
* unbounded growth) `dedupSentinelPath` above has carried since #567, relying
|
|
356
|
+
* on the OS's tmpdir reaping. Fine at the measured rate: 158 distinct claims
|
|
357
|
+
* across a repo's worst 90 days. REVISIT TRIGGER: a repo whose distinct-claim
|
|
358
|
+
* count per session reaches the hundreds — then this belongs in a single
|
|
359
|
+
* per-session state file rather than one inode per claim.
|
|
360
|
+
*
|
|
361
|
+
* @param {object} opts
|
|
362
|
+
* @param {string} opts.projectRoot
|
|
363
|
+
* @param {string|null} opts.sessionId
|
|
364
|
+
* @param {string} opts.normalizedClaim — `normalizeClaim()` output
|
|
365
|
+
* @returns {string|null}
|
|
366
|
+
*/
|
|
367
|
+
function claimLedgerSentinelPath({ projectRoot, sessionId, normalizedClaim }) {
|
|
368
|
+
if (typeof sessionId !== 'string' || !sessionId.trim()) return null;
|
|
369
|
+
if (typeof normalizedClaim !== 'string' || !normalizedClaim) return null;
|
|
370
|
+
|
|
371
|
+
const claimHash = createHash('sha256').update(normalizedClaim).digest('hex').slice(0, 16);
|
|
372
|
+
return path.join(
|
|
373
|
+
tmpdir(),
|
|
374
|
+
`psa006-claim-${projectRootHash(projectRoot)}-${safeSentinelComponent(sessionId)}-${claimHash}.lock`
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
|
|
660
378
|
// ---------------------------------------------------------------------------
|
|
661
379
|
// main
|
|
662
380
|
// ---------------------------------------------------------------------------
|
|
@@ -678,10 +396,8 @@ async function main() {
|
|
|
678
396
|
const { violations, undatedVerified } = findViolations(text);
|
|
679
397
|
if (violations.length === 0) return;
|
|
680
398
|
|
|
681
|
-
const
|
|
682
|
-
|
|
683
|
-
(await readAgentTypeFromMeta(sidecar.meta));
|
|
684
|
-
const agent = agentForDedup ?? 'unknown';
|
|
399
|
+
const attribution = await resolveAgentAttribution(input, sidecar.meta);
|
|
400
|
+
const agent = attribution.agent;
|
|
685
401
|
// session_id precedence: parent_session_id first, mirroring the sibling hook
|
|
686
402
|
// hooks/subagent-telemetry.mjs (firstNonEmptyString(['parent_session_id',
|
|
687
403
|
// 'session_id'])). W2-review LOW finding (#567) — the prior `session_id ||
|
|
@@ -694,23 +410,55 @@ async function main() {
|
|
|
694
410
|
// globally. Keyed on agentId (not agent TYPE, #1198 FIX 1) plus the first
|
|
695
411
|
// violation's claim text so distinct real agents and distinct claims never
|
|
696
412
|
// share a sentinel.
|
|
413
|
+
const projectRoot = getProjectDir();
|
|
697
414
|
const sentinel = dedupSentinelPath({
|
|
698
|
-
projectRoot
|
|
415
|
+
projectRoot,
|
|
699
416
|
sessionId,
|
|
700
417
|
agentId,
|
|
701
|
-
claimText: violations[0],
|
|
418
|
+
claimText: violations[0].claim,
|
|
702
419
|
});
|
|
703
420
|
|
|
421
|
+
// Ledger write, ONE record per distinct normalized claim (#1198). Two
|
|
422
|
+
// levels, because the duplication had two sources: `findViolations()` already
|
|
423
|
+
// collapsed the repeats INSIDE this transcript tail into an `occurrences`
|
|
424
|
+
// count, and the session sentinel below drops a claim this session has
|
|
425
|
+
// already recorded from an earlier SubagentStop.
|
|
704
426
|
const filePath = eventsFilePath();
|
|
705
|
-
|
|
427
|
+
let written = 0;
|
|
428
|
+
for (const violation of violations) {
|
|
429
|
+
const claimSentinel = claimLedgerSentinelPath({
|
|
430
|
+
projectRoot,
|
|
431
|
+
sessionId,
|
|
432
|
+
normalizedClaim: violation.normalized,
|
|
433
|
+
});
|
|
434
|
+
if (claimSentinel !== null) {
|
|
435
|
+
let seen = false;
|
|
436
|
+
try {
|
|
437
|
+
await fs.writeFile(claimSentinel, '', { flag: 'wx' });
|
|
438
|
+
} catch (err) {
|
|
439
|
+
// EEXIST = this session already recorded this claim. Any OTHER
|
|
440
|
+
// filesystem error must NOT suppress the record: losing a finding to a
|
|
441
|
+
// full disk or a read-only tmpdir is the worse failure.
|
|
442
|
+
seen = err && err.code === 'EEXIST';
|
|
443
|
+
}
|
|
444
|
+
if (seen) continue;
|
|
445
|
+
}
|
|
706
446
|
await appendJsonl(filePath, {
|
|
707
447
|
event: 'discovery_validator_violation',
|
|
708
448
|
timestamp: new Date().toISOString(),
|
|
709
449
|
agent,
|
|
450
|
+
// Always present, even (especially) on 'none': a bare "unknown" is
|
|
451
|
+
// indistinguishable from a real agent type of that name, and says
|
|
452
|
+
// nothing about WHY resolution failed.
|
|
453
|
+
agent_source: attribution.source,
|
|
454
|
+
...(attribution.description !== null ? { agent_description: attribution.description } : {}),
|
|
455
|
+
...(attribution.source === 'none' ? { payload_keys: attribution.payloadKeys } : {}),
|
|
710
456
|
...(agentId !== null ? { agent_id: agentId } : {}),
|
|
711
457
|
...(sessionId !== null ? { session_id: sessionId } : {}),
|
|
712
|
-
claim_text: claim,
|
|
458
|
+
claim_text: violation.claim,
|
|
459
|
+
occurrences: violation.occurrences,
|
|
713
460
|
});
|
|
461
|
+
written++;
|
|
714
462
|
}
|
|
715
463
|
|
|
716
464
|
// Advisory only (#908 item 4) — never promoted to a violation in v1.
|
|
@@ -718,10 +466,18 @@ async function main() {
|
|
|
718
466
|
? ` ${undatedVerified} verified claim(s) carry no measurement timestamp (advisory).`
|
|
719
467
|
: '';
|
|
720
468
|
|
|
469
|
+
// `written < violations.length` means this session had already recorded the
|
|
470
|
+
// difference from an earlier SubagentStop — say so rather than let the WARN
|
|
471
|
+
// count and the ledger count disagree with no explanation (#1198).
|
|
472
|
+
const suppressedNote = written < violations.length
|
|
473
|
+
? ` ${violations.length - written} already recorded earlier in this session.`
|
|
474
|
+
: '';
|
|
475
|
+
|
|
721
476
|
const warnText =
|
|
722
|
-
`⚠ PSA-006: ${violations.length} repo-state/distributional claim(s) from agent
|
|
723
|
-
`lack an adjacent measurement transcript
|
|
724
|
-
|
|
477
|
+
`⚠ PSA-006: ${violations.length} distinct repo-state/distributional claim(s) from agent ` +
|
|
478
|
+
`"${agent}" (source: ${attribution.source}) lack an adjacent measurement transcript ` +
|
|
479
|
+
`(grep/rg/find/git/wc/jq/ls/node/npm) (non-blocking).` +
|
|
480
|
+
`${suppressedNote}${undatedNote} ` +
|
|
725
481
|
`See .claude/rules/parallel-sessions.md § PSA-006.`;
|
|
726
482
|
process.stderr.write(warnText + '\n');
|
|
727
483
|
|