@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.
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +245 -388
- package/README.md +1 -1
- package/dist/agent-src/commands/evals/git-pr-merge.json +13 -0
- package/dist/agent-src/commands/evals/roadmap.json +17 -1
- package/dist/agent-src/commands/pr/create.md +14 -2
- package/dist/agent-src/commands/pr/merge.md +379 -0
- package/dist/agent-src/commands/roadmap/process-full.md +157 -3
- package/dist/agent-src/commands/roadmap.md +1 -1
- package/dist/agent-src/contexts/execution/auto-dispatch-classification.md +46 -36
- package/dist/agent-src/contexts/execution/autonomy-mechanics.md +37 -2
- package/dist/agent-src/contexts/execution/roadmap-execution-contract.md +64 -2
- package/dist/agent-src/contexts/execution/roadmap-process-loop.md +154 -2
- package/dist/agent-src/rules/artifact-drafting-protocol.md +5 -1
- package/dist/agent-src/rules/decision-revisit-gate.md +65 -33
- package/dist/agent-src/rules/recurring-criticism.md +1 -1
- package/dist/agent-src/rules/roadmap-progress-sync.md +19 -0
- package/dist/agent-src/rules/source-confidentiality.md +1 -1
- package/dist/agent-src/scripts/archive_completed_roadmaps.ts +7 -4
- package/dist/agent-src/scripts/dashboard_mode.ts +212 -0
- package/dist/agent-src/scripts/update_roadmap_progress.ts +47 -48
- package/dist/agent-src/skills/adr-create/SKILL.md +124 -5
- package/dist/agent-src/skills/decision-record/SKILL.md +61 -3
- package/dist/agent-src/skills/decision-review/SKILL.md +65 -0
- package/dist/agent-src/skills/merge-conflicts/SKILL.md +26 -0
- package/dist/agent-src/skills/security/SKILL.md +8 -0
- package/dist/agent-src/skills/security-audit/SKILL.md +12 -1
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/roadmaps.md +20 -2
- package/dist/cli/registry.js +1 -0
- package/dist/cli/registry.js.map +1 -1
- package/dist/cli-delegate/{chunk-JZ4NR2TW.js → chunk-7PRE6ZV2.js} +1 -1
- package/dist/cli-delegate/{chunk-7F3VYSM2.js → chunk-EEJUMCDZ.js} +14 -14
- package/dist/cli-delegate/{chunk-74E3TKWV.js → chunk-H4VIVD4C.js} +5 -5
- package/dist/cli-delegate/chunk-I3EIN2YG.js +36 -0
- package/dist/cli-delegate/{chunk-4VR4ORQ6.js → chunk-LPPWO5CE.js} +4 -3
- package/dist/cli-delegate/{chunk-UEZ7D35A.js → chunk-N7CDDGI3.js} +4 -23
- package/dist/cli-delegate/cmd_adr_effective.js +1162 -0
- package/dist/cli-delegate/cmd_conformance.js +14 -14
- package/dist/cli-delegate/cmd_converge.js +3 -3
- package/dist/cli-delegate/cmd_doctor.js +7 -7
- package/dist/cli-delegate/cmd_export.js +6 -6
- package/dist/cli-delegate/cmd_handoff.js +7 -6
- package/dist/cli-delegate/cmd_memory_get.js +2 -1
- package/dist/cli-delegate/cmd_migrate.js +2 -2
- package/dist/cli-delegate/cmd_preflight.js +5 -5
- package/dist/cli-delegate/cmd_prune.js +6 -6
- package/dist/cli-delegate/cmd_route_audit.js +6 -5
- package/dist/cli-delegate/cmd_route_explain.js +3 -3
- package/dist/cli-delegate/cmd_session_recycle.js +3 -3
- package/dist/cli-delegate/cmd_settings_get.js +3 -3
- package/dist/cli-delegate/cmd_settings_migrate.js +6 -6
- package/dist/cli-delegate/cmd_sync.js +6 -6
- package/dist/cli-delegate/cmd_uninstall.js +6 -6
- 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 +6 -6
- package/dist/cli-delegate/cmd_versions.js +3 -3
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +53 -22
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +3 -3
- package/dist/discovery/dormancy-report.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +7 -6
- package/dist/discovery/trust-report.md +2 -2
- package/dist/discovery/workspaces.json +6 -5
- package/dist/hooks/dispatch.js +806 -681
- package/dist/install/globalRuleLayers.js +169 -0
- package/dist/install/globalRuleLayers.js.map +1 -0
- package/dist/install/partitionEligibility.js +156 -0
- package/dist/install/partitionEligibility.js.map +1 -1
- package/dist/install/ruleLayerPartition.js +157 -0
- package/dist/install/ruleLayerPartition.js.map +1 -0
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +654 -578
- package/dist/scripts/_lib/prune_empty_dirs.js +85 -0
- package/dist/scripts/_lib/prune_empty_dirs.js.map +1 -0
- package/dist/scripts/condense.js +52 -89
- package/dist/scripts/condense.js.map +1 -1
- package/docs/guidelines/agent-infra/artifact-drafting-protocol-mechanics.md +45 -1
- package/docs/guidelines/agent-infra/false-green.md +47 -1
- package/package.json +1 -1
- package/src/agent-src/scripts/archive_completed_roadmaps.ts +7 -4
- package/src/agent-src/scripts/dashboard_mode.ts +212 -0
- package/src/agent-src/scripts/update_roadmap_progress.ts +47 -48
- package/src/config/ci-local-parity.yml +21 -0
- package/src/config/estate-count-budget.json +275 -2
- package/src/config/evaluator-budgets.json +3 -3
- package/src/config/gate-coverage.yml +220 -12
- package/src/config/gate-violation-baselines.json +73 -63
- package/src/config/gitignore-block.txt +15 -2
- package/src/config/hook-latency-budget.json +5 -0
- package/src/config/rule-stub-ceilings.json +17 -3
- package/src/install/globalRuleLayers.ts +197 -0
- package/src/install/partitionEligibility.ts +177 -0
- package/src/install/ruleLayerPartition.ts +212 -0
- package/src/scripts/_cli/cmd_adr_effective.ts +484 -0
- package/src/scripts/_dispatch.bash +11 -0
- package/src/scripts/_lib/adr_frontmatter.test.ts +365 -0
- package/src/scripts/_lib/adr_frontmatter.ts +459 -0
- package/src/scripts/_lib/envelope_grounding.ts +11 -1
- package/src/scripts/_lib/layer_overlap_notice.ts +11 -2
- package/src/scripts/_lib/prune_empty_dirs.ts +80 -0
- package/src/scripts/adr/evidence_census.ts +899 -0
- package/src/scripts/adr/regenerate_index.ts +12 -32
- package/src/scripts/adr_cite_check.ts +540 -39
- package/src/scripts/audit_adr_coverage.ts +103 -17
- package/src/scripts/bench_hook_latency.ts +204 -0
- package/src/scripts/build_archive_index.ts +34 -0
- package/src/scripts/check_adr_frontmatter.ts +574 -39
- package/src/scripts/check_hook_bundle_content.ts +219 -0
- package/src/scripts/check_new_adr_evidence.ts +529 -0
- package/src/scripts/check_references.ts +18 -0
- package/src/scripts/check_rule_layer_partition.ts +531 -0
- package/src/scripts/check_single_delivery.ts +137 -4
- package/src/scripts/check_standing_rule_delivery.ts +8 -2
- package/src/scripts/condense.ts +61 -91
- package/src/scripts/external_sources_denylist.json +4 -1
- package/src/scripts/hooks/block_unauthorized_git.ts +22 -2
- package/src/scripts/hooks/dispatch_hook.ts +154 -187
- package/src/scripts/hooks/dispatch_issues.ts +38 -19
- package/src/scripts/hooks/fallback_yaml.ts +66 -0
- package/src/scripts/hooks/host_semantics.ts +21 -0
- package/src/scripts/hooks/injection_budget.ts +13 -3
- package/src/scripts/hooks/py_json_dumps.ts +103 -0
- package/src/scripts/hooks/state_io.ts +88 -1
- package/src/scripts/hooks/stdin_failure_policy.ts +96 -0
- package/src/scripts/injection_scan_hook.ts +137 -5
- package/src/scripts/lint_hook_manifest.ts +49 -0
- package/src/scripts/lint_provenance_vocabulary.ts +472 -4
- package/src/scripts/routing_doctor.ts +5 -1
- package/src/scripts/sync_pr_branch.ts +163 -20
- package/dist/cli-delegate/{chunk-PPQO4NGR.js → chunk-6RJA2F5A.js} +3 -3
- package/dist/cli-delegate/{chunk-6EOCNQAM.js → chunk-E4XIZDIB.js} +0 -0
- package/dist/cli-delegate/{chunk-FBKL6JWT.js → chunk-MMSGVZKV.js} +3 -3
- package/dist/cli-delegate/{chunk-GJ5BVHXM.js → chunk-PMEKGDED.js} +3 -3
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Remove empty directories under a root, deepest first.
|
|
3
|
+
*
|
|
4
|
+
* ## Why this exists as a shared helper
|
|
5
|
+
*
|
|
6
|
+
* A generator that stops writing into a tree leaves the tree's DIRECTORIES
|
|
7
|
+
* behind. Measured 2026-08-21 while extending the ADR-236 partition to the
|
|
8
|
+
* colon-form commands: after the generator was gated, `.claude/commands/`
|
|
9
|
+
* held **0 `.md` files and 40 empty cluster directories** — and
|
|
10
|
+
* `check_single_delivery` counts directory NAMES, so it still reported 40
|
|
11
|
+
* overlapping commands against a layer that delivered none. A partition that
|
|
12
|
+
* stops the writes and leaves the shape is invisible to the gate that is
|
|
13
|
+
* supposed to confirm it.
|
|
14
|
+
*
|
|
15
|
+
* `condense.ts` already carried this loop inline for the skills tree. It is
|
|
16
|
+
* lifted here rather than copied because the second caller is what turns an
|
|
17
|
+
* inline block into a shape two producers must agree on — and because
|
|
18
|
+
* `condense.ts` sits above `check_source_size_budget`'s ceiling, where a
|
|
19
|
+
* shrink-only ratchet refuses even a small addition.
|
|
20
|
+
*
|
|
21
|
+
* Best-effort by construction: a race or a permission error on one directory is
|
|
22
|
+
* swallowed, matching the inline original. Pruning is cleanup, never a gate.
|
|
23
|
+
*
|
|
24
|
+
* ## Symlinks: MEASURED, because a neutral review split on it
|
|
25
|
+
*
|
|
26
|
+
* One seat called an unguarded recursion a merge blocker (a directory symlink
|
|
27
|
+
* pointing at an ancestor would loop forever); the other called the finding
|
|
28
|
+
* false, on the grounds that `Dirent.isDirectory()` does not follow the target.
|
|
29
|
+
* A vote cannot settle that, so it was probed — `readdirSync(dir, {withFileTypes:
|
|
30
|
+
* true})` over a real symlink-to-directory on darwin, Node 26:
|
|
31
|
+
*
|
|
32
|
+
* link isDirectory=false isSymbolicLink=true
|
|
33
|
+
* real isDirectory=true isSymbolicLink=false
|
|
34
|
+
*
|
|
35
|
+
* `Dirent` carries `lstat` semantics, so the walk below cannot descend into a
|
|
36
|
+
* symlink at all and no loop is reachable — and no inode set is needed. The
|
|
37
|
+
* second seat was right. Recorded rather than fixed, so the next reader does not
|
|
38
|
+
* re-raise it: a defensive `!e.isSymbolicLink()` here would be dead code
|
|
39
|
+
* asserting something the type already guarantees.
|
|
40
|
+
*
|
|
41
|
+
* The same property is why this is safe on a tree of symlinks: `.claude/commands`
|
|
42
|
+
* holds symlinks into `src/domains/**`, and pruning must not follow one out of
|
|
43
|
+
* the tree it was pointed at.
|
|
44
|
+
*/
|
|
45
|
+
import * as fs from 'node:fs';
|
|
46
|
+
import * as path from 'node:path';
|
|
47
|
+
/** @returns the number of directories removed. */
|
|
48
|
+
export function pruneEmptyDirs(root) {
|
|
49
|
+
let removed = 0;
|
|
50
|
+
const walk = (dir) => {
|
|
51
|
+
let entries;
|
|
52
|
+
try {
|
|
53
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
for (const e of entries) {
|
|
59
|
+
if (e.isDirectory())
|
|
60
|
+
walk(path.join(dir, e.name));
|
|
61
|
+
}
|
|
62
|
+
try {
|
|
63
|
+
if (fs.readdirSync(dir).length === 0) {
|
|
64
|
+
fs.rmdirSync(dir);
|
|
65
|
+
removed += 1;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
/* race or permission — cleanup is best-effort, mirrors the inline original */
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
let top;
|
|
73
|
+
try {
|
|
74
|
+
top = fs.readdirSync(root, { withFileTypes: true });
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return 0;
|
|
78
|
+
}
|
|
79
|
+
for (const e of top) {
|
|
80
|
+
if (e.isDirectory())
|
|
81
|
+
walk(path.join(root, e.name));
|
|
82
|
+
}
|
|
83
|
+
return removed;
|
|
84
|
+
}
|
|
85
|
+
//# sourceMappingURL=prune_empty_dirs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prune_empty_dirs.js","sourceRoot":"","sources":["../../../src/scripts/_lib/prune_empty_dirs.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AACH,OAAO,KAAK,EAAE,MAAM,SAAS,CAAC;AAC9B,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAElC,kDAAkD;AAClD,MAAM,UAAU,cAAc,CAAC,IAAY;IACvC,IAAI,OAAO,GAAG,CAAC,CAAC;IAChB,MAAM,IAAI,GAAG,CAAC,GAAW,EAAQ,EAAE;QAC/B,IAAI,OAAoB,CAAC;QACzB,IAAI,CAAC;YACD,OAAO,GAAG,EAAE,CAAC,WAAW,CAAC,GAAG,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC;QAC3D,CAAC;QAAC,MAAM,CAAC;YACL,OAAO;QACX,CAAC;QACD,KAAK,MAAM,CAAC,IAAI,OAAO,EAAE,CAAC;YACtB,IAAI,CAAC,CAAC,WAAW,EAAE;gBAAE,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;QACtD,CAAC;QACD,IAAI,CAAC;YACD,IAAI,EAAE,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;gBACnC,EAAE,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;gBAClB,OAAO,IAAI,CAAC,CAAC;YACjB,CAAC;QACL,CAAC;QAAC,MAAM,CAAC;YACL,8EAA8E;QAClF,CAAC;IACL,CAAC,CAAC;IACF,IAAI,GAAgB,CAAC;IACrB,IAAI,CAAC;QACD,GAAG,GAAG,EAAE,CAAC,WAAW,CAAC,IAAI,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC;IACxD,CAAC;IAAC,MAAM,CAAC;QACL,OAAO,CAAC,CAAC;IACb,CAAC;IACD,KAAK,MAAM,CAAC,IAAI,GAAG,EAAE,CAAC;QAClB,IAAI,CAAC,CAAC,WAAW,EAAE;YAAE,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;IACvD,CAAC;IACD,OAAO,OAAO,CAAC;AACnB,CAAC"}
|
package/dist/scripts/condense.js
CHANGED
|
@@ -230,7 +230,9 @@ export { rule_in_scope } from '../install/ruleInScope.js';
|
|
|
230
230
|
// Re-exported for in-repo consumers; definitions moved beside emit_host_rules_cli.ts (Phase 5).
|
|
231
231
|
export { CLAUDE_PATHS_PATTERN_BUDGET, _claude_paths_plan, _escape_claude_bracket, _expanded_pattern_count, _has_non_path_trigger, _is_unresolved_placeholder, derive_trigger_globs, } from '../install/claudePathsPlan.js';
|
|
232
232
|
import { rule_in_scope } from '../install/ruleInScope.js';
|
|
233
|
-
import {
|
|
233
|
+
import { pruneEmptyDirs } from './_lib/prune_empty_dirs.js';
|
|
234
|
+
import { commandsWithheld, partitionActive, personaPartition, setPartitionAnnounce } from '../install/partitionEligibility.js'; // ADR-236
|
|
235
|
+
import { dedupableRules, partitionRulesForDir } from '../install/ruleLayerPartition.js'; // ADR-236, per-host evidence
|
|
234
236
|
import { _claude_paths_plan, derive_trigger_globs } from '../install/claudePathsPlan.js';
|
|
235
237
|
const _HERE = path.dirname(fileURLToPath(import.meta.url)); // <repo>/src/scripts
|
|
236
238
|
const _DEFAULT_PROJECT_ROOT = path.resolve(_HERE, '..', '..');
|
|
@@ -366,14 +368,6 @@ function _read_augment_rules_use_symlinks() {
|
|
|
366
368
|
}
|
|
367
369
|
return false;
|
|
368
370
|
}
|
|
369
|
-
/**
|
|
370
|
-
* Per-tool user-scope rule directory, for the scope-dedup below. Only tools
|
|
371
|
-
* whose host reads a user-scope rules directory can have a redundant twin;
|
|
372
|
-
* everything else has nothing to de-duplicate against.
|
|
373
|
-
*/
|
|
374
|
-
const USER_SCOPE_RULE_DIRS = {
|
|
375
|
-
'.claude/rules': path.join('.claude', 'rules'),
|
|
376
|
-
};
|
|
377
371
|
function _read_projection_scope_dedup() {
|
|
378
372
|
const data = load_agent_settings({ project_path: MODULE_STATE.SETTINGS_FILE });
|
|
379
373
|
const projection = data['projection'];
|
|
@@ -389,63 +383,6 @@ function _read_projection_scope_dedup() {
|
|
|
389
383
|
}
|
|
390
384
|
return false;
|
|
391
385
|
}
|
|
392
|
-
/**
|
|
393
|
-
* Rules whose user-scope twin is BYTE-IDENTICAL to the source, i.e. safe to
|
|
394
|
-
* skip at project scope: the host still loads the same text, once instead of
|
|
395
|
-
* twice, so nothing the model sees changes.
|
|
396
|
-
*
|
|
397
|
-
* Byte-identity is the whole safety argument and is not negotiable. Keying on
|
|
398
|
-
* the filename alone would silently let a stale globally-installed copy win
|
|
399
|
-
* whenever the two scopes hold different versions — which is the NORMAL state
|
|
400
|
-
* while developing the package (measured: 110/110 shared filenames differing in
|
|
401
|
-
* bytes). This is deliberately not the thin-projection mechanism: no rule
|
|
402
|
-
* becomes trigger-gated, so the quality floor that disabled thin projection
|
|
403
|
-
* does not apply here.
|
|
404
|
-
*/
|
|
405
|
-
function _dedupable_rules(tool_dir, rules, userHome) {
|
|
406
|
-
const relative = USER_SCOPE_RULE_DIRS[tool_dir];
|
|
407
|
-
if (relative === undefined) {
|
|
408
|
-
return new Set();
|
|
409
|
-
}
|
|
410
|
-
// A hostile or simply absent $HOME must make the dedup inert, not
|
|
411
|
-
// adventurous. In a container `$HOME` is often unset (so `homedir()` can
|
|
412
|
-
// resolve to `/`) or world-writable, and this function decides which rules
|
|
413
|
-
// to STOP emitting — reading an unexpected tree there is how a projection
|
|
414
|
-
// silently loses a rule. Council review of PR #1055 raised exactly this.
|
|
415
|
-
let userDirStat;
|
|
416
|
-
const userDir = path.join(userHome, relative);
|
|
417
|
-
try {
|
|
418
|
-
userDirStat = fs.statSync(userDir);
|
|
419
|
-
}
|
|
420
|
-
catch {
|
|
421
|
-
return new Set();
|
|
422
|
-
}
|
|
423
|
-
if (!userDirStat.isDirectory()) {
|
|
424
|
-
return new Set();
|
|
425
|
-
}
|
|
426
|
-
// World-writable user scope: anyone on the box could plant a byte-identical
|
|
427
|
-
// twin and thereby delete a rule from the project projection. Refuse.
|
|
428
|
-
if ((userDirStat.mode & 0o002) !== 0) {
|
|
429
|
-
_print(` ⚠️ ${tool_dir}: user-scope rules dir is world-writable — scope-dedup skipped`);
|
|
430
|
-
return new Set();
|
|
431
|
-
}
|
|
432
|
-
const skip = new Set();
|
|
433
|
-
for (const rule of rules) {
|
|
434
|
-
const twin = path.join(userDir, rule);
|
|
435
|
-
const source = path.join(MODULE_STATE.RULES_SOURCE, rule);
|
|
436
|
-
try {
|
|
437
|
-
if (!_isFile(twin) || !_isFile(source))
|
|
438
|
-
continue;
|
|
439
|
-
if (fs.readFileSync(twin).equals(fs.readFileSync(source))) {
|
|
440
|
-
skip.add(rule);
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
catch {
|
|
444
|
-
// An unreadable twin is simply not de-duplicable — emit the project copy.
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
return skip;
|
|
448
|
-
}
|
|
449
386
|
function _lean_projection_mode() {
|
|
450
387
|
const data = load_agent_settings({ project_path: MODULE_STATE.SETTINGS_FILE });
|
|
451
388
|
const lean = data['lean_projection'];
|
|
@@ -1009,10 +946,16 @@ function _scoped_rule_basenames() {
|
|
|
1009
946
|
// is its fourth consumer, after the router compiler, the dist writer, and
|
|
1010
947
|
// check_sync. Same predicate in all four; that is the point.
|
|
1011
948
|
.filter((p) => !skip_compile_disabled_rule(`rules/${path.basename(p)}`))
|
|
1012
|
-
.filter((p) => !partitionActive(MODULE_STATE.PROJECT_ROOT) || isExclusivelyPackageOnly(p))
|
|
1013
949
|
.map((p) => path.basename(p))
|
|
1014
950
|
.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
|
|
1015
951
|
}
|
|
952
|
+
/** Adapter over {@link partitionRulesForDir}; that module carries the reasoning. */
|
|
953
|
+
export function partition_rules_for_dir(tool_dir, rules, user_home = process.env['HOME'] ?? os.homedir()) {
|
|
954
|
+
return partitionRulesForDir({
|
|
955
|
+
toolDir: tool_dir, rules, userHome: user_home, announce: _print,
|
|
956
|
+
projectRoot: MODULE_STATE.PROJECT_ROOT, rulesSource: MODULE_STATE.RULES_SOURCE,
|
|
957
|
+
});
|
|
958
|
+
}
|
|
1016
959
|
/**
|
|
1017
960
|
* The emit plan of {@link generate_rule_symlinks}: active tool dir → the exact
|
|
1018
961
|
* rule basenames it projects there.
|
|
@@ -1035,8 +978,12 @@ export function projected_rule_trees(user_home = process.env['HOME'] ?? os.homed
|
|
|
1035
978
|
const dedup_on = _read_projection_scope_dedup();
|
|
1036
979
|
const out = {};
|
|
1037
980
|
for (const tool_dir of Object.keys(_filter_tool_dirs(TOOL_DIRS))) {
|
|
1038
|
-
const skip = dedup_on
|
|
1039
|
-
|
|
981
|
+
const skip = dedup_on
|
|
982
|
+
? dedupableRules({ toolDir: tool_dir, rules, userHome: user_home,
|
|
983
|
+
rulesSource: MODULE_STATE.RULES_SOURCE, announce: _print })
|
|
984
|
+
: new Set();
|
|
985
|
+
const kept = rules.filter((r) => !skip.has(r));
|
|
986
|
+
out[tool_dir] = partition_rules_for_dir(tool_dir, kept, user_home);
|
|
1040
987
|
}
|
|
1041
988
|
return out;
|
|
1042
989
|
}
|
|
@@ -1143,7 +1090,10 @@ export function generate_rule_symlinks() {
|
|
|
1143
1090
|
return total;
|
|
1144
1091
|
}
|
|
1145
1092
|
export function generate_windsurfrules() {
|
|
1146
|
-
|
|
1093
|
+
// Windsurf's LEGACY single-file surface — same host, so the same evidence, keyed
|
|
1094
|
+
// on `.windsurf/rules`. Without this line removing the per-run filter regressed
|
|
1095
|
+
// it 13 → 113: the per-run filter had been covering it by accident.
|
|
1096
|
+
const rules = partition_rules_for_dir('.windsurf/rules', _scoped_rule_basenames());
|
|
1147
1097
|
const parts = ['# Auto-generated from dist/agent-src/rules/ — do not edit directly\n'];
|
|
1148
1098
|
for (const rule of rules) {
|
|
1149
1099
|
const p = path.join(MODULE_STATE.RULES_SOURCE, rule);
|
|
@@ -1298,12 +1248,16 @@ function _clean_modern_dir(target_dir, valid_names) {
|
|
|
1298
1248
|
export function generate_cursor_mdc_rules() {
|
|
1299
1249
|
const scope = _read_rule_workspaces();
|
|
1300
1250
|
const pack_scope = _read_rule_packs();
|
|
1301
|
-
|
|
1251
|
+
let rules = _rglobSorted(MODULE_STATE.RULES_SOURCE, '*.md')
|
|
1302
1252
|
.filter((p) => _isFile(p))
|
|
1303
1253
|
.filter((p) => rule_in_scope(p, scope, pack_scope))
|
|
1304
1254
|
// ADR-004 manual rules are reference-only — never auto-injected into a
|
|
1305
1255
|
// per-tool tree. Same predicate as generate_rule_symlinks.
|
|
1306
1256
|
.filter((p) => !_is_manual_rule(p));
|
|
1257
|
+
// ADR-236 per-host partition (src/install/ruleLayerPartition.ts). Narrowing the
|
|
1258
|
+
// list is a REMOVAL, because `_clean_modern_dir` below sweeps to `valid`.
|
|
1259
|
+
const kept = new Set(partition_rules_for_dir('.cursor/rules', rules.map((r) => path.basename(r))));
|
|
1260
|
+
rules = rules.filter((r) => kept.has(path.basename(r)));
|
|
1307
1261
|
const stems = rules.map((r) => path.basename(r, '.md'));
|
|
1308
1262
|
const valid = new Set([
|
|
1309
1263
|
...stems.map((s) => `${s}.mdc`),
|
|
@@ -1319,12 +1273,15 @@ export function generate_cursor_mdc_rules() {
|
|
|
1319
1273
|
export function generate_windsurf_modern_rules() {
|
|
1320
1274
|
const scope = _read_rule_workspaces();
|
|
1321
1275
|
const pack_scope = _read_rule_packs();
|
|
1322
|
-
|
|
1276
|
+
let rules = _rglobSorted(MODULE_STATE.RULES_SOURCE, '*.md')
|
|
1323
1277
|
.filter((p) => _isFile(p))
|
|
1324
1278
|
.filter((p) => rule_in_scope(p, scope, pack_scope))
|
|
1325
1279
|
// ADR-004 manual rules are reference-only — never auto-injected into a
|
|
1326
1280
|
// per-tool tree. Same predicate as generate_rule_symlinks.
|
|
1327
1281
|
.filter((p) => !_is_manual_rule(p));
|
|
1282
|
+
// ADR-236 per-host partition — same shape as the cursor emitter above.
|
|
1283
|
+
const kept = new Set(partition_rules_for_dir('.windsurf/rules', rules.map((r) => path.basename(r))));
|
|
1284
|
+
rules = rules.filter((r) => kept.has(path.basename(r)));
|
|
1328
1285
|
const valid = new Set(rules.map((r) => path.basename(r)));
|
|
1329
1286
|
_clean_modern_dir(_WINDSURF_RULES_DIR(), valid);
|
|
1330
1287
|
for (const rule of rules) {
|
|
@@ -1599,23 +1556,24 @@ function _nested_command_subpath(source_file) {
|
|
|
1599
1556
|
/**
|
|
1600
1557
|
* Project-scope `.claude/commands/<cluster>/<sub>.md` — the colon form.
|
|
1601
1558
|
*
|
|
1602
|
-
* Before this, the package repo's own `.claude/` carried NO commands
|
|
1603
|
-
*
|
|
1604
|
-
*
|
|
1605
|
-
*
|
|
1606
|
-
*
|
|
1607
|
-
*
|
|
1608
|
-
* of always-loaded catalog for zero added reach — measured 2026-08-02.
|
|
1559
|
+
* Before this, the package repo's own `.claude/` carried NO commands directory:
|
|
1560
|
+
* every command reached Claude Code as a hyphen-named skill wrapper in
|
|
1561
|
+
* `.claude/skills/`, while the colon form existed only in the user-global tree
|
|
1562
|
+
* written by `install.ts`. Every clustered command was therefore listed TWICE
|
|
1563
|
+
* for anyone with a global install (`roadmap-process-full` AND
|
|
1564
|
+
* `roadmap:process-full`), costing 4,214 GPT tok of catalog — measured 2026-08-02.
|
|
1609
1565
|
*
|
|
1610
1566
|
* Emitting the nested form here lets `generate_claude_commands` stop wrapping
|
|
1611
1567
|
* the clustered commands: one listing, same reachability, and reachability no
|
|
1612
|
-
* longer depends on a global deploy
|
|
1613
|
-
* and user scope by name, so the two copies of `/cluster:sub` collapse.
|
|
1568
|
+
* longer depends on a global deploy. Dedup AND precedence: `commandsWithheld`.
|
|
1614
1569
|
*
|
|
1615
1570
|
* ADR-003 (colon canonical for clusters) and ADR-044 (flat commands stay
|
|
1616
1571
|
* hyphenated) both hold: flat commands are untouched and keep their wrapper.
|
|
1617
1572
|
*/
|
|
1618
1573
|
export function generate_claude_project_commands(active_command_slugs = null) {
|
|
1574
|
+
if (commandsWithheld(MODULE_STATE.PROJECT_ROOT))
|
|
1575
|
+
active_command_slugs = new Set();
|
|
1576
|
+
// Returns before the stale sweep — invariant, see `commandsWithheld`.
|
|
1619
1577
|
if (!_isDir(path.join(MODULE_STATE.PROJECT_ROOT, 'src', 'domains'))) {
|
|
1620
1578
|
return 0;
|
|
1621
1579
|
}
|
|
@@ -1631,8 +1589,7 @@ export function generate_claude_project_commands(active_command_slugs = null) {
|
|
|
1631
1589
|
}
|
|
1632
1590
|
const target_root = _CLAUDE_COMMANDS_DIR();
|
|
1633
1591
|
const valid = new Set(nested.map(([, sub]) => `${sub}.md`));
|
|
1634
|
-
// Sweep stale links first
|
|
1635
|
-
// a dangling `/cluster:sub`.
|
|
1592
|
+
// Sweep stale links first — a renamed, deleted or WITHHELD command must not linger.
|
|
1636
1593
|
if (_exists(target_root)) {
|
|
1637
1594
|
for (const item of _rglobSorted(target_root, '*.md')) {
|
|
1638
1595
|
const rel = _relativeToPosix(item, target_root);
|
|
@@ -1640,6 +1597,7 @@ export function generate_claude_project_commands(active_command_slugs = null) {
|
|
|
1640
1597
|
fs.unlinkSync(item);
|
|
1641
1598
|
}
|
|
1642
1599
|
}
|
|
1600
|
+
pruneEmptyDirs(target_root); // else the gate counts 40 empty cluster dirs as 40 commands
|
|
1643
1601
|
}
|
|
1644
1602
|
let count = 0;
|
|
1645
1603
|
for (const [source_file, sub] of nested) {
|
|
@@ -2077,7 +2035,7 @@ export function generate_persona_symlinks() {
|
|
|
2077
2035
|
.map((p) => path.basename(p))
|
|
2078
2036
|
.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
|
|
2079
2037
|
void personas;
|
|
2080
|
-
const
|
|
2038
|
+
const partition = personaPartition(MODULE_STATE.PROJECT_ROOT, direct);
|
|
2081
2039
|
const tool_dirs = _filter_tool_dirs(PERSONA_TOOL_DIRS);
|
|
2082
2040
|
let total = 0;
|
|
2083
2041
|
for (const [tool_dir, rel_prefix] of Object.entries(tool_dirs)) {
|
|
@@ -2085,11 +2043,11 @@ export function generate_persona_symlinks() {
|
|
|
2085
2043
|
_mkdirp(target_dir);
|
|
2086
2044
|
for (const item of _iterdirSorted(target_dir)) {
|
|
2087
2045
|
const name = path.basename(item);
|
|
2088
|
-
if (_isSymlink(item) && !
|
|
2046
|
+
if (_isSymlink(item) && !partition.listFor(tool_dir).includes(name) && name !== 'README.md') {
|
|
2089
2047
|
fs.unlinkSync(item);
|
|
2090
2048
|
}
|
|
2091
2049
|
}
|
|
2092
|
-
for (const persona of
|
|
2050
|
+
for (const persona of partition.listFor(tool_dir)) {
|
|
2093
2051
|
const link = path.join(target_dir, persona);
|
|
2094
2052
|
const target = path.join(rel_prefix, persona);
|
|
2095
2053
|
if (_existsOrSymlink(link)) {
|
|
@@ -2099,7 +2057,8 @@ export function generate_persona_symlinks() {
|
|
|
2099
2057
|
total += 1;
|
|
2100
2058
|
}
|
|
2101
2059
|
}
|
|
2102
|
-
|
|
2060
|
+
const per_dir = Object.keys(tool_dirs).map((d) => `${d}=${partition.countFor(d)}`);
|
|
2061
|
+
info(` ✅ Created ${total} persona symlinks — ${per_dir.join(' · ')}${partition.note}`);
|
|
2103
2062
|
return total;
|
|
2104
2063
|
}
|
|
2105
2064
|
export function generate_user_type_symlinks() {
|
|
@@ -2272,10 +2231,14 @@ export function project_to_augment() {
|
|
|
2272
2231
|
const current = new Set();
|
|
2273
2232
|
let written = 0;
|
|
2274
2233
|
if (_exists(src_rules)) {
|
|
2275
|
-
|
|
2234
|
+
// ADR-236 per-host partition, ahead of the `use_symlinks` branch so both
|
|
2235
|
+
// writers narrow; the `existing` minus `current` sweep below makes it a
|
|
2236
|
+
// removal. This emitter never had the scope or ADR-004 manual filters
|
|
2237
|
+
// either — why its count is 15 and the symlink trees' 13. Pre-existing.
|
|
2238
|
+
const rules = partition_rules_for_dir('.augment/rules', _iterdirSorted(src_rules)
|
|
2276
2239
|
.filter((p) => p.endsWith('.md') && _isFile(p))
|
|
2277
2240
|
.map((p) => path.basename(p))
|
|
2278
|
-
.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
|
|
2241
|
+
.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0)));
|
|
2279
2242
|
for (const rule of rules) {
|
|
2280
2243
|
const target = path.join(dst_rules, rule);
|
|
2281
2244
|
if (_isSymlink(target) || _existsOrSymlink(target)) {
|