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
package/bin/ark-mcp.mjs CHANGED
@@ -23,6 +23,8 @@ function launcherArgs(argv) {
23
23
  failOnNewSmells: /^(?:1|true|yes|on)$/i.test(
24
24
  String(process.env.ARK_FAIL_ON_NEW_SMELLS ?? '').trim()
25
25
  ),
26
+ help: false,
27
+ version: false,
26
28
  };
27
29
  for (let index = 2; index < argv.length; index += 1) {
28
30
  const value = argv[index];
@@ -35,6 +37,8 @@ function launcherArgs(argv) {
35
37
  else if (value === '--config' && argv[index + 1]) args.config = argv[++index];
36
38
  else if (value === '--manifest' && argv[index + 1]) args.manifest = argv[++index];
37
39
  else if (value === '--tsconfig' && argv[index + 1]) args.tsconfig = argv[++index];
40
+ else if (value === '--help' || value === '-h') args.help = true;
41
+ else if (value === '--version' || value === '-V' || value === '-v') args.version = true;
38
42
  }
39
43
  return args;
40
44
  }
@@ -92,13 +96,56 @@ async function tryResidentHook(args, hookInput) {
92
96
  return response;
93
97
  }
94
98
 
99
+ const USAGE = `Usage: arkgate-mcp [--root <dir>] [--config <file>] [--manifest <file>] [--tsconfig <file>]
100
+ [--hook | --hook-repair] [--fail-on-new-smells] [--root-env <VAR[,VAR]>]
101
+ [--session-context]
102
+
103
+ Starts the ArkGate MCP stdio server (JSON-RPC on stdin/stdout). With --hook, runs one
104
+ PreToolUse write-gate evaluation from the host payload on stdin: exit 0 allows, exit 2
105
+ blocks (a gate that cannot run blocks governed source writes too).
106
+
107
+ -h, --help Show this help
108
+ -V, --version Print the arkgate version
109
+ `;
110
+
95
111
  const args = launcherArgs(process.argv);
112
+ if (args.version) {
113
+ const pkg = JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
114
+ process.stdout.write(`${pkg.version}\n`);
115
+ process.exit(0);
116
+ }
117
+ if (args.help) {
118
+ process.stdout.write(USAGE);
119
+ process.exit(0);
120
+ }
96
121
  let hookInput;
122
+ let hookRoot;
97
123
  let residentHandled = false;
