@event4u/agent-config 9.2.0 → 9.4.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 +111 -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/fix/route.md +49 -0
- package/dist/agent-src/commands/fix.md +4 -2
- package/dist/agent-src/commands/tdd/green.md +32 -0
- package/dist/agent-src/commands/tdd/red.md +33 -0
- package/dist/agent-src/commands/tdd/refactor.md +31 -0
- package/dist/agent-src/commands/tdd.md +50 -0
- package/dist/agent-src/commands/team.md +4 -0
- package/dist/agent-src/contexts/communication/rules-auto/guidelines-mechanics.md +1 -0
- package/dist/agent-src/contexts/execution/subagent-spawn-contract.md +81 -2
- package/dist/agent-src/contexts/model-recommendations.md +6 -0
- package/dist/agent-src/rules/architecture.md +7 -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/fe-design/SKILL.md +1 -0
- package/dist/agent-src/skills/judge-code-quality/SKILL.md +1 -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/persona-improvement/SKILL.md +81 -0
- package/dist/agent-src/skills/react-shadcn-ui/SKILL.md +10 -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 +14 -24
- 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 +253 -30
- 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 +19 -10
- package/dist/discovery/trust-report.md +3 -3
- package/dist/discovery/workspaces.json +15 -6
- package/dist/mcp/registry-manifest.json +3 -3
- package/docs/CLAIMS.md +16 -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 +3 -3
- package/docs/catalog.md +303 -278
- package/docs/command-flows.md +10 -3
- 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/command-clusters.md +2 -1
- package/docs/contracts/mcp-tool-inventory.md +25 -22
- package/docs/contracts/tier-3-contrib-plugin.md +12 -4
- package/docs/decisions/ADR-122-adversarial-verification-council.md +1 -1
- package/docs/development.md +10 -1
- 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/guidelines/component-oriented-and-oop-development.md +181 -0
- package/docs/proof.md +10 -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 +3 -2
- 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/prompts.ts +7 -0
- package/src/scripts/ai_council/stance_tally.ts +42 -5
- 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/hook_manifest.yaml +17 -6
- package/src/scripts/install-hooks.sh +32 -17
- 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/pr_url_reminder_hook.ts +177 -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
|
@@ -484,7 +484,10 @@ export function consult(
|
|
|
484
484
|
|
|
485
485
|
const spent: Spent = { input: 0, output: 0, usd: 0.0 };
|
|
486
486
|
let last_results: CouncilResponse[] = [];
|
|
487
|
-
|
|
487
|
+
// A3 read unlock: the stable prefix is ALWAYS the original user prompt;
|
|
488
|
+
// per-round critiques + the STANCE contract ride in a volatile suffix so
|
|
489
|
+
// cache-capable clients read the [system + artefact] prefix on round N+1.
|
|
490
|
+
let current_suffix = '';
|
|
488
491
|
|
|
489
492
|
const stance_tally = opts.stance_tally ?? false;
|
|
490
493
|
for (let round_idx = 0; round_idx < rounds; round_idx++) {
|
|
@@ -492,11 +495,11 @@ export function consult(
|
|
|
492
495
|
// contract when the tally is enabled. Off (default) → the original
|
|
493
496
|
// question object flows through untouched, byte-identical to today.
|
|
494
497
|
const is_final = round_idx === rounds - 1;
|
|
495
|
-
const
|
|
496
|
-
const prompt_for_round =
|
|
498
|
+
const suffix_for_round =
|
|
497
499
|
is_final && stance_tally
|
|
498
|
-
? `${
|
|
499
|
-
:
|
|
500
|
+
? `${current_suffix}\n\n---\n\n${STANCE_LINE_CONTRACT}`
|
|
501
|
+
: current_suffix;
|
|
502
|
+
const prompt_for_round = `${question.user_prompt}${suffix_for_round}`;
|
|
500
503
|
const round_question =
|
|
501
504
|
round_idx === 0 && prompt_for_round === question.user_prompt
|
|
502
505
|
? question
|
|
@@ -511,16 +514,15 @@ export function consult(
|
|
|
511
514
|
project,
|
|
512
515
|
original_ask,
|
|
513
516
|
advisor_plans,
|
|
517
|
+
split: suffix_for_round
|
|
518
|
+
? { stable: question.user_prompt, suffix: suffix_for_round }
|
|
519
|
+
: null,
|
|
514
520
|
});
|
|
515
521
|
if (on_round_complete !== null) {
|
|
516
522
|
on_round_complete(round_idx, last_results);
|
|
517
523
|
}
|
|
518
524
|
if (round_idx + 1 < rounds) {
|
|
519
|
-
|
|
520
|
-
question.user_prompt,
|
|
521
|
-
last_results,
|
|
522
|
-
round_idx + 2,
|
|
523
|
-
);
|
|
525
|
+
current_suffix = _critique_suffix(last_results, round_idx + 2);
|
|
524
526
|
}
|
|
525
527
|
}
|
|
526
528
|
|
|
@@ -579,6 +581,14 @@ interface RunRoundOptions {
|
|
|
579
581
|
project: ProjectContext | null;
|
|
580
582
|
original_ask: string;
|
|
581
583
|
advisor_plans?: Map<string, AdvisorPlan> | null;
|
|
584
|
+
/**
|
|
585
|
+
* A3 cross-round read unlock: when set, members are called via
|
|
586
|
+
* `ask_split(system, stable, suffix)` so cache-capable clients keep the
|
|
587
|
+
* breakpoint on the byte-stable [system + artefact] prefix and the
|
|
588
|
+
* per-round critiques ride behind it. `question.user_prompt` stays the
|
|
589
|
+
* full concatenation (estimates + persistence unchanged).
|
|
590
|
+
*/
|
|
591
|
+
split?: { stable: string; suffix: string } | null;
|
|
582
592
|
}
|
|
583
593
|
|
|
584
594
|
/** Run a single round; mutate `spent` with cumulative totals. */
|
|
@@ -626,11 +636,18 @@ function _run_round(
|
|
|
626
636
|
if (!(_getattr(member, 'billable', true) as boolean)) {
|
|
627
637
|
let response: CouncilResponse;
|
|
628
638
|
try {
|
|
629
|
-
response =
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
639
|
+
response = opts.split
|
|
640
|
+
? member.ask_split(
|
|
641
|
+
_system_prompt_for_member(member),
|
|
642
|
+
opts.split.stable,
|
|
643
|
+
opts.split.suffix,
|
|
644
|
+
question.max_tokens,
|
|
645
|
+
)
|
|
646
|
+
: member.ask(
|
|
647
|
+
_system_prompt_for_member(member),
|
|
648
|
+
question.user_prompt,
|
|
649
|
+
question.max_tokens,
|
|
650
|
+
);
|
|
634
651
|
} catch (exc) {
|
|
635
652
|
response = new CouncilResponse({
|
|
636
653
|
provider: member.name,
|
|
@@ -702,11 +719,18 @@ function _run_round(
|
|
|
702
719
|
// ── actual call ──────────────────────────────────────────────
|
|
703
720
|
let response: CouncilResponse;
|
|
704
721
|
try {
|
|
705
|
-
response =
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
722
|
+
response = opts.split
|
|
723
|
+
? member.ask_split(
|
|
724
|
+
_system_prompt_for_member(member),
|
|
725
|
+
opts.split.stable,
|
|
726
|
+
opts.split.suffix,
|
|
727
|
+
question.max_tokens,
|
|
728
|
+
)
|
|
729
|
+
: member.ask(
|
|
730
|
+
_system_prompt_for_member(member),
|
|
731
|
+
question.user_prompt,
|
|
732
|
+
question.max_tokens,
|
|
733
|
+
);
|
|
710
734
|
} catch (exc) {
|
|
711
735
|
response = new CouncilResponse({
|
|
712
736
|
provider: member.name,
|
|
@@ -796,6 +820,19 @@ function _augment_for_next_round(
|
|
|
796
820
|
original_prompt: string,
|
|
797
821
|
prior_responses: CouncilResponse[],
|
|
798
822
|
next_round_number: number,
|
|
823
|
+
): string {
|
|
824
|
+
return `${original_prompt}${_critique_suffix(prior_responses, next_round_number)}`;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
/**
|
|
828
|
+
* The volatile per-round block ONLY (A3 read unlock) — everything after the
|
|
829
|
+
* byte-stable original prompt. Empty string when no prior response survives.
|
|
830
|
+
* `_augment_for_next_round` = original + this; the rounds loop feeds this to
|
|
831
|
+
* `ask_split` so the cached [system + artefact] prefix stays byte-identical.
|
|
832
|
+
*/
|
|
833
|
+
function _critique_suffix(
|
|
834
|
+
prior_responses: CouncilResponse[],
|
|
835
|
+
next_round_number: number,
|
|
799
836
|
): string {
|
|
800
837
|
const blocks: string[] = [];
|
|
801
838
|
let label_idx = 0;
|
|
@@ -808,11 +845,11 @@ function _augment_for_next_round(
|
|
|
808
845
|
blocks.push(`### Reviewer ${label}\n\n${_pyStrip(r.text)}`);
|
|
809
846
|
}
|
|
810
847
|
if (blocks.length === 0) {
|
|
811
|
-
return
|
|
848
|
+
return '';
|
|
812
849
|
}
|
|
813
850
|
const prior_block = blocks.join('\n\n');
|
|
814
851
|
return (
|
|
815
|
-
|
|
852
|
+
`\n\n` +
|
|
816
853
|
`---\n\n` +
|
|
817
854
|
`## Prior round critiques (round ${next_round_number - 1})\n\n` +
|
|
818
855
|
`You are now in round ${next_round_number}. Below are anonymised\n` +
|
|
@@ -910,6 +947,15 @@ function _augment_for_debate_round(
|
|
|
910
947
|
// Phase 3: when the debate gates are enabled, prepend the anti-conformity
|
|
911
948
|
// directive. Default `false` keeps the prompt byte-identical to today.
|
|
912
949
|
anti_conformity = false,
|
|
950
|
+
): string {
|
|
951
|
+
return `${original_prompt}${_debate_suffix(prior_responses, next_round_number, anti_conformity)}`;
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
/** Debate twin of `_critique_suffix` — the volatile block only (A3). */
|
|
955
|
+
function _debate_suffix(
|
|
956
|
+
prior_responses: CouncilResponse[],
|
|
957
|
+
next_round_number: number,
|
|
958
|
+
anti_conformity = false,
|
|
913
959
|
): string {
|
|
914
960
|
const blocks: string[] = [];
|
|
915
961
|
let label_idx = 0;
|
|
@@ -922,12 +968,12 @@ function _augment_for_debate_round(
|
|
|
922
968
|
blocks.push(`### Reviewer ${label}\n\n${_pyStrip(r.text)}`);
|
|
923
969
|
}
|
|
924
970
|
if (blocks.length === 0) {
|
|
925
|
-
return
|
|
971
|
+
return '';
|
|
926
972
|
}
|
|
927
973
|
const prior_block = blocks.join('\n\n');
|
|
928
974
|
const anti_conformity_block = anti_conformity ? `${ANTI_CONFORMITY_DIRECTIVE}\n\n` : '';
|
|
929
975
|
return (
|
|
930
|
-
|
|
976
|
+
`\n\n` +
|
|
931
977
|
`---\n\n` +
|
|
932
978
|
`## Prior round positions (round ${next_round_number - 1})\n\n` +
|
|
933
979
|
`You are now in round ${next_round_number} of a structured\n` +
|
|
@@ -1122,6 +1168,9 @@ export function run_debate(
|
|
|
1122
1168
|
|
|
1123
1169
|
const spent: Spent = { input: 0, output: 0, usd: 0.0 };
|
|
1124
1170
|
const all_rounds: CouncilResponse[][] = [];
|
|
1171
|
+
// A3 read unlock (debate twin): stable prefix = the original prompt; the
|
|
1172
|
+
// per-round positions block rides in a volatile suffix (see consult loop).
|
|
1173
|
+
let current_suffix = '';
|
|
1125
1174
|
let current_user_prompt = question.user_prompt;
|
|
1126
1175
|
|
|
1127
1176
|
// Phase 3 restate: one pre-round-1 call per member. Runs BEFORE any debate
|
|
@@ -1167,6 +1216,9 @@ export function run_debate(
|
|
|
1167
1216
|
project,
|
|
1168
1217
|
original_ask,
|
|
1169
1218
|
advisor_plans,
|
|
1219
|
+
split: current_suffix
|
|
1220
|
+
? { stable: question.user_prompt, suffix: current_suffix }
|
|
1221
|
+
: null,
|
|
1170
1222
|
});
|
|
1171
1223
|
}
|
|
1172
1224
|
|
|
@@ -1195,12 +1247,12 @@ export function run_debate(
|
|
|
1195
1247
|
// Prep the user-prompt for the next round so the cost estimate
|
|
1196
1248
|
// below covers the augmented bytes.
|
|
1197
1249
|
if (round_idx + 1 < max_rounds) {
|
|
1198
|
-
|
|
1199
|
-
question.user_prompt,
|
|
1250
|
+
current_suffix = _debate_suffix(
|
|
1200
1251
|
results,
|
|
1201
1252
|
round_number + 1,
|
|
1202
1253
|
opts.debate_gates ?? false,
|
|
1203
1254
|
);
|
|
1255
|
+
current_user_prompt = `${question.user_prompt}${current_suffix}`;
|
|
1204
1256
|
// Hard-cap + continue-prompt gating before kicking off N+1.
|
|
1205
1257
|
let next_round_usd: number;
|
|
1206
1258
|
if (table !== null) {
|
|
@@ -119,6 +119,65 @@ export interface CacheTokenBreakdown {
|
|
|
119
119
|
output_tokens: number;
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
+
/** A1↔A3 coupling verdict for a proposed model downgrade (Phase A3). */
|
|
123
|
+
export interface DowngradeCoupling {
|
|
124
|
+
/** Full-rate cost delta per call: estimate(current) − estimate(suggested). */
|
|
125
|
+
downgrade_savings_usd: number;
|
|
126
|
+
/**
|
|
127
|
+
* Cache value forfeited by leaving the current model: the prompt cache is
|
|
128
|
+
* model-scoped, so a downgraded member re-pays the full input rate on the
|
|
129
|
+
* span that would have been read back at CACHE_READ_MULTIPLIER.
|
|
130
|
+
*/
|
|
131
|
+
lost_cache_savings_usd: number;
|
|
132
|
+
/** True iff downgrade_savings_usd > lost_cache_savings_usd. */
|
|
133
|
+
net_positive: boolean;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Decide whether a suggested downgrade is net-positive AFTER accounting for
|
|
138
|
+
* the model-scoped prompt cache (roadmap A3 gate: downgrade only when
|
|
139
|
+
* `downgrade_savings > lost_cache_savings`).
|
|
140
|
+
*
|
|
141
|
+
* `cache.expected_reads` is the number of same-model cache reads the run
|
|
142
|
+
* would plausibly realize at the CURRENT model (rounds − 1 on multi-round
|
|
143
|
+
* runs, 0 on one-shot paths like the low-impact fast-path). With caching
|
|
144
|
+
* disabled the lost term is 0 and any cheaper model wins.
|
|
145
|
+
*/
|
|
146
|
+
export function downgrade_coupling(
|
|
147
|
+
provider: string,
|
|
148
|
+
current_model: string,
|
|
149
|
+
suggested_model: string,
|
|
150
|
+
input_tokens: number,
|
|
151
|
+
max_output_tokens: number,
|
|
152
|
+
cache: {
|
|
153
|
+
enabled: boolean;
|
|
154
|
+
cacheable_prefix_tokens: number;
|
|
155
|
+
expected_reads: number;
|
|
156
|
+
},
|
|
157
|
+
table: PriceTable,
|
|
158
|
+
): DowngradeCoupling {
|
|
159
|
+
const cur = estimate_cost(provider, current_model, input_tokens, max_output_tokens, table);
|
|
160
|
+
const sug = estimate_cost(provider, suggested_model, input_tokens, max_output_tokens, table);
|
|
161
|
+
const downgrade_savings_usd =
|
|
162
|
+
cur.input_usd + cur.output_usd - (sug.input_usd + sug.output_usd);
|
|
163
|
+
let lost_cache_savings_usd = 0;
|
|
164
|
+
if (cache.enabled && cache.expected_reads > 0 && cache.cacheable_prefix_tokens > 0) {
|
|
165
|
+
const cur_rates = lookup(table, provider, current_model);
|
|
166
|
+
if (cur_rates !== null) {
|
|
167
|
+
lost_cache_savings_usd =
|
|
168
|
+
(cache.cacheable_prefix_tokens / 1_000_000) *
|
|
169
|
+
cur_rates.input_per_1m_usd *
|
|
170
|
+
(1 - CACHE_READ_MULTIPLIER) *
|
|
171
|
+
cache.expected_reads;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return {
|
|
175
|
+
downgrade_savings_usd,
|
|
176
|
+
lost_cache_savings_usd,
|
|
177
|
+
net_positive: downgrade_savings_usd > lost_cache_savings_usd,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
122
181
|
/**
|
|
123
182
|
* Realized cost of a call given OBSERVED cache-token counts (from
|
|
124
183
|
* `CouncilResponse.usage`). Deliberately separate from `estimate_cost`, which
|
|
@@ -88,6 +88,13 @@ correctness lens AND a shipping-risk lens:
|
|
|
88
88
|
Is the blast radius bigger than the title implies?
|
|
89
89
|
5. Reviewer fatigue — is anything in the diff that a tired reviewer
|
|
90
90
|
would rubber-stamp but should not?
|
|
91
|
+
6. Reuse & object-oriented shape — does the diff reinvent a component
|
|
92
|
+
or abstraction the codebase likely already provides (should compose
|
|
93
|
+
/ reuse instead), and where it adds structure, is the OO shape
|
|
94
|
+
(encapsulation, composition over inheritance) justified by
|
|
95
|
+
complexity it removes rather than speculative abstraction? Judge in
|
|
96
|
+
the codebase's own paradigm — do not push classes onto functional /
|
|
97
|
+
pipeline code; smallest-change still wins over OO advocacy.
|
|
91
98
|
End with: APPROVE / REQUEST_CHANGES / REJECT, one sentence why, and
|
|
92
99
|
the single highest-leverage change the PR author should make before
|
|
93
100
|
merge.`;
|
|
@@ -59,6 +59,37 @@ export interface StanceTallyResult {
|
|
|
59
59
|
const _STANCE_RE =
|
|
60
60
|
/STANCE:\s*(.+?)\s*\|\s*CONFIDENCE:\s*(high|med(?:ium)?|low)\s*\|\s*DEALBREAKER:\s*(yes|no)/gi;
|
|
61
61
|
|
|
62
|
+
|
|
63
|
+
/** Run the strict stance grammar; return the LAST match or null. */
|
|
64
|
+
function _last_stance_match(text: string): RegExpExecArray | null {
|
|
65
|
+
_STANCE_RE.lastIndex = 0;
|
|
66
|
+
let last: RegExpExecArray | null = null;
|
|
67
|
+
let m: RegExpExecArray | null;
|
|
68
|
+
while ((m = _STANCE_RE.exec(text)) !== null) {
|
|
69
|
+
last = m;
|
|
70
|
+
}
|
|
71
|
+
return last;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Normalize the two cosmetic defect classes the lenient pass forgives, on
|
|
76
|
+
* lines that mention STANCE only: markdown emphasis characters and comma /
|
|
77
|
+
* semicolon field separators. Everything else is left byte-identical.
|
|
78
|
+
*/
|
|
79
|
+
export function _normalize_stance_cosmetics(text: string): string {
|
|
80
|
+
return text
|
|
81
|
+
.split('\n')
|
|
82
|
+
.map((line) => {
|
|
83
|
+
if (!/stance/i.test(line)) {
|
|
84
|
+
return line;
|
|
85
|
+
}
|
|
86
|
+
let out = line.replace(/[*_`]/g, '');
|
|
87
|
+
out = out.replace(/\s*[;,]\s*(?=(?:CONFIDENCE|DEALBREAKER)\b)/gi, ' | ');
|
|
88
|
+
return out;
|
|
89
|
+
})
|
|
90
|
+
.join('\n');
|
|
91
|
+
}
|
|
92
|
+
|
|
62
93
|
/**
|
|
63
94
|
* Parse the member's stance line from their reply text. Returns the LAST stance
|
|
64
95
|
* line if several appear (the final, most-authoritative one), or `null` when no
|
|
@@ -66,11 +97,17 @@ const _STANCE_RE =
|
|
|
66
97
|
* infers a stance from the surrounding prose.
|
|
67
98
|
*/
|
|
68
99
|
export function parse_stance_line(text: string): Omit<StanceLine, 'member'> | null {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
100
|
+
let last = _last_stance_match(text);
|
|
101
|
+
if (last === null) {
|
|
102
|
+
// Lenient fallback (A3 repair-tightening): a stance whose only defect
|
|
103
|
+
// is cosmetic — markdown emphasis around the field names (`**STANCE:**`),
|
|
104
|
+
// or `,`/`;` used where the contract says `|` — is NOT "genuinely
|
|
105
|
+
// unparseable" and must not burn a repair call. Normalize those two
|
|
106
|
+
// defect classes on stance-bearing lines only, then re-run the SAME
|
|
107
|
+
// strict grammar. Anything the strict grammar still rejects (missing
|
|
108
|
+
// fields, invalid enum values, no STANCE line at all) stays a repair
|
|
109
|
+
// marker — leniency never invents a stance from prose.
|
|
110
|
+
last = _last_stance_match(_normalize_stance_cosmetics(text));
|
|
74
111
|
}
|
|
75
112
|
if (last === null) {
|
|
76
113
|
return null;
|
|
@@ -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
|
'',
|