session-orchestrator 5.1.0 → 5.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/architecture/SKILL.md +3 -1
- package/.agents/skills/autopilot/SKILL.md +6 -1
- package/.agents/skills/autopilot/agents/openai.yaml +5 -0
- package/.agents/skills/bootstrap/SKILL.md +7 -1
- package/.agents/skills/bootstrap/agents/openai.yaml +5 -0
- package/.agents/skills/brainstorm/SKILL.md +8 -1
- package/.agents/skills/brainstorm/agents/openai.yaml +5 -0
- package/.agents/skills/claude-md-drift-check/SKILL.md +3 -1
- package/.agents/skills/close/SKILL.md +21 -0
- package/.agents/skills/close/agents/openai.yaml +5 -0
- package/.agents/skills/convergence-monitoring/SKILL.md +4 -2
- package/.agents/skills/debug/SKILL.md +7 -1
- package/.agents/skills/debug/agents/openai.yaml +5 -0
- package/.agents/skills/discovery/SKILL.md +7 -2
- package/.agents/skills/discovery/agents/openai.yaml +5 -0
- package/.agents/skills/dispatcher/SKILL.md +7 -1
- package/.agents/skills/dispatcher/agents/openai.yaml +5 -0
- package/.agents/skills/docs-orchestrator/SKILL.md +3 -1
- package/.agents/skills/ecosystem-health/SKILL.md +3 -1
- package/.agents/skills/eli5/SKILL.md +7 -1
- package/.agents/skills/eli5/agents/openai.yaml +5 -0
- package/.agents/skills/eval/SKILL.md +7 -2
- package/.agents/skills/eval/agents/openai.yaml +5 -0
- package/.agents/skills/evolve/SKILL.md +8 -3
- package/.agents/skills/evolve/agents/openai.yaml +5 -0
- package/.agents/skills/frontmatter-guard/SKILL.md +3 -1
- package/.agents/skills/gitlab-ops/SKILL.md +3 -1
- package/.agents/skills/gitlab-portfolio/SKILL.md +3 -1
- package/.agents/skills/go/SKILL.md +22 -0
- package/.agents/skills/go/agents/openai.yaml +5 -0
- package/.agents/skills/grill/SKILL.md +7 -1
- package/.agents/skills/grill/agents/openai.yaml +5 -0
- package/.agents/skills/harness-audit/SKILL.md +20 -0
- package/.agents/skills/harness-audit/agents/openai.yaml +5 -0
- package/.agents/skills/hook-development/SKILL.md +3 -1
- package/.agents/skills/mcp-builder/SKILL.md +3 -1
- package/.agents/skills/memory-cleanup/SKILL.md +6 -1
- package/.agents/skills/memory-cleanup/agents/openai.yaml +5 -0
- package/.agents/skills/mode-selector/SKILL.md +3 -1
- package/.agents/skills/npm-publish/SKILL.md +4 -2
- package/.agents/skills/peekaboo-driver/SKILL.md +3 -1
- package/.agents/skills/persona-panel/SKILL.md +6 -1
- package/.agents/skills/persona-panel/agents/openai.yaml +5 -0
- package/.agents/skills/plan/SKILL.md +8 -2
- package/.agents/skills/plan/agents/openai.yaml +5 -0
- package/.agents/skills/playwright-driver/SKILL.md +3 -1
- package/.agents/skills/portfolio/SKILL.md +21 -0
- package/.agents/skills/portfolio/agents/openai.yaml +5 -0
- package/.agents/skills/quality-gates/SKILL.md +3 -1
- package/.agents/skills/reconcile/SKILL.md +6 -1
- package/.agents/skills/reconcile/agents/openai.yaml +5 -0
- package/.agents/skills/release/SKILL.md +22 -0
- package/.agents/skills/release/agents/openai.yaml +5 -0
- package/.agents/skills/remote-offload/SKILL.md +3 -1
- package/.agents/skills/repo-audit/SKILL.md +6 -1
- package/.agents/skills/repo-audit/agents/openai.yaml +5 -0
- package/.agents/skills/session/SKILL.md +21 -0
- package/.agents/skills/session/agents/openai.yaml +5 -0
- package/.agents/skills/session-end/SKILL.md +3 -1
- package/.agents/skills/session-plan/SKILL.md +3 -1
- package/.agents/skills/session-start/SKILL.md +3 -1
- package/.agents/skills/spinout/SKILL.md +6 -1
- package/.agents/skills/spinout/agents/openai.yaml +5 -0
- package/.agents/skills/sunset-review/SKILL.md +7 -1
- package/.agents/skills/sunset-review/agents/openai.yaml +5 -0
- package/.agents/skills/templates-ack/SKILL.md +21 -0
- package/.agents/skills/templates-ack/agents/openai.yaml +5 -0
- package/.agents/skills/test/SKILL.md +21 -0
- package/.agents/skills/test/agents/openai.yaml +5 -0
- package/.agents/skills/test-runner/SKILL.md +3 -1
- package/.agents/skills/tmux-layout/SKILL.md +3 -1
- package/.agents/skills/using-orchestrator/SKILL.md +3 -1
- package/.agents/skills/ux-grill/SKILL.md +7 -1
- package/.agents/skills/ux-grill/agents/openai.yaml +5 -0
- package/.agents/skills/vault-mirror/SKILL.md +3 -1
- package/.agents/skills/vault-sync/SKILL.md +3 -1
- package/.agents/skills/wave-executor/SKILL.md +3 -1
- package/.agents/skills/write-executable-plan/SKILL.md +3 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +4 -4
- package/.codex-plugin/skills/autopilot/SKILL.md +5 -4
- package/.codex-plugin/skills/bootstrap/SKILL.md +8 -4
- package/.codex-plugin/skills/brainstorm/SKILL.md +11 -4
- package/.codex-plugin/skills/close/SKILL.md +3 -3
- package/.codex-plugin/skills/convergence-monitoring/SKILL.md +1 -1
- package/.codex-plugin/skills/debug/SKILL.md +11 -4
- package/.codex-plugin/skills/discovery/SKILL.md +8 -4
- package/.codex-plugin/skills/dispatcher/SKILL.md +4 -4
- package/.codex-plugin/skills/eli5/SKILL.md +9 -4
- package/.codex-plugin/skills/eval/SKILL.md +9 -4
- package/.codex-plugin/skills/evolve/SKILL.md +9 -4
- package/.codex-plugin/skills/go/SKILL.md +3 -3
- package/.codex-plugin/skills/grill/SKILL.md +11 -4
- package/.codex-plugin/skills/harness-audit/SKILL.md +4 -3
- package/.codex-plugin/skills/memory-cleanup/SKILL.md +9 -4
- package/.codex-plugin/skills/npm-publish/SKILL.md +1 -1
- package/.codex-plugin/skills/persona-panel/SKILL.md +5 -5
- package/.codex-plugin/skills/plan/SKILL.md +8 -4
- package/.codex-plugin/skills/portfolio/SKILL.md +3 -3
- package/.codex-plugin/skills/reconcile/SKILL.md +9 -4
- package/.codex-plugin/skills/release/SKILL.md +3 -3
- package/.codex-plugin/skills/repo-audit/SKILL.md +6 -4
- package/.codex-plugin/skills/session/SKILL.md +1 -1
- package/.codex-plugin/skills/spinout/SKILL.md +4 -4
- package/.codex-plugin/skills/sunset-review/SKILL.md +5 -4
- package/.codex-plugin/skills/test/SKILL.md +3 -3
- package/.codex-plugin/skills/ux-grill/SKILL.md +11 -4
- package/.cursor/commands/autopilot.md +4 -4
- package/.cursor/commands/bootstrap.md +5 -4
- package/.cursor/commands/brainstorm.md +5 -4
- package/.cursor/commands/close.md +4 -3
- package/.cursor/commands/debug.md +4 -4
- package/.cursor/commands/discovery.md +4 -4
- package/.cursor/commands/dispatcher.md +4 -4
- package/.cursor/commands/eli5.md +4 -4
- package/.cursor/commands/eval.md +4 -4
- package/.cursor/commands/evolve.md +4 -4
- package/.cursor/commands/go.md +4 -3
- package/.cursor/commands/grill.md +4 -4
- package/.cursor/commands/harness-audit.md +3 -3
- package/.cursor/commands/memory-cleanup.md +4 -4
- package/.cursor/commands/persona-panel.md +4 -4
- package/.cursor/commands/plan.md +5 -4
- package/.cursor/commands/portfolio.md +3 -3
- package/.cursor/commands/reconcile.md +4 -4
- package/.cursor/commands/release.md +4 -3
- package/.cursor/commands/repo-audit.md +4 -4
- package/.cursor/commands/session.md +1 -1
- package/.cursor/commands/spinout.md +4 -4
- package/.cursor/commands/sunset-review.md +4 -4
- package/.cursor/commands/test.md +3 -3
- package/.cursor/commands/ux-grill.md +4 -4
- package/.cursor/rules/000-session-orchestrator.mdc +0 -2
- package/.cursor/rules/010-session-workflow.mdc +2 -2
- package/.cursor/rules/050-plan.mdc +1 -1
- package/.cursor/skills/bootstrap/SKILL.md +1 -0
- package/.cursor/skills/close/SKILL.md +13 -0
- package/.cursor/skills/convergence-monitoring/SKILL.md +1 -0
- package/.cursor/skills/debug/SKILL.md +0 -1
- package/.cursor/skills/discovery/SKILL.md +0 -1
- package/.cursor/skills/dispatcher/SKILL.md +0 -1
- package/.cursor/skills/eli5/SKILL.md +0 -1
- package/.cursor/skills/eval/SKILL.md +1 -1
- package/.cursor/skills/evolve/SKILL.md +0 -1
- package/.cursor/skills/go/SKILL.md +13 -0
- package/.cursor/skills/grill/SKILL.md +0 -1
- package/.cursor/skills/harness-audit/SKILL.md +12 -0
- package/.cursor/skills/npm-publish/SKILL.md +1 -0
- package/.cursor/skills/portfolio/SKILL.md +12 -0
- package/.cursor/skills/release/SKILL.md +13 -0
- package/.cursor/skills/repo-audit/SKILL.md +0 -1
- package/.cursor/skills/sunset-review/SKILL.md +0 -1
- package/.cursor/skills/test/SKILL.md +12 -0
- package/.cursor/skills/ux-grill/SKILL.md +0 -1
- package/.cursor-plugin/plugin.json +1 -1
- package/.orchestrator/policy/blocked-commands.json +13 -4
- package/AGENTS.md +3 -2
- package/CHANGELOG.md +197 -0
- package/README.md +11 -9
- package/SECURITY.md +12 -0
- package/agents/dialectic-deriver.md +13 -10
- package/agents/eval-judge.md +67 -45
- package/agents/skill-applied-judge.md +34 -19
- package/commands/session.md +17 -3
- package/docs/baseline.md +12 -6
- package/docs/ci-setup.md +53 -0
- package/docs/codex-setup.md +15 -3
- package/docs/components.md +13 -6
- package/docs/events-schema.md +59 -9
- package/docs/install.md +16 -0
- package/docs/persona-panel.md +1 -1
- package/docs/pi-setup.md +1 -1
- package/docs/rule-authoring.md +135 -14
- package/docs/scope-collision-guard.md +2 -0
- package/docs/session-config-reference.md +106 -11
- package/docs/session-config-template.md +31 -2
- package/docs/telemetry.md +2 -0
- package/hooks/_lib/hook-import-set.json +125 -8
- package/hooks/_lib/subagent-paths.mjs +15 -0
- package/hooks/_lib/subagent-transcript.mjs +582 -31
- package/hooks/_lib/vcs-create-matcher.mjs +217 -62
- package/hooks/config-protection.mjs +11 -3
- package/hooks/cwd-change-restore.mjs +11 -3
- package/hooks/enforce-commands.mjs +70 -23
- package/hooks/enforce-scope.mjs +143 -33
- package/hooks/hooks-codex.json +1 -1
- package/hooks/hooks.json +1 -1
- package/hooks/loop-guard.mjs +11 -3
- package/hooks/on-session-end.mjs +72 -25
- package/hooks/on-session-start.mjs +48 -11
- package/hooks/on-stop.mjs +211 -23
- package/hooks/operator-steer.mjs +11 -3
- package/hooks/post-bash-issue-budget-refund.mjs +18 -8
- package/hooks/post-bash-write-verify.mjs +6 -2
- package/hooks/post-edit-import-probe.mjs +17 -9
- package/hooks/post-edit-validate.mjs +13 -5
- package/hooks/post-subagent-discovery-validator.mjs +98 -13
- package/hooks/post-tool-batch-wave-signal.mjs +200 -38
- package/hooks/post-tool-failure-corrective-context.mjs +11 -5
- package/hooks/post-tooluse-frontend-slop.mjs +10 -4
- package/hooks/pre-auq-clarity.mjs +18 -2
- package/hooks/pre-bash-destructive-guard.mjs +80 -9
- package/hooks/pre-bash-issue-budget.mjs +119 -28
- package/hooks/pre-bash-memory-propose-audit.mjs +86 -54
- package/hooks/pre-bash-sessions-ledger-guard.mjs +391 -20
- package/hooks/pre-bash-staging-fence.mjs +335 -31
- package/hooks/pre-bash-templates-first.mjs +19 -14
- package/hooks/pre-task-scope-disjoint.mjs +385 -5
- package/hooks/skill-invocation-telemetry.mjs +2 -1
- package/hooks/subagent-telemetry.mjs +15 -19
- package/hooks/wave-scope-commit-guard.mjs +197 -100
- package/monitors/monitors.json +1 -1
- package/output-styles/wave-summary.md +1 -1
- package/package.json +2 -1
- package/pi/prompts/autopilot.md +3 -3
- package/pi/prompts/bootstrap.md +3 -3
- package/pi/prompts/brainstorm.md +3 -3
- package/pi/prompts/close.md +2 -2
- package/pi/prompts/debug.md +3 -3
- package/pi/prompts/discovery.md +3 -3
- package/pi/prompts/dispatcher.md +3 -3
- package/pi/prompts/eli5.md +3 -3
- package/pi/prompts/eval.md +3 -3
- package/pi/prompts/evolve.md +3 -3
- package/pi/prompts/go.md +2 -2
- package/pi/prompts/grill.md +3 -3
- package/pi/prompts/harness-audit.md +2 -3
- package/pi/prompts/memory-cleanup.md +3 -3
- package/pi/prompts/persona-panel.md +3 -3
- package/pi/prompts/plan.md +3 -3
- package/pi/prompts/portfolio.md +2 -2
- package/pi/prompts/reconcile.md +3 -3
- package/pi/prompts/release.md +3 -3
- package/pi/prompts/repo-audit.md +3 -4
- package/pi/prompts/session.md +2 -2
- package/pi/prompts/spinout.md +3 -3
- package/pi/prompts/sunset-review.md +3 -3
- package/pi/prompts/templates-ack.md +1 -1
- package/pi/prompts/test.md +3 -3
- package/pi/prompts/ux-grill.md +3 -3
- package/rules/README.md +1 -1
- package/rules/opt-in-domain/prompt-caching.md +1 -1
- package/rules/opt-in-stack/backend-data.md +1 -1
- package/rules/opt-in-stack/backend.md +3 -3
- package/rules/opt-in-stack/frontend.md +1 -1
- package/rules/opt-in-stack/security-web.md +3 -3
- package/rules/opt-in-stack/swift.md +1 -1
- package/scripts/archive-closed-prds.mjs +2 -2
- package/scripts/auq-audit.mjs +2 -3
- package/scripts/autopilot.mjs +23 -2
- package/scripts/backfill-abandoned-sessions.mjs +171 -15
- package/scripts/backfill-evidence-digest.mjs +2 -1
- package/scripts/backfill-learnings-from-vault.mjs +2 -2
- package/scripts/check-package-manager.mjs +2 -2
- package/scripts/check-sessions-integrity.mjs +300 -0
- package/scripts/ci/assert-vitest-green.mjs +2 -1
- package/scripts/dialectic-deriver.mjs +50 -13
- package/scripts/emit-session.mjs +77 -32
- package/scripts/eval-session.mjs +65 -3
- package/scripts/export-hw-learnings.mjs +2 -1
- package/scripts/express-path.mjs +1 -1
- package/scripts/gc-stale-worktrees.mjs +2 -1
- package/scripts/generate-agents-skills.mjs +102 -29
- package/scripts/generate-codex-skills.mjs +48 -4
- package/scripts/generate-cursor-adapter.mjs +220 -11
- package/scripts/generate-hook-import-set.mjs +12 -27
- package/scripts/generate-pi-prompts.mjs +183 -13
- package/scripts/github-protection-audit.mjs +2 -3
- package/scripts/lib/agent-frontmatter.mjs +23 -1
- package/scripts/lib/agent-status.mjs +2 -31
- package/scripts/lib/auq/clarity.mjs +10 -2
- package/scripts/lib/auq/parse.mjs +12 -31
- package/scripts/lib/auq/schema.mjs +56 -41
- package/scripts/lib/auto-dialectic.mjs +304 -15
- package/scripts/lib/autopilot/flags.mjs +12 -1
- package/scripts/lib/autopilot/kill-switches.mjs +6 -3
- package/scripts/lib/autopilot/loop.mjs +14 -1
- package/scripts/lib/autopilot/stall-sampler.mjs +80 -23
- package/scripts/lib/ci-status-banner.mjs +376 -16
- package/scripts/lib/claude-md-budget-lint.mjs +2 -5
- package/scripts/lib/command-blocker.mjs +408 -33
- package/scripts/lib/config/dialectic.mjs +12 -3
- package/scripts/lib/config/drift-check.mjs +19 -0
- package/scripts/lib/config/gate.mjs +74 -0
- package/scripts/lib/config/reaper.mjs +162 -0
- package/scripts/lib/config.mjs +14 -0
- package/scripts/lib/convergence-monitor.mjs +76 -13
- package/scripts/lib/cursor-hook-bridge.mjs +2 -2
- package/scripts/lib/description-surface.mjs +2 -5
- package/scripts/lib/dispatcher/cli.mjs +2 -1
- package/scripts/lib/ecosystem-health.mjs +11 -0
- package/scripts/lib/ecosystem-wizard.mjs +2 -1
- package/scripts/lib/eval/engine.mjs +421 -53
- package/scripts/lib/eval/judge.mjs +463 -40
- package/scripts/lib/eval/schema.mjs +10 -1
- package/scripts/lib/events-rotation.mjs +221 -25
- package/scripts/lib/events-schema.mjs +114 -0
- package/scripts/lib/events.mjs +524 -5
- package/scripts/lib/fetch-baseline.mjs +3 -8
- package/scripts/lib/frontmatter-guard.mjs +21 -10
- package/scripts/lib/gates/gate-baseline.mjs +27 -2
- package/scripts/lib/gates/gate-full.mjs +28 -3
- package/scripts/lib/gates/gate-helpers.mjs +243 -21
- package/scripts/lib/gates/gate-incremental.mjs +28 -3
- package/scripts/lib/gates/gate-per-file.mjs +27 -2
- package/scripts/lib/gitlab-ops/stale-mr-sweep.mjs +2 -1
- package/scripts/lib/gitlab-portfolio/cli.mjs +2 -1
- package/scripts/lib/gitlab-portfolio/markdown-writer.mjs +6 -1
- package/scripts/lib/instruction-budget-guard.mjs +332 -50
- package/scripts/lib/io.mjs +42 -8
- package/scripts/lib/is-main-module.mjs +82 -0
- package/scripts/lib/issue-close-strip-labels.mjs +207 -49
- package/scripts/lib/js-mask.mjs +197 -0
- package/scripts/lib/learnings/evolve-telemetry.mjs +11 -7
- package/scripts/lib/locks/index.mjs +32 -25
- package/scripts/lib/maintenance-due-banner.mjs +122 -91
- package/scripts/lib/orphan-reaper.mjs +1588 -0
- package/scripts/lib/peer-cards/merger.mjs +48 -10
- package/scripts/lib/peer-cards/reader.mjs +78 -2
- package/scripts/lib/peer-discovery.mjs +2 -5
- package/scripts/lib/playwright-driver/runner.mjs +2 -1
- package/scripts/lib/process-group.mjs +899 -0
- package/scripts/lib/quality-gate.mjs +107 -28
- package/scripts/lib/reconcile/backlog.mjs +368 -0
- package/scripts/lib/reconcile/engine.mjs +55 -188
- package/scripts/lib/reconcile/rule-expiry-sweep.mjs +884 -0
- package/scripts/lib/reconcile/sanitize.mjs +69 -3
- package/scripts/lib/reconcile-nudge-banner.mjs +138 -45
- package/scripts/lib/resource-probe/parsers.mjs +31 -0
- package/scripts/lib/rule-loader.mjs +41 -12
- package/scripts/lib/rules-sync.mjs +2 -5
- package/scripts/lib/scope-echo.mjs +429 -7
- package/scripts/lib/scope-gate.mjs +605 -1
- package/scripts/lib/session-close-backfill.mjs +91 -12
- package/scripts/lib/session-id.mjs +9 -20
- package/scripts/lib/session-invocation.mjs +20 -0
- package/scripts/lib/session-schema/constants.mjs +30 -2
- package/scripts/lib/session-schema/normalizer.mjs +56 -4
- package/scripts/lib/session-schema.mjs +8 -3
- package/scripts/lib/session-start-probes.mjs +95 -10
- package/scripts/lib/sessions-canonical.mjs +23 -0
- package/scripts/lib/sessions-integrity-banner.mjs +7 -1
- package/scripts/lib/sessions-staleness-banner.mjs +193 -51
- package/scripts/lib/skill-evidence-window.mjs +891 -0
- package/scripts/lib/skill-evolution/candidate-intake.mjs +133 -12
- package/scripts/lib/skill-evolution/engine.mjs +18 -9
- package/scripts/lib/skill-judge.mjs +45 -3
- package/scripts/lib/state-md.mjs +84 -3
- package/scripts/lib/sunset/walker.mjs +31 -4
- package/scripts/lib/tail-window.mjs +56 -0
- package/scripts/lib/telemetry/schema.mjs +30 -0
- package/scripts/lib/telemetry/sync.mjs +61 -6
- package/scripts/lib/telemetry-flush-health-banner.mjs +4 -22
- package/scripts/lib/test-runner/issue-reconcile.mjs +48 -16
- package/scripts/lib/tests-src-ratio.mjs +2 -6
- package/scripts/lib/tmux-layout/telemetry-stats.mjs +74 -14
- package/scripts/lib/user-invocable-skills.mjs +205 -0
- package/scripts/lib/ux-grill/reconcile.mjs +48 -22
- package/scripts/lib/validate/check-agents-skills.mjs +26 -15
- package/scripts/lib/validate/check-banner-parity.mjs +2 -2
- package/scripts/lib/validate/check-cursor-adapter.mjs +3 -2
- package/scripts/lib/validate/check-dead-bridge.mjs +2 -2
- package/scripts/lib/validate/check-doc-cli-commands.mjs +2 -2
- package/scripts/lib/validate/check-entry-guard.mjs +329 -0
- package/scripts/lib/validate/check-guard-requires-parity.mjs +2 -2
- package/scripts/lib/validate/check-hook-entry-guards.mjs +636 -0
- package/scripts/lib/validate/check-hooks-emit-event-guard.mjs +2 -2
- package/scripts/lib/validate/check-learning-provenance.mjs +2 -2
- package/scripts/lib/validate/check-pi-prompts.mjs +1 -0
- package/scripts/lib/validate/check-rules.mjs +7 -5
- package/scripts/lib/validate/check-skill-links.mjs +35 -6
- package/scripts/lib/validate/check-skill-script-paths.mjs +241 -29
- package/scripts/lib/validate/check-test-git-config-target.mjs +26 -36
- package/scripts/lib/validate/check-unicode-safety.mjs +2 -2
- package/scripts/lib/validate/check-untracked-test-deps.mjs +9 -104
- package/scripts/lib/validate/check-unwired-features.mjs +220 -33
- package/scripts/lib/validate/check-validator-registration.mjs +36 -12
- package/scripts/lib/validate/check-vcs-repo-flag.mjs +2 -2
- package/scripts/lib/validate/confidential-names.mjs +10 -0
- package/scripts/lib/validate-vendored-rules.mjs +39 -12
- package/scripts/lib/vault-mirror/namespace.mjs +46 -8
- package/scripts/lib/vault-mirror/process.mjs +10 -3
- package/scripts/lib/vault-mirror/render-sessions.mjs +12 -2
- package/scripts/lib/vault-status/narrative-mirror.mjs +31 -7
- package/scripts/lib/vault-yaml.mjs +118 -0
- package/scripts/lib/wave-transcript-tail.mjs +2 -2
- package/scripts/lib/worktree/lifecycle.mjs +153 -1
- package/scripts/lock-reaper.mjs +2 -1
- package/scripts/materialize-wave-scope.mjs +87 -4
- package/scripts/migrate-sessions-jsonl.mjs +2 -1
- package/scripts/migrate-vault-paths.mjs +2 -3
- package/scripts/release-session-lock.mjs +305 -0
- package/scripts/release.mjs +109 -39
- package/scripts/relocate-vault-corpus.mjs +2 -3
- package/scripts/repair-invalid-sessions.mjs +2 -2
- package/scripts/resolve-session-invocation.mjs +59 -0
- package/scripts/run-quality-gate.mjs +156 -17
- package/scripts/session-shape.mjs +2 -2
- package/scripts/site-numbers.mjs +35 -11
- package/scripts/sweep-expired-rules.mjs +227 -0
- package/scripts/validate-plugin.mjs +21 -0
- package/scripts/validate-wave-scope.mjs +32 -105
- package/scripts/vault-consolidate.mjs +2 -2
- package/scripts/vault-mirror.mjs +11 -4
- package/scripts/wave-scope-binding.mjs +2 -3
- package/skills/_shared/bootstrap-gate.md +1 -1
- package/skills/_shared/monitor-patterns.md +1 -1
- package/skills/_shared/platform-tools.md +23 -11
- package/skills/_shared/research-evidence.md +53 -0
- package/skills/_shared/state-ownership.md +3 -0
- package/skills/autopilot/SKILL.md +80 -11
- package/skills/bootstrap/SKILL.md +51 -1
- package/skills/brainstorm/SKILL.md +16 -0
- package/skills/claude-md-drift-check/SKILL.md +1 -1
- package/skills/claude-md-drift-check/checker.mjs +49 -11
- package/{commands/close.md → skills/close/SKILL.md} +9 -3
- package/skills/convergence-monitoring/README.md +8 -1
- package/skills/convergence-monitoring/SIGNALS.md +50 -6
- package/skills/convergence-monitoring/SKILL.md +15 -6
- package/skills/debug/SKILL.md +10 -0
- package/skills/discovery/SKILL.md +24 -1
- package/skills/discovery/probes-session.md +2 -2
- package/skills/dispatcher/SKILL.md +38 -7
- package/skills/eli5/SKILL.md +11 -0
- package/skills/eval/SKILL.md +52 -23
- package/skills/eval/rubric-v1.md +1 -0
- package/skills/eval/rubric-v2.md +457 -0
- package/skills/evolve/SKILL.md +9 -2
- package/skills/evolve/references/evolve-dialectic-mode.md +46 -25
- package/skills/gitlab-ops/SKILL.md +3 -2
- package/{commands/go.md → skills/go/SKILL.md} +9 -1
- package/skills/grill/SKILL.md +19 -0
- package/{commands/harness-audit.md → skills/harness-audit/SKILL.md} +7 -2
- package/skills/hook-development/SKILL.md +46 -41
- package/skills/memory-cleanup/SKILL.md +7 -0
- package/skills/npm-publish/SKILL.md +2 -2
- package/skills/persona-panel/SKILL.md +56 -1
- package/skills/persona-panel/persona-format.md +1 -1
- package/skills/plan/SKILL.md +28 -1
- package/{commands/portfolio.md → skills/portfolio/SKILL.md} +8 -2
- package/skills/reconcile/SKILL.md +21 -0
- package/{commands/release.md → skills/release/SKILL.md} +16 -2
- package/skills/repo-audit/SKILL.md +7 -0
- package/skills/session-end/SKILL.md +13 -16
- package/skills/session-end/discovery-scan.md +1 -1
- package/skills/session-end/phase-3-6-tail.md +55 -9
- package/skills/session-end/plan-verification.md +2 -2
- package/skills/session-end/references/phase-5-issue-cleanup.md +9 -14
- package/skills/session-end/session-metrics-write.md +10 -0
- package/skills/session-plan/SKILL.md +18 -6
- package/skills/session-plan/references/session-plan-task-classification.md +2 -2
- package/skills/session-start/SKILL.md +5 -4
- package/skills/session-start/phase-8-5-express-path.md +6 -6
- package/skills/session-start/references/phase-1-5-session-continuity.md +1 -1
- package/skills/session-start/references/phase-2-7-portfolio-snapshot.md +1 -1
- package/skills/session-start/references/phase-4-ssot-environment-check.md +6 -4
- package/skills/spinout/SKILL.md +12 -1
- package/skills/sunset-review/SKILL.md +13 -0
- package/{commands/test.md → skills/test/SKILL.md} +10 -4
- package/skills/ux-grill/SKILL.md +20 -2
- package/skills/wave-executor/SKILL.md +14 -7
- package/skills/wave-executor/circuit-breaker.md +2 -0
- package/skills/wave-executor/references/wave-executor-state-init.md +18 -4
- package/skills/wave-executor/references/wave-loop-dispatch.md +5 -2
- package/skills/wave-executor/references/wave-loop-review.md +17 -1
- package/commands/autopilot.md +0 -80
- package/commands/bootstrap.md +0 -56
- package/commands/brainstorm.md +0 -48
- package/commands/debug.md +0 -36
- package/commands/discovery.md +0 -32
- package/commands/dispatcher.md +0 -59
- package/commands/eli5.md +0 -33
- package/commands/eval.md +0 -28
- package/commands/evolve.md +0 -10
- package/commands/grill.md +0 -45
- package/commands/memory-cleanup.md +0 -26
- package/commands/persona-panel.md +0 -121
- package/commands/plan.md +0 -15
- package/commands/reconcile.md +0 -23
- package/commands/repo-audit.md +0 -24
- package/commands/spinout.md +0 -15
- package/commands/sunset-review.md +0 -27
- package/commands/ux-grill.md +0 -51
|
@@ -11,11 +11,18 @@
|
|
|
11
11
|
* (string in, findings out); the hook keeps stdin, config gate, sidecar
|
|
12
12
|
* resolution, sentinels and the three output channels.
|
|
13
13
|
*
|
|
14
|
-
*
|
|
14
|
+
* Four responsibilities, in the order the hook uses them:
|
|
15
15
|
* 1. `readTranscriptTail()` — the last N assistant records of ONE transcript
|
|
16
|
-
* JSONL, text blocks concatenated.
|
|
16
|
+
* JSONL, text blocks concatenated. This is the HOOK'S VIEW: what the
|
|
17
|
+
* agent SAID.
|
|
18
|
+
* 1b. `readTranscriptObservations()` — the vitest run summaries found in the
|
|
19
|
+
* `tool_result` blocks of the SAME byte window. This is what the agent
|
|
20
|
+
* actually RAN. A second reader rather than a second pattern, because
|
|
21
|
+
* `readTranscriptTail` collects assistant `text` blocks only and a
|
|
22
|
+
* `tool_result` lives in a `user` record.
|
|
17
23
|
* 2. `findViolations()` — distributional/repo-state claims that carry no
|
|
18
|
-
* adjacent measurement evidence
|
|
24
|
+
* adjacent measurement evidence, gate verdicts that carry no run receipt,
|
|
25
|
+
* and (R1) a claimed test count that contradicts every run observed.
|
|
19
26
|
* 3. `normalizeClaim()` / `dedupeViolations()` — one record per distinct
|
|
20
27
|
* claim, with an `occurrences` count (#1198: the fleet's worst repo held
|
|
21
28
|
* 3,360 records over 205 distinct `claim_text` values = duplication
|
|
@@ -25,7 +32,7 @@
|
|
|
25
32
|
* @module hooks/_lib/subagent-transcript
|
|
26
33
|
*/
|
|
27
34
|
|
|
28
|
-
import {
|
|
35
|
+
import { readTailWindow } from '../../scripts/lib/tail-window.mjs';
|
|
29
36
|
|
|
30
37
|
// ---------------------------------------------------------------------------
|
|
31
38
|
// Constants
|
|
@@ -33,6 +40,20 @@ import { promises as fs } from 'node:fs';
|
|
|
33
40
|
|
|
34
41
|
/** Number of trailing assistant records to scan. */
|
|
35
42
|
export const TAIL_RECORDS = 8;
|
|
43
|
+
/**
|
|
44
|
+
* Byte window `readTranscriptTail()` reads from the END of a transcript (#1388
|
|
45
|
+
* P4) — the ceiling on this deliberate simplification, per BV-004.
|
|
46
|
+
*
|
|
47
|
+
* Measured 2026-09-18 over 1016 real subagent transcripts: the byte span of the
|
|
48
|
+
* last TAIL_RECORDS assistant records was median 41 KB, p99 322 KB, max 1.25 MB
|
|
49
|
+
* (file size median 676 KB, max 8.2 MB). 2 MiB loses zero records in that
|
|
50
|
+
* sample; 1 MiB would already lose one.
|
|
51
|
+
*
|
|
52
|
+
* REVISIT TRIGGER: if TAIL_RECORDS grows, or a re-measurement puts the max span
|
|
53
|
+
* above ~1.5 MB, raise this window — a too-small window silently drops the
|
|
54
|
+
* OLDEST of the eight records rather than failing.
|
|
55
|
+
*/
|
|
56
|
+
export const TAIL_WINDOW_BYTES = 2 * 1024 * 1024;
|
|
36
57
|
/** Proximity window (in lines) for an adjacent grep transcript. */
|
|
37
58
|
export const GREP_PROXIMITY_LINES = 5;
|
|
38
59
|
/** Max characters of claim text persisted to the event record. */
|
|
@@ -198,6 +219,276 @@ const INLINE_CODE_RE = /`[^`\n]*`/g;
|
|
|
198
219
|
const GATE_SUMMARY_LINE_RE =
|
|
199
220
|
/\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|gruen|rot)\b|\bGate \d[\d.]*\/\d+\b|\b\d+\s+Wellen?,\s*\d+\s+Agents?\b|\bArbeitsbaum leer\b|\bmit Nachweis geschlossen\b/i;
|
|
200
221
|
|
|
222
|
+
/**
|
|
223
|
+
* GATE/DONE-VERDICT claim (#1397 w4-1) — the second claim class, and the one
|
|
224
|
+
* the exemption above structurally hid.
|
|
225
|
+
*
|
|
226
|
+
* `GATE_SUMMARY_LINE_RE` is CORRECT for what it was built for: it keeps the
|
|
227
|
+
* DISTRIBUTIONAL patterns off the harness's own completion output, where a
|
|
228
|
+
* number is a field value rather than an assertion (186/400 = 46.5% of sampled
|
|
229
|
+
* violations, #1198 D8). What it did NOT do is look at those lines for a claim
|
|
230
|
+
* of a DIFFERENT kind — so `STATUS: done` and "alles grün" were the two line
|
|
231
|
+
* shapes the scanner could never flag, which is precisely where "done" and
|
|
232
|
+
* "green" get claimed.
|
|
233
|
+
*
|
|
234
|
+
* Measured 2026-09-19 over a 130-case labelled done-claim corpus
|
|
235
|
+
* (`~/.cache/jev-eval/so/tasks/s2-done-claim.json`, gold `qid: "support"`):
|
|
236
|
+
* `findViolations()` answered `supported` on 128 of 130 cases — accuracy 30.2%,
|
|
237
|
+
* Cohen κ 0.007, statistically indistinguishable from guessing. Independent
|
|
238
|
+
* evidence from the same build: of 50 done/partial reports carrying neither a
|
|
239
|
+
* test count nor `exit 0`, 3 carry a bare gate-verdict claim with no command
|
|
240
|
+
* anywhere in the report.
|
|
241
|
+
*
|
|
242
|
+
* TWO separate triggers, both deliberately gap-free (no `[^\n]{0,N}` bridge) so
|
|
243
|
+
* a number or a verb between the noun and the verdict word breaks the match —
|
|
244
|
+
* "The gate ran green on 2026-07-29" and "CI-Pipeline #6995 grün" are prose
|
|
245
|
+
* about a past run, not a verdict being asserted now.
|
|
246
|
+
*/
|
|
247
|
+
const DONE_STATUS_RE =
|
|
248
|
+
/^\s*(?:[-*+>]\s+|#{1,6}\s+)*\**\s*STATUS\**\s*[:=]\s*\**\s*(?:done|partial|complete|abgeschlossen|fertig)\b/i;
|
|
249
|
+
|
|
250
|
+
const GATE_GREEN_RE = new RegExp(
|
|
251
|
+
[
|
|
252
|
+
// English verdicts
|
|
253
|
+
'\\b(?:all\\s+)?tests?\\s+(?:now\\s+)?pass(?:es|ed|ing)?\\b',
|
|
254
|
+
'\\b(?:suite|gate|build|pipeline|ci)\\s+(?:is\\s+|are\\s+)?green\\b',
|
|
255
|
+
'\\bgreen\\s+(?:gate|suite|build|pipeline)\\b',
|
|
256
|
+
'\\b(?:typecheck|lint|build|gate|suite)\\s+(?:is\\s+)?(?:clean|passes|passing)\\b',
|
|
257
|
+
'\\beverything\\s+(?:passes|is\\s+green)\\b',
|
|
258
|
+
// "<gate>: PASS|clean|green|OK" — the shape the code-implementer report
|
|
259
|
+
// template itself prescribes ("Verification — Typecheck: pass").
|
|
260
|
+
'\\b(?:tests?|typecheck|lint|gate|build|suite)\\s*:\\s*\\**\\s*(?:pass|passed|clean|green|ok)\\b',
|
|
261
|
+
// German verdicts
|
|
262
|
+
'\\balles\\s+gr(?:ü|ue)n\\b',
|
|
263
|
+
'\\balle\\s+Tests?\\s+gr(?:ü|ue)n\\b',
|
|
264
|
+
'\\bgr(?:ü|ue)ne(?:[rsn]|nes)?\\s+(?:Gate|Suite|Lauf)\\b',
|
|
265
|
+
'\\b(?:Full\\s+Gate|Gate|Suite)\\s*:?\\s*\\**\\s*(?:ist\\s+)?gr(?:ü|ue)n',
|
|
266
|
+
].join('|'),
|
|
267
|
+
'i'
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* A RUN RECEIPT — the counted result PSA-006 item 1-3 asks a gate claim to
|
|
272
|
+
* carry: a pass/fail count, a typecheck file count, an exit code, or a
|
|
273
|
+
* numerator/denominator green ratio. German forms alongside the English ones.
|
|
274
|
+
*
|
|
275
|
+
* Scope is the WHOLE scanned text, not the ±GREP_PROXIMITY_LINES window the
|
|
276
|
+
* distributional class uses, and that asymmetry is the point: a distributional
|
|
277
|
+
* claim is about ONE measurement and needs its transcript adjacent, while a
|
|
278
|
+
* done/gate verdict is about the whole report's work — a report that quotes
|
|
279
|
+
* `npm test` at the top and writes `STATUS: done` forty lines later IS
|
|
280
|
+
* evidenced. The window still applies to the command half via
|
|
281
|
+
* `nearIndex(measurementLines, …)`; this is the counted-result half.
|
|
282
|
+
*
|
|
283
|
+
* Deliberately LENIENT in the false-positive-safe direction: any exit code (not
|
|
284
|
+
* only `0`) counts, because the question this class asks is "was anything run
|
|
285
|
+
* at all?", never "did it pass?". A quoted `exit 1` means a run happened; the
|
|
286
|
+
* verdict's truth is the coordinator's judgement, not a regex's.
|
|
287
|
+
*
|
|
288
|
+
* `(?<![A-Za-z])exit` rather than `\bexit`: the shells here emit `LINT_EXIT=0`
|
|
289
|
+
* / `TYPECHECK_EXIT=0` / `VITEST_EXIT=0`, where `_` is a word character and
|
|
290
|
+
* `\b` would therefore NOT match.
|
|
291
|
+
*
|
|
292
|
+
* All quantifiers bounded ({0,20}) — linear-time, ReDoS-safe.
|
|
293
|
+
*/
|
|
294
|
+
const RUN_RECEIPT_RE = new RegExp(
|
|
295
|
+
[
|
|
296
|
+
'\\b\\d+\\s+(?:passed|failed|bestanden|fehlgeschlagen|skipped)\\b',
|
|
297
|
+
'\\b\\d+\\s+file(?:\\(s\\)|s)?\\s+OK\\b',
|
|
298
|
+
'(?<![A-Za-z])exit(?:\\s*code)?\\s*[=:]?\\s*\\d+',
|
|
299
|
+
'\\b\\d+\\s*/\\s*\\d+\\b[^\\n]{0,20}?\\b(?:passed|green|gr(?:ü|ue)n|tests?)\\b',
|
|
300
|
+
'\\b(?:passed|failed)\\s*[=:]\\s*\\d+',
|
|
301
|
+
].join('|'),
|
|
302
|
+
'i'
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
/** Claim-class discriminators carried on every violation record. */
|
|
306
|
+
export const KIND_DISTRIBUTIONAL = 'distributional';
|
|
307
|
+
export const KIND_GATE_VERDICT = 'gate-verdict';
|
|
308
|
+
/**
|
|
309
|
+
* CLAIM-MISMATCH (#1385 R1) — the third class, and the one that closes an
|
|
310
|
+
* INVERSION the other two leave standing.
|
|
311
|
+
*
|
|
312
|
+
* `RUN_RECEIPT_RE` is report-wide and knows `\d+\s+passed`, so the number an
|
|
313
|
+
* agent asserts is accepted as its own receipt. Measured on this module at
|
|
314
|
+
* HEAD before this change:
|
|
315
|
+
*
|
|
316
|
+
* 'STATUS: done\nTests pass: 5129 passed / 0 failed.' -> []
|
|
317
|
+
* 'STATUS: done\nAlles grün.' -> ['gate-verdict','gate-verdict']
|
|
318
|
+
*
|
|
319
|
+
* An invented number therefore DISARMS the guard, while an honest report with
|
|
320
|
+
* no number is flagged. This class reads the `tool_result` side of the same
|
|
321
|
+
* window and compares: the claim is only a violation when runs WERE observed
|
|
322
|
+
* and NONE of them carries the asserted pass count.
|
|
323
|
+
*
|
|
324
|
+
* Deliberately NOT built (BV-001.1): the SHA-claim check. Measured 2026-09-19
|
|
325
|
+
* by w1-3 over 1037 real subagent transcripts — 244 carried a SHA claim, 103
|
|
326
|
+
* distinct tokens, 5 unresolvable in-repo, and all 5 were cross-repo
|
|
327
|
+
* artefacts, 0 hallucinations. A check there produces only false alarms.
|
|
328
|
+
*/
|
|
329
|
+
export const KIND_CLAIM_MISMATCH = 'claim-mismatch';
|
|
330
|
+
/** `mismatch` sub-kind: a claimed test COUNT no observed run carries. */
|
|
331
|
+
export const MISMATCH_COUNT = 'count';
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Commands whose `tool_result` may carry a vitest run summary. Restricted to
|
|
335
|
+
* vitest ON PURPOSE: `validate-plugin`'s own tally ("229 passed, 0 failed")
|
|
336
|
+
* supplied 5 of the 12 false-alarm candidates in w1-3's pre-measurement, and
|
|
337
|
+
* admitting a second counter means two number vocabularies in one comparison.
|
|
338
|
+
*
|
|
339
|
+
* Structurally reinforced by OBSERVED_SUMMARY_RE below, which demands vitest's
|
|
340
|
+
* `Tests N passed (M)` shape — validate-plugin's comma form cannot match it
|
|
341
|
+
* even when it prints inside a `npx vitest` invocation (it is the suite's
|
|
342
|
+
* globalSetup).
|
|
343
|
+
*/
|
|
344
|
+
const VITEST_CMD_RE = /\b(?:npm\s+(?:run\s+)?test|pnpm\s+(?:run\s+)?test|npx\s+vitest|vitest\s+run)\b/;
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Tools whose `tool_result` may carry a run summary even though the tool is
|
|
348
|
+
* not the runner. Measured 2026-09-19 over 1044 real subagent transcripts:
|
|
349
|
+
* the dominant local idiom is `npm test > run.log 2>&1` followed by a SEPARATE
|
|
350
|
+
* step that surfaces the log — `Read` on the log file (2 of 9 residual
|
|
351
|
+
* firings) or `BashOutput` on a backgrounded run (1 of 9). Keyed on the TOOL
|
|
352
|
+
* NAME because neither carries the runner command in its own input.
|
|
353
|
+
*
|
|
354
|
+
* Admitting these can only ever CLEAR a claim, never raise one — an extra
|
|
355
|
+
* observation adds a number the claim may match. NAMED CEILING (BV-004): an
|
|
356
|
+
* agent that WRITES a file containing a fabricated `Tests N passed (N)` line
|
|
357
|
+
* and then `Read`s it back manufactures its own observation. Accepted: that
|
|
358
|
+
* is a deliberate two-step forgery, where the class's target is the ordinary
|
|
359
|
+
* one-step invented number. REVISIT TRIGGER: one observed case of a claim
|
|
360
|
+
* cleared by a self-authored file.
|
|
361
|
+
*/
|
|
362
|
+
const RECEIPT_BEARING_TOOLS = new Set(['BashOutput', 'Read']);
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* A vitest run summary line: `Tests 15419 passed | 11 skipped (15430)`.
|
|
366
|
+
*
|
|
367
|
+
* Anchored at line start so `Test Files 612 passed (612)` (a FILE count, not
|
|
368
|
+
* a test count) and vitest's `⎯⎯ Failed Tests 1 ⎯⎯` banner cannot match — the
|
|
369
|
+
* former because `Tests` is not followed by whitespace there, the latter
|
|
370
|
+
* because the line does not begin with it.
|
|
371
|
+
*
|
|
372
|
+
* The optional `30:` / ` 30→` prefix is NOT cosmetic: the log-reading idiom
|
|
373
|
+
* above surfaces the summary through `grep -n`, `cat -n` or `Read`, all of
|
|
374
|
+
* which prepend a line number. Without it the anchor missed a real receipt
|
|
375
|
+
* that sat in the window — 2 of the 9 residual firings measured 2026-09-19
|
|
376
|
+
* read `"30: Tests 1 failed | 1338 passed (1339)"` verbatim.
|
|
377
|
+
*
|
|
378
|
+
* All quantifiers bounded — linear-time, ReDoS-safe.
|
|
379
|
+
*/
|
|
380
|
+
const OBSERVED_SUMMARY_RE =
|
|
381
|
+
/^[^\S\n]{0,16}(?:\d{1,7}[:|→\t][^\S\n]{0,8})?Tests[^\S\n]{1,8}([^\n(]{0,120}?)[^\S\n]{0,8}\((\d{1,9})\)[^\S\n]{0,8}$/gm;
|
|
382
|
+
const PASSED_COUNT_RE = /(\d{1,9})\s+passed\b/i;
|
|
383
|
+
const FAILED_COUNT_RE = /(\d{1,9})\s+failed\b/i;
|
|
384
|
+
|
|
385
|
+
/** ANSI SGR sequences, stripped before a summary line is matched. */
|
|
386
|
+
// eslint-disable-next-line no-control-regex
|
|
387
|
+
const ANSI_SGR_RE = /\u001b\[[0-9;]{0,16}m/g;
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Digit-group separators admitted in a claimed count and stripped before it is
|
|
391
|
+
* parsed: ASCII dot/comma plus NO-BREAK SPACE (U+00A0) and NARROW NO-BREAK
|
|
392
|
+
* SPACE (U+202F), the two `toLocaleString` emits for de-AT / fr grouping.
|
|
393
|
+
*
|
|
394
|
+
* Written as ESCAPES inside a plain string, never as the literal characters.
|
|
395
|
+
* Both forms behave identically at runtime and only one survives review: the
|
|
396
|
+
* literal pair tripped `no-irregular-whitespace` AND is invisible on the page,
|
|
397
|
+
* the same property that makes `validate-plugin`'s dangerous-invisible check
|
|
398
|
+
* reject a U+200B. Defined ABOVE its first use — `CLAIMED_COUNT_RE` reads it
|
|
399
|
+
* at module-evaluation time, and a `const` referenced from above is a TDZ
|
|
400
|
+
* ReferenceError that `node --check` does not catch (only an import probe
|
|
401
|
+
* does; see `.claude/rules/toolchain-and-build.md`).
|
|
402
|
+
*/
|
|
403
|
+
const THOUSANDS_SEP_CLASS = '[.,\\u00a0\\u202f]';
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* A CLAIMED pass count in the agent's own prose: `Tests pass: 5129 passed`,
|
|
407
|
+
* `51 tests passed`, `14,340 passed`. Global — a line routinely names SEVERAL
|
|
408
|
+
* (`2 passed files; 70 tests passed`), and treating only the first as "the"
|
|
409
|
+
* claim is what produced most of the measured false alarms (below).
|
|
410
|
+
*
|
|
411
|
+
* Three shapes the corpus forced, each measured 2026-09-19 over 1044 real
|
|
412
|
+
* subagent transcripts (`~/.claude/projects/<slug>/<session>/subagents/`):
|
|
413
|
+
*
|
|
414
|
+
* 1. THOUSANDS SEPARATORS. `14,340 passed` read as `340` under a bare
|
|
415
|
+
* `\d{1,9}`, inventing a mismatch out of a parse error — 3 of the first
|
|
416
|
+
* 12 samples. The group admits `1.091` / `14,340` / `14 340` and the
|
|
417
|
+
* separators are stripped before `Number()`.
|
|
418
|
+
* 2. AN INTERVENING NOUN. `51 tests passed` hid the only number on the line
|
|
419
|
+
* that WAS observed.
|
|
420
|
+
* 3. `passed` AS AN ORDINARY VERB. "the 8 test call sites at lines 715 …
|
|
421
|
+
* and 1080 passed bodies" is not a test result at all — `1080` is a line
|
|
422
|
+
* number and `passed` takes an object. The trailing lookahead therefore
|
|
423
|
+
* requires a RESULT context after the word: end of line, punctuation, a
|
|
424
|
+
* digit, or a short connector. The same lookahead subsumes the file
|
|
425
|
+
* tally (`2 passed files`), whose column-header form `Test Files` is
|
|
426
|
+
* caught by COUNT_CLAIM_EXCLUSION_RES.
|
|
427
|
+
*
|
|
428
|
+
* All quantifiers bounded — linear-time, ReDoS-safe.
|
|
429
|
+
*/
|
|
430
|
+
const CLAIMED_COUNT_RE = new RegExp(
|
|
431
|
+
String.raw`(\d{1,3}(?:${THOUSANDS_SEP_CLASS}\d{3})+|\d{1,9})\s{1,4}(?:tests?\s{1,4})?passed\b` +
|
|
432
|
+
String.raw`(?=\s{0,4}(?:$|[,.;:/|)*\]!—-]|\d|(?:and|und|in|with|on|at|after|before|for|across|under|exit)\b))`,
|
|
433
|
+
'gi'
|
|
434
|
+
);
|
|
435
|
+
const THOUSANDS_SEP_RE = new RegExp(THOUSANDS_SEP_CLASS, 'g');
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Every pass count asserted on one line, in encounter order.
|
|
439
|
+
*
|
|
440
|
+
* @param {string} line
|
|
441
|
+
* @returns {number[]}
|
|
442
|
+
*/
|
|
443
|
+
function extractClaimedCounts(line) {
|
|
444
|
+
const out = [];
|
|
445
|
+
CLAIMED_COUNT_RE.lastIndex = 0;
|
|
446
|
+
for (const m of line.matchAll(CLAIMED_COUNT_RE)) {
|
|
447
|
+
const n = Number(m[1].replace(THOUSANDS_SEP_RE, ''));
|
|
448
|
+
if (Number.isFinite(n)) out.push(n);
|
|
449
|
+
}
|
|
450
|
+
return out;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Lines that carry `N passed` WITHOUT asserting this run's test count. Each
|
|
455
|
+
* entry is one of the four false-alarm forms w1-3 measured; a fifth would go
|
|
456
|
+
* here rather than into a widened threshold (HR-101 / development.md §
|
|
457
|
+
* Guard & Threshold Design — category separation, never a looser number).
|
|
458
|
+
*
|
|
459
|
+
* 1. A DIFFERENT instrument with the same word shape — vitest's own file
|
|
460
|
+
* tally, `validate-plugin`, the `check-rules`/`check-skills`/`check-docs`
|
|
461
|
+
* validators, a `Results:` summary line.
|
|
462
|
+
* 2. A QUOTED foreign assertion — "the reviewer claimed 5129 passed".
|
|
463
|
+
* 3. A RED-BEFORE-FIX number — the fake-regression proof a bugfix owes
|
|
464
|
+
* (`.claude/rules/testing.md` § Negative-Assertion Fake-Regression
|
|
465
|
+
* Check) names a count that deliberately does NOT match the green run.
|
|
466
|
+
* The marker list is wider than the briefed four because the corpus said
|
|
467
|
+
* so: the first real hit read "RED proof: mutated peer-discovery loader
|
|
468
|
+
* — 2 failed, 0 passed", which `mutation` and `red before` both miss.
|
|
469
|
+
* 4. A JSON key line — a field value, not an assertion (same reasoning as
|
|
470
|
+
* CONFIG_KEY_LINE_RE above, narrowed to the quoted-key form).
|
|
471
|
+
*/
|
|
472
|
+
const COUNT_CLAIM_EXCLUSION_RES = [
|
|
473
|
+
/Test\s+Files/i,
|
|
474
|
+
/validate[-_]plugin/i,
|
|
475
|
+
/check[-_](?:rules|skills|docs)/i,
|
|
476
|
+
/Results:/i,
|
|
477
|
+
/^\s*"[^"\n]{1,64}"\s*:/,
|
|
478
|
+
/\b(?:red before|red proof|rot vor|vor dem Fix|before the fix|mutat(?:e|ed|es|ing|ion)|Fake-Regression|baseline|Rot-Lauf|Rot-Beweis)\b/i,
|
|
479
|
+
/\b(?:claimed|stated|behauptet|laut)\b/i,
|
|
480
|
+
];
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* Cap on observations carried into the pair-sum search below.
|
|
484
|
+
*
|
|
485
|
+
* NAMED CEILING (BV-004): the partial-run match is O(n²) over this list. 50
|
|
486
|
+
* keeps the worst case at 1,225 additions — free beside the regex passes this
|
|
487
|
+
* module already makes. REVISIT TRIGGER: a transcript whose window holds more
|
|
488
|
+
* than 50 vitest summaries AND a claim that only the dropped ones explain.
|
|
489
|
+
*/
|
|
490
|
+
const MAX_OBSERVATIONS = 50;
|
|
491
|
+
|
|
201
492
|
/**
|
|
202
493
|
* NON-PROSE structural lines (#1218 negative-context guard). A PSA-006 claim is
|
|
203
494
|
* an ASSERTION in prose; these four shapes are not prose at all, and every one
|
|
@@ -239,6 +530,20 @@ const NON_PROSE_LINE_RE = new RegExp(
|
|
|
239
530
|
'i'
|
|
240
531
|
);
|
|
241
532
|
|
|
533
|
+
/**
|
|
534
|
+
* The same guard MINUS the heading rule, for the gate/done-verdict class. The
|
|
535
|
+
* #1218 heading exemption was measured on DISTRIBUTIONAL claims, where a
|
|
536
|
+
* heading labels a section and the claim is restated in the body below. A gate
|
|
537
|
+
* verdict is the opposite: `### Tests: PASS` and `## Wave 3 Complete — Gate: …`
|
|
538
|
+
* ARE the verdict, and nothing restates them. Table rows, separators and
|
|
539
|
+
* plan/intent items stay excluded for both classes — a task-list `- [ ] make
|
|
540
|
+
* the suite green` is intent, and a status-matrix cell is structured data.
|
|
541
|
+
*/
|
|
542
|
+
const NON_PROSE_NO_HEADING_RE = new RegExp(
|
|
543
|
+
`(${TABLE_ROW_RE.source})|(${TABLE_SEPARATOR_RE.source})|(${PLAN_INTENT_RE.source})`,
|
|
544
|
+
'i'
|
|
545
|
+
);
|
|
546
|
+
|
|
242
547
|
/**
|
|
243
548
|
* CONFIG / SCORE / VERSION literal line (#1218). A `key: <scalar>` line is a
|
|
244
549
|
* configuration or JSON fragment, not an assertion — the number is a SETTING.
|
|
@@ -317,24 +622,52 @@ const LEADING_MARKER_RE = /^(?:\s*(?:[-*+•]|\d{1,3}[.)]|#{1,6}|>)\s+)+/;
|
|
|
317
622
|
* @param {string} transcriptPath
|
|
318
623
|
* @returns {Promise<string>}
|
|
319
624
|
*/
|
|
320
|
-
|
|
321
|
-
|
|
625
|
+
/**
|
|
626
|
+
* Parse the bounded tail window of `transcriptPath` into JSONL records.
|
|
627
|
+
*
|
|
628
|
+
* Shared by both readers below; each still performs its own window read, so a
|
|
629
|
+
* hook that wants both pays two ≤2 MiB reads. Accepted (BV-004): the hook runs
|
|
630
|
+
* once per SubagentStop and the page cache serves the second read.
|
|
631
|
+
*
|
|
632
|
+
* @param {string} transcriptPath
|
|
633
|
+
* @returns {object[]} parsed records, oldest first; `[]` on any failure
|
|
634
|
+
*/
|
|
635
|
+
function readTailRecords(transcriptPath) {
|
|
636
|
+
if (typeof transcriptPath !== 'string' || !transcriptPath) return [];
|
|
322
637
|
let raw;
|
|
638
|
+
let cut;
|
|
323
639
|
try {
|
|
324
|
-
|
|
640
|
+
// Bounded read (#1388 P4): the whole transcript used to be decoded and
|
|
641
|
+
// JSON-parsed line by line only to keep the last 8 assistant records.
|
|
642
|
+
({ text: raw, cut } = readTailWindow(transcriptPath, TAIL_WINDOW_BYTES));
|
|
325
643
|
} catch {
|
|
326
|
-
|
|
644
|
+
// Every fs error (ENOENT, /dev/null EACCES, …) maps to empty —
|
|
645
|
+
// readTailWindow THROWS where the former fs.readFile catch swallowed, and
|
|
646
|
+
// the callers (post-subagent-discovery-validator) rely on that contract.
|
|
647
|
+
return [];
|
|
327
648
|
}
|
|
328
|
-
if (!raw.trim()) return
|
|
649
|
+
if (!raw.trim()) return [];
|
|
650
|
+
|
|
651
|
+
const lines = raw.split(/\r?\n/);
|
|
652
|
+
// `cut` means the window did not start at byte 0, so line 0 is (or may be) a
|
|
653
|
+
// record fragment, possibly severed mid-UTF-8. Drop it explicitly rather than
|
|
654
|
+
// leaning on the JSON.parse catch below: a truncated record can still parse.
|
|
655
|
+
if (cut) lines.shift();
|
|
329
656
|
|
|
330
|
-
const
|
|
331
|
-
for (const line of
|
|
657
|
+
const records = [];
|
|
658
|
+
for (const line of lines) {
|
|
332
659
|
const trimmed = line.trim();
|
|
333
660
|
if (!trimmed) continue;
|
|
334
661
|
let rec;
|
|
335
662
|
try { rec = JSON.parse(trimmed); } catch { continue; }
|
|
336
|
-
if (rec
|
|
663
|
+
if (rec) records.push(rec);
|
|
337
664
|
}
|
|
665
|
+
return records;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
export async function readTranscriptTail(transcriptPath) {
|
|
669
|
+
const assistantRecords = readTailRecords(transcriptPath).filter((r) => r.type === 'assistant');
|
|
670
|
+
if (assistantRecords.length === 0) return '';
|
|
338
671
|
|
|
339
672
|
const tail = assistantRecords.slice(-TAIL_RECORDS);
|
|
340
673
|
const textBlocks = [];
|
|
@@ -350,6 +683,91 @@ export async function readTranscriptTail(transcriptPath) {
|
|
|
350
683
|
return textBlocks.join('\n');
|
|
351
684
|
}
|
|
352
685
|
|
|
686
|
+
/**
|
|
687
|
+
* Flatten a `tool_result` block's content to text. Measured on a real
|
|
688
|
+
* transcript (2026-09-19, `~/.claude/projects/<slug>/<session>/subagents/`):
|
|
689
|
+
* `content` is a plain STRING on the Bash results sampled, and the array-of-
|
|
690
|
+
* blocks form is the documented alternative — both are handled, because a
|
|
691
|
+
* reader that knows only one shape silently observes nothing and every claim
|
|
692
|
+
* then reads as unobserved (the fail-OPEN direction is correct here, but a
|
|
693
|
+
* shape gap would make this whole class inert without saying so).
|
|
694
|
+
*
|
|
695
|
+
* @param {*} content
|
|
696
|
+
* @returns {string}
|
|
697
|
+
*/
|
|
698
|
+
function toolResultText(content) {
|
|
699
|
+
if (typeof content === 'string') return content;
|
|
700
|
+
if (!Array.isArray(content)) return '';
|
|
701
|
+
const parts = [];
|
|
702
|
+
for (const block of content) {
|
|
703
|
+
if (typeof block === 'string') parts.push(block);
|
|
704
|
+
else if (block && block.type === 'text' && typeof block.text === 'string') parts.push(block.text);
|
|
705
|
+
}
|
|
706
|
+
return parts.join('\n');
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
/**
|
|
710
|
+
* Read the vitest run summaries the agent ACTUALLY produced inside the same
|
|
711
|
+
* bounded tail window `readTranscriptTail()` reads.
|
|
712
|
+
*
|
|
713
|
+
* A second reader rather than a second pattern: `readTranscriptTail()`
|
|
714
|
+
* collects assistant `text` blocks, and a `tool_result` lives in a `user`
|
|
715
|
+
* record — the two never meet. The `tool_use` → `tool_result` join is by
|
|
716
|
+
* `tool_use_id`, so only the results of a vitest Bash command (or of the
|
|
717
|
+
* receipt-bearing tools above) are read; a `git log` result that happens to
|
|
718
|
+
* contain the word "passed" is not an observation.
|
|
719
|
+
*
|
|
720
|
+
* Returns at most `MAX_OBSERVATIONS` entries, newest last.
|
|
721
|
+
*
|
|
722
|
+
* @param {string} transcriptPath
|
|
723
|
+
* @returns {Promise<{passed: number, failed: number, total: number}[]>}
|
|
724
|
+
*/
|
|
725
|
+
export async function readTranscriptObservations(transcriptPath) {
|
|
726
|
+
const records = readTailRecords(transcriptPath);
|
|
727
|
+
if (records.length === 0) return [];
|
|
728
|
+
|
|
729
|
+
/** @type {Set<string>} tool_use ids whose result may carry a run summary */
|
|
730
|
+
const receiptToolUseIds = new Set();
|
|
731
|
+
for (const rec of records) {
|
|
732
|
+
const content = rec?.message?.content;
|
|
733
|
+
if (!Array.isArray(content)) continue;
|
|
734
|
+
for (const block of content) {
|
|
735
|
+
if (!block || block.type !== 'tool_use') continue;
|
|
736
|
+
if (typeof block.id !== 'string' || !block.id) continue;
|
|
737
|
+
if (RECEIPT_BEARING_TOOLS.has(block.name)) { receiptToolUseIds.add(block.id); continue; }
|
|
738
|
+
if (block.name !== 'Bash') continue;
|
|
739
|
+
const cmd = block?.input?.command;
|
|
740
|
+
if (typeof cmd === 'string' && VITEST_CMD_RE.test(cmd)) receiptToolUseIds.add(block.id);
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
if (receiptToolUseIds.size === 0) return [];
|
|
744
|
+
|
|
745
|
+
const observations = [];
|
|
746
|
+
for (const rec of records) {
|
|
747
|
+
const content = rec?.message?.content;
|
|
748
|
+
if (!Array.isArray(content)) continue;
|
|
749
|
+
for (const block of content) {
|
|
750
|
+
if (!block || block.type !== 'tool_result') continue;
|
|
751
|
+
if (!receiptToolUseIds.has(block.tool_use_id)) continue;
|
|
752
|
+
const text = toolResultText(block.content).replace(ANSI_SGR_RE, '');
|
|
753
|
+
if (!text) continue;
|
|
754
|
+
OBSERVED_SUMMARY_RE.lastIndex = 0;
|
|
755
|
+
for (const m of text.matchAll(OBSERVED_SUMMARY_RE)) {
|
|
756
|
+
const body = m[1] ?? '';
|
|
757
|
+
const passedMatch = PASSED_COUNT_RE.exec(body);
|
|
758
|
+
if (!passedMatch) continue; // `Tests no tests (0)` and friends
|
|
759
|
+
const failedMatch = FAILED_COUNT_RE.exec(body);
|
|
760
|
+
observations.push({
|
|
761
|
+
passed: Number(passedMatch[1]),
|
|
762
|
+
failed: failedMatch ? Number(failedMatch[1]) : 0,
|
|
763
|
+
total: Number(m[2]),
|
|
764
|
+
});
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
return observations.slice(-MAX_OBSERVATIONS);
|
|
769
|
+
}
|
|
770
|
+
|
|
353
771
|
// ---------------------------------------------------------------------------
|
|
354
772
|
// claim scanning
|
|
355
773
|
// ---------------------------------------------------------------------------
|
|
@@ -494,57 +912,190 @@ export function normalizeClaim(text) {
|
|
|
494
912
|
}
|
|
495
913
|
|
|
496
914
|
/**
|
|
497
|
-
* Collapse repeated claims into one entry per distinct normalized key,
|
|
915
|
+
* Collapse repeated claims into one entry per distinct (kind, normalized key),
|
|
498
916
|
* preserving first-seen order and counting `occurrences` (#1198).
|
|
499
917
|
*
|
|
500
|
-
*
|
|
501
|
-
*
|
|
918
|
+
* Accepts a bare string (kind defaults to `distributional`) or a
|
|
919
|
+
* `{claim, kind, detail?}` record — the claim classes never merge into one
|
|
920
|
+
* entry even when they land on the identical line, because a coordinator
|
|
921
|
+
* triaging the ledger needs to know WHICH rule the line broke.
|
|
922
|
+
*
|
|
923
|
+
* `detail` (the `claim-mismatch` class's `{mismatch, claimed, observed,
|
|
924
|
+
* observed_n}` payload) rides along FIRST-SEEN-WINS: a repeat of the same
|
|
925
|
+
* normalized line was compared against the same observation set, so a second
|
|
926
|
+
* copy would carry identical numbers.
|
|
927
|
+
*
|
|
928
|
+
* @param {(string|{claim: string, kind?: string, detail?: object})[]} claims — in encounter order
|
|
929
|
+
* @returns {{claim: string, normalized: string, occurrences: number, kind: string, detail?: object}[]}
|
|
502
930
|
*/
|
|
503
931
|
export function dedupeViolations(claims) {
|
|
504
|
-
/** @type {Map<string, {claim: string, normalized: string, occurrences: number}>} */
|
|
932
|
+
/** @type {Map<string, {claim: string, normalized: string, occurrences: number, kind: string, detail?: object}>} */
|
|
505
933
|
const byKey = new Map();
|
|
506
|
-
for (const
|
|
934
|
+
for (const entry of claims) {
|
|
935
|
+
const claim = typeof entry === 'string' ? entry : entry?.claim;
|
|
936
|
+
const kind = (typeof entry === 'string' ? undefined : entry?.kind) ?? KIND_DISTRIBUTIONAL;
|
|
937
|
+
const detail = typeof entry === 'string' ? undefined : entry?.detail;
|
|
938
|
+
if (typeof claim !== 'string') continue;
|
|
507
939
|
const normalized = normalizeClaim(claim);
|
|
508
940
|
if (!normalized) continue;
|
|
509
|
-
const
|
|
941
|
+
const key = `${kind}\u0000${normalized}`;
|
|
942
|
+
const hit = byKey.get(key);
|
|
510
943
|
if (hit) { hit.occurrences += 1; continue; }
|
|
511
|
-
byKey.set(
|
|
944
|
+
byKey.set(key, {
|
|
945
|
+
claim,
|
|
946
|
+
normalized,
|
|
947
|
+
occurrences: 1,
|
|
948
|
+
kind,
|
|
949
|
+
...(detail !== undefined ? { detail } : {}),
|
|
950
|
+
});
|
|
512
951
|
}
|
|
513
952
|
return [...byKey.values()];
|
|
514
953
|
}
|
|
515
954
|
|
|
955
|
+
/**
|
|
956
|
+
* ANY-MATCH test for a claimed pass count against the observed runs (R1).
|
|
957
|
+
*
|
|
958
|
+
* "Any" is the whole design: a report legitimately quotes ONE of several runs
|
|
959
|
+
* it made, so a claim is evidenced the moment a single observation carries its
|
|
960
|
+
* number. Only a claim that matches NONE of them is a mismatch.
|
|
961
|
+
*
|
|
962
|
+
* PARTIAL RUNS: two observations may be summed, because splitting a suite over
|
|
963
|
+
* two `npx vitest run <files>` invocations and reporting the total is normal
|
|
964
|
+
* here. NAMED CEILING (BV-004): PAIRS ONLY — three-way sums are not searched,
|
|
965
|
+
* since admitting them makes almost any number reachable from a handful of
|
|
966
|
+
* runs and the class stops discriminating. REVISIT TRIGGER: one documented
|
|
967
|
+
* report whose honest total is the sum of three separate runs.
|
|
968
|
+
*
|
|
969
|
+
* @param {number} claimedPassed
|
|
970
|
+
* @param {{passed: number}[]} observations
|
|
971
|
+
* @returns {boolean} true when some observation (or observation PAIR) carries it
|
|
972
|
+
*/
|
|
973
|
+
function observationsCarryCount(claimedPassed, observations) {
|
|
974
|
+
for (const o of observations) {
|
|
975
|
+
if (o.passed === claimedPassed) return true;
|
|
976
|
+
}
|
|
977
|
+
for (let i = 0; i < observations.length; i++) {
|
|
978
|
+
for (let j = i + 1; j < observations.length; j++) {
|
|
979
|
+
if (observations[i].passed + observations[j].passed === claimedPassed) return true;
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
return false;
|
|
983
|
+
}
|
|
984
|
+
|
|
516
985
|
/**
|
|
517
986
|
* Scan concatenated transcript text for claims lacking an adjacent measurement
|
|
518
987
|
* block (within ±GREP_PROXIMITY_LINES).
|
|
519
988
|
*
|
|
989
|
+
* THREE claim classes, reported through one list and told apart by `kind`:
|
|
990
|
+
* - `distributional` (#567/#908/#1211) — "4 of 4 callers", "14 commits".
|
|
991
|
+
* Evidence must be ADJACENT (±GREP_PROXIMITY_LINES).
|
|
992
|
+
* - `gate-verdict` (w4-1) — "STATUS: done", "alles grün", "Tests: PASS".
|
|
993
|
+
* Evidence is an adjacent measurement command OR a RUN RECEIPT anywhere in
|
|
994
|
+
* the report (see RUN_RECEIPT_RE for why the scopes differ).
|
|
995
|
+
* - `claim-mismatch` (#1385 R1) — "5129 passed" where every vitest run in
|
|
996
|
+
* the window reported a different count. Evidence is the `tool_result`
|
|
997
|
+
* side of the transcript, supplied by the caller as `observations`.
|
|
998
|
+
*
|
|
520
999
|
* @param {string} text
|
|
521
|
-
* @
|
|
1000
|
+
* @param {object} [opts]
|
|
1001
|
+
* @param {{passed: number, failed: number, total: number}[]} [opts.observations]
|
|
1002
|
+
* vitest run summaries from `readTranscriptObservations()`. DEFAULT EMPTY,
|
|
1003
|
+
* and an empty list disables the `claim-mismatch` class entirely — absence
|
|
1004
|
+
* of evidence is `gate-verdict`'s job, never this one's. (Measured
|
|
1005
|
+
* 2026-09-19: 488 of 503 transcripts carrying a count claim had at least one
|
|
1006
|
+
* vitest summary in the window, so the disabled case is the rare one.)
|
|
1007
|
+
* @returns {{ violations: {claim: string, normalized: string, occurrences: number, kind: string, detail?: object}[], undatedVerified: number }}
|
|
522
1008
|
* `violations` — deduplicated, truncated claim snippets with an occurrence
|
|
523
|
-
* count
|
|
524
|
-
* measurement timestamp (advisory).
|
|
1009
|
+
* count and a claim-class `kind`; `undatedVerified` — count of DISTRIBUTIONAL
|
|
1010
|
+
* claims that ARE verified but carry no measurement timestamp (advisory).
|
|
525
1011
|
*/
|
|
526
|
-
export function findViolations(text) {
|
|
1012
|
+
export function findViolations(text, opts = {}) {
|
|
527
1013
|
if (!text) return { violations: [], undatedVerified: 0 };
|
|
1014
|
+
const observations = Array.isArray(opts.observations) ? opts.observations : [];
|
|
528
1015
|
const lines = text.split(/\r?\n/);
|
|
529
1016
|
const { measurementLines, fencedLines } = scanFences(lines);
|
|
530
1017
|
const configLines = scanConfigBlocks(lines);
|
|
1018
|
+
// Report-wide, computed ONCE: the counted-result half of the gate class's
|
|
1019
|
+
// evidence test. Costs one regex pass over the tail, not one per line.
|
|
1020
|
+
const hasRunReceipt = RUN_RECEIPT_RE.test(text);
|
|
531
1021
|
const raw = [];
|
|
532
1022
|
let undatedVerified = 0;
|
|
533
1023
|
|
|
534
1024
|
for (let i = 0; i < lines.length; i++) {
|
|
535
1025
|
const line = lines[i];
|
|
536
|
-
// #1198 FIX
|
|
537
|
-
//
|
|
1026
|
+
// #1198 FIX 3 (masking-order bug): mask inline-code spans ONCE, then test
|
|
1027
|
+
// every pattern against the masked text, so a claim quoted entirely inside
|
|
1028
|
+
// backticks (evidence or example text, not an assertion) cannot trip one.
|
|
1029
|
+
const masked = line.replace(INLINE_CODE_RE, ' ');
|
|
1030
|
+
|
|
1031
|
+
// --- gate/done-verdict class -------------------------------------------
|
|
1032
|
+
// Runs BEFORE the GATE_SUMMARY_LINE_RE skip BY DESIGN: that skip exists to
|
|
1033
|
+
// keep the DISTRIBUTIONAL patterns off harness gate output, and the two
|
|
1034
|
+
// line shapes it exempts are exactly the ones this class must see.
|
|
1035
|
+
if (
|
|
1036
|
+
!fencedLines.has(i) &&
|
|
1037
|
+
!NON_PROSE_NO_HEADING_RE.test(line) &&
|
|
1038
|
+
(DONE_STATUS_RE.test(masked) || GATE_GREEN_RE.test(masked)) &&
|
|
1039
|
+
!hasRunReceipt &&
|
|
1040
|
+
!nearIndex(measurementLines, i)
|
|
1041
|
+
) {
|
|
1042
|
+
raw.push({ claim: line.trim().slice(0, CLAIM_TEXT_MAX), kind: KIND_GATE_VERDICT });
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
// --- claim-mismatch class (#1385 R1) -----------------------------------
|
|
1046
|
+
// Also BEFORE the GATE_SUMMARY_LINE_RE skip, and for the same reason the
|
|
1047
|
+
// gate class is: that skip exempts exactly the `N passed / M failed` line
|
|
1048
|
+
// shape this class must read. The distributional patterns keep the skip
|
|
1049
|
+
// untouched (#1198 — 46.5% of a 400-event false-positive sample).
|
|
1050
|
+
//
|
|
1051
|
+
// FENCED LINES ARE NOT CLAIMS: a pasted run summary inside ``` is quoted
|
|
1052
|
+
// tool output. Scanning it would flag the honest report that quotes a run
|
|
1053
|
+
// older than the window, which is the expensive direction.
|
|
1054
|
+
if (
|
|
1055
|
+
observations.length > 0 &&
|
|
1056
|
+
!fencedLines.has(i) &&
|
|
1057
|
+
!NON_PROSE_NO_HEADING_RE.test(line) &&
|
|
1058
|
+
!COUNT_CLAIM_EXCLUSION_RES.some((re) => re.test(line))
|
|
1059
|
+
) {
|
|
1060
|
+
// ANY-MATCH ON BOTH SIDES. A line names several counts routinely
|
|
1061
|
+
// (`2 passed files; 70 tests passed`, `40 passed (40) / Tests 1091
|
|
1062
|
+
// passed (1091)`), and treating the FIRST as "the" claim flagged
|
|
1063
|
+
// reports whose observed number sat later on the same line — 5 of the
|
|
1064
|
+
// first 12 corpus hits. The line is a mismatch only when NOT ONE of the
|
|
1065
|
+
// counts it names is carried by any observed run.
|
|
1066
|
+
const claimedCounts = extractClaimedCounts(masked);
|
|
1067
|
+
if (claimedCounts.length > 0) {
|
|
1068
|
+
if (!claimedCounts.some((n) => observationsCarryCount(n, observations))) {
|
|
1069
|
+
const failedMatch = FAILED_COUNT_RE.exec(masked);
|
|
1070
|
+
raw.push({
|
|
1071
|
+
claim: line.trim().slice(0, CLAIM_TEXT_MAX),
|
|
1072
|
+
kind: KIND_CLAIM_MISMATCH,
|
|
1073
|
+
detail: {
|
|
1074
|
+
mismatch: MISMATCH_COUNT,
|
|
1075
|
+
claimed: {
|
|
1076
|
+
passed: claimedCounts[0],
|
|
1077
|
+
failed: failedMatch ? Number(failedMatch[1]) : null,
|
|
1078
|
+
},
|
|
1079
|
+
// The three most recent runs — enough for the coordinator to see
|
|
1080
|
+
// WHAT was actually measured without copying the whole window
|
|
1081
|
+
// into the ledger. NO raw command text: precedent is `8f15f77b`
|
|
1082
|
+
// (`command_hash` instead of the raw command).
|
|
1083
|
+
observed: observations.slice(-3),
|
|
1084
|
+
observed_n: observations.length,
|
|
1085
|
+
},
|
|
1086
|
+
});
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
// --- distributional class (unchanged) ----------------------------------
|
|
1092
|
+
// #1198 FIX 2: gate-summary/STATUS lines are tool OUTPUT, not a
|
|
1093
|
+
// distributional claim — skipped before any pattern runs.
|
|
538
1094
|
if (GATE_SUMMARY_LINE_RE.test(line)) continue;
|
|
539
1095
|
// #1218: table rows, headings and plan/intent items are not prose
|
|
540
1096
|
// assertions at all — skipped before any pattern runs.
|
|
541
1097
|
if (NON_PROSE_LINE_RE.test(line)) continue;
|
|
542
1098
|
|
|
543
|
-
// #1198 FIX 3 (masking-order bug): mask inline-code spans ONCE, then test
|
|
544
|
-
// BOTH the CLAIM_PATTERNS and the cardinal/ratio patterns against the
|
|
545
|
-
// masked text, so a claim quoted entirely inside backticks (evidence or
|
|
546
|
-
// example text, not an assertion) cannot trip a pattern.
|
|
547
|
-
const masked = line.replace(INLINE_CODE_RE, ' ');
|
|
548
1099
|
let matched = CLAIM_PATTERNS.some((re) => re.test(masked));
|
|
549
1100
|
if (!matched && !fencedLines.has(i) && !configLines.has(i)) {
|
|
550
1101
|
matched = CARDINAL_PATTERN.test(masked) || CARDINAL_RATIO_PATTERN.test(masked);
|
|
@@ -556,7 +1107,7 @@ export function findViolations(text) {
|
|
|
556
1107
|
continue;
|
|
557
1108
|
}
|
|
558
1109
|
|
|
559
|
-
raw.push(line.trim().slice(0, CLAIM_TEXT_MAX));
|
|
1110
|
+
raw.push({ claim: line.trim().slice(0, CLAIM_TEXT_MAX), kind: KIND_DISTRIBUTIONAL });
|
|
560
1111
|
}
|
|
561
1112
|
return { violations: dedupeViolations(raw), undatedVerified };
|
|
562
1113
|
}
|