@event4u/agent-config 14.7.0 → 14.8.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 (138) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +245 -388
  3. package/README.md +1 -1
  4. package/dist/agent-src/commands/evals/git-pr-merge.json +13 -0
  5. package/dist/agent-src/commands/evals/roadmap.json +17 -1
  6. package/dist/agent-src/commands/pr/create.md +14 -2
  7. package/dist/agent-src/commands/pr/merge.md +379 -0
  8. package/dist/agent-src/commands/roadmap/process-full.md +157 -3
  9. package/dist/agent-src/commands/roadmap.md +1 -1
  10. package/dist/agent-src/contexts/execution/auto-dispatch-classification.md +46 -36
  11. package/dist/agent-src/contexts/execution/autonomy-mechanics.md +37 -2
  12. package/dist/agent-src/contexts/execution/roadmap-execution-contract.md +64 -2
  13. package/dist/agent-src/contexts/execution/roadmap-process-loop.md +154 -2
  14. package/dist/agent-src/rules/artifact-drafting-protocol.md +5 -1
  15. package/dist/agent-src/rules/decision-revisit-gate.md +65 -33
  16. package/dist/agent-src/rules/recurring-criticism.md +1 -1
  17. package/dist/agent-src/rules/roadmap-progress-sync.md +19 -0
  18. package/dist/agent-src/rules/source-confidentiality.md +1 -1
  19. package/dist/agent-src/scripts/archive_completed_roadmaps.ts +7 -4
  20. package/dist/agent-src/scripts/dashboard_mode.ts +212 -0
  21. package/dist/agent-src/scripts/update_roadmap_progress.ts +47 -48
  22. package/dist/agent-src/skills/adr-create/SKILL.md +124 -5
  23. package/dist/agent-src/skills/decision-record/SKILL.md +61 -3
  24. package/dist/agent-src/skills/decision-review/SKILL.md +65 -0
  25. package/dist/agent-src/skills/merge-conflicts/SKILL.md +26 -0
  26. package/dist/agent-src/skills/security/SKILL.md +8 -0
  27. package/dist/agent-src/skills/security-audit/SKILL.md +12 -1
  28. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  29. package/dist/agent-src/templates/roadmaps.md +20 -2
  30. package/dist/cli/registry.js +1 -0
  31. package/dist/cli/registry.js.map +1 -1
  32. package/dist/cli-delegate/{chunk-JZ4NR2TW.js → chunk-7PRE6ZV2.js} +1 -1
  33. package/dist/cli-delegate/{chunk-7F3VYSM2.js → chunk-EEJUMCDZ.js} +14 -14
  34. package/dist/cli-delegate/{chunk-74E3TKWV.js → chunk-H4VIVD4C.js} +5 -5
  35. package/dist/cli-delegate/chunk-I3EIN2YG.js +36 -0
  36. package/dist/cli-delegate/{chunk-4VR4ORQ6.js → chunk-LPPWO5CE.js} +4 -3
  37. package/dist/cli-delegate/{chunk-UEZ7D35A.js → chunk-N7CDDGI3.js} +4 -23
  38. package/dist/cli-delegate/cmd_adr_effective.js +1162 -0
  39. package/dist/cli-delegate/cmd_conformance.js +14 -14
  40. package/dist/cli-delegate/cmd_converge.js +3 -3
  41. package/dist/cli-delegate/cmd_doctor.js +7 -7
  42. package/dist/cli-delegate/cmd_export.js +6 -6
  43. package/dist/cli-delegate/cmd_handoff.js +7 -6
  44. package/dist/cli-delegate/cmd_memory_get.js +2 -1
  45. package/dist/cli-delegate/cmd_migrate.js +2 -2
  46. package/dist/cli-delegate/cmd_preflight.js +5 -5
  47. package/dist/cli-delegate/cmd_prune.js +6 -6
  48. package/dist/cli-delegate/cmd_route_audit.js +6 -5
  49. package/dist/cli-delegate/cmd_route_explain.js +3 -3
  50. package/dist/cli-delegate/cmd_session_recycle.js +3 -3
  51. package/dist/cli-delegate/cmd_settings_get.js +3 -3
  52. package/dist/cli-delegate/cmd_settings_migrate.js +6 -6
  53. package/dist/cli-delegate/cmd_sync.js +6 -6
  54. package/dist/cli-delegate/cmd_uninstall.js +6 -6
  55. package/dist/cli-delegate/cmd_update.js +3 -3
  56. package/dist/cli-delegate/cmd_upgrade.js +3 -3
  57. package/dist/cli-delegate/cmd_validate.js +6 -6
  58. package/dist/cli-delegate/cmd_versions.js +3 -3
  59. package/dist/discovery/deprecation-report.md +1 -1
  60. package/dist/discovery/discovery-manifest.json +53 -22
  61. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  62. package/dist/discovery/discovery-manifest.summary.md +3 -3
  63. package/dist/discovery/dormancy-report.md +1 -1
  64. package/dist/discovery/orphan-report.md +1 -1
  65. package/dist/discovery/packs.json +7 -6
  66. package/dist/discovery/trust-report.md +2 -2
  67. package/dist/discovery/workspaces.json +6 -5
  68. package/dist/hooks/dispatch.js +806 -681
  69. package/dist/install/globalRuleLayers.js +169 -0
  70. package/dist/install/globalRuleLayers.js.map +1 -0
  71. package/dist/install/partitionEligibility.js +156 -0
  72. package/dist/install/partitionEligibility.js.map +1 -1
  73. package/dist/install/ruleLayerPartition.js +157 -0
  74. package/dist/install/ruleLayerPartition.js.map +1 -0
  75. package/dist/mcp/registry-manifest.json +3 -3
  76. package/dist/mcp/server.json +2 -2
  77. package/dist/mcp/server.mjs +654 -578
  78. package/dist/scripts/_lib/prune_empty_dirs.js +85 -0
  79. package/dist/scripts/_lib/prune_empty_dirs.js.map +1 -0
  80. package/dist/scripts/condense.js +52 -89
  81. package/dist/scripts/condense.js.map +1 -1
  82. package/docs/guidelines/agent-infra/artifact-drafting-protocol-mechanics.md +45 -1
  83. package/docs/guidelines/agent-infra/false-green.md +47 -1
  84. package/package.json +1 -1
  85. package/src/agent-src/scripts/archive_completed_roadmaps.ts +7 -4
  86. package/src/agent-src/scripts/dashboard_mode.ts +212 -0
  87. package/src/agent-src/scripts/update_roadmap_progress.ts +47 -48
  88. package/src/config/ci-local-parity.yml +21 -0
  89. package/src/config/estate-count-budget.json +275 -2
  90. package/src/config/evaluator-budgets.json +3 -3
  91. package/src/config/gate-coverage.yml +220 -12
  92. package/src/config/gate-violation-baselines.json +73 -63
  93. package/src/config/gitignore-block.txt +15 -2
  94. package/src/config/hook-latency-budget.json +5 -0
  95. package/src/config/rule-stub-ceilings.json +17 -3
  96. package/src/install/globalRuleLayers.ts +197 -0
  97. package/src/install/partitionEligibility.ts +177 -0
  98. package/src/install/ruleLayerPartition.ts +212 -0
  99. package/src/scripts/_cli/cmd_adr_effective.ts +484 -0
  100. package/src/scripts/_dispatch.bash +11 -0
  101. package/src/scripts/_lib/adr_frontmatter.test.ts +365 -0
  102. package/src/scripts/_lib/adr_frontmatter.ts +459 -0
  103. package/src/scripts/_lib/envelope_grounding.ts +11 -1
  104. package/src/scripts/_lib/layer_overlap_notice.ts +11 -2
  105. package/src/scripts/_lib/prune_empty_dirs.ts +80 -0
  106. package/src/scripts/adr/evidence_census.ts +899 -0
  107. package/src/scripts/adr/regenerate_index.ts +12 -32
  108. package/src/scripts/adr_cite_check.ts +540 -39
  109. package/src/scripts/audit_adr_coverage.ts +103 -17
  110. package/src/scripts/bench_hook_latency.ts +204 -0
  111. package/src/scripts/build_archive_index.ts +34 -0
  112. package/src/scripts/check_adr_frontmatter.ts +574 -39
  113. package/src/scripts/check_hook_bundle_content.ts +219 -0
  114. package/src/scripts/check_new_adr_evidence.ts +529 -0
  115. package/src/scripts/check_references.ts +18 -0
  116. package/src/scripts/check_rule_layer_partition.ts +531 -0
  117. package/src/scripts/check_single_delivery.ts +137 -4
  118. package/src/scripts/check_standing_rule_delivery.ts +8 -2
  119. package/src/scripts/condense.ts +61 -91
  120. package/src/scripts/external_sources_denylist.json +4 -1
  121. package/src/scripts/hooks/block_unauthorized_git.ts +22 -2
  122. package/src/scripts/hooks/dispatch_hook.ts +154 -187
  123. package/src/scripts/hooks/dispatch_issues.ts +38 -19
  124. package/src/scripts/hooks/fallback_yaml.ts +66 -0
  125. package/src/scripts/hooks/host_semantics.ts +21 -0
  126. package/src/scripts/hooks/injection_budget.ts +13 -3
  127. package/src/scripts/hooks/py_json_dumps.ts +103 -0
  128. package/src/scripts/hooks/state_io.ts +88 -1
  129. package/src/scripts/hooks/stdin_failure_policy.ts +96 -0
  130. package/src/scripts/injection_scan_hook.ts +137 -5
  131. package/src/scripts/lint_hook_manifest.ts +49 -0
  132. package/src/scripts/lint_provenance_vocabulary.ts +472 -4
  133. package/src/scripts/routing_doctor.ts +5 -1
  134. package/src/scripts/sync_pr_branch.ts +163 -20
  135. package/dist/cli-delegate/{chunk-PPQO4NGR.js → chunk-6RJA2F5A.js} +3 -3
  136. package/dist/cli-delegate/{chunk-6EOCNQAM.js → chunk-E4XIZDIB.js} +0 -0
  137. package/dist/cli-delegate/{chunk-FBKL6JWT.js → chunk-MMSGVZKV.js} +3 -3
  138. package/dist/cli-delegate/{chunk-GJ5BVHXM.js → chunk-PMEKGDED.js} +3 -3
