@event4u/agent-config 9.3.0 → 9.5.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 +92 -0
- package/CONTRIBUTING.md +14 -0
- package/README.md +12 -4
- package/dist/agent-src/commands/contribution-precheck.md +100 -0
- package/dist/agent-src/commands/team.md +4 -0
- package/dist/agent-src/contexts/model-recommendations.md +6 -0
- package/dist/agent-src/rules/token-efficiency.md +7 -0
- package/dist/agent-src/skills/adversarial-review/SKILL.md +5 -0
- package/dist/agent-src/skills/ai-council/SKILL.md +3 -0
- package/dist/agent-src/skills/build-buy-partner/SKILL.md +1 -0
- package/dist/agent-src/skills/decision-record/SKILL.md +35 -1
- package/dist/agent-src/skills/decision-record/evals/triggers.json +82 -12
- package/dist/agent-src/skills/decision-record/references/weighted-matrix.md +119 -0
- package/dist/agent-src/skills/evaluate-llm-feature/SKILL.md +142 -0
- package/dist/agent-src/skills/judge-synthesis/SKILL.md +1 -0
- package/dist/agent-src/skills/learning-tutor/SKILL.md +215 -0
- package/dist/agent-src/skills/rice-prioritization/SKILL.md +1 -0
- package/dist/agent-src/skills/stakeholder-tradeoff/SKILL.md +3 -1
- package/dist/agent-src/skills/subagent-orchestration/SKILL.md +2 -1
- 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 +94 -24
- 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 +13 -10
- package/dist/discovery/trust-report.md +3 -3
- package/dist/discovery/workspaces.json +9 -6
- package/dist/mcp/registry-manifest.json +3 -3
- package/docs/CLAIMS.md +23 -2
- package/docs/announcements/2026-07-honest-cost-sweep-launch.md +84 -0
- package/docs/anti-reskin-gate.md +60 -0
- package/docs/architecture.md +2 -2
- package/docs/benchmark.md +40 -0
- package/docs/catalog.md +303 -278
- package/docs/command-flows.md +2 -1
- package/docs/comparison.yaml +10 -0
- package/docs/contracts/adoption-signal-floor.md +17 -0
- package/docs/contracts/ai-council-config.md +25 -0
- package/docs/contracts/mcp-tool-inventory.md +25 -22
- package/docs/contracts/tier-3-contrib-plugin.md +12 -4
- package/docs/featured-skills.md +2 -2
- 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/proof.md +11 -6
- package/docs/self-review-gate.md +14 -0
- package/docs/setup/per-ide/antigravity.md +8 -3
- package/docs/us-vs-the-category.md +38 -0
- package/docs/wedge/production-validator/README.md +24 -0
- package/docs/wedge/production-validator/first-run-check.sh +42 -0
- package/package.json +1 -1
- package/src/scripts/_lib/catalog_score.ts +64 -0
- package/src/scripts/_lib/shingle_similarity.ts +146 -0
- package/src/scripts/ai_council/clients.ts +50 -1
- package/src/scripts/ai_council/config.ts +28 -3
- package/src/scripts/ai_council/low_impact.ts +16 -0
- package/src/scripts/ai_council/necessity.ts +11 -0
- package/src/scripts/ai_council/orchestrator.ts +78 -26
- package/src/scripts/ai_council/pricing.ts +59 -0
- package/src/scripts/ai_council/stance_tally.ts +42 -5
- package/src/scripts/bench_defect_finding.ts +348 -0
- package/src/scripts/build_catalog_index.ts +188 -0
- package/src/scripts/council_cli.ts +56 -1
- package/src/scripts/generate_index.ts +6 -3
- package/src/scripts/lint_originality.ts +464 -0
- package/src/scripts/mcp_server/catalog_tools.ts +114 -0
- package/src/scripts/mcp_server/consumer_tool_catalog.json +46 -0
- package/src/scripts/schemas/flow.schema.json +17 -0
- package/src/scripts/schemas/skill.schema.json +166 -32
- package/src/scripts/self_review_gate.test.ts +48 -0
- package/src/scripts/self_review_gate.ts +73 -5
- package/src/scripts/validate_flow_teams.ts +127 -0
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* Defect-finding benchmark runner (road-to-team-mode Phase 5, Steps 2-4).
|
|
4
|
+
*
|
|
5
|
+
* Three arms over the pre-registered corpus (internal/bench/corpora/defect-finding.yaml):
|
|
6
|
+
* (a) self-review — single strong model (Anthropic) + the adversarial-review frame
|
|
7
|
+
* (b) team — codex exec (subscription; reviewer pinned via --codex-model)
|
|
8
|
+
* (c) council — the neutral breadth arm: Anthropic + OpenAI independent reviews, union
|
|
9
|
+
*
|
|
10
|
+
* Every arm gets the SAME strict output contract so scoring is fair + objective:
|
|
11
|
+
* a review must end with `VERDICT: <n>` and one `DEFECT: <file>:<line> — <why>`
|
|
12
|
+
* line per defect. Recall + false-positives are then scored DETERMINISTICALLY
|
|
13
|
+
* against the corpus ground truth (no LLM judge for the PRIMARY metric — the
|
|
14
|
+
* blind rubric judge is the SECONDARY metric and is deferred this pass).
|
|
15
|
+
*
|
|
16
|
+
* Spend: arm (a) + arm (c) are billable API calls; arm (b) is the ChatGPT
|
|
17
|
+
* subscription (non-billable through this process). `--max-usd` is a HARD cap:
|
|
18
|
+
* the run aborts before a call that would cross it. `--dry-run` makes zero calls.
|
|
19
|
+
*
|
|
20
|
+
* Exit 0 ok · 1 aborted-on-cap / error · 2 usage.
|
|
21
|
+
*/
|
|
22
|
+
import { execFileSync } from 'node:child_process';
|
|
23
|
+
import * as fs from 'node:fs';
|
|
24
|
+
import * as path from 'node:path';
|
|
25
|
+
import { fileURLToPath } from 'node:url';
|
|
26
|
+
|
|
27
|
+
import { load as yamlLoad } from 'js-yaml';
|
|
28
|
+
|
|
29
|
+
import {
|
|
30
|
+
AnthropicClient,
|
|
31
|
+
OpenAIClient,
|
|
32
|
+
load_anthropic_key,
|
|
33
|
+
load_openai_key,
|
|
34
|
+
} from './ai_council/clients.js';
|
|
35
|
+
|
|
36
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
37
|
+
const ROOT = path.resolve(HERE, '..', '..');
|
|
38
|
+
const CORPUS = path.join(ROOT, 'internal', 'bench', 'corpora', 'defect-finding.yaml');
|
|
39
|
+
const OUT_JSON = path.join(ROOT, 'internal', 'bench', 'reports', 'defect-finding.json');
|
|
40
|
+
const OUT_MD = path.join(ROOT, 'internal', 'bench', 'reports', 'defect-finding.md');
|
|
41
|
+
|
|
42
|
+
const OUTPUT_CONTRACT =
|
|
43
|
+
'\n\nReport STRICTLY in this format, nothing after it:\n' +
|
|
44
|
+
'DEFECT: <file>:<line> — <one-line why> (one line per real defect; omit if none)\n' +
|
|
45
|
+
'VERDICT: <integer count of defects>';
|
|
46
|
+
|
|
47
|
+
interface Fixture {
|
|
48
|
+
id: string;
|
|
49
|
+
defect_class: string;
|
|
50
|
+
prompt: string;
|
|
51
|
+
ground_truth: string;
|
|
52
|
+
rubric: string;
|
|
53
|
+
negative_control?: boolean;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
type ArmId = 'self-review' | 'team' | 'council';
|
|
57
|
+
|
|
58
|
+
interface ArmResult {
|
|
59
|
+
arm: ArmId;
|
|
60
|
+
text: string;
|
|
61
|
+
calls: number;
|
|
62
|
+
input_tokens: number;
|
|
63
|
+
output_tokens: number;
|
|
64
|
+
usd: number;
|
|
65
|
+
ms: number;
|
|
66
|
+
error: string | null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Ground-truth FILENAME for a fixture, or null for a control. Each planted
|
|
70
|
+
* fixture touches exactly ONE unique file, so "a DEFECT line names this file"
|
|
71
|
+
* is the honest, line-number-robust recall signal (diff line numbering is
|
|
72
|
+
* ambiguous and models cite lines inconsistently — matching the exact
|
|
73
|
+
* file:line token systematically under-counts real finds). */
|
|
74
|
+
function groundTruthFile(gt: string): string | null {
|
|
75
|
+
if (/^\s*NONE\b/i.test(gt)) return null;
|
|
76
|
+
const head = gt.split('—')[0] ?? gt;
|
|
77
|
+
const m = /([\w./-]+\.\w+):[\d,]+/.exec(head);
|
|
78
|
+
return m ? (m[1] as string) : null;
|
|
79
|
+
}
|
|
80
|
+
/** Basename of a path token (find.ts from src/search/find.ts). */
|
|
81
|
+
function basename(p: string): string { return p.split('/').pop() ?? p; }
|
|
82
|
+
|
|
83
|
+
/** Parse the DEFECT lines an arm emitted (file:line tokens it claims). */
|
|
84
|
+
function parseClaimedDefects(text: string): { count: number; files: string[] } {
|
|
85
|
+
const files: string[] = [];
|
|
86
|
+
for (const line of text.split('\n')) {
|
|
87
|
+
// DEFECT: <file>[:line] — ... ; capture the file token (line optional).
|
|
88
|
+
const m = /^\s*DEFECT:\s*([\w./-]+\.\w+)/.exec(line);
|
|
89
|
+
if (m) files.push(m[1] as string);
|
|
90
|
+
}
|
|
91
|
+
const vm = /VERDICT:\s*(\d+)/i.exec(text);
|
|
92
|
+
const count = vm ? Number(vm[1]) : files.length;
|
|
93
|
+
return { count, files };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Deterministic recall/FP for one arm on one fixture. */
|
|
97
|
+
function score(
|
|
98
|
+
fx: Fixture,
|
|
99
|
+
arm: ArmResult,
|
|
100
|
+
): { recall_hit: boolean; false_positive: boolean; claimed: number } {
|
|
101
|
+
const gtFile = groundTruthFile(fx.ground_truth);
|
|
102
|
+
const claimed = parseClaimedDefects(arm.text);
|
|
103
|
+
if (gtFile === null || fx.negative_control === true) {
|
|
104
|
+
// Control: any claimed defect is a false positive.
|
|
105
|
+
return { recall_hit: false, false_positive: claimed.count > 0, claimed: claimed.count };
|
|
106
|
+
}
|
|
107
|
+
// Recall: a DEFECT line names the planted file (by basename — robust to the
|
|
108
|
+
// model prefixing or omitting the dir). Line-agnostic on purpose.
|
|
109
|
+
const target = basename(gtFile);
|
|
110
|
+
const hit = claimed.files.some((f) => basename(f) === target) || arm.text.includes(target);
|
|
111
|
+
return { recall_hit: hit, false_positive: false, claimed: claimed.count };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function usd(inTok: number, outTok: number, inRate: number, outRate: number): number {
|
|
115
|
+
return (inTok / 1_000_000) * inRate + (outTok / 1_000_000) * outRate;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Conservative published-tier rates (per 1M tokens) for the estimate + tally.
|
|
119
|
+
const RATES = {
|
|
120
|
+
anthropic: { in: 3, out: 15 }, // claude-sonnet class
|
|
121
|
+
openai: { in: 2.5, out: 10 }, // gpt-4o class
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
interface Budget {
|
|
125
|
+
cap: number;
|
|
126
|
+
spent: number;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function guard(b: Budget, next: number): void {
|
|
130
|
+
if (b.cap > 0 && b.spent + next > b.cap) {
|
|
131
|
+
throw new Error(`hard cap $${b.cap} would be crossed (spent $${b.spent.toFixed(4)}, next ~$${next.toFixed(4)})`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function armSelfReview(fx: Fixture, key: string, b: Budget): ArmResult {
|
|
136
|
+
const t0 = Date.now();
|
|
137
|
+
guard(b, 0.05);
|
|
138
|
+
const client = new AnthropicClient({ api_key: key });
|
|
139
|
+
const sys =
|
|
140
|
+
'You are a single-model adversarial code reviewer (Attack-Defend-Revise). ' +
|
|
141
|
+
'Find real defects in the diff; do not invent problems in correct code.';
|
|
142
|
+
const r = client.ask(sys, fx.prompt + OUTPUT_CONTRACT, 1024);
|
|
143
|
+
const cost = usd(r.input_tokens, r.output_tokens, RATES.anthropic.in, RATES.anthropic.out);
|
|
144
|
+
b.spent += cost;
|
|
145
|
+
return {
|
|
146
|
+
arm: 'self-review', text: r.text, calls: 1,
|
|
147
|
+
input_tokens: r.input_tokens, output_tokens: r.output_tokens,
|
|
148
|
+
usd: cost, ms: Date.now() - t0, error: r.error,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function armTeam(fx: Fixture, model: string, _b: Budget): ArmResult {
|
|
153
|
+
const t0 = Date.now();
|
|
154
|
+
// codex = ChatGPT subscription: non-billable through this process. `--json`
|
|
155
|
+
// streams events; the review is the LAST agent_message item's text (the
|
|
156
|
+
// interactive mode without --json blocks/hangs — must use --json here).
|
|
157
|
+
let text = '';
|
|
158
|
+
let error: string | null = null;
|
|
159
|
+
try {
|
|
160
|
+
const raw = execFileSync('codex', ['exec', '--json', '-m', model, fx.prompt + OUTPUT_CONTRACT], {
|
|
161
|
+
encoding: 'utf8', maxBuffer: 16 * 1024 * 1024, timeout: 180_000,
|
|
162
|
+
});
|
|
163
|
+
for (const line of raw.split('\n')) {
|
|
164
|
+
const s = line.trim();
|
|
165
|
+
if (!s.startsWith('{')) continue;
|
|
166
|
+
try {
|
|
167
|
+
const ev = JSON.parse(s) as { item?: { type?: string; text?: string } };
|
|
168
|
+
if (ev.item?.type === 'agent_message' && typeof ev.item.text === 'string') {
|
|
169
|
+
text = ev.item.text; // keep the last agent_message
|
|
170
|
+
}
|
|
171
|
+
} catch { /* skip non-JSON / partial lines */ }
|
|
172
|
+
}
|
|
173
|
+
if (!text) error = 'no agent_message in codex --json stream';
|
|
174
|
+
} catch (e) {
|
|
175
|
+
error = e instanceof Error ? e.message.slice(0, 300) : String(e);
|
|
176
|
+
}
|
|
177
|
+
return { arm: 'team', text, calls: 1, input_tokens: 0, output_tokens: 0, usd: 0, ms: Date.now() - t0, error };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function armCouncil(fx: Fixture, aKey: string, oKey: string, b: Budget): ArmResult {
|
|
181
|
+
const t0 = Date.now();
|
|
182
|
+
const sys =
|
|
183
|
+
'You are a neutral code reviewer. Review ONLY the supplied diff for real ' +
|
|
184
|
+
'defects; never invent problems in correct code.';
|
|
185
|
+
guard(b, 0.05);
|
|
186
|
+
const a = new AnthropicClient({ api_key: aKey }).ask(sys, fx.prompt + OUTPUT_CONTRACT, 1024);
|
|
187
|
+
let cost = usd(a.input_tokens, a.output_tokens, RATES.anthropic.in, RATES.anthropic.out);
|
|
188
|
+
b.spent += cost;
|
|
189
|
+
guard(b, 0.05);
|
|
190
|
+
const o = new OpenAIClient({ api_key: oKey }).ask(sys, fx.prompt + OUTPUT_CONTRACT, 1024);
|
|
191
|
+
const oc = usd(o.input_tokens, o.output_tokens, RATES.openai.in, RATES.openai.out);
|
|
192
|
+
b.spent += oc;
|
|
193
|
+
cost += oc;
|
|
194
|
+
// Council = breadth: union the two independent reviews.
|
|
195
|
+
const text = `# reviewer A\n${a.text}\n\n# reviewer B\n${o.text}`;
|
|
196
|
+
return {
|
|
197
|
+
arm: 'council', text, calls: 2,
|
|
198
|
+
input_tokens: a.input_tokens + o.input_tokens,
|
|
199
|
+
output_tokens: a.output_tokens + o.output_tokens,
|
|
200
|
+
usd: cost, ms: Date.now() - t0, error: a.error ?? o.error,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const CORRECTNESS = new Set(['logic-bug', 'off-by-one', 'race']);
|
|
205
|
+
const DESIGN = new Set(['missing-empty-state', 'security-smell']);
|
|
206
|
+
|
|
207
|
+
export function main(argv: string[]): 0 | 1 | 2 {
|
|
208
|
+
const dryRun = argv.includes('--dry-run');
|
|
209
|
+
const capIdx = argv.indexOf('--max-usd');
|
|
210
|
+
const cap = capIdx >= 0 ? Number(argv[capIdx + 1]) : 1.2;
|
|
211
|
+
const modelIdx = argv.indexOf('--codex-model');
|
|
212
|
+
const codexModel = (modelIdx >= 0 ? argv[modelIdx + 1] : undefined) ?? 'gpt-5.5';
|
|
213
|
+
|
|
214
|
+
const doc = yamlLoad(fs.readFileSync(CORPUS, 'utf8')) as { fixtures: Fixture[] };
|
|
215
|
+
const fixtures = doc.fixtures;
|
|
216
|
+
|
|
217
|
+
if (dryRun) {
|
|
218
|
+
const planted = fixtures.filter((f) => !f.negative_control).length;
|
|
219
|
+
process.stdout.write(
|
|
220
|
+
`defect-finding (dry-run — no calls):\n` +
|
|
221
|
+
` fixtures: ${fixtures.length} (${planted} planted, ${fixtures.length - planted} control)\n` +
|
|
222
|
+
` arms: self-review (anthropic, ${fixtures.length} calls) · team (codex ${codexModel}, ${fixtures.length} calls, subscription) · council (anthropic+openai, ${fixtures.length * 2} calls)\n` +
|
|
223
|
+
` billable estimate: arm a ~$${(fixtures.length * 0.02).toFixed(2)} + arm c ~$${(fixtures.length * 0.04).toFixed(2)} ≈ $${(fixtures.length * 0.06).toFixed(2)} (hard cap $${cap})\n` +
|
|
224
|
+
` arm b (codex): 0 billable (ChatGPT subscription quota)\n`,
|
|
225
|
+
);
|
|
226
|
+
return 0;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const aKey = load_anthropic_key();
|
|
230
|
+
const oKey = load_openai_key();
|
|
231
|
+
const budget: Budget = { cap, spent: 0 };
|
|
232
|
+
const rows: Array<{ fx: Fixture; arms: Record<ArmId, ArmResult> }> = [];
|
|
233
|
+
|
|
234
|
+
try {
|
|
235
|
+
for (const fx of fixtures) {
|
|
236
|
+
process.stdout.write(`· ${fx.id} (${fx.defect_class})\n`);
|
|
237
|
+
const arms = {
|
|
238
|
+
'self-review': armSelfReview(fx, aKey, budget),
|
|
239
|
+
team: armTeam(fx, codexModel, budget),
|
|
240
|
+
council: armCouncil(fx, aKey, oKey, budget),
|
|
241
|
+
} as Record<ArmId, ArmResult>;
|
|
242
|
+
rows.push({ fx, arms });
|
|
243
|
+
}
|
|
244
|
+
} catch (e) {
|
|
245
|
+
process.stderr.write(`ABORTED: ${e instanceof Error ? e.message : String(e)}\n`);
|
|
246
|
+
// fall through to write a partial report
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// ── aggregate ──
|
|
250
|
+
const ARMS: ArmId[] = ['self-review', 'team', 'council'];
|
|
251
|
+
const agg: Record<string, unknown> = {};
|
|
252
|
+
const perArm: Record<ArmId, { recall: number; planted: number; fp: number; usd: number; ms: number; calls: number; byClass: Record<string, { hit: number; n: number }> }> =
|
|
253
|
+
Object.fromEntries(ARMS.map((a) => [a, { recall: 0, planted: 0, fp: 0, usd: 0, ms: 0, calls: 0, byClass: {} }])) as never;
|
|
254
|
+
|
|
255
|
+
const detail: unknown[] = [];
|
|
256
|
+
for (const { fx, arms } of rows) {
|
|
257
|
+
const d: Record<string, unknown> = { id: fx.id, defect_class: fx.defect_class, control: !!fx.negative_control };
|
|
258
|
+
for (const a of ARMS) {
|
|
259
|
+
const ar = arms[a];
|
|
260
|
+
const sc = score(fx, ar);
|
|
261
|
+
const pa = perArm[a];
|
|
262
|
+
pa.usd += ar.usd; pa.ms += ar.ms; pa.calls += ar.calls;
|
|
263
|
+
if (!fx.negative_control) {
|
|
264
|
+
pa.planted += 1;
|
|
265
|
+
if (sc.recall_hit) pa.recall += 1;
|
|
266
|
+
const c = (pa.byClass[fx.defect_class] ??= { hit: 0, n: 0 });
|
|
267
|
+
c.n += 1; if (sc.recall_hit) c.hit += 1;
|
|
268
|
+
} else if (sc.false_positive) {
|
|
269
|
+
pa.fp += 1;
|
|
270
|
+
}
|
|
271
|
+
d[a] = { recall_hit: sc.recall_hit, false_positive: sc.false_positive, claimed: sc.claimed, usd: Number(ar.usd.toFixed(4)), ms: ar.ms, error: ar.error, review: ar.text.slice(0, 1200) };
|
|
272
|
+
}
|
|
273
|
+
detail.push(d);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const recallRate = (a: ArmId): number => (perArm[a].planted ? perArm[a].recall / perArm[a].planted : 0);
|
|
277
|
+
const classRecall = (a: ArmId, classes: Set<string>): { hit: number; n: number } => {
|
|
278
|
+
let hit = 0, n = 0;
|
|
279
|
+
for (const [cls, c] of Object.entries(perArm[a].byClass)) if (classes.has(cls)) { hit += c.hit; n += c.n; }
|
|
280
|
+
return { hit, n };
|
|
281
|
+
};
|
|
282
|
+
const rr = (x: { hit: number; n: number }): number => (x.n ? x.hit / x.n : 0);
|
|
283
|
+
|
|
284
|
+
// Hypotheses (pre-registered thresholds).
|
|
285
|
+
const aCorr = classRecall('self-review', CORRECTNESS), bCorr = classRecall('team', CORRECTNESS);
|
|
286
|
+
const bDes = classRecall('team', DESIGN), cDes = classRecall('council', DESIGN);
|
|
287
|
+
const h1 = rr(bCorr) - rr(aCorr); // team - self on correctness; predict >= +0.20
|
|
288
|
+
const h2 = Math.abs(rr(cDes) - rr(bDes)); // council vs team on design; predict within 0.10
|
|
289
|
+
const h3ok = ARMS.every((a) => perArm[a].fp <= 1);
|
|
290
|
+
|
|
291
|
+
agg['preregistered'] = {
|
|
292
|
+
roadmap: 'road-to-team-mode Phase 5', authored: '2026-07-20',
|
|
293
|
+
primary: ['recall', 'false_positives', 'cost/time/calls'],
|
|
294
|
+
secondary: 'blind rubric judge — DEFERRED this pass (primary recall is deterministic)',
|
|
295
|
+
hypotheses: { H1: 'team>self on correctness, Δrecall>=+0.20', H2: 'council≈team on design, within 0.10', H3: 'FP<=1/arm on controls' },
|
|
296
|
+
codex_model: codexModel, models_cache_fetched: '2026-07-20T12:21Z',
|
|
297
|
+
};
|
|
298
|
+
agg['arms'] = Object.fromEntries(ARMS.map((a) => [a, {
|
|
299
|
+
recall_rate: Number(recallRate(a).toFixed(3)), recall: perArm[a].recall, planted: perArm[a].planted,
|
|
300
|
+
false_positives: perArm[a].fp, usd: Number(perArm[a].usd.toFixed(4)), ms: perArm[a].ms, calls: perArm[a].calls,
|
|
301
|
+
correctness_recall: Number(rr(classRecall(a, CORRECTNESS)).toFixed(3)),
|
|
302
|
+
design_recall: Number(rr(classRecall(a, DESIGN)).toFixed(3)),
|
|
303
|
+
}]));
|
|
304
|
+
const nullish = Math.abs(h1) <= 0.1 && h2 <= 0.1;
|
|
305
|
+
agg['verdict'] = {
|
|
306
|
+
H1_team_minus_self_correctness: Number(h1.toFixed(3)), H1_met: h1 >= 0.2,
|
|
307
|
+
H2_council_vs_team_design_absdelta: Number(h2.toFixed(3)), H2_met: h2 <= 0.1,
|
|
308
|
+
H3_fp_within_bound: h3ok,
|
|
309
|
+
honest_null: nullish,
|
|
310
|
+
disposition: nullish
|
|
311
|
+
? 'HONEST NULL — arms indistinguishable within pre-registered thresholds; no lift claim binds.'
|
|
312
|
+
: h1 >= 0.2
|
|
313
|
+
? 'LIFT on correctness-class — H1 met; a team-lift CLAIM may bind (maintainer decision).'
|
|
314
|
+
: 'MIXED — see per-class rates; no blanket lift claim.',
|
|
315
|
+
};
|
|
316
|
+
agg['total_billable_usd'] = Number((perArm['self-review'].usd + perArm.council.usd).toFixed(4));
|
|
317
|
+
agg['detail'] = detail;
|
|
318
|
+
|
|
319
|
+
fs.mkdirSync(path.dirname(OUT_JSON), { recursive: true });
|
|
320
|
+
fs.writeFileSync(OUT_JSON, JSON.stringify(agg, null, 2) + '\n');
|
|
321
|
+
|
|
322
|
+
const v = agg['verdict'] as Record<string, unknown>;
|
|
323
|
+
const md = [
|
|
324
|
+
'# Defect-finding benchmark — results (road-to-team-mode Phase 5)',
|
|
325
|
+
'', `> Run 2026-07-20. Codex reviewer: \`${codexModel}\`. Primary metric = deterministic`,
|
|
326
|
+
'> recall against corpus ground truth; blind rubric judge (secondary) deferred.',
|
|
327
|
+
'', '## Per-arm', '', '| arm | recall | correctness | design | false-pos | calls | $ |', '|---|--:|--:|--:|--:|--:|--:|',
|
|
328
|
+
...ARMS.map((a) => {
|
|
329
|
+
const x = (agg['arms'] as Record<string, Record<string, number>>)[a] as Record<string, number>;
|
|
330
|
+
return `| ${a} | ${x.recall_rate} | ${x.correctness_recall} | ${x.design_recall} | ${x.false_positives} | ${x.calls} | ${x.usd} |`;
|
|
331
|
+
}),
|
|
332
|
+
'', '## Verdict', '',
|
|
333
|
+
`- H1 (team − self, correctness recall Δ): **${v.H1_team_minus_self_correctness}** — met: ${v.H1_met}`,
|
|
334
|
+
`- H2 (council vs team, design recall |Δ|): **${v.H2_council_vs_team_design_absdelta}** — met: ${v.H2_met}`,
|
|
335
|
+
`- H3 (false positives ≤ 1/arm): ${v.H3_fp_within_bound}`,
|
|
336
|
+
'', `**Disposition:** ${v.disposition}`,
|
|
337
|
+
'', `Total billable: $${agg['total_billable_usd']} (arm b codex = subscription, $0 billable).`,
|
|
338
|
+
'',
|
|
339
|
+
].join('\n');
|
|
340
|
+
fs.writeFileSync(OUT_MD, md);
|
|
341
|
+
process.stdout.write(`\nwrote ${path.relative(ROOT, OUT_JSON)} + ${path.relative(ROOT, OUT_MD)}\n`);
|
|
342
|
+
process.stdout.write(`spent ~$${budget.spent.toFixed(4)} (cap $${cap})\n`);
|
|
343
|
+
return 0;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (fs.existsSync(process.argv[1] ?? '') && import.meta.url === `file://${process.argv[1]}`) {
|
|
347
|
+
process.exit(main(process.argv.slice(2)));
|
|
348
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* Build the lazy-catalog index — `dist/catalog-index-v1.json`.
|
|
4
|
+
*
|
|
5
|
+
* A queryable metadata index over every skill, persona, and domain command:
|
|
6
|
+
* `{ id, cls, name, description, tags, path }` — **metadata only, no bodies**.
|
|
7
|
+
* Bodies load lazily from disk at `catalog_load` time (the stdio server has the
|
|
8
|
+
* repo underneath it, unlike the upstream Hermes plugin which embeds bodies).
|
|
9
|
+
*
|
|
10
|
+
* Feeds the `catalog_search / _inspect / _load` MCP tools (built as discovery
|
|
11
|
+
* stubs; activation deferred behind a quality A/B — see
|
|
12
|
+
* road-to-persona-library-harvest.md Phase 2). Deterministic output (entries
|
|
13
|
+
* sorted by (cls, id), stable keys) so the artifact diffs cleanly. `--check`
|
|
14
|
+
* fails on drift instead of writing (CI mode), mirroring the
|
|
15
|
+
* build_mcp_registry_manifest convention.
|
|
16
|
+
*/
|
|
17
|
+
import * as fs from 'node:fs';
|
|
18
|
+
import * as path from 'node:path';
|
|
19
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
20
|
+
|
|
21
|
+
import { parse as parseYaml } from 'yaml';
|
|
22
|
+
|
|
23
|
+
const _HERE = fileURLToPath(import.meta.url);
|
|
24
|
+
export const ROOT = path.resolve(path.dirname(_HERE), '..', '..');
|
|
25
|
+
// The `-v1` namespace tracks `schema_version` below (per lint_versioned_cache):
|
|
26
|
+
// bump both together on a shape change so any stale cached copy self-invalidates
|
|
27
|
+
// by name rather than being read as the new shape.
|
|
28
|
+
export const OUT = path.join(ROOT, 'dist', 'catalog-index-v1.json');
|
|
29
|
+
|
|
30
|
+
export type CatalogClass = 'skill' | 'persona' | 'command';
|
|
31
|
+
|
|
32
|
+
export interface CatalogIndexEntry {
|
|
33
|
+
/** Globally unique handle: `<cls>:<slug>`. */
|
|
34
|
+
id: string;
|
|
35
|
+
cls: CatalogClass;
|
|
36
|
+
name: string;
|
|
37
|
+
description: string;
|
|
38
|
+
/** Domain / packs / tier — for filtering and search-text weighting. */
|
|
39
|
+
tags: string[];
|
|
40
|
+
/** Repo-relative path to the body (resolved by catalog_load). */
|
|
41
|
+
path: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const SKILLS_DIR = path.join(ROOT, 'src', 'skills');
|
|
45
|
+
const PERSONAS_DIR = path.join(ROOT, 'src', 'agent-src', 'personas');
|
|
46
|
+
const DOMAINS_DIR = path.join(ROOT, 'src', 'domains');
|
|
47
|
+
|
|
48
|
+
function _isDir(p: string): boolean {
|
|
49
|
+
try { return fs.statSync(p).isDirectory(); } catch { return false; }
|
|
50
|
+
}
|
|
51
|
+
function _isFile(p: string): boolean {
|
|
52
|
+
try { return fs.statSync(p).isFile(); } catch { return false; }
|
|
53
|
+
}
|
|
54
|
+
function _rel(abs: string): string {
|
|
55
|
+
return path.relative(ROOT, abs).split(path.sep).join('/');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function _frontmatter(abs: string): Record<string, unknown> {
|
|
59
|
+
const text = fs.readFileSync(abs, 'utf-8');
|
|
60
|
+
const m = /^---\n([\s\S]*?)\n---/.exec(text);
|
|
61
|
+
if (!m || m.index !== 0) return {};
|
|
62
|
+
try {
|
|
63
|
+
const fm = parseYaml(m[1] as string, { version: '1.1' });
|
|
64
|
+
return fm !== null && typeof fm === 'object' && !Array.isArray(fm) ? (fm as Record<string, unknown>) : {};
|
|
65
|
+
} catch {
|
|
66
|
+
return {};
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function _str(v: unknown): string {
|
|
71
|
+
return typeof v === 'string' ? v.trim() : v == null ? '' : String(v);
|
|
72
|
+
}
|
|
73
|
+
function _list(v: unknown): string[] {
|
|
74
|
+
if (Array.isArray(v)) return v.map((x) => _str(x)).filter((s) => s.length > 0);
|
|
75
|
+
const s = _str(v);
|
|
76
|
+
return s ? [s] : [];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function _walkLeaf(dir: string, leaf: string, out: string[]): void {
|
|
80
|
+
let entries: fs.Dirent[];
|
|
81
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
|
|
82
|
+
for (const ent of entries) {
|
|
83
|
+
const full = path.join(dir, ent.name);
|
|
84
|
+
if (ent.isDirectory() || (ent.isSymbolicLink() && _isDir(full))) _walkLeaf(full, leaf, out);
|
|
85
|
+
else if (ent.name === leaf) out.push(full);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function buildEntries(): CatalogIndexEntry[] {
|
|
90
|
+
const out: CatalogIndexEntry[] = [];
|
|
91
|
+
|
|
92
|
+
// Skills
|
|
93
|
+
if (_isDir(SKILLS_DIR)) {
|
|
94
|
+
for (const slug of fs.readdirSync(SKILLS_DIR).sort()) {
|
|
95
|
+
const abs = path.join(SKILLS_DIR, slug, 'SKILL.md');
|
|
96
|
+
if (!_isFile(abs)) continue;
|
|
97
|
+
const fm = _frontmatter(abs);
|
|
98
|
+
out.push({
|
|
99
|
+
id: `skill:${slug}`,
|
|
100
|
+
cls: 'skill',
|
|
101
|
+
name: _str(fm['name']) || slug,
|
|
102
|
+
description: _str(fm['description']),
|
|
103
|
+
tags: [_str(fm['domain']), ..._list(fm['packs'])].filter((s) => s.length > 0),
|
|
104
|
+
path: _rel(abs),
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Personas (top-level *.md, skip _template/advisors and underscore-prefixed)
|
|
110
|
+
if (_isDir(PERSONAS_DIR)) {
|
|
111
|
+
for (const name of fs.readdirSync(PERSONAS_DIR).sort()) {
|
|
112
|
+
if (!name.endsWith('.md') || name.startsWith('_')) continue;
|
|
113
|
+
const abs = path.join(PERSONAS_DIR, name);
|
|
114
|
+
if (!_isFile(abs)) continue;
|
|
115
|
+
const fm = _frontmatter(abs);
|
|
116
|
+
const id = _str(fm['id']) || name.replace(/\.md$/, '');
|
|
117
|
+
out.push({
|
|
118
|
+
id: `persona:${id}`,
|
|
119
|
+
cls: 'persona',
|
|
120
|
+
name: _str(fm['role']) || id,
|
|
121
|
+
description: _str(fm['description']),
|
|
122
|
+
tags: [_str(fm['tier']), _str(fm['mode'])].filter((s) => s.length > 0),
|
|
123
|
+
path: _rel(abs),
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Domain commands
|
|
129
|
+
const cmds: string[] = [];
|
|
130
|
+
_walkLeaf(DOMAINS_DIR, 'command.md', cmds);
|
|
131
|
+
for (const abs of cmds.sort()) {
|
|
132
|
+
const fm = _frontmatter(abs);
|
|
133
|
+
// logical id = <pack>/<subpath> under src/domains, without /command.md
|
|
134
|
+
const relFromDomains = path.relative(DOMAINS_DIR, path.dirname(abs)).split(path.sep).join('/');
|
|
135
|
+
const name = _str(fm['name']) || relFromDomains.split('/').pop() || relFromDomains;
|
|
136
|
+
out.push({
|
|
137
|
+
id: `command:${relFromDomains}`,
|
|
138
|
+
cls: 'command',
|
|
139
|
+
name,
|
|
140
|
+
description: _str(fm['description']) || _str(fm['intent']),
|
|
141
|
+
tags: [_str(fm['pack']), _str(fm['tier']), _str(fm['visibility'])].filter((s) => s.length > 0),
|
|
142
|
+
path: _rel(abs),
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Deterministic order: (cls, id).
|
|
147
|
+
out.sort((a, b) => (a.cls < b.cls ? -1 : a.cls > b.cls ? 1 : a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
148
|
+
return out;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function serialize(entries: CatalogIndexEntry[]): string {
|
|
152
|
+
return JSON.stringify({ schema_version: 1, count: entries.length, entries }, null, 2) + '\n';
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export function main(argv?: string[]): number {
|
|
156
|
+
const args = argv ?? process.argv.slice(2);
|
|
157
|
+
const check = args.includes('--check');
|
|
158
|
+
const quiet = args.includes('--quiet');
|
|
159
|
+
const entries = buildEntries();
|
|
160
|
+
const rendered = serialize(entries);
|
|
161
|
+
|
|
162
|
+
if (check) {
|
|
163
|
+
const current = _isFile(OUT) ? fs.readFileSync(OUT, 'utf-8') : '';
|
|
164
|
+
if (current !== rendered) {
|
|
165
|
+
process.stderr.write(`build_catalog_index: DRIFT — ${_rel(OUT)} is stale; run build_catalog_index to regenerate.\n`);
|
|
166
|
+
return 1;
|
|
167
|
+
}
|
|
168
|
+
if (!quiet) process.stdout.write(`build_catalog_index: OK — ${entries.length} entries, index up to date.\n`);
|
|
169
|
+
return 0;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
fs.mkdirSync(path.dirname(OUT), { recursive: true });
|
|
173
|
+
fs.writeFileSync(OUT, rendered, 'utf-8');
|
|
174
|
+
if (!quiet) process.stdout.write(`build_catalog_index: wrote ${entries.length} entries → ${_rel(OUT)}\n`);
|
|
175
|
+
return 0;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function _isCliEntry(): boolean {
|
|
179
|
+
if (process.argv[1] === undefined) return false;
|
|
180
|
+
if (import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) return true;
|
|
181
|
+
try {
|
|
182
|
+
return fs.realpathSync(fileURLToPath(import.meta.url)) === fs.realpathSync(path.resolve(process.argv[1]));
|
|
183
|
+
} catch { return false; }
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (_isCliEntry()) {
|
|
187
|
+
process.exit(main());
|
|
188
|
+
}
|
|
@@ -98,7 +98,9 @@ import {
|
|
|
98
98
|
} from './ai_council/orchestrator.js';
|
|
99
99
|
import {
|
|
100
100
|
type PriceTable,
|
|
101
|
+
downgrade_coupling,
|
|
101
102
|
estimate_cost,
|
|
103
|
+
estimate_input_tokens,
|
|
102
104
|
load_prices,
|
|
103
105
|
prices_file_for,
|
|
104
106
|
} from './ai_council/pricing.js';
|
|
@@ -1605,7 +1607,8 @@ function _necessity_gate(opts: {
|
|
|
1605
1607
|
function _resolve_model_downgrade(ai_cfg: Dict, lens: string): [boolean, boolean] {
|
|
1606
1608
|
const md_block = (ai_cfg['model_downgrade'] as Dict) || {};
|
|
1607
1609
|
let enabled = md_block['enabled'] === undefined ? true : Boolean(_pyBool(md_block['enabled']));
|
|
1608
|
-
|
|
1610
|
+
// A3: auto-downgrade is the default; `auto_apply: false` is the opt-out.
|
|
1611
|
+
let auto_apply = md_block['auto_apply'] === undefined ? true : Boolean(_pyBool(md_block['auto_apply']));
|
|
1609
1612
|
const overrides = ((ai_cfg['lens_overrides'] as Dict) || {})['model_downgrade'] || {};
|
|
1610
1613
|
const lens_override = _isDict(overrides) ? (overrides as Dict)[lens] : null;
|
|
1611
1614
|
if (_isDict(lens_override)) {
|
|
@@ -1622,6 +1625,10 @@ function _size_fit_gate(opts: {
|
|
|
1622
1625
|
members: ExternalAIClient[];
|
|
1623
1626
|
ai_cfg: Dict;
|
|
1624
1627
|
stdout?: ((s: string) => void) | null;
|
|
1628
|
+
/** Planned rounds for the run; expected same-model cache reads = rounds − 1. */
|
|
1629
|
+
rounds?: number;
|
|
1630
|
+
/** Price table for the A1↔A3 cache-coupling gate; null → coupling skipped. */
|
|
1631
|
+
price_table?: PriceTable | null;
|
|
1625
1632
|
}): Array<[string, SizeFitVerdict, boolean]> {
|
|
1626
1633
|
const out = opts.stdout ?? _stdout;
|
|
1627
1634
|
const [enabled, auto_apply] = _resolve_model_downgrade(opts.ai_cfg, opts.lens);
|
|
@@ -1629,8 +1636,25 @@ function _size_fit_gate(opts: {
|
|
|
1629
1636
|
if (!enabled) {
|
|
1630
1637
|
return decisions;
|
|
1631
1638
|
}
|
|
1639
|
+
const md_block = (opts.ai_cfg['model_downgrade'] as Dict) || {};
|
|
1640
|
+
const tier_override = (md_block['model_tier_override'] as Dict) || {};
|
|
1641
|
+
const cache_enabled = opts.ai_cfg['prompt_cache'] !== false;
|
|
1642
|
+
const expected_reads = Math.max(0, (opts.rounds ?? 1) - 1);
|
|
1632
1643
|
const members_cfg = (opts.ai_cfg['members'] as Dict) || {};
|
|
1633
1644
|
for (const member of opts.members) {
|
|
1645
|
+
// Per-run escape hatch: a member pinned in model_tier_override gets
|
|
1646
|
+
// exactly that model — classifier and coupling both skipped.
|
|
1647
|
+
const pinned = tier_override[member.name];
|
|
1648
|
+
if (typeof pinned === 'string' && pinned.length > 0) {
|
|
1649
|
+
if (pinned !== member.model) {
|
|
1650
|
+
out(
|
|
1651
|
+
`council:size-fit · ${member.name} · model_tier_override ` +
|
|
1652
|
+
`\`${member.model}\` → \`${pinned}\` (operator pin)\n`,
|
|
1653
|
+
);
|
|
1654
|
+
member.model = pinned;
|
|
1655
|
+
}
|
|
1656
|
+
continue;
|
|
1657
|
+
}
|
|
1634
1658
|
const member_cfg = (members_cfg[member.name] as Dict) || {};
|
|
1635
1659
|
const ladder = (member_cfg['model_ladder'] as string[]) || [];
|
|
1636
1660
|
if (ladder.length === 0) {
|
|
@@ -1639,6 +1663,35 @@ function _size_fit_gate(opts: {
|
|
|
1639
1663
|
const verdict = classify_size_fit(opts.prompt, member.model, ladder, opts.lens);
|
|
1640
1664
|
let applied = false;
|
|
1641
1665
|
if (!verdict.fit && verdict.suggested_model) {
|
|
1666
|
+
// A1↔A3 coupling: a downgraded member misses the model-scoped
|
|
1667
|
+
// prompt cache — downgrade only when the model saving beats the
|
|
1668
|
+
// forfeited cache reads. No price table → conservative skip is
|
|
1669
|
+
// wrong for the cost goal, so coupling only gates when computable.
|
|
1670
|
+
if (opts.price_table) {
|
|
1671
|
+
const prefix_tokens = estimate_input_tokens(opts.prompt);
|
|
1672
|
+
const coupling = downgrade_coupling(
|
|
1673
|
+
member.name,
|
|
1674
|
+
member.model,
|
|
1675
|
+
verdict.suggested_model,
|
|
1676
|
+
prefix_tokens,
|
|
1677
|
+
4096,
|
|
1678
|
+
{
|
|
1679
|
+
enabled: cache_enabled,
|
|
1680
|
+
cacheable_prefix_tokens: prefix_tokens,
|
|
1681
|
+
expected_reads,
|
|
1682
|
+
},
|
|
1683
|
+
opts.price_table,
|
|
1684
|
+
);
|
|
1685
|
+
if (!coupling.net_positive) {
|
|
1686
|
+
out(
|
|
1687
|
+
`council:size-fit · ${member.name} · downgrade skipped — ` +
|
|
1688
|
+
`lost cache savings $${coupling.lost_cache_savings_usd.toFixed(4)} ` +
|
|
1689
|
+
`≥ model savings $${coupling.downgrade_savings_usd.toFixed(4)}\n`,
|
|
1690
|
+
);
|
|
1691
|
+
decisions.push([member.name, verdict, false]);
|
|
1692
|
+
continue;
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1642
1695
|
if (auto_apply) {
|
|
1643
1696
|
out(
|
|
1644
1697
|
`council:size-fit · ${member.name} · auto-downgrade ` +
|
|
@@ -1845,6 +1898,8 @@ function cmd_run(
|
|
|
1845
1898
|
lens: question.mode,
|
|
1846
1899
|
members,
|
|
1847
1900
|
ai_cfg,
|
|
1901
|
+
rounds: _getattr(args, 'rounds', 1) as number,
|
|
1902
|
+
price_table: table,
|
|
1848
1903
|
});
|
|
1849
1904
|
const project = detect_project_context(REPO_ROOT);
|
|
1850
1905
|
const billable = members.filter((m) => _getattr(m, 'billable', true));
|
|
@@ -232,7 +232,10 @@ export function _collect_skills(): Entry[] {
|
|
|
232
232
|
kind: 'skill',
|
|
233
233
|
name,
|
|
234
234
|
description: _truncate(fm['description'] ?? ''),
|
|
235
|
-
|
|
235
|
+
// Provenance: `official` unless the skill declares
|
|
236
|
+
// `provenance: community` (external contribution — credited in
|
|
237
|
+
// CREDITS.md; the catalog column makes the credit visible).
|
|
238
|
+
extra: fm['provenance'] ?? 'official',
|
|
236
239
|
path: _relativeToPosix(skillMd, ROOT),
|
|
237
240
|
});
|
|
238
241
|
}
|
|
@@ -372,7 +375,7 @@ export function _render_index(
|
|
|
372
375
|
'',
|
|
373
376
|
`## Skills (${skills.length})`,
|
|
374
377
|
'',
|
|
375
|
-
_render_table(skills, ['kind', 'name', '
|
|
378
|
+
_render_table(skills, ['kind', 'name', 'source', 'description'], '../'),
|
|
376
379
|
'',
|
|
377
380
|
`## Rules (${rules.length})`,
|
|
378
381
|
'',
|
|
@@ -428,7 +431,7 @@ export function _render_catalog(
|
|
|
428
431
|
'> (`./scripts-run src/scripts/domain_soundness_status`). A disclaimer',
|
|
429
432
|
'> floor bounds liability, not correctness — see [the proof page](proof.md).',
|
|
430
433
|
'',
|
|
431
|
-
_render_table(skills, ['kind', 'name', '
|
|
434
|
+
_render_table(skills, ['kind', 'name', 'source', 'description'], '../', _to_shipped_path),
|
|
432
435
|
'',
|
|
433
436
|
`## Rules (${publicRules.length})`,
|
|
434
437
|
'',
|