@event4u/agent-config 14.21.0 → 14.22.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/.augment-plugin/marketplace.json +3 -3
- package/.augment-plugin/plugin.json +1 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +198 -0
- package/CREDITS.md +37 -0
- package/dist/agent-src/commands/analyze/inbox.md +380 -5
- package/dist/agent-src/commands/analyze/repo.md +4 -2
- package/dist/agent-src/contexts/communication/rules-auto/token-efficiency-mechanics.md +69 -0
- package/dist/agent-src/contexts/execution/auto-dispatch-classification.md +5 -5
- package/dist/agent-src/rules/external-code-graph-interop.md +19 -14
- package/dist/agent-src/rules/token-efficiency.md +26 -3
- package/dist/agent-src/scripts/_cli_entry.ts +51 -0
- package/dist/agent-src/scripts/archive_completed_roadmaps.ts +14 -23
- package/dist/agent-src/scripts/roadmap_gates.ts +4 -20
- package/dist/agent-src/scripts/roadmap_set_step.ts +5 -2
- package/dist/agent-src/scripts/stubs_due.ts +4 -18
- package/dist/agent-src/scripts/update_roadmap_progress.ts +7 -24
- package/dist/agent-src/skills/code-intelligence/SKILL.md +25 -0
- package/dist/agent-src/skills/roadmap-writing/SKILL.md +17 -0
- package/dist/agent-src/templates/agent-settings.md +0 -1
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/cli-delegate/_cli_entry.js +8 -0
- package/dist/cli-delegate/_lib/pin_resolver.js +170 -0
- package/dist/cli-delegate/archival_sweep.js +8 -0
- package/dist/cli-delegate/archive_completed_roadmaps.js +494 -0
- package/dist/cli-delegate/check_update_banner.js +923 -0
- package/dist/cli-delegate/chunk-5EK5WQN6.js +28 -0
- package/dist/cli-delegate/chunk-7XGD2OEU.js +96 -0
- package/dist/cli-delegate/{chunk-643Z7FIC.js → chunk-BIXEDQPZ.js} +6 -0
- package/dist/cli-delegate/chunk-DBH4AHDK.js +155 -0
- package/dist/cli-delegate/chunk-JO25G5H7.js +20 -0
- package/dist/cli-delegate/chunk-LTYVIY76.js +7961 -0
- package/dist/cli-delegate/chunk-QAJS5YWY.js +999 -0
- package/dist/cli-delegate/{chunk-ZVBOHSVF.js → chunk-TGIPZBCW.js} +1 -1
- package/dist/cli-delegate/{chunk-YS6XTLEK.js → chunk-TYSY33MD.js} +56 -6
- package/dist/cli-delegate/chunk-ULET4BYS.js +328 -0
- package/dist/cli-delegate/chunk-YDQSHTD7.js +187 -0
- package/dist/cli-delegate/chunk-YWVXLR44.js +48 -0
- package/dist/cli-delegate/chunk-ZZQBUHFK.js +273 -0
- package/dist/cli-delegate/cmd_conformance.js +2 -2
- package/dist/cli-delegate/cmd_export.js +1 -1
- package/dist/cli-delegate/cmd_handoff.js +26 -2
- package/dist/cli-delegate/cmd_prune.js +1 -1
- package/dist/cli-delegate/cmd_session_recycle.js +2 -2
- package/dist/cli-delegate/cmd_settings_migrate.js +1 -1
- package/dist/cli-delegate/cmd_sync.js +1 -1
- package/dist/cli-delegate/cmd_uninstall.js +1 -1
- package/dist/cli-delegate/cmd_validate.js +1 -1
- package/dist/cli-delegate/dashboard_mode.js +16 -0
- package/dist/cli-delegate/gate_budget.js +24 -0
- package/dist/cli-delegate/gate_execute.js +25 -0
- package/dist/cli-delegate/guarded_baseline.js +18 -0
- package/dist/cli-delegate/resume_probe.js +22 -0
- package/dist/cli-delegate/roadmap_gates.js +638 -0
- package/dist/cli-delegate/roadmap_set_step.js +173 -0
- package/dist/cli-delegate/stub_queue.js +47 -0
- package/dist/cli-delegate/stubs_due.js +38 -0
- package/dist/cli-delegate/ticket_bundles.js +59 -0
- package/dist/cli-delegate/update_roadmap_progress.js +60 -0
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +8 -8
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +1 -1
- package/dist/discovery/dormancy-report.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +2 -2
- package/dist/discovery/trust-report.md +1 -1
- package/dist/discovery/workspaces.json +2 -2
- package/dist/hooks/dispatch.js +1193 -917
- package/dist/install/claudeLayerCarriage.js +237 -0
- package/dist/install/claudePathsPlan.js +79 -0
- package/dist/install/hostLayerFingerprint.js +1 -1
- package/dist/install/install.mjs +6 -0
- package/dist/install/partitionEligibility.js +231 -117
- package/dist/install/ruleLayerPartition.js +10 -7
- package/dist/mcp/registry-manifest.json +2 -2
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +39219 -38995
- package/dist/scripts/condense.js +32 -25
- package/docs/MIGRATION.md +49 -1
- package/docs/contracts/settings-classes.md +1 -1
- package/docs/guidelines/agent-infra/minimal-safe-diff-mechanics.md +73 -0
- package/package.json +7 -4
- package/src/agent-src/scripts/_cli_entry.ts +51 -0
- package/src/agent-src/scripts/archive_completed_roadmaps.ts +14 -23
- package/src/agent-src/scripts/roadmap_gates.ts +4 -20
- package/src/agent-src/scripts/roadmap_set_step.ts +5 -2
- package/src/agent-src/scripts/stubs_due.ts +4 -18
- package/src/agent-src/scripts/update_roadmap_progress.ts +7 -24
- package/src/config/continuity-surface.json +509 -0
- package/src/config/estate-count-budget.json +1 -1
- package/src/config/gate-coverage.yml +29 -8
- package/src/config/gate-violation-baselines.json +8 -8
- package/src/config/gitignore-block.txt +19 -0
- package/src/config/host-injection-effect.json +73 -0
- package/src/config/packed-binary-manifest.json +33 -0
- package/src/config/preamble-payload-budget.json +112 -105
- package/src/config/publish-surface.json +4 -2
- package/src/install/claudeLayerCarriage.ts +273 -0
- package/src/install/claudePathsPlan.ts +76 -0
- package/src/install/hostLayerFingerprint.ts +1 -1
- package/src/install/partitionEligibility.ts +261 -127
- package/src/install/ruleLayerPartition.ts +19 -23
- package/src/scripts/_dispatch.bash +77 -27
- package/src/scripts/_lib/auto_dispatch.ts +2 -2
- package/src/scripts/_lib/catalogue_layer_parity.ts +146 -0
- package/src/scripts/_lib/continuity_surface.ts +296 -0
- package/src/scripts/_lib/corpus_manifest.ts +26 -9
- package/src/scripts/_lib/hook_settings.ts +1 -1
- package/src/scripts/_lib/injection_effect.ts +151 -0
- package/src/scripts/_lib/layer_overlap_notice.ts +50 -6
- package/src/scripts/_lib/packed_binary_predicate.ts +225 -0
- package/src/scripts/_lib/pin_resolver.ts +13 -0
- package/src/scripts/_lib/regression_neighbourhood.ts +1 -1
- package/src/scripts/_lib/retired_status.ts +74 -0
- package/src/scripts/_lib/skill_catalogue.ts +156 -21
- package/src/scripts/_lib/subagent_capsule.ts +117 -8
- package/src/scripts/ai_council/api_transport.ts +5 -0
- package/src/scripts/ai_council/deliberation_metrics.ts +353 -0
- package/src/scripts/ai_council/prompts.ts +9 -0
- package/src/scripts/ai_council/synthesis_disagreement.ts +206 -0
- package/src/scripts/capture_skill_catalogue.ts +24 -11
- package/src/scripts/check_candidate_lines.ts +499 -0
- package/src/scripts/check_continuity_surface.ts +475 -0
- package/src/scripts/check_dependency_floors.ts +13 -6
- package/src/scripts/check_estate_count.ts +40 -79
- package/src/scripts/check_pack_size.ts +262 -23
- package/src/scripts/check_roadmap_trackable.ts +19 -16
- package/src/scripts/check_rule_layer_partition.ts +54 -34
- package/src/scripts/check_single_delivery.ts +97 -31
- package/src/scripts/check_update_banner.ts +13 -0
- package/src/scripts/code_graph/build.ts +138 -15
- package/src/scripts/code_graph/cli.ts +76 -8
- package/src/scripts/code_graph/extract.ts +11 -0
- package/src/scripts/code_graph/grammar_probe.ts +43 -0
- package/src/scripts/code_graph/loader.ts +88 -14
- package/src/scripts/code_graph/query.ts +121 -11
- package/src/scripts/code_graph/resolution_tiers.ts +238 -0
- package/src/scripts/code_graph/sqlite_store.ts +247 -6
- package/src/scripts/code_graph/types.ts +66 -1
- package/src/scripts/code_graph/validate.ts +23 -0
- package/src/scripts/condense.ts +34 -22
- package/src/scripts/corpus_manifest.ts +5 -5
- package/src/scripts/council_cli.ts +11 -12
- package/src/scripts/count_intent_disagreement.ts +426 -0
- package/src/scripts/handoff_context_hook.ts +79 -0
- package/src/scripts/hook_effect_doctor.ts +82 -1
- package/src/scripts/hook_manifest.json +1 -1
- package/src/scripts/hook_manifest.yaml +64 -12
- package/src/scripts/hooks/chain_nudge_hook.ts +282 -0
- package/src/scripts/hooks/code_graph_context_hook.ts +212 -0
- package/src/scripts/hooks/concern_registry.ts +4 -2
- package/src/scripts/hooks/host_semantics.ts +1 -1
- package/src/scripts/hooks/orchestration_record_hook.ts +2 -2
- package/src/scripts/hooks/rule_inject_hook.ts +1 -1
- package/src/scripts/hooks/skill_route_hook.ts +11 -4
- package/src/scripts/inbox_source_census.ts +322 -0
- package/src/scripts/install-hooks.sh +82 -0
- package/src/scripts/{lint_carrier_integrity.ts → lint_deferral_integrity.ts} +60 -214
- package/src/scripts/lint_plan_risk_register.ts +13 -17
- package/src/scripts/mcp_server/suggest_skill.ts +72 -0
- package/src/scripts/mcp_server/tools.ts +2 -56
- package/src/scripts/media/lib/adapter-contract.md +7 -2
- package/src/scripts/media/lib/fixtures/flux/asset-0001.png +0 -0
- package/src/scripts/media/lib/fixtures/gemini-image/asset-0001.png +0 -0
- package/src/scripts/media/lib/fixtures/ideogram/asset-0001.png +0 -0
- package/src/scripts/rdp_candidates_delta.ts +276 -0
- package/src/scripts/rdp_quality_eval.ts +217 -19
- package/src/scripts/release_mix_taxonomy.json +3 -2
- package/src/scripts/report_host_injection_effect.ts +189 -0
- package/src/scripts/report_layer_overlap.ts +41 -0
- package/src/scripts/report_skill_menu_census.ts +341 -0
- package/src/scripts/skill_tools/score_skill_relevance.ts +70 -6
- package/src/scripts/skill_tools/suggest_skill_for_task.ts +37 -3
- package/src/vendor/grammars/README.md +61 -0
- package/src/vendor/grammars/tree-sitter-javascript.wasm +0 -0
- package/src/vendor/grammars/tree-sitter-php.wasm +0 -0
- package/src/vendor/grammars/tree-sitter-typescript.wasm +0 -0
- package/src/scripts/_lib/carrier_status.ts +0 -118
- package/src/scripts/_lib/estate_carrier_cases.ts +0 -72
- package/src/scripts/hooks/code_graph_nudge_hook.ts +0 -191
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Continuity-surface inventory — the behavioural half of the ratchet.
|
|
3
|
+
*
|
|
4
|
+
* WHY THIS EXISTS
|
|
5
|
+
* ---------------
|
|
6
|
+
* `road-to-one-continuity-record` measured its own success as five numbers —
|
|
7
|
+
* public continuity commands, session resume pickers, persistent continuity
|
|
8
|
+
* artefacts, continuity schemas, normal-path manual actions — and its council
|
|
9
|
+
* of 2026-09-07 named the defect in measuring them by name:
|
|
10
|
+
*
|
|
11
|
+
* "Any persisted state written for later session/run recovery, verification,
|
|
12
|
+
* or context restoration must be listed, whether or not it is labeled
|
|
13
|
+
* continuity."
|
|
14
|
+
*
|
|
15
|
+
* A count keyed on the WORD `continuity` is gameable by renaming. So the
|
|
16
|
+
* artefact axis is DISCOVERED behaviourally — every distinct leaf under
|
|
17
|
+
* `agents/runtime/state/` that any source file names — and the inventory then
|
|
18
|
+
* has to dispose of each one as `counted` or `excluded` WITH A REASON. A leaf
|
|
19
|
+
* nothing disposes of is a hard failure, which is what makes a newly added
|
|
20
|
+
* persisted-state surface impossible to slip past the count silently.
|
|
21
|
+
*
|
|
22
|
+
* The exclusions are the load-bearing half, not the leftovers. `checkpoints/`
|
|
23
|
+
* is the worked example the council named: it is kept (D2 — run-integrity
|
|
24
|
+
* evidence is keyed by run id, consumed by `run:supervise`, and a disagreement
|
|
25
|
+
* there is an integrity alarm rather than a degraded resume), so it is excluded
|
|
26
|
+
* from the continuity axis, and that exclusion is PRINTED on every run rather
|
|
27
|
+
* than buried in a predicate.
|
|
28
|
+
*
|
|
29
|
+
* WHAT THIS MODULE IS NOT
|
|
30
|
+
* -----------------------
|
|
31
|
+
* It does not decide policy. It reads a committed inventory, checks that the
|
|
32
|
+
* inventory still describes the tree (every `locus` resolves; every discovered
|
|
33
|
+
* leaf is disposed of), and derives the five numbers from the dispositions.
|
|
34
|
+
* The ratchet that forbids growth lives in `check_continuity_surface.ts`, which
|
|
35
|
+
* runs these same functions over a materialised base tree.
|
|
36
|
+
*/
|
|
37
|
+
import * as fs from 'node:fs';
|
|
38
|
+
import * as path from 'node:path';
|
|
39
|
+
|
|
40
|
+
/** Repo-relative POSIX path of the committed inventory. */
|
|
41
|
+
export const INVENTORY_POSIX = 'src/config/continuity-surface.json';
|
|
42
|
+
|
|
43
|
+
/** Source roots the behavioural discovery walks. */
|
|
44
|
+
export const DISCOVERY_ROOTS: readonly string[] = [
|
|
45
|
+
'src/scripts',
|
|
46
|
+
'src/agent-src/scripts',
|
|
47
|
+
'src/cli',
|
|
48
|
+
'src/shared',
|
|
49
|
+
'src/server',
|
|
50
|
+
'src/install',
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The five axes, in the order the parent roadmap's goal states them. The order
|
|
55
|
+
* is part of the contract: the goal reads `0 / 0 / 1 / 1 / 0` positionally, and
|
|
56
|
+
* a reader comparing a run against that line needs the same positions.
|
|
57
|
+
*/
|
|
58
|
+
export const AXES = [
|
|
59
|
+
'public_continuity_commands',
|
|
60
|
+
'session_resume_pickers',
|
|
61
|
+
'persistent_continuity_artefacts',
|
|
62
|
+
'continuity_schemas',
|
|
63
|
+
'normal_path_manual_actions',
|
|
64
|
+
] as const;
|
|
65
|
+
|
|
66
|
+
export type Axis = (typeof AXES)[number];
|
|
67
|
+
|
|
68
|
+
/** The target the parent roadmap's goal states, by axis. */
|
|
69
|
+
export const TARGETS: Readonly<Record<Axis, number>> = {
|
|
70
|
+
public_continuity_commands: 0,
|
|
71
|
+
session_resume_pickers: 0,
|
|
72
|
+
persistent_continuity_artefacts: 1,
|
|
73
|
+
continuity_schemas: 1,
|
|
74
|
+
normal_path_manual_actions: 0,
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export interface InventoryRow {
|
|
78
|
+
/** Stable identifier. For a discovered artefact this is the state leaf. */
|
|
79
|
+
id: string;
|
|
80
|
+
/** Which axis this row belongs to. */
|
|
81
|
+
axis: Axis;
|
|
82
|
+
/** `counted` contributes to the axis number; `excluded` does not. */
|
|
83
|
+
disposition: 'counted' | 'excluded';
|
|
84
|
+
/**
|
|
85
|
+
* Repo-relative `file` or `file:line` the row is anchored to. It must
|
|
86
|
+
* resolve, which is what makes a retirement MEASURED: deleting the surface
|
|
87
|
+
* without deleting or re-disposing its row reddens.
|
|
88
|
+
*/
|
|
89
|
+
locus: string;
|
|
90
|
+
/**
|
|
91
|
+
* Why. Mandatory on every row, not only on exclusions — a `counted` row
|
|
92
|
+
* whose reason nobody wrote is a number without a meaning.
|
|
93
|
+
*/
|
|
94
|
+
reason: string;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface Inventory {
|
|
98
|
+
rows: readonly InventoryRow[];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** A leaf the discovery found, with the first source position that names it. */
|
|
102
|
+
export interface DiscoveredLeaf {
|
|
103
|
+
leaf: string;
|
|
104
|
+
locus: string;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const LEAF_RE =
|
|
108
|
+
/agents\/runtime\/state\/([A-Za-z0-9_.-]+)|['"]runtime['"],\s*['"]state['"],\s*['"]([A-Za-z0-9_.-]+)['"]/g;
|
|
109
|
+
|
|
110
|
+
const SOURCE_EXT = /\.(ts|js|mjs|sh|bash)$/;
|
|
111
|
+
|
|
112
|
+
function _walk(dir: string, out: string[]): void {
|
|
113
|
+
let entries: fs.Dirent[];
|
|
114
|
+
try {
|
|
115
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
116
|
+
} catch {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
for (const entry of entries) {
|
|
120
|
+
const full = path.join(dir, entry.name);
|
|
121
|
+
if (entry.isDirectory()) {
|
|
122
|
+
_walk(full, out);
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (!SOURCE_EXT.test(entry.name)) continue;
|
|
126
|
+
// Test files name state paths in fixtures; a fixture is not a surface.
|
|
127
|
+
if (entry.name.endsWith('.test.ts')) continue;
|
|
128
|
+
out.push(full);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Every distinct `agents/runtime/state/<leaf>` any source file names, with the
|
|
134
|
+
* first `file:line` that names it. Sorted, so two runs over the same tree
|
|
135
|
+
* produce byte-identical output and a diff of the count is a diff of the tree.
|
|
136
|
+
*/
|
|
137
|
+
export function discoverStateLeaves(root: string): DiscoveredLeaf[] {
|
|
138
|
+
const files: string[] = [];
|
|
139
|
+
for (const rel of DISCOVERY_ROOTS) {
|
|
140
|
+
_walk(path.join(root, rel), files);
|
|
141
|
+
}
|
|
142
|
+
files.sort();
|
|
143
|
+
const found = new Map<string, string>();
|
|
144
|
+
for (const file of files) {
|
|
145
|
+
let text: string;
|
|
146
|
+
try {
|
|
147
|
+
text = fs.readFileSync(file, 'utf-8');
|
|
148
|
+
} catch {
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
const rel = path.relative(root, file).split(path.sep).join('/');
|
|
152
|
+
const lines = text.split('\n');
|
|
153
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
154
|
+
const line = lines[i] ?? '';
|
|
155
|
+
LEAF_RE.lastIndex = 0;
|
|
156
|
+
let match: RegExpExecArray | null;
|
|
157
|
+
while ((match = LEAF_RE.exec(line)) !== null) {
|
|
158
|
+
const raw = match[1] ?? match[2] ?? '';
|
|
159
|
+
// A trailing dot is string concatenation in the source
|
|
160
|
+
// (`'...json' + '.tmp'`), never part of the leaf name.
|
|
161
|
+
const leaf = raw.replace(/\.+$/, '');
|
|
162
|
+
if (leaf === '' || leaf === '.') continue;
|
|
163
|
+
if (!found.has(leaf)) found.set(leaf, `${rel}:${String(i + 1)}`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return [...found.keys()].sort().map((leaf) => ({ leaf, locus: found.get(leaf) ?? '' }));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function readInventory(root: string): Inventory {
|
|
171
|
+
const file = path.join(root, INVENTORY_POSIX);
|
|
172
|
+
const parsed = JSON.parse(fs.readFileSync(file, 'utf-8')) as { rows?: unknown };
|
|
173
|
+
const rows = Array.isArray(parsed.rows) ? (parsed.rows as InventoryRow[]) : [];
|
|
174
|
+
return { rows };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Does a `file` or `file:line` locus still resolve in this tree? */
|
|
178
|
+
export function locusResolves(root: string, locus: string): boolean {
|
|
179
|
+
const match = /^(.*?):(\d+)$/.exec(locus);
|
|
180
|
+
const rel = match ? (match[1] ?? '') : locus;
|
|
181
|
+
const target = path.join(root, rel);
|
|
182
|
+
let stat: fs.Stats;
|
|
183
|
+
try {
|
|
184
|
+
stat = fs.statSync(target);
|
|
185
|
+
} catch {
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
if (!match) return true;
|
|
189
|
+
if (!stat.isFile()) return false;
|
|
190
|
+
// A line anchor must still BE a line. Content is deliberately not compared:
|
|
191
|
+
// a locus that pins prose would rot on every reflow and turn the inventory
|
|
192
|
+
// into a line-number chore, which is how anchor checks get deleted.
|
|
193
|
+
const line = Number(match[2]);
|
|
194
|
+
let text: string;
|
|
195
|
+
try {
|
|
196
|
+
text = fs.readFileSync(target, 'utf-8');
|
|
197
|
+
} catch {
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
return line >= 1 && line <= text.split('\n').length;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export interface SurfaceCounts {
|
|
204
|
+
public_continuity_commands: number;
|
|
205
|
+
session_resume_pickers: number;
|
|
206
|
+
persistent_continuity_artefacts: number;
|
|
207
|
+
continuity_schemas: number;
|
|
208
|
+
normal_path_manual_actions: number;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export function countAxes(inv: Inventory): SurfaceCounts {
|
|
212
|
+
const counts = {
|
|
213
|
+
public_continuity_commands: 0,
|
|
214
|
+
session_resume_pickers: 0,
|
|
215
|
+
persistent_continuity_artefacts: 0,
|
|
216
|
+
continuity_schemas: 0,
|
|
217
|
+
normal_path_manual_actions: 0,
|
|
218
|
+
};
|
|
219
|
+
for (const row of inv.rows) {
|
|
220
|
+
if (row.disposition !== 'counted') continue;
|
|
221
|
+
if (!AXES.includes(row.axis)) continue;
|
|
222
|
+
counts[row.axis] += 1;
|
|
223
|
+
}
|
|
224
|
+
return counts;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export interface InventoryFinding {
|
|
228
|
+
kind: 'undisposed-leaf' | 'dead-locus' | 'bad-row' | 'duplicate-row';
|
|
229
|
+
detail: string;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Validate the inventory against the tree.
|
|
234
|
+
*
|
|
235
|
+
* Three failure classes, and each exists because a specific way of faking a
|
|
236
|
+
* lower number would otherwise work:
|
|
237
|
+
*
|
|
238
|
+
* - `undisposed-leaf` — a persisted-state leaf the source names and the
|
|
239
|
+
* inventory does not mention. This is the add-a-surface-quietly route.
|
|
240
|
+
* - `dead-locus` — a row anchored to something that no longer exists. This
|
|
241
|
+
* is the claim-a-retirement-that-did-not-happen route in reverse: it forces
|
|
242
|
+
* the row to be deleted (a real subtraction) rather than left standing.
|
|
243
|
+
* - `bad-row` / `duplicate-row` — shape defects that would make either of the
|
|
244
|
+
* above unenforceable.
|
|
245
|
+
*/
|
|
246
|
+
export function validateInventory(
|
|
247
|
+
root: string,
|
|
248
|
+
inv: Inventory,
|
|
249
|
+
discovered: readonly DiscoveredLeaf[],
|
|
250
|
+
): InventoryFinding[] {
|
|
251
|
+
const findings: InventoryFinding[] = [];
|
|
252
|
+
const seen = new Set<string>();
|
|
253
|
+
for (const row of inv.rows) {
|
|
254
|
+
const id = typeof row.id === 'string' ? row.id : '';
|
|
255
|
+
if (id === '') {
|
|
256
|
+
findings.push({ kind: 'bad-row', detail: 'a row carries no `id`' });
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
if (seen.has(id)) {
|
|
260
|
+
findings.push({ kind: 'duplicate-row', detail: `two rows share the id \`${id}\`` });
|
|
261
|
+
}
|
|
262
|
+
seen.add(id);
|
|
263
|
+
if (!AXES.includes(row.axis)) {
|
|
264
|
+
findings.push({ kind: 'bad-row', detail: `\`${id}\`: unknown axis \`${String(row.axis)}\`` });
|
|
265
|
+
}
|
|
266
|
+
if (row.disposition !== 'counted' && row.disposition !== 'excluded') {
|
|
267
|
+
findings.push({
|
|
268
|
+
kind: 'bad-row',
|
|
269
|
+
detail: `\`${id}\`: disposition must be \`counted\` or \`excluded\`, got \`${String(row.disposition)}\``,
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
if (typeof row.reason !== 'string' || row.reason.trim().length < 20) {
|
|
273
|
+
findings.push({
|
|
274
|
+
kind: 'bad-row',
|
|
275
|
+
detail: `\`${id}\`: every row needs a reason of substance — a number without a stated meaning is not an inventory`,
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
if (typeof row.locus !== 'string' || row.locus.trim() === '') {
|
|
279
|
+
findings.push({ kind: 'bad-row', detail: `\`${id}\`: no \`locus\`` });
|
|
280
|
+
} else if (!locusResolves(root, row.locus)) {
|
|
281
|
+
findings.push({
|
|
282
|
+
kind: 'dead-locus',
|
|
283
|
+
detail: `\`${id}\`: locus \`${row.locus}\` does not resolve — if the surface was retired, DELETE the row (that is the subtraction); if it moved, re-anchor it`,
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
for (const item of discovered) {
|
|
288
|
+
if (!seen.has(item.leaf)) {
|
|
289
|
+
findings.push({
|
|
290
|
+
kind: 'undisposed-leaf',
|
|
291
|
+
detail: `\`${item.leaf}\` (${item.locus}) is persisted state under agents/runtime/state/ that the inventory does not dispose of — add a row with \`disposition: counted\` (it is continuity state) or \`disposition: excluded\` with the reason it is not`,
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
return findings;
|
|
296
|
+
}
|
|
@@ -46,7 +46,19 @@ import {
|
|
|
46
46
|
import { isExclusivelyPackageOnly } from '../../install/partitionEligibility.js';
|
|
47
47
|
import { dedupableRules } from '../../install/ruleLayerPartition.js';
|
|
48
48
|
|
|
49
|
-
|
|
49
|
+
/**
|
|
50
|
+
* Bumped to v2 on 2026-09-07 with the `partition_active` -> `host_layer_verified`
|
|
51
|
+
* rename, after a neutral review named the alternative: `parseManifest` accepts
|
|
52
|
+
* any object carrying the declared version and validates three fields, and
|
|
53
|
+
* `diffManifests` compares with `String(e) !== String(a)`. So a v1 manifest
|
|
54
|
+
* (which has `partition_active` and no `host_layer_verified`) compared against
|
|
55
|
+
* another v1 yields `"undefined" === "undefined"` on the renamed field — NO
|
|
56
|
+
* difference reported, on a field neither side has. That is verbatim the failure
|
|
57
|
+
* `parseManifest`'s own error text says the version check exists to prevent:
|
|
58
|
+
* "comparing it partially would report equivalence from the fields that happened
|
|
59
|
+
* to survive". A rename inside a frozen version is exactly that.
|
|
60
|
+
*/
|
|
61
|
+
export const CORPUS_MANIFEST_VERSION = 'corpus-manifest-v2';
|
|
50
62
|
|
|
51
63
|
/** The tool directory the experiment's corpus is enumerated from. */
|
|
52
64
|
export const CORPUS_TOOL_DIR = '.claude/rules';
|
|
@@ -142,7 +154,7 @@ export interface ProjectionDecision {
|
|
|
142
154
|
* `null` is the default and is the honest value for a capture that cannot
|
|
143
155
|
* establish it. Every other field in this struct is a function of the
|
|
144
156
|
* `(repoRoot, userHome)` pair the capture was given; this one is not.
|
|
145
|
-
* `
|
|
157
|
+
* `hostLayerVerified` resolves through `resolveHostLayerVerdict`, which reads
|
|
146
158
|
* the host layer at `os.homedir()` and the install lockfile at its own
|
|
147
159
|
* fixed location — ignoring `userHome` entirely — and memoises the answer
|
|
148
160
|
* for the life of the process. So a capture over a temporary tree that
|
|
@@ -150,7 +162,7 @@ export interface ProjectionDecision {
|
|
|
150
162
|
* one process would reuse the first verdict. A caller for which those
|
|
151
163
|
* globals ARE the run's own host supplies the predicate; nobody else does.
|
|
152
164
|
*/
|
|
153
|
-
readonly
|
|
165
|
+
readonly host_layer_verified: boolean | null;
|
|
154
166
|
readonly tool_id: string | null;
|
|
155
167
|
/** The global layer directory, home-relative; `null` when this host has none. */
|
|
156
168
|
readonly layer_dir: string | null;
|
|
@@ -365,7 +377,7 @@ export interface ProjectionDecisionOptions {
|
|
|
365
377
|
* The host partition verdict, supplied by a caller for which the process
|
|
366
378
|
* globals it reads are this run's own host. Omitted → `null`.
|
|
367
379
|
*/
|
|
368
|
-
readonly
|
|
380
|
+
readonly hostLayerVerified?: ((repoRoot: string) => boolean) | undefined;
|
|
369
381
|
}
|
|
370
382
|
|
|
371
383
|
export function captureProjectionDecision(
|
|
@@ -401,7 +413,8 @@ export function captureProjectionDecision(
|
|
|
401
413
|
sha256: layerPath === null ? null : sha256OfFile(path.join(layerPath, n)),
|
|
402
414
|
}));
|
|
403
415
|
return {
|
|
404
|
-
|
|
416
|
+
host_layer_verified:
|
|
417
|
+
opts.hostLayerVerified === undefined ? null : opts.hostLayerVerified(repoRoot),
|
|
405
418
|
tool_id: toolId,
|
|
406
419
|
layer_dir: layerPath === null ? null : homeRelative(layerPath, userHome),
|
|
407
420
|
carries: verdict.carries,
|
|
@@ -451,8 +464,8 @@ export interface CaptureOptions {
|
|
|
451
464
|
readonly limit: number;
|
|
452
465
|
readonly enumerationRule: string;
|
|
453
466
|
readonly env?: Readonly<Record<string, string | undefined>> | undefined;
|
|
454
|
-
/** See {@link ProjectionDecisionOptions.
|
|
455
|
-
readonly
|
|
467
|
+
/** See {@link ProjectionDecisionOptions.hostLayerVerified}. Omitted → `null`. */
|
|
468
|
+
readonly hostLayerVerified?: ((repoRoot: string) => boolean) | undefined;
|
|
456
469
|
/** See {@link UserScopeOptions.ruleNames}. Omitted → the projection-source superset. */
|
|
457
470
|
readonly ruleNames?: readonly string[] | undefined;
|
|
458
471
|
}
|
|
@@ -531,7 +544,7 @@ export function captureManifest(opts: CaptureOptions): CorpusManifest {
|
|
|
531
544
|
user_scope_population:
|
|
532
545
|
opts.ruleNames === undefined ? 'projection-source-superset' : 'caller-supplied',
|
|
533
546
|
projection: captureProjectionDecision(repoRoot, userHome, {
|
|
534
|
-
|
|
547
|
+
hostLayerVerified: opts.hostLayerVerified,
|
|
535
548
|
}),
|
|
536
549
|
generator_config: generatorConfig,
|
|
537
550
|
runtime: { node: process.version, platform: os.platform(), arch: os.arch() },
|
|
@@ -656,7 +669,11 @@ export function diffManifests(expected: CorpusManifest, actual: CorpusManifest):
|
|
|
656
669
|
// The partition half, which is where the observed skips actually came from.
|
|
657
670
|
// Diffed by digest rather than by name list because the inventory runs to
|
|
658
671
|
// three figures and a diff nobody reads is a diff that does not exist.
|
|
659
|
-
push(
|
|
672
|
+
push(
|
|
673
|
+
'projection.host_layer_verified',
|
|
674
|
+
expected.projection.host_layer_verified,
|
|
675
|
+
actual.projection.host_layer_verified,
|
|
676
|
+
);
|
|
660
677
|
push('projection.carries', expected.projection.carries, actual.projection.carries);
|
|
661
678
|
push('projection.reason', expected.projection.reason, actual.projection.reason);
|
|
662
679
|
push('projection.layer_digest', expected.projection.layer_digest, actual.projection.layer_digest);
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* `.agent-settings.yml` without dragging a YAML parser into a hook.
|
|
4
4
|
*
|
|
5
5
|
* Every default-OFF hook needs the same two-level lookup, and the tree already
|
|
6
|
-
* carries two hand-rolled copies of it (`
|
|
6
|
+
* carries two hand-rolled copies of it (`code_graph_context_hook (no flag)` and the
|
|
7
7
|
* equivalent inside `design_slop_hook`). Adding a third copy for the route
|
|
8
8
|
* nudge is what this file prevents; the two existing copies are pre-existing
|
|
9
9
|
* duplication and are deliberately left alone — folding them in is a separate,
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Does a delivered rule body actually reach the model's context on this host?
|
|
3
|
+
*
|
|
4
|
+
* `road-to-delivery-on-hook-hosts` step 1.1, and the record owner ruling E3
|
|
5
|
+
* admits a host on.
|
|
6
|
+
*
|
|
7
|
+
* THE ONE THING THIS MODULE EXISTS TO REFUSE
|
|
8
|
+
* ------------------------------------------
|
|
9
|
+
* A host that BINDS `user_prompt_submit` looks, from every artefact in this
|
|
10
|
+
* tree, exactly like a host that DELIVERS. Cowork is the proof that they are
|
|
11
|
+
* different: it binds eight slots and its trampoline discards dispatcher output
|
|
12
|
+
* and `exit 0`s (`docs/enforcement-by-host.md:21`). Cursor and Cline bind the
|
|
13
|
+
* same slot the delivery concern needs, and whether their hook stdout reaches
|
|
14
|
+
* the model is unmeasured.
|
|
15
|
+
*
|
|
16
|
+
* So the binding is computable and the effect is not. This module keeps them
|
|
17
|
+
* apart by construction:
|
|
18
|
+
*
|
|
19
|
+
* · `emits` — computed. Is the concern bound on the slot at all?
|
|
20
|
+
* Read from the manifest. Cheap, always available,
|
|
21
|
+
* and NEVER promoted to an observation.
|
|
22
|
+
* · `injection_effect` — observed. Did a model visibly act on a delivered
|
|
23
|
+
* body? Read from a committed record and from nowhere
|
|
24
|
+
* else. There is no code path that derives it.
|
|
25
|
+
*
|
|
26
|
+
* An `observed-*` value is admissible only with the four-part citation the
|
|
27
|
+
* host-capability observation protocol requires (host · host version ·
|
|
28
|
+
* transcript or artefact reference · date) — see
|
|
29
|
+
* `_lib/host_capability.ts:183`. A record missing any part is rejected rather
|
|
30
|
+
* than downgraded, because a half-cited observation is the shape that gets
|
|
31
|
+
* quoted as a whole one.
|
|
32
|
+
*
|
|
33
|
+
* `unobserved` is a RESULT, not a gap. Most hosts cannot be observed from a
|
|
34
|
+
* Claude Code session at all, and the roadmap's K6 makes that an outcome: the
|
|
35
|
+
* host keeps its full rule bodies and loses nothing while it waits.
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
import * as fs from 'node:fs';
|
|
39
|
+
import * as path from 'node:path';
|
|
40
|
+
|
|
41
|
+
/** Where the committed observations live. One file, so there is one place to audit. */
|
|
42
|
+
export const RECORD_REL = path.join('src', 'config', 'host-injection-effect.json');
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* The three states, and the reason there is no fourth.
|
|
46
|
+
*
|
|
47
|
+
* `observed-false` is not "probably not" — it is a recorded observation that a
|
|
48
|
+
* delivered body did NOT reach the model, which is as citable as the positive.
|
|
49
|
+
* Anything not observed in either direction is `unobserved`.
|
|
50
|
+
*/
|
|
51
|
+
export const INJECTION_STATES = ['observed-true', 'observed-false', 'unobserved'] as const;
|
|
52
|
+
export type InjectionState = (typeof INJECTION_STATES)[number];
|
|
53
|
+
|
|
54
|
+
export interface InjectionObservation {
|
|
55
|
+
readonly state: InjectionState;
|
|
56
|
+
/** Host version the observation was taken on. Required for an `observed-*` state. */
|
|
57
|
+
readonly host_version: string | null;
|
|
58
|
+
/** Transcript or artefact reference. Required for an `observed-*` state. */
|
|
59
|
+
readonly transcript: string | null;
|
|
60
|
+
/** ISO date of the observation. Required for an `observed-*` state. */
|
|
61
|
+
readonly date: string | null;
|
|
62
|
+
/** Why it reads the way it does — required always, including for `unobserved`. */
|
|
63
|
+
readonly reason: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export type InjectionRecord = Readonly<Record<string, InjectionObservation>>;
|
|
67
|
+
|
|
68
|
+
/** A malformed or under-cited row, in the words a maintainer needs to fix it. */
|
|
69
|
+
export interface InjectionProblem {
|
|
70
|
+
readonly host: string;
|
|
71
|
+
readonly message: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Validate a record.
|
|
76
|
+
*
|
|
77
|
+
* The citation requirement is enforced HERE rather than at the reader, so every
|
|
78
|
+
* consumer gets the same answer: an `observed-*` row without all three citation
|
|
79
|
+
* fields is a problem, and `unobserved` needs only its reason.
|
|
80
|
+
*/
|
|
81
|
+
export function recordProblems(rec: InjectionRecord): InjectionProblem[] {
|
|
82
|
+
const out: InjectionProblem[] = [];
|
|
83
|
+
for (const [host, obs] of Object.entries(rec)) {
|
|
84
|
+
// A leading `_comment` is the convention every config JSON in this tree
|
|
85
|
+
// uses to carry its own contract, and it is not a host row. Skipping the
|
|
86
|
+
// underscore prefix rather than a literal `_comment` keeps a second
|
|
87
|
+
// annotation key from becoming a fake host with a fake verdict.
|
|
88
|
+
if (host.startsWith('_')) continue;
|
|
89
|
+
if (!(INJECTION_STATES as readonly string[]).includes(obs.state)) {
|
|
90
|
+
out.push({ host, message: `state ${JSON.stringify(obs.state)} is not one of ${INJECTION_STATES.join(' | ')}` });
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (typeof obs.reason !== 'string' || obs.reason.trim().length < 20) {
|
|
94
|
+
out.push({ host, message: 'reason is missing or shorter than 20 characters — an unexplained row reads as answered' });
|
|
95
|
+
}
|
|
96
|
+
if (obs.state === 'unobserved') continue;
|
|
97
|
+
for (const field of ['host_version', 'transcript', 'date'] as const) {
|
|
98
|
+
const v = obs[field];
|
|
99
|
+
if (typeof v !== 'string' || v.trim() === '') {
|
|
100
|
+
out.push({
|
|
101
|
+
host,
|
|
102
|
+
message:
|
|
103
|
+
`state ${obs.state} without \`${field}\` — the observation protocol ` +
|
|
104
|
+
'(host · host version · transcript · date) admits no partial citation',
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return out;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Load the committed record. A missing file is an empty record, never a throw. */
|
|
113
|
+
export function loadRecord(root: string): InjectionRecord {
|
|
114
|
+
try {
|
|
115
|
+
return JSON.parse(fs.readFileSync(path.join(root, RECORD_REL), 'utf-8')) as InjectionRecord;
|
|
116
|
+
} catch {
|
|
117
|
+
return {};
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* The state for one host — `unobserved` when the record says nothing.
|
|
123
|
+
*
|
|
124
|
+
* Defaulting to `unobserved` rather than to a computed guess is the whole
|
|
125
|
+
* contract: a host nobody has looked at must never inherit a neighbour's
|
|
126
|
+
* verdict, and a bound slot must never imply a delivered body.
|
|
127
|
+
*/
|
|
128
|
+
export function stateFor(rec: InjectionRecord, host: string): InjectionState {
|
|
129
|
+
return rec[host]?.state ?? 'unobserved';
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Is the concern bound on this host's slot? Computed from the manifest.
|
|
134
|
+
*
|
|
135
|
+
* Deliberately a separate function from `stateFor`, with a name that cannot be
|
|
136
|
+
* mistaken for it. This answers "could a body be emitted here", never "did one
|
|
137
|
+
* arrive".
|
|
138
|
+
*/
|
|
139
|
+
export function concernBound(
|
|
140
|
+
platforms: Readonly<Record<string, Readonly<Record<string, readonly string[]>>>>,
|
|
141
|
+
host: string,
|
|
142
|
+
slot: string,
|
|
143
|
+
concern: string,
|
|
144
|
+
): boolean {
|
|
145
|
+
return (platforms[host]?.[slot] ?? []).includes(concern);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Does this host admit to `lean_projection.hosts` under E3? Observation only. */
|
|
149
|
+
export function admissibleUnderE3(rec: InjectionRecord, host: string): boolean {
|
|
150
|
+
return stateFor(rec, host) === 'observed-true';
|
|
151
|
+
}
|
|
@@ -54,12 +54,16 @@ const TYPES = ['rules', 'skills', 'commands', 'personas', 'user-types', 'agents'
|
|
|
54
54
|
export function overlapFindings(homeDir: string, projectRoot: string): string[] {
|
|
55
55
|
const findings: string[] = [];
|
|
56
56
|
for (const type of TYPES) {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
57
|
+
// `commands` keys on the POSIX SUBPATH, matching
|
|
58
|
+
// `check_single_delivery`'s own key. Top-level names are the CLUSTER, and
|
|
59
|
+
// since per-name withholding (2026-09-07) the project layer holds exactly
|
|
60
|
+
// the commands the host lacks — which can share a cluster with commands the
|
|
61
|
+
// host has. Observed the same day: `analyze/` in both layers over two
|
|
62
|
+
// disjoint command sets, reported here as `commands=1`. This notice and
|
|
63
|
+
// that gate must not disagree about the same question.
|
|
64
|
+
const g = _entries(path.join(homeDir, '.claude', type), type === 'commands');
|
|
65
|
+
const p = _entries(path.join(projectRoot, '.claude', type), type === 'commands');
|
|
66
|
+
if (g === null || p === null) {
|
|
63
67
|
continue;
|
|
64
68
|
}
|
|
65
69
|
const gset = new Set(g);
|
|
@@ -69,6 +73,46 @@ export function overlapFindings(homeDir: string, projectRoot: string): string[]
|
|
|
69
73
|
return findings;
|
|
70
74
|
}
|
|
71
75
|
|
|
76
|
+
/**
|
|
77
|
+
* One layer's entry names — top level, or every `*.md` subpath when `recursive`.
|
|
78
|
+
*
|
|
79
|
+
* `null` means the directory is not readable, which the caller must treat as "skip
|
|
80
|
+
* this type": one layer absent means nothing is doubled there.
|
|
81
|
+
*/
|
|
82
|
+
function _entries(dir: string, recursive: boolean): string[] | null {
|
|
83
|
+
if (!recursive) {
|
|
84
|
+
try {
|
|
85
|
+
return fs.readdirSync(dir);
|
|
86
|
+
} catch {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
let top: fs.Dirent[];
|
|
91
|
+
try {
|
|
92
|
+
top = fs.readdirSync(dir, { withFileTypes: true });
|
|
93
|
+
} catch {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
const out: string[] = [];
|
|
97
|
+
const walk = (kids: fs.Dirent[], abs: string, rel: string): void => {
|
|
98
|
+
for (const k of kids) {
|
|
99
|
+
const childRel = rel === '' ? k.name : `${rel}/${k.name}`;
|
|
100
|
+
if (k.isDirectory()) {
|
|
101
|
+
try {
|
|
102
|
+
walk(fs.readdirSync(path.join(abs, k.name), { withFileTypes: true }), path.join(abs, k.name), childRel);
|
|
103
|
+
} catch {
|
|
104
|
+
// An unreadable subdirectory contributes no names; the layer
|
|
105
|
+
// itself was readable, so the type stays compared.
|
|
106
|
+
}
|
|
107
|
+
} else if (k.name.endsWith('.md') && k.name !== 'README.md') {
|
|
108
|
+
out.push(childRel);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
walk(top, dir, '');
|
|
113
|
+
return out;
|
|
114
|
+
}
|
|
115
|
+
|
|
72
116
|
/** Render the notice, or `null` when there is nothing to say. */
|
|
73
117
|
export function overlapNotice(homeDir: string, projectRoot: string): string | null {
|
|
74
118
|
const findings = overlapFindings(homeDir, projectRoot);
|