@event4u/agent-config 9.1.0 → 9.2.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/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +22 -251
- package/README.md +1 -1
- package/dist/agent-src/commands/implement-ticket.md +2 -0
- package/dist/agent-src/commands/review/changes.md +22 -0
- package/dist/agent-src/contexts/execution/subagent-modes-detail.md +41 -6
- package/dist/agent-src/contexts/execution/verify-budget.md +15 -0
- package/dist/agent-src/rules/cross-source-consistency.md +92 -0
- package/dist/agent-src/rules/delegation-policy.md +1 -1
- package/dist/agent-src/skills/estimate-ticket/SKILL.md +2 -0
- package/dist/agent-src/skills/feature-planning/SKILL.md +1 -1
- package/dist/agent-src/skills/po-discovery/SKILL.md +2 -0
- package/dist/agent-src/skills/refine-ticket/SKILL.md +7 -0
- package/dist/agent-src/skills/roadmap-writing/SKILL.md +14 -0
- package/dist/agent-src/skills/subagent-orchestration/SKILL.md +26 -5
- package/dist/agent-src/skills/subagent-orchestration/evals/evals.json +18 -0
- package/dist/agent-src/skills/subagent-orchestration/prompts/README.md +3 -2
- package/dist/agent-src/skills/subagent-orchestration/prompts/adversarial-verification-council.md +79 -0
- package/dist/agent-src/skills/subagent-orchestration/schemas/adversarial-findings.json +56 -0
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +42 -19
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +4 -4
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +10 -8
- package/dist/discovery/trust-report.md +3 -3
- package/dist/discovery/workspaces.json +8 -6
- package/dist/install/install.mjs +9 -0
- package/dist/mcp/registry-manifest.json +2 -2
- package/dist/router.json +1 -1
- package/dist/server/schemas/settings.js +5 -0
- package/dist/server/schemas/settings.js.map +1 -1
- package/docs/CLAIMS.md +8 -1
- package/docs/architecture.md +2 -2
- package/docs/archive/CHANGELOG-pre-9.2.0.md +276 -0
- package/docs/benchmark.md +24 -0
- package/docs/decisions/ADR-122-adversarial-verification-council.md +136 -0
- package/docs/decisions/INDEX.md +1 -0
- package/docs/design/adversarial-council-eval.md +106 -0
- package/docs/getting-started-by-role.md +1 -1
- package/docs/getting-started.md +1 -1
- package/docs/governance-advantage.md +1 -1
- package/docs/guidelines/agent-infra/cross-source-consistency-mechanics.md +109 -0
- package/docs/proof.md +2 -2
- package/package.json +1 -1
- package/src/config/agent-settings.template.yml +42 -0
- package/src/scripts/_lib/adversarial_council_gate.test.ts +112 -0
- package/src/scripts/_lib/adversarial_council_gate.ts +98 -0
- package/src/scripts/_lib/adversarial_reconcile.test.ts +197 -0
- package/src/scripts/_lib/adversarial_reconcile.ts +187 -0
- package/src/scripts/_lib/verify_budget.ts +4 -1
- package/src/scripts/schemas/agent-settings.schema.json +11 -0
- package/src/server/schemas/settings.ts +9 -0
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# Cross-Source Consistency — Mechanics
|
|
2
|
+
|
|
3
|
+
> The discrepancy taxonomy, the scan procedure, the confidence-tiered noise control, worked examples, and the precedence table for the `cross-source-consistency` rule
|
|
4
|
+
|
|
5
|
+
_Companion to `src/rules/cross-source-consistency.md` (P4 pattern of `road-to-kernel-and-router.md`). The Iron Law, the fire/not-fire scope, and the subordination clauses stay in the rule; this file carries the taxonomy detail, the scan procedure, and the examples._
|
|
6
|
+
|
|
7
|
+
## The discrepancy taxonomy
|
|
8
|
+
|
|
9
|
+
| # | Class | Definition | Canonical example |
|
|
10
|
+
|---|---|---|---|
|
|
11
|
+
| a | **text ↔ image** | The ticket/spec text contradicts an attached mockup, screenshot, or diagram. | Text: "show birthdays **today**". Mockup: a birthday from two days ago. |
|
|
12
|
+
| b | **silent-but-needed** | The spec is silent on a behavior that is clearly required for the feature to be correct. | Spec: weekend-shifting only. Clearly also needed: public-holiday shifting — but never specified. |
|
|
13
|
+
| c | **spec ↔ codebase/reality** | The spec contradicts what the code, DB, API, or a real source actually does. | Spec: "the endpoint returns `dueDate`". The DTO has no such field. |
|
|
14
|
+
| d | **intra-ticket** | Acceptance criteria, description, and comments contradict each other. | Description: real-time. AC: "nightly batch is fine." |
|
|
15
|
+
|
|
16
|
+
Type (a) and (d) are **contradictions between present sources** — the agent
|
|
17
|
+
must not pick one silently. Type (b) is a **scope decision** — the inferred
|
|
18
|
+
behavior may be right, but adding it is a scope expansion the user owns. Type
|
|
19
|
+
(c) routes the *verification* to `source-discovery` (read the real source); this
|
|
20
|
+
rule only owns *surfacing the conflict and asking*.
|
|
21
|
+
|
|
22
|
+
## The scan procedure
|
|
23
|
+
|
|
24
|
+
Run this whenever planning, refining, estimating, roadmapping, or implementing
|
|
25
|
+
from a ticket/spec that carries a second source:
|
|
26
|
+
|
|
27
|
+
1. **Enumerate the sources.** Ticket text, every attachment (mockup / screenshot
|
|
28
|
+
/ diagram), the AC block, description, comments, and — for implementation —
|
|
29
|
+
the relevant codebase/DB/API surface. If an attachment cannot be read,
|
|
30
|
+
say so; do not assume it agrees.
|
|
31
|
+
2. **Compare pairwise for type (a), (c), (d).** For each pair, ask: do they state
|
|
32
|
+
the same thing about the same requirement? A mismatch is a discrepancy.
|
|
33
|
+
3. **Scan for type (b) — silence.** List the behaviors the feature obviously
|
|
34
|
+
needs but the spec never mentions (weekend/holiday shifts, empty states,
|
|
35
|
+
error paths, permission edges, pagination, timezone). Each unstated-but-needed
|
|
36
|
+
behavior is a candidate to surface — not to implement.
|
|
37
|
+
4. **Classify confidence.** High = the sources plainly disagree, or the missing
|
|
38
|
+
behavior is clearly load-bearing. Low = the "conflict" is a plausible
|
|
39
|
+
re-reading, or the missing behavior may be intentionally out of scope.
|
|
40
|
+
5. **Decide per the setting** (below), then **batch** every surviving discrepancy
|
|
41
|
+
into ONE question folded into this turn's single `ask-when-uncertain` prompt.
|
|
42
|
+
|
|
43
|
+
## Confidence-tiered noise control (`consistency.cross_source`)
|
|
44
|
+
|
|
45
|
+
The rule fires only under a real trade-off (it must pass the
|
|
46
|
+
`no-cheap-questions` Pre-Send Self-Check). The setting tunes how aggressively:
|
|
47
|
+
|
|
48
|
+
| Value | High-confidence discrepancy | Low-confidence discrepancy |
|
|
49
|
+
|---|---|---|
|
|
50
|
+
| `on` (default) | Surface + ask before proceeding. | Surface + ask (batched into the same question). |
|
|
51
|
+
| `auto` | Surface + ask. | State as an explicit assumption and proceed (no question). |
|
|
52
|
+
| `off` | No cross-source checking — legacy behavior. | No cross-source checking. |
|
|
53
|
+
|
|
54
|
+
Batching is the anti-noise mechanism: N discrepancies for one artefact become
|
|
55
|
+
one numbered-options block, never N prompts. Plain vagueness (a single unclear
|
|
56
|
+
source, no second source to compare) is **not** a discrepancy — it stays with
|
|
57
|
+
`ask-when-uncertain` so the two disciplines never double-fire on the same input.
|
|
58
|
+
|
|
59
|
+
## Worked examples
|
|
60
|
+
|
|
61
|
+
### Example 1 — text ↔ image (type a)
|
|
62
|
+
|
|
63
|
+
Ticket: *"The tile shows employees who have their birthday **today**."* Attached
|
|
64
|
+
mockup: a card with a birthday dated **two days ago**.
|
|
65
|
+
|
|
66
|
+
- Wrong: implement "today" (or implement "recent, like the mockup") and move on.
|
|
67
|
+
Both are silent guesses on a real contradiction — a violation even if the
|
|
68
|
+
chosen reading turns out correct.
|
|
69
|
+
- Right: *"The ticket text says 'today' but the mockup shows a birthday from two
|
|
70
|
+
days ago. Which is authoritative — (1) strictly today, (2) a recent window as
|
|
71
|
+
the mockup implies, (3) something else?"* Then implement the confirmed reading;
|
|
72
|
+
`design-fidelity` governs building it 1:1 afterwards.
|
|
73
|
+
|
|
74
|
+
### Example 2 — silent-but-needed (type b)
|
|
75
|
+
|
|
76
|
+
Ticket update: *"If a birthday falls on the weekend, show it on the next
|
|
77
|
+
workday."* The spec never mentions public holidays.
|
|
78
|
+
|
|
79
|
+
- Wrong: also shift birthdays that fall on a public holiday, because it is
|
|
80
|
+
"obviously" consistent with the weekend rule — a silent scope expansion.
|
|
81
|
+
- Right: *"The spec covers weekend-shifting but is silent on public holidays.
|
|
82
|
+
Should birthdays on a public holiday also shift to the next workday? (1) yes,
|
|
83
|
+
same as weekends, (2) no, holidays are out of scope, (3) only regional
|
|
84
|
+
holidays."* Obvious-to-the-agent ≠ in-scope; `scope-control` owns the
|
|
85
|
+
expansion permission.
|
|
86
|
+
|
|
87
|
+
## Precedence & subordination
|
|
88
|
+
|
|
89
|
+
| Neighbour rule | Relationship |
|
|
90
|
+
|---|---|
|
|
91
|
+
| `ask-when-uncertain` | **Parent ask discipline.** This rule folds its discrepancy into that rule's single question. `ask-when-uncertain` fires on *missing* info; this adds *conflicting present* sources. Never a second question block. |
|
|
92
|
+
| `scope-control` | **Permission authority** for the type-(b) silent-scope-expansion. This rule surfaces the expansion; `scope-control` gates it. |
|
|
93
|
+
| `design-fidelity` | **Fires after** this rule. This rule resolves a text↔image conflict at decision time; `design-fidelity` then enforces build-time 1:1 fidelity to the resolved design. |
|
|
94
|
+
| `source-discovery` / `source-discovery-gate` | **Owns verification** for type (c). This rule surfaces the spec↔code conflict; `source-discovery` provides the evidence procedure. |
|
|
95
|
+
| `no-cheap-questions` | **Floor.** A discrepancy ask must carry a real trade-off; batch, never nag. |
|
|
96
|
+
| `active-remediation` | **Sibling shape.** Its never-silently-ignore ladder is the response model this rule applies to source discrepancies rather than code issues. |
|
|
97
|
+
|
|
98
|
+
## Failure modes
|
|
99
|
+
|
|
100
|
+
- **Silent-correct-guess.** Picking the right reading of a contradiction without
|
|
101
|
+
asking. Correct output, wrong process — still a violation.
|
|
102
|
+
- **Double-firing.** Emitting a discrepancy question AND a separate
|
|
103
|
+
`ask-when-uncertain` / `improve-before-implement` question about the same
|
|
104
|
+
underlying uncertainty. Fold into one turn's one question.
|
|
105
|
+
- **Noise creep.** Surfacing plausible re-readings as if they were hard
|
|
106
|
+
contradictions. Use the confidence tier; `auto` states low-confidence as an
|
|
107
|
+
assumption instead of asking.
|
|
108
|
+
- **Silent scope expansion.** Implementing an inferred-but-unspecified behavior
|
|
109
|
+
because it seems obvious. It is a scope decision the user owns.
|
package/docs/proof.md
CHANGED
|
@@ -41,7 +41,7 @@ evidence pointer, or `task check-claims` fails the build.
|
|
|
41
41
|
| The whole layer is compiled into host agents with zero runtime daemon. | qual | `docs/contracts/no-runtime-boundary.md#file-first, no-runtime suite` | ✅ |
|
|
42
42
|
| On a 12-fixture option-decision corpus (3 arms × 2 providers, blind rubric judge claude-opus-4-8, pre-registered hypotheses), famous-figure identity framing added nothing beyond the underlying method text (method 5.04 vs figure 4.88, Δ=0.17, sign-test p=0.607), and provider diversity moved judged quality ~15× more than persona identity (provider Δ=2.58 vs identity Δ=0.17); the whole persona layer lifted only +0.08 over bare prompts. Honest null — persona panel-mode stays CUT, evidence-closed. | quant | `internal/bench/reports/persona-placebo.json#honest-null` | ✅ |
|
|
43
43
|
| On the live end-to-end retrieval benchmark (9 tasks × 3 arms × 3 seeds on claude-haiku, fixture store with keyword-overlapping confusers), the memory retrieval substrate scored precision@5 = 100% (9/9) with 100% poisoned-entry rejection, and the retrieval-on arm passed 27/27 model-scored tasks vs 2/27 with retrieval off and 4/27 with a placebo injection. Known limit stays published: mean tie-set 4.1 means top-k ties break by store order, not relevance (the ADR-116/FTS5 signal). | quant | `internal/bench/reports/second-brain-retrieval.json#retrieval-on` | ✅ |
|
|
44
|
-
|
|
|
44
|
+
| 105 governed rules. | quant | `src/scripts/check_artefact_count_messaging.ts#Artefact-count messaging gate` | ✅ |
|
|
45
45
|
| On a deterministic multi-session recall corpus, the memory substrate produces a measured, placebo-controlled recall lift — memory-on 27/27 vs no-memory 10/27 and vs equal-byte placebo 9/27 (claude-haiku-4-5, n=9 tasks x 3 seeds, sign test p=0.031 for BOTH pairings). Scoped honestly: this is the context-value upper bound (perfect retrieval on a one-fact-per-task corpus), not retrieval precision under a large store. | quant | `internal/bench/reports/second-brain-delta.json` | ✅ |
|
|
46
46
|
| Removing the perfect-retrieval assumption, the substrate's REAL keyword retrieval recalls the needed decision into the top-5 under keyword-overlapping confusers (precision@5 9/9) and the model disambiguates it from the co-injected confusers — retrieval-on 27/27 vs no-memory 5/27 and vs equal-count placebo 5/27 (claude-haiku-4-5, 9 tasks x 3 seeds, sign test p=0.008 both). Named limit: retrieval RECALLS but does not RANK (mean tie-set 3.3, ties broken by store order, not relevance) — the discrimination gap that motivates the SQLite-FTS5 activation path (ADR-116) at larger scale. | quant | `internal/bench/reports/second-brain-retrieval.json` | ✅ |
|
|
47
47
|
| Every artifact the package ships — source AND the condensed projection that reaches consumers — is machine-scanned in CI for hidden-Unicode, mixed-script-confusable, and instruction-smuggling payloads (the rules-file-backdoor class); a finding blocks the release before `npm publish`, not just the merge. | qual | `.github/workflows/publish-npm.yml#lint_agent_security` | ✅ |
|
|
@@ -56,7 +56,7 @@ guidelines, personas) are **generated from source and CI-drift-checked**:
|
|
|
56
56
|
fails the build on any count-shaped prose mention that drifts from the
|
|
57
57
|
source count — or on two different numbers for the same artefact kind.
|
|
58
58
|
|
|
59
|
-
We also publish our **debt**:
|
|
59
|
+
We also publish our **debt**: 5 claim(s) are logged as
|
|
60
60
|
`unbacked` inventory in the ledger — not yet bound, and therefore not
|
|
61
61
|
allowed to carry a marker in public prose. Hiding them would be the
|
|
62
62
|
opposite of the point.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@event4u/agent-config",
|
|
3
|
-
"version": "9.
|
|
3
|
+
"version": "9.2.0",
|
|
4
4
|
"description": "Universal AI Agent OS — audited skills, governance rules, commands, and templates for AI coding tools (Claude Code, Cursor, Windsurf, Copilot).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"private": false,
|
|
@@ -518,6 +518,34 @@ design:
|
|
|
518
518
|
# autonomy setting, roadmap, or standing instruction lifts it).
|
|
519
519
|
fidelity_mode: strict
|
|
520
520
|
|
|
521
|
+
# --- Cross-source consistency ---
|
|
522
|
+
#
|
|
523
|
+
# When the agent plans, refines, roadmaps, or implements from multiple
|
|
524
|
+
# sources — ticket text, an attached image / mockup / screenshot, the spec,
|
|
525
|
+
# the codebase — it actively checks them AGAINST each other and STOPS to ask
|
|
526
|
+
# before proceeding on a detected discrepancy, instead of silently guessing
|
|
527
|
+
# (even a correct guess). Consumed by the `cross-source-consistency` rule.
|
|
528
|
+
# Catches: (a) ticket text vs an attached image/mockup, (b) a spec silent on
|
|
529
|
+
# a clearly-needed behavior (weekend/holiday shifting, empty/error states),
|
|
530
|
+
# (c) spec vs the codebase, (d) intra-ticket contradiction (AC vs description).
|
|
531
|
+
#
|
|
532
|
+
# The discipline is deliberately subordinate to `ask-when-uncertain` (it folds
|
|
533
|
+
# a detected discrepancy into the same turn's single question — never a second
|
|
534
|
+
# prompt) and defers to `scope-control` for the permission to expand scope.
|
|
535
|
+
consistency:
|
|
536
|
+
# on | auto | off (default: on)
|
|
537
|
+
#
|
|
538
|
+
# on = default. Detect discrepancies across sources and surface ONE
|
|
539
|
+
# numbered-options question before proceeding. A silent guess on a
|
|
540
|
+
# real contradiction is a rule violation, even when the guess is
|
|
541
|
+
# correct. An inferred-but-unspecified behavior is a scope expansion:
|
|
542
|
+
# ask before implementing it.
|
|
543
|
+
# auto = detect + surface only HIGH-confidence contradictions; low-confidence
|
|
544
|
+
# ones are stated as an assumption and proceed (noise control).
|
|
545
|
+
# off = no cross-source checking (legacy: the agent may proceed on its own
|
|
546
|
+
# reading of whichever source it happened to read).
|
|
547
|
+
cross_source: "on"
|
|
548
|
+
|
|
521
549
|
code_style:
|
|
522
550
|
# minimal | full (default: minimal)
|
|
523
551
|
#
|
|
@@ -604,6 +632,20 @@ subagents:
|
|
|
604
632
|
# Integer, default 3. Set to 1 to serialize. Hard cap enforced by runtime.
|
|
605
633
|
max_parallel: 3
|
|
606
634
|
|
|
635
|
+
# Opt-in adversarial-verification-council (subagent-orchestration Mode 9,
|
|
636
|
+
# ADR-122): off | ask | on
|
|
637
|
+
#
|
|
638
|
+
# off (default) = never runs. ask = offer it on an explicit high-risk change.
|
|
639
|
+
# on = auto-run on high-risk changes (security, tenant, migration, public API).
|
|
640
|
+
#
|
|
641
|
+
# A panel of distinct-model skeptics red-teams a real, already-verified change
|
|
642
|
+
# for defect FINDING coverage (not a go/no-go decision) and returns an advisory
|
|
643
|
+
# findings-by-severity verdict. ADVISORY ONLY — it NEVER auto-gates the change
|
|
644
|
+
# (Hard Floor). The registered claim + high-risk tier use cross-vendor skeptics.
|
|
645
|
+
# Stays default-off until the adversarial-council-finding-coverage claim is
|
|
646
|
+
# backed by the two-stage residual-detection benchmark (prove-or-drop).
|
|
647
|
+
adversarial_council: off
|
|
648
|
+
|
|
607
649
|
# --- Git worktrees ---
|
|
608
650
|
#
|
|
609
651
|
# Controls how the `using-git-worktrees` skill and `subagent-orchestration`
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adversarial council prove-or-drop gate — coverage (Phase 4).
|
|
3
|
+
*
|
|
4
|
+
* Locks the pre-registered dual-threshold semantics: BOTH relative (>=+25%) and
|
|
5
|
+
* absolute (>=+8pp) residual-recall lift, AND panel FP not worse than baseline
|
|
6
|
+
* within noise. Guards the base-rate-compression case the council flagged
|
|
7
|
+
* (high single-judge baseline → relative easy but absolute must still clear).
|
|
8
|
+
*/
|
|
9
|
+
import { describe, expect, it } from 'vitest';
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
ABSOLUTE_LIFT_THRESHOLD_PP,
|
|
13
|
+
RELATIVE_LIFT_THRESHOLD,
|
|
14
|
+
evaluateCouncilBench,
|
|
15
|
+
} from './adversarial_council_gate.js';
|
|
16
|
+
|
|
17
|
+
const CLEAN_FP = { single_judge_fp_rate: 0.05, panel_fp_rate: 0.05, fp_noise_margin: 0.03 };
|
|
18
|
+
|
|
19
|
+
describe('evaluateCouncilBench', () => {
|
|
20
|
+
it('exposes the locked thresholds', () => {
|
|
21
|
+
expect(RELATIVE_LIFT_THRESHOLD).toBe(0.25);
|
|
22
|
+
expect(ABSOLUTE_LIFT_THRESHOLD_PP).toBe(0.08);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('backs the claim when both recall thresholds and FP pass', () => {
|
|
26
|
+
const v = evaluateCouncilBench({
|
|
27
|
+
single_judge_residual_recall: 0.5,
|
|
28
|
+
panel_residual_recall: 0.65, // +30% relative, +15pp absolute
|
|
29
|
+
...CLEAN_FP,
|
|
30
|
+
});
|
|
31
|
+
expect(v.relative_pass).toBe(true);
|
|
32
|
+
expect(v.absolute_pass).toBe(true);
|
|
33
|
+
expect(v.fp_pass).toBe(true);
|
|
34
|
+
expect(v.verdict).toBe('backed');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('honest-null when relative passes but absolute fails (base-rate compression)', () => {
|
|
38
|
+
// baseline 0.90; +0.05 → relative 5.5% (fails) — the high-baseline trap.
|
|
39
|
+
const v = evaluateCouncilBench({
|
|
40
|
+
single_judge_residual_recall: 0.9,
|
|
41
|
+
panel_residual_recall: 0.95,
|
|
42
|
+
...CLEAN_FP,
|
|
43
|
+
});
|
|
44
|
+
expect(v.verdict).toBe('honest-null');
|
|
45
|
+
expect(v.reasons.join(' ')).toMatch(/relative lift/);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('honest-null when absolute passes but relative fails', () => {
|
|
49
|
+
// baseline 0.6; +0.09 absolute (passes) but +15% relative (fails at 25%).
|
|
50
|
+
const v = evaluateCouncilBench({
|
|
51
|
+
single_judge_residual_recall: 0.6,
|
|
52
|
+
panel_residual_recall: 0.69,
|
|
53
|
+
...CLEAN_FP,
|
|
54
|
+
});
|
|
55
|
+
expect(v.absolute_pass).toBe(true);
|
|
56
|
+
expect(v.relative_pass).toBe(false);
|
|
57
|
+
expect(v.verdict).toBe('honest-null');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('honest-null when recall passes but FP regresses beyond noise', () => {
|
|
61
|
+
const v = evaluateCouncilBench({
|
|
62
|
+
single_judge_residual_recall: 0.5,
|
|
63
|
+
panel_residual_recall: 0.7,
|
|
64
|
+
single_judge_fp_rate: 0.05,
|
|
65
|
+
panel_fp_rate: 0.12, // +7pp > 3pp noise
|
|
66
|
+
fp_noise_margin: 0.03,
|
|
67
|
+
});
|
|
68
|
+
expect(v.relative_pass).toBe(true);
|
|
69
|
+
expect(v.absolute_pass).toBe(true);
|
|
70
|
+
expect(v.fp_pass).toBe(false);
|
|
71
|
+
expect(v.verdict).toBe('honest-null');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('allows a within-noise FP increase', () => {
|
|
75
|
+
const v = evaluateCouncilBench({
|
|
76
|
+
single_judge_residual_recall: 0.5,
|
|
77
|
+
panel_residual_recall: 0.7,
|
|
78
|
+
single_judge_fp_rate: 0.05,
|
|
79
|
+
panel_fp_rate: 0.075, // +2.5pp <= 3pp noise
|
|
80
|
+
fp_noise_margin: 0.03,
|
|
81
|
+
});
|
|
82
|
+
expect(v.fp_pass).toBe(true);
|
|
83
|
+
expect(v.verdict).toBe('backed');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('handles a zero baseline (infinite relative lift, absolute still gates)', () => {
|
|
87
|
+
const backed = evaluateCouncilBench({
|
|
88
|
+
single_judge_residual_recall: 0,
|
|
89
|
+
panel_residual_recall: 0.2,
|
|
90
|
+
...CLEAN_FP,
|
|
91
|
+
});
|
|
92
|
+
expect(backed.relative_lift).toBe(Infinity);
|
|
93
|
+
expect(backed.verdict).toBe('backed'); // +20pp absolute clears
|
|
94
|
+
|
|
95
|
+
const nullAbs = evaluateCouncilBench({
|
|
96
|
+
single_judge_residual_recall: 0,
|
|
97
|
+
panel_residual_recall: 0.05, // +5pp < 8pp
|
|
98
|
+
...CLEAN_FP,
|
|
99
|
+
});
|
|
100
|
+
expect(nullAbs.verdict).toBe('honest-null');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('rejects out-of-range inputs', () => {
|
|
104
|
+
expect(() =>
|
|
105
|
+
evaluateCouncilBench({
|
|
106
|
+
single_judge_residual_recall: 1.5,
|
|
107
|
+
panel_residual_recall: 0.5,
|
|
108
|
+
...CLEAN_FP,
|
|
109
|
+
}),
|
|
110
|
+
).toThrow(/\[0,1\]/);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adversarial verification council — prove-or-drop gate (Phase 4).
|
|
3
|
+
*
|
|
4
|
+
* Pure, deterministic. Encodes the pre-registered `adversarial-council-finding-
|
|
5
|
+
* coverage` claim (docs/CLAIMS.md, ADR-122) as a machine-checkable verdict on
|
|
6
|
+
* the two-stage residual-detection benchmark: does a cross-vendor skeptic panel
|
|
7
|
+
* out-find a single strong judge on the RESIDUAL defect pool (defects that
|
|
8
|
+
* survived stage 1), at a false-positive rate no worse than baseline on the
|
|
9
|
+
* controversial-but-correct control?
|
|
10
|
+
*
|
|
11
|
+
* Dual threshold, both required, to guard against base-rate compression when the
|
|
12
|
+
* single-judge residual recall is already high (the council's Round-1 critique):
|
|
13
|
+
* (a) relative residual-recall lift >= +25%
|
|
14
|
+
* (b) absolute residual-recall lift >= +8 percentage points
|
|
15
|
+
* AND panel FP not worse than single-judge FP within the noise margin.
|
|
16
|
+
*
|
|
17
|
+
* Thresholds are LOCKED at pre-registration (roadmap Phase 0). This module is the
|
|
18
|
+
* "countable logic in TS with tests" the anti-lesson mandates — never LLM-computed.
|
|
19
|
+
*
|
|
20
|
+
* Design: docs/design/adversarial-council-eval.md
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/** Relative residual-recall lift threshold (fraction). */
|
|
24
|
+
export const RELATIVE_LIFT_THRESHOLD = 0.25;
|
|
25
|
+
/** Absolute residual-recall lift threshold (percentage points, as a fraction). */
|
|
26
|
+
export const ABSOLUTE_LIFT_THRESHOLD_PP = 0.08;
|
|
27
|
+
|
|
28
|
+
export interface CouncilBenchInputs {
|
|
29
|
+
/** Single-judge recall on the judge-passed residual pool (0..1). */
|
|
30
|
+
single_judge_residual_recall: number;
|
|
31
|
+
/** Panel recall on the same judge-passed residual pool (0..1). */
|
|
32
|
+
panel_residual_recall: number;
|
|
33
|
+
/** Single-judge false-positive rate on the controversial-clean control (0..1). */
|
|
34
|
+
single_judge_fp_rate: number;
|
|
35
|
+
/** Panel false-positive rate on the same control (0..1). */
|
|
36
|
+
panel_fp_rate: number;
|
|
37
|
+
/** Noise margin within which a higher panel FP still counts as "not worse" (0..1). */
|
|
38
|
+
fp_noise_margin: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export type CouncilVerdict = 'backed' | 'honest-null';
|
|
42
|
+
|
|
43
|
+
export interface CouncilBenchVerdict {
|
|
44
|
+
relative_lift: number;
|
|
45
|
+
absolute_lift_pp: number;
|
|
46
|
+
relative_pass: boolean;
|
|
47
|
+
absolute_pass: boolean;
|
|
48
|
+
fp_pass: boolean;
|
|
49
|
+
verdict: CouncilVerdict;
|
|
50
|
+
reasons: string[];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function assertUnit(name: string, v: number): void {
|
|
54
|
+
if (typeof v !== 'number' || Number.isNaN(v) || v < 0 || v > 1) {
|
|
55
|
+
throw new Error(`${name} must be a number in [0,1], got ${JSON.stringify(v)}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Evaluate the pre-registered dual-threshold gate. Returns `backed` only when
|
|
61
|
+
* BOTH recall thresholds AND the FP constraint pass; otherwise `honest-null`
|
|
62
|
+
* (which keeps the surface default-off permanently, like recursive-verification).
|
|
63
|
+
*/
|
|
64
|
+
export function evaluateCouncilBench(inputs: CouncilBenchInputs): CouncilBenchVerdict {
|
|
65
|
+
assertUnit('single_judge_residual_recall', inputs.single_judge_residual_recall);
|
|
66
|
+
assertUnit('panel_residual_recall', inputs.panel_residual_recall);
|
|
67
|
+
assertUnit('single_judge_fp_rate', inputs.single_judge_fp_rate);
|
|
68
|
+
assertUnit('panel_fp_rate', inputs.panel_fp_rate);
|
|
69
|
+
assertUnit('fp_noise_margin', inputs.fp_noise_margin);
|
|
70
|
+
|
|
71
|
+
const single = inputs.single_judge_residual_recall;
|
|
72
|
+
const panel = inputs.panel_residual_recall;
|
|
73
|
+
const absolute_lift_pp = panel - single;
|
|
74
|
+
// A zero baseline makes the ratio undefined — any positive panel recall is an
|
|
75
|
+
// infinite relative lift; no lift is zero. This never masks the absolute gate.
|
|
76
|
+
const relative_lift = single === 0 ? (panel > 0 ? Infinity : 0) : absolute_lift_pp / single;
|
|
77
|
+
|
|
78
|
+
const relative_pass = relative_lift >= RELATIVE_LIFT_THRESHOLD;
|
|
79
|
+
const absolute_pass = absolute_lift_pp >= ABSOLUTE_LIFT_THRESHOLD_PP;
|
|
80
|
+
const fp_pass = inputs.panel_fp_rate <= inputs.single_judge_fp_rate + inputs.fp_noise_margin;
|
|
81
|
+
|
|
82
|
+
const reasons: string[] = [];
|
|
83
|
+
if (!relative_pass) {
|
|
84
|
+
reasons.push(`relative lift ${(relative_lift * 100).toFixed(1)}% < ${RELATIVE_LIFT_THRESHOLD * 100}%`);
|
|
85
|
+
}
|
|
86
|
+
if (!absolute_pass) {
|
|
87
|
+
reasons.push(`absolute lift ${(absolute_lift_pp * 100).toFixed(1)}pp < ${ABSOLUTE_LIFT_THRESHOLD_PP * 100}pp`);
|
|
88
|
+
}
|
|
89
|
+
if (!fp_pass) {
|
|
90
|
+
reasons.push(
|
|
91
|
+
`panel FP ${(inputs.panel_fp_rate * 100).toFixed(1)}% > baseline ${(inputs.single_judge_fp_rate * 100).toFixed(1)}% + noise ${(inputs.fp_noise_margin * 100).toFixed(1)}%`,
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const verdict: CouncilVerdict = relative_pass && absolute_pass && fp_pass ? 'backed' : 'honest-null';
|
|
96
|
+
if (verdict === 'backed') reasons.push('all thresholds met');
|
|
97
|
+
return { relative_lift, absolute_lift_pp, relative_pass, absolute_pass, fp_pass, verdict, reasons };
|
|
98
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adversarial reconciliation core — deterministic coverage (Phase 1).
|
|
3
|
+
*
|
|
4
|
+
* Covers: dedup across skeptics, severity aggregation (strict-er wins),
|
|
5
|
+
* quorum-confidence math, false-positive suppression (demote, never drop),
|
|
6
|
+
* empty panel, single-finding, unanimous vs split, malformed-input rejection,
|
|
7
|
+
* and hand-rolled validation of every output against
|
|
8
|
+
* adversarial-findings.json (no jsonschema runtime dep — mirrors
|
|
9
|
+
* test_subagent_status_schema.py's approach).
|
|
10
|
+
*/
|
|
11
|
+
import { readFileSync } from 'node:fs';
|
|
12
|
+
import { fileURLToPath } from 'node:url';
|
|
13
|
+
import path from 'node:path';
|
|
14
|
+
|
|
15
|
+
import { describe, expect, it } from 'vitest';
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
type AdversarialFindings,
|
|
19
|
+
type RawFinding,
|
|
20
|
+
type SkepticReturn,
|
|
21
|
+
assertValidRawFinding,
|
|
22
|
+
findingKey,
|
|
23
|
+
isSuppressedFalsePositive,
|
|
24
|
+
reconcileFindings,
|
|
25
|
+
severityQuorum,
|
|
26
|
+
} from './adversarial_reconcile.js';
|
|
27
|
+
|
|
28
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
29
|
+
const SCHEMA_PATH = path.resolve(
|
|
30
|
+
HERE,
|
|
31
|
+
'../../skills/subagent-orchestration/schemas/adversarial-findings.json',
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
const SEVERITIES = new Set(['critical', 'high', 'medium', 'low']);
|
|
35
|
+
const CONFIDENCES = new Set(['high', 'medium', 'low']);
|
|
36
|
+
|
|
37
|
+
/** Minimal hand-rolled validator matching the schema's required keys + enums. */
|
|
38
|
+
function validateEnvelope(env: AdversarialFindings): void {
|
|
39
|
+
expect(env).toHaveProperty('panel');
|
|
40
|
+
expect(env.panel.models.length).toBeGreaterThanOrEqual(1);
|
|
41
|
+
expect(env.panel.skeptic_count).toBe(env.panel.models.length);
|
|
42
|
+
for (const bucket of [env.findings, env.false_positives_suppressed]) {
|
|
43
|
+
for (const f of bucket) {
|
|
44
|
+
for (const val of [f.id, f.severity, f.category, f.location, f.description, f.confidence]) {
|
|
45
|
+
expect(typeof val).toBe('string');
|
|
46
|
+
expect(val.length).toBeGreaterThan(0);
|
|
47
|
+
}
|
|
48
|
+
expect(SEVERITIES.has(f.severity)).toBe(true);
|
|
49
|
+
expect(CONFIDENCES.has(f.confidence)).toBe(true);
|
|
50
|
+
expect(Array.isArray(f.raised_by)).toBe(true);
|
|
51
|
+
expect(f.raised_by.length).toBeGreaterThanOrEqual(1);
|
|
52
|
+
expect(Array.isArray(f.refuted_by)).toBe(true);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function raw(over: Partial<RawFinding> = {}): RawFinding {
|
|
58
|
+
return {
|
|
59
|
+
severity: 'high',
|
|
60
|
+
category: 'correctness',
|
|
61
|
+
location: 'src/a.ts:42',
|
|
62
|
+
description: 'off-by-one in loop bound',
|
|
63
|
+
...over,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
describe('adversarial-findings.json schema', () => {
|
|
68
|
+
it('parses and declares the finding contract', () => {
|
|
69
|
+
const schema = JSON.parse(readFileSync(SCHEMA_PATH, 'utf8'));
|
|
70
|
+
expect(schema.required).toEqual(['panel', 'findings', 'false_positives_suppressed']);
|
|
71
|
+
expect(schema.additionalProperties).toBe(false);
|
|
72
|
+
expect(schema.definitions.finding.required).toContain('raised_by');
|
|
73
|
+
expect(schema.definitions.finding.required).toContain('confidence');
|
|
74
|
+
expect(schema.definitions.finding.properties.severity.enum).toEqual([
|
|
75
|
+
'critical',
|
|
76
|
+
'high',
|
|
77
|
+
'medium',
|
|
78
|
+
'low',
|
|
79
|
+
]);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe('findingKey', () => {
|
|
84
|
+
it('is location + normalized category', () => {
|
|
85
|
+
expect(findingKey({ location: 'src/a.ts:42 ', category: 'Security' })).toBe(
|
|
86
|
+
'src/a.ts:42::security',
|
|
87
|
+
);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
describe('assertValidRawFinding', () => {
|
|
92
|
+
it('rejects a bad severity', () => {
|
|
93
|
+
expect(() => assertValidRawFinding(raw({ severity: 'blocker' as never }))).toThrow(/severity/);
|
|
94
|
+
});
|
|
95
|
+
it('rejects an empty location', () => {
|
|
96
|
+
expect(() => assertValidRawFinding(raw({ location: ' ' }))).toThrow(/location/);
|
|
97
|
+
});
|
|
98
|
+
it('accepts a well-formed finding', () => {
|
|
99
|
+
expect(() => assertValidRawFinding(raw())).not.toThrow();
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe('severityQuorum', () => {
|
|
104
|
+
it('high when corroborated by a panel quorum', () => {
|
|
105
|
+
expect(severityQuorum(2, 0, 2)).toBe('high');
|
|
106
|
+
expect(severityQuorum(2, 0, 3)).toBe('high');
|
|
107
|
+
});
|
|
108
|
+
it('medium when corroborated below quorum', () => {
|
|
109
|
+
expect(severityQuorum(2, 0, 5)).toBe('medium');
|
|
110
|
+
});
|
|
111
|
+
it('medium for a lone raiser on a solo panel', () => {
|
|
112
|
+
expect(severityQuorum(1, 0, 1)).toBe('medium');
|
|
113
|
+
});
|
|
114
|
+
it('low for a single raiser on a multi-skeptic panel', () => {
|
|
115
|
+
expect(severityQuorum(1, 0, 3)).toBe('low');
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe('isSuppressedFalsePositive', () => {
|
|
120
|
+
it('suppresses a lone finding refuted by a strict majority of the rest', () => {
|
|
121
|
+
expect(isSuppressedFalsePositive(1, 1, 2)).toBe(true); // rest=1, refuted=1 > 0.5
|
|
122
|
+
expect(isSuppressedFalsePositive(1, 2, 3)).toBe(true); // rest=2, refuted=2 > 1
|
|
123
|
+
});
|
|
124
|
+
it('does not suppress when refutation is not a strict majority', () => {
|
|
125
|
+
expect(isSuppressedFalsePositive(1, 1, 3)).toBe(false); // rest=2, refuted=1 == 1, not > 1
|
|
126
|
+
});
|
|
127
|
+
it('never suppresses a corroborated finding', () => {
|
|
128
|
+
expect(isSuppressedFalsePositive(2, 5, 7)).toBe(false);
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
describe('reconcileFindings', () => {
|
|
133
|
+
it('handles an empty panel', () => {
|
|
134
|
+
const env = reconcileFindings([]);
|
|
135
|
+
expect(env.panel).toEqual({ models: [], skeptic_count: 0 });
|
|
136
|
+
expect(env.findings).toEqual([]);
|
|
137
|
+
expect(env.false_positives_suppressed).toEqual([]);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('dedups the same defect across skeptics and records both as raisers', () => {
|
|
141
|
+
const env = reconcileFindings([
|
|
142
|
+
{ model: 'anthropic:x', findings: [raw()] },
|
|
143
|
+
{ model: 'openai:y', findings: [raw({ description: 'the loop reads one past the end of the array' })] },
|
|
144
|
+
]);
|
|
145
|
+
expect(env.findings).toHaveLength(1);
|
|
146
|
+
expect(env.findings[0]!.raised_by.sort()).toEqual(['anthropic:x', 'openai:y']);
|
|
147
|
+
expect(env.findings[0]!.confidence).toBe('high'); // 2/2 = quorum
|
|
148
|
+
// Longest description wins.
|
|
149
|
+
expect(env.findings[0]!.description).toMatch(/one past the end/);
|
|
150
|
+
validateEnvelope(env);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('aggregates severity strict-er-wins', () => {
|
|
154
|
+
const env = reconcileFindings([
|
|
155
|
+
{ model: 'a', findings: [raw({ severity: 'medium' })] },
|
|
156
|
+
{ model: 'b', findings: [raw({ severity: 'critical' })] },
|
|
157
|
+
]);
|
|
158
|
+
expect(env.findings[0]!.severity).toBe('critical');
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('takes the union of distinct findings and orders by severity then key', () => {
|
|
162
|
+
const env = reconcileFindings([
|
|
163
|
+
{ model: 'a', findings: [raw({ location: 'src/a.ts:1', severity: 'low', category: 'quality' })] },
|
|
164
|
+
{ model: 'b', findings: [raw({ location: 'src/b.ts:9', severity: 'critical', category: 'security' })] },
|
|
165
|
+
]);
|
|
166
|
+
expect(env.findings.map((f) => f.severity)).toEqual(['critical', 'low']);
|
|
167
|
+
expect(env.findings.map((f) => f.id)).toEqual(['avc-001', 'avc-002']);
|
|
168
|
+
validateEnvelope(env);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('demotes a lone finding refuted by a majority into false_positives_suppressed', () => {
|
|
172
|
+
const loneKey = findingKey({ location: 'src/c.ts:5', category: 'security' });
|
|
173
|
+
const env = reconcileFindings([
|
|
174
|
+
{ model: 'a', findings: [raw({ location: 'src/c.ts:5', category: 'security' })] },
|
|
175
|
+
{ model: 'b', findings: [], refutes: [loneKey] },
|
|
176
|
+
]);
|
|
177
|
+
expect(env.findings).toHaveLength(0);
|
|
178
|
+
expect(env.false_positives_suppressed).toHaveLength(1);
|
|
179
|
+
expect(env.false_positives_suppressed[0]!.refuted_by).toEqual(['b']);
|
|
180
|
+
validateEnvelope(env);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('is deterministic across input order', () => {
|
|
184
|
+
const a: SkepticReturn = { model: 'a', findings: [raw({ location: 'src/z.ts:1', severity: 'high' })] };
|
|
185
|
+
const b: SkepticReturn = { model: 'b', findings: [raw({ location: 'src/a.ts:1', severity: 'high', category: 'security' })] };
|
|
186
|
+
const one = reconcileFindings([a, b]);
|
|
187
|
+
const two = reconcileFindings([b, a]);
|
|
188
|
+
expect(one.findings.map((f) => f.location)).toEqual(two.findings.map((f) => f.location));
|
|
189
|
+
expect(one.findings.map((f) => f.id)).toEqual(two.findings.map((f) => f.id));
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('rejects malformed findings from a skeptic', () => {
|
|
193
|
+
expect(() =>
|
|
194
|
+
reconcileFindings([{ model: 'a', findings: [raw({ severity: 'nope' as never })] }]),
|
|
195
|
+
).toThrow(/severity/);
|
|
196
|
+
});
|
|
197
|
+
});
|