arkgate 4.8.22 → 4.8.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. package/CHANGELOG.md +91 -0
  2. package/README.md +7 -5
  3. package/bin/ark-check-runtime.mjs +27 -78
  4. package/bin/ark-dashboard.mjs +56 -53
  5. package/bin/ark-layer-match.mjs +1117 -38
  6. package/bin/ark-mcp-runtime.mjs +412 -138
  7. package/bin/ark-mcp.mjs +68 -4
  8. package/bin/ark-shared.mjs +39 -0
  9. package/bin/ark.mjs +65 -97
  10. package/bin/lib/adapter-contract-types.mjs +5 -0
  11. package/bin/lib/adapter-contract.mjs +15 -0
  12. package/bin/lib/agent-projection-command.mjs +5 -1
  13. package/bin/lib/analysis-engine.mjs +15 -11
  14. package/bin/lib/architecture-scan.mjs +50 -6
  15. package/bin/lib/ark-order-doctor.mjs +1 -1
  16. package/bin/lib/ark-order-facts.mjs +410 -15
  17. package/bin/lib/ark-order-invariants.mjs +51 -9
  18. package/bin/lib/ark-order-sensors.mjs +28 -101
  19. package/bin/lib/ark-order-types.mjs +2 -0
  20. package/bin/lib/ark-run-doctor.mjs +14 -0
  21. package/bin/lib/ark-run-facts.mjs +434 -11
  22. package/bin/lib/ark-run-sensors.mjs +5 -3
  23. package/bin/lib/arkgate-pins.mjs +91 -0
  24. package/bin/lib/arkrules-sensors.mjs +50 -249
  25. package/bin/lib/check-args.mjs +23 -1
  26. package/bin/lib/ci-and-commands.mjs +16 -6
  27. package/bin/lib/class-source-scan.mjs +479 -0
  28. package/bin/lib/codex-home.mjs +4 -2
  29. package/bin/lib/config-contract-slices.mjs +701 -0
  30. package/bin/lib/config-contract.mjs +5 -2
  31. package/bin/lib/config-warnings.mjs +32 -0
  32. package/bin/lib/dashboard-cli.mjs +62 -0
  33. package/bin/lib/design-delta.mjs +1 -1
  34. package/bin/lib/diagnostic-catalog.mjs +45 -6
  35. package/bin/lib/doctor-advisories.mjs +84 -1
  36. package/bin/lib/doctor-next-actions.mjs +17 -2
  37. package/bin/lib/doctor-plan.mjs +7 -9
  38. package/bin/lib/effective-contract-load.mjs +121 -59
  39. package/bin/lib/enforcement-honesty.mjs +22 -0
  40. package/bin/lib/enforcement-profiles.mjs +40 -4
  41. package/bin/lib/first-run-help.mjs +3 -2
  42. package/bin/lib/flat-parent-importers.mjs +137 -0
  43. package/bin/lib/flat-parent-pilot.mjs +192 -0
  44. package/bin/lib/gate-files.mjs +29 -2
  45. package/bin/lib/git-change-scope.mjs +109 -0
  46. package/bin/lib/github-enforcement.mjs +1 -1
  47. package/bin/lib/hook-templates.mjs +18 -16
  48. package/bin/lib/host-support-matrix.mjs +3 -1
  49. package/bin/lib/html-report-advisories.mjs +2 -0
  50. package/bin/lib/html-report-depth.mjs +3 -1
  51. package/bin/lib/import-resolve.mjs +13 -1
  52. package/bin/lib/install-migrate.mjs +50 -73
  53. package/bin/lib/invariant-coverage.mjs +59 -11
  54. package/bin/lib/mcp-adoption.mjs +9 -1
  55. package/bin/lib/mcp-hook-payload.mjs +401 -61
  56. package/bin/lib/package-manager.mjs +59 -1
  57. package/bin/lib/passthrough-commands.mjs +83 -0
  58. package/bin/lib/persistence-write-hint.mjs +81 -0
  59. package/bin/lib/physical-cohesion.mjs +16 -18
  60. package/bin/lib/pilot-loop.mjs +21 -8
  61. package/bin/lib/pin-dependent-gates.mjs +117 -0
  62. package/bin/lib/policy-delta-io.mjs +168 -10
  63. package/bin/lib/prepare-change.mjs +120 -6
  64. package/bin/lib/prepare-write.mjs +1 -0
  65. package/bin/lib/presets.mjs +3 -5
  66. package/bin/lib/remediation.mjs +23 -4
  67. package/bin/lib/resolved-candidate-facts.mjs +315 -55
  68. package/bin/lib/rules-inventory.mjs +22 -3
  69. package/bin/lib/rules-under-contract.mjs +179 -5
  70. package/bin/lib/scan-files.mjs +32 -0
  71. package/bin/lib/snippet-analysis.mjs +140 -6
  72. package/bin/lib/source-policy.mjs +34 -0
  73. package/bin/lib/stable-hash.mjs +90 -0
  74. package/bin/lib/start-install-recovery.mjs +71 -1
  75. package/bin/lib/start-preview.mjs +30 -0
  76. package/bin/lib/status-command.mjs +33 -4
  77. package/bin/lib/status-manifest.mjs +32 -4
  78. package/bin/lib/team-parliament-io.mjs +79 -75
  79. package/bin/lib/team-parliament.mjs +6 -1
  80. package/bin/lib/write-path-capabilities.mjs +4 -2
  81. package/dist/chunk-A5EIY3HI.cjs +5 -0
  82. package/dist/chunk-MBXCFLO4.js +16 -0
  83. package/dist/chunk-RNQHRDCQ.cjs +16 -0
  84. package/dist/chunk-SVSQVGFJ.js +5 -0
  85. package/dist/{configTypes-CTELo87-.d.ts → configTypes-BJbaeoj4.d.cts} +53 -4
  86. package/dist/configTypes-BJbaeoj4.d.ts +246 -0
  87. package/dist/eslint/index.cjs +17 -4
  88. package/dist/eslint/index.d.cts +359 -0
  89. package/dist/eslint/index.d.ts +178 -158
  90. package/dist/eslint/index.js +10 -4
  91. package/dist/index.cjs +38 -37
  92. package/dist/index.d.cts +5475 -0
  93. package/dist/index.d.ts +3408 -296
  94. package/dist/index.js +38 -37
  95. package/dist/inspectorListen-6DQVJ4DC.js +1 -0
  96. package/dist/inspectorListen-KGMIQ2TF.cjs +1 -0
  97. package/dist/nestjs/index.cjs +1 -18
  98. package/dist/nestjs/index.d.cts +22 -0
  99. package/dist/nestjs/index.d.ts +1 -3
  100. package/dist/nestjs/index.js +1 -18
  101. package/dist/order/index.cjs +1 -1
  102. package/dist/order/index.d.cts +155 -0
  103. package/dist/order/index.d.ts +15 -3
  104. package/dist/order/index.js +1 -1
  105. package/dist/runtime/index.cjs +13 -30
  106. package/dist/{diagnosticCatalog-WD2TPSwZ.d.ts → runtime/index.d.cts} +814 -290
  107. package/dist/runtime/index.d.ts +2678 -108
  108. package/dist/runtime/index.js +13 -30
  109. package/dist/{types-yDNEQ603.d.ts → types-DRinG-vT.d.cts} +576 -140
  110. package/dist/types-DRinG-vT.d.ts +1792 -0
  111. package/docs/README.md +3 -1
  112. package/docs/agent-guide.md +92 -15
  113. package/docs/ai-gates.md +160 -26
  114. package/docs/arkorder.md +32 -15
  115. package/docs/brownfield-adoption.md +5 -1
  116. package/docs/configuration.md +273 -12
  117. package/docs/develop.md +2 -1
  118. package/docs/diagnostics.md +122 -13
  119. package/docs/package-surface.md +31 -8
  120. package/docs/product-voice.md +1 -1
  121. package/package.json +59 -18
  122. package/schemas/ark.analysis-result.schema.json +15 -0
  123. package/schemas/ark.config.schema.json +163 -4
  124. package/schemas/ark.resolved-candidate-facts.schema.json +1 -1
  125. package/schemas/ark.status-manifest.schema.json +22 -0
  126. package/server.json +3 -9
  127. package/templates/agent-skills/README.md +1 -1
  128. package/templates/agent-skills/ark-adopt/SKILL.md +4 -1
  129. package/templates/agent-skills/ark-order/SKILL.md +4 -2
  130. package/templates/agent-skills/ark-runtime/SKILL.md +6 -1
  131. package/templates/arkrules/PersistenceAdapters.json +1 -1
  132. package/templates/arkrules/PresentationAdapters.json +1 -1
  133. package/templates/skills/ark-adopt.md +4 -1
  134. package/templates/skills/ark-order.md +4 -2
  135. package/templates/skills/ark-runtime.md +6 -1
  136. package/dist/types-fQ9-KrBj.d.ts +0 -183