@@ -76,9 +76,9 @@ import { reportScanned } from './_lib/scan_scope.js';
76
76
 
77
77
  // ledger-exempt: this gate has no skip semantics to account for. `_lib/gate_ledger`
78
78
  // exists so a gate that PASSES OVER targets says which and why; here the target set
79
- // is four fixed artefact types, every one of them appears in the output, and a type
79
+ // is six fixed artefact types, every one of them appears in the output, and a type
80
80
  // whose layer is missing is printed as `absent` rather than quietly dropped — see
81
- // the `absent` field and the `types_compared=N of 4` ledger line, which together
81
+ // the `absent` field and the `types_compared=N of 6` ledger line, which together
82
82
  // already carry what a per-target ledger would. Adding one would restate the render
83
83
  // in a second format, and the gate's own docstring warns that the degenerate pass
84
84
  // here is a marker with no argument behind it. If a future version gains a real skip
@@ -88,10 +88,51 @@ import { reportScanned } from './_lib/scan_scope.js';
88
88
  const _HERE = fileURLToPath(import.meta.url);
89
89
  const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
90
90
 
91
- /** Artefact types the host loads from both layers. */
92
- const TYPES = ['rules', 'skills', 'commands', 'agents'] as const;
91
+ /**
92
+ * Artefact types the host loads from both layers.
93
+ *
94
+ * ## The identity key, named because it is not the same for every row
95
+ *
96
+ * Every row below compares BASENAMES within one family directory. For `rules`,
97
+ * `personas`, `user-types` and `agents` that IS the host's unit, so basename
98
+ * equality is the right key. For `commands` it is not: the host-visible unit is
99
+ * the invocation name `/cluster:sub`, which the colon form encodes in the PATH
100
+ * (`commands/<cluster>/<sub>.md`), so this gate's `commands` row compares cluster
101
+ * DIRECTORY names — a coarser key than the thing the host registers.
102
+ *
103
+ * That gap is recorded rather than closed, and the reason is a measurement: since
104
+ * `commandsWithheld` gates the colon-form generator, a machine with a verified
105
+ * host layer projects ZERO commands, so there is no project-side name for the
106
+ * coarse key to be wrong about. On a machine WITHOUT one, the project layer is
107
+ * the only layer and there is nothing to compare. The key is therefore imprecise
108
+ * exactly where it has no work to do. Closing it would mean reading each command
109
+ * file's own slug — real work for a row whose overlap is 0 in both topologies.
110
+ *
111
+ * `personas` and `user-types` were added 2026-08-21 (closure correction). See
112
+ * `unknownProjectFamilies` for what now prevents the next omission.
113
+ */
114
+ const TYPES = ['rules', 'skills', 'commands', 'personas', 'user-types', 'agents'] as const;
93
115
  type ArtefactType = (typeof TYPES)[number];
