@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.
Files changed (117) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +41 -0
  3. package/README.md +5 -5
  4. package/dist/agent-src/commands/cost/report.md +13 -3
  5. package/dist/agent-src/commands/tests/create.md +28 -6
  6. package/dist/agent-src/commands/work.md +33 -0
  7. package/dist/agent-src/commands/worktree/cleanup.md +78 -0
  8. package/dist/agent-src/commands/worktree/create.md +62 -0
  9. package/dist/agent-src/commands/worktree/status.md +63 -0
  10. package/dist/agent-src/commands/worktree/verify.md +70 -0
  11. package/dist/agent-src/commands/worktree.md +85 -0
  12. package/dist/agent-src/contexts/execution/orchestration-telemetry.md +47 -34
  13. package/dist/agent-src/rules/design-fidelity.md +40 -0
  14. package/dist/agent-src/rules/spreadsheet-source-quality.md +61 -0
  15. package/dist/agent-src/skills/accessibility-auditor/evals/evals.json +15 -0
  16. package/dist/agent-src/skills/analysis-skill-router/evals/evals.json +13 -0
  17. package/dist/agent-src/skills/api-testing/SKILL.md +15 -1
  18. package/dist/agent-src/skills/character-consistency/evals/evals.json +16 -0
  19. package/dist/agent-src/skills/command-routing/evals/evals.json +14 -0
  20. package/dist/agent-src/skills/dashboard-design/evals/evals.json +16 -0
  21. package/dist/agent-src/skills/dcf-modeling/evals/evals.json +20 -0
  22. package/dist/agent-src/skills/decision-record/evals/evals.json +20 -0
  23. package/dist/agent-src/skills/design-intelligence/evals/evals.json +14 -0
  24. package/dist/agent-src/skills/design-review/SKILL.md +6 -0
  25. package/dist/agent-src/skills/design-system-capture/SKILL.md +21 -0
  26. package/dist/agent-src/skills/design-system-capture/evals/evals.json +15 -0
  27. package/dist/agent-src/skills/developer-like-execution/evals/evals.json +16 -0
  28. package/dist/agent-src/skills/doc-coauthoring/SKILL.md +27 -0
  29. package/dist/agent-src/skills/doc-coauthoring/evals/evals.json +16 -0
  30. package/dist/agent-src/skills/editorial-calendar/evals/domain-truth.json +16 -0
  31. package/dist/agent-src/skills/editorial-calendar/evals/evals.json +16 -0
  32. package/dist/agent-src/skills/estimate-ticket/evals/evals.json +16 -0
  33. package/dist/agent-src/skills/existing-ui-audit/SKILL.md +30 -0
  34. package/dist/agent-src/skills/existing-ui-audit/evals/triggers.json +47 -0
  35. package/dist/agent-src/skills/fe-design/SKILL.md +36 -5
  36. package/dist/agent-src/skills/fe-design/evals/triggers.json +47 -0
  37. package/dist/agent-src/skills/forecasting/evals/evals.json +20 -0
  38. package/dist/agent-src/skills/fundraising-narrative/evals/domain-truth.json +16 -0
  39. package/dist/agent-src/skills/fundraising-narrative/evals/evals.json +16 -0
  40. package/dist/agent-src/skills/html-deck/SKILL.md +27 -0
  41. package/dist/agent-src/skills/iconography/SKILL.md +24 -0
  42. package/dist/agent-src/skills/incident-commander/evals/domain-truth.json +16 -0
  43. package/dist/agent-src/skills/incident-commander/evals/evals.json +20 -0
  44. package/dist/agent-src/skills/launch-readiness/evals/evals.json +21 -0
  45. package/dist/agent-src/skills/logging-monitoring/evals/evals.json +16 -0
  46. package/dist/agent-src/skills/messaging-architecture/evals/domain-truth.json +16 -0
  47. package/dist/agent-src/skills/messaging-architecture/evals/evals.json +16 -0
  48. package/dist/agent-src/skills/perf-feedback-craft/evals/evals.json +16 -0
  49. package/dist/agent-src/skills/pest-testing/SKILL.md +1 -1
  50. package/dist/agent-src/skills/playwright-testing/SKILL.md +16 -0
  51. package/dist/agent-src/skills/prompt-engineering-patterns/evals/evals.json +16 -0
  52. package/dist/agent-src/skills/refine-prompt/evals/evals.json +16 -0
  53. package/dist/agent-src/skills/refine-ticket/evals/evals.json +16 -0
  54. package/dist/agent-src/skills/release-comms/evals/evals.json +16 -0
  55. package/dist/agent-src/skills/rice-prioritization/evals/evals.json +20 -0
  56. package/dist/agent-src/skills/runway-cognition/evals/evals.json +20 -0
  57. package/dist/agent-src/skills/scenario-modeling/evals/evals.json +20 -0
  58. package/dist/agent-src/skills/spreadsheet-authoring/SKILL.md +98 -0
  59. package/dist/agent-src/skills/spreadsheet-authoring/evals/triggers.json +47 -0
  60. package/dist/agent-src/skills/systematic-debugging/evals/evals.json +20 -0
  61. package/dist/agent-src/skills/test-case-discovery/SKILL.md +118 -0
  62. package/dist/agent-src/skills/test-case-discovery/evals/triggers.json +52 -0
  63. package/dist/agent-src/skills/test-driven-development/SKILL.md +32 -6
  64. package/dist/agent-src/skills/test-driven-development/evals/evals.json +16 -0
  65. package/dist/agent-src/skills/testing-anti-patterns/SKILL.md +7 -6
  66. package/dist/agent-src/skills/threat-modeling/evals/evals.json +21 -0
  67. package/dist/agent-src/skills/typography-system/evals/evals.json +14 -0
  68. package/dist/agent-src/skills/unit-economics-modeling/evals/evals.json +24 -0
  69. package/dist/agent-src/skills/verify-completion-evidence/evals/evals.json +16 -0
  70. package/dist/agent-src/skills/vision-articulation/evals/evals.json +16 -0
  71. package/dist/agent-src/skills/voice-and-tone-design/evals/evals.json +16 -0
  72. package/dist/agent-src/skills/worktree-lifecycle/SKILL.md +180 -0
  73. package/dist/agent-src/skills/worktree-lifecycle/evals/triggers.json +52 -0
  74. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  75. package/dist/discovery/deprecation-report.md +1 -1
  76. package/dist/discovery/discovery-manifest.json +249 -32
  77. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  78. package/dist/discovery/discovery-manifest.summary.md +6 -6
  79. package/dist/discovery/orphan-report.md +1 -1
  80. package/dist/discovery/packs.json +22 -13
  81. package/dist/discovery/trust-report.md +4 -4
  82. package/dist/discovery/workspaces.json +22 -11
  83. package/dist/mcp/registry-manifest.json +2 -2
  84. package/dist/router.json +1 -1
  85. package/docs/CLAIMS.md +3 -3
  86. package/docs/architecture.md +3 -3
  87. package/docs/catalog.md +19 -8
  88. package/docs/command-flows.md +6 -1
  89. package/docs/contracts/CHANGELOG-conventions.md +6 -0
  90. package/docs/contracts/design-artifact-lifecycle.md +93 -0
  91. package/docs/contracts/design-artifact-verification.md +30 -1
  92. package/docs/contracts/release-pr-gating.md +26 -0
  93. package/docs/contracts/release-sizing.md +85 -0
  94. package/docs/contracts/surface-agent-contracts.md +153 -0
  95. package/docs/decisions/ADR-115-command-cluster-consolidation-phase-4.md +1 -1
  96. package/docs/distribution/consumer-matrix.md +96 -0
  97. package/docs/featured-skills.md +2 -2
  98. package/docs/getting-started-by-role.md +1 -1
  99. package/docs/getting-started.md +2 -2
  100. package/docs/governance-advantage.md +1 -1
  101. package/docs/proof.md +4 -4
  102. package/docs/release-runbook.md +3 -0
  103. package/docs/self-review-gate.md +51 -0
  104. package/package.json +1 -1
  105. package/src/scripts/_lib/orchestration_record.ts +13 -0
  106. package/src/scripts/_lib/orchestration_savings.ts +52 -0
  107. package/src/scripts/check_release_adjacent_health.ts +121 -0
  108. package/src/scripts/check_rule_invariants.ts +275 -0
  109. package/src/scripts/consumer_matrix.ts +353 -0
  110. package/src/scripts/generate_index.ts +3 -2
  111. package/src/scripts/lint_changelog_rollback.ts +293 -0
  112. package/src/scripts/orchestration_record.ts +16 -0
  113. package/src/scripts/orchestration_savings_report.ts +36 -3
  114. package/src/scripts/second_brain_retrieval.ts +44 -2
  115. package/src/scripts/self_review_gate.test.ts +65 -0
  116. package/src/scripts/self_review_gate.ts +272 -0
  117. 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
- function renderText(r: SavingsReport, dir: string): string {
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(` net token_delta: ${r.net_token_delta} (negative = net saved)`);
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(): { type: string; k: number; tasks: RetrievalTask[] } {
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
+ }