@event4u/agent-config 9.13.0 → 9.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +48 -0
  3. package/README.md +1 -1
  4. package/dist/agent-src/commands/optimize/deep.md +147 -0
  5. package/dist/agent-src/commands/optimize.md +12 -6
  6. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  7. package/dist/cli-delegate/{chunk-QFKQLQ7X.js → chunk-GCVFLBP2.js} +399 -365
  8. package/dist/cli-delegate/cmd_export.js +1 -1
  9. package/dist/cli-delegate/cmd_prune.js +1 -1
  10. package/dist/cli-delegate/cmd_settings_migrate.js +1 -1
  11. package/dist/cli-delegate/cmd_sync.js +1 -1
  12. package/dist/cli-delegate/cmd_uninstall.js +1 -1
  13. package/dist/cli-delegate/cmd_validate.js +1 -1
  14. package/dist/discovery/deprecation-report.md +1 -1
  15. package/dist/discovery/discovery-manifest.json +39 -11
  16. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  17. package/dist/discovery/discovery-manifest.summary.md +3 -3
  18. package/dist/discovery/orphan-report.md +1 -1
  19. package/dist/discovery/packs.json +6 -5
  20. package/dist/discovery/trust-report.md +3 -3
  21. package/dist/discovery/workspaces.json +5 -4
  22. package/dist/install/install.mjs +648 -614
  23. package/dist/mcp/registry-manifest.json +2 -2
  24. package/dist/mcp/server.json +2 -2
  25. package/dist/mcp/server.mjs +46 -13
  26. package/package.json +1 -1
  27. package/src/scripts/_lib/exec_evidence.ts +5 -0
  28. package/src/scripts/_lib/scoped_projection.ts +212 -0
  29. package/src/scripts/check_artefact_count_messaging.ts +128 -15
  30. package/src/scripts/count_scoped_projection.ts +104 -0
  31. package/src/scripts/install.ts +13 -90
  32. package/src/scripts/skill_linter.ts +81 -14
  33. package/src/scripts/update_counts.ts +90 -11
@@ -74,6 +74,7 @@ import type * as YamlModule from 'yaml';
74
74
  import { build_merge_entries } from './_lib/json_pointers.js';
75
75
  import { is_claude_builtin_name } from './_lib/claude_builtin_names.js';
76
76
  import * as installed_lock from './_lib/installed_lock.js';
77
+ import * as scoped_projection from './_lib/scoped_projection.js';
77
78
  import * as surface_tiers from './_lib/surface_tiers.js';
78
79
  import * as global_deploy_inventory from './_lib/global_deploy_inventory.js';
79
80
  import * as installed_tools from './_lib/installed_tools.js';