94
116
 
117
+ /**
118
+ * Directory names under `<repo>/.claude/` that the HOST writes, not a generator.
119
+ *
120
+ * `unknownProjectFamilies` used to rest on the premise that `<repo>/.claude/` is
121
+ * "written by `condense.ts` generators and nothing else". That premise is false
122
+ * and the gate proved it on 2026-08-22, refusing a release push over
123
+ * `.claude/worktrees/` — the maintainer's git-worktree root, gitignored at
124
+ * `.gitignore:246`, named as a conventional worktree location by
125
+ * `worktree_cleanup_check.STANDARD_WORKTREE_DIRS`, and excluded from
126
+ * `inventory_abstraction_budget`. Nothing about it is an artefact family, so
127
+ * "add it to TYPES" was advice that could not be taken.
128
+ *
129
+ * The set is hand-written and stays independent of any producer registry, for
130
+ * the same reason `TYPES` is: a verifier that derives its scope from a producer
131
+ * inherits that producer's omissions. It grows only on tree evidence that a name
132
+ * is host state — never to silence a family a generator actually emits.
133
+ */
134
+ const HOST_OWNED = new Set(['worktrees']);
135
+
95
136
  /**
96
137
  * What a layer's entries physically ARE — symlinks, directories, or regular files.
97
138
  *
@@ -273,6 +314,54 @@ export interface Verdict {
273
314
  namesRead: number;
274
315
  /** True when no type had BOTH layers present — nothing was actually compared. */
275
316
  readNothing: boolean;