98
124
  try {
99
125
  if (args.hook) {
100
126
  hookInput = fs.readFileSync(0, 'utf8');
101
- const resident = await tryResidentHook(args, hookInput);
127
+ // Root: a set --root-env var or a root holding the config, else walk up from the
128
+ // payload (file, cwd, workspace roots) to the nearest config, like ark-check.
129
+ const { discoverHookRoot, hookOutsideArkProjectResponse } = await import(
130
+ './lib/mcp-hook-payload.mjs'
131
+ );
132
+ const discovered = discoverHookRoot({ argv: process.argv, hookInput });
133
+ if (!discovered.configFound && discovered.configExplicit) {
134
+ // No Ark project owns this write (4.8.23 never blocked it either).
135
+ const response = hookOutsideArkProjectResponse({
136
+ hookInput,
137
+ config: args.config,
138
+ grokHookEvent: Boolean(process.env.GROK_HOOK_EVENT),
139
+ });
140
+ if (response.stdout) process.stdout.write(response.stdout);
141
+ if (response.stderr) process.stderr.write(response.stderr);
142
+ process.exitCode = response.status;
143
+ residentHandled = true;
144
+ } else {
145
+ hookRoot = discovered.root;
146
+ args.root = hookRoot;
147
+ }
148
+ const resident = residentHandled ? null : await tryResidentHook(args, hookInput);
102
149
  if (resident) {
103
150
  if (resident.stdout) process.stdout.write(resident.stdout);
104
151
  if (resident.stderr) process.stderr.write(resident.stderr);
@@ -108,9 +155,26 @@ try {
108
155
  }
109
156
  if (!residentHandled) {
110
157
  const runtime = await import('./ark-mcp-runtime.mjs');
111
- await runtime.runArkMcp({ hookInput });
158
+ await runtime.runArkMcp({ hookInput, hookRoot });
112
159
  }
113
160
  } catch (error) {
114
- process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
115
- process.exitCode = 1;
161
+ if (args.hook) {
162
+ // Hosts treat exit 1 as non-blocking: a gate that cannot run must not fail open
163
+ // for governed source writes. Plumbing cases (bad payload, non-source) still allow.
164
+ const { hookFailClosedResponse, hookRootFromArgv } = await import('./lib/mcp-hook-payload.mjs');
165
+ const response = hookFailClosedResponse({
166
+ hookInput,
167
+ error,
168
+ root: hookRoot ?? hookRootFromArgv(process.argv),
169
+ grokHookEvent: Boolean(process.env.GROK_HOOK_EVENT),
170
+ });
171
+ if (response.stdout) process.stdout.write(response.stdout);
172
+ process.stderr.write(
173
+ response.stderr || `${error instanceof Error ? error.message : String(error)}\n`
174
+ );
175
+ process.exitCode = response.status;
176
+ } else {
177
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
178
+ process.exitCode = 1;
179
+ }
116
180
  }
@@ -2262,3 +2262,42 @@ export function loadPolicyPackMeta(packId, packsPath = defaultPolicyPacksPath())
2262
2262
  }
2263
2263
  return pack;
2264
2264
  }
2265
+
2266
+ /**
2267
+ * Unique starter ArkRules ids. Rule ids must be unique across arkrules/*.json
2268
+ * (`ark-check --promote <id> --apply` refuses an id declared in two documents), and several
2269
+ * project layers can clone the same archetype template. A clone for a layer other than the
2270
+ * archetype's own therefore gets its ids prefixed with the kebab-cased layer name
2271
+ * (`INV-*` invariants keep their prefix: `INV-<LAYER>-…`).
2272
+ */
2273
+
2274
+ export function kebabLayerName(layerName) {
2275
+ return String(layerName)
2276
+ .replace(/([a-z0-9])([A-Z])/g, '$1-$2')
2277
+ .replace(/[^A-Za-z0-9]+/g, '-')
2278
+ .replace(/^-|-$/g, '')
2279
+ .toLowerCase();
2280
+ }
2281
+
2282
+ /**
2283
+ * @param {{ layer?: string, structure?: unknown[], invariants?: unknown[] }} archetype parsed template
2284
+ * @param {string} layerName target project layer
2285
+ */
2286
+ export function cloneArchetypeForLayer(archetype, layerName) {
2287
+ const prefix =
2288
+ archetype.layer && archetype.layer !== layerName ? `${kebabLayerName(layerName)}-` : '';
2289
+ const prefixedId = (id) =>
2290
+ id.startsWith('INV-') ? `INV-${prefix.toUpperCase()}${id.slice('INV-'.length)}` : `${prefix}${id}`;
2291
+ const withPrefix = (rules) =>
2292
+ Array.isArray(rules)
2293
+ ? rules.map((rule) =>
2294
+ prefix && rule && typeof rule.id === 'string' ? { ...rule, id: prefixedId(rule.id) } : rule
2295
+ )
2296
+ : rules;
2297
+ return {
2298
+ ...archetype,
2299
+ layer: layerName,
2300
+ structure: withPrefix(archetype.structure),
2301
+ invariants: withPrefix(archetype.invariants),
2302
+ };
2303
+ }
package/bin/ark.mjs CHANGED
@@ -20,7 +20,9 @@ import {
20
20
  resolveOperatingMode,
21
21
  shouldSkipArkgateInstall,
22
22
  } from './ark-shared.mjs';
