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
@@ -1,6 +1,10 @@
1
1
  /**
2
2
  * Tooling adapter: load Effective Contract from disk for a root config.
3
3
  * Pure resolution lives in Domain (`resolveEffectiveContract`); this module owns I/O.
4
+ *
5
+ * `loadEffectiveArkRules` is the source-agnostic core (working tree, a policy base
6
+ * ref, or supplied file objects all pass the same path validation); only the
7
+ * reader differs.
4
8
  */
5
9
  import fs from 'node:fs';
6
10
  import path from 'node:path';
@@ -10,7 +14,7 @@ import {
10
14
  loadArkRulesContract,
11
15
  } from './arkrules-contract.mjs';
12
16
 
13
- function normalizeProjectRelativePath(value) {
17
+ export function normalizeProjectRelativePath(value) {
14
18
  const normalized = value.replace(/\\/g, '/');
15
19
  if (
16
20
  !normalized ||
@@ -29,7 +33,7 @@ function normalizeProjectRelativePath(value) {
29
33
  return segments.length > 0 ? segments.join('/') : undefined;
30
34
  }
31
35
 
32
- function isWithinRoot(root, candidate) {
36
+ export function isWithinRoot(root, candidate) {
33
37
  const relative = path.relative(root, candidate);
34
38
  return (
35
39
  relative === '' ||
@@ -40,26 +44,29 @@ function isWithinRoot(root, candidate) {
40
44
  }
41
45
 
42
46
  /**
43
- * @param {string} root
47
+ * @typedef {{ ok: true, content: string } | { ok: false, missing?: boolean, message: string }} ArkRulesRead
48
+ */
49
+
50
+ /**
51
+ * Core loader shared by every source.
52
+ *
44
53
  * @param {Record<string, unknown>} config loaded ark.config.json object
45
- * @param {{ observeInput?: (abs: string, kind: string) => void }} [opts]
46
- * @returns {{ arkRules: ReturnType<typeof emptyEffectiveArkRules>, warnings: Array<{path:string,message:string,severity:string}>, errors: Array<{path:string,message:string}> }}
54
+ * @param {(rel: string) => ArkRulesRead} readRelative
55
+ * @param {{ missingAsEmpty?: boolean }} [opts] missingAsEmpty: a referenced file absent
56
+ * from the source counts as an empty layer (a base that predates the file), not an error.
57
+ * @returns {{ arkRules: ReturnType<typeof emptyEffectiveArkRules>, errors: Array<{path:string,message:string}>, referenced: Set<string> }}
47
58
  */
48
- export function loadEffectiveArkRulesFromDisk(root, config, opts = {}) {
59
+ export function loadEffectiveArkRules(config, readRelative, opts = {}) {
49
60
  const refs = config?.arkRules;
61
+ const referenced = new Set();
50
62
  if (!refs || typeof refs !== 'object' || Object.keys(refs).length === 0) {
51
- return { arkRules: emptyEffectiveArkRules(), warnings: [], errors: [] };
63
+ return { arkRules: emptyEffectiveArkRules(), errors: [], referenced };
52
64
  }
53
-
54
65
  const layerNames = new Set(
55
66
  Array.isArray(config.layers) ? config.layers.map((layer) => layer.name) : []
56
67
  );
57
68
  const errors = [];
58
- const warnings = [];
59
69
  const parts = [];
60
- const referenced = new Set();
61
- const canonicalRoot = fs.realpathSync(root);
62
-
63
70
  for (const layer of Object.keys(refs).sort()) {
64
71
  const relRaw = refs[layer];
65
72
  const pathKey = `$.arkRules[${JSON.stringify(layer)}]`;
@@ -83,70 +90,102 @@ export function loadEffectiveArkRulesFromDisk(root, config, opts = {}) {
83
90
  });
84
91
  continue;
85
92
  }
86
-
87
93
  referenced.add(rel);
88
- const lexicalTarget = path.resolve(canonicalRoot, ...rel.split('/'));
89
- if (!isWithinRoot(canonicalRoot, lexicalTarget)) {
94
+ const read = readRelative(rel);
95
+ if (!read.ok) {
96
+ if (read.missing && opts.missingAsEmpty) continue;
97
+ errors.push({ path: pathKey, message: read.message });
98
+ continue;
99
+ }
100
+ try {
101
+ const loaded = loadArkRulesContract(JSON.parse(read.content), rel, layer);
102
+ parts.push({ layer, sourceFile: rel, file: loaded.config });
103
+ } catch (error) {
90
104
  errors.push({
91
105
  path: pathKey,
92
- message: `referenced ArkRules path ${JSON.stringify(rel)} resolves outside the project root`,
106
+ message:
107
+ error instanceof Error
108
+ ? error.message
109
+ : `referenced ArkRules file ${JSON.stringify(rel)} failed to load`,
93
110
  });
94
- continue;
95
111
  }
112
+ }
113
+ if (errors.length > 0) return { arkRules: emptyEffectiveArkRules(), errors, referenced };
114
+ return { arkRules: buildEffectiveArkRules(parts), errors: [], referenced };
115
+ }
116
+
117
+ /**
118
+ * Directory reader: realpath + within-root guards, then readFileSync.
119
+ * @param {string} canonicalRoot realpath of the directory the paths are relative to
120
+ * @param {{ observeInput?: (abs: string, kind: string) => void }} [opts]
121
+ * @returns {(rel: string) => ArkRulesRead}
122
+ */
123
+ export function directoryArkRulesReader(canonicalRoot, opts = {}) {
124
+ return (rel) => {
125
+ const lexicalTarget = path.resolve(canonicalRoot, ...rel.split('/'));
126
+ const outside = {
127
+ ok: false,
128
+ message: `referenced ArkRules path ${JSON.stringify(rel)} resolves outside the project root`,
129
+ };
130
+ if (!isWithinRoot(canonicalRoot, lexicalTarget)) return outside;
96
131
  if (!fs.existsSync(lexicalTarget)) {
97
- errors.push({
98
- path: pathKey,
132
+ return {
133
+ ok: false,
134
+ missing: true,
99
135
  message: `referenced ArkRules file ${JSON.stringify(rel)} is missing`,
100
- });
101
- continue;
136
+ };
102
137
  }
103
138
  let absolute;
104
139
  try {
105
140
  absolute = fs.realpathSync(lexicalTarget);
106
141
  } catch (error) {
107
- errors.push({
108
- path: pathKey,
142
+ return {
143
+ ok: false,
109
144
  message: `referenced ArkRules file ${JSON.stringify(rel)} could not be resolved: ${
110
145
  error instanceof Error ? error.message : String(error)
111
146
  }`,
112
- });
113
- continue;
114
- }
115
- if (!isWithinRoot(canonicalRoot, absolute)) {
116
- errors.push({
117
- path: pathKey,
118
- message: `referenced ArkRules path ${JSON.stringify(rel)} resolves outside the project root`,
119
- });
120
- continue;
147
+ };
121
148
  }
149
+ if (!isWithinRoot(canonicalRoot, absolute)) return outside;
122
150
  opts.observeInput?.(absolute, 'arkrules');
123
- let content;
124
151
  try {
125
- content = fs.readFileSync(absolute, 'utf8');
152
+ return { ok: true, content: fs.readFileSync(absolute, 'utf8') };
126
153
  } catch (error) {
127
- errors.push({
128
- path: pathKey,
154
+ return {
155
+ ok: false,
129
156
  message: `referenced ArkRules file ${JSON.stringify(rel)} could not be read: ${
130
157
  error instanceof Error ? error.message : String(error)
131
158
  }`,
132
- });
133
- continue;
134
- }
135
- try {
136
- const loaded = loadArkRulesContract(JSON.parse(content), rel, layer);
137
- parts.push({ layer, sourceFile: rel, file: loaded.config });
138
- } catch (error) {
139
- errors.push({
140
- path: pathKey,
141
- message:
142
- error instanceof Error
143
- ? error.message
144
- : `referenced ArkRules file ${JSON.stringify(rel)} failed to load`,
145
- });
159
+ };
146
160
  }
161
+ };
162
+ }
163
+
164
+ /**
165
+ * Reader over supplied ArkRules file objects keyed by project-relative path
166
+ * (MCP callers hand a catalog as data instead of a checkout).
167
+ * @param {Record<string, unknown>} files
168
+ * @returns {(rel: string) => ArkRulesRead}
169
+ */
170
+ export function objectArkRulesReader(files) {
171
+ const byPath = new Map();
172
+ for (const [key, value] of Object.entries(files ?? {})) {
173
+ const rel = normalizeProjectRelativePath(String(key));
174
+ if (rel) byPath.set(rel, value);
147
175
  }
176
+ return (rel) =>
177
+ byPath.has(rel)
178
+ ? { ok: true, content: JSON.stringify(byPath.get(rel)) }
179
+ : {
180
+ ok: false,
181
+ missing: true,
182
+ message: `ArkRules file ${JSON.stringify(rel)} was not supplied`,
183
+ };
184
+ }
148
185
 
149
- // Drift: unreferenced files under arkrules/
186
+ /** Advisory drift: `arkrules/*.json` files the arkRules map does not reference (ADR 0012 D2). */
187
+ function unreferencedArkRulesWarnings(canonicalRoot, referenced) {
188
+ const warnings = [];
150
189
  const arkrulesDir = path.join(canonicalRoot, 'arkrules');
151
190
  const resolvedArkRulesDir = fs.existsSync(arkrulesDir)
152
191
  ? fs.realpathSync(arkrulesDir)
@@ -168,13 +207,36 @@ export function loadEffectiveArkRulesFromDisk(root, config, opts = {}) {
168
207
  }
169
208
  }
170
209
  }
210
+ return warnings;
211
+ }
171
212
 
172
- if (errors.length > 0) {
173
- return { arkRules: emptyEffectiveArkRules(), warnings, errors };
174
- }
175
- return {
176
- arkRules: buildEffectiveArkRules(parts),
177
- warnings,
178
- errors: [],
179
- };
213
+ export const ARKRULE_FILE_UNREFERENCED = 'ARKRULE_FILE_UNREFERENCED';
214
+
215
+ /**
216
+ * Loader drift warnings as advisory check warnings (never fail --strict-config).
217
+ * @param {Array<{path:string,message:string}>} warnings
218
+ */
219
+ export function arkRulesDriftWarnings(warnings) {
220
+ return (warnings ?? []).map((warning) => ({
221
+ ruleId: ARKRULE_FILE_UNREFERENCED,
222
+ file: warning.path,
223
+ line: 1,
224
+ message: warning.message,
225
+ severity: 'warning',
226
+ failsStrict: false,
227
+ }));
228
+ }
229
+
230
+ /**
231
+ * @param {string} root
232
+ * @param {Record<string, unknown>} config loaded ark.config.json object
233
+ * @param {{ observeInput?: (abs: string, kind: string) => void }} [opts]
234
+ * @returns {{ arkRules: ReturnType<typeof emptyEffectiveArkRules>, warnings: Array<{path:string,message:string,severity:string}>, errors: Array<{path:string,message:string}> }}
235
+ */
236
+ export function loadEffectiveArkRulesFromDisk(root, config, opts = {}) {
237
+ const canonicalRoot = fs.realpathSync(root);
238
+ const loaded = loadEffectiveArkRules(config, directoryArkRulesReader(canonicalRoot, opts));
239
+ // Drift runs with or without a map: an arkrules/ directory nobody references is visible.
240
+ const warnings = unreferencedArkRulesWarnings(canonicalRoot, loaded.referenced);
241
+ return { arkRules: loaded.arkRules, warnings, errors: loaded.errors };
180
242
  }
@@ -289,6 +289,8 @@ export function buildWritePathHonesty(activeHost, hardWriteActive = false, extra
289
289
  * operatingMode?: string | null,
290
290
  * activeBlockingViolations?: number | null,
291
291
  * physicalCohesionResidual?: boolean,
292
+ * childWallAdvisory?: boolean,
293
+ * sliceAliasDebt?: boolean,
292
294
  * }} input
293
295
  */
294
296
  export function physicalCohesionResidualRemains(cohesion) {
@@ -489,6 +491,22 @@ export function buildProductHonesty(input = {}) {
489
491
  // Informational only when no enforced arkrule plane — does not alone make unfinished.
490
492
  }
491
493
 
494
+ if (input.childWallAdvisory === true) {
495
+ reasons.push({
496
+ id: 'child-wall-advisory',
497
+ message:
498
+ 'A child wall lists siblings as advisory. Recorded crossings stay non-blocking, and a new sibling crossing past that baseline still fails. An enforce list makes those subtrees errors and does not finish the house. This house stays unfinished until siblings is deny.',
499
+ });
500
+ }
501
+
502
+ if (input.sliceAliasDebt === true) {
503
+ reasons.push({
504
+ id: 'slice-alias-debt',
505
+ message:
506
+ 'Slice aliases are an owed move, not a destination. Aliased files are not finished until they move into the target slice folder.',
507
+ });
508
+ }
509
+
492
510
  // EH05: environment residual deny-list (future reason ids stay architecture debt by default).
493
511
  const ENVIRONMENT_REASON_IDS = new Set(['soft-write-host', 'native-fail-open']);
494
512
 
@@ -683,6 +701,8 @@ export function computeDoctorEnforcementHonesty({
683
701
  nativeFailClosed,
684
702
  nativeFailClosedPolicy,
685
703
  physicalCohesionResidual,
704
+ childWallAdvisory,
705
+ sliceAliasDebt,
686
706
  } = {}) {
687
707
  const coverageHonesty = buildCoverageHonesty({
688
708
  percent: governedPercent,
@@ -741,6 +761,8 @@ export function computeDoctorEnforcementHonesty({
741
761
  emptyStewards,
742
762
  stewardNudge,
743
763
  physicalCohesionResidual,
764
+ childWallAdvisory,
765
+ sliceAliasDebt,
744
766
  });
745
767
  return {
746
768
  coverageHonesty,
@@ -10,6 +10,35 @@ import {
10
10
  HOST_SUPPORT_MATRIX,
11
11
  } from './host-support-matrix.mjs';
12
12
  import { KNOWN_TOOLS, normalizeToolsList } from './skill-install.mjs';
13
+ import {
14
+ antigravityHooks,
15
+ claudeSettings,
16
+ codexHooks,
17
+ cursorHooks,
18
+ mergeAntigravityArkHook,
19
+ mergeClaudeStyleArkHooks,
20
+ mergeCursorArkHook,
21
+ } from './hook-templates.mjs';
22
+
23
+ /** Hosts whose hook file the installer merges (keeping user keys) instead of replacing. */
24
+ const MERGEABLE_HOOK_FILES = {
25
+ claude: [claudeSettings, mergeClaudeStyleArkHooks],
26
+ codex: [codexHooks, mergeClaudeStyleArkHooks],
27
+ cursor: [cursorHooks, mergeCursorArkHook],
28
+ antigravity: [antigravityHooks, mergeAntigravityArkHook],
29
+ };
30
+
31
+ /** True when install will upsert Ark's hook into the existing host file without --force. */
32
+ export function hostHookFileMergeable(root, host, hookFile) {
33
+ const entry = MERGEABLE_HOOK_FILES[host];
34
+ if (!entry) return false;
35
+ try {
36
+ const [template, merge] = entry;
37
+ return merge(fs.readFileSync(hookFile, 'utf8'), template(root)) != null;
38
+ } catch {
39
+ return false;
40
+ }
41
+ }
13
42
 
14
43
  export const HOST_ENFORCEMENT_SUPPORT = Object.freeze(
15
44
  Object.fromEntries(
@@ -79,12 +108,19 @@ export function validateHardWriteRequest({ root, host, tools, force = false }) {
79
108
  }
80
109
 
81
110
  const hookFile = path.join(root, support.hookPath);
82
- if (fs.existsSync(hookFile) && !force && !hasHardWriteHook(root, normalizedHost)) {
111
+ if (
112
+ fs.existsSync(hookFile) &&
113
+ !force &&
114
+ !hasHardWriteHook(root, normalizedHost) &&
115
+ !hostHookFileMergeable(root, normalizedHost, hookFile)
116
+ ) {
83
117
  return {
84
118
  ok: false,
85
- error:
86
- `${support.hookPath} already exists without an Ark hard-write hook and would be preserved. ` +
87
- 'Use --force to replace that host file, or omit --require-write-hook for merge-only enforcement.',
119
+ error: MERGEABLE_HOOK_FILES[normalizedHost]
120
+ ? `${support.hookPath} already exists but is not a JSON object Ark can merge into, so it would be preserved. ` +
121
+ 'Fix that file (or use --force to replace it), or omit --require-write-hook for merge-only enforcement.'
122
+ : `${support.hookPath} already exists without an Ark hard-write hook and would be preserved. ` +
123
+ 'Use --force to replace that host file, or omit --require-write-hook for merge-only enforcement.',
88
124
  };
89
125
  }
90
126
 
@@ -3,6 +3,7 @@
3
3
  */
4
4
 
5
5
  import { NORTH_STAR_ONE_LINE } from './product-copy.mjs';
6
+ import { KNOWN_TOOLS } from './skill-install.mjs';
6
7
 
7
8
  export function setupUsage() {
8
9
  return `arkgate (alias ark) — Write. Check. Ship.
@@ -96,8 +97,8 @@ Options:
96
97
  Valid ids: crud-product, api-backend, frontend-surface, library-sdk, cli-utility,
97
98
  worker-pipeline, event-coordinator, integration-bridge, multi-app-workspace, prototype-spike,
98
99
  vertical-slice-product, ddd-bounded-contexts.
99
- --tools One active agent host for start (claude,cursor,codex,grok,antigravity,windsurf,cline,copilot,kiro,roo,continue,gemini).
100
- Omit to use the active host; an unknown host creates only the shared compact router.
100
+ --tools One active agent host for start (${KNOWN_TOOLS.join(',')}).
101
+ Omit to use the active host; a list or an unknown host is rejected (exit 2).
101
102
  --remove-host <host>
102
103
  Preview or apply removal of that compact host integration; re-add it with --tools <host>.
103
104
  --require-write-hook <host>
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Importer graph for the flat-parent doctor suggestion.
3
+ *
4
+ * Prefers the resolved dependency facts ark-check already built (tsconfig
5
+ * paths included). Without facts it scans the text and resolves relative
6
+ * specifiers plus tsconfig path aliases, so an `@/…` importer is never
7
+ * invisible. Advisory input only.
8
+ */
9
+ import fs from 'node:fs';
10
+ import path from 'node:path';
11
+ import { readTsconfigAliases, resolveSpecifierToRel } from './import-resolve.mjs';
12
+
13
+ const SPEC_MARKS = [' from "', " from '", ' from `', 'require("', "require('", 'import("', "import('"];
14
+ const EXT = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.mts', '.cts'];
15
+
16
+ function withoutComments(text) {
17
+ let out = '';
18
+ let i = 0;
19
+ while (i < text.length) {
20
+ const c = text[i];
21
+ const n = text[i + 1];
22
+ if (c === '/' && n === '/') {
23
+ while (i < text.length && text[i] !== '\n') i += 1;
24
+ continue;
25
+ }
26
+ if (c === '/' && n === '*') {
27
+ i += 2;
28
+ while (i + 1 < text.length && !(text[i] === '*' && text[i + 1] === '/')) i += 1;
29
+ i = Math.min(text.length, i + 2);
30
+ continue;
31
+ }
32
+ out += c;
33
+ i += 1;
34
+ }
35
+ return out;
36
+ }
37
+
38
+ /** Every string module specifier in `text` (relative and bare). */
39
+ export function moduleSpecifiers(text) {
40
+ const specs = [];
41
+ const source = withoutComments(String(text));
42
+ let i = 0;
43
+ while (i < source.length) {
44
+ let at = -1;
45
+ let mark = '';
46
+ for (const candidate of SPEC_MARKS) {
47
+ const found = source.indexOf(candidate, i);
48
+ if (found !== -1 && (at === -1 || found < at)) {
49
+ at = found;
50
+ mark = candidate;
51
+ }
52
+ }
53
+ if (at === -1) break;
54
+ const quote = mark[mark.length - 1];
55
+ const start = at + mark.length;
56
+ const end = source.indexOf(quote, start);
57
+ if (end === -1) break;
58
+ const spec = source.slice(start, end);
59
+ if (spec.length > 0) specs.push(spec);
60
+ i = end + 1;
61
+ }
62
+ return specs;
63
+ }
64
+
65
+ function resolveRelative(fromFile, spec) {
66
+ const base = fromFile.split('/');
67
+ base.pop();
68
+ for (const part of spec.split('/')) {
69
+ if (part === '' || part === '.') continue;
70
+ if (part === '..') {
71
+ if (base.length === 0) return null;
72
+ base.pop();
73
+ continue;
74
+ }
75
+ base.push(part);
76
+ }
77
+ return base.join('/');
78
+ }
79
+
80
+ function probe(joined, fileSet) {
81
+ if (!joined) return null;
82
+ const names = [joined];
83
+ for (const ext of EXT) names.push(`${joined}${ext}`, `${joined}/index${ext}`);
84
+ const stem = joined.replace(/\.(?:m|c)?js$/, '');
85
+ if (stem !== joined) for (const ext of EXT) names.push(`${stem}${ext}`);
86
+ for (const name of names) {
87
+ if (fileSet.has(name)) return name;
88
+ }
89
+ return null;
90
+ }
91
+
92
+ function readText(root, rel) {
93
+ try {
94
+ return fs.readFileSync(path.join(root, rel), 'utf8');
95
+ } catch {
96
+ return '';
97
+ }
98
+ }
99
+
100
+ function addEdge(graph, target, importer) {
101
+ let set = graph.get(target);
102
+ if (!set) {
103
+ set = new Set();
104
+ graph.set(target, set);
105
+ }
106
+ set.add(importer);
107
+ }
108
+
109
+ /**
110
+ * target file → every file that imports it (value and type-only edges; a
111
+ * peerIsolation wall judges both).
112
+ * @param {{ root: string, files: string[], facts?: object, ts?: object }} input
113
+ */
114
+ export function flatParentImporterGraph({ root, files, facts, ts }) {
115
+ const graph = new Map();
116
+ const fileSet = new Set(files);
117
+ const dependencies = Array.isArray(facts?.dependencies) ? facts.dependencies : null;
118
+ if (dependencies && dependencies.length > 0) {
119
+ for (const dep of dependencies) {
120
+ if (dep?.resolution !== 'resolved-project' || typeof dep.target !== 'string') continue;
121
+ if (typeof dep.from !== 'string' || dep.from === dep.target) continue;
122
+ addEdge(graph, dep.target, dep.from);
123
+ }
124
+ return graph;
125
+ }
126
+ const aliases = readTsconfigAliases(ts, root);
127
+ for (const file of files) {
128
+ for (const spec of moduleSpecifiers(readText(root, file))) {
129
+ const joined = spec.startsWith('.')
130
+ ? resolveRelative(file, spec)
131
+ : resolveSpecifierToRel(spec, file, root, aliases);
132
+ const target = probe(joined, fileSet);
133
+ if (target && target !== file) addEdge(graph, target, file);
134
+ }
135
+ }
136
+ return graph;
137
+ }