317
+ /**
318
+ * Family directories present in the PROJECT layer that `TYPES` does not name.
319
+ *
320
+ * The forcing function that makes this gate's own 2026-08-21 omission
321
+ * non-recurring: `personas` was written by a generator, shipped by the
322
+ * installer, and named by neither measurer, so it was invisible rather than
323
+ * reported — a gate cannot report a family it was never told exists.
324
+ *
325
+ * Only the PROJECT layer is scanned, and that is the whole reason this can be
326
+ * a hard refusal rather than a warning. Every entry in `<repo>/.claude/` is a
327
+ * family this repository chose to emit, EXCEPT the host-written names in
328
+ * `HOST_OWNED` — see that constant for why the unqualified version of this
329
+ * sentence was wrong. `~/.claude/` is the host's own directory —
330
+ * `plugins`, `projects`, `sessions`, `shell-snapshots`, `telemetry` and a
331
+ * dozen more live there — and treating an unrecognised name in it as a defect
332
+ * would be a false-positive generator, not a check.
333
+ */
334
+ unknownFamilies: string[];
335
+ }
336
+
337
+ /**
338
+ * Family directories the project layer holds that `TYPES` does not name.
339
+ *
340
+ * Deliberately NOT derived from the installer's `_CLAUDE_SKILL_BUNDLE` or from
341
+ * `check_generator_output_coverage`'s registry: a verifier whose scope comes from
342
+ * a producer inherits that producer's omissions, which is the exact defect this
343
+ * function exists to catch. `TYPES` stays hand-written and independent; this
344
+ * compares it against the filesystem the generators actually produced and refuses
345
+ * on disagreement.
346
+ */
347
+ function unknownProjectFamilies(projectRoot: string): string[] {
348
+ const known = new Set<string>(TYPES);
349
+ let entries: fs.Dirent[];
350
+ try {
351
+ entries = fs.readdirSync(projectRoot, { withFileTypes: true });
352
+ } catch {
353
+ return []; // no project layer at all — nothing emitted, nothing to miss
354
+ }
355
+ return entries
356
+ .filter(
357
+ (e) =>
358
+ e.isDirectory() &&
359
+ !e.name.startsWith('.') &&
360
+ !known.has(e.name) &&
361
+ !HOST_OWNED.has(e.name),
362
+ )
363
+ .map((e) => e.name)
364
+ .sort();
276
365
  }
277
366
 
278
367
  export function evaluate(globalRoot: string, projectRoot: string): Verdict {
@@ -299,6 +388,7 @@ export function evaluate(globalRoot: string, projectRoot: string): Verdict {
299
388
  0,
300
389
  ),
301
390
  readNothing: compared === 0,
391
+ unknownFamilies: unknownProjectFamilies(projectRoot),
302
392
  };
303
393
  }
304
394
 
@@ -354,12 +444,24 @@ export function render(v: Verdict, globalRoot: string, projectRoot: string): str
354
444
  out.push('');
355
445
  out.push(` layers not present (nothing to compare there): ${v.absent.join(', ')}`);
356
446
  }
447
+ if (v.unknownFamilies.length > 0) {
448
+ out.push('');
449
+ out.push(
450
+ ` ❌ unknown family in the project layer: ${v.unknownFamilies.join(', ')}` +
451
+ ' — add it to TYPES in this gate AND in _lib/layer_overlap_notice.ts,',
452
+ );
453
+ out.push(
454
+ ' or the next family shipped from two layers is invisible the way `personas`' +
455
+ ' was until 2026-08-21.',
456
+ );
457
+ }
357
458
  out.push('');
358
459
  out.push(
359
460
  `check_single_delivery ledger: duplicated=${v.duplicated}` +
360
461
  ` name_overlap_different_shape=${v.nameOverlapDifferentShape}` +
361
462
  ` scope_defeat=${v.defeated}` +
362
463
  ` types_compared=${v.typesCompared} of ${v.readings.length}` +
464
+ ` unknown_families=${v.unknownFamilies.length}` +
363
465
  ` names_read=${v.namesRead}`,
364
466
  );
365
467
  return out.join('\n');
@@ -459,6 +561,37 @@ export function main(argv?: readonly string[]): number {
459
561
  ' case (layers present but never paired) is refused separately.',
460
562
  });
461
563
 