23
- import { pinArkgateDevDependency, FALSE_GREEN_GAP_ID } from './lib/field-install.mjs';
23
+ import { FALSE_GREEN_GAP_ID } from './lib/field-install.mjs';
24
+
25
+ export { ensureProjectArkgateDependency };
24
26
  import { validateHardWriteRequest } from './lib/enforcement-profiles.mjs';
25
27
  import {
26
28
  applyStartPreview,
@@ -30,7 +32,9 @@ import {
30
32
  renderStartPreview,
31
33
  } from './lib/start-preview.mjs';
32
34
  import {
35
+ ensureProjectArkgateDependency,
33
36
  formatStartPackageInstallFailure,
37
+ pinArkgateForSetup,
34
38
  runStartPackageInstall,
35
39
  } from './lib/start-install-recovery.mjs';
36
40
  import { runUpgradeCommand } from './lib/upgrade-command.mjs';
@@ -47,50 +51,34 @@ import { runStatusCommand } from './lib/status-command.mjs';
47
51
  import { runAgentProjectionCommand } from './lib/agent-projection-command.mjs';
48
52
  import { setupUsage, setupUsageAll, upgradeUsage } from './lib/first-run-help.mjs';
49
53
  import { runUpstreamReportCommand } from './lib/upstream-report.mjs';
54
+ import {
55
+ dashboardHelp,
56
+ isMcpCommand,
57
+ isPassthroughCommand,
58
+ runDashboard,
59
+ runMcp,
60
+ withDashboardHelp,
61
+ } from './lib/passthrough-commands.mjs';
50
62
 
51
63
  const here = path.dirname(fileURLToPath(import.meta.url));
52
64
  const arkCheck = path.join(here, 'ark-check.mjs');
53
- const arkDashboard = path.join(here, 'ark-dashboard.mjs');
54
- const dashboardHelp = `arkgate dashboard (alias ark dashboard) — observability TUI.
55
- Usage: arkgate dashboard [--url <snapshot-url>] [--interval <ms>]
56
- Polls an ArkRun inspector; it does not start the kernel.`;
57
-
58
- function withDashboardHelp(text, detailed) {
59
- const extra = detailed
60
- ? ' arkgate dashboard [--url <snapshot-url>] [--interval <ms>]\n arkgate report [--root <project>] [--json] [--title <text>] [--finding <ref>] [--submit] [--i-confirm-submit]\n'
61
- : '';
62
- const extraDesc = detailed
63
- ? ' dashboard ANSI observability TUI (spawns ark-dashboard).\n report Draft an upstream GitHub issue for pedroknigge/arkgate. Create needs --submit plus confirm. --yes does not submit.\n'
64
- : '';
65
- if (detailed) {
66
- return text
67
- .replace(' arkgate agents-md [--root', `${extra} arkgate agents-md [--root`)
68
- .replace(' agents-md Version-matched', `${extraDesc} agents-md Version-matched`);
69
- }
70
- return text.replace(
71
- ' arkgate-check --doctor status — one next step\n',
72
- ' arkgate-check --doctor status — one next step\n arkgate dashboard observability TUI (inspector)\n arkgate report draft an upstream GitHub issue\n'
73
- );
74
- }
75
-
76
- function runDashboard(passthroughArgs) {
77
- const result = spawnSync(process.execPath, [arkDashboard, ...passthroughArgs], {
78
- stdio: 'inherit',
79
- encoding: 'utf8',
80
- });
81
- return result.status ?? 1;
82
- }
83
-
84
65
  /**
85
66
  * Day-zero architecture picture: freeze origin under `.ark/reports/` as soon as
86
67
  * `ark.config.json` exists — **before** agent docs, skills, CI templates, or cleanups.
87
68
  * Idempotent: origin is written only once (`--report` archive semantics).
88
69
  */
70
+ /**
71
+ * Day-zero HTML lives under `.ark/reports/`, never as a stray root artifact. The report
72
+ * writer appends an `.ark/` rule only to an existing `.gitignore` (EH03); a fresh project
73
+ * without one still sees `.ark/` as untracked.
74
+ */
75
+ const DAY_ZERO_REPORT_PATH = path.join('.ark', 'reports', 'latest.html');
76
+
89
77
  function freezeDayZeroOrigin(root) {
90
78
  const configPath = path.join(root, 'ark.config.json');
91
79
  if (!fs.existsSync(configPath)) {
92
80
  console.log(
93
- ` Skip origin freeze — no ark.config.json yet. After init: ${arkCommand(root, 'ark-check', '--report ark-report.html')}`
81
+ ` Skip origin freeze — no ark.config.json yet. After init: ${arkCommand(root, 'ark-check', '--report .ark/reports/latest.html')}`
94
82
  );
95
83
  return;
96
84
  }
@@ -102,11 +90,14 @@ function freezeDayZeroOrigin(root) {
102
90
  : 'Freezing day-zero architecture picture (origin) before agent docs / gates…'
103
91
  );
104
92
  runArkCheck(
105
- ['--root', root, '--config', 'ark.config.json', '--report', 'ark-report.html'],
93
+ ['--root', root, '--config', 'ark.config.json', '--report', DAY_ZERO_REPORT_PATH],
106
94
  { cwd: root }
107
95
  );
108
96
  }
109
97
 
98
+ /** Commands that read a non-default contract file via --config. */
99
+ const CONFIG_FLAG_COMMANDS = new Set(['preflight', 'status', 'agents-md', 'agent-projection']);
100
+
110
101
  function parseArgs(argv) {
111
102
  const args = {
112
103
  command: undefined,
@@ -191,8 +182,8 @@ function parseArgs(argv) {
191
182
  else if (arg === '--version' || arg === '-V') args.version = true;
192
183
  else if (!arg.startsWith('-') && args.command === undefined) {
193
184
  args.command = arg;
194
- // Dashboard owns its flags (--url/--interval); pass the rest through untouched.
195
- if (arg === 'dashboard' || arg === 'report') {
185
+ // dashboard / report / mcp own their flags; pass the rest through untouched.
186
+ if (isPassthroughCommand(arg)) {
196
187
  args.passthrough = argv.slice(i + 1);
197
188
  break;
198
189
  }
@@ -265,33 +256,6 @@ async function askYesNo(rl, question, defaultYes = true) {
265
256
  return answer === 'y' || answer === 'yes';
266
257
  }
267
258
 
268
- /**
269
- * Pin arkgate in package.json (and optionally run the package manager).
270
- * start calls this so CI/`npx` is not forced to rely on a stale global install.
271
- *
272
- * @param {string} root
273
- * @param {{ install?: boolean, runPackageManager?: boolean }} [opts]
274
- */
275
- export function ensureProjectArkgateDependency(root, opts = {}) {
276
- const install = opts.install !== false;
277
- const runPm = opts.runPackageManager === true;
278
- if (!install) {
279
- return { pinned: { changed: false, reason: 'skipped-no-install' }, installStatus: null };
280
- }
281
- const pinned = pinArkgateDevDependency(root);
282
- let installStatus = null;
283
- let hostOutput = '';
284
- // Only run the package manager after a successful pin change — avoid surprise
285
- // network on every start when arkgate is already listed.
286
- if (runPm && pinned.changed) {
287
- const [command, commandArgs] = packageInstallArgv(root, pinned.version);
288
- const install = runStartPackageInstall(command, commandArgs, root);
289
- installStatus = install.status;
290
- hostOutput = `${install.stdout}\n${install.stderr}`;
291
- }
292
- return { pinned, installStatus, hostOutput };
293
- }
294
-
295
259
  async function resolveArchetypeInteractive(rl, root) {
296
260
  console.log('');
297
261
  console.log('What are you building? (application shape — not a framework name)');
@@ -384,6 +348,10 @@ async function init(args) {
384
348
  console.log('Skipped ark.config.json generation.');
385
349
  }
386
350
 
351
+ // Pin arkgate before the gate templates that call its bins (CI workflow, host hooks, MCP).
352
+ console.log('');
353
+ pinArkgateForSetup(root, args, cliVersion);
354
+
387
355
  // Origin first: contract-on-tree picture before AGENTS.md / skills / CI templates.
388
356
  console.log('');
389
357
  freezeDayZeroOrigin(root);
@@ -447,7 +415,12 @@ async function start(args) {
447
415
  confidence: preview.analysis?.confidence,
448
416
  projectedCoveragePercent: preview.projectedCoverage?.percent,
449
417
  totalFiles: preview.projectedCoverage?.totalFiles,
450
- explicitShape: Boolean(args.archetype || args.preset),
418
+ // An existing ark.config.json is a locked shape: start keeps it verbatim, so an
419
+ // idempotent re-run (or the remove-host restore command) must not be re-gated on
420
+ // auto-detected confidence. Low coverage still surfaces as an unresolved decision.
421
+ explicitShape:
422
+ Boolean(args.archetype || args.preset) ||
423
+ fs.existsSync(path.join(args.root, 'ark.config.json')),
451
424
  force: Boolean(args.force),
452
425
  });
453
426
  if (!gate.ok) {
@@ -493,8 +466,11 @@ async function start(args) {
493
466
  }
494
467
  // After applying exact preview bytes, install the pinned package when requested
495
468
  // (preview itself never runs the package manager — field: start left pin without node_modules).
469
+ // Host removal never needs the package: no network, no package.json edits.
496
470
  if (
497
471
  args.install &&
472
+ !args.removeHost &&
473
+ preview.mode !== 'remove-host' &&
498
474
  !args.skipPackageManager &&
499
475
  fs.existsSync(path.join(args.root, 'package.json'))
500
476
  ) {
@@ -549,9 +525,18 @@ async function start(args) {
549
525
  rec = undefined;
550
526
  }
551
527
 
552
- // 2) Suggest a shape, in plain language, and confirm.
553
- let archetype = rec?.archetype;
554
- if (rec) {
528
+ // 2) Suggest a shape, in plain language, and confirm. An explicit --archetype/--preset
529
+ // locks the shape: it must drive the written contract, not just bypass the preview gate.
530
+ const explicitShape = resolveInitPreset(args);
531
+ let archetype = explicitShape ? explicitShape.archetype : rec?.archetype;
532
+ if (explicitShape) {
533
+ console.log('');
534
+ console.log(
535
+ explicitShape.archetype
536
+ ? `Using your shape: ${explicitShape.archetype} → preset ${explicitShape.preset}.`
537
+ : `Using your preset: ${explicitShape.preset}.`
538
+ );
539
+ } else if (rec) {
555
540
  console.log('');
556
541
  console.log(`Your project looks like: ${rec.label}.`);
557
542
  if (rec.analogy) console.log(`In plain terms — ${rec.analogy}`);
@@ -573,30 +558,7 @@ async function start(args) {
573
558
 
574
559
  // 2b) Pin arkgate as a project devDependency so CI/npx do not depend on a stale global.
575
560
  // Default install=true; only --no-install skips. (installExplicit tracks user override for copy.)
576
- if (args.install && fs.existsSync(path.join(root, 'package.json'))) {
577
- const { pinned, installStatus, hostOutput } = ensureProjectArkgateDependency(root, {
578
- install: true,
579
- runPackageManager: !args.skipPackageManager,
580
- });
581
- if (pinned.changed) {
582
- console.log(` Pinned arkgate@${pinned.version} in package.json devDependencies.`);
583
- if (installStatus !== null && installStatus !== 0) {
584
- const [command, commandArgs] = packageInstallArgv(root, pinned.version);
585
- console.error(
586
- formatStartPackageInstallFailure({
587
- exitStatus: installStatus,
588
- installCommand: `${command} ${commandArgs.join(' ')}`,
589
- hostOutput,
590
- packageVersion: cliVersion(),
591
- })
592
- );
593
- }
594
- } else if (pinned.reason === 'already-present') {
595
- console.log(` arkgate already in package.json (${pinned.version}).`);
596
- }
597
- } else if (!args.install) {
598
- console.log(' Skipping arkgate package pin (--no-install).');
599
- }
561
+ pinArkgateForSetup(root, args, cliVersion);
600
562
 
601
563
  // 3) Contract first (config only). Greenfield → shape preset; established repo → detection,
602
564
  // so the contract anchors to directories you already have instead of aspirational globs.
@@ -605,7 +567,10 @@ async function start(args) {
605
567
  const configPath = path.join(root, 'ark.config.json');
606
568
  if (!fs.existsSync(configPath)) {
607
569
  const initArgs = ['--root', root, '--init'];
608
- const startPreset = resolveStartInitPreset(root, rec ?? {}, archetype);
570
+ // Explicit shape bypasses detection entirely (monorepo/ui-surface heuristics included).
571
+ const startPreset = explicitShape?.preset
572
+ ? explicitShape.preset
573
+ : resolveStartInitPreset(root, rec ?? {}, archetype);
609
574
  const includeRoots = resolveIncludeRoots(root);
610
575
  const tsPackages = detectTsPackageRoots(root);
611
576
  const nestedTsPackages = tsPackages.filter((entry) => entry !== '.');
@@ -814,6 +779,8 @@ async function main() {
814
779
  console.error(error instanceof Error ? error.message : String(error));
815
780
  return 2;
816
781
  }
782
+ // Before --version/--help: the MCP server owns stdout (JSON-RPC) and all of its flags.
783
+ if (isMcpCommand(args.command)) return runMcp(args.passthrough);
817
784
  if (args.version) {
818
785
  console.log(cliVersion());
819
786
  return 0;
@@ -843,12 +810,13 @@ async function main() {
843
810
  }
844
811
  if (
845
812
  args.command !== 'preflight' &&
846
- (args.changes || args.changeMap || args.manifest || args.tsconfig ||
847
- args.config !== 'ark.config.json')
813
+ (args.changes || args.changeMap || args.manifest || args.tsconfig)
848
814
  ) {
849
- console.error(
850
- '--changes, --change-map, --config, --manifest, and --tsconfig are supported by ark preflight.'
851
- );
815
+ console.error('--changes, --change-map, --manifest, and --tsconfig are supported by ark preflight.');
816
+ return 2;
817
+ }
818
+ if (args.config !== 'ark.config.json' && !CONFIG_FLAG_COMMANDS.has(args.command)) {
819
+ console.error('--config is supported by ark preflight, ark status, and ark agents-md.');
852
820
  return 2;
853
821
  }
854
822
  const enforcement = validateHardWriteRequest({
@@ -118,6 +118,11 @@ export const ARK_ANALYSIS_RESULT_SCHEMA = {
118
118
  edgeKind: { type: 'string', minLength: 1 },
119
119
  arkruleId: { type: 'string', minLength: 1 },
120
120
  arkruleSource: { type: 'string', minLength: 1 },
121
+ reasonId: {
122
+ enum: ['CROSS_PARENT_SLICE', 'CROSS_SIBLING_SLICE', 'CROSS_PARENT_VIA_SHARED'],
123
+ },
124
+ universeFrom: { type: 'string', minLength: 1 },
125
+ universeTo: { type: 'string', minLength: 1 },
121
126
  },
122
127
  },
123
128
  nextAction: { type: 'string', minLength: 1 },
@@ -35,8 +35,19 @@ function nextActionForDiagnostic(ruleId, evidence, violation) {
35
35
  capability: text(evidence.capability) ?? text(violation.capability) ?? undefined,
36
36
  arkruleId: text(evidence.arkruleId) ?? undefined,
37
37
  arkruleSource: text(evidence.arkruleSource) ?? undefined,
38
+ reasonId: text(evidence.reasonId) ?? undefined,
38
39
  });
39
40
  }
41
+ const SLICE_REASON_IDS = new Set(['CROSS_PARENT_SLICE', 'CROSS_SIBLING_SLICE', 'CROSS_PARENT_VIA_SHARED']);
42
+ /** Nested-wall reason from the top level or from gate `details`. */
43
+ function sliceReasonOf(violation) {
44
+ const details = violation.details;
45
+ const nested = details !== null && typeof details === 'object' ? details.reasonId : undefined;
46
+ const raw = text(violation.reasonId) ?? text(nested);
47
+ return raw && SLICE_REASON_IDS.has(raw)
48
+ ? raw
49
+ : undefined;
50
+ }
40
51
  export function toAdapterDiagnostic(violation, fallbackSeverity = 'error',
41
52
  /**
42
53
  * Optional precomputed baseline-compatible targetKey (e.g. occurrence-aware from
@@ -50,6 +61,7 @@ targetKeyOverride) {
50
61
  (violation.typeOnly === true && violation.peerIsolation !== true)
51
62
  ? 'warning'
52
63
  : fallbackSeverity;
64
+ const reasonId = sliceReasonOf(violation);
53
65
  const evidence = {
54
66
  ...(text(violation.target) ? { target: text(violation.target) } : {}),
55
67
  ...(text(violation.fromLayer) ? { fromLayer: text(violation.fromLayer) } : {}),
@@ -74,6 +86,9 @@ targetKeyOverride) {
74
86
  ...(text(violation.edgeKind) ? { edgeKind: text(violation.edgeKind) } : {}),
75
87
  ...(text(violation.arkruleId) ? { arkruleId: text(violation.arkruleId) } : {}),
76
88
  ...(text(violation.arkruleSource) ? { arkruleSource: text(violation.arkruleSource) } : {}),
89
+ ...(reasonId ? { reasonId } : {}),
90
+ ...(reasonId && text(violation.universeFrom) ? { universeFrom: text(violation.universeFrom) } : {}),
91
+ ...(reasonId && text(violation.universeTo) ? { universeTo: text(violation.universeTo) } : {}),
77
92
  };
78
93
  const targetKey = targetKeyOverride ?? adapterFindingTargetKey(violation);
79
94
  const findingRef = adapterFindingRefFromTargetKey(targetKey);
@@ -71,15 +71,18 @@ export function collectAgentProjectionFacts(options = {}) {
71
71
  const startRoot = path.resolve(options.root || process.cwd());
72
72
  const configName = options.config || 'ark.config.json';
73
73
  let resolvedRoot = startRoot;
74
+ let configPath = null;
74
75
  try {
75
76
  const resolved = resolveEffectiveProjectRoot(startRoot, {
76
77
  configName,
77
78
  writeMode: false,
78
79
  });
79
80
  resolvedRoot = path.resolve(resolved.root || startRoot);
81
+ if (resolved.configFound && resolved.configPath) configPath = path.resolve(resolved.configPath);
80
82
  } catch {
81
83
  resolvedRoot = startRoot;
82
84
  }
85
+ if (!configPath) configPath = path.resolve(resolvedRoot, configName);
83
86
 
84
87
  const version =
85
88
  (typeof options.arkgateVersion === 'string' && options.arkgateVersion.trim()) ||
@@ -88,7 +91,8 @@ export function collectAgentProjectionFacts(options = {}) {
88
91
 
89
92
  let layers = options.layers;
90
93
  if (layers === undefined) {
91
- layers = loadConfigLayersForAgents(resolvedRoot);
94
+ // Read the contract the caller named (--config), not a hardcoded ark.config.json.
95
+ layers = loadConfigLayersForAgents(resolvedRoot, configPath);
92
96
  }
93
97
 
94
98
  const layerSummaries = Array.isArray(layers)