@@ -3344,89 +3345,16 @@ function _prune_lab_modules(
3344
3345
  // `requires` graph). Mirrors the core-only lab prune above — same
3345
3346
  // `_prune_modules_by` mechanics, a different predicate.
3346
3347
 
3347
- /** `workspaces` values whose packs are ALWAYS active under `scoped` mode. */
3348
- const SCOPED_ACTIVE_WORKSPACES: ReadonlySet<string> = new Set(['engineering', 'agent-config-maintainer']);
3349
-
3350
- interface PackRecord {
3351
- id: string;
3352
- workspaces: string[];
3353
- requires: string[];
3354
- }
3355
-
3356
- /**
3357
- * Parse `src/config/discovery/packs.yml` into `{id, workspaces, requires}`
3358
- * records. `requires` falls back to the legacy `requires_hint` field name
3359
- * when `requires` is absent.
3360
- *
3361
- * Deliberately THROWS on a missing file or a parse result that is not a
3362
- * list — an empty active-pack set would be the LEAST safe fallback here
3363
- * (it would prune every tagged artefact, the opposite of the fail-safe
3364
- * contract). The caller wraps this in a try/catch that restores the full
3365
- * tree on any error instead.
3366
- */
3367
- function _load_packs_registry(package_root: string): PackRecord[] {
3368
- const vocab_path = path.join(package_root, 'src', 'config', 'discovery', 'packs.yml');
3369
- const data = yamlSafeLoad(readText(vocab_path));
3370
- if (!Array.isArray(data)) {
3371
- throw new Error(`packs.yml did not parse to a list: ${vocab_path}`);
3372
- }
3373
- const out: PackRecord[] = [];
3374
- for (const entry of data) {
3375
- if (!_isPlainObject(entry)) continue;
3376
- const id = entry['id'];
3377
- if (typeof id !== 'string' || id === '') continue;
3378
- const workspaces_raw = entry['workspaces'];
3379
- const workspaces = Array.isArray(workspaces_raw)
3380
- ? workspaces_raw.filter((w): w is string => typeof w === 'string')
3381
- : [];
3382
- const requires_raw = entry['requires'] ?? entry['requires_hint'];
3383
- const requires = Array.isArray(requires_raw)
3384
- ? requires_raw.filter((r): r is string => typeof r === 'string')
3385
- : [];
3386
- out.push({ id, workspaces, requires });
3387
- }
3388
- return out;
3389
- }
3390
-
3391
- /**
3392
- * Active pack id set for `scoped` mode: every pack whose `workspaces`
3393
- * intersects `SCOPED_ACTIVE_WORKSPACES`, unioned with `runtime_active_packs`,
3394
- * expanded over the `requires` graph (transitive closure).
3348
+ /*
3349
+ * The scoped-projection predicate lives in `_lib/scoped_projection.ts` so the
3350
+ * installer and `count_scoped_projection.ts` — which produces the number the
3351
+ * claims ledger publishes — run the SAME code rather than the same rule
3352
+ * written twice. Re-exported under the existing private names to keep the
3353
+ * prune tests' import surface unchanged.
3395
3354
  */
3396
- function _compute_active_pack_ids(
3397
- packs: readonly PackRecord[],
3398
- runtime_active_packs: readonly string[],
3399
- ): Set<string> {
3400
- const by_id = new Map<string, PackRecord>();
3401
- for (const p of packs) by_id.set(p.id, p);
3402
-
3403
- const active = new Set<string>();
3404
- for (const p of packs) {
3405
- if (p.workspaces.some((w) => SCOPED_ACTIVE_WORKSPACES.has(w))) {
3406
- active.add(p.id);
3407
- }
3408
- }
3409
- for (const id of runtime_active_packs) {
3410
- active.add(id);
3411
- }
3412
-
3413
- let frontier = [...active];
3414
- while (frontier.length > 0) {
3415
- const next: string[] = [];
3416
- for (const id of frontier) {
3417
- const rec = by_id.get(id);
3418
- if (rec === undefined) continue;
3419
- for (const dep of rec.requires) {
3420
- if (!active.has(dep)) {
3421
- active.add(dep);
3422
- next.push(dep);
3423
- }
3424
- }
3425
- }
3426
- frontier = next;
3427
- }
3428
- return active;
3429
- }
3355
+ type PackRecord = scoped_projection.PackRecord;
3356
+ const _load_packs_registry = scoped_projection.load_packs_registry;
3357
+ const _compute_active_pack_ids = scoped_projection.compute_active_pack_ids;
3430
3358
 
3431
3359
  /**
3432
3360
  * Read the GLOBAL settings document that governs a global deploy, if one
@@ -3574,14 +3502,9 @@ function _prune_scoped_modules(
3574
3502
  deploy_results: Record<string, DeployResult>,
3575
3503
  active_ids: ReadonlySet<string>,
3576
3504
  ): [number, Record<string, DeployResult>] {
3577
- return _prune_modules_by(deploy_results, (p) => {
3578
- const packs = surface_tiers.frontmatter_packs(p);
3579
- if (packs.size === 0) return false;
3580
- for (const id of packs) {
3581
- if (active_ids.has(id)) return false;
3582
- }
3583
- return true;
3584
- });
3505
+ return _prune_modules_by(deploy_results, (p) =>
3506
+ scoped_projection.is_pruned_under_scoped(p, active_ids),
3507
+ );
3585
3508
  }
3586
3509
 
3587
3510
  function install_global(
@@ -2439,6 +2439,16 @@ export function gather_candidate_files_under(srcRoot: string): string[] {
2439
2439
  */
2440
2440
  export let last_changed_scope_reason = '';
2441
2441
 
2442
+ /**
2443
+ * Whether the base-span diff (`base...HEAD`) itself ran cleanly.
2444
+ *
2445
+ * The verified-empty release outcome (see `main`) may only rest on an empty
2446
+ * change set that was actually COMPUTED — a base diff that errored (missing
2447
+ * tag object, corrupt ref) degrades to the worktree fallbacks and would
2448
+ * otherwise be indistinguishable from a genuinely empty span.
2449
+ */
2450
+ export let last_changed_base_diff_ok = false;
2451
+
2442
2452
  /**
2443
2453
  * Candidate files changed against the resolved base.
2444
2454
  *
@@ -2454,6 +2464,7 @@ export let last_changed_scope_reason = '';
2454
2464
  export function gather_changed_candidate_files(root: string, since?: string | null): string[] {
2455
2465
  const scope = resolveContentLintScope({ cwd: root, since: since ?? null });
2456
2466
  last_changed_scope_reason = scope.reason;
2467
+ last_changed_base_diff_ok = false;
2457
2468
  const diffCommands = [
2458
2469
  ['git', 'diff', '--name-only', `${scope.base}...HEAD`],
2459
2470
  ['git', 'diff', '--name-only', '--cached', 'HEAD'],
@@ -2461,10 +2472,15 @@ export function gather_changed_candidate_files(root: string, since?: string | nu
2461
2472
  ];
2462
2473
  try {
2463
2474
  let rawLines: string[] = [];
2475
+ let first = true;
2464
2476
  for (const cmd of diffCommands) {
2465
2477
  // A truncated diff would silently shrink the change set — the same
2466
2478
  // degrade-to-zero shape this roadmap closes elsewhere.
2467
2479
  const result = runCountedProbe(cmd[0] as string, cmd.slice(1), { cwd: root });
2480
+ if (first) {
2481
+ last_changed_base_diff_ok = result.ok;
2482
+ first = false;
2483
+ }
2468
2484
  if (result.ok && result.stdout.trim()) {
2469
2485
  rawLines = splitlines(result.stdout);
2470
2486
  break;
@@ -4202,6 +4218,43 @@ function _release_scope_requires_findings(root: string, since?: string | null):
4202
4218
  return scope.isRelease && scope.previousTag !== null;
4203
4219
  }
4204
4220
 
4221
+ /**
4222
+ * True when the `--changed` corpus filter still matches tracked files at HEAD.
4223
+ *
4224
+ * The discriminator between a DEAD scope and a VERIFIED-EMPTY release span.
4225
+ * A release that touches no skill/rule file is a legitimate outcome — but only
4226
+ * provably so when the exact predicate `gather_changed_candidate_files`
4227
+ * applies to diff lines demonstrably still sees the corpus. Probing
4228
+ * `git ls-files` with that same predicate means a moved projection root
4229
+ * (the ADR-051 blindness class) fails this probe exactly as it would blind
4230
+ * the diff — so "empty because the root moved" cannot masquerade as
4231
+ * "empty because nothing changed".
4232
+ */
4233
+ function _changed_filter_matches_head(root: string): boolean {
4234
+ // Probes the canonical projection root only (ADR-051): a corpus that
4235
+ // exists solely under a legacy path counts as dead, not alive.
4236
+ const result = runCountedProbe('git', ['ls-files', '--', 'dist/agent-src'], { cwd: root });
4237
+ if (!result.ok) {
4238
+ return false;
4239
+ }
4240
+ for (const raw of splitlines(result.stdout)) {
4241
+ const norm = raw.trim().replace(/\\/g, '/');
4242
+ if (!norm || norm.includes('/evals/')) {
4243
+ continue;
4244
+ }
4245
+ if (!norm.startsWith('dist/agent-src/') && !norm.includes('/dist/agent-src/')) {
4246
+ continue;
4247
+ }
4248
+ if (
4249
+ norm.endsWith('/SKILL.md') ||
4250
+ (norm.endsWith('.md') && (norm.includes('/rules/') || norm.includes('/commands/')))
4251
+ ) {
4252
+ return true;
4253
+ }
4254
+ }
4255
+ return false;
4256
+ }
4257
+
4205
4258
  export function main(argv: string[]): number {
4206
4259
  let args: Args;
4207
4260
  try {
@@ -4244,25 +4297,39 @@ export function main(argv: string[]): number {
4244
4297
  process.stdout.write(`${format_json([])}\n`);
4245
4298
  }
4246
4299
  // A release PR that resolved a real tag span and still matched
4247
- // nothing has not passed — it examined nothing. Mirrors the
4248
- // scan-scope regime the other gates already adopted
4300
+ // nothing needs a discriminator: DEAD scope (the diff errored, or
4301
+ // the corpus filter no longer matches anything at HEAD — the
4302
+ // ADR-051 moved-root blindness) fails; VERIFIED-EMPTY (the base
4303
+ // diff ran cleanly AND the filter provably still sees tracked
4304
+ // corpus files) is a legitimate release that simply changed no
4305
+ // skill/rule — failing it would red every scripts-/docs-only
4306
+ // release forever. Mirrors the scan-scope regime
4249
4307
  // (`_lib/scan_scope.ts`; exit 2, as `check_iron_law_prominence`).
4250
4308
  // Ordinary PRs keep the historical exit 0: a change set with no
4251
4309
  // skill or rule in it is a legitimate empty scope, not a dead one.
4252
4310
  if (args.changed && _release_scope_requires_findings(root, args.since)) {
4253
- try {
4254
- assertScanned({
4255
- gate: 'skill_linter --changed (release PR)',
4256
- scanned: 0,
4257
- units: 'skill/rule files',
4258
- roots: [last_changed_scope_reason],
4259
- });
4260
- } catch (exc) {
4261
- if (exc instanceof DeadScopeError) {
4262
- process.stderr.write(`❌ ${exc.message}\n`);
4263
- return 2;
4311
+ if (last_changed_base_diff_ok && _changed_filter_matches_head(root)) {
4312
+ process.stderr.write(
4313
+ '✅ verified-empty release scope: the base-span diff ran cleanly and ' +
4314
+ 'the corpus filter still matches tracked files at HEAD — this ' +
4315
+ 'release genuinely changes no skill/rule file. The gate is alive, ' +
4316
+ 'not blind.\n',
4317
+ );
4318
+ } else {
4319
+ try {
4320
+ assertScanned({
4321
+ gate: 'skill_linter --changed (release PR)',
4322
+ scanned: 0,
4323
+ units: 'skill/rule files',
4324
+ roots: [last_changed_scope_reason],
4325
+ });
4326
+ } catch (exc) {
4327
+ if (exc instanceof DeadScopeError) {
4328
+ process.stderr.write(`❌ ${exc.message}\n`);
4329
+ return 2;
4330
+ }
4331
+ throw exc;
4264
4332
  }
4265
- throw exc;
4266
4333
  }
4267
4334
  }
4268
4335
  process.stderr.write('No matching skill/rule files found.\n');
@@ -26,6 +26,8 @@ import process from 'node:process';
26
26
  import { fileURLToPath, pathToFileURL } from 'node:url';
27
27
 
28
28
  import { artefact_roots, iter_commands } from './_lib/agent_src.js';
29
+ import { scoped_projection_stats } from './_lib/scoped_projection.js';
30
+ import { canonical_counts } from './check_command_count_messaging.js';
29
31
 
30
32
  const _HERE = fileURLToPath(import.meta.url);
31
33
  // _HERE === <repo>/src/scripts/update_counts.ts ; the retired Python implementation derives
@@ -77,7 +79,48 @@ function _exists(p: string): boolean {
77
79
  }
78
80
  }
79
81
 
82
+ /**
83
+ * Every `SKILL.md` the canonical `count('skills')` counts, as absolute paths.
84
+ *
85
+ * Exported so `count_scoped_projection.ts` partitions the SAME file set the
86
+ * ledger's total is derived from. That is what makes the published pair
87
+ * ("N of M skills") coherent by construction: `projected + pruned === M`
88
+ * cannot drift, because there is one walk, not two.
89
+ */
90
+ export function* iter_skills(): Generator<string> {
91
+ const seen = new Set<string>();
92
+ for (const root of artefact_roots()) {
93
+ const subdir = path.join(root, 'skills');
94
+ if (!_exists(subdir)) {
95
+ continue;
96
+ }
97
+ for (const f of _rglob(subdir, (n) => n === 'SKILL.md')) {
98
+ const rel = path.relative(root, f).split(path.sep).join('/');
99
+ if (seen.has(rel)) continue;
100
+ seen.add(rel);
101
+ yield f;
102
+ }
103
+ }
104
+ }
105
+
80
106
  export function count(kind: string): number {
107
+ if (kind === 'commands_active') {
108
+ // Commands MINUS deprecation shims — the number the hero badge and the
109
+ // "Browse all N active commands" line carry, and the one prose
110
+ // mentions resolve to. Distinct from `commands` (raw file count):
111
+ // they are equal only while the shim set is empty, so anchoring an
112
+ // active-count position to `commands` would rewrite it to the wrong
113
+ // number the moment a command is superseded.
114
+ const [, , active] = canonical_counts();
115
+ return active;
116
+ }
117
+ if (kind === 'skills_scoped') {
118
+ // Skills a DEFAULT `projection.mode: scoped` install deploys. Derived
119
+ // by the installer's own prune predicate over the same walk the
120
+ // canonical `skills` total uses, so the published pair
121
+ // ("N of M skills") cannot drift apart. See count_scoped_projection.ts.
122
+ return scoped_projection_stats(REPO_ROOT, iter_skills()).projected;
123
+ }
81
124
  if (kind === 'router_rules') {
82
125
  // Routed rule entries in the compiled router (kernel + tier_1 +
83
126
  // tier_2). Read from dist/router.json — the artifact hosts consume —
@@ -119,19 +162,19 @@ export function count(kind: string): number {
119
162
  }
120
163
  return total;
121
164
  }
165
+ if (kind === 'skills') {
166
+ // Single walk, shared with count_scoped_projection.ts — see iter_skills.
167
+ for (const _ of iter_skills()) {
168
+ total += 1;
169
+ }
170
+ return total;
171
+ }
122
172
  for (const root of artefact_roots()) {
123
173
  const subdir = path.join(root, kind);
124
174
  if (!_exists(subdir)) {
125
175
  continue;
126
176
  }
127
- if (kind === 'skills') {
128
- for (const f of _rglob(subdir, (n) => n === 'SKILL.md')) {
129
- const rel = path.relative(root, f).split(path.sep).join('/');
130
- if (seen.has(rel)) continue;
131
- seen.add(rel);
132
- total += 1;
133
- }
134
- } else if (kind === 'personas') {
177
+ if (kind === 'personas') {
135
178
  // personas live as flat .md files, README excluded
136
179
  for (const f of _glob(subdir, (n) => n.endsWith('.md'))) {
137
180
  if (path.basename(f) === 'README.md') continue;
@@ -164,6 +207,12 @@ export const TARGETS: ReadonlyArray<[string, ReadonlyArray<[string, string]>]> =
164
207
  // badge is owned by check_command_count_messaging.
165
208
  ['(/badge/Skills-)(\\d+)(-)', 'skills'],
166
209
  ['(/badge/Rules-)(\\d+)(-)', 'rules'],
210
+ // The Commands badge WAS left to check_command_count_messaging
211
+ // alone ("avoids double-ownership"). Nothing generated it, so it
212
+ // drifted to 191 against a canonical 192 and only the checker
213
+ // noticed — the exact gap this roadmap closes. Generator writes,
214
+ // checker verifies: that IS the ownership split, not a conflict.
215
+ ['(/badge/Commands-)(\\d+)(-)', 'commands_active'],
167
216
  ['(/badge/Guidelines-)(\\d+)(-)', 'guidelines'],
168
217
  ['(/badge/Personas-)(\\d+)(-)', 'personas'],
169
218
  ],
@@ -177,8 +226,22 @@ export const TARGETS: ReadonlyArray<[string, ReadonlyArray<[string, string]>]> =
177
226
  ['(- claim: )(\\d+)( skills\\.)', 'skills'],
178
227
  ['(- claim: )(\\d+)( commands\\.)', 'commands'],
179
228
  ['(- claim: )(\\d+)( governed rules\\.)', 'rules'],
229
+ // The scoped-projection claim: BOTH halves of "N of M skills".
230
+ // `M` was scanner-checked but generator-blind (a maintainer
231
+ // hand-edited it on every skill addition); `N` was guarded by
232
+ // nothing at all and had drifted 2 from the benchmark doc the
233
+ // claim names as its own method.
234
+ ['(installs ships )(\\d+)( of )', 'skills_scoped'],
235
+ ['( of )(\\d+)( skills \\(untagged core)', 'skills'],
236
+ // "14 of 111 governed rules" — the total was scanner-checked and
237
+ // generator-blind for the same reason.
238
+ ['( of )(\\d+)( governed rules \\()', 'rules'],
180
239
  ],
181
240
  ],
241
+ [
242
+ 'docs/command-flows.md',
243
+ [['(\\*\\*)(\\d+)( commands\\*\\*)', 'commands_active']],
244
+ ],
182
245
  [
183
246
  'docs/getting-started-by-role.md',
184
247
  [
@@ -206,6 +269,11 @@ export const TARGETS: ReadonlyArray<[string, ReadonlyArray<[string, string]>]> =
206
269
  'docs/getting-started.md',
207
270
  [
208
271
  ['(automatically by )(\\d+)( rules)', 'rules'],
272
+ // Was deliberately unsynced to avoid double-ownership with
273
+ // check_command_count_messaging; it drifted to 191 as a result.
274
+ // Now generated from `commands_active` — the SAME canonical the
275
+ // checker compares against, so the two cannot disagree.
276
+ ['(Browse all )(\\d+)( active commands)', 'commands_active'],
209
277
  // NOTE: the "Browse all N active commands" line here carries the
210
278
  // *active* command count and is owned by
211
279
  // check_command_count_messaging.py — intentionally not synced
@@ -254,7 +322,16 @@ export function apply_to_text(
254
322
  return [new_text, drifts];
255
323
  }
256
324
 
257
- const KINDS = ['skills', 'rules', 'commands', 'guidelines', 'personas', 'router_rules'] as const;
325
+ const KINDS = [
326
+ 'skills',
327
+ 'skills_scoped',
328
+ 'rules',
329
+ 'commands',
330
+ 'commands_active',
331
+ 'guidelines',
332
+ 'personas',
333
+ 'router_rules',
334
+ ] as const;
258
335
 
259
336
  function parse_args(argv: readonly string[]): { check: boolean } {
260
337
  const args = { check: false };
@@ -282,8 +359,10 @@ export function main(argv: readonly string[] = process.argv.slice(2)): number {
282
359
  counts[k] = count(k);
283
360
  }
284
361
  process.stdout.write(
285
- `📊 Truth: skills=${counts['skills']} rules=${counts['rules']} ` +
286
- `commands=${counts['commands']} guidelines=${counts['guidelines']} ` +
362
+ `📊 Truth: skills=${counts['skills']} skills_scoped=${counts['skills_scoped']} ` +
363
+ `rules=${counts['rules']} commands=${counts['commands']} ` +
364
+ `commands_active=${counts['commands_active']} ` +
365
+ `guidelines=${counts['guidelines']} ` +
287
366
  `personas=${counts['personas']} router_rules=${counts['router_rules']}\n`,
288
367
  );
289
368