564
+ // Refuses in both modes, unlike every other branch here — but ONLY over this
565
+ // repository's own generated tree, and that qualification was added by a
566
+ // neutral review (2026-08-21) rather than being there from the start.
567
+ //
568
+ // The asymmetry is the argument for the refusal: every other verdict here is
569
+ // a property of ONE MACHINE's two layers, which is why `--enforce` is opt-in.
570
+ // "this repository emits a family no measurer names" is true on every checkout
571
+ // and fixable in one line, so gating it behind `--enforce` would put the one
572
+ // topology-independent check behind the flag meant for topology-dependent
573
+ // ones.
574
+ //
575
+ // The qualification is the review's, and it is right: an unrecognised
576
+ // DIRECTORY does not by itself establish generator ownership. It does under
577
+ // `<repo>/.claude/`, which `condense.ts` writes and nothing else does. It does
578
+ // NOT under an arbitrary `--project` path, which may be a consumer tree with
579
+ // a host-native or project-only family that legitimately cannot overlap — and
580
+ // refusing there would make a nominal report exit 1 on a correct topology.
581
+ // So a repointed root REPORTS and does not refuse.
582
+ if (v.unknownFamilies.length > 0) {
583
+ const own = projectRoot === path.join(REPO_ROOT, '.claude');
584
+ process.stdout.write(
585
+ `${own ? '❌' : '⚠️ '} check_single_delivery: unknown family in the project` +
586
+ ` layer: ${v.unknownFamilies.join(', ')}. Add measurement before it can ship` +
587
+ ' — TYPES in this gate and in _lib/layer_overlap_notice.ts.' +
588
+ (own ? '' : ' Reported, not enforced: --project is repointed, so generator' +
589
+ ' ownership of this tree is not established.') +
590
+ '\n',
591
+ );
592
+ if (own) return 1;
593
+ }
594
+
462
595
  if (v.readNothing) {
463
596
  // Never a pass. A gate that compared nothing has not verified anything,
464
597
  // and saying "invariant holds" here is the false-green shape this repo
@@ -326,8 +326,14 @@ export function main(argv: readonly string[] = process.argv.slice(2)): number {
326
326
  if (pct > 1) {
327
327
  process.stderr.write(
328
328
  `❌ ${PROG}: ${total} tok of standing rule prose exceeds the ${budget.total_cap_tokens} cap. `
329
- + 'If two layers overlap, `agent-config install --layer=<global|project>` suppresses one '
330
- + 'without deleting a file; otherwise scope rules with `paths:` or move bodies into the digest.\n',
329
+ + 'If two layers overlap, that is the ADR-236 partition not yet armed on this '
330
+ + 'machine: run `agent-config install` so the host layer is fingerprinted in '
331
+ + 'installed.lock, and the next `task generate-tools` withholds the globally-owned '
332
+ + 'artefacts instead of writing a second copy. (Superseded advice: this line used to '
333
+ + 'name `install --layer=<global|project>`, the layer-suppression remedy ADR-226 '
334
+ + 'declined and ADR-236 replaced — a reader following it could not fix the '
335
+ + 'condition.) Otherwise the total is body length, not duplication: scope rules with '
336
+ + '`paths:` or move bodies into the digest.\n',
331
337
  );
332
338
  return 1;
333
339
  }
@@ -263,7 +263,9 @@ export {
263
263
  type ClaudePathsPlan,
264
264
  } from '../install/claudePathsPlan.js';
265
265
  import { rule_in_scope } from '../install/ruleInScope.js';
266
- import { isExclusivelyPackageOnly, partitionActive, setPartitionAnnounce } from '../install/partitionEligibility.js'; // ADR-236
266
+ import { pruneEmptyDirs } from './_lib/prune_empty_dirs.js';
267
+ import { commandsWithheld, partitionActive, personaPartition, setPartitionAnnounce } from '../install/partitionEligibility.js'; // ADR-236
268
+ import { dedupableRules, partitionRulesForDir } from '../install/ruleLayerPartition.js'; // ADR-236, per-host evidence
267
269
  import { _claude_paths_plan, derive_trigger_globs } from '../install/claudePathsPlan.js';
268
270
 
269
271
  const _HERE = path.dirname(fileURLToPath(import.meta.url)); // <repo>/src/scripts
@@ -439,15 +441,6 @@ function _read_augment_rules_use_symlinks(): boolean {
439
441
  return false;
440
442
  }
441
443
 
442
- /**
443
- * Per-tool user-scope rule directory, for the scope-dedup below. Only tools
444
- * whose host reads a user-scope rules directory can have a redundant twin;
445
- * everything else has nothing to de-duplicate against.
446
- */
447
- const USER_SCOPE_RULE_DIRS: Readonly<Record<string, string>> = {
448
- '.claude/rules': path.join('.claude', 'rules'),
449
- };
450
-
451
444
  function _read_projection_scope_dedup(): boolean {
452
445
  const data = load_agent_settings({ project_path: MODULE_STATE.SETTINGS_FILE });
453
446
  const projection = data['projection'];
@@ -464,60 +457,6 @@ function _read_projection_scope_dedup(): boolean {
464
457
  return false;
465
458
  }
466
459
 
467
- /**
468
- * Rules whose user-scope twin is BYTE-IDENTICAL to the source, i.e. safe to
469
- * skip at project scope: the host still loads the same text, once instead of
470
- * twice, so nothing the model sees changes.
471
- *
472
- * Byte-identity is the whole safety argument and is not negotiable. Keying on
473
- * the filename alone would silently let a stale globally-installed copy win
474
- * whenever the two scopes hold different versions — which is the NORMAL state
475
- * while developing the package (measured: 110/110 shared filenames differing in
476
- * bytes). This is deliberately not the thin-projection mechanism: no rule
477
- * becomes trigger-gated, so the quality floor that disabled thin projection
478
- * does not apply here.
479
- */
480
- function _dedupable_rules(tool_dir: string, rules: readonly string[], userHome: string): Set<string> {
481
- const relative = USER_SCOPE_RULE_DIRS[tool_dir];
482
- if (relative === undefined) {
483
- return new Set();
484
- }
485
- // A hostile or simply absent $HOME must make the dedup inert, not
486
- // adventurous. In a container `$HOME` is often unset (so `homedir()` can
487
- // resolve to `/`) or world-writable, and this function decides which rules
488
- // to STOP emitting — reading an unexpected tree there is how a projection
489
- // silently loses a rule. Council review of PR #1055 raised exactly this.
490
- let userDirStat: fs.Stats;
491
- const userDir = path.join(userHome, relative);
492
- try {
493
- userDirStat = fs.statSync(userDir);
494
- } catch {
495
- return new Set();
496
- }
497
- if (!userDirStat.isDirectory()) {
498
- return new Set();
499
- }
500
- // World-writable user scope: anyone on the box could plant a byte-identical
501
- // twin and thereby delete a rule from the project projection. Refuse.
502
- if ((userDirStat.mode & 0o002) !== 0) {
503
- _print(` ⚠️ ${tool_dir}: user-scope rules dir is world-writable — scope-dedup skipped`);
504
- return new Set();
505
- }
506
- const skip = new Set<string>();
507
- for (const rule of rules) {
508
- const twin = path.join(userDir, rule);
509
- const source = path.join(MODULE_STATE.RULES_SOURCE, rule);
510
- try {
511
- if (!_isFile(twin) || !_isFile(source)) continue;
512
- if (fs.readFileSync(twin).equals(fs.readFileSync(source))) {
513
- skip.add(rule);
514
- }
515
- } catch {
516
- // An unreadable twin is simply not de-duplicable — emit the project copy.
517
- }
518
- }
519
- return skip;
520
- }
521
460
 
522
461
  function _lean_projection_mode(): string {
523
462
  const data = load_agent_settings({ project_path: MODULE_STATE.SETTINGS_FILE });
@@ -1132,11 +1071,22 @@ function _scoped_rule_basenames(): string[] {
1132
1071
  // is its fourth consumer, after the router compiler, the dist writer, and
1133
1072
  // check_sync. Same predicate in all four; that is the point.
1134
1073
  .filter((p) => !skip_compile_disabled_rule(`rules/${path.basename(p)}`))
1135
- .filter((p) => !partitionActive(MODULE_STATE.PROJECT_ROOT) || isExclusivelyPackageOnly(p))
1136
1074
  .map((p) => path.basename(p))
1137
1075
  .sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
1138
1076
  }
1139
1077
 
1078
+ /** Adapter over {@link partitionRulesForDir}; that module carries the reasoning. */
1079
+ export function partition_rules_for_dir(
1080
+ tool_dir: string,
1081
+ rules: readonly string[],
1082
+ user_home: string = process.env['HOME'] ?? os.homedir(),
1083
+ ): string[] {
1084
+ return partitionRulesForDir({
1085
+ toolDir: tool_dir, rules, userHome: user_home, announce: _print,
1086
+ projectRoot: MODULE_STATE.PROJECT_ROOT, rulesSource: MODULE_STATE.RULES_SOURCE,
1087
+ });
1088
+ }
1089
+
1140
1090
  /**
1141
1091
  * The emit plan of {@link generate_rule_symlinks}: active tool dir → the exact
1142
1092
  * rule basenames it projects there.
@@ -1161,8 +1111,12 @@ export function projected_rule_trees(
1161
1111
  const dedup_on = _read_projection_scope_dedup();
1162
1112
  const out: Record<string, string[]> = {};
1163
1113
  for (const tool_dir of Object.keys(_filter_tool_dirs(TOOL_DIRS))) {
1164
- const skip = dedup_on ? _dedupable_rules(tool_dir, rules, user_home) : new Set<string>();
1165
- out[tool_dir] = rules.filter((r) => !skip.has(r));
1114
+ const skip = dedup_on
1115
+ ? dedupableRules({ toolDir: tool_dir, rules, userHome: user_home,
1116
+ rulesSource: MODULE_STATE.RULES_SOURCE, announce: _print })
1117
+ : new Set<string>();
1118
+ const kept = rules.filter((r) => !skip.has(r));
1119
+ out[tool_dir] = partition_rules_for_dir(tool_dir, kept, user_home);
1166
1120
  }
1167
1121
  return out;
1168
1122
  }
@@ -1281,7 +1235,10 @@ export function generate_rule_symlinks(): number {
1281
1235
  }
1282
1236
 
1283
1237
  export function generate_windsurfrules(): number {
1284
- const rules = _scoped_rule_basenames();
1238
+ // Windsurf's LEGACY single-file surface — same host, so the same evidence, keyed
1239
+ // on `.windsurf/rules`. Without this line removing the per-run filter regressed
1240
+ // it 13 → 113: the per-run filter had been covering it by accident.
1241
+ const rules = partition_rules_for_dir('.windsurf/rules', _scoped_rule_basenames());
1285
1242
  const parts = ['# Auto-generated from dist/agent-src/rules/ — do not edit directly\n'];
1286
1243
  for (const rule of rules) {
1287
1244
  const p = path.join(MODULE_STATE.RULES_SOURCE, rule);
@@ -1451,12 +1408,16 @@ function _clean_modern_dir(target_dir: string, valid_names: ReadonlySet<string>)
1451
1408
  export function generate_cursor_mdc_rules(): number {
1452
1409
  const scope = _read_rule_workspaces();
1453
1410
  const pack_scope = _read_rule_packs();
1454
- const rules = _rglobSorted(MODULE_STATE.RULES_SOURCE, '*.md')
1411
+ let rules = _rglobSorted(MODULE_STATE.RULES_SOURCE, '*.md')
1455
1412
  .filter((p) => _isFile(p))
1456
1413
  .filter((p) => rule_in_scope(p, scope, pack_scope))
1457
1414
  // ADR-004 manual rules are reference-only — never auto-injected into a
1458
1415
  // per-tool tree. Same predicate as generate_rule_symlinks.
1459
1416
  .filter((p) => !_is_manual_rule(p));
1417
+ // ADR-236 per-host partition (src/install/ruleLayerPartition.ts). Narrowing the
1418
+ // list is a REMOVAL, because `_clean_modern_dir` below sweeps to `valid`.
1419
+ const kept = new Set(partition_rules_for_dir('.cursor/rules', rules.map((r) => path.basename(r))));
1420
+ rules = rules.filter((r) => kept.has(path.basename(r)));
1460
1421
  const stems = rules.map((r) => path.basename(r, '.md'));
1461
1422
  const valid = new Set([
1462
1423
  ...stems.map((s) => `${s}.mdc`),
@@ -1473,12 +1434,15 @@ export function generate_cursor_mdc_rules(): number {
1473
1434
  export function generate_windsurf_modern_rules(): number {
1474
1435
  const scope = _read_rule_workspaces();
1475
1436
  const pack_scope = _read_rule_packs();
1476
- const rules = _rglobSorted(MODULE_STATE.RULES_SOURCE, '*.md')
1437
+ let rules = _rglobSorted(MODULE_STATE.RULES_SOURCE, '*.md')
1477
1438
  .filter((p) => _isFile(p))
1478
1439
  .filter((p) => rule_in_scope(p, scope, pack_scope))
1479
1440
  // ADR-004 manual rules are reference-only — never auto-injected into a
1480
1441
  // per-tool tree. Same predicate as generate_rule_symlinks.
1481
1442
  .filter((p) => !_is_manual_rule(p));
1443
+ // ADR-236 per-host partition — same shape as the cursor emitter above.
1444
+ const kept = new Set(partition_rules_for_dir('.windsurf/rules', rules.map((r) => path.basename(r))));
1445
+ rules = rules.filter((r) => kept.has(path.basename(r)));
1482
1446
  const valid = new Set(rules.map((r) => path.basename(r)));
1483
1447
  _clean_modern_dir(_WINDSURF_RULES_DIR(), valid);
1484
1448
  for (const rule of rules) {
@@ -1781,18 +1745,16 @@ function _nested_command_subpath(source_file: string): string | null {
1781
1745
  /**
1782
1746
  * Project-scope `.claude/commands/<cluster>/<sub>.md` — the colon form.
1783
1747
  *
1784
- * Before this, the package repo's own `.claude/` carried NO commands
1785
- * directory: every command reached Claude Code as a hyphen-named skill
1786
- * wrapper in `.claude/skills/`, while the colon form existed only in the
1787
- * user-global tree written by `install.ts`. Every clustered command was
1788
- * therefore listed TWICE for anyone with a global install
1789
- * (`roadmap-process-full` AND `roadmap:process-full`), costing 4,214 GPT tok
1790
- * of always-loaded catalog for zero added reach — measured 2026-08-02.
1748
+ * Before this, the package repo's own `.claude/` carried NO commands directory:
1749
+ * every command reached Claude Code as a hyphen-named skill wrapper in
1750
+ * `.claude/skills/`, while the colon form existed only in the user-global tree
1751
+ * written by `install.ts`. Every clustered command was therefore listed TWICE
1752
+ * for anyone with a global install (`roadmap-process-full` AND
1753
+ * `roadmap:process-full`), costing 4,214 GPT tok of catalog — measured 2026-08-02.
1791
1754
  *
1792
1755
  * Emitting the nested form here lets `generate_claude_commands` stop wrapping
1793
1756
  * the clustered commands: one listing, same reachability, and reachability no
1794
- * longer depends on a global deploy having run. Claude Code dedupes project
1795
- * and user scope by name, so the two copies of `/cluster:sub` collapse.
1757
+ * longer depends on a global deploy. Dedup AND precedence: `commandsWithheld`.
1796
1758
  *
1797
1759
  * ADR-003 (colon canonical for clusters) and ADR-044 (flat commands stay
1798
1760
  * hyphenated) both hold: flat commands are untouched and keep their wrapper.
@@ -1800,6 +1762,8 @@ function _nested_command_subpath(source_file: string): string | null {
1800
1762
  export function generate_claude_project_commands(
1801
1763
  active_command_slugs: ReadonlySet<string> | null = null,
1802
1764
  ): number {
1765
+ if (commandsWithheld(MODULE_STATE.PROJECT_ROOT)) active_command_slugs = new Set<string>();
1766
+ // Returns before the stale sweep — invariant, see `commandsWithheld`.
1803
1767
  if (!_isDir(path.join(MODULE_STATE.PROJECT_ROOT, 'src', 'domains'))) {
1804
1768
  return 0;
1805
1769
  }
@@ -1816,8 +1780,7 @@ export function generate_claude_project_commands(
1816
1780
 
1817
1781
  const target_root = _CLAUDE_COMMANDS_DIR();
1818
1782
  const valid = new Set(nested.map(([, sub]) => `${sub}.md`));
1819
- // Sweep stale links first: a renamed or deleted command must not linger as
1820
- // a dangling `/cluster:sub`.
1783
+ // Sweep stale links first — a renamed, deleted or WITHHELD command must not linger.
1821
1784
  if (_exists(target_root)) {
1822
1785
  for (const item of _rglobSorted(target_root, '*.md')) {
1823
1786
  const rel = _relativeToPosix(item, target_root);
@@ -1825,6 +1788,7 @@ export function generate_claude_project_commands(
1825
1788
  fs.unlinkSync(item);
1826
1789
  }
1827
1790
  }
1791
+ pruneEmptyDirs(target_root); // else the gate counts 40 empty cluster dirs as 40 commands
1828
1792
  }
1829
1793
 
1830
1794
  let count = 0;
@@ -2297,7 +2261,7 @@ export function generate_persona_symlinks(): number {
2297
2261
  .map((p) => path.basename(p))
2298
2262
  .sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
2299
2263
  void personas;
2300
- const list = direct;
2264
+ const partition = personaPartition(MODULE_STATE.PROJECT_ROOT, direct);
2301
2265
  const tool_dirs = _filter_tool_dirs(PERSONA_TOOL_DIRS);
2302
2266
  let total = 0;
2303
2267
  for (const [tool_dir, rel_prefix] of Object.entries(tool_dirs)) {
@@ -2305,11 +2269,11 @@ export function generate_persona_symlinks(): number {
2305
2269
  _mkdirp(target_dir);
2306
2270
  for (const item of _iterdirSorted(target_dir)) {
2307
2271
  const name = path.basename(item);
2308
- if (_isSymlink(item) && !list.includes(name) && name !== 'README.md') {
2272
+ if (_isSymlink(item) && !partition.listFor(tool_dir).includes(name) && name !== 'README.md') {
2309
2273
  fs.unlinkSync(item);
2310
2274
  }
2311
2275
  }
2312
- for (const persona of list) {
2276
+ for (const persona of partition.listFor(tool_dir)) {
2313
2277
  const link = path.join(target_dir, persona);
2314
2278
  const target = path.join(rel_prefix, persona);
2315
2279
  if (_existsOrSymlink(link)) {
@@ -2319,9 +2283,8 @@ export function generate_persona_symlinks(): number {
2319
2283
  total += 1;
2320
2284
  }
2321
2285
  }
2322
- info(
2323
- ` ✅ Created ${total} persona symlinks across ${Object.keys(tool_dirs).length} tool directories (${list.length} personas each)`,
2324
- );
2286
+ const per_dir = Object.keys(tool_dirs).map((d) => `${d}=${partition.countFor(d)}`);
2287
+ info(` ✅ Created ${total} persona symlinks — ${per_dir.join(' · ')}${partition.note}`);
2325
2288
  return total;
2326
2289
  }
2327
2290
 
@@ -2521,10 +2484,17 @@ export function project_to_augment(): void {
2521
2484
  const current = new Set<string>();
2522
2485
  let written = 0;
2523
2486
  if (_exists(src_rules)) {
2524
- const rules = _iterdirSorted(src_rules)
2525
- .filter((p) => p.endsWith('.md') && _isFile(p))
2526
- .map((p) => path.basename(p))
2527
- .sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
2487
+ // ADR-236 per-host partition, ahead of the `use_symlinks` branch so both
2488
+ // writers narrow; the `existing` minus `current` sweep below makes it a
2489
+ // removal. This emitter never had the scope or ADR-004 manual filters
2490
+ // either — why its count is 15 and the symlink trees' 13. Pre-existing.
2491
+ const rules = partition_rules_for_dir(
2492
+ '.augment/rules',
2493
+ _iterdirSorted(src_rules)
2494
+ .filter((p) => p.endsWith('.md') && _isFile(p))
2495
+ .map((p) => path.basename(p))
2496
+ .sort((a, b) => (a < b ? -1 : a > b ? 1 : 0)),
2497
+ );
2528
2498
  for (const rule of rules) {
2529
2499
  const target = path.join(dst_rules, rule);
2530
2500
  if (_isSymlink(target) || _existsOrSymlink(target)) {
@@ -62,7 +62,10 @@
62
62
  "\\bagent-reach\\b",
63
63
  "panniantong",
64
64
  "virgiliojr94",
65
- "book-to-skill"
65
+ "book-to-skill",
66
+ "project-codeguard",
67
+ "\\bcodeguard\\b",
68
+ "\\bcosai\\b"
66
69
  ],
67
70
  "skip_paths": [
68
71
  "src/scripts/check_no_external_sources.py",
@@ -485,8 +485,28 @@ export function commandOp(command: string): GitOp | null {
485
485
  return found;
486
486
  }
487
487
 
488
- /** A ledger older than this is not "this turn" under any reading. */
489
- export const LEDGER_MAX_AGE_MS = 6 * 60 * 60 * 1000; // TEMP: PR-drain run, revert after
488
+ /**
489
+ * A ledger older than this is not "this turn" under any reading.
490
+ *
491
+ * **Widening this constant for a long run is forbidden practice.** On
492
+ * 2026-08-21 it was sed-patched to twelve times this value — six hours —
493
+ * behind a marker promising a revert that never came, so an autonomous PR-drain could
494
+ * merge past the 30-minute bound. The widening was committed to the trunk and
495
+ * left there. It is a twelvefold expansion of the authorization lifetime on
496
+ * the guard that gates `pr-merge`, which is a `BLOCK_OPS` member precisely
497
+ * because it is irreversible.
498
+ *
499
+ * Neither the widened expression nor the marker text is written out here: the
500
+ * obvious regression check for either is a grep, and a comment reciting the
501
+ * literal makes that grep match the guard's own prose on a clean tree.
502
+ *
503
+ * The supported answer to "my run is longer than the window" is that the run
504
+ * STOPS and REPORTS at expiry and the user re-authorizes — never that the
505
+ * window grows. The agent never edits this constant, this file, or the built
506
+ * bundle; `dist/hooks/dispatch.js` is what actually executes, and a source
507
+ * edit without a rebuild is silently inert.
508
+ */
509
+ export const LEDGER_MAX_AGE_MS = 30 * 60 * 1000;
490
510
 
491
511
  /**
492
512
  * Load the ledger, and refuse one that does not belong to THIS session and