@@ -0,0 +1,192 @@
1
+ /**
2
+ * Doctor suggestion: a universe-level flat file that one child slice imports.
3
+ * Advisory only. Not a finding, not a verdict, not a config field.
4
+ */
5
+ import {
6
+ findDeniedEdgeDecision,
7
+ layerForRelativePath,
8
+ resolveGovernedSlice,
9
+ sliceAliasDestination,
10
+ sliceIdForPath,
11
+ } from '../ark-layer-match.mjs';
12
+ import { destinationKeepsLayerAndSlice } from './physical-cohesion.mjs';
13
+ import { flatParentImporterGraph } from './flat-parent-importers.mjs';
14
+
15
+ function ruleKey(rule) {
16
+ const child = rule?.childSlices;
17
+ if (!child?.sliceFolders?.length) return '';
18
+ return [
19
+ (rule.sliceFolders ?? []).join('\0'),
20
+ rule.sliceIdentity ?? '',
21
+ child.sliceFolders.join('\0'),
22
+ child.sliceIdentity ?? '',
23
+ (child.commonFolders ?? []).join('\0'),
24
+ ].join('\n');
25
+ }
26
+
27
+ function distinctChildRules(rules) {
28
+ const out = [];
29
+ const seen = new Set();
30
+ for (const rule of Array.isArray(rules) ? rules : []) {
31
+ const key = ruleKey(rule);
32
+ if (!key || seen.has(key)) continue;
33
+ seen.add(key);
34
+ out.push(rule);
35
+ }
36
+ return out;
37
+ }
38
+
39
+ function isFlatParent(rel, rule) {
40
+ const universe = sliceIdForPath(rel, rule.sliceFolders, rule.sliceIdentity);
41
+ const raw = sliceIdForPath(rel, rule.childSlices.sliceFolders, rule.childSlices.sliceIdentity);
42
+ return Boolean(universe && raw && raw.toLowerCase() === universe.toLowerCase());
43
+ }
44
+
45
+ /**
46
+ * Importers of each flat file, split by child. A file any non-child imports
47
+ * (universe common, a shared root, another universe, another layer) is marked:
48
+ * moving it into one child would turn that edge into a wall deny.
49
+ */
50
+ function childImporters(flatFiles, graph, rule) {
51
+ const out = new Map();
52
+ for (const target of flatFiles) {
53
+ const universe = resolveGovernedSlice(target, rule).universeId ?? '';
54
+ const ids = new Set();
55
+ let nonChild = false;
56
+ for (const importer of graph.get(target) ?? []) {
57
+ const place = resolveGovernedSlice(importer, rule);
58
+ if (!place.childId || (place.universeId ?? '') !== universe) nonChild = true;
59
+ else ids.add(place.childId);
60
+ }
61
+ out.set(target, { ids, nonChild, importers: [...(graph.get(target) ?? [])] });
62
+ }
63
+ return out;
64
+ }
65
+
66
+ /** Would any recorded importer be denied by the rules after the move? */
67
+ function moveBreaksWall(importers, destFile, rules, layers) {
68
+ const toLayer = layerForRelativePath(destFile, layers);
69
+ for (const importer of importers) {
70
+ const fromLayer = layerForRelativePath(importer, layers);
71
+ if (!fromLayer || !toLayer) return true;
72
+ const denied = findDeniedEdgeDecision(rules, fromLayer, toLayer, {
73
+ fromPath: importer,
74
+ toPath: destFile,
75
+ layers,
76
+ });
77
+ if (denied) return true;
78
+ }
79
+ return false;
80
+ }
81
+
82
+ function basename(rel) {
83
+ const parts = rel.split('/');
84
+ return parts[parts.length - 1] ?? rel;
85
+ }
86
+
87
+ /**
88
+ * @param {{ root: string, files: string[], rules: object[] | undefined, layers: object[] | undefined, facts?: object, ts?: object }} input
89
+ * @returns {object | null}
90
+ */
91
+ export function collectFlatParentPilot(input) {
92
+ const rules = distinctChildRules(input?.rules);
93
+ if (rules.length === 0) return null;
94
+ const files = (Array.isArray(input.files) ? input.files : [])
95
+ .map((file) => String(file).replace(/\\/g, '/'))
96
+ .filter((file) => file.length > 0);
97
+ const layers = input.layers;
98
+ const moves = [];
99
+ const seen = new Set();
100
+ let graph = null;
101
+ for (const rule of rules) {
102
+ const flat = files.filter((file) => isFlatParent(file, rule));
103
+ if (flat.length === 0) continue;
104
+ graph ??= flatParentImporterGraph({ root: input.root, files, facts: input.facts, ts: input.ts });
105
+ const importers = childImporters(flat, graph, rule);
106
+ for (const file of flat) {
107
+ if (seen.has(file)) continue;
108
+ const entry = importers.get(file);
109
+ // Exactly one child and nobody else: two children, or any non-child importer, keeps it in place.
110
+ if (!entry || entry.nonChild || entry.ids.size !== 1) continue;
111
+ const importer = [...entry.ids][0];
112
+ const destination = sliceAliasDestination(file, importer, rule.childSlices);
113
+ if (!destination) continue;
114
+ const destFile = `${destination}/${basename(file)}`;
115
+ const layer = layerForRelativePath(file, layers);
116
+ if (!layer || layer !== layerForRelativePath(destFile, layers)) continue;
117
+ const toPlace = resolveGovernedSlice(destFile, rule);
118
+ if ((toPlace.childId ?? '') !== importer) continue;
119
+ if (moveBreaksWall(entry.importers, destFile, input.rules, layers)) continue;
120
+ const fromPlace = resolveGovernedSlice(file, rule);
121
+ const keepsSlice = destinationKeepsLayerAndSlice(file, destFile, layers, [rule]);
122
+ const wallSame =
123
+ (fromPlace.universeId ?? '') === (toPlace.universeId ?? '') &&
124
+ (fromPlace.childId ?? '') === (toPlace.childId ?? '');
125
+ seen.add(file);
126
+ moves.push({
127
+ file,
128
+ importer,
129
+ destination,
130
+ layer,
131
+ keepsLayer: true,
132
+ keepsSlice,
133
+ destinationChild: toPlace.childId,
134
+ agreesWithWall: keepsSlice === wallSame,
135
+ evidence: `One child imports ${file} (${importer}). Move it into ${destination}. The ${layer} layer stays. This is a suggestion with that importer as its evidence.`,
136
+ });
137
+ }
138
+ }
139
+ if (moves.length === 0) return null;
140
+ moves.sort((left, right) => (left.file < right.file ? -1 : left.file > right.file ? 1 : 0));
141
+ return {
142
+ advisory: true,
143
+ notAScore: true,
144
+ proposed: true,
145
+ applied: false,
146
+ neverMechanicalSafe: true,
147
+ moves,
148
+ };
149
+ }
150
+
151
+ /** One pilot candidate per move. Appended after pattern bets and the reshape card. */
152
+ export function flatParentCandidates(section) {
153
+ if (!section || !Array.isArray(section.moves)) return [];
154
+ return section.moves
155
+ .filter((move) => move && typeof move.file === 'string' && move.file.length > 0)
156
+ .map((move) => ({
157
+ source: 'flat-parent',
158
+ target: move.file,
159
+ move: typeof move.evidence === 'string' ? move.evidence : `Move ${move.file} into its one importing child. This is a suggestion.`,
160
+ moveSample: [{ from: move.file, to: `${move.destination}/${basename(move.file)}` }],
161
+ successSignal: 're-run doctor: this file is no longer a single-importer flat parent',
162
+ killSwitch: 'leave the file where it is',
163
+ doNot: [
164
+ 'a suggestion with one importer as its evidence',
165
+ 'leave a file in place when two children import it',
166
+ 'one pilot at a time; re-doctor before the next card',
167
+ 'never weaken the contract to make a move pass',
168
+ ],
169
+ }));
170
+ }
171
+
172
+ export function printFlatParentPilot(section, io) {
173
+ const moves = Array.isArray(section?.moves) ? section.moves : [];
174
+ if (moves.length === 0) return;
175
+ console.log('');
176
+ console.log(io.color.bold('Flat parent files (suggestion)'));
177
+ for (const move of moves.slice(0, 5)) io.line(io.warn, move.evidence);
178
+ if (moves.length > 5) io.line(' ', io.color.dim(`…(+${moves.length - 5} more in doctor JSON)`));
179
+ io.line(' ', io.color.dim('suggestion only — one importer is the evidence; the gate verdict is unchanged'));
180
+ }
181
+
182
+ export function flatParentPilotHtml(section, escape) {
183
+ const moves = Array.isArray(section?.moves) ? section.moves : [];
184
+ if (moves.length === 0) return '';
185
+ const esc = typeof escape === 'function' ? escape : (value) => String(value);
186
+ const items = moves
187
+ .slice(0, 5)
188
+ .map((move) => `<li>${esc(move.evidence ?? '')}</li>`)
189
+ .join('');
190
+ const more = moves.length > 5 ? `<p class="muted">…(+${moves.length - 5} more in doctor JSON)</p>` : '';
191
+ return `<section class="section card" data-advisory="flatParentPilot"><h2>Flat parent files <span class="muted">(suggestion — one importer is the evidence; not a score; the verdict is unchanged)</span></h2><ul>${items}</ul>${more}</section>`;
192
+ }
@@ -3,9 +3,11 @@
3
3
  */
4
4
  import fs from 'node:fs';
5
5
  import path from 'node:path';
6
+ import { createRequire } from 'node:module';
6
7
  import { fileURLToPath } from 'node:url';
7
8
  import { codexProjectMcpIsValid } from './codex-home.mjs';
8
9
  import { enforcingArkRunText, runsArkCheck } from './github-enforcement.mjs';
10
+ import { npxArkgatePrefixLength } from './package-manager.mjs';
9
11
 
10
12
  export const __packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
11
13
  export const __arkCheckCli = path.join(__packageRoot, 'bin', 'ark-check.mjs');
@@ -86,10 +88,27 @@ export function treeHasTypecheckScript(root) {
86
88
  * @param {{ write?: boolean }} [opts]
87
89
  * @returns {{
88
90
  * changed: boolean,
89
- * reason: 'added' | 'already' | 'no-tsconfig' | 'no-package-json',
91
+ * reason: 'added' | 'already' | 'no-tsconfig' | 'no-package-json' | 'no-typescript',
90
92
  * script?: string,
91
93
  * }}
92
94
  */
95
+ /**
96
+ * True when `typescript` is declared by the project (deps/devDeps/peerDeps) or resolves
97
+ * from its root (e.g. hoisted in a workspace), so a generated `tsc --noEmit` can run.
98
+ */
99
+ export function projectHasTypeScript(root, pkg = readPackageJson(root) || {}) {
100
+ for (const field of ['dependencies', 'devDependencies', 'peerDependencies']) {
101
+ const deps = pkg?.[field];
102
+ if (deps && typeof deps === 'object' && Object.hasOwn(deps, 'typescript')) return true;
103
+ }
104
+ try {
105
+ createRequire(path.join(root, 'package.json')).resolve('typescript/package.json');
106
+ return true;
107
+ } catch {
108
+ return false;
109
+ }
110
+ }
111
+
93
112
  export function ensureTypecheckScript(root, opts = {}) {
94
113
  const write = opts.write !== false;
95
114
  const hasTsconfig =
@@ -105,6 +124,8 @@ export function ensureTypecheckScript(root, opts = {}) {
105
124
  }
106
125
 
107
126
  const pkg = readPackageJson(root) || {};
127
+ // `tsc` only exists when typescript resolves — otherwise CI fails with `tsc: not found`.
128
+ if (!projectHasTypeScript(root, pkg)) return { changed: false, reason: 'no-typescript' };
108
129
  const scripts =
109
130
  pkg.scripts && typeof pkg.scripts === 'object' ? { ...pkg.scripts } : {};
110
131
  const script = 'tsc --noEmit';
@@ -194,7 +215,10 @@ function arkMcpArgs(server) {
194
215
  const isArkBin = (value) => /^(?:arkgate-mcp|ark-mcp)(?:\.mjs)?$/.test(executableName(value));
195
216
  if ([server.command, ...args].filter(isArkBin).length !== 1) return null;
196
217
  if (isArkBin(server.command)) return args;
197
- if ((command === 'npx' || command === 'yarn') && isArkBin(args[0])) return args.slice(1);
218
+ const npxPrefix = command === 'npx' ? npxArkgatePrefixLength(args) : 0;
219
+ if ((command === 'npx' || command === 'yarn') && isArkBin(args[npxPrefix])) {
220
+ return args.slice(npxPrefix + 1);
221
+ }
198
222
  if (command === 'pnpm') {
199
223
  const binIndex =
200
224
  args[0] === 'exec'
@@ -285,6 +309,9 @@ export function hasArkMcpRegistration(root, relativePath = '.mcp.json') {
285
309
  */
286
310
  export const MCP_JSON_GATE_FILES = ['.mcp.json', '.cursor/mcp.json', '.agents/mcp_config.json'];
287
311
 
312
+ /** Claude-style hook files: Ark SessionStart/PreToolUse groups are merged, never whole-file replaced. */
313
+ export const CLAUDE_STYLE_HOOK_FILES = ['.claude/settings.json', '.codex/hooks.json'];
314
+
288
315
  /**
289
316
  * Upsert mcpServers.ark into an existing MCP JSON document.
290
317
  * Preserves sibling servers and unknown top-level keys. Returns null if unreadable.
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Git plumbing for diff-scoped checks (`--changed`, `--local`, `--against`, `--persona`).
3
+ *
4
+ * Every path this module returns or reads is relative to the Ark ROOT (the `-C` directory),
5
+ * never to the repository top level, so a package inside a monorepo sees its own files:
6
+ * - `git diff --relative` strips the package prefix and drops paths outside the root;
7
+ * - `git ls-files --others` is already cwd-relative;
8
+ * - `<ref>:./<path>` makes `git show` resolve from the root instead of the top level.
9
+ * Output is NUL-separated (`-z`) so core.quotePath never C-quotes a non-ASCII name.
10
+ * Every listing is checked: a failed or truncated call is an error, never an empty diff.
11
+ */
12
+ import { spawnSync } from 'node:child_process';
13
+ import path from 'node:path';
14
+
15
+ /** Kill hung git instead of stalling CI. */
16
+ export const SPAWN_TIMEOUT_MS = 8000;
17
+ /** Same ceiling as design-delta: a large untracked tree must not overflow the default 1 MiB. */
18
+ export const GIT_MAX_BUFFER = 64 * 1024 * 1024;
19
+
20
+ /** Refs tried, in order, when no `--base` / `--against` is given. */
21
+ export const TEAM_BASE_CANDIDATES = Object.freeze([
22
+ 'origin/dev',
23
+ 'origin/main',
24
+ 'origin/master',
25
+ 'dev',
26
+ 'main',
27
+ 'master',
28
+ ]);
29
+
30
+ export function runGit(cwd, args) {
31
+ return spawnSync('git', ['-C', cwd, '-c', 'core.quotePath=false', ...args], {
32
+ encoding: 'utf8',
33
+ stdio: ['ignore', 'pipe', 'pipe'],
34
+ timeout: SPAWN_TIMEOUT_MS,
35
+ maxBuffer: GIT_MAX_BUFFER,
36
+ });
37
+ }
38
+
39
+ export function safeGitRef(value) {
40
+ return (
41
+ typeof value === 'string' &&
42
+ /^[A-Za-z0-9][A-Za-z0-9._/-]{0,200}$/.test(value) &&
43
+ !value.includes('..')
44
+ );
45
+ }
46
+
47
+ export function discoverTeamBaseRef(root, preferred) {
48
+ if (safeGitRef(preferred)) return preferred;
49
+ for (const candidate of TEAM_BASE_CANDIDATES) {
50
+ const exists = runGit(root, ['rev-parse', '--verify', `${candidate}^{commit}`]);
51
+ if (exists.status === 0) return candidate;
52
+ }
53
+ return null;
54
+ }
55
+
56
+ function gitFailure(label, result) {
57
+ if (result.error) return `git ${label} failed: ${result.error.code || result.error.message}`;
58
+ if (result.status !== 0) {
59
+ const detail = String(result.stderr || '').trim();
60
+ return `git ${label} failed${detail ? `: ${detail}` : ` (status ${result.status})`}`;
61
+ }
62
+ return null;
63
+ }
64
+
65
+ /**
66
+ * Root-relative paths changed vs `baseRef` (committed since the merge base, staged,
67
+ * unstaged, untracked). Fails closed: any git failure returns `ok: false`.
68
+ */
69
+ export function listChangedPaths(root, baseRef) {
70
+ if (!safeGitRef(baseRef)) {
71
+ return { ok: false, paths: [], error: 'Invalid or missing git base ref.' };
72
+ }
73
+ const verify = runGit(root, ['rev-parse', '--verify', `${baseRef}^{commit}`]);
74
+ if (verify.status !== 0) {
75
+ return { ok: false, paths: [], error: `Cannot resolve git ref ${baseRef}.` };
76
+ }
77
+ const calls = [
78
+ [`diff ${baseRef}...HEAD`, ['diff', '--relative', '--name-only', '-z', `${baseRef}...HEAD`]],
79
+ ['diff (unstaged)', ['diff', '--relative', '--name-only', '-z']],
80
+ ['diff --cached', ['diff', '--relative', '--name-only', '-z', '--cached']],
81
+ ['ls-files --others', ['ls-files', '-z', '--others', '--exclude-standard']],
82
+ ];
83
+ const paths = [];
84
+ for (const [label, gitArgs] of calls) {
85
+ const result = runGit(root, gitArgs);
86
+ const failure = gitFailure(label, result);
87
+ if (failure) return { ok: false, paths: [], error: failure };
88
+ paths.push(...String(result.stdout).split('\0'));
89
+ }
90
+ return { ok: true, paths: [...new Set(paths.filter(Boolean))].sort(), error: null };
91
+ }
92
+
93
+ /** Root-relative form of `relPath` for a `<ref>:./<path>` spec; null when outside the root. */
94
+ export function rootRelativeGitPath(root, relPath) {
95
+ if (typeof relPath !== 'string' || !relPath) return null;
96
+ const rel = path.isAbsolute(relPath) ? path.relative(path.resolve(root), relPath) : relPath;
97
+ const normalized = path.posix.normalize(rel.split(path.sep).join('/'));
98
+ if (!normalized || normalized === '.' || normalized === '..' || normalized.startsWith('../')) return null;
99
+ return normalized.replace(/^\.\//, '');
100
+ }
101
+
102
+ /** File text at `baseRef`, resolved relative to the Ark root (not the repo top level). */
103
+ export function gitShowText(root, baseRef, relPath) {
104
+ const rel = rootRelativeGitPath(root, relPath);
105
+ if (!safeGitRef(baseRef) || !rel) return null;
106
+ const shown = runGit(root, ['show', `${baseRef}:./${rel}`]);
107
+ if (shown.error || shown.status !== 0) return null;
108
+ return shown.stdout;
109
+ }
@@ -22,7 +22,7 @@ const SAFE_IF = /^(?:['"]?true['"]?|['"]?\$\{\{\s*(?:true|always\(\))\s*\}\}['"]
22
22
  const SAFE_NEEDS_IF = /^['"]?\$\{\{\s*always\(\)\s*\}\}['"]?$/i;
23
23
  const SAFE_CONTINUE = /^['"]?false['"]?$/i;
24
24
  const ENV_PREFIX = /^(?:(?:env\s+)?(?:[A-Za-z_][A-Za-z0-9_]*=(?:"[^"]*"|'[^']*'|\S+)\s+)*)/;
25
- const DIRECT_ARK = /^(?:(?:npx|yarn)\s+(?:arkgate-check|ark-check)(?=\s|$)|pnpm(?:\s+--\S+)*\s+exec\s+(?:arkgate-check|ark-check)(?=\s|$)|node\s+(?:\S+\/)?bin\/(?:arkgate-check|ark-check)\.mjs(?=\s|$)|(?:arkgate-check|ark-check)(?=\s|$))/;
25
+ const DIRECT_ARK = /^(?:(?:npx(?:\s+(?:-y|--yes))?(?:\s+(?:-p\s+|--package[\s=])arkgate(?:@\S+)?)?|yarn)\s+(?:arkgate-check|ark-check)(?=\s|$)|pnpm(?:\s+--\S+)*\s+exec\s+(?:arkgate-check|ark-check)(?=\s|$)|node\s+(?:\S+\/)?bin\/(?:arkgate-check|ark-check)\.mjs(?=\s|$)|(?:arkgate-check|ark-check)(?=\s|$))/;
26
26
  const CHECK_SCRIPT = /^(?:(?:npm|pnpm)\s+run\s+check:architecture(?=\s|$)|yarn(?:\s+run)?\s+check:architecture(?=\s|$))/;
27
27
  const FAIL_CLOSED = /(?:^|\s)--(?:strict|strict-merge|require-gates)(?=\s|$)/;
28
28
 
@@ -1,22 +1,24 @@
1
1
  // Generated from hook-templates.source.mjs — run npm run generate:packaged-tooling.
2
- import{execCommandParts as l,execRunner as i}from"../ark-shared.mjs";const s="arkgate-mcp";function h(e){const r=i(e);return`${JSON.stringify({version:1,hooks:{preToolUse:[{command:`${r} ${s} --hook --hook-repair --fail-on-new-smells --root . --root-env CURSOR_PROJECT_DIR --config ark.config.json`,matcher:"Write|StrReplace",failClosed:!0,timeout:30}]}},null,2)}
3
- `}function y(e){const r=i(e);return`${JSON.stringify({hooks:{SessionStart:[{hooks:[{type:"command",command:`${r} ${s} --session-context --root . --root-env CLAUDE_PROJECT_DIR --config ark.config.json`}]}],PreToolUse:[{matcher:"Write|Edit|MultiEdit",hooks:[{type:"command",command:`${r} ${s} --hook --hook-repair --fail-on-new-smells --root . --root-env CLAUDE_PROJECT_DIR --config ark.config.json`}]}]}},null,2)}
4
- `}function $(e){const r=i(e);return`${JSON.stringify({hooks:{SessionStart:[{hooks:[{type:"command",timeout:30,command:`${r} ${s} --session-context --root . --root-env CODEX_PROJECT_DIR --config ark.config.json`}]}],PreToolUse:[{matcher:"ApplyPatch|apply_patch|Write|Edit|MultiEdit",hooks:[{type:"command",timeout:30,command:`${r} ${s} --hook --hook-repair --fail-on-new-smells --root . --root-env CODEX_PROJECT_DIR --config ark.config.json`}]}]}},null,2)}
5
- `}function O(e){const{command:r,args:o}=l(e,s,["--root",".","--config","ark.config.json"]),t=a=>a.replace(/\\/g,"\\\\").replace(/"/g,'\\"'),c=o.map(a=>`"${t(a)}"`).join(", ");return`# Generated by ark-check --install-agent-gates (Codex project scope).
2
+ import{execCommandParts as d,execRunner as g}from"../ark-shared.mjs";const p="arkgate-mcp";function S(r){const t=g(r);return`${JSON.stringify({version:1,hooks:{preToolUse:[{command:`${t} ${p} --hook --hook-repair --fail-on-new-smells --root . --root-env CURSOR_PROJECT_DIR --config ark.config.json`,matcher:"Write|StrReplace",failClosed:!0,timeout:30}]}},null,2)}
3
+ `}function R(r){const t=g(r);return`${JSON.stringify({hooks:{SessionStart:[{hooks:[{type:"command",command:`${t} ${p} --session-context --root . --root-env CLAUDE_PROJECT_DIR --config ark.config.json`}]}],PreToolUse:[{matcher:"Write|Edit|MultiEdit",hooks:[{type:"command",command:`${t} ${p} --hook --hook-repair --fail-on-new-smells --root . --root-env CLAUDE_PROJECT_DIR --config ark.config.json`}]}]}},null,2)}
4
+ `}function _(r){const t=g(r);return`${JSON.stringify({hooks:{SessionStart:[{hooks:[{type:"command",timeout:30,command:`${t} ${p} --session-context --root . --root-env CODEX_PROJECT_DIR --config ark.config.json`}]}],PreToolUse:[{matcher:"ApplyPatch|apply_patch|Write|Edit|MultiEdit",hooks:[{type:"command",timeout:30,command:`${t} ${p} --hook --hook-repair --fail-on-new-smells --root . --root-env CODEX_PROJECT_DIR --config ark.config.json`}]}]}},null,2)}
5
+ `}function C(r){const{command:t,args:o}=d(r,p,["--root",".","--config","ark.config.json"]),e=n=>n.replace(/\\/g,"\\\\").replace(/"/g,'\\"'),s=o.map(n=>`"${e(n)}"`).join(", ");return`# Generated by ark-check --install-agent-gates (Codex project scope).
6
6
  # CONFIGURED ON DISK \u2014 RUNTIME NOT VERIFIED.
7
7
  # Restart Codex, then call ark_identity with expectedRoot before trusting MCP verdicts.
8
8
  [mcp_servers.ark]
9
- command = "${t(r)}"
10
- args = [${c}]
11
- `}function R(e){const{command:r,args:o}=l(e,s,["--root",".","--config","ark.config.json"]),t=o.map(c=>`"${c.replace(/\\/g,"\\\\").replace(/"/g,'\\"')}"`).join(", ");return`# Generated by ark-check --install-agent-gates (Grok Build project scope).
9
+ command = "${e(t)}"
10
+ args = [${s}]
11
+ `}function E(r){const{command:t,args:o}=d(r,p,["--root",".","--config","ark.config.json"]),e=o.map(s=>`"${s.replace(/\\/g,"\\\\").replace(/"/g,'\\"')}"`).join(", ");return`# Generated by ark-check --install-agent-gates (Grok Build project scope).
12
12
  # Restart Grok (or /mcps \u2192 refresh) after changes. Also loads repo-root .mcp.json.
13
13
  [mcp_servers.ark]
14
- command = "${r.replace(/\\/g,"\\\\").replace(/"/g,'\\"')}"
15
- args = [${t}]
16
- `}function _(e){const r=i(e);return`${JSON.stringify({hooks:{SessionStart:[{hooks:[{type:"command",timeout:30,command:`${r} ${s} --session-context --root . --root-env GROK_WORKSPACE_ROOT,CLAUDE_PROJECT_DIR --config ark.config.json`}]}],PreToolUse:[{matcher:"Write|Edit|MultiEdit|write|search_replace",hooks:[{type:"command",timeout:30,command:`${r} ${s} --hook --hook-repair --fail-on-new-smells --root . --root-env GROK_WORKSPACE_ROOT,CLAUDE_PROJECT_DIR --config ark.config.json`}]}]}},null,2)}
17
- `}function A(e){const r=i(e);return`${JSON.stringify({"ark-write-gate":{PreToolUse:[{matcher:"write_to_file|replace_file_content|multi_replace_file_content",hooks:[{type:"command",timeout:30,command:`${r} ${s} --hook --hook-repair --fail-on-new-smells --root . --config ark.config.json`}]}]}},null,2)}
18
- `}function j(e){const{command:r,args:o}=l(e,s,["--root",".","--config","ark.config.json"]);return`${JSON.stringify({$schema:"https://opencode.ai/config.json",mcp:{ark:{type:"local",command:[r,...o],enabled:!0}}},null,2)}
19
- `}function E(e,r){let o,t;try{o=e&&e.trim()?JSON.parse(e):{},t=JSON.parse(r)}catch{return null}if(!o||typeof o!="object"||Array.isArray(o)||!t||typeof t!="object"||Array.isArray(t))return null;const c=t.hooks,p=(Array.isArray(c?.preToolUse)?c.preToolUse:[]).find(n=>n&&typeof n=="object"&&typeof n.command=="string"&&/arkgate-mcp|ark-mcp/.test(n.command)&&/\s--hook(?:\s|$)/.test(` ${n.command} `));if(!p)return null;const m=o.hooks&&typeof o.hooks=="object"&&!Array.isArray(o.hooks)?{...o.hooks}:{},f=Array.isArray(m.preToolUse)?[...m.preToolUse]:[],k=n=>n&&typeof n=="object"&&typeof n.command=="string"&&/arkgate-mcp|ark-mcp/.test(n.command)&&/\s--hook(?:\s|$)/.test(` ${n.command} `)&&(typeof n.matcher!="string"||/Write/.test(n.matcher)||/StrReplace/.test(n.matcher)),g=f.filter(n=>!k(n));m.preToolUse=[...g,p];const u={...o,version:o.version??t.version??1,hooks:m};return`${JSON.stringify(u,null,2)}
20
- `}function S(e,r){let o,t;try{o=e&&e.trim()?JSON.parse(e):{},t=JSON.parse(r)}catch{return null}if(!o||typeof o!="object"||Array.isArray(o)||!t||typeof t!="object"||Array.isArray(t))return null;const c=t["ark-write-gate"];if(!c||typeof c!="object")return null;const a={...o,"ark-write-gate":c};return`${JSON.stringify(a,null,2)}
21
- `}function C(e,r){let o,t;try{o=e&&e.trim()?JSON.parse(e):{},t=JSON.parse(r)}catch{return null}if(!o||typeof o!="object"||Array.isArray(o))return null;const c={...o};!c.$schema&&t.$schema&&(c.$schema=t.$schema);const a=o.mcp&&typeof o.mcp=="object"&&!Array.isArray(o.mcp)?{...o.mcp}:{};return a.ark=t.mcp.ark,c.mcp=a,`${JSON.stringify(c,null,2)}
22
- `}export{s as PREFERRED_MCP_BIN,A as antigravityHooks,y as claudeSettings,$ as codexHooks,O as codexProjectConfig,h as cursorHooks,_ as grokHooks,R as grokProjectConfig,S as mergeAntigravityArkHook,E as mergeCursorArkHook,C as mergeOpencodeArkMcp,j as opencodeProjectConfig};
14
+ command = "${t.replace(/\\/g,"\\\\").replace(/"/g,'\\"')}"
15
+ args = [${e}]
16
+ `}function v(r){const t=g(r);return`${JSON.stringify({hooks:{SessionStart:[{hooks:[{type:"command",timeout:30,command:`${t} ${p} --session-context --root . --root-env GROK_WORKSPACE_ROOT,CLAUDE_PROJECT_DIR --config ark.config.json`}]}],PreToolUse:[{matcher:"Write|Edit|MultiEdit|write|search_replace",hooks:[{type:"command",timeout:30,command:`${t} ${p} --hook --hook-repair --fail-on-new-smells --root . --root-env GROK_WORKSPACE_ROOT,CLAUDE_PROJECT_DIR --config ark.config.json`}]}]}},null,2)}
17
+ `}function J(r){const t=g(r);return`${JSON.stringify({"ark-write-gate":{PreToolUse:[{matcher:"write_to_file|replace_file_content|multi_replace_file_content",hooks:[{type:"command",timeout:30,command:`${t} ${p} --hook --hook-repair --fail-on-new-smells --root . --config ark.config.json`}]}]}},null,2)}
18
+ `}function N(r){const{command:t,args:o}=d(r,p,["--root",".","--config","ark.config.json"]);return`${JSON.stringify({$schema:"https://opencode.ai/config.json",mcp:{ark:{type:"local",command:[t,...o],enabled:!0}}},null,2)}
19
+ `}function x(r,t){let o,e;try{o=r&&r.trim()?JSON.parse(r):{},e=JSON.parse(t)}catch{return null}if(!o||typeof o!="object"||Array.isArray(o)||!e||typeof e!="object"||Array.isArray(e))return null;const s=e.hooks,k=(Array.isArray(s?.preToolUse)?s.preToolUse:[]).find(a=>a&&typeof a=="object"&&typeof a.command=="string"&&/arkgate-mcp|ark-mcp/.test(a.command)&&/\s--hook(?:\s|$)/.test(` ${a.command} `));if(!k)return null;const u=o.hooks&&typeof o.hooks=="object"&&!Array.isArray(o.hooks)?{...o.hooks}:{},l=Array.isArray(u.preToolUse)?[...u.preToolUse]:[],f=a=>a&&typeof a=="object"&&typeof a.command=="string"&&/arkgate-mcp|ark-mcp/.test(a.command)&&/\s--hook(?:\s|$)/.test(` ${a.command} `)&&(typeof a.matcher!="string"||/Write/.test(a.matcher)||/StrReplace/.test(a.matcher)),c=l.filter(a=>!f(a));u.preToolUse=[...c,k];const i={...o,version:o.version??e.version??1,hooks:u};return`${JSON.stringify(i,null,2)}
20
+ `}const O=/(?:^|[\s/])(?:arkgate-mcp|ark-mcp)(?:\.mjs)?(?:\s|$)/,j=/\s--(?:hook|session-context)(?:\s|$)/;function A(r){return!!r&&typeof r=="object"&&typeof r.command=="string"&&O.test(` ${r.command} `)&&j.test(` ${r.command} `)}function P(r,t){let o,e;try{o=r&&r.trim()?JSON.parse(r):{},e=JSON.parse(t)}catch{return null}if(!o||typeof o!="object"||Array.isArray(o)||!e||typeof e!="object"||Array.isArray(e))return null;const s=e.hooks;if(!s||typeof s!="object"||o.hooks!==void 0&&(!o.hooks||typeof o.hooks!="object"||Array.isArray(o.hooks)))return null;const n={...o.hooks??{}};for(const[k,u]of Object.entries(s)){if(!Array.isArray(u))continue;const l=n[k];if(l!==void 0&&!Array.isArray(l))return null;const f=[];for(const c of l??[]){if(!c||typeof c!="object"||!Array.isArray(c.hooks)){f.push(c);continue}const i=c.hooks.filter(a=>!A(a));i.length===c.hooks.length?f.push(c):i.length>0&&f.push({...c,hooks:i})}n[k]=[...f,...u]}return`${JSON.stringify({...o,hooks:n},null,2)}
21
+ `}function m(r){const t=o=>typeof o=="string"?o.replace(/arkgate@[^\s"']+/g,"arkgate@*"):Array.isArray(o)?o.map(t):o&&typeof o=="object"?Object.fromEntries(Object.keys(o).sort().map(e=>[e,t(o[e])])):o;return JSON.stringify(t(r))}function h(r,t){try{const o=JSON.parse(r),e=JSON.parse(t);return!o||typeof o!="object"||Array.isArray(o)||!e||typeof e!="object"||Array.isArray(e)?null:{existing:o,generated:e}}catch{return null}}function y(r){return{status:"removed",text:Object.keys(r).length===0?null:`${JSON.stringify(r,null,2)}
22
+ `}}function U(r,t){const o=h(r,t);if(!o)return{status:"unreadable"};const{existing:e,generated:s}=o;if(e.hooks===void 0)return{status:"absent"};if(!e.hooks||typeof e.hooks!="object"||Array.isArray(e.hooks))return{status:"unreadable"};const n={...e.hooks};let k=0;for(const[l,f]of Object.entries(s.hooks??{})){if(!Array.isArray(f)||n[l]===void 0)continue;if(!Array.isArray(n[l]))return{status:"unreadable"};const c=new Set(f.map(m)),i=[];for(const a of n[l]){if(!(a&&typeof a=="object"&&Array.isArray(a.hooks)&&a.hooks.some(A))){i.push(a);continue}if(!c.has(m(a)))return{status:"customized"};k+=1}i.length>0?n[l]=i:delete n[l]}if(k===0)return{status:"absent"};const u={...e,hooks:n};return Object.keys(n).length===0&&delete u.hooks,y(u)}function T(r,t){const o=h(r,t);if(!o)return{status:"unreadable"};const{existing:e,generated:s}=o,n=e.hooks?.preToolUse;if(n===void 0)return{status:"absent"};if(!Array.isArray(n))return{status:"unreadable"};const k=new Set((Array.isArray(s.hooks?.preToolUse)?s.hooks.preToolUse:[]).map(m)),u=[];let l=0;for(const i of n){if(!(i&&typeof i=="object"&&typeof i.command=="string"&&/arkgate-mcp|ark-mcp/.test(i.command)&&/\s--hook(?:\s|$)/.test(` ${i.command} `))){u.push(i);continue}if(!k.has(m(i)))return{status:"customized"};l+=1}if(l===0)return{status:"absent"};const f={...e.hooks};u.length>0?f.preToolUse=u:delete f.preToolUse;const c={...e,hooks:f};return Object.keys(f).length===0&&delete c.hooks,Object.keys(c).length===1&&c.version===(s.version??1)&&delete c.version,y(c)}function D(r,t){const o=h(r,t);if(!o)return{status:"unreadable"};const{existing:e,generated:s}=o;if(!Object.prototype.hasOwnProperty.call(e,"ark-write-gate"))return{status:"absent"};if(m(e["ark-write-gate"])!==m(s["ark-write-gate"]))return{status:"customized"};const n={...e};return delete n["ark-write-gate"],y(n)}function H(r,t){let o,e;try{o=r&&r.trim()?JSON.parse(r):{},e=JSON.parse(t)}catch{return null}if(!o||typeof o!="object"||Array.isArray(o)||!e||typeof e!="object"||Array.isArray(e))return null;const s=e["ark-write-gate"];if(!s||typeof s!="object")return null;const n={...o,"ark-write-gate":s};return`${JSON.stringify(n,null,2)}
23
+ `}function w(r,t){let o,e;try{o=r&&r.trim()?JSON.parse(r):{},e=JSON.parse(t)}catch{return null}if(!o||typeof o!="object"||Array.isArray(o))return null;const s={...o};!s.$schema&&e.$schema&&(s.$schema=e.$schema);const n=o.mcp&&typeof o.mcp=="object"&&!Array.isArray(o.mcp)?{...o.mcp}:{};return n.ark=e.mcp.ark,s.mcp=n,`${JSON.stringify(s,null,2)}
24
+ `}export{p as PREFERRED_MCP_BIN,J as antigravityHooks,R as claudeSettings,_ as codexHooks,C as codexProjectConfig,S as cursorHooks,v as grokHooks,E as grokProjectConfig,H as mergeAntigravityArkHook,P as mergeClaudeStyleArkHooks,x as mergeCursorArkHook,w as mergeOpencodeArkMcp,N as opencodeProjectConfig,D as removeAntigravityArkHook,U as removeClaudeStyleArkHooks,T as removeCursorArkHook};
@@ -37,7 +37,9 @@ function hostProfile(label, hookPath, hookSurface, hookOperations, hardWrite, re
37
37
  }),
38
38
  // Host-native "deny when the checker cannot run" flag (FS-permission analog).
39
39
  // required = schema field must be true for hard-write evidence (Cursor failClosed).
40
- // unsupported = host has no documented flag; hook-crash behavior is host-owned.
40
+ // unsupported = host has no documented flag. ark-mcp --hook fails closed itself
41
+ // (exit 2 / host deny) when its checker cannot run for a governed source write;
42
+ // only a crash of node itself before the launcher runs stays host-owned.
41
43
  // none = advisory host; no local write block.
42
44
  nativeFailClosed: extras.nativeFailClosed ?? (hardWrite ? 'unsupported' : 'none'),
43
45
  // EH07 minimum ops matrix (hard=false for soft hosts on every listed op).
@@ -13,6 +13,7 @@ import { formatRulesUnderContractHtml } from './rules-under-contract.mjs';
13
13
  import { formatArkRunHtml } from './ark-run-report.mjs';
14
14
  import { formatArkOrderHtml } from './ark-order-report.mjs';
15
15
  import { primaryImprovementCompassNextAction } from './improvement-compass.mjs';
16
+ import { flatParentPilotHtml } from './flat-parent-pilot.mjs';
16
17
 
17
18
  // htmlEscape is injected by the caller (html-report.mjs) — importing it back
18
19
  // would be a dependency cycle, and the repo's own gate blocks that. The
@@ -355,6 +356,7 @@ export function renderAdvisorySections(advisories, escape) {
355
356
  deepModuleCoachHtml(advisories.deepModuleCoach),
356
357
  stewardNudgeHtml(advisories.stewardNudge),
357
358
  sliceIdentityHtml(advisories.sliceIdentity),
359
+ flatParentPilotHtml(advisories.flatParentPilot, esc),
358
360
  contractHealthHtml(advisories.contractHealth),
359
361
  ambientStateHtml(advisories.ambientState),
360
362
  physicalCohesionHtml(advisories.physicalCohesion),
@@ -28,6 +28,7 @@ import { buildDeepModuleCoachAdvisory } from './deep-module-coach.mjs';
28
28
  import { computePhysicalCohesion } from './physical-cohesion.mjs';
29
29
  import { computeReshapeDecisionMemory } from './reshape-decisions.mjs';
30
30
  import { POST_GREEN_LEDE, operatingModeTitle } from './product-copy.mjs';
31
+ import { anyChildWallAdvisory, anySliceAlias } from '../ark-layer-match.mjs';
31
32
 
32
33
  function esc(value) {
33
34
  return String(value)
@@ -174,7 +175,8 @@ export function buildReportDepthPayload(
174
175
  : null,
175
176
  primaryNextAction: postGreenPath?.action ?? dualTruthNext,
176
177
  activeBlockingViolations: activeBlockingCount,
177
- physicalCohesionResidual: physicalCohesionResidualRemains(physicalCohesion),
178
+ physicalCohesionResidual: physicalCohesionResidualRemains(physicalCohesion), childWallAdvisory: anyChildWallAdvisory(config?.rules),
179
+ sliceAliasDebt: anySliceAlias(config?.rules),
178
180
  });
179
181
  // Doctor parity: same physical-cohesion + baseline stale facts as runDoctor.
180
182
  const baselineStale =
@@ -56,7 +56,19 @@ export function readTsconfigAliases(ts, root) {
56
56
  if (!configPath) return { baseUrl: root, aliases: [] };
57
57
  const read = ts.readConfigFile(configPath, ts.sys.readFile);
58
58
  if (read.error) return { baseUrl: root, aliases: [] };
59
- const parsed = ts.parseJsonConfigFileContent(read.config, ts.sys, path.dirname(configPath));
59
+ // Options-only parse: `fileNames` are unused here. With the real `ts.sys` the
60
+ // parser walked the whole `include` tree on every write-hook call (#205 twin).
61
+ const optionsOnlyHost = {
62
+ useCaseSensitiveFileNames: ts.sys.useCaseSensitiveFileNames,
63
+ readDirectory: () => [],
64
+ fileExists: (fileName) => ts.sys.fileExists(fileName),
65
+ readFile: (fileName) => ts.sys.readFile(fileName),
66
+ };
67
+ const parsed = ts.parseJsonConfigFileContent(
68
+ read.config,
69
+ optionsOnlyHost,
70
+ path.dirname(configPath)
71
+ );
60
72
  const opts = parsed.options || {};
61
73
  const baseUrl = opts.baseUrl || path.dirname(configPath);
62
74
  const aliases = [];