@event4u/agent-config 8.9.0 → 8.10.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 +41 -0
- package/README.md +5 -5
- package/dist/agent-src/commands/cost/report.md +13 -3
- package/dist/agent-src/commands/tests/create.md +28 -6
- package/dist/agent-src/commands/work.md +33 -0
- package/dist/agent-src/commands/worktree/cleanup.md +78 -0
- package/dist/agent-src/commands/worktree/create.md +62 -0
- package/dist/agent-src/commands/worktree/status.md +63 -0
- package/dist/agent-src/commands/worktree/verify.md +70 -0
- package/dist/agent-src/commands/worktree.md +85 -0
- package/dist/agent-src/contexts/execution/orchestration-telemetry.md +47 -34
- package/dist/agent-src/rules/design-fidelity.md +40 -0
- package/dist/agent-src/rules/spreadsheet-source-quality.md +61 -0
- package/dist/agent-src/skills/accessibility-auditor/evals/evals.json +15 -0
- package/dist/agent-src/skills/analysis-skill-router/evals/evals.json +13 -0
- package/dist/agent-src/skills/api-testing/SKILL.md +15 -1
- package/dist/agent-src/skills/character-consistency/evals/evals.json +16 -0
- package/dist/agent-src/skills/command-routing/evals/evals.json +14 -0
- package/dist/agent-src/skills/dashboard-design/evals/evals.json +16 -0
- package/dist/agent-src/skills/dcf-modeling/evals/evals.json +20 -0
- package/dist/agent-src/skills/decision-record/evals/evals.json +20 -0
- package/dist/agent-src/skills/design-intelligence/evals/evals.json +14 -0
- package/dist/agent-src/skills/design-review/SKILL.md +6 -0
- package/dist/agent-src/skills/design-system-capture/SKILL.md +21 -0
- package/dist/agent-src/skills/design-system-capture/evals/evals.json +15 -0
- package/dist/agent-src/skills/developer-like-execution/evals/evals.json +16 -0
- package/dist/agent-src/skills/doc-coauthoring/SKILL.md +27 -0
- package/dist/agent-src/skills/doc-coauthoring/evals/evals.json +16 -0
- package/dist/agent-src/skills/editorial-calendar/evals/domain-truth.json +16 -0
- package/dist/agent-src/skills/editorial-calendar/evals/evals.json +16 -0
- package/dist/agent-src/skills/estimate-ticket/evals/evals.json +16 -0
- package/dist/agent-src/skills/existing-ui-audit/SKILL.md +30 -0
- package/dist/agent-src/skills/existing-ui-audit/evals/triggers.json +47 -0
- package/dist/agent-src/skills/fe-design/SKILL.md +36 -5
- package/dist/agent-src/skills/fe-design/evals/triggers.json +47 -0
- package/dist/agent-src/skills/forecasting/evals/evals.json +20 -0
- package/dist/agent-src/skills/fundraising-narrative/evals/domain-truth.json +16 -0
- package/dist/agent-src/skills/fundraising-narrative/evals/evals.json +16 -0
- package/dist/agent-src/skills/html-deck/SKILL.md +27 -0
- package/dist/agent-src/skills/iconography/SKILL.md +24 -0
- package/dist/agent-src/skills/incident-commander/evals/domain-truth.json +16 -0
- package/dist/agent-src/skills/incident-commander/evals/evals.json +20 -0
- package/dist/agent-src/skills/launch-readiness/evals/evals.json +21 -0
- package/dist/agent-src/skills/logging-monitoring/evals/evals.json +16 -0
- package/dist/agent-src/skills/messaging-architecture/evals/domain-truth.json +16 -0
- package/dist/agent-src/skills/messaging-architecture/evals/evals.json +16 -0
- package/dist/agent-src/skills/perf-feedback-craft/evals/evals.json +16 -0
- package/dist/agent-src/skills/pest-testing/SKILL.md +1 -1
- package/dist/agent-src/skills/playwright-testing/SKILL.md +16 -0
- package/dist/agent-src/skills/prompt-engineering-patterns/evals/evals.json +16 -0
- package/dist/agent-src/skills/refine-prompt/evals/evals.json +16 -0
- package/dist/agent-src/skills/refine-ticket/evals/evals.json +16 -0
- package/dist/agent-src/skills/release-comms/evals/evals.json +16 -0
- package/dist/agent-src/skills/rice-prioritization/evals/evals.json +20 -0
- package/dist/agent-src/skills/runway-cognition/evals/evals.json +20 -0
- package/dist/agent-src/skills/scenario-modeling/evals/evals.json +20 -0
- package/dist/agent-src/skills/spreadsheet-authoring/SKILL.md +98 -0
- package/dist/agent-src/skills/spreadsheet-authoring/evals/triggers.json +47 -0
- package/dist/agent-src/skills/systematic-debugging/evals/evals.json +20 -0
- package/dist/agent-src/skills/test-case-discovery/SKILL.md +118 -0
- package/dist/agent-src/skills/test-case-discovery/evals/triggers.json +52 -0
- package/dist/agent-src/skills/test-driven-development/SKILL.md +32 -6
- package/dist/agent-src/skills/test-driven-development/evals/evals.json +16 -0
- package/dist/agent-src/skills/testing-anti-patterns/SKILL.md +7 -6
- package/dist/agent-src/skills/threat-modeling/evals/evals.json +21 -0
- package/dist/agent-src/skills/typography-system/evals/evals.json +14 -0
- package/dist/agent-src/skills/unit-economics-modeling/evals/evals.json +24 -0
- package/dist/agent-src/skills/verify-completion-evidence/evals/evals.json +16 -0
- package/dist/agent-src/skills/vision-articulation/evals/evals.json +16 -0
- package/dist/agent-src/skills/voice-and-tone-design/evals/evals.json +16 -0
- package/dist/agent-src/skills/worktree-lifecycle/SKILL.md +180 -0
- package/dist/agent-src/skills/worktree-lifecycle/evals/triggers.json +52 -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 +249 -32
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +6 -6
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +22 -13
- package/dist/discovery/trust-report.md +4 -4
- package/dist/discovery/workspaces.json +22 -11
- package/dist/mcp/registry-manifest.json +2 -2
- package/dist/router.json +1 -1
- package/docs/CLAIMS.md +3 -3
- package/docs/architecture.md +3 -3
- package/docs/catalog.md +19 -8
- package/docs/command-flows.md +6 -1
- package/docs/contracts/CHANGELOG-conventions.md +6 -0
- package/docs/contracts/design-artifact-lifecycle.md +93 -0
- package/docs/contracts/design-artifact-verification.md +30 -1
- package/docs/contracts/release-pr-gating.md +26 -0
- package/docs/contracts/release-sizing.md +85 -0
- package/docs/contracts/surface-agent-contracts.md +153 -0
- package/docs/decisions/ADR-115-command-cluster-consolidation-phase-4.md +1 -1
- package/docs/distribution/consumer-matrix.md +96 -0
- package/docs/featured-skills.md +2 -2
- package/docs/getting-started-by-role.md +1 -1
- package/docs/getting-started.md +2 -2
- package/docs/governance-advantage.md +1 -1
- package/docs/proof.md +4 -4
- package/docs/release-runbook.md +3 -0
- package/docs/self-review-gate.md +51 -0
- package/package.json +1 -1
- package/src/scripts/_lib/orchestration_record.ts +13 -0
- package/src/scripts/_lib/orchestration_savings.ts +52 -0
- package/src/scripts/check_release_adjacent_health.ts +121 -0
- package/src/scripts/check_rule_invariants.ts +275 -0
- package/src/scripts/consumer_matrix.ts +353 -0
- package/src/scripts/generate_index.ts +3 -2
- package/src/scripts/lint_changelog_rollback.ts +293 -0
- package/src/scripts/orchestration_record.ts +16 -0
- package/src/scripts/orchestration_savings_report.ts +36 -3
- package/src/scripts/second_brain_retrieval.ts +44 -2
- package/src/scripts/self_review_gate.test.ts +65 -0
- package/src/scripts/self_review_gate.ts +272 -0
- package/src/scripts/trigger_eval_grandfather.json +223 -224
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* --spawn-count 1 --token-delta -72000 --provenance measured \
|
|
12
12
|
* --tier-chosen lite --tier-source inferred --task-class read-only-fanout \
|
|
13
13
|
* [--tiers sonnet,opus] [--wall-clock-ms 18500] [--dispatch-outcome DONE] \
|
|
14
|
+
* [--first-pass-success true|false] [--escalated true|false] \
|
|
14
15
|
* [--dir <audit-dir>] [--dry-run]
|
|
15
16
|
*
|
|
16
17
|
* Read by `src/scripts/orchestration_savings_report.ts`.
|
|
@@ -65,6 +66,19 @@ function str(flags: Flags, key: string): string | undefined {
|
|
|
65
66
|
return typeof v === 'string' ? v : undefined;
|
|
66
67
|
}
|
|
67
68
|
|
|
69
|
+
/**
|
|
70
|
+
* Parse an explicit boolean flag value. `--key true|false` → boolean; a bare
|
|
71
|
+
* `--key` counts as `true`; any other value is passed through so the lib's
|
|
72
|
+
* non-boolean validation rejects it with a clear error.
|
|
73
|
+
*/
|
|
74
|
+
function bool(flags: Flags, key: string): boolean | undefined {
|
|
75
|
+
const v = flags[key];
|
|
76
|
+
if (v === undefined) return undefined;
|
|
77
|
+
if (v === true || v === 'true') return true;
|
|
78
|
+
if (v === 'false') return false;
|
|
79
|
+
return v as unknown as boolean; // invalid string → caught by the lib's typeof validation
|
|
80
|
+
}
|
|
81
|
+
|
|
68
82
|
function int(flags: Flags, key: string): number | undefined {
|
|
69
83
|
const v = str(flags, key);
|
|
70
84
|
if (v === undefined) return undefined;
|
|
@@ -104,6 +118,8 @@ export function main(argv: string[] = process.argv.slice(2)): number {
|
|
|
104
118
|
session_tier: str(flags, 'session-tier'),
|
|
105
119
|
dispatch_outcome: str(flags, 'dispatch-outcome') as DispatchOutcome | undefined,
|
|
106
120
|
verify_mode: str(flags, 'verify-mode') as VerifyMode | undefined,
|
|
121
|
+
first_pass_success: bool(flags, 'first-pass-success'),
|
|
122
|
+
escalated: bool(flags, 'escalated'),
|
|
107
123
|
phase: str(flags, 'phase') as LinePhase | undefined,
|
|
108
124
|
outcome: str(flags, 'outcome') as LineOutcome | undefined,
|
|
109
125
|
confidence_band: str(flags, 'confidence-band') as Band | undefined,
|
|
@@ -5,6 +5,11 @@
|
|
|
5
5
|
* and prints an aggregate token-savings report. Read-only; no network, no
|
|
6
6
|
* mutation. Handles a missing/empty audit dir gracefully.
|
|
7
7
|
*
|
|
8
|
+
* QUALITY × COST PAIRING (council verdict): the savings figures never render
|
|
9
|
+
* without the quality columns (first_pass_success_rate / escalation_rate).
|
|
10
|
+
* When ≥ 20 lines in the window carry the quality fields the real rates
|
|
11
|
+
* render; below 20 the columns render as "n/a (n=<count>)" beside savings.
|
|
12
|
+
*
|
|
8
13
|
* Usage:
|
|
9
14
|
* ./scripts-run src/scripts/orchestration_savings_report \
|
|
10
15
|
* [--dir <path>] [--format text|json] [--weights lite=1,medium=5,high=15]
|
|
@@ -77,13 +82,41 @@ export function readAuditLines(dir: string): AuditLine[] {
|
|
|
77
82
|
return lines;
|
|
78
83
|
}
|
|
79
84
|
|
|
80
|
-
|
|
85
|
+
/** Dispatches carrying the quality fields at/above this render the real rates; below, "n/a (n=<count>)". */
|
|
86
|
+
export const QUALITY_GATE_MIN_LINES = 20;
|
|
87
|
+
|
|
88
|
+
function pct(v: number): string {
|
|
89
|
+
return `${(v * 100).toFixed(0)}%`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Render the paired cost × quality block. Council verdict: savings NEVER render
|
|
94
|
+
* without the quality columns — at n ≥ QUALITY_GATE_MIN_LINES the real rates
|
|
95
|
+
* render; below, the quality columns render as "n/a (n=<count>)" alongside the
|
|
96
|
+
* savings figures. There is no savings-only render path.
|
|
97
|
+
*/
|
|
98
|
+
function renderPairedCostQuality(r: SavingsReport): string[] {
|
|
99
|
+
const q = r.quality;
|
|
100
|
+
const gated = q.quality_lines >= QUALITY_GATE_MIN_LINES;
|
|
101
|
+
const fps = gated && q.first_pass_success_rate !== null
|
|
102
|
+
? `${pct(q.first_pass_success_rate)} (n=${q.first_pass_lines})`
|
|
103
|
+
: `n/a (n=${q.first_pass_lines})`;
|
|
104
|
+
const esc = gated && q.escalation_rate !== null
|
|
105
|
+
? `${pct(q.escalation_rate)} (n=${q.escalated_lines})`
|
|
106
|
+
: `n/a (n=${q.escalated_lines})`;
|
|
107
|
+
return [
|
|
108
|
+
' cost × quality (paired — never savings alone):',
|
|
109
|
+
` net token_delta: ${r.net_token_delta} (negative = net saved) | first_pass_success_rate: ${fps}`,
|
|
110
|
+
` tokens saved: ${r.tokens_saved} · tokens added: ${r.tokens_added} | escalation_rate: ${esc}`,
|
|
111
|
+
];
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function renderText(r: SavingsReport, dir: string): string {
|
|
81
115
|
const out: string[] = [];
|
|
82
116
|
out.push('Orchestration savings report');
|
|
83
117
|
out.push(` source: ${dir}`);
|
|
84
118
|
out.push(` dispatches: ${r.dispatches} (total spawns: ${r.total_spawns})`);
|
|
85
|
-
out.push(
|
|
86
|
-
out.push(` tokens saved: ${r.tokens_saved} tokens added: ${r.tokens_added}`);
|
|
119
|
+
out.push(...renderPairedCostQuality(r));
|
|
87
120
|
out.push(` provenance: measured ${r.by_provenance.measured.dispatches} (Δ ${r.by_provenance.measured.net_token_delta}) · estimated ${r.by_provenance.estimated.dispatches} (Δ ${r.by_provenance.estimated.net_token_delta})`);
|
|
88
121
|
out.push(` measured share: ${(r.measured_share * 100).toFixed(0)}%`);
|
|
89
122
|
if (Object.keys(r.by_tier).length) {
|
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
_setMemoryRoot,
|
|
39
39
|
retrieve,
|
|
40
40
|
} from './memory_lookup.js';
|
|
41
|
+
import { sanitize_text } from './_lib/retrieval_sanitize.js';
|
|
41
42
|
import { scoreTask } from './second_brain_score.js';
|
|
42
43
|
|
|
43
44
|
const _HERE = fileURLToPath(import.meta.url);
|
|
@@ -67,11 +68,19 @@ export function bindStore(): void {
|
|
|
67
68
|
_setIntakeRoot(path.join(STORE_DIR, 'intake-none'));
|
|
68
69
|
}
|
|
69
70
|
|
|
70
|
-
export function loadCorpus(): {
|
|
71
|
+
export function loadCorpus(): {
|
|
72
|
+
type: string;
|
|
73
|
+
k: number;
|
|
74
|
+
tasks: RetrievalTask[];
|
|
75
|
+
stale_ids?: string[];
|
|
76
|
+
poisoned_ids?: string[];
|
|
77
|
+
} {
|
|
71
78
|
const doc = YAML.parse(fs.readFileSync(path.join(STORE_DIR, 'retrieval-corpus.yml'), 'utf-8')) as {
|
|
72
79
|
type: string;
|
|
73
80
|
k: number;
|
|
74
81
|
tasks: RetrievalTask[];
|
|
82
|
+
stale_ids?: string[];
|
|
83
|
+
poisoned_ids?: string[];
|
|
75
84
|
};
|
|
76
85
|
return doc;
|
|
77
86
|
}
|
|
@@ -162,11 +171,17 @@ export function signTestP(wins: number, losses: number): number {
|
|
|
162
171
|
|
|
163
172
|
export async function run(opts: { mode: 'dry-run' | 'live'; host: string; seeds: number }): Promise<Record<string, unknown>> {
|
|
164
173
|
bindStore();
|
|
165
|
-
const { type, k, tasks } = loadCorpus();
|
|
174
|
+
const { type, k, tasks, stale_ids = [], poisoned_ids = [] } = loadCorpus();
|
|
175
|
+
const staleSet = new Set(stale_ids);
|
|
176
|
+
const poisonedSet = new Set(poisoned_ids);
|
|
166
177
|
const apiKey = opts.mode === 'live' ? load_anthropic_key() : '';
|
|
167
178
|
const placebo = _placeboBodies(type);
|
|
168
179
|
|
|
169
180
|
let precisionHits = 0;
|
|
181
|
+
let topkSlots = 0;
|
|
182
|
+
let staleHits = 0;
|
|
183
|
+
let poisonedRetrieved = 0;
|
|
184
|
+
let poisonedNeutralized = 0;
|
|
170
185
|
let tieSetSum = 0;
|
|
171
186
|
const perTask: Array<Record<string, unknown>> = [];
|
|
172
187
|
const armPass: Record<Arm, number> = { 'retrieval-off': 0, 'retrieval-on': 0, placebo: 0 };
|
|
@@ -177,6 +192,18 @@ export async function run(opts: { mode: 'dry-run' | 'live'; host: string; seeds:
|
|
|
177
192
|
for (const task of tasks) {
|
|
178
193
|
const { topk, rank, tieSetSize } = retrieveFor(task, type, k);
|
|
179
194
|
const ids = topk.map((r) => r.id);
|
|
195
|
+
topkSlots += topk.length;
|
|
196
|
+
for (const hit of topk) {
|
|
197
|
+
if (staleSet.has(hit.id)) staleHits += 1;
|
|
198
|
+
if (poisonedSet.has(hit.id)) {
|
|
199
|
+
poisonedRetrieved += 1;
|
|
200
|
+
// Rejection = the sanitize floor strips every hidden-instruction
|
|
201
|
+
// vector before the body could reach a prompt.
|
|
202
|
+
const clean = sanitize_text(hit.body);
|
|
203
|
+
if (!/[---]/u.test(clean)) poisonedNeutralized += 1;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
180
207
|
const neededInTopK = rank >= 0 && rank < k;
|
|
181
208
|
if (neededInTopK) precisionHits += 1;
|
|
182
209
|
tieSetSum += tieSetSize;
|
|
@@ -226,6 +253,21 @@ export async function run(opts: { mode: 'dry-run' | 'live'; host: string; seeds:
|
|
|
226
253
|
k,
|
|
227
254
|
store: 'internal/bench/second-brain/retrieval-store/ (5 needed + 19 distractors, keyword-overlapping confusers)',
|
|
228
255
|
precision_at_k: precisionAtK,
|
|
256
|
+
// road-to-proof-under-real-conditions Phase 5 — retrieval quality pair:
|
|
257
|
+
// stale_hit_rate: fraction of retrieved top-k slots occupied by
|
|
258
|
+
// SUPERSEDED store entries (lower is better; the substrate has no
|
|
259
|
+
// recency/supersede weighting yet, so a non-zero rate is the honest
|
|
260
|
+
// baseline the FTS5/recency activation path improves on).
|
|
261
|
+
stale_hit_rate: topkSlots ? staleHits / topkSlots : 0,
|
|
262
|
+
stale_hits: staleHits,
|
|
263
|
+
topk_slots: topkSlots,
|
|
264
|
+
// poisoned_rejection_rate: of poisoned entries that WERE retrieved,
|
|
265
|
+
// the fraction whose hidden-instruction vectors the sanitize floor
|
|
266
|
+
// strips (target 1.0; null when none was retrieved this run).
|
|
267
|
+
poisoned_rejection_rate: poisonedRetrieved ? poisonedNeutralized / poisonedRetrieved : null,
|
|
268
|
+
poisoned_retrieved: poisonedRetrieved,
|
|
269
|
+
poisoned_neutralized: poisonedNeutralized,
|
|
270
|
+
|
|
229
271
|
precision_hits: precisionHits,
|
|
230
272
|
mean_tie_set_size: meanTieSet,
|
|
231
273
|
discrimination_note:
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
classifyBlocking,
|
|
5
|
+
gateVerdict,
|
|
6
|
+
isReviewablePath,
|
|
7
|
+
renderReview,
|
|
8
|
+
type Finding,
|
|
9
|
+
} from './self_review_gate.js';
|
|
10
|
+
|
|
11
|
+
const f = (severity: Finding['severity'], kind: Finding['kind']): Finding => ({
|
|
12
|
+
severity,
|
|
13
|
+
kind,
|
|
14
|
+
title: `${severity} ${kind}`,
|
|
15
|
+
detail: 'x',
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
describe('classifyBlocking', () => {
|
|
19
|
+
it('blocks security + high and claim + critical', () => {
|
|
20
|
+
expect(classifyBlocking(f('high', 'security'))).toBe(true);
|
|
21
|
+
expect(classifyBlocking(f('critical', 'claim'))).toBe(true);
|
|
22
|
+
});
|
|
23
|
+
it('does not block low/medium security, or any style/correctness', () => {
|
|
24
|
+
expect(classifyBlocking(f('medium', 'security'))).toBe(false);
|
|
25
|
+
expect(classifyBlocking(f('low', 'claim'))).toBe(false);
|
|
26
|
+
expect(classifyBlocking(f('critical', 'style'))).toBe(false);
|
|
27
|
+
expect(classifyBlocking(f('high', 'correctness'))).toBe(false);
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe('gateVerdict', () => {
|
|
32
|
+
const blocking = [f('high', 'security')];
|
|
33
|
+
it('advisory (default shipped mode) never blocks, even with a blocking finding', () => {
|
|
34
|
+
expect(gateVerdict(blocking, { enforce: false })).toBe(0);
|
|
35
|
+
});
|
|
36
|
+
it('enforce blocks only on a merge-blocking finding', () => {
|
|
37
|
+
expect(gateVerdict(blocking, { enforce: true })).toBe(2);
|
|
38
|
+
expect(gateVerdict([f('critical', 'style')], { enforce: true })).toBe(0);
|
|
39
|
+
expect(gateVerdict([], { enforce: true })).toBe(0);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe('isReviewablePath', () => {
|
|
44
|
+
it('skips generated projections + lockfiles, keeps source', () => {
|
|
45
|
+
expect(isReviewablePath('dist/agent-src/skills/x/SKILL.md')).toBe(false);
|
|
46
|
+
expect(isReviewablePath('.augment/rules/x.md')).toBe(false);
|
|
47
|
+
expect(isReviewablePath('.windsurfrules')).toBe(false);
|
|
48
|
+
expect(isReviewablePath('package-lock.json')).toBe(false);
|
|
49
|
+
expect(isReviewablePath('src/rules/x.md')).toBe(true);
|
|
50
|
+
expect(isReviewablePath('src/scripts/y.ts')).toBe(true);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe('renderReview', () => {
|
|
55
|
+
it('carries the HUMAN REVIEW REQUIRED banner + the floor-not-human-review caveat', () => {
|
|
56
|
+
const out = renderReview([], false);
|
|
57
|
+
expect(out).toContain('HUMAN REVIEW REQUIRED');
|
|
58
|
+
expect(out).toContain('not** independent human review');
|
|
59
|
+
});
|
|
60
|
+
it('advisory phrasing says WOULD block, enforce phrasing says blocking', () => {
|
|
61
|
+
const blocking = [f('high', 'security')];
|
|
62
|
+
expect(renderReview(blocking, false)).toContain('WOULD block');
|
|
63
|
+
expect(renderReview(blocking, true)).toContain('merge-blocking');
|
|
64
|
+
});
|
|
65
|
+
});
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Self-review gate — the package reviewing its own PRs with the exact machinery
|
|
3
|
+
* it sells (`adversarial-review` + `agent-security-review`).
|
|
4
|
+
*
|
|
5
|
+
* road-to-maintainer-bus-factor Phase 1. Ships ADVISORY + INERT-WITHOUT-SECRET:
|
|
6
|
+
* - `--dry-run` (the PR gate job): no API, no spend, no secret. Assembles and
|
|
7
|
+
* prints the review plan (which skills, which files, budget estimate) and
|
|
8
|
+
* validates the harness. Always exit 0.
|
|
9
|
+
* - live `--advisory` (the secret-gated job): loads the two review skill bodies
|
|
10
|
+
* as the system prompt, sends the diff to the model, collects structured
|
|
11
|
+
* findings, classifies them, records a verdict, and posts a PR review. In
|
|
12
|
+
* advisory mode it NEVER blocks the merge (exit 0) — it records what WOULD
|
|
13
|
+
* block. If no key is available it is a logged no-op (exit 0), never a failure.
|
|
14
|
+
*
|
|
15
|
+
* Turning the gate REQUIRED (block-on-verdict) and wiring the API secret + the
|
|
16
|
+
* per-PR budget are the maintainer's acts (blocker `self-review-gate-cost`); this
|
|
17
|
+
* script exposes the `--enforce` path so that flip is a one-flag change, but the
|
|
18
|
+
* shipped workflow runs advisory only.
|
|
19
|
+
*
|
|
20
|
+
* `classifyBlocking` + `gateVerdict` are pure and unit-tested (no API) — mirrors
|
|
21
|
+
* the `check_quality_regression.gateVerdict` pattern.
|
|
22
|
+
*/
|
|
23
|
+
import { spawnSync } from 'node:child_process';
|
|
24
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
25
|
+
import path from 'node:path';
|
|
26
|
+
import { fileURLToPath } from 'node:url';
|
|
27
|
+
|
|
28
|
+
import { AnthropicClient, load_anthropic_key } from './ai_council/clients.js';
|
|
29
|
+
|
|
30
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
31
|
+
const REPO_ROOT = path.resolve(__dirname, '..', '..');
|
|
32
|
+
|
|
33
|
+
// ── Finding model ─────────────────────────────────────────────────────
|
|
34
|
+
export type Severity = 'critical' | 'high' | 'medium' | 'low';
|
|
35
|
+
export type FindingKind = 'security' | 'claim' | 'correctness' | 'style';
|
|
36
|
+
|
|
37
|
+
export interface Finding {
|
|
38
|
+
severity: Severity;
|
|
39
|
+
kind: FindingKind;
|
|
40
|
+
title: string;
|
|
41
|
+
detail: string;
|
|
42
|
+
file?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* A finding is merge-blocking iff it is security- or claim-affecting AND at
|
|
47
|
+
* least `high` severity. Style and correctness findings advise; low/medium
|
|
48
|
+
* security findings advise. Council 2026-07-08 (claude-sonnet-4-5 + gpt-4o):
|
|
49
|
+
* a 100 %-blocking gate at solo-maintainer token cost gets ignored or gamed —
|
|
50
|
+
* block ONLY on the narrow security/claim × {critical,high} intersection.
|
|
51
|
+
*/
|
|
52
|
+
export function classifyBlocking(f: Finding): boolean {
|
|
53
|
+
return (f.kind === 'security' || f.kind === 'claim') && (f.severity === 'critical' || f.severity === 'high');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Deterministic gate verdict. Mirrors `check_quality_regression.gateVerdict`:
|
|
58
|
+
* exit 2 blocks the merge, exit 0 passes.
|
|
59
|
+
* - advisory (default shipped mode): always 0; the caller reports the
|
|
60
|
+
* would-block set without failing the check.
|
|
61
|
+
* - enforce (maintainer flip): 2 iff any finding is merge-blocking.
|
|
62
|
+
*/
|
|
63
|
+
export function gateVerdict(findings: readonly Finding[], opts: { enforce: boolean }): 0 | 2 {
|
|
64
|
+
if (!opts.enforce) return 0;
|
|
65
|
+
return findings.some(classifyBlocking) ? 2 : 0;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ── Diff collection ───────────────────────────────────────────────────
|
|
69
|
+
/** Generated projections + lockfiles a self-review should not re-read. */
|
|
70
|
+
const SKIP_PREFIXES = ['dist/agent-src/', '.augment/', '.claude/', '.cursor/', '.clinerules/'];
|
|
71
|
+
const SKIP_EXACT = new Set(['.windsurfrules', 'GEMINI.md', 'package-lock.json']);
|
|
72
|
+
|
|
73
|
+
export function isReviewablePath(p: string): boolean {
|
|
74
|
+
if (SKIP_EXACT.has(p)) return false;
|
|
75
|
+
return !SKIP_PREFIXES.some((pre) => p.startsWith(pre));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function changedFiles(baseRef: string): string[] {
|
|
79
|
+
const r = spawnSync('git', ['diff', '--name-only', `${baseRef}...HEAD`], {
|
|
80
|
+
cwd: REPO_ROOT,
|
|
81
|
+
encoding: 'utf8',
|
|
82
|
+
});
|
|
83
|
+
if (r.status !== 0) {
|
|
84
|
+
throw new Error(`git diff failed: ${(r.stderr ?? '').toString().slice(0, 300)}`);
|
|
85
|
+
}
|
|
86
|
+
return (r.stdout ?? '')
|
|
87
|
+
.split('\n')
|
|
88
|
+
.map((s) => s.trim())
|
|
89
|
+
.filter(Boolean)
|
|
90
|
+
.filter(isReviewablePath);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function diffText(baseRef: string, files: string[]): string {
|
|
94
|
+
if (files.length === 0) return '';
|
|
95
|
+
const r = spawnSync('git', ['diff', `${baseRef}...HEAD`, '--', ...files], {
|
|
96
|
+
cwd: REPO_ROOT,
|
|
97
|
+
encoding: 'utf8',
|
|
98
|
+
maxBuffer: 32 * 1024 * 1024,
|
|
99
|
+
});
|
|
100
|
+
return (r.stdout ?? '').toString();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ── Review skill bodies (the system prompt) ───────────────────────────
|
|
104
|
+
const REVIEW_SKILLS = ['adversarial-review', 'agent-security-review'] as const;
|
|
105
|
+
|
|
106
|
+
function loadSkillBody(name: string): string {
|
|
107
|
+
const p = path.join(REPO_ROOT, 'src', 'skills', name, 'SKILL.md');
|
|
108
|
+
return readFileSync(p, 'utf8');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function buildSystemPrompt(): string {
|
|
112
|
+
const bodies = REVIEW_SKILLS.map((s) => `# Skill: ${s}\n\n${loadSkillBody(s)}`).join('\n\n---\n\n');
|
|
113
|
+
return [
|
|
114
|
+
'You are the self-review gate for an AI-agent governance package. Apply the',
|
|
115
|
+
'two skills below to the supplied PR diff. Return ONLY a JSON object of the',
|
|
116
|
+
'shape {"findings":[{"severity":"critical|high|medium|low",',
|
|
117
|
+
'"kind":"security|claim|correctness|style","title":"...","detail":"...",',
|
|
118
|
+
'"file":"optional/path"}]}. Use kind="claim" for an unbacked headline number',
|
|
119
|
+
'or a proof/CLAIMS-affecting statement, kind="security" for an auth/tenant/',
|
|
120
|
+
'secret/egress/injection gap. No prose outside the JSON.',
|
|
121
|
+
'',
|
|
122
|
+
bodies,
|
|
123
|
+
].join('\n');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ── Plan (dry-run) ────────────────────────────────────────────────────
|
|
127
|
+
export interface ReviewPlan {
|
|
128
|
+
skills: string[];
|
|
129
|
+
files: string[];
|
|
130
|
+
promptChars: number;
|
|
131
|
+
note: string;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function buildPlan(baseRef: string): ReviewPlan {
|
|
135
|
+
const files = changedFiles(baseRef);
|
|
136
|
+
const diff = diffText(baseRef, files);
|
|
137
|
+
return {
|
|
138
|
+
skills: [...REVIEW_SKILLS],
|
|
139
|
+
files,
|
|
140
|
+
promptChars: buildSystemPrompt().length + diff.length,
|
|
141
|
+
note:
|
|
142
|
+
files.length === 0
|
|
143
|
+
? 'No reviewable (non-generated) files changed — the live review would no-op.'
|
|
144
|
+
: `${files.length} reviewable file(s); live review would send ~${Math.ceil(
|
|
145
|
+
(buildSystemPrompt().length + diff.length) / 4,
|
|
146
|
+
)} input tokens.`,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ── Live review ───────────────────────────────────────────────────────
|
|
151
|
+
function parseFindings(text: string): Finding[] {
|
|
152
|
+
const start = text.indexOf('{');
|
|
153
|
+
const end = text.lastIndexOf('}');
|
|
154
|
+
if (start < 0 || end <= start) return [];
|
|
155
|
+
let parsed: unknown;
|
|
156
|
+
try {
|
|
157
|
+
parsed = JSON.parse(text.slice(start, end + 1));
|
|
158
|
+
} catch {
|
|
159
|
+
return [];
|
|
160
|
+
}
|
|
161
|
+
const arr = (parsed as { findings?: unknown })?.findings;
|
|
162
|
+
if (!Array.isArray(arr)) return [];
|
|
163
|
+
const sev = new Set<Severity>(['critical', 'high', 'medium', 'low']);
|
|
164
|
+
const kind = new Set<FindingKind>(['security', 'claim', 'correctness', 'style']);
|
|
165
|
+
return arr
|
|
166
|
+
.filter((f): f is Record<string, unknown> => typeof f === 'object' && f !== null)
|
|
167
|
+
.filter((f) => sev.has(f.severity as Severity) && kind.has(f.kind as FindingKind))
|
|
168
|
+
.map((f) => ({
|
|
169
|
+
severity: f.severity as Severity,
|
|
170
|
+
kind: f.kind as FindingKind,
|
|
171
|
+
title: String(f.title ?? '').slice(0, 300),
|
|
172
|
+
detail: String(f.detail ?? '').slice(0, 2000),
|
|
173
|
+
...(typeof f.file === 'string' ? { file: f.file } : {}),
|
|
174
|
+
}));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function renderReview(findings: Finding[], enforce: boolean): string {
|
|
178
|
+
const banner =
|
|
179
|
+
'> HUMAN REVIEW REQUIRED — dogfooded AI adversarial-review + security gate. ' +
|
|
180
|
+
'Findings are decision support, not a guarantee; detection is probabilistic. ' +
|
|
181
|
+
'This is a floor, **not** independent human review.';
|
|
182
|
+
if (findings.length === 0) {
|
|
183
|
+
return `${banner}\n\n✅ Self-review gate: no findings.`;
|
|
184
|
+
}
|
|
185
|
+
const blocking = findings.filter(classifyBlocking);
|
|
186
|
+
const rows = findings
|
|
187
|
+
.map((f) => `| ${f.severity} | ${f.kind} | ${f.file ?? '—'} | ${f.title} |`)
|
|
188
|
+
.join('\n');
|
|
189
|
+
const verdictLine = blocking.length
|
|
190
|
+
? enforce
|
|
191
|
+
? `❌ ${blocking.length} merge-blocking finding(s) (security/claim × high+).`
|
|
192
|
+
: `⚠️ ${blocking.length} finding(s) WOULD block merge under an enforced gate (advisory now).`
|
|
193
|
+
: '✅ No merge-blocking findings (style/correctness advise only).';
|
|
194
|
+
return `${banner}\n\n${verdictLine}\n\n| severity | kind | file | finding |\n|---|---|---|---|\n${rows}`;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function postReview(body: string): void {
|
|
198
|
+
const pr = process.env.PR_NUMBER;
|
|
199
|
+
if (!pr) {
|
|
200
|
+
process.stdout.write('::notice::self-review-gate — no PR_NUMBER; printing review instead of posting.\n');
|
|
201
|
+
process.stdout.write(body + '\n');
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
const r = spawnSync('gh', ['pr', 'comment', pr, '--body', body], { cwd: REPO_ROOT, encoding: 'utf8' });
|
|
205
|
+
if (r.status !== 0) {
|
|
206
|
+
process.stdout.write(`::warning::self-review-gate — gh pr comment failed: ${(r.stderr ?? '').toString().slice(0, 300)}\n`);
|
|
207
|
+
process.stdout.write(body + '\n');
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function resolveKey(): string | null {
|
|
212
|
+
const env = process.env.ANTHROPIC_API_KEY;
|
|
213
|
+
if (env && env.trim()) return env.trim();
|
|
214
|
+
try {
|
|
215
|
+
return load_anthropic_key();
|
|
216
|
+
} catch {
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// ── CLI ───────────────────────────────────────────────────────────────
|
|
222
|
+
export function main(argv: string[]): 0 | 2 {
|
|
223
|
+
const dryRun = argv.includes('--dry-run');
|
|
224
|
+
const enforce = argv.includes('--enforce');
|
|
225
|
+
const baseIdx = argv.indexOf('--base');
|
|
226
|
+
const baseRef = (baseIdx >= 0 ? argv[baseIdx + 1] : undefined) ?? 'origin/main';
|
|
227
|
+
|
|
228
|
+
const plan = buildPlan(baseRef);
|
|
229
|
+
|
|
230
|
+
if (dryRun) {
|
|
231
|
+
process.stdout.write(
|
|
232
|
+
`self-review-gate (dry-run — no spend, advisory):\n` +
|
|
233
|
+
` skills: ${plan.skills.join(', ')}\n` +
|
|
234
|
+
` files: ${plan.files.length}\n` +
|
|
235
|
+
` ${plan.note}\n`,
|
|
236
|
+
);
|
|
237
|
+
return 0;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (plan.files.length === 0) {
|
|
241
|
+
process.stdout.write('::notice::self-review-gate — no reviewable files changed; skipping.\n');
|
|
242
|
+
return 0;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const key = resolveKey();
|
|
246
|
+
if (!key) {
|
|
247
|
+
process.stdout.write(
|
|
248
|
+
'::notice::self-review-gate skipped — set the ANTHROPIC_API_KEY repo secret to enable the live dogfooded review.\n',
|
|
249
|
+
);
|
|
250
|
+
return 0;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const client = new AnthropicClient({ api_key: key });
|
|
254
|
+
const resp = client.ask(buildSystemPrompt(), diffText(baseRef, plan.files), 4096);
|
|
255
|
+
if (resp.error) {
|
|
256
|
+
process.stdout.write(`::warning::self-review-gate — model call failed: ${resp.error}\n`);
|
|
257
|
+
return 0; // advisory: a transport failure never blocks the merge
|
|
258
|
+
}
|
|
259
|
+
const findings = parseFindings(resp.text);
|
|
260
|
+
const body = renderReview(findings, enforce);
|
|
261
|
+
postReview(body);
|
|
262
|
+
|
|
263
|
+
const code = gateVerdict(findings, { enforce });
|
|
264
|
+
if (code === 2) {
|
|
265
|
+
process.stdout.write('::error::self-review-gate — merge-blocking findings under enforce mode.\n');
|
|
266
|
+
}
|
|
267
|
+
return code;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (existsSync(process.argv[1] ?? '') && import.meta.url === `file://${process.argv[1]}`) {
|
|
271
|
+
process.exit(main(process.argv.slice(2)));
|
|
272
|
+
}
|