session-orchestrator 3.23.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/architecture/SKILL.md +18 -0
- package/.agents/skills/autopilot/SKILL.md +17 -0
- package/.agents/skills/bootstrap/SKILL.md +20 -0
- package/.agents/skills/brainstorm/SKILL.md +22 -0
- package/.agents/skills/claude-md-drift-check/SKILL.md +15 -0
- package/.agents/skills/convergence-monitoring/SKILL.md +22 -0
- package/.agents/skills/debug/SKILL.md +22 -0
- package/.agents/skills/discovery/SKILL.md +20 -0
- package/.agents/skills/dispatcher/SKILL.md +15 -0
- package/.agents/skills/docs-orchestrator/SKILL.md +18 -0
- package/.agents/skills/ecosystem-health/SKILL.md +20 -0
- package/.agents/skills/eli5/SKILL.md +20 -0
- package/.agents/skills/eval/SKILL.md +21 -0
- package/.agents/skills/evolve/SKILL.md +21 -0
- package/.agents/skills/frontmatter-guard/SKILL.md +15 -0
- package/.agents/skills/gitlab-ops/SKILL.md +20 -0
- package/.agents/skills/gitlab-portfolio/SKILL.md +15 -0
- package/.agents/skills/grill/SKILL.md +22 -0
- package/.agents/skills/hook-development/SKILL.md +15 -0
- package/.agents/skills/mcp-builder/SKILL.md +15 -0
- package/.agents/skills/memory-cleanup/SKILL.md +21 -0
- package/.agents/skills/mode-selector/SKILL.md +17 -0
- package/.agents/skills/npm-publish/SKILL.md +16 -0
- package/.agents/skills/peekaboo-driver/SKILL.md +18 -0
- package/.agents/skills/persona-panel/SKILL.md +17 -0
- package/.agents/skills/plan/SKILL.md +20 -0
- package/.agents/skills/playwright-driver/SKILL.md +20 -0
- package/.agents/skills/quality-gates/SKILL.md +20 -0
- package/.agents/skills/reconcile/SKILL.md +21 -0
- package/.agents/skills/remote-offload/SKILL.md +20 -0
- package/.agents/skills/repo-audit/SKILL.md +16 -0
- package/.agents/skills/session-end/SKILL.md +20 -0
- package/.agents/skills/session-plan/SKILL.md +20 -0
- package/.agents/skills/session-start/SKILL.md +20 -0
- package/.agents/skills/spinout/SKILL.md +16 -0
- package/.agents/skills/sunset-review/SKILL.md +16 -0
- package/.agents/skills/test-runner/SKILL.md +20 -0
- package/.agents/skills/tmux-layout/SKILL.md +21 -0
- package/.agents/skills/using-orchestrator/SKILL.md +17 -0
- package/.agents/skills/vault-mirror/SKILL.md +15 -0
- package/.agents/skills/vault-sync/SKILL.md +15 -0
- package/.agents/skills/wave-executor/SKILL.md +20 -0
- package/.agents/skills/write-executable-plan/SKILL.md +22 -0
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.cursor/commands/autopilot.md +2 -2
- package/.cursor/commands/bootstrap.md +1 -1
- package/.cursor/commands/brainstorm.md +1 -1
- package/.cursor/commands/debug.md +1 -1
- package/.cursor/commands/discovery.md +1 -1
- package/.cursor/commands/dispatcher.md +2 -2
- package/.cursor/commands/eli5.md +2 -2
- package/.cursor/commands/eval.md +2 -2
- package/.cursor/commands/evolve.md +1 -1
- package/.cursor/commands/go.md +1 -1
- package/.cursor/commands/grill.md +2 -2
- package/.cursor/commands/memory-cleanup.md +2 -2
- package/.cursor/commands/persona-panel.md +1 -1
- package/.cursor/commands/plan.md +1 -1
- package/.cursor/commands/portfolio.md +1 -1
- package/.cursor/commands/reconcile.md +2 -2
- package/.cursor/commands/release.md +2 -2
- package/.cursor/commands/session.md +2 -2
- package/.cursor/commands/spinout.md +2 -2
- package/.cursor/commands/sunset-review.md +2 -2
- package/.cursor/commands/templates-ack.md +2 -2
- package/.cursor/commands/test.md +2 -2
- package/.cursor/skills/brainstorm/SKILL.md +1 -1
- package/.cursor/skills/eval/SKILL.md +1 -1
- package/.cursor/skills/quality-gates/SKILL.md +1 -1
- package/.cursor/skills/remote-offload/SKILL.md +13 -0
- package/.orchestrator/policy/blocked-commands.json +121 -0
- package/.orchestrator/policy/ecosystem.schema.json +66 -0
- package/.orchestrator/policy/quality-gates.example.json +16 -0
- package/.orchestrator/policy/quality-gates.schema.json +38 -0
- package/.orchestrator/policy/templates-policy.json +27 -0
- package/.orchestrator/policy/test-profiles.json +47 -0
- package/AGENTS.md +225 -0
- package/CHANGELOG.md +1401 -0
- package/NOTICE +11 -6
- package/README.md +127 -92
- package/agents/db-specialist.md +0 -1
- package/agents/eval-judge.md +1 -1
- package/agents/skill-applied-judge.md +1 -1
- package/assets/wave-lifecycle.svg +98 -0
- package/commands/release.md +6 -3
- package/commands/session.md +18 -3
- package/docs/README.md +4 -0
- package/{agents/AGENTS.md → docs/agent-authoring.md} +19 -26
- package/docs/baseline.md +67 -0
- package/docs/ci-setup.md +249 -48
- package/docs/codex-setup.md +66 -22
- package/docs/components.md +37 -16
- package/docs/cursor-setup.md +6 -2
- package/docs/events-schema.md +51 -10
- package/docs/instruction-delivery.md +62 -0
- package/{agents/memory-proposal-collector.md → docs/memory-proposal-flow.md} +1 -8
- package/docs/migration-v4.md +341 -0
- package/docs/pi-setup.md +6 -1
- package/docs/plugin-architecture-v3.md +1 -1
- package/docs/rule-authoring.md +85 -19
- package/docs/scope-collision-guard.md +8 -8
- package/docs/session-config-reference.md +120 -61
- package/docs/session-config-template.md +40 -33
- package/docs/telemetry/telemetry-claims.md +11 -10
- package/docs/telemetry.md +187 -4
- package/docs/vault-docs-architecture.md +50 -11
- package/hooks/_lib/atomic-json.mjs +111 -0
- package/hooks/_lib/hook-import-set.json +1487 -0
- package/hooks/_lib/subagent-paths.mjs +143 -0
- package/hooks/_lib/subagent-transcript.mjs +562 -0
- package/hooks/config-protection.mjs +2 -2
- package/hooks/cwd-change-restore.mjs +11 -31
- package/hooks/enforce-commands.mjs +69 -0
- package/hooks/enforce-scope.mjs +35 -6
- package/hooks/hooks-codex.json +1 -1
- package/hooks/hooks-cursor.json +10 -0
- package/hooks/hooks-pi.json +5 -0
- package/hooks/hooks.json +6 -1
- package/hooks/loop-guard.mjs +3 -3
- package/hooks/on-session-end.mjs +280 -14
- package/hooks/on-session-start.mjs +153 -4
- package/hooks/on-stop.mjs +371 -17
- package/hooks/operator-steer.mjs +2 -2
- package/hooks/post-bash-write-verify.mjs +189 -4
- package/hooks/post-edit-import-probe.mjs +344 -0
- package/hooks/post-subagent-discovery-validator.mjs +278 -392
- package/hooks/post-tool-batch-wave-signal.mjs +272 -44
- package/hooks/post-tool-failure-corrective-context.mjs +11 -34
- package/hooks/post-tooluse-frontend-slop.mjs +3 -3
- package/hooks/pre-bash-destructive-guard.mjs +39 -13
- package/hooks/pre-bash-memory-propose-audit.mjs +13 -7
- package/hooks/skill-invocation-telemetry.mjs +17 -5
- package/hooks/subagent-telemetry.mjs +24 -30
- package/monitors/monitors.json +3 -3
- package/package.json +9 -1
- package/pi/prompts/session.md +2 -2
- package/plugin.json +27 -0
- package/scripts/autopilot.mjs +26 -12
- package/scripts/backfill-abandoned-sessions.mjs +130 -15
- package/scripts/backfill-learnings-from-vault.mjs +9 -3
- package/scripts/dialectic-deriver.mjs +73 -8
- package/scripts/emit-event.mjs +10 -2
- package/scripts/export-hw-learnings.mjs +113 -1
- package/scripts/generate-agents-skills.mjs +378 -0
- package/scripts/generate-cursor-adapter.mjs +45 -8
- package/scripts/generate-hook-import-set.mjs +249 -0
- package/scripts/lib/agent-status.mjs +13 -2
- package/scripts/lib/auq/parse.mjs +5 -29
- package/scripts/lib/auto-dialectic.mjs +68 -0
- package/scripts/lib/auto-dream.mjs +38 -36
- package/scripts/lib/autonomy/suitability.mjs +6 -0
- package/scripts/lib/autopilot/loop.mjs +2 -2
- package/scripts/lib/autopilot/worktree-pipeline.mjs +82 -6
- package/scripts/lib/build-live-signals.mjs +25 -22
- package/scripts/lib/ci-status-banner.mjs +220 -75
- package/scripts/lib/codex/plugin-contract.mjs +82 -6
- package/scripts/lib/cold-start-detector.mjs +23 -14
- package/scripts/lib/config/auto-dream.mjs +2 -1
- package/scripts/lib/config/block-header.mjs +63 -0
- package/scripts/lib/config/block-preprocess.mjs +177 -0
- package/scripts/lib/config/broken-window.mjs +2 -1
- package/scripts/lib/config/cold-start.mjs +2 -1
- package/scripts/lib/config/config-protection.mjs +22 -2
- package/scripts/lib/config/context-coverage.mjs +2 -1
- package/scripts/lib/config/cross-repo.mjs +2 -1
- package/scripts/lib/config/custom-phases.mjs +2 -1
- package/scripts/lib/config/dialectic.mjs +2 -1
- package/scripts/lib/config/discovery-validator.mjs +9 -3
- package/scripts/lib/config/dispatcher-autonomy-capture.mjs +24 -1
- package/scripts/lib/config/dispatcher-autonomy.mjs +2 -1
- package/scripts/lib/config/docs-orchestrator.mjs +2 -1
- package/scripts/lib/config/docs-staleness.mjs +2 -1
- package/scripts/lib/config/drift-check.mjs +2 -1
- package/scripts/lib/config/eval.mjs +2 -1
- package/scripts/lib/config/events-rotation.mjs +2 -1
- package/scripts/lib/config/evolve.mjs +8 -2
- package/scripts/lib/config/frontend-slop-hook.mjs +7 -3
- package/scripts/lib/config/gitlab-portfolio.mjs +2 -1
- package/scripts/lib/config/handover-gate.mjs +2 -1
- package/scripts/lib/config/health-endpoints.mjs +388 -0
- package/scripts/lib/config/issue-budget.mjs +2 -1
- package/scripts/lib/config/loop-guard.mjs +2 -1
- package/scripts/lib/config/memory.mjs +2 -1
- package/scripts/lib/config/moc-staleness.mjs +2 -1
- package/scripts/lib/config/persona-gate-wave.mjs +2 -1
- package/scripts/lib/config/private-config-dir.mjs +67 -0
- package/scripts/lib/config/reconcile.mjs +2 -1
- package/scripts/lib/config/remote-hosts.mjs +234 -0
- package/scripts/lib/config/section-extractor.mjs +7 -1
- package/scripts/lib/config/skill-evolution.mjs +2 -1
- package/scripts/lib/config/slopcheck.mjs +2 -1
- package/scripts/lib/config/state-md-lock.mjs +2 -1
- package/scripts/lib/config/templates-first.mjs +2 -1
- package/scripts/lib/config/test.mjs +2 -1
- package/scripts/lib/config/vault-integration.mjs +7 -1
- package/scripts/lib/config/vault-mirror-quality.mjs +2 -1
- package/scripts/lib/config/vault-staleness.mjs +2 -1
- package/scripts/lib/config/vault-sync.mjs +2 -1
- package/scripts/lib/config/verification-auto-fix.mjs +2 -1
- package/scripts/lib/config/wave-reviewers.mjs +2 -1
- package/scripts/lib/config/worktree-orphans.mjs +2 -1
- package/scripts/lib/config.mjs +31 -3
- package/scripts/lib/convergence-monitor.mjs +82 -16
- package/scripts/lib/dispatcher/enumerate.mjs +2 -17
- package/scripts/lib/dispatcher/rank.mjs +124 -48
- package/scripts/lib/ecosystem-health.mjs +16 -2
- package/scripts/lib/eval/engine.mjs +9 -1
- package/scripts/lib/eval/session-resolve.mjs +23 -4
- package/scripts/lib/events-schema.mjs +48 -0
- package/scripts/lib/events.mjs +256 -7
- package/scripts/lib/evolve/autonomy-verdict.mjs +9 -4
- package/scripts/lib/evolve/autopilot-effectiveness.mjs +18 -1
- package/scripts/lib/frontmatter-guard.mjs +131 -13
- package/scripts/lib/gates/gate-full.mjs +26 -0
- package/scripts/lib/gates/gate-helpers.mjs +76 -0
- package/scripts/lib/gitlab-portfolio/cli.mjs +3 -15
- package/scripts/lib/hardware-pattern-detector.mjs +18 -1
- package/scripts/lib/harness-audit/categories/category1.mjs +17 -6
- package/scripts/lib/harness-audit/categories/category4.mjs +31 -11
- package/scripts/lib/host-identity.mjs +50 -11
- package/scripts/lib/instruction-budget-guard.mjs +171 -5
- package/scripts/lib/learnings/evolve-telemetry.mjs +178 -0
- package/scripts/lib/learnings/io.mjs +60 -6
- package/scripts/lib/memory-banner.mjs +20 -8
- package/scripts/lib/memory-proposals/store.mjs +30 -22
- package/scripts/lib/owner-config-banner.mjs +43 -6
- package/scripts/lib/owner-config-loader.mjs +21 -10
- package/scripts/lib/owner-interview.mjs +3 -3
- package/scripts/lib/owner-yaml.mjs +207 -14
- package/scripts/lib/peer-discovery.mjs +20 -2
- package/scripts/lib/platform.mjs +108 -15
- package/scripts/lib/plugin-update-banner.mjs +406 -0
- package/scripts/lib/project-hygiene.mjs +38 -2
- package/scripts/lib/qg-command-drift-banner.mjs +50 -12
- package/scripts/lib/quality-gate.mjs +133 -44
- package/scripts/lib/reconcile/emitter.mjs +68 -6
- package/scripts/lib/reconcile/engine.mjs +249 -9
- package/scripts/lib/reconcile/idempotency.mjs +37 -4
- package/scripts/lib/reconcile/writer.mjs +40 -18
- package/scripts/lib/scope-gate.mjs +36 -0
- package/scripts/lib/session-close-backfill.mjs +125 -18
- package/scripts/lib/session-discovery.mjs +57 -3
- package/scripts/lib/session-end/phase-skip.mjs +2 -2
- package/scripts/lib/session-id.mjs +12 -23
- package/scripts/lib/session-identity/own-session.mjs +187 -11
- package/scripts/lib/session-lock-shape.mjs +43 -0
- package/scripts/lib/session-lock.mjs +5 -10
- package/scripts/lib/session-registry.mjs +25 -9
- package/scripts/lib/session-schema/constants.mjs +36 -2
- package/scripts/lib/session-schema/validator.mjs +38 -4
- package/scripts/lib/session-start-probes.mjs +18 -1
- package/scripts/lib/session-transition.mjs +1 -1
- package/scripts/lib/sessions-canonical.mjs +446 -0
- package/scripts/lib/sessions-staleness-banner.mjs +18 -11
- package/scripts/lib/skill-health/join.mjs +17 -4
- package/scripts/lib/state-md.mjs +78 -0
- package/scripts/lib/sunset/walker.mjs +6 -0
- package/scripts/lib/telemetry/schema.mjs +255 -17
- package/scripts/lib/telemetry/sync.mjs +417 -24
- package/scripts/lib/tmux-layout/telemetry.mjs +14 -2
- package/scripts/lib/validate/check-agents-skills.mjs +327 -0
- package/scripts/lib/validate/check-agents.mjs +3 -3
- package/scripts/lib/validate/check-cursor-adapter.mjs +234 -72
- package/scripts/lib/validate/check-doc-cli-commands.mjs +9 -33
- package/scripts/lib/validate/check-hooks-emit-event-guard.mjs +370 -0
- package/scripts/lib/validate/check-hooks-symmetry.mjs +45 -16
- package/scripts/lib/validate/check-owner-leakage.mjs +281 -20
- package/scripts/lib/validate/check-skill-links.mjs +163 -0
- package/scripts/lib/validate/check-skill-script-paths.mjs +455 -0
- package/scripts/lib/validate/check-untracked-test-deps.mjs +10 -0
- package/scripts/lib/validate/check-unwired-features.mjs +0 -9
- package/scripts/lib/validate/check-validator-registration.mjs +254 -0
- package/scripts/lib/validate/check-vcs-repo-flag.mjs +6 -28
- package/scripts/lib/validate/enumerate-repo-files.mjs +317 -0
- package/scripts/lib/validate/markdown-fences.mjs +196 -0
- package/scripts/lib/vault-backfill/template.mjs +63 -6
- package/scripts/lib/vault-mirror/process.mjs +165 -42
- package/scripts/lib/vault-mirror/telemetry.mjs +2 -2
- package/scripts/lib/vault-status/board-lock.mjs +185 -0
- package/scripts/lib/vault-status/board-writer.mjs +174 -135
- package/scripts/lib/vault-status/narrative-mirror.mjs +129 -37
- package/scripts/lib/wave-executor/dispatch-common.mjs +164 -0
- package/scripts/lib/wave-executor/foreign-dispatch.mjs +7 -142
- package/scripts/lib/wave-executor/remote-dispatch.mjs +502 -0
- package/scripts/lib/wave-resource-gate.mjs +133 -7
- package/scripts/lib/wave-sizing.mjs +4 -1
- package/scripts/lib/wave-transcript-tail.mjs +142 -8
- package/scripts/materialize-wave-scope.mjs +32 -9
- package/scripts/memory-propose.mjs +146 -8
- package/scripts/migrate-cold-start-seed.mjs +4 -1
- package/scripts/parse-config.mjs +60 -3
- package/scripts/promote-vault-strict.mjs +4 -15
- package/scripts/release.mjs +337 -29
- package/scripts/repair-invalid-sessions.mjs +3 -3
- package/scripts/run-quality-gate.mjs +128 -11
- package/scripts/site-numbers.mjs +36 -4
- package/scripts/sweep-expired-learnings.mjs +90 -0
- package/scripts/sync-vault-schema.mjs +3 -1
- package/scripts/telemetry.mjs +2 -2
- package/scripts/validate-plugin.mjs +187 -0
- package/scripts/validate-wave-scope.mjs +28 -8
- package/scripts/vault-consolidate.mjs +3 -11
- package/scripts/vault-integration-watcher.mjs +2 -4
- package/scripts/vault-mirror.mjs +111 -26
- package/scripts/wave-scope-binding.mjs +215 -0
- package/skills/_shared/instruction-file-resolution.md +10 -0
- package/skills/_shared/parallel-aware-auq.md +31 -2
- package/skills/_shared/parallel-aware-preamble.md +18 -4
- package/skills/_shared/platform-tools.md +1 -1
- package/skills/_shared/state-ownership.md +1 -1
- package/skills/architecture/SKILL.md +7 -5
- package/skills/{domain-model/SKILL.md → architecture/references/domain-model.md} +9 -9
- package/skills/autopilot/SKILL.md +4 -18
- package/skills/claude-md-drift-check/SKILL.md +5 -1
- package/skills/claude-md-drift-check/checker.mjs +62 -2
- package/skills/convergence-monitoring/SIGNALS.md +55 -0
- package/skills/discovery/probes/vault-staleness.mjs +37 -13
- package/skills/discovery/probes-arch.md +20 -18
- package/skills/dispatcher/SKILL.md +3 -2
- package/skills/ecosystem-health/SKILL.md +4 -1
- package/skills/ecosystem-health/wizard.md +5 -0
- package/skills/evolve/SKILL.md +87 -11
- package/skills/frontmatter-guard/SKILL.md +11 -5
- package/skills/npm-publish/SKILL.md +1 -1
- package/skills/reconcile/SKILL.md +38 -2
- package/skills/remote-offload/SKILL.md +89 -0
- package/skills/session-end/SKILL.md +18 -905
- package/skills/session-end/phase-3-6-tail.md +19 -9
- package/skills/session-end/plan-verification.md +221 -155
- package/skills/session-end/references/phase-2-quality-gate.md +93 -0
- package/skills/session-end/references/phase-3-documentation-updates.md +229 -0
- package/skills/session-end/references/phase-4a-worktree-cleanup.md +120 -0
- package/skills/session-end/references/phase-4b-worktree-orphan-sweep.md +58 -0
- package/skills/session-end/references/phase-5-issue-cleanup.md +104 -0
- package/skills/session-end/references/session-summary-template.md +62 -0
- package/skills/session-plan/SKILL.md +49 -0
- package/skills/session-start/SKILL.md +41 -900
- package/skills/session-start/phase-8-5-express-path.md +1 -1
- package/skills/session-start/references/phase-1-1-dispatcher-autonomy-capture.md +55 -0
- package/skills/session-start/references/phase-1-2-session-lock.md +140 -0
- package/skills/session-start/references/phase-1-5-session-continuity.md +254 -0
- package/skills/session-start/references/phase-1-7-vault-status-board.md +53 -0
- package/skills/session-start/references/phase-2-7-portfolio-snapshot.md +75 -0
- package/skills/session-start/references/phase-4-ssot-environment-check.md +155 -0
- package/skills/session-start/references/phase-6-5-forced-reads.md +75 -0
- package/skills/session-start/references/phase-6-6-project-intelligence.md +81 -0
- package/skills/session-start/references/phase-6-7-memory-banner-telemetry-consent.md +103 -0
- package/skills/vault-sync/validator.mjs +21 -27
- package/skills/wave-executor/SKILL.md +16 -2
- package/skills/wave-executor/references/wave-loop-dispatch.md +612 -0
- package/skills/wave-executor/references/wave-loop-review.md +570 -0
- package/skills/wave-executor/references/wave-loop-scope-manifest.md +162 -0
- package/skills/wave-executor/wave-loop.md +14 -1271
- package/templates/_shared/journey-manifest.md +10 -6
- package/.cursor/commands/autopilot-multi.md +0 -14
- package/.cursor/commands/contract-version-bump.md +0 -14
- package/.cursor/commands/journey-audit.md +0 -14
- package/.cursor/skills/contract-version-bump/SKILL.md +0 -12
- package/.cursor/skills/daily/SKILL.md +0 -12
- package/.cursor/skills/domain-model/SKILL.md +0 -13
- package/.cursor/skills/journey-audit/SKILL.md +0 -13
- package/.cursor/skills/skill-creator/SKILL.md +0 -13
- package/.cursor/skills/ubiquitous-language/SKILL.md +0 -13
- package/commands/autopilot-multi.md +0 -74
- package/commands/contract-version-bump.md +0 -28
- package/commands/journey-audit.md +0 -43
- package/pi/prompts/autopilot-multi.md +0 -12
- package/pi/prompts/contract-version-bump.md +0 -12
- package/pi/prompts/journey-audit.md +0 -12
- package/scripts/autopilot-multi.mjs +0 -885
- package/scripts/backfill-learnings-expires.mjs +0 -196
- package/scripts/backfill-learnings.mjs +0 -203
- package/scripts/fleet-instruction-scan.mjs +0 -141
- package/scripts/lib/autopilot/dep-graph.mjs +0 -417
- package/scripts/lib/autopilot/multi-killswitch.mjs +0 -184
- package/scripts/lib/webhook-url.mjs +0 -105
- package/scripts/lifecycle-sim-v6.mjs +0 -347
- package/scripts/migrate-learnings-jsonl.mjs +0 -189
- package/scripts/migrate-subagents-jsonl.mjs +0 -196
- package/scripts/upload-social-preview.mjs +0 -316
- package/skills/_shared/model-selection.md +0 -64
- package/skills/contract-version-bump/SKILL.md +0 -219
- package/skills/daily/SKILL.md +0 -222
- package/skills/daily/generate.sh +0 -92
- package/skills/daily/templates/daily.md.tpl +0 -36
- package/skills/journey-audit/SKILL.md +0 -269
- package/skills/skill-creator/SKILL.md +0 -168
- package/skills/ubiquitous-language/SKILL.md +0 -97
- package/skills/vault-sync/package-lock.json +0 -40
- /package/skills/{domain-model → architecture/references}/ADR-FORMAT.md +0 -0
- /package/skills/{domain-model → architecture/references}/CONTEXT-FORMAT.md +0 -0
|
@@ -11,24 +11,39 @@
|
|
|
11
11
|
* 1. shouldRunHook('post-subagent-discovery-validator') gate — exit 0 when disabled.
|
|
12
12
|
* 2. Read JSON payload from stdin; require hook_event_name === 'SubagentStop'.
|
|
13
13
|
* 3. Read `discovery-validator.enabled` from CLAUDE.md/AGENTS.md Session Config.
|
|
14
|
-
* Default OFF — exit 0 immediately unless explicitly enabled.
|
|
15
|
-
* 4.
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
14
|
+
* Default OFF (opt-in) — exit 0 immediately unless explicitly enabled.
|
|
15
|
+
* 4. Resolve the STOPPING SUBAGENT's OWN transcript (never the parent):
|
|
16
|
+
* `input.agent_transcript_path` when the harness sends it, else
|
|
17
|
+
* `<dir(transcript_path)>/<base>/subagents/agent-<agent_id>.jsonl`. Scan its
|
|
18
|
+
* TAIL (last ~8 `type:"assistant"` records), concat text blocks. When no
|
|
19
|
+
* `agent_id` is derivable (or the file is absent) the hook exits 0 and
|
|
20
|
+
* records NOTHING — see the scope note below.
|
|
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.
|
|
29
33
|
*
|
|
30
|
-
* Why read
|
|
31
|
-
* field
|
|
34
|
+
* Why read a transcript at all: the SubagentStop stdin payload has NO
|
|
35
|
+
* output_text field — the agent's text only exists on disk.
|
|
36
|
+
*
|
|
37
|
+
* WHICH transcript (#1191, the root cause behind the fleet false-positive
|
|
38
|
+
* flood): `input.transcript_path` is the PARENT/MAIN session transcript, not
|
|
39
|
+
* the subagent's. Scanning it flagged the COORDINATOR's own prose — wave plans,
|
|
40
|
+
* TL;DRs, complexity scores. Measured 2026-09-02 on a seeded random sample of
|
|
41
|
+
* 60 violations: 100% coordinator text, scope-adjusted precision 0%, and
|
|
42
|
+
* `agent` was `"unknown"` in 90.8% of 1,541 vault events. The hook therefore
|
|
43
|
+
* reads `<transcriptDir>/<session>/subagents/agent-<agent_id>.jsonl` (the same
|
|
44
|
+
* layout `hooks/subagent-telemetry.mjs` and `scripts/lib/wave-transcript-tail.mjs`
|
|
45
|
+
* read) and NEVER falls back to the parent path: a scan of the wrong transcript
|
|
46
|
+
* is worse than no scan.
|
|
32
47
|
*
|
|
33
48
|
* Output channels — THREE writes, TWO different recipients:
|
|
34
49
|
* - `discovery_validator_violation` in .orchestrator/metrics/events.jsonl,
|
|
@@ -55,194 +70,13 @@ import { promises as fs } from 'node:fs';
|
|
|
55
70
|
import { tmpdir } from 'node:os';
|
|
56
71
|
import path from 'node:path';
|
|
57
72
|
|
|
73
|
+
import { resolveSubagentSidecar } from './_lib/subagent-paths.mjs';
|
|
74
|
+
import { findViolations, readTranscriptTail } from './_lib/subagent-transcript.mjs';
|
|
58
75
|
import { appendJsonl } from '../scripts/lib/common.mjs';
|
|
59
76
|
import { eventsFilePath } from '../scripts/lib/events.mjs';
|
|
60
|
-
import {
|
|
77
|
+
import { getProjectDir } from '../scripts/lib/platform.mjs';
|
|
61
78
|
import { _parseDiscoveryValidator } from '../scripts/lib/config/discovery-validator.mjs';
|
|
62
79
|
|
|
63
|
-
// ---------------------------------------------------------------------------
|
|
64
|
-
// Constants
|
|
65
|
-
// ---------------------------------------------------------------------------
|
|
66
|
-
|
|
67
|
-
/** Number of trailing assistant records to scan. */
|
|
68
|
-
const TAIL_RECORDS = 8;
|
|
69
|
-
/** Proximity window (in lines) for an adjacent grep transcript. */
|
|
70
|
-
const GREP_PROXIMITY_LINES = 5;
|
|
71
|
-
/** Max characters of claim text persisted to the event record. */
|
|
72
|
-
const CLAIM_TEXT_MAX = 200;
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Code-distribution context nouns (singular or plural). A distributional claim
|
|
76
|
-
* is only a PSA-006 *code* claim when one of these appears near the trigger —
|
|
77
|
-
* this is what separates "4 of 4 callers" (a real claim) from "Turn 3 of 25"
|
|
78
|
-
* (a turn counter) and "every caller imports X" from "every developer should
|
|
79
|
-
* test" (generic advice). Tightening per W2-review LOW finding (#567).
|
|
80
|
-
*
|
|
81
|
-
* Single optional `\s?` only (call site / callsite) — no nested quantifiers,
|
|
82
|
-
* so the alternation stays linear-time / ReDoS-safe.
|
|
83
|
-
*/
|
|
84
|
-
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?)';
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Repo-STATE nouns (#908). The four documented #908 drift cases were counts of
|
|
88
|
-
* repository state, not of code locations: "14 commits", "92 learnings",
|
|
89
|
-
* "5 dirty files", "412 lines". None of them contains a CTX noun, so the
|
|
90
|
-
* original six patterns could not see them.
|
|
91
|
-
*
|
|
92
|
-
* Vocabulary taken from this repo's own artefacts (`.orchestrator/metrics/*.jsonl`
|
|
93
|
-
* record kinds, `.claude/rules/`, `skills/`, `agents/`, `hooks/`) rather than a
|
|
94
|
-
* generic English list — a noun that never names a countable repo artefact here
|
|
95
|
-
* only buys false positives.
|
|
96
|
-
*/
|
|
97
|
-
const STATE = '(?:commits?|learnings?|issues?|branches?|lines?|entries|records?|sessions?|rules?|skills?|probes?|waves?|proposals?|worktrees?)';
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* The noun class the BARE-CARDINAL pattern may use — deliberately a strict
|
|
101
|
-
* subset of STATE ∪ CTX, restricted to the artefact kinds the #908 drift was
|
|
102
|
-
* actually measured in (commit counts, learnings counts, open-issue counts,
|
|
103
|
-
* branch counts, line counts, dirty-file counts).
|
|
104
|
-
*
|
|
105
|
-
* Measured, not guessed: over 32 real agent-stop windows from this repo's own
|
|
106
|
-
* transcripts, admitting the full CTX ∪ STATE set fired 93 times (2.9 per
|
|
107
|
-
* stop — the "validator gets switched off" zone). Every noun below earns its
|
|
108
|
-
* place by naming one of the documented #908 facts; the ones that only cost
|
|
109
|
-
* false positives (`tests`, `references`, `matches`, `agents`, `files` without
|
|
110
|
-
* a state adjective) are excluded here and remain reachable through the six
|
|
111
|
-
* quantifier-triggered patterns above, which have a lexical anchor.
|
|
112
|
-
*
|
|
113
|
-
* `callers` (#918): PSA-006's own canonical noun — the rule text's worked
|
|
114
|
-
* examples ("4 of 4 callers", "100% of callers opt-in") all count callers, yet
|
|
115
|
-
* the cardinal pattern could not see a bare "14 callers". Re-measured with it
|
|
116
|
-
* admitted: 490 real SubagentStop transcripts (2026-07-31), +0 additional
|
|
117
|
-
* firings from the bare form — the noun is free on this corpus (all +3 delta
|
|
118
|
-
* firings came from the #918 slash pattern below).
|
|
119
|
-
*/
|
|
120
|
-
const CARDINAL_NOUN = '(?:commits?|learnings?|issues?|branches?|lines?|files?|callers?)';
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
* Wide noun class = code-distribution nouns ∪ repo-state nouns. Used by the six
|
|
124
|
-
* QUANTIFIER-triggered patterns ("N of M", "100% of", "all N", "no remaining",
|
|
125
|
-
* "none of") — each of those carries a strong lexical trigger, so widening the
|
|
126
|
-
* noun set there is low-risk.
|
|
127
|
-
*
|
|
128
|
-
* Deliberately NOT used by the `every <noun>` pattern: `every` has no numeric
|
|
129
|
-
* anchor, so `every commit must be signed` / `every rule is always-on` are
|
|
130
|
-
* ordinary prose, not measured claims. That pattern keeps the narrow CTX.
|
|
131
|
-
*/
|
|
132
|
-
const WIDE = `(?:${CTX}|${STATE})`;
|
|
133
|
-
|
|
134
|
-
/** Bounded same-line gap between a trigger and its context noun. */
|
|
135
|
-
const CTX_GAP = '[^\\n]{0,40}?';
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* Distributional-claim patterns (case-insensitive). A match is a PSA-006 claim
|
|
139
|
-
* that requires an adjacent measurement transcript.
|
|
140
|
-
*
|
|
141
|
-
* Each pattern requires a context noun within a small, bounded same-line window
|
|
142
|
-
* of the trigger. The `[^\n]{0,40}?` gap is a bounded lazy character class
|
|
143
|
-
* (linear-time — the ReDoS-safety the W2 reviewer verified is preserved). True
|
|
144
|
-
* claims ("4 of 4 callers opt-in", "every caller imports X", "no remaining
|
|
145
|
-
* references to Y") still flag; benign strings ("Turn 3 of 25 complete",
|
|
146
|
-
* "every developer should test", "100% of users love it") do not.
|
|
147
|
-
*/
|
|
148
|
-
const CLAIM_PATTERNS = [
|
|
149
|
-
new RegExp(`\\b\\d+ of \\d+\\b${CTX_GAP}\\b${WIDE}\\b`, 'i'),
|
|
150
|
-
new RegExp(`100% of\\b${CTX_GAP}\\b${WIDE}\\b`, 'i'),
|
|
151
|
-
new RegExp(`\\ball \\d+\\b${CTX_GAP}\\b${WIDE}\\b`, 'i'),
|
|
152
|
-
new RegExp(`no remaining\\b${CTX_GAP}\\b${WIDE}\\b`, 'i'),
|
|
153
|
-
new RegExp(`every ${CTX}\\b`, 'i'),
|
|
154
|
-
new RegExp(`none of\\b${CTX_GAP}\\b${WIDE}\\b`, 'i'),
|
|
155
|
-
];
|
|
156
|
-
|
|
157
|
-
/**
|
|
158
|
-
* Pattern 7 (#908) — the BARE CARDINAL repo-state fact. `14 commits` has no
|
|
159
|
-
* quantifier trigger at all, which is exactly why the #908 drift went unseen.
|
|
160
|
-
*
|
|
161
|
-
* A naive `\d+` would fire on every issue reference, version literal, date,
|
|
162
|
-
* line number and percentage in a normal report — and a validator that fires on
|
|
163
|
-
* every report gets switched off, which is strictly worse than no validator.
|
|
164
|
-
* Precision is therefore bought three ways:
|
|
165
|
-
*
|
|
166
|
-
* 1. TRIGGER: a digit run that is not glued to identifier punctuation.
|
|
167
|
-
* Lookbehind rejects `#906`, `v3`, `PSA-006`, `W2`, `foo.mjs:123`;
|
|
168
|
-
* lookahead rejects `3.17`, `70%`, `2026-07-29`, `12615/0`.
|
|
169
|
-
* 2. GAP: at most two intervening ADJECTIVE-like words, and never a
|
|
170
|
-
* preposition/article/copula. "5 dirty files" matches; "3 of 5 stars" and
|
|
171
|
-
* "2 sections below the rules" do not.
|
|
172
|
-
* 3. SCOPE: evaluated only on prose lines with inline-code spans masked out —
|
|
173
|
-
* fenced blocks are skipped entirely (see `findViolations`), because a
|
|
174
|
-
* number inside a fence is tool OUTPUT (the evidence itself), not an
|
|
175
|
-
* unverified assertion about it.
|
|
176
|
-
*
|
|
177
|
-
* All quantifiers are bounded ({1,9}, {0,2}) — linear-time, ReDoS-safe.
|
|
178
|
-
*/
|
|
179
|
-
const CARDINAL_TRIGGER = '(?<![\\w#$:/.-])\\d{1,9}(?![\\d.%:/-])';
|
|
180
|
-
const CARDINAL_STOPWORDS =
|
|
181
|
-
'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';
|
|
182
|
-
const CARDINAL_GAP = `(?:\\s+(?!(?:${CARDINAL_STOPWORDS})\\b)[A-Za-z][\\w-]*){0,2}`;
|
|
183
|
-
const CARDINAL_PATTERN = new RegExp(`${CARDINAL_TRIGGER}${CARDINAL_GAP}\\s+${CARDINAL_NOUN}\\b`, 'i');
|
|
184
|
-
|
|
185
|
-
/**
|
|
186
|
-
* Pattern 8 (#918) — the PSA-006 CANONICAL numerator/denominator slash form:
|
|
187
|
-
* "12/14 files", "4/4 callers". PSA-006 literally demands "Quote the numerator
|
|
188
|
-
* AND denominator", and `N/M` is the notation that demand produces — yet the
|
|
189
|
-
* cardinal trigger's lookahead `(?![\d.%:/-])` excludes the slash, making the
|
|
190
|
-
* rule's own canon form structurally invisible (#918).
|
|
191
|
-
*
|
|
192
|
-
* Admitting the slash re-opens the `12615/0/11` gate-summary class the
|
|
193
|
-
* lookahead exists to suppress, so the slash form gets its OWN pattern with a
|
|
194
|
-
* STRICTER contract than the bare cardinal:
|
|
195
|
-
*
|
|
196
|
-
* 1. RATIO: exactly two slash-joined numbers. Both boundary guards stay:
|
|
197
|
-
* the lookbehind rejects a numerator glued to a path/id (`hooks/12/14`),
|
|
198
|
-
* the lookahead after the DENOMINATOR rejects a third slash-segment —
|
|
199
|
-
* `12615/0/11` fails twice over (denominator `0` is followed by `/`, and
|
|
200
|
-
* the trailing pair `0/11` has a `/`-glued numerator).
|
|
201
|
-
* 2. NOUN IMMEDIATELY AFTER — no CARDINAL_GAP. The noun-after-denominator is
|
|
202
|
-
* the discriminator that separates a measured ratio ("12/14 files") from
|
|
203
|
-
* a bare slash pair that is a date or score ("on 12/14 we shipped",
|
|
204
|
-
* "rated 3/5 overall"): those are followed by anything BUT an artefact
|
|
205
|
-
* noun. Widening to the gapped form would admit US-date + adjective +
|
|
206
|
-
* noun collisions with no PSA-006 payoff.
|
|
207
|
-
*
|
|
208
|
-
* FP re-measured on the CORRECT text sort (#918 requirement — the prior 1.07
|
|
209
|
-
* rate was measured on coordinator narration, not on what this hook reads):
|
|
210
|
-
* 490 real SubagentStop subagent transcripts (`~/.claude/projects/<slug>/<session>/
|
|
211
|
-
* subagents/*.jsonl`), spawning THIS hook per transcript, 2026-07-31.
|
|
212
|
-
* Baseline (pre-#918): 345 firings / 0.7041 per stop. With slash form +
|
|
213
|
-
* `callers` admitted: 348 / 0.7102 — +3 firings, of which 1 is a true positive
|
|
214
|
-
* ("0/49 Learnings mit Allow-List-Typ", an unverified canon-form claim), 1
|
|
215
|
-
* quotes the #918 example sentence itself (mention-not-use), and 1 is a
|
|
216
|
-
* before/after line-count pair ("858/857 lines", class-consistent with the
|
|
217
|
-
* bare "412 lines" behaviour of pattern 7).
|
|
218
|
-
*
|
|
219
|
-
* Bounded quantifiers only ({1,9}) — linear-time, ReDoS-safe.
|
|
220
|
-
*/
|
|
221
|
-
const CARDINAL_RATIO_PATTERN = new RegExp(
|
|
222
|
-
`(?<![\\w#$:/.-])\\d{1,9}/\\d{1,9}(?![\\d.%:/-])\\s+${CARDINAL_NOUN}\\b`,
|
|
223
|
-
'i'
|
|
224
|
-
);
|
|
225
|
-
|
|
226
|
-
/** Inline-code spans are masked before the cardinal pattern runs. */
|
|
227
|
-
const INLINE_CODE_RE = /`[^`\n]*`/g;
|
|
228
|
-
|
|
229
|
-
/**
|
|
230
|
-
* Commands that count as a MEASUREMENT inside a fenced block. `grep|rg|find`
|
|
231
|
-
* (the #567 set) only covers text search; the #908 facts are measured with
|
|
232
|
-
* `git log --oneline | wc -l`, `jq` over a JSONL metrics file, `ls | wc -l`,
|
|
233
|
-
* or a `node`/`npm` script. Refusing to recognise those made the honest,
|
|
234
|
-
* evidence-quoting path fail verification.
|
|
235
|
-
*/
|
|
236
|
-
const MEASUREMENT_CMD_RE = /\b(grep|rg|find|git|wc|jq|ls|node|npm)\b/;
|
|
237
|
-
|
|
238
|
-
/**
|
|
239
|
-
* Markers that date a measurement (#908 Baustein 2 input). ADVISORY in v1:
|
|
240
|
-
* an undated-but-verified claim is counted and reported, never a violation —
|
|
241
|
-
* a hard contract without an established authoring habit buys friction, not
|
|
242
|
-
* accuracy. Baustein 2 can escalate this to a violation once the habit exists.
|
|
243
|
-
*/
|
|
244
|
-
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;
|
|
245
|
-
|
|
246
80
|
// ---------------------------------------------------------------------------
|
|
247
81
|
// stdin reading (inline — Stop-family hooks exit 0 always, never deny)
|
|
248
82
|
// ---------------------------------------------------------------------------
|
|
@@ -287,8 +121,8 @@ function readStdinJson() {
|
|
|
287
121
|
*/
|
|
288
122
|
async function isEnabled() {
|
|
289
123
|
const candidates = [
|
|
290
|
-
path.join(
|
|
291
|
-
path.join(
|
|
124
|
+
path.join(getProjectDir(), 'CLAUDE.md'),
|
|
125
|
+
path.join(getProjectDir(), 'AGENTS.md'),
|
|
292
126
|
];
|
|
293
127
|
for (const file of candidates) {
|
|
294
128
|
try {
|
|
@@ -302,205 +136,143 @@ async function isEnabled() {
|
|
|
302
136
|
}
|
|
303
137
|
|
|
304
138
|
// ---------------------------------------------------------------------------
|
|
305
|
-
//
|
|
139
|
+
// payload helpers
|
|
306
140
|
// ---------------------------------------------------------------------------
|
|
307
141
|
|
|
308
142
|
/**
|
|
309
|
-
*
|
|
310
|
-
*
|
|
311
|
-
*
|
|
143
|
+
* Pick the first non-empty trimmed string value from `input` across the given
|
|
144
|
+
* candidate keys, in order. Returns `fallback` when none match. Mirrors the
|
|
145
|
+
* helper in hooks/subagent-telemetry.mjs so the two hooks resolve session ids
|
|
146
|
+
* identically (parent_session_id first).
|
|
312
147
|
*
|
|
313
|
-
* @param {
|
|
314
|
-
* @
|
|
148
|
+
* @param {object} input
|
|
149
|
+
* @param {string[]} keys
|
|
150
|
+
* @param {*} fallback
|
|
151
|
+
* @returns {string|*}
|
|
315
152
|
*/
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
raw = await fs.readFile(transcriptPath, 'utf8');
|
|
321
|
-
} catch {
|
|
322
|
-
return '';
|
|
323
|
-
}
|
|
324
|
-
if (!raw.trim()) return '';
|
|
325
|
-
|
|
326
|
-
const assistantRecords = [];
|
|
327
|
-
for (const line of raw.split(/\r?\n/)) {
|
|
328
|
-
const trimmed = line.trim();
|
|
329
|
-
if (!trimmed) continue;
|
|
330
|
-
let rec;
|
|
331
|
-
try { rec = JSON.parse(trimmed); } catch { continue; }
|
|
332
|
-
if (rec && rec.type === 'assistant') assistantRecords.push(rec);
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
const tail = assistantRecords.slice(-TAIL_RECORDS);
|
|
336
|
-
const textBlocks = [];
|
|
337
|
-
for (const rec of tail) {
|
|
338
|
-
const content = rec?.message?.content;
|
|
339
|
-
if (!Array.isArray(content)) continue;
|
|
340
|
-
for (const block of content) {
|
|
341
|
-
if (block && block.type === 'text' && typeof block.text === 'string') {
|
|
342
|
-
textBlocks.push(block.text);
|
|
343
|
-
}
|
|
344
|
-
}
|
|
153
|
+
function firstNonEmptyString(input, keys, fallback) {
|
|
154
|
+
for (const k of keys) {
|
|
155
|
+
const v = input[k];
|
|
156
|
+
if (typeof v === 'string' && v.trim()) return v;
|
|
345
157
|
}
|
|
346
|
-
return
|
|
158
|
+
return fallback;
|
|
347
159
|
}
|
|
348
160
|
|
|
349
|
-
// ---------------------------------------------------------------------------
|
|
350
|
-
// claim scanning
|
|
351
|
-
// ---------------------------------------------------------------------------
|
|
352
|
-
|
|
353
161
|
/**
|
|
354
|
-
*
|
|
355
|
-
* - `measurementLines`: lines belonging to a fenced block whose body contains
|
|
356
|
-
* a MEASUREMENT_CMD_RE invocation (the evidence a claim can lean on).
|
|
357
|
-
* - `fencedLines`: lines belonging to ANY fenced block (evidence or not).
|
|
358
|
-
* Used only to keep the greedy #908 cardinal pattern out of tool output;
|
|
359
|
-
* the six quantifier patterns are unchanged and still scan fenced lines.
|
|
162
|
+
* Resolve the STOPPING SUBAGENT's own sidecar pair (#1191, #1196).
|
|
360
163
|
*
|
|
361
|
-
*
|
|
362
|
-
*
|
|
363
|
-
*
|
|
164
|
+
* Thin wrapper over the consolidated derivation —
|
|
165
|
+
* `hooks/_lib/subagent-paths.mjs` `resolveSubagentSidecar()` — which now ALSO
|
|
166
|
+
* containment-checks the `agent_transcript_path` override this file used to
|
|
167
|
+
* return unvalidated (see that module's header divergence table). Returns
|
|
168
|
+
* null when the derivation is impossible — the caller must then scan
|
|
169
|
+
* NOTHING. Falling back to `input.transcript_path` is the defect this
|
|
170
|
+
* function exists to remove: that path is the coordinator's transcript.
|
|
364
171
|
*
|
|
365
|
-
* @param {
|
|
366
|
-
* @
|
|
172
|
+
* @param {object} input — SubagentStop stdin payload
|
|
173
|
+
* @param {string|null} agentId
|
|
174
|
+
* @returns {{base: string, transcript: string, meta: string}|null}
|
|
367
175
|
*/
|
|
368
|
-
function
|
|
369
|
-
const
|
|
370
|
-
|
|
371
|
-
let fenceStart = -1;
|
|
372
|
-
let buffer = [];
|
|
373
|
-
let inFence = false;
|
|
374
|
-
|
|
375
|
-
for (let i = 0; i < lines.length; i++) {
|
|
376
|
-
const isFence = /^\s*```/.test(lines[i]);
|
|
377
|
-
if (!inFence) {
|
|
378
|
-
if (isFence) { inFence = true; fenceStart = i; buffer = []; }
|
|
379
|
-
continue;
|
|
380
|
-
}
|
|
381
|
-
// inside a fence
|
|
382
|
-
if (isFence) {
|
|
383
|
-
// closing fence — evaluate the buffered body
|
|
384
|
-
const body = buffer.join('\n');
|
|
385
|
-
const isMeasurement = MEASUREMENT_CMD_RE.test(body);
|
|
386
|
-
for (let j = fenceStart; j <= i; j++) {
|
|
387
|
-
fencedLines.add(j);
|
|
388
|
-
if (isMeasurement) measurementLines.add(j);
|
|
389
|
-
}
|
|
390
|
-
inFence = false;
|
|
391
|
-
fenceStart = -1;
|
|
392
|
-
buffer = [];
|
|
393
|
-
} else {
|
|
394
|
-
buffer.push(lines[i]);
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
// Unterminated trailing fence — suppress cardinal matches, grant no evidence.
|
|
399
|
-
if (inFence) {
|
|
400
|
-
for (let j = fenceStart; j < lines.length; j++) fencedLines.add(j);
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
// INLINE-code evidence: PSA-006 asks for "the exact pattern executed" to be
|
|
404
|
-
// quoted — an inline `git log --oneline | wc -l` satisfies that exactly as
|
|
405
|
-
// well as a fenced block, and one-line findings are commonly written that
|
|
406
|
-
// way. Refusing to count it would penalise the honest path.
|
|
407
|
-
for (let i = 0; i < lines.length; i++) {
|
|
408
|
-
if (fencedLines.has(i)) continue;
|
|
409
|
-
for (const span of lines[i].match(INLINE_CODE_RE) ?? []) {
|
|
410
|
-
if (MEASUREMENT_CMD_RE.test(span)) { measurementLines.add(i); break; }
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
return { measurementLines, fencedLines };
|
|
176
|
+
function resolveAgentTranscriptPath(input, agentId) {
|
|
177
|
+
const agentTranscriptPath = firstNonEmptyString(input, ['agent_transcript_path'], null);
|
|
178
|
+
return resolveSubagentSidecar({ transcriptPath: input.transcript_path, agentId, agentTranscriptPath });
|
|
415
179
|
}
|
|
416
180
|
|
|
417
181
|
/**
|
|
418
|
-
*
|
|
419
|
-
*
|
|
420
|
-
*
|
|
421
|
-
*
|
|
422
|
-
* @returns {boolean}
|
|
182
|
+
* Agent types carry a plugin qualifier, so the COLON is part of the real shape:
|
|
183
|
+
* `session-orchestrator:code-implementer` (37 chars, measured on-disk in a real
|
|
184
|
+
* sidecar meta.json 2026-09-02). Same constant as `AGENT_TYPE_META_RE` in
|
|
185
|
+
* hooks/on-stop.mjs — kept local because the two hooks share no module.
|
|
423
186
|
*/
|
|
424
|
-
|
|
425
|
-
for (let j = i - GREP_PROXIMITY_LINES; j <= i + GREP_PROXIMITY_LINES; j++) {
|
|
426
|
-
if (indices.has(j)) return true;
|
|
427
|
-
}
|
|
428
|
-
return false;
|
|
429
|
-
}
|
|
187
|
+
const AGENT_TYPE_META_RE = /^[A-Za-z0-9_.:-]{1,64}$/;
|
|
430
188
|
|
|
431
189
|
/**
|
|
432
|
-
*
|
|
433
|
-
*
|
|
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).
|
|
434
195
|
*
|
|
435
|
-
*
|
|
436
|
-
*
|
|
437
|
-
*
|
|
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.
|
|
438
202
|
*/
|
|
439
|
-
|
|
440
|
-
const from = Math.max(0, i - GREP_PROXIMITY_LINES);
|
|
441
|
-
const to = Math.min(lines.length - 1, i + GREP_PROXIMITY_LINES);
|
|
442
|
-
for (let j = from; j <= to; j++) {
|
|
443
|
-
if (TIMESTAMP_MARKER_RE.test(lines[j])) return true;
|
|
444
|
-
}
|
|
445
|
-
return false;
|
|
446
|
-
}
|
|
203
|
+
const AGENT_DESCRIPTION_MAX = 120;
|
|
447
204
|
|
|
448
205
|
/**
|
|
449
|
-
*
|
|
450
|
-
*
|
|
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:
|
|
451
219
|
*
|
|
452
|
-
*
|
|
453
|
-
*
|
|
454
|
-
*
|
|
455
|
-
*
|
|
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`.
|
|
231
|
+
*
|
|
232
|
+
* @param {object} input — SubagentStop stdin payload
|
|
233
|
+
* @param {string} metaPath — `resolveSubagentSidecar(...).meta`
|
|
234
|
+
* @returns {Promise<{agent: string, source: 'payload'|'meta'|'none', description: string|null, payloadKeys: string[]}>}
|
|
456
235
|
*/
|
|
457
|
-
function
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
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;
|
|
248
|
+
try {
|
|
249
|
+
const meta = JSON.parse(await fs.readFile(metaPath, 'utf8'));
|
|
250
|
+
const t = meta?.agentType;
|
|
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);
|
|
476
264
|
}
|
|
477
|
-
|
|
478
|
-
|
|
265
|
+
} catch {
|
|
266
|
+
// absent or corrupt sidecar meta — the payload branch still stands
|
|
479
267
|
}
|
|
480
|
-
return { violations, undatedVerified };
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
// ---------------------------------------------------------------------------
|
|
484
|
-
// payload helpers
|
|
485
|
-
// ---------------------------------------------------------------------------
|
|
486
268
|
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
* @param {string[]} keys
|
|
495
|
-
* @param {*} fallback
|
|
496
|
-
* @returns {string|*}
|
|
497
|
-
*/
|
|
498
|
-
function firstNonEmptyString(input, keys, fallback) {
|
|
499
|
-
for (const k of keys) {
|
|
500
|
-
const v = input[k];
|
|
501
|
-
if (typeof v === 'string' && v.trim()) return v;
|
|
502
|
-
}
|
|
503
|
-
return fallback;
|
|
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 };
|
|
504
276
|
}
|
|
505
277
|
|
|
506
278
|
/**
|
|
@@ -524,23 +296,82 @@ function projectRootHash(projectRoot) {
|
|
|
524
296
|
}
|
|
525
297
|
|
|
526
298
|
/**
|
|
527
|
-
* Build the dedup sentinel path for real project/session/agent contexts.
|
|
299
|
+
* Build the dedup sentinel path for real project/session/agent/claim contexts.
|
|
528
300
|
* Missing fallback IDs intentionally return null so unrelated hooks/tests do
|
|
529
301
|
* not collide on a global "unknown" key.
|
|
530
302
|
*
|
|
303
|
+
* #1198 FIX 1: the key used to be `(projectRoot, sessionId, agent_type)` —
|
|
304
|
+
* `agent_type` is a CLASS ("discovery"), not an individual agent, so two
|
|
305
|
+
* DIFFERENT real subagents of the same type running in the same session
|
|
306
|
+
* collided on the same sentinel: the second agent's own `additionalContext`
|
|
307
|
+
* feedback was silently suppressed even though it never received a copy of
|
|
308
|
+
* the first agent's warning. Keying on `agentId` (the harness's per-process
|
|
309
|
+
* `agent_id`/`subagent_id`) instead removes that cross-agent collision. The
|
|
310
|
+
* claim-text hash is ADDITIVE: it lets a genuinely SECOND, DISTINCT claim
|
|
311
|
+
* from the same real agent still surface its own suppression check, rather
|
|
312
|
+
* than being silenced merely because that agent already triggered once for a
|
|
313
|
+
* different claim.
|
|
314
|
+
*
|
|
531
315
|
* @param {object} opts
|
|
532
316
|
* @param {string} opts.projectRoot
|
|
533
317
|
* @param {string|null} opts.sessionId
|
|
534
|
-
* @param {string|null} opts.
|
|
318
|
+
* @param {string|null} opts.agentId
|
|
319
|
+
* @param {string|null} opts.claimText
|
|
535
320
|
* @returns {string|null}
|
|
536
321
|
*/
|
|
537
|
-
function dedupSentinelPath({ projectRoot, sessionId,
|
|
322
|
+
function dedupSentinelPath({ projectRoot, sessionId, agentId, claimText }) {
|
|
538
323
|
if (typeof sessionId !== 'string' || !sessionId.trim()) return null;
|
|
539
|
-
if (typeof
|
|
324
|
+
if (typeof agentId !== 'string' || !agentId.trim()) return null;
|
|
325
|
+
if (typeof claimText !== 'string' || !claimText.trim()) return null;
|
|
540
326
|
|
|
327
|
+
const claimHash = createHash('sha256').update(claimText).digest('hex').slice(0, 16);
|
|
541
328
|
return path.join(
|
|
542
329
|
tmpdir(),
|
|
543
|
-
`psa006-${projectRootHash(projectRoot)}-${safeSentinelComponent(sessionId)}-${safeSentinelComponent(
|
|
330
|
+
`psa006-${projectRootHash(projectRoot)}-${safeSentinelComponent(sessionId)}-${safeSentinelComponent(agentId)}-${claimHash}.lock`
|
|
331
|
+
);
|
|
332
|
+
}
|
|
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`
|
|
544
375
|
);
|
|
545
376
|
}
|
|
546
377
|
|
|
@@ -555,32 +386,79 @@ async function main() {
|
|
|
555
386
|
|
|
556
387
|
if (!(await isEnabled())) return;
|
|
557
388
|
|
|
558
|
-
|
|
389
|
+
// #1191: scan the SUBAGENT's own transcript, never the parent. No agent_id
|
|
390
|
+
// (or no derivable path) → scan nothing and record nothing.
|
|
391
|
+
const agentId = firstNonEmptyString(input, ['agent_id', 'subagent_id'], null);
|
|
392
|
+
const sidecar = resolveAgentTranscriptPath(input, agentId);
|
|
393
|
+
if (sidecar === null) return;
|
|
394
|
+
|
|
395
|
+
const text = await readTranscriptTail(sidecar.transcript);
|
|
559
396
|
const { violations, undatedVerified } = findViolations(text);
|
|
560
397
|
if (violations.length === 0) return;
|
|
561
398
|
|
|
562
|
-
const
|
|
563
|
-
const agent =
|
|
399
|
+
const attribution = await resolveAgentAttribution(input, sidecar.meta);
|
|
400
|
+
const agent = attribution.agent;
|
|
564
401
|
// session_id precedence: parent_session_id first, mirroring the sibling hook
|
|
565
402
|
// hooks/subagent-telemetry.mjs (firstNonEmptyString(['parent_session_id',
|
|
566
403
|
// 'session_id'])). W2-review LOW finding (#567) — the prior `session_id ||
|
|
567
404
|
// parent_session_id` order disagreed with telemetry and could log the wrong id.
|
|
568
405
|
const sessionId = firstNonEmptyString(input, ['parent_session_id', 'session_id'], null);
|
|
569
406
|
|
|
570
|
-
// Project/session/agent deduplication: only emit additionalContext
|
|
571
|
-
// repeated real contexts. Missing session IDs never create/read a
|
|
572
|
-
// so fallback traffic still surfaces warnings and cannot collide
|
|
573
|
-
|
|
407
|
+
// Project/session/agent/claim deduplication: only emit additionalContext
|
|
408
|
+
// once for repeated real contexts. Missing session IDs never create/read a
|
|
409
|
+
// sentinel, so fallback traffic still surfaces warnings and cannot collide
|
|
410
|
+
// globally. Keyed on agentId (not agent TYPE, #1198 FIX 1) plus the first
|
|
411
|
+
// violation's claim text so distinct real agents and distinct claims never
|
|
412
|
+
// share a sentinel.
|
|
413
|
+
const projectRoot = getProjectDir();
|
|
414
|
+
const sentinel = dedupSentinelPath({
|
|
415
|
+
projectRoot,
|
|
416
|
+
sessionId,
|
|
417
|
+
agentId,
|
|
418
|
+
claimText: violations[0].claim,
|
|
419
|
+
});
|
|
574
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.
|
|
575
426
|
const filePath = eventsFilePath();
|
|
576
|
-
|
|
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
|
+
}
|
|
577
446
|
await appendJsonl(filePath, {
|
|
578
447
|
event: 'discovery_validator_violation',
|
|
579
448
|
timestamp: new Date().toISOString(),
|
|
580
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 } : {}),
|
|
456
|
+
...(agentId !== null ? { agent_id: agentId } : {}),
|
|
581
457
|
...(sessionId !== null ? { session_id: sessionId } : {}),
|
|
582
|
-
claim_text: claim,
|
|
458
|
+
claim_text: violation.claim,
|
|
459
|
+
occurrences: violation.occurrences,
|
|
583
460
|
});
|
|
461
|
+
written++;
|
|
584
462
|
}
|
|
585
463
|
|
|
586
464
|
// Advisory only (#908 item 4) — never promoted to a violation in v1.
|
|
@@ -588,10 +466,18 @@ async function main() {
|
|
|
588
466
|
? ` ${undatedVerified} verified claim(s) carry no measurement timestamp (advisory).`
|
|
589
467
|
: '';
|
|
590
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
|
+
|
|
591
476
|
const warnText =
|
|
592
|
-
`⚠ PSA-006: ${violations.length} repo-state/distributional claim(s) from agent
|
|
593
|
-
`lack an adjacent measurement transcript
|
|
594
|
-
|
|
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} ` +
|
|
595
481
|
`See .claude/rules/parallel-sessions.md § PSA-006.`;
|
|
596
482
|
process.stderr.write(warnText + '\n');
|
|
597
483
|
|