@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
|
@@ -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
|
|
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
|
|
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
|
-
/**
|
|
92
|
-
|
|
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,
|
|
330
|
-
+ '
|
|
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
|
}
|
package/src/scripts/condense.ts
CHANGED
|
@@ -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 {
|
|
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
|
|
1165
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
1786
|
-
*
|
|
1787
|
-
*
|
|
1788
|
-
*
|
|
1789
|
-
*
|
|
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
|
|
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
|
|
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
|
|
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) && !
|
|
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
|
|
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
|
-
|
|
2323
|
-
|
|
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
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
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)) {
|
|
@@ -485,8 +485,28 @@ export function commandOp(command: string): GitOp | null {
|
|
|
485
485
|
return found;
|
|
486
486
|
}
|
|
487
487
|
|
|
488
|
-
/**
|
|
489
|
-
|
|
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
|