@event4u/agent-config 9.15.0 → 9.17.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/AGENTS.md +1 -1
- package/CHANGELOG.md +74 -0
- package/README.md +1 -1
- package/dist/agent-src/commands/agent-handoff.md +100 -48
- package/dist/agent-src/commands/commit.md +1 -1
- package/dist/agent-src/commands/condense.md +19 -64
- package/dist/agent-src/commands/evals/agent-handoff.json +13 -1
- package/dist/agent-src/commands/memory/promote.md +14 -1
- package/dist/agent-src/commands/mode.md +9 -5
- package/dist/agent-src/commands/optimize/deep.md +72 -11
- package/dist/agent-src/contexts/authority/commit-mechanics.md +5 -5
- package/dist/agent-src/contexts/communication/rules-auto/guidelines-mechanics.md +1 -0
- package/dist/agent-src/contexts/communication/rules-auto/skill-quality-mechanics.md +1 -1
- package/dist/agent-src/contexts/communication/rules-auto/think-before-action-mechanics.md +6 -1
- package/dist/agent-src/contexts/communication/rules-auto/token-efficiency-mechanics.md +4 -1
- package/dist/agent-src/contexts/execution/autonomy-mechanics.md +27 -1
- package/dist/agent-src/contexts/execution/roadmap-process-loop.md +5 -2
- package/dist/agent-src/profiles/agency.yml +3 -3
- package/dist/agent-src/profiles/content_creator.yml +2 -2
- package/dist/agent-src/profiles/developer.yml +2 -2
- package/dist/agent-src/profiles/finance.yml +1 -1
- package/dist/agent-src/profiles/founder.yml +2 -2
- package/dist/agent-src/profiles/ops.yml +2 -2
- package/dist/agent-src/rules/architecture.md +2 -2
- package/dist/agent-src/rules/autonomous-execution.md +1 -1
- package/dist/agent-src/rules/context-hygiene.md +3 -2
- package/dist/agent-src/rules/domain-safety-pii.md +1 -1
- package/dist/agent-src/rules/output-discipline.md +9 -2
- package/dist/agent-src/rules/token-budget-discipline.md +2 -2
- package/dist/agent-src/rules/user-interaction.md +3 -4
- package/dist/agent-src/skills/build-buy-partner/SKILL.md +1 -1
- package/dist/agent-src/skills/copilot-agents-optimization/SKILL.md +4 -4
- package/dist/agent-src/skills/copilot-config/SKILL.md +1 -1
- package/dist/agent-src/skills/deep-reading-analyst/SKILL.md +7 -4
- package/dist/agent-src/skills/description-assist/SKILL.md +1 -1
- package/dist/agent-src/skills/fe-design/SKILL.md +1 -1
- package/dist/agent-src/skills/market-entry-analysis/SKILL.md +1 -1
- package/dist/agent-src/skills/persona-writing/SKILL.md +2 -2
- package/dist/agent-src/skills/readme-reviewer/SKILL.md +2 -1
- package/dist/agent-src/skills/rule-writing/SKILL.md +4 -2
- package/dist/agent-src/skills/skill-reviewer/SKILL.md +1 -1
- package/dist/agent-src/skills/skill-writing/SKILL.md +2 -2
- package/dist/agent-src/skills/tailwind-engineer/SKILL.md +3 -1
- package/dist/agent-src/skills/token-optimizer/SKILL.md +2 -2
- package/dist/agent-src/skills/ui-component-architect/SKILL.md +6 -3
- package/dist/agent-src/templates/agent-settings.md +13 -15
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +6 -2
- package/dist/agent-src/templates/persona.md +2 -2
- package/dist/agent-src/templates/roadmaps.md +1 -1
- package/dist/agent-src/templates/rule.md +1 -1
- package/dist/agent-src/templates/skill.md +1 -1
- package/dist/cli/mcp/content.js +1 -1
- package/dist/cli/mcp/content.js.map +1 -1
- package/dist/cli/registry.js +1 -0
- package/dist/cli/registry.js.map +1 -1
- package/dist/cli-delegate/{chunk-ZXXJLL5K.js → chunk-BSLUHV2F.js} +127 -1487
- package/dist/cli-delegate/{chunk-H574KOUH.js → chunk-IMSXPRVH.js} +24 -1
- package/dist/cli-delegate/chunk-VSYT7OHO.js +1382 -0
- package/dist/cli-delegate/chunk-XGA2NTU4.js +3727 -0
- package/dist/cli-delegate/cmd_conformance.js +4 -3
- package/dist/cli-delegate/cmd_converge.js +3 -3
- package/dist/cli-delegate/cmd_doctor.js +3 -2
- package/dist/cli-delegate/cmd_export.js +4 -4
- package/dist/cli-delegate/cmd_handoff.js +3194 -0
- package/dist/cli-delegate/cmd_memory_get.js +35 -3754
- package/dist/cli-delegate/cmd_migrate.js +2 -2
- package/dist/cli-delegate/cmd_prune.js +4 -4
- package/dist/cli-delegate/cmd_settings_migrate.js +4 -4
- package/dist/cli-delegate/cmd_sync.js +4 -4
- package/dist/cli-delegate/cmd_uninstall.js +4 -4
- package/dist/cli-delegate/cmd_update.js +3 -3
- package/dist/cli-delegate/cmd_upgrade.js +3 -3
- package/dist/cli-delegate/cmd_validate.js +4 -4
- package/dist/cli-delegate/cmd_versions.js +3 -3
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +31 -31
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +2 -2
- package/dist/discovery/trust-report.md +1 -1
- package/dist/discovery/workspaces.json +2 -2
- package/dist/hooks/dispatch.js +602 -507
- package/dist/install/install.mjs +23 -0
- package/dist/mcp/awesome-mcp-servers.row.md +1 -1
- package/dist/mcp/mcp-cloudflare-catalogue.json +2 -2
- package/dist/mcp/registry-manifest.json +4 -4
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +25 -3
- package/dist/scripts/_lib/agent_src.js +39 -6
- package/dist/scripts/_lib/agent_src.js.map +1 -1
- package/dist/server/routes/workspace.js +1 -1
- package/dist/server/routes/workspace.js.map +1 -1
- package/dist/server/schemas/settings.js +13 -0
- package/dist/server/schemas/settings.js.map +1 -1
- package/dist/shared/userMd/utils.js +1 -1
- package/dist/shared/userMd/utils.js.map +1 -1
- package/dist/ui/assets/index-DmR0mbk4.js +26 -0
- package/dist/ui/assets/index-DmR0mbk4.js.map +1 -0
- package/dist/ui/index.html +1 -1
- package/docs/guidelines/abstraction-thresholds.md +53 -0
- package/docs/guidelines/agent-infra/agent-interaction-and-decision-quality.md +1 -1
- package/docs/guidelines/agent-infra/asking-and-brevity-examples.md +7 -0
- package/docs/guidelines/agent-infra/context-hygiene-mechanics.md +2 -2
- package/docs/guidelines/agent-infra/minimal-safe-diff-mechanics.md +5 -3
- package/docs/guidelines/agent-infra/size-and-scope.md +8 -2
- package/docs/guidelines/component-oriented-and-oop-development.md +11 -6
- package/docs/guidelines/docs/readme-size-and-splitting.md +5 -2
- package/llms.txt +1 -0
- package/package.json +4 -4
- package/src/config/agent-settings.template.yml +25 -1
- package/src/config/budget-routing.json +7 -0
- package/src/config/evaluator-budgets.json +8 -11
- package/src/config/gate-coverage.yml +30 -0
- package/src/scripts/_cli/cmd_handoff.ts +405 -0
- package/src/scripts/_cli/handoff_generate.ts +453 -0
- package/src/scripts/_cli/handoff_sessions.ts +435 -0
- package/src/scripts/_dispatch.bash +9 -0
- package/src/scripts/_lib/agent_src.ts +36 -6
- package/src/scripts/_lib/release_material.ts +156 -0
- package/src/scripts/_lib/tier_budget_routing.ts +154 -28
- package/src/scripts/anchor_scoring_run.ts +1 -1
- package/src/scripts/bench_persona_placebo.ts +1 -1
- package/src/scripts/bench_quality_run.ts +1 -1
- package/src/scripts/check_cli_registry_budget_sync.ts +168 -0
- package/src/scripts/check_condensation.ts +2 -1
- package/src/scripts/check_finding_dispositions.ts +278 -0
- package/src/scripts/check_gate_coverage.ts +1 -1
- package/src/scripts/check_pack_size.ts +4 -3
- package/src/scripts/check_references.ts +1 -0
- package/src/scripts/check_release_highlights.ts +300 -0
- package/src/scripts/check_release_pr_shape.ts +4 -0
- package/src/scripts/check_release_surface_equality.ts +132 -0
- package/src/scripts/check_reply_consistency.ts +187 -138
- package/src/scripts/check_token_quality_golden.ts +1 -1
- package/src/scripts/cost/budget.mjs +18 -4
- package/src/scripts/handoff_context_hook.ts +148 -0
- package/src/scripts/hook_manifest.yaml +17 -7
- package/src/scripts/hooks/concern_registry.ts +2 -0
- package/src/scripts/lint_abstraction_thresholds.ts +325 -0
- package/src/scripts/lint_framework_leakage_allowlist.json +2 -2
- package/src/scripts/lint_profile_personas.ts +153 -0
- package/src/scripts/lint_token_budget_discipline.ts +148 -0
- package/src/scripts/release.ts +264 -37
- package/src/scripts/release_drill.ts +491 -0
- package/src/scripts/routing_doctor.ts +76 -1
- package/src/scripts/schemas/command.schema.json +33 -0
- package/src/scripts/self_review_gate.ts +41 -3
- package/src/scripts/skill_linter.ts +3 -1
- package/src/scripts/smoke/router.sh +1 -1
- package/src/scripts/update_counts.ts +60 -12
- package/src/server/schemas/settings.ts +23 -0
- package/src/shared/userMd/utils.ts +1 -1
- package/dist/ui/assets/index-DJY_vQVn.js +0 -40
- package/dist/ui/assets/index-DJY_vQVn.js.map +0 -1
- package/dist/cli-delegate/{chunk-XX6TOPD7.js → chunk-WR6GTRHJ.js} +3 -3
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
*/
|
|
21
21
|
import * as fs from 'node:fs';
|
|
22
22
|
import * as path from 'node:path';
|
|
23
|
+
import { fileURLToPath } from 'node:url';
|
|
23
24
|
|
|
24
25
|
export type BudgetTier = 'cheap' | 'medium' | 'strong';
|
|
25
26
|
|
|
@@ -123,19 +124,91 @@ export const RESERVE_FILE = 'tier-reserves.jsonl';
|
|
|
123
124
|
export const COOLDOWN_FILE = 'tier-cooldowns.json';
|
|
124
125
|
export const DEFAULT_COOLDOWN_MS = 60 * 60 * 1000;
|
|
125
126
|
|
|
127
|
+
// Reserve lifecycle constants — single source of truth shared with
|
|
128
|
+
// budget.mjs (the second reserve reader) via src/config/budget-routing.json.
|
|
129
|
+
// Duplicating the literal here was the exact two-truths defect the external
|
|
130
|
+
// review flagged; both readers now load the same file.
|
|
131
|
+
const _LIFECYCLE_CONFIG = path.join(
|
|
132
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
133
|
+
'..',
|
|
134
|
+
'..',
|
|
135
|
+
'config',
|
|
136
|
+
'budget-routing.json',
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
function _lifecycle(): { reserve_ttl_ms: number; lock_break_ms: number } {
|
|
140
|
+
try {
|
|
141
|
+
const raw = JSON.parse(fs.readFileSync(_LIFECYCLE_CONFIG, 'utf-8')) as Record<string, unknown>;
|
|
142
|
+
const ttl = typeof raw['reserve_ttl_ms'] === 'number' ? raw['reserve_ttl_ms'] : 600_000;
|
|
143
|
+
const brk = typeof raw['lock_break_ms'] === 'number' ? raw['lock_break_ms'] : 30_000;
|
|
144
|
+
return { reserve_ttl_ms: ttl, lock_break_ms: brk };
|
|
145
|
+
} catch {
|
|
146
|
+
return { reserve_ttl_ms: 600_000, lock_break_ms: 30_000 };
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function reserveTtlMs(): number {
|
|
151
|
+
return _lifecycle().reserve_ttl_ms;
|
|
152
|
+
}
|
|
153
|
+
|
|
126
154
|
interface ReserveEntry {
|
|
127
155
|
ts_ms: number;
|
|
128
156
|
tier: BudgetTier;
|
|
129
157
|
est_usd: number;
|
|
130
|
-
status: 'pending';
|
|
158
|
+
status: 'pending' | 'settled';
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function _readReserves(reservePath: string): ReserveEntry[] {
|
|
162
|
+
if (!fs.existsSync(reservePath)) {
|
|
163
|
+
return [];
|
|
164
|
+
}
|
|
165
|
+
const out: ReserveEntry[] = [];
|
|
166
|
+
for (const line of fs.readFileSync(reservePath, 'utf-8').split('\n')) {
|
|
167
|
+
if (!line.trim()) continue;
|
|
168
|
+
try {
|
|
169
|
+
out.push(JSON.parse(line) as ReserveEntry);
|
|
170
|
+
} catch {
|
|
171
|
+
// tolerate a torn line — reserve file is advisory spend state
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return out;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Take the reserve lock; break a stale one (crash leftover) once. */
|
|
178
|
+
function _takeLock(lock: string, now_ms: number, lock_break_ms: number): number | null {
|
|
179
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
180
|
+
try {
|
|
181
|
+
return fs.openSync(lock, 'wx');
|
|
182
|
+
} catch {
|
|
183
|
+
try {
|
|
184
|
+
const age = now_ms - fs.statSync(lock).mtimeMs;
|
|
185
|
+
if (age > lock_break_ms) {
|
|
186
|
+
fs.rmSync(lock, { force: true });
|
|
187
|
+
continue; // one retry after breaking the stale lock
|
|
188
|
+
}
|
|
189
|
+
} catch {
|
|
190
|
+
continue; // lock vanished between open and stat — retry once
|
|
191
|
+
}
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return null;
|
|
131
196
|
}
|
|
132
197
|
|
|
133
198
|
/**
|
|
134
|
-
* Atomic budget reserve
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
199
|
+
* Atomic budget reserve — full lifecycle (external review 2026-08-03):
|
|
200
|
+
*
|
|
201
|
+
* acquire → expire (TTL) / settle → compact-on-write
|
|
202
|
+
*
|
|
203
|
+
* Under the lock: live reserves = pending entries younger than the shared
|
|
204
|
+
* TTL (reserves are RACE PROTECTION, not spend accounting — real spend is
|
|
205
|
+
* the ledger's job); the file is COMPACTED to exactly those live entries
|
|
206
|
+
* on every acquire, so it stays bounded; a lock file older than
|
|
207
|
+
* `lock_break_ms` is a crash leftover and is broken with one retry.
|
|
208
|
+
* Returns granted=false (never throws) when the ceiling would be crossed
|
|
209
|
+
* or a fresh lock is genuinely held — the caller falls back per the
|
|
210
|
+
* relation (fail-open at the DECISION layer, conservative at the SPEND
|
|
211
|
+
* layer).
|
|
139
212
|
*/
|
|
140
213
|
export function acquireBudgetPermit(options: {
|
|
141
214
|
tracking_dir: string;
|
|
@@ -148,44 +221,45 @@ export function acquireBudgetPermit(options: {
|
|
|
148
221
|
const dir = options.tracking_dir;
|
|
149
222
|
fs.mkdirSync(dir, { recursive: true });
|
|
150
223
|
const lock = path.join(dir, `${RESERVE_FILE}.lock`);
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
} catch {
|
|
224
|
+
const { reserve_ttl_ms, lock_break_ms } = _lifecycle();
|
|
225
|
+
const fd = _takeLock(lock, options.now_ms, lock_break_ms);
|
|
226
|
+
if (fd === null) {
|
|
155
227
|
return { granted: false, reason: 'reserve lock busy — treat tier as unavailable this instant' };
|
|
156
228
|
}
|
|
157
229
|
try {
|
|
158
230
|
const reservePath = path.join(dir, RESERVE_FILE);
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
if (e.tier === options.tier && e.status === 'pending') {
|
|
166
|
-
reserved += e.est_usd;
|
|
167
|
-
}
|
|
168
|
-
} catch {
|
|
169
|
-
// tolerate a torn line — reserve file is advisory spend state
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
}
|
|
231
|
+
const live = _readReserves(reservePath).filter(
|
|
232
|
+
(e) => e.status === 'pending' && options.now_ms - e.ts_ms < reserve_ttl_ms,
|
|
233
|
+
);
|
|
234
|
+
const reserved = live
|
|
235
|
+
.filter((e) => e.tier === options.tier)
|
|
236
|
+
.reduce((acc, e) => acc + e.est_usd, 0);
|
|
173
237
|
if (
|
|
174
238
|
options.ceiling_usd !== null &&
|
|
175
239
|
options.spent_usd + reserved + options.estimated_cost_usd > options.ceiling_usd
|
|
176
240
|
) {
|
|
241
|
+
// Compact even on deny — expiry must not depend on a grant.
|
|
242
|
+
fs.writeFileSync(
|
|
243
|
+
reservePath,
|
|
244
|
+
live.map((e) => JSON.stringify(e)).join('\n') + (live.length ? '\n' : ''),
|
|
245
|
+
'utf-8',
|
|
246
|
+
);
|
|
177
247
|
return {
|
|
178
248
|
granted: false,
|
|
179
249
|
reason: `ceiling ${options.ceiling_usd} would be crossed (spent ${options.spent_usd} + reserved ${reserved})`,
|
|
180
250
|
};
|
|
181
251
|
}
|
|
182
|
-
|
|
252
|
+
live.push({
|
|
183
253
|
ts_ms: options.now_ms,
|
|
184
254
|
tier: options.tier,
|
|
185
255
|
est_usd: options.estimated_cost_usd,
|
|
186
256
|
status: 'pending',
|
|
187
|
-
};
|
|
188
|
-
fs.
|
|
257
|
+
});
|
|
258
|
+
fs.writeFileSync(
|
|
259
|
+
reservePath,
|
|
260
|
+
live.map((e) => JSON.stringify(e)).join('\n') + '\n',
|
|
261
|
+
'utf-8',
|
|
262
|
+
);
|
|
189
263
|
return { granted: true, reason: 'reserved' };
|
|
190
264
|
} finally {
|
|
191
265
|
fs.closeSync(fd);
|
|
@@ -193,7 +267,54 @@ export function acquireBudgetPermit(options: {
|
|
|
193
267
|
}
|
|
194
268
|
}
|
|
195
269
|
|
|
196
|
-
/**
|
|
270
|
+
/**
|
|
271
|
+
* Settle the oldest live pending reserve of a tier (call when the real
|
|
272
|
+
* cost lands in the ledger, so the estimate stops counting immediately
|
|
273
|
+
* instead of waiting out the TTL). Settled entries are dropped by the
|
|
274
|
+
* next compaction. Best-effort: lock busy → no-op (the TTL is the
|
|
275
|
+
* backstop that makes settling optional, never load-bearing).
|
|
276
|
+
*/
|
|
277
|
+
export function settlePermit(options: {
|
|
278
|
+
tracking_dir: string;
|
|
279
|
+
tier: BudgetTier;
|
|
280
|
+
now_ms: number;
|
|
281
|
+
}): boolean {
|
|
282
|
+
const lock = path.join(options.tracking_dir, `${RESERVE_FILE}.lock`);
|
|
283
|
+
const { reserve_ttl_ms, lock_break_ms } = _lifecycle();
|
|
284
|
+
const fd = _takeLock(lock, options.now_ms, lock_break_ms);
|
|
285
|
+
if (fd === null) {
|
|
286
|
+
return false;
|
|
287
|
+
}
|
|
288
|
+
try {
|
|
289
|
+
const reservePath = path.join(options.tracking_dir, RESERVE_FILE);
|
|
290
|
+
const live = _readReserves(reservePath).filter(
|
|
291
|
+
(e) => e.status === 'pending' && options.now_ms - e.ts_ms < reserve_ttl_ms,
|
|
292
|
+
);
|
|
293
|
+
const idx = live.findIndex((e) => e.tier === options.tier);
|
|
294
|
+
if (idx === -1) {
|
|
295
|
+
return false;
|
|
296
|
+
}
|
|
297
|
+
live.splice(idx, 1);
|
|
298
|
+
fs.writeFileSync(
|
|
299
|
+
reservePath,
|
|
300
|
+
live.map((e) => JSON.stringify(e)).join('\n') + (live.length ? '\n' : ''),
|
|
301
|
+
'utf-8',
|
|
302
|
+
);
|
|
303
|
+
return true;
|
|
304
|
+
} finally {
|
|
305
|
+
fs.closeSync(fd);
|
|
306
|
+
fs.rmSync(lock, { force: true });
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Record a quota/429 error: the tier cools down; pickTier skips it.
|
|
312
|
+
*
|
|
313
|
+
* Deliberately lock-free: last-writer-wins on a 3-key timestamp map is
|
|
314
|
+
* benign (two concurrent trips write near-identical "until" values), so
|
|
315
|
+
* the reserve lock is not reused here (accepted in the 2026-08-03 review).
|
|
316
|
+
* Expired entries are purged on every write so the map never grows stale.
|
|
317
|
+
*/
|
|
197
318
|
export function tripCooldown(options: {
|
|
198
319
|
tracking_dir: string;
|
|
199
320
|
tier: BudgetTier;
|
|
@@ -208,6 +329,11 @@ export function tripCooldown(options: {
|
|
|
208
329
|
} catch {
|
|
209
330
|
state = {};
|
|
210
331
|
}
|
|
332
|
+
for (const [k, v] of Object.entries(state)) {
|
|
333
|
+
if (typeof v !== 'number' || v <= options.now_ms) {
|
|
334
|
+
delete state[k];
|
|
335
|
+
}
|
|
336
|
+
}
|
|
211
337
|
state[options.tier] = until;
|
|
212
338
|
fs.mkdirSync(options.tracking_dir, { recursive: true });
|
|
213
339
|
fs.writeFileSync(p, `${JSON.stringify(state, null, 2)}\n`, 'utf-8');
|
|
@@ -22,7 +22,7 @@ import * as path from 'node:path';
|
|
|
22
22
|
import process from 'node:process';
|
|
23
23
|
import { fileURLToPath } from 'node:url';
|
|
24
24
|
|
|
25
|
-
import yaml from 'js-yaml';
|
|
25
|
+
import * as yaml from 'js-yaml';
|
|
26
26
|
|
|
27
27
|
import { build_thin, RULES_SOURCE } from './project_thin_rules.js';
|
|
28
28
|
import { cohensKappa } from './check_quality_regression.js';
|
|
@@ -29,7 +29,7 @@ import * as path from 'node:path';
|
|
|
29
29
|
import process from 'node:process';
|
|
30
30
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
31
31
|
|
|
32
|
-
import yaml from 'js-yaml';
|
|
32
|
+
import * as yaml from 'js-yaml';
|
|
33
33
|
|
|
34
34
|
import { signTestP } from './second_brain_retrieval.js';
|
|
35
35
|
|
|
@@ -30,7 +30,7 @@ import * as path from 'node:path';
|
|
|
30
30
|
import process from 'node:process';
|
|
31
31
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
32
32
|
|
|
33
|
-
import yaml from 'js-yaml';
|
|
33
|
+
import * as yaml from 'js-yaml';
|
|
34
34
|
|
|
35
35
|
import { build_thin, RULES_SOURCE } from './project_thin_rules.js';
|
|
36
36
|
import { evaluatePair, type JudgeFn, type JudgeVerdict, type PairResult } from './check_quality_regression.js';
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* CLI-registry ↔ evaluator-budget sync gate.
|
|
4
|
+
*
|
|
5
|
+
* The evaluator budget `cli_help_command_count` (src/config/evaluator-budgets.json)
|
|
6
|
+
* and the committed measurement record (agents/evidence/metrics/
|
|
7
|
+
* evaluator-measurements.json) are derived from `src/cli/registry.ts`, but until
|
|
8
|
+
* this gate nothing forced the PR that changes the registry to move them: the
|
|
9
|
+
* breach only WARNED on main and BLOCKED the next release. That is how 9.15.0
|
|
10
|
+
* went red — f02db54c2 registered `routing:doctor` (80 → 81 entries) and the
|
|
11
|
+
* drift surfaced days later, at release time, on an unrelated branch.
|
|
12
|
+
*
|
|
13
|
+
* This gate runs on every PR (Static Checks + `task preflight`) and fails the
|
|
14
|
+
* moment the three numbers disagree, so the registering PR carries the budget
|
|
15
|
+
* move itself.
|
|
16
|
+
*
|
|
17
|
+
* The count uses the SAME pinned method as evaluator_umbrella.sh — the number
|
|
18
|
+
* of `{ name: ` entry openers in src/cli/registry.ts (NOT --help prose
|
|
19
|
+
* parsing; methods historically disagreed 74 vs 76 vs 79).
|
|
20
|
+
*
|
|
21
|
+
* Exit codes: 0 in sync · 1 out of sync · 2 a required file is missing or
|
|
22
|
+
* unparseable (fail-closed: a gate that cannot read its inputs must not
|
|
23
|
+
* certify them).
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import * as fs from 'node:fs';
|
|
27
|
+
import * as path from 'node:path';
|
|
28
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
29
|
+
|
|
30
|
+
const _HERE = fileURLToPath(import.meta.url);
|
|
31
|
+
const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
|
|
32
|
+
|
|
33
|
+
const REGISTRY_PATH = path.join(REPO_ROOT, 'src/cli/registry.ts');
|
|
34
|
+
const BUDGETS_PATH = path.join(REPO_ROOT, 'src/config/evaluator-budgets.json');
|
|
35
|
+
const RECORD_PATH = path.join(REPO_ROOT, 'agents/evidence/metrics/evaluator-measurements.json');
|
|
36
|
+
|
|
37
|
+
const METRIC = 'cli_help_command_count';
|
|
38
|
+
|
|
39
|
+
/** Pinned method — byte-for-byte the umbrella's CLI_COUNT probe. */
|
|
40
|
+
function count_registry_entries(source: string): number {
|
|
41
|
+
return (source.match(/\{ name: /g) || []).length;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface BudgetEntry {
|
|
45
|
+
max?: unknown;
|
|
46
|
+
last_measured?: unknown;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface SyncInputs {
|
|
50
|
+
count: number;
|
|
51
|
+
budget: BudgetEntry | undefined;
|
|
52
|
+
recorded: unknown;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const REMEDY =
|
|
56
|
+
'Fix, in THIS PR (the one that moves src/cli/registry.ts):\n' +
|
|
57
|
+
` 1. src/config/evaluator-budgets.json → budgets.${METRIC}: set max +\n` +
|
|
58
|
+
' last_measured to the new count, bump last_measured_at, and extend\n' +
|
|
59
|
+
' baseline_note naming the commit that moves the surface.\n' +
|
|
60
|
+
' 2. Regenerate the committed record (never hand-edit):\n' +
|
|
61
|
+
' node src/scripts/record_evaluator_measurements.mjs \\\n' +
|
|
62
|
+
' --measurements <file with the re-measured metrics> \\\n' +
|
|
63
|
+
' --out agents/evidence/metrics/evaluator-measurements.json \\\n' +
|
|
64
|
+
' --sha "$(git rev-parse HEAD)"\n' +
|
|
65
|
+
'Otherwise the drift stays invisible until the next release, where the\n' +
|
|
66
|
+
'evaluator gate blocks it (the 9.15.0 failure mode this gate exists to stop).\n';
|
|
67
|
+
|
|
68
|
+
/** Pure comparison — the CLI owns the filesystem. */
|
|
69
|
+
function collect_findings(inputs: SyncInputs): string[] {
|
|
70
|
+
const findings: string[] = [];
|
|
71
|
+
const { count, budget, recorded } = inputs;
|
|
72
|
+
|
|
73
|
+
if (budget === undefined) {
|
|
74
|
+
findings.push(`budgets.${METRIC} is missing from evaluator-budgets.json`);
|
|
75
|
+
return findings;
|
|
76
|
+
}
|
|
77
|
+
if (budget.last_measured !== count) {
|
|
78
|
+
findings.push(
|
|
79
|
+
`registry has ${count} entries but budgets.${METRIC}.last_measured says ` +
|
|
80
|
+
`${String(budget.last_measured)}`,
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
if (typeof budget.max !== 'number' || count > budget.max) {
|
|
84
|
+
findings.push(
|
|
85
|
+
`registry has ${count} entries but budgets.${METRIC}.max is ` +
|
|
86
|
+
`${String(budget.max)}`,
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
if (recorded === undefined) {
|
|
90
|
+
findings.push(
|
|
91
|
+
`measurements.${METRIC} is missing from the committed record ` +
|
|
92
|
+
'(agents/evidence/metrics/evaluator-measurements.json)',
|
|
93
|
+
);
|
|
94
|
+
} else if (recorded !== count) {
|
|
95
|
+
findings.push(
|
|
96
|
+
`registry has ${count} entries but the committed record says ` +
|
|
97
|
+
`${String(recorded)}`,
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
return findings;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function _read_json(p: string): Record<string, unknown> {
|
|
104
|
+
return JSON.parse(fs.readFileSync(p, 'utf-8')) as Record<string, unknown>;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function main(): number {
|
|
108
|
+
let registrySource: string;
|
|
109
|
+
let budgetsDoc: Record<string, unknown>;
|
|
110
|
+
let recordDoc: Record<string, unknown>;
|
|
111
|
+
try {
|
|
112
|
+
registrySource = fs.readFileSync(REGISTRY_PATH, 'utf-8');
|
|
113
|
+
budgetsDoc = _read_json(BUDGETS_PATH);
|
|
114
|
+
recordDoc = _read_json(RECORD_PATH);
|
|
115
|
+
} catch (err) {
|
|
116
|
+
process.stderr.write(
|
|
117
|
+
`ERROR: cannot read a gate input: ${err instanceof Error ? err.message : String(err)}\n`,
|
|
118
|
+
);
|
|
119
|
+
return 2;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const count = count_registry_entries(registrySource);
|
|
123
|
+
const budgets = budgetsDoc['budgets'] as Record<string, BudgetEntry> | undefined;
|
|
124
|
+
const measurements = recordDoc['measurements'] as Record<string, unknown> | undefined;
|
|
125
|
+
const findings = collect_findings({
|
|
126
|
+
count,
|
|
127
|
+
budget: budgets?.[METRIC],
|
|
128
|
+
recorded: measurements?.[METRIC],
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
process.stdout.write('scanned: 3\n');
|
|
132
|
+
if (findings.length > 0) {
|
|
133
|
+
for (const f of findings) {
|
|
134
|
+
process.stdout.write(`❌ ${METRIC} out of sync: ${f}\n`);
|
|
135
|
+
}
|
|
136
|
+
process.stdout.write('\n' + REMEDY);
|
|
137
|
+
return 1;
|
|
138
|
+
}
|
|
139
|
+
process.stdout.write(
|
|
140
|
+
`✅ ${METRIC} in sync — registry (${count}) == budget == committed record.\n`,
|
|
141
|
+
);
|
|
142
|
+
return 0;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function _isCliEntry(): boolean {
|
|
146
|
+
if (process.argv[1] === undefined) {
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
|
|
150
|
+
if (import.meta.url === argvUrl) {
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
// Symlinked invocations (installed projections, macOS /var → /private/var)
|
|
154
|
+
// make the raw URLs differ — compare realpaths so the entry guard fires.
|
|
155
|
+
try {
|
|
156
|
+
const here = fs.realpathSync(fileURLToPath(import.meta.url));
|
|
157
|
+
const argvPath = fs.realpathSync(path.resolve(process.argv[1]));
|
|
158
|
+
return here === argvPath;
|
|
159
|
+
} catch {
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (_isCliEntry() || process.argv[1] === _HERE) {
|
|
165
|
+
process.exit(main());
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export { collect_findings, count_registry_entries, main };
|
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
* - All headings from source present in condensed
|
|
15
15
|
* - All code blocks preserved exactly
|
|
16
16
|
* - YAML frontmatter identical
|
|
17
|
-
* - Word count reduction
|
|
17
|
+
* - Word count: excessive-reduction alarm only (> 60% lost warns; expected
|
|
18
|
+
* reduction is 0% post-ADR-201 verbatim copy — see the note near the check)
|
|
18
19
|
* - Iron Law sections (## Iron Law / ### Iron Law / ## The Iron Law /
|
|
19
20
|
* Iron Laws / numbered) preserved per `preservation-guard`: heading
|
|
20
21
|
* verbatim at original level, structural-unit survival.
|