arkgate 4.8.23 → 4.8.25

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 (201) hide show
  1. package/CHANGELOG.md +153 -0
  2. package/README.md +7 -5
  3. package/bin/ark-check-runtime.mjs +41 -91
  4. package/bin/ark-dashboard.mjs +56 -53
  5. package/bin/ark-layer-match.mjs +707 -121
  6. package/bin/ark-mcp-runtime.mjs +448 -169
  7. package/bin/ark-mcp.mjs +68 -4
  8. package/bin/ark-shared.mjs +42 -22
  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/adoption-stance.mjs +2 -2
  13. package/bin/lib/adr-path.mjs +3 -3
  14. package/bin/lib/agent-gates.mjs +0 -61
  15. package/bin/lib/agent-homes.mjs +12 -21
  16. package/bin/lib/agent-projection-command.mjs +9 -5
  17. package/bin/lib/ambient-state.mjs +9 -30
  18. package/bin/lib/analysis-engine.mjs +15 -11
  19. package/bin/lib/architecture-scan.mjs +61 -15
  20. package/bin/lib/ark-order-doctor.mjs +4 -29
  21. package/bin/lib/ark-order-facts.mjs +319 -17
  22. package/bin/lib/ark-order-sensors.mjs +28 -101
  23. package/bin/lib/ark-order-types.mjs +3 -2
  24. package/bin/lib/ark-run-doctor.mjs +17 -28
  25. package/bin/lib/ark-run-facts.mjs +461 -48
  26. package/bin/lib/ark-run-sensors.mjs +6 -4
  27. package/bin/lib/arkgate-pins.mjs +91 -0
  28. package/bin/lib/arkrule-file-hints.mjs +2 -47
  29. package/bin/lib/arkrules-contract.mjs +124 -133
  30. package/bin/lib/arkrules-sensors.mjs +55 -252
  31. package/bin/lib/check-args.mjs +85 -1
  32. package/bin/lib/ci-and-commands.mjs +24 -78
  33. package/bin/lib/ci-merge-boundary.mjs +1 -1
  34. package/bin/lib/class-source-scan.mjs +479 -0
  35. package/bin/lib/clone-detection.mjs +474 -0
  36. package/bin/lib/codex-home.mjs +5 -3
  37. package/bin/lib/config-contract-slices.mjs +739 -0
  38. package/bin/lib/config-contract.mjs +42 -607
  39. package/bin/lib/config-extras.mjs +16 -22
  40. package/bin/lib/config-warnings.mjs +32 -0
  41. package/bin/lib/contract-smells.mjs +33 -15
  42. package/bin/lib/core-ratchet.mjs +0 -2
  43. package/bin/lib/dashboard-cli.mjs +62 -0
  44. package/bin/lib/deep-module-coach.mjs +2 -4
  45. package/bin/lib/deploy-path.mjs +13 -38
  46. package/bin/lib/design-delta.mjs +1 -1
  47. package/bin/lib/design-smells.mjs +2 -2
  48. package/bin/lib/diagnostic-catalog.mjs +23 -9
  49. package/bin/lib/doctor-advisories.mjs +78 -8
  50. package/bin/lib/doctor-green-cite.mjs +1 -1
  51. package/bin/lib/doctor-human.mjs +1 -1
  52. package/bin/lib/doctor-next-actions.mjs +19 -4
  53. package/bin/lib/doctor-plan.mjs +9 -13
  54. package/bin/lib/duplication-io.mjs +572 -0
  55. package/bin/lib/effective-contract-load.mjs +282 -83
  56. package/bin/lib/enforcement-profiles.mjs +40 -4
  57. package/bin/lib/entry-points-io.mjs +424 -0
  58. package/bin/lib/extra-merge-teeth.mjs +36 -0
  59. package/bin/lib/field-install.mjs +38 -14
  60. package/bin/lib/first-run-help.mjs +10 -5
  61. package/bin/lib/flat-parent-importers.mjs +139 -0
  62. package/bin/lib/flat-parent-pilot.mjs +40 -100
  63. package/bin/lib/gate-files.mjs +58 -90
  64. package/bin/lib/git-change-scope.mjs +109 -0
  65. package/bin/lib/github-enforcement.mjs +166 -29
  66. package/bin/lib/golden-pattern.mjs +1 -1
  67. package/bin/lib/hook-templates.mjs +17 -15
  68. package/bin/lib/host-support-matrix.mjs +3 -1
  69. package/bin/lib/html-report-advisories.mjs +4 -0
  70. package/bin/lib/html-report-depth.mjs +2 -2
  71. package/bin/lib/html-report.mjs +2 -2
  72. package/bin/lib/import-graph-projection.mjs +112 -0
  73. package/bin/lib/import-resolve.mjs +13 -1
  74. package/bin/lib/improvement-compass-doctor.mjs +1 -24
  75. package/bin/lib/install-migrate.mjs +50 -73
  76. package/bin/lib/invariant-coverage-io.mjs +4 -3
  77. package/bin/lib/invariant-coverage.mjs +85 -11
  78. package/bin/lib/invariant-probe-cli.mjs +347 -0
  79. package/bin/lib/invariant-probe-io.mjs +165 -0
  80. package/bin/lib/invariant-probe-runner.mjs +268 -0
  81. package/bin/lib/invariant-probe-sites.mjs +174 -0
  82. package/bin/lib/invariant-probe-workspace.mjs +183 -0
  83. package/bin/lib/invariant-probe.mjs +461 -0
  84. package/bin/lib/invariant-tests-path.mjs +16 -22
  85. package/bin/lib/literal-path-drift.mjs +4 -2
  86. package/bin/lib/managed-upgrade.mjs +2 -8
  87. package/bin/lib/mcp-adoption.mjs +13 -6
  88. package/bin/lib/mcp-hook-payload.mjs +403 -63
  89. package/bin/lib/orphan-modules-io.mjs +458 -0
  90. package/bin/lib/orphan-modules.mjs +251 -0
  91. package/bin/lib/outside-importers.mjs +197 -0
  92. package/bin/lib/package-manager.mjs +60 -2
  93. package/bin/lib/parse-health.mjs +1 -1
  94. package/bin/lib/passthrough-commands.mjs +83 -0
  95. package/bin/lib/persistence-write-hint.mjs +106 -0
  96. package/bin/lib/pin-dependent-gates.mjs +117 -0
  97. package/bin/lib/policy-delta-io.mjs +219 -14
  98. package/bin/lib/post-green-path.mjs +1 -1
  99. package/bin/lib/prepare-change.mjs +115 -7
  100. package/bin/lib/prepare-write.mjs +2 -2
  101. package/bin/lib/presets.mjs +59 -73
  102. package/bin/lib/product-copy.mjs +1 -6
  103. package/bin/lib/project-root.mjs +31 -62
  104. package/bin/lib/remediation.mjs +23 -4
  105. package/bin/lib/resolved-candidate-facts.mjs +405 -117
  106. package/bin/lib/rules-inventory.mjs +22 -3
  107. package/bin/lib/rules-under-contract.mjs +247 -9
  108. package/bin/lib/scan-files.mjs +36 -4
  109. package/bin/lib/schema-validation.mjs +130 -0
  110. package/bin/lib/sensor-promote-cli.mjs +2 -11
  111. package/bin/lib/sensor-promote-io.mjs +33 -22
  112. package/bin/lib/sensor-promotion.mjs +1 -1
  113. package/bin/lib/skill-catalog-apply.mjs +1 -1
  114. package/bin/lib/skill-install.mjs +42 -38
  115. package/bin/lib/snippet-analysis.mjs +140 -6
  116. package/bin/lib/source-policy.mjs +34 -0
  117. package/bin/lib/start-install-recovery.mjs +72 -2
  118. package/bin/lib/start-preview.mjs +31 -1
  119. package/bin/lib/states-transitions-presence.mjs +3 -3
  120. package/bin/lib/status-command.mjs +35 -22
  121. package/bin/lib/status-manifest.mjs +50 -30
  122. package/bin/lib/status-transition-catalog.mjs +0 -6
  123. package/bin/lib/suggestions.mjs +3 -3
  124. package/bin/lib/team-parliament-io.mjs +76 -88
  125. package/bin/lib/team-parliament.mjs +21 -10
  126. package/bin/lib/upgrade-behavior-changes.mjs +2 -2
  127. package/bin/lib/upgrade-command.mjs +2 -24
  128. package/bin/lib/upgrade-outcome.mjs +1 -1
  129. package/bin/lib/upstream-report.mjs +21 -10
  130. package/bin/lib/violations.mjs +10 -9
  131. package/bin/lib/weakest-link.mjs +13 -0
  132. package/bin/lib/write-path-capabilities.mjs +6 -4
  133. package/dist/chunk-A5EIY3HI.cjs +5 -0
  134. package/dist/chunk-AHACEKSV.js +16 -0
  135. package/dist/chunk-SVSQVGFJ.js +5 -0
  136. package/dist/chunk-UN5EH4QV.cjs +16 -0
  137. package/dist/{configTypes-Bbf_MFGe.d.ts → configTypes-GPvvdleM.d.cts} +30 -4
  138. package/dist/configTypes-GPvvdleM.d.ts +257 -0
  139. package/dist/eslint/index.cjs +18 -5
  140. package/dist/eslint/index.d.cts +368 -0
  141. package/dist/eslint/index.d.ts +139 -156
  142. package/dist/eslint/index.js +11 -5
  143. package/dist/index.cjs +36 -35
  144. package/dist/index.d.cts +5547 -0
  145. package/dist/index.d.ts +3283 -198
  146. package/dist/index.js +36 -35
  147. package/dist/inspectorListen-6DQVJ4DC.js +1 -0
  148. package/dist/inspectorListen-KGMIQ2TF.cjs +1 -0
  149. package/dist/nestjs/index.cjs +1 -18
  150. package/dist/nestjs/index.d.cts +22 -0
  151. package/dist/nestjs/index.d.ts +1 -3
  152. package/dist/nestjs/index.js +1 -18
  153. package/dist/order/index.cjs +1 -1
  154. package/dist/order/index.d.cts +155 -0
  155. package/dist/order/index.d.ts +15 -3
  156. package/dist/order/index.js +1 -1
  157. package/dist/runtime/index.cjs +13 -30
  158. package/dist/{diagnosticCatalog-35mrwAHe.d.ts → runtime/index.d.cts} +828 -276
  159. package/dist/runtime/index.d.ts +2741 -133
  160. package/dist/runtime/index.js +13 -30
  161. package/dist/{types-Bdj32QwA.d.ts → types-CnauMQQL.d.cts} +607 -163
  162. package/dist/types-CnauMQQL.d.ts +1800 -0
  163. package/docs/README.md +3 -1
  164. package/docs/agent-guide.md +217 -16
  165. package/docs/ai-gates.md +174 -26
  166. package/docs/arkorder.md +32 -15
  167. package/docs/brownfield-adoption.md +5 -1
  168. package/docs/configuration.md +230 -31
  169. package/docs/develop.md +2 -1
  170. package/docs/diagnostics.md +212 -19
  171. package/docs/package-surface.md +35 -9
  172. package/docs/product-voice.md +7 -1
  173. package/package.json +69 -19
  174. package/schemas/ark.analysis-result.schema.json +15 -0
  175. package/schemas/ark.config.schema.json +77 -12
  176. package/schemas/ark.invariant-probe.schema.json +368 -0
  177. package/schemas/ark.resolved-candidate-facts.schema.json +1 -1
  178. package/schemas/ark.status-manifest.schema.json +22 -0
  179. package/server.json +3 -9
  180. package/templates/agent-skills/README.md +1 -1
  181. package/templates/agent-skills/ark-adopt/SKILL.md +5 -1
  182. package/templates/agent-skills/ark-contract/SKILL.md +17 -0
  183. package/templates/agent-skills/ark-coverage/SKILL.md +2 -0
  184. package/templates/agent-skills/ark-explore/SKILL.md +7 -0
  185. package/templates/agent-skills/ark-fix/SKILL.md +20 -0
  186. package/templates/agent-skills/ark-order/SKILL.md +4 -2
  187. package/templates/agent-skills/ark-place/SKILL.md +8 -0
  188. package/templates/agent-skills/ark-runtime/SKILL.md +6 -1
  189. package/templates/arkrules/PersistenceAdapters.json +1 -1
  190. package/templates/arkrules/PresentationAdapters.json +1 -1
  191. package/templates/skills/ark-adopt.md +5 -1
  192. package/templates/skills/ark-contract.md +17 -0
  193. package/templates/skills/ark-coverage.md +2 -0
  194. package/templates/skills/ark-explore.md +7 -0
  195. package/templates/skills/ark-fix.md +20 -0
  196. package/templates/skills/ark-order.md +4 -2
  197. package/templates/skills/ark-place.md +8 -0
  198. package/templates/skills/ark-runtime.md +6 -1
  199. package/bin/lib/ark-order-error.mjs +0 -18
  200. package/bin/lib/ark-order-invariants.mjs +0 -346
  201. package/dist/types-Ik_OakcI.d.ts +0 -183
@@ -16,6 +16,7 @@ import {
16
16
  invariantIdsFromCatalog,
17
17
  loadInvariantCoverageInputs,
18
18
  } from './invariant-coverage-io.mjs';
19
+ import { attachProbeEvidence } from './invariant-probe-io.mjs';
19
20
 
20
21
  /**
21
22
  * The root as the filesystem sees it. Comparing a realpath against a lexical
@@ -79,28 +80,11 @@ export function loadSensorMap(root, config, facts) {
79
80
  }));
80
81
 
81
82
  const catalogued = loaded.arkRules.invariants ?? [];
82
- let coverageRows = [];
83
- let partial = false;
84
- if (catalogued.length > 0) {
85
- const inputs = loadInvariantCoverageInputs(root, facts ?? { files: [] }, {
86
- invariantIds: invariantIdsFromCatalog(loaded.arkRules),
87
- ...coverageOptionsFromConfig(config),
88
- });
89
- const coverage = evaluateInvariantCoverage({
90
- arkRules: loaded.arkRules,
91
- fileContents: inputs.fileContents,
92
- testFiles: inputs.testFiles,
93
- testGlobsMissing: inputs.testGlobsMissing,
94
- coverageBudgetExhausted: inputs.coverageBudgetExhausted === true,
95
- ...(inputs.stats ? { coverageStats: inputs.stats } : {}),
96
- // The declared roots decide `outsideDeclaredRoots`, which is one of the
97
- // four reasons canPromoteInvariant refuses. Dropping them here would make
98
- // this surface promise a promotion the gate then denies.
99
- ...(inputs.coverageRoots ? { coverageRoots: inputs.coverageRoots } : {}),
100
- });
101
- coverageRows = coverage.coverage ?? [];
102
- partial = coverage.partial === true;
103
- }
83
+ const evaluated = evaluateCatalogCoverage(root, config, facts, loaded.arkRules);
84
+ // ADR 0039: a committed probe artifact can only subtract promotability.
85
+ // Read-only here — this path never runs a test.
86
+ const coverageRows = attachProbeEvidence(root, evaluated.rows, catalogued).rows;
87
+ const partial = evaluated.partial;
104
88
  const evidenceById = new Map(coverageRows.map((row) => [row.invariantId, row]));
105
89
 
106
90
  const invariants = catalogued.map((entry) => ({
@@ -120,6 +104,33 @@ export function loadSensorMap(root, config, facts) {
120
104
  };
121
105
  }
122
106
 
107
+ /**
108
+ * Invariant coverage rows for a loaded catalog, evaluated the way the
109
+ * promotion judge needs them (declared `coverageRoots` included). `inputs` is
110
+ * the loaded file set, reused by the probe to pick covering tests.
111
+ */
112
+ export function evaluateCatalogCoverage(root, config, facts, arkRules) {
113
+ const catalogued = arkRules?.invariants ?? [];
114
+ if (catalogued.length === 0) return { rows: [], partial: false, inputs: null };
115
+ const inputs = loadInvariantCoverageInputs(root, facts ?? { files: [] }, {
116
+ invariantIds: invariantIdsFromCatalog(arkRules),
117
+ ...coverageOptionsFromConfig(config),
118
+ });
119
+ const coverage = evaluateInvariantCoverage({
120
+ arkRules,
121
+ fileContents: inputs.fileContents,
122
+ testFiles: inputs.testFiles,
123
+ testGlobsMissing: inputs.testGlobsMissing,
124
+ coverageBudgetExhausted: inputs.coverageBudgetExhausted === true,
125
+ ...(inputs.stats ? { coverageStats: inputs.stats } : {}),
126
+ // The declared roots decide `outsideDeclaredRoots`, which is one of the
127
+ // four reasons canPromoteInvariant refuses. Dropping them here would make
128
+ // this surface promise a promotion the gate then denies.
129
+ ...(inputs.coverageRoots ? { coverageRoots: inputs.coverageRoots } : {}),
130
+ });
131
+ return { rows: coverage.coverage ?? [], partial: coverage.partial === true, inputs };
132
+ }
133
+
123
134
  /**
124
135
  * Count the findings each declared rule produced in an analysis that already
125
136
  * ran. One run answers for every rule: the loop this replaces was one full
@@ -188,7 +188,7 @@ export function buildSensorMap(input) {
188
188
  sourceFile,
189
189
  description: nullable(rule.description),
190
190
  promotable: verdict.ok,
191
- blocker: verdict.ok ? null : 'no-coverage-evidence',
191
+ blocker: verdict.ok ? null : verdict.blocker ?? 'no-coverage-evidence',
192
192
  reason: `${locate(rule.id, sourceFile)}: ${verdict.reason}`,
193
193
  coverageEvaluated: rule.coverage != null,
194
194
  ...ambiguity(rule.id),
@@ -19,7 +19,7 @@ function skillName(skill) {
19
19
  return Array.isArray(skill) ? skill[0] : skill?.name || skill;
20
20
  }
21
21
 
22
- export function projectCatalogReady(root, skillNames) {
22
+ function projectCatalogReady(root, skillNames) {
23
23
  return skillNames.some((name) => fs.existsSync(path.join(root, canonicalSkillPath(name))));
24
24
  }
25
25
 
@@ -7,7 +7,6 @@ import os from 'node:os';
7
7
  import path from 'node:path';
8
8
  import { arkCommand } from '../ark-shared.mjs';
9
9
  import {
10
- parseSkillDescriptionVersion,
11
10
  stampSkillDescription,
12
11
  stripSkillDescriptionVersion,
13
12
  } from './agent-skills-package.mjs';
@@ -227,14 +226,14 @@ export function canonicalSkillPath(name) {
227
226
  * Hosts that natively load `.agents/skills` — do not also copy bytes there under
228
227
  * a second name. Cursor/Codex list every path they scan; two copies = two picker rows.
229
228
  */
230
- export const SKILL_NATIVE_AGENTS_HOSTS = Object.freeze(['codex', 'cursor', 'antigravity']);
229
+ const SKILL_NATIVE_AGENTS_HOSTS = Object.freeze(['codex', 'cursor', 'antigravity']);
231
230
 
232
231
  /**
233
232
  * Hosts that do not scan `.agents/skills`. Adapter is a relative symlink to the
234
233
  * canonical catalog so Grok/Claude/OpenCode see the same bytes.
235
234
  * Cursor also scans `.claude/skills` — doctor warns; still one body + visible version.
236
235
  */
237
- export const SKILL_ADAPTER_LINKS = Object.freeze({
236
+ const SKILL_ADAPTER_LINKS = Object.freeze({
238
237
  claude: (name) => ({
239
238
  link: `.claude/skills/${name}`,
240
239
  target: `../../${SKILL_CANONICAL_DIR}/${name}`,
@@ -299,10 +298,10 @@ export function arkPackageVersion() {
299
298
  }
300
299
 
301
300
  /**
302
- * Rewrite a YAML `description:` line with a visible `arkgate@<version>. ` prefix.
303
- * Preserves quoting. Hosts show this string in the skill picker (unlike arkVersion).
301
+ * Rewrite the value of a YAML `description:` line, preserving its quoting.
302
+ * `transform` returns the new value, or null to keep the line untouched.
304
303
  */
305
- function stampDescriptionYamlLine(line, version) {
304
+ function rewriteDescriptionYamlLine(line, transform) {
306
305
  const match = String(line).match(/^(description:\s*)(.*)$/);
307
306
  if (!match) return line;
308
307
  let raw = match[2] ?? '';
@@ -311,26 +310,26 @@ function stampDescriptionYamlLine(line, version) {
311
310
  (raw.startsWith("'") && raw.endsWith("'") && raw.length >= 2);
312
311
  const quote = quoted ? raw[0] : '';
313
312
  const value = quoted ? raw.slice(1, -1) : raw;
314
- const stamped = stampSkillDescription(value, version);
315
- if (!quoted) return `${match[1]}${stamped}`;
316
- const escaped = stamped.replaceAll('\\', '\\\\').replaceAll(quote, `\\${quote}`);
313
+ const next = transform(value);
314
+ if (next === null) return line;
315
+ if (!quoted) return `${match[1]}${next}`;
316
+ const escaped = next.replaceAll('\\', '\\\\').replaceAll(quote, `\\${quote}`);
317
317
  return `${match[1]}${quote}${escaped}${quote}`;
318
318
  }
319
319
 
320
+ /**
321
+ * Rewrite a YAML `description:` line with a visible `arkgate@<version>. ` prefix.
322
+ * Preserves quoting. Hosts show this string in the skill picker (unlike arkVersion).
323
+ */
324
+ function stampDescriptionYamlLine(line, version) {
325
+ return rewriteDescriptionYamlLine(line, (value) => stampSkillDescription(value, version));
326
+ }
327
+
320
328
  function managedDescriptionYamlLine(line) {
321
- const match = String(line).match(/^(description:\s*)(.*)$/);
322
- if (!match) return line;
323
- let raw = match[2] ?? '';
324
- const quoted =
325
- (raw.startsWith('"') && raw.endsWith('"') && raw.length >= 2) ||
326
- (raw.startsWith("'") && raw.endsWith("'") && raw.length >= 2);
327
- const quote = quoted ? raw[0] : '';
328
- const value = quoted ? raw.slice(1, -1) : raw;
329
- const stripped = stripSkillDescriptionVersion(value);
330
- if (stripped === value) return line;
331
- if (!quoted) return `${match[1]}${stripped}`;
332
- const escaped = stripped.replaceAll('\\', '\\\\').replaceAll(quote, `\\${quote}`);
333
- return `${match[1]}${quote}${escaped}${quote}`;
329
+ return rewriteDescriptionYamlLine(line, (value) => {
330
+ const stripped = stripSkillDescriptionVersion(value);
331
+ return stripped === value ? null : stripped;
332
+ });
334
333
  }
335
334
 
336
335
  // Insert `arkVersion: <v>` and a visible `arkgate@<v>. ` description prefix.
@@ -593,7 +592,7 @@ export function skillTemplates() {
593
592
  * Relative symlink so clones keep working. Fallback copy when the OS refuses links.
594
593
  * @returns {'linked'|'copied'|'current'|'skipped-customized'|'missing-canonical'}
595
594
  */
596
- export function ensureSkillAdapterLink(root, name, adapter, force = false) {
595
+ function ensureSkillAdapterLink(root, name, adapter, force = false) {
597
596
  const canonicalDir = path.join(root, SKILL_CANONICAL_DIR, name);
598
597
  const canonicalFile = path.join(canonicalDir, 'SKILL.md');
599
598
  if (!fs.existsSync(canonicalFile)) return 'missing-canonical';
@@ -693,8 +692,6 @@ export function pruneHomeArkSkillDuplicates(root, skillNames = skillTemplateName
693
692
  return { ok: true, reason: 'pruned', removed };
694
693
  }
695
694
 
696
- export { parseSkillDescriptionVersion, stripSkillDescriptionVersion };
697
-
698
695
  // Skill names only, silent on a missing templates dir — for the freshness
699
696
  // advisory below, which must not print packaging warnings on every check run.
700
697
  export function skillTemplateNames() {
@@ -821,20 +818,15 @@ function readCodexHomeCatalogMetadata(file, kind) {
821
818
  }
822
819
  }
823
820
 
824
- function codexHomeCatalogState(skillsDir) {
825
- const catalog = readCodexHomeCatalogMetadata(
826
- path.join(skillsDir, CODEX_HOME_CATALOG),
827
- 'catalog'
828
- );
829
- const pending = readCodexHomeCatalogMetadata(
830
- path.join(skillsDir, CODEX_HOME_PENDING_CATALOG),
831
- 'pending'
832
- );
821
+ /**
822
+ * Merge a home skill catalog and its pending catalog metadata: the floor is the
823
+ * newer of the two versions; either file present means metadata is in play.
824
+ * @param {{ exists: boolean, valid: boolean, version: string|null }} catalog
825
+ * @param {{ exists: boolean, valid: boolean, version: string|null }} pending
826
+ */
827
+ export function homeCatalogFloorState(catalog, pending) {
833
828
  let floorVersion = catalog.version;
834
- if (
835
- pending.version &&
836
- (!floorVersion || isVersionOlder(floorVersion, pending.version))
837
- ) {
829
+ if (pending.version && (!floorVersion || isVersionOlder(floorVersion, pending.version))) {
838
830
  floorVersion = pending.version;
839
831
  }
840
832
  return {
@@ -846,6 +838,18 @@ function codexHomeCatalogState(skillsDir) {
846
838
  };
847
839
  }
848
840
 
841
+ function codexHomeCatalogState(skillsDir) {
842
+ const catalog = readCodexHomeCatalogMetadata(
843
+ path.join(skillsDir, CODEX_HOME_CATALOG),
844
+ 'catalog'
845
+ );
846
+ const pending = readCodexHomeCatalogMetadata(
847
+ path.join(skillsDir, CODEX_HOME_PENDING_CATALOG),
848
+ 'pending'
849
+ );
850
+ return homeCatalogFloorState(catalog, pending);
851
+ }
852
+
849
853
  function newerCodexHomeCatalogVersion(skillsDir, packageVersion, state = null) {
850
854
  if (!isValidSemver(packageVersion)) return null;
851
855
  const floorVersion = (state ?? codexHomeCatalogState(skillsDir)).floorVersion;
@@ -2,16 +2,31 @@
2
2
  import { layerForRelativePath } from '../ark-layer-match.mjs';
3
3
  import { ANALYSIS_COMPLETENESS } from './analysis-completeness.mjs';
4
4
  import { evaluateArkRunEditorSensorsFromSource } from './ark-run-sensors.mjs';
5
+ import { evaluateArkOrderEditorSensors } from './ark-order-sensors.mjs';
6
+ import {
7
+ buildArkRuleFileHints,
8
+ evaluateArkRuleSensors,
9
+ extractClassShapesFromSource,
10
+ } from './arkrules-sensors.mjs';
5
11
  import { getDiagnosticCatalogEntry } from './diagnostic-catalog.mjs';
12
+ import { demoteExtraPlaneTeethUnderClassificationFloor } from './extra-merge-teeth.mjs';
6
13
 
7
- function lexicalEvidenceIncompleteMessage(file) {
14
+ /** Hook-only guidance: a hook deny is already the verdict for that write. */
15
+ const HOOK_DENY_PREPARE_CHANGE_NOTE = 'Do not call ark_prepare_change from a hook deny.';
16
+
17
+ function surfaceNote(context) {
18
+ return context?.surface === 'hook' ? ` ${HOOK_DENY_PREPARE_CHANGE_NOTE}` : '';
19
+ }
20
+
21
+ function lexicalEvidenceIncompleteMessage(file, context) {
8
22
  const entry = getDiagnosticCatalogEntry('LEXICAL_EVIDENCE_INCOMPLETE');
9
23
  const text = [entry?.why, entry?.fix].filter(Boolean).join(' ');
10
24
  return {
11
25
  code: 'LEXICAL_EVIDENCE_INCOMPLETE',
12
26
  message:
13
- text ||
14
- 'This check only saw one file, so it cannot fully prove how the import resolves. The result is provisional — `ark-check` on the project is the authority. Run `npx arkgate-check --root . --config ark.config.json` to confirm. Do not call ark_prepare_change from a hook deny.',
27
+ (text ||
28
+ 'This check only saw one file, so it cannot fully prove how the import resolves. The result is provisional — `ark-check` on the project is the authority. For a complete verdict, run `npx arkgate-check --root . --config ark.config.json` (or ark_prepare_change over MCP with the full candidate batch).') +
29
+ surfaceNote(context),
15
30
  ...(file ? { file } : {}),
16
31
  };
17
32
  }
@@ -81,14 +96,132 @@ function arkRunSnippetViolations(source, context = {}) {
81
96
  }));
82
97
  }
83
98
 
99
+ const TS_CLASS_SHAPE_FILE = /\.(tsx?|mts|cts)$/i;
100
+
101
+ /**
102
+ * File-local ArkRules structure sensors on the proposed source — the same sensors,
103
+ * class-shape extraction, and per-file hints ark-check runs, pinned to this one file.
104
+ * Only enforced (failsStrict) findings block; advisory rules stay CI/doctor warnings.
105
+ */
106
+ function arkRulesSnippetViolations(source, context = {}) {
107
+ const arkRules = context.arkRules;
108
+ const file = context.relFile;
109
+ if (!arkRules || !(arkRules.structure?.length > 0) || typeof file !== 'string' || !file) {
110
+ return [];
111
+ }
112
+ const layers = Array.isArray(context.layers) ? context.layers : [];
113
+ const layerForFile = (pathValue) =>
114
+ pathValue === file && typeof context.layer === 'string'
115
+ ? context.layer
116
+ : layerForRelativePath(pathValue, layers);
117
+ let classShapes = [];
118
+ if (TS_CLASS_SHAPE_FILE.test(file)) {
119
+ try {
120
+ classShapes = extractClassShapesFromSource(file, source);
121
+ } catch {
122
+ // Same as the CI resolver: shape extraction never fails the run.
123
+ classShapes = [];
124
+ }
125
+ }
126
+ const findings = evaluateArkRuleSensors({
127
+ arkRules,
128
+ classShapes,
129
+ files: [file],
130
+ layerForFile,
131
+ fileHints: buildArkRuleFileHints({ [file]: source }),
132
+ });
133
+ // Same classification floor ark-check applies before merge (teeth only on a classified tree).
134
+ return demoteExtraPlaneTeethUnderClassificationFloor(findings, context.classification ?? {})
135
+ .filter((finding) => finding.failsStrict)
136
+ .map((finding) => ({
137
+ ...finding,
138
+ nextAction:
139
+ getDiagnosticCatalogEntry('ARKRULE_STRUCTURE')?.fix ??
140
+ 'Fix the ArkRules structure finding in this file, then retry the write.',
141
+ failsStrict: true,
142
+ severity: 'error',
143
+ }));
144
+ }
145
+
146
+ /** ArkOrder editor sensors (generic update, kernel-in-domain, ξ writes) on one source. */
147
+ function arkOrderSnippetViolations(source, context = {}) {
148
+ const arkOrder = context.arkOrder;
149
+ const file = context.relFile;
150
+ if (!arkOrder || typeof file !== 'string' || !file) return [];
151
+ const layers = Array.isArray(context.layers) ? context.layers : [];
152
+ const fromLayer =
153
+ typeof context.layer === 'string' ? context.layer : layerForRelativePath(file, layers);
154
+ const layerMeta = layers.find((layer) => layer?.name === fromLayer);
155
+ return evaluateArkOrderEditorSensors({
156
+ arkOrder,
157
+ file,
158
+ source,
159
+ fromLayer,
160
+ intentPrefixes: layerMeta?.intentPrefixes ?? [],
161
+ ...(context.classification ? { classification: context.classification } : {}),
162
+ })
163
+ .filter((finding) => finding.failsStrict)
164
+ .map((finding) => ({
165
+ ruleId: finding.ruleId,
166
+ code: finding.ruleId,
167
+ message: finding.message,
168
+ file: finding.file,
169
+ line: finding.line,
170
+ ...(finding.fromLayer ? { fromLayer: finding.fromLayer } : {}),
171
+ ...(finding.target ? { target: finding.target } : {}),
172
+ nextAction: finding.nextAction,
173
+ failsStrict: true,
174
+ severity: 'error',
175
+ }));
176
+ }
177
+
178
+ /**
179
+ * The write gate could not load part of its own contract (e.g. a referenced ArkRules
180
+ * file is missing). CI fails closed on the same input, so the write path does too.
181
+ */
182
+ function contractLoadViolations(context = {}) {
183
+ const errors = Array.isArray(context.contractErrors) ? context.contractErrors : [];
184
+ return errors.map((error) => ({
185
+ ruleId: 'WRITE_GATE_UNAVAILABLE',
186
+ code: 'WRITE_GATE_UNAVAILABLE',
187
+ message: `Invalid Effective Contract: ${
188
+ typeof error === 'string' ? error : `${error?.path ?? ''}: ${error?.message ?? ''}`.trim()
189
+ }`,
190
+ ...(context.relFile ? { file: context.relFile } : {}),
191
+ nextAction:
192
+ getDiagnosticCatalogEntry('WRITE_GATE_UNAVAILABLE')?.fix ??
193
+ 'Fix the ArkRules reference in ark.config.json, then retry the write.',
194
+ failsStrict: true,
195
+ severity: 'error',
196
+ }));
197
+ }
198
+
199
+ /**
200
+ * Extras (ArkRules structure + ArkOrder) run only on files CI would scan. A file
201
+ * outside include / excluded is never judged by these planes in CI either.
202
+ */
203
+ function extraPlaneSnippetViolations(source, context = {}) {
204
+ if (context.inScope === false) return [];
205
+ return [
206
+ ...contractLoadViolations(context),
207
+ ...arkRulesSnippetViolations(source, context),
208
+ ...arkOrderSnippetViolations(source, context),
209
+ ];
210
+ }
211
+
84
212
  export function validateSnippetAnalysis({ gate, ts, source, context = {} }) {
85
213
  const observed = gate.validate(source, context);
86
214
  const arkRunViolations = arkRunSnippetViolations(source, context);
215
+ const extraViolations = extraPlaneSnippetViolations(source, context);
87
216
  const base = {
88
- valid: Boolean(observed.lexicalValid ?? observed.valid) && arkRunViolations.length === 0,
217
+ valid:
218
+ Boolean(observed.lexicalValid ?? observed.valid) &&
219
+ arkRunViolations.length === 0 &&
220
+ extraViolations.length === 0,
89
221
  violations: [
90
222
  ...(Array.isArray(observed.violations) ? observed.violations : []),
91
223
  ...arkRunViolations,
224
+ ...extraViolations,
92
225
  ],
93
226
  };
94
227
  const file = context.filePath;
@@ -154,7 +287,8 @@ export function validateSnippetAnalysis({ gate, ts, source, context = {} }) {
154
287
  'ANALYSIS_PARSE_INCOMPLETE',
155
288
  `Analysis partial: ${detail}`,
156
289
  file,
157
- 'Incremental mid-edit parse errors are normal. Finish the source, then re-run `npx arkgate-check` (or the write hook). Do not call ark_prepare_change from a hook deny.'
290
+ 'Incremental mid-edit parse errors are normal. Finish the source, then re-run `npx arkgate-check` (or the write hook).' +
291
+ surfaceNote(context)
158
292
  ),
159
293
  line: first?.line ?? 1,
160
294
  column: first?.column ?? 1,
@@ -169,7 +303,7 @@ export function validateSnippetAnalysis({ gate, ts, source, context = {} }) {
169
303
  valid: false,
170
304
  lexicalValid: base.valid,
171
305
  completeness: ANALYSIS_COMPLETENESS.partial,
172
- completenessReasons: [lexicalEvidenceIncompleteMessage(file)],
306
+ completenessReasons: [lexicalEvidenceIncompleteMessage(file, context)],
173
307
  };
174
308
  } catch {
175
309
  return {
@@ -25,6 +25,40 @@ export const DEFAULT_INTENT_PREFIXES = Object.freeze([
25
25
  { layer: 'SecurityAuditObservability', prefixes: ['Security.', 'Audit.', 'Observability.'] },
26
26
  { layer: 'Kernel', prefixes: ['Kernel.'] },
27
27
  ]);
28
+ /**
29
+ * Runtime intent prefixes for one config layer: its declared `intentPrefixes`,
30
+ * else the built-in prefixes when the layer uses a canonical 11-layer name
31
+ * (`DomainModel` → `Domain.`, …; the same table the static gate falls back to),
32
+ * else none. Keeps `ark init` configs, which name canonical layers without
33
+ * declaring prefixes, resolvable by the ArkRun kernel.
34
+ */
35
+ export function effectiveIntentPrefixes(layer) {
36
+ const declared = (layer.intentPrefixes ?? []).filter((prefix) => prefix.trim().length > 0);
37
+ if (declared.length > 0)
38
+ return declared;
39
+ return DEFAULT_INTENT_PREFIXES.find((entry) => entry.layer === layer.name)?.prefixes ?? [];
40
+ }
41
+ /**
42
+ * Layers named by an `allowed: false` (non-peerIsolation) rule that no intent can map to at runtime
43
+ * (no declared `intentPrefixes` and not a canonical layer name). Hard observed
44
+ * layer flow can never fire for such a rule. Sorted, unique.
45
+ */
46
+ export function unresolvableLayerFlowLayers(config) {
47
+ const resolvable = new Set(config.layers
48
+ .filter((layer) => effectiveIntentPrefixes(layer).length > 0)
49
+ .map((layer) => layer.name));
50
+ const missing = new Set();
51
+ for (const rule of config.rules ?? []) {
52
+ // peerIsolation walls are file-path slice rules; runtime flow never evaluates them.
53
+ if (rule.allowed !== false || rule.peerIsolation === true)
54
+ continue;
55
+ for (const name of [rule.from, rule.to]) {
56
+ if (!resolvable.has(name))
57
+ missing.add(name);
58
+ }
59
+ }
60
+ return [...missing].sort();
61
+ }
28
62
  /** Longest matching prefix wins; declaration order resolves an identical-prefix tie. */
29
63
  export function resolveIntentLayer(intent, layers) {
30
64
  const candidates = layers.flatMap((layer, layerIndex) => (layer.prefixes ?? layer.intentPrefixes ?? []).map((prefix) => ({
@@ -5,7 +5,10 @@
5
5
  * pin-aware npx, and AgeExclude-must-be-in-config stay behind this module.
6
6
  */
7
7
  import { spawnSync } from 'node:child_process';
8
- import { arkPackageRecoveryCommand } from './package-manager.mjs';
8
+ import fs from 'node:fs';
9
+ import path from 'node:path';
10
+ import { pinArkgateDevDependency } from './field-install.mjs';
11
+ import { arkPackageRecoveryCommand, packageInstallArgv } from './package-manager.mjs';
9
12
 
10
13
  const AGE_MARK = /ERR_PNPM_NO_MATURE_MATCHING_VERSION|minimumReleaseAge/i;
11
14
  const AGE_EXCLUDE_LINE =
@@ -32,7 +35,7 @@ function hostOutputTail(hostOutput) {
32
35
  * @param {string|undefined} installCommand
33
36
  * @returns {string|null}
34
37
  */
35
- export function resolveStartInstallPackagePin(packageVersion, installCommand) {
38
+ function resolveStartInstallPackagePin(packageVersion, installCommand) {
36
39
  const fromArg = String(packageVersion ?? '')
37
40
  .trim()
38
41
  .replace(/^arkgate@/i, '')
@@ -154,3 +157,70 @@ export function runStartPackageInstall(command, commandArgs, cwd) {
154
157
  stderr: result.stderr ?? '',
155
158
  };
156
159
  }
160
+
161
+ // Setup-time arkgate pin shared by `ark start` and `ark init` (kept out of bin/ark.mjs).
162
+ /**
163
+ * Pin arkgate in package.json (and optionally run the package manager).
164
+ * start calls this so CI/`npx` is not forced to rely on a stale global install.
165
+ *
166
+ * @param {string} root
167
+ * @param {{ install?: boolean, runPackageManager?: boolean }} [opts]
168
+ */
169
+ export function ensureProjectArkgateDependency(root, opts = {}) {
170
+ const install = opts.install !== false;
171
+ const runPm = opts.runPackageManager === true;
172
+ if (!install) {
173
+ return { pinned: { changed: false, reason: 'skipped-no-install' }, installStatus: null };
174
+ }
175
+ const pinned = pinArkgateDevDependency(root);
176
+ let installStatus = null;
177
+ let hostOutput = '';
178
+ // Only run the package manager after a successful pin change — avoid surprise
179
+ // network on every start when arkgate is already listed.
180
+ if (runPm && pinned.changed) {
181
+ const [command, commandArgs] = packageInstallArgv(root, pinned.version);
182
+ const install = runStartPackageInstall(command, commandArgs, root);
183
+ installStatus = install.status;
184
+ hostOutput = `${install.stdout}\n${install.stderr}`;
185
+ }
186
+ return { pinned, installStatus, hostOutput };
187
+ }
188
+
189
+ /**
190
+ * Setup-time pin shared by `ark start` and `ark init`. With the pin, generated CI and host
191
+ * hooks call the local `ark-check` / `arkgate-mcp` bins through the project runner. Without
192
+ * it they run `npx -y -p arkgate@<this exact version> <bin>`, because a bare `npx ark-check`
193
+ * asks npm for a package named `ark-check` (404) or runs a stale global.
194
+ */
195
+ export function pinArkgateForSetup(root, args, cliVersion) {
196
+ if (!args.install) {
197
+ console.log(' Skipping arkgate package pin (--no-install).');
198
+ return;
199
+ }
200
+ if (!fs.existsSync(path.join(root, 'package.json'))) {
201
+ console.log(
202
+ ` Warning: no package.json — the generated CI workflow and host hooks run npx -y -p arkgate@${cliVersion()} (downloaded on each run). To run a local install instead, create package.json, add arkgate@^${cliVersion()} to devDependencies, then re-run setup.`
203
+ );
204
+ return;
205
+ }
206
+ const { pinned, installStatus, hostOutput } = ensureProjectArkgateDependency(root, {
207
+ install: true,
208
+ runPackageManager: !args.skipPackageManager,
209
+ });
210
+ if (pinned.changed) {
211
+ console.log(` Pinned arkgate@${pinned.version} in package.json devDependencies.`);
212
+ if (installStatus !== null && installStatus !== 0) {
213
+ const [command, commandArgs] = packageInstallArgv(root, pinned.version);
214
+ console.error(
215
+ formatStartPackageInstallFailure({
216
+ exitStatus: installStatus,
217
+ installCommand: `${command} ${commandArgs.join(' ')}`,
218
+ hostOutput,
219
+ packageVersion: cliVersion(),
220
+ })
221
+ );
222
+ }
223
+ } else if (pinned.reason === 'already-present') {
224
+ console.log(` arkgate already in package.json (${pinned.version}).`);
225
+ }
226
+ }
@@ -20,6 +20,9 @@ import {
20
20
  grokHooks,
21
21
  grokProjectConfig,
22
22
  opencodeProjectConfig,
23
+ removeAntigravityArkHook,
24
+ removeClaudeStyleArkHooks,
25
+ removeCursorArkHook,
23
26
  } from './hook-templates.mjs';
24
27
  import { codexPrimaryTable, codexProjectMcpIsValid } from './codex-home.mjs';
25
28
  import { codexRuntimeActivation } from './enforcement-state.mjs';
@@ -56,6 +59,17 @@ const COMPACT_HOST_TEMPLATES = {
56
59
  gemini: (root) => [['GEMINI.md', instructionRule(root)]],
57
60
  };
58
61
 
62
+ /**
63
+ * Host hook files the installer merges into (keeping user keys). Removal drops only the
64
+ * Ark-owned entries, with the same identity as the merge helper.
65
+ */
66
+ const MERGED_HOOK_FILE_REMOVERS = {
67
+ '.claude/settings.json': removeClaudeStyleArkHooks,
68
+ '.codex/hooks.json': removeClaudeStyleArkHooks,
69
+ '.cursor/hooks.json': removeCursorArkHook,
70
+ '.agents/hooks.json': removeAntigravityArkHook,
71
+ };
72
+
59
73
  function treeFiles(root) {
60
74
  const files = new Map();
61
75
  const visit = (dir) => {
@@ -133,7 +147,7 @@ export function setupBudget(changes) {
133
147
  }
134
148
 
135
149
  /** One next write when compact start is the wrong size. `--force` does not unlock. */
136
- export const START_SETUP_BUDGET_NEXT = 'arkgate-check --init';
150
+ const START_SETUP_BUDGET_NEXT = 'arkgate-check --init';
137
151
 
138
152
  function formatSetupKb(bytes) {
139
153
  return `${Math.max(1, Math.round(Number(bytes) / 1024))} KB`;
@@ -334,6 +348,22 @@ function planHostRemoval(args, helpers) {
334
348
  changes.push(change(relativePath, before, after));
335
349
  continue;
336
350
  }
351
+ const remover = MERGED_HOOK_FILE_REMOVERS[relativePath];
352
+ if (remover && before.toString('utf8') !== expected) {
353
+ const removal = remover(before.toString('utf8'), expected);
354
+ if (removal.status === 'removed') {
355
+ changes.push(
356
+ change(relativePath, before, removal.text == null ? null : Buffer.from(removal.text))
357
+ );
358
+ } else if (removal.status === 'customized') {
359
+ unresolvedDecisions.push(
360
+ `${relativePath}: the Ark hook entry was customized and was left untouched.`
361
+ );
362
+ } else if (removal.status === 'unreadable') {
363
+ unresolvedDecisions.push(`${relativePath} is not readable JSON and was left untouched.`);
364
+ }
365
+ continue;
366
+ }
337
367
  if (before.toString('utf8') !== expected) {
338
368
  unresolvedDecisions.push(`${relativePath} was customized and was left untouched.`);
339
369
  continue;
@@ -9,7 +9,7 @@ import fs from 'node:fs';
9
9
  import path from 'node:path';
10
10
 
11
11
  /** Product-domain / domain-model homes. Closed list — no tree walk. */
12
- export const STATES_TRANSITIONS_DOMAIN_HOMES = Object.freeze([
12
+ const STATES_TRANSITIONS_DOMAIN_HOMES = Object.freeze([
13
13
  'docs/domain.md',
14
14
  'docs/data-model.md',
15
15
  'docs/product-domain.md',
@@ -19,7 +19,7 @@ export const STATES_TRANSITIONS_DOMAIN_HOMES = Object.freeze([
19
19
  ]);
20
20
 
21
21
  /** Evolved dedicated homes. Adopt if present; do not force a filename. */
22
- export const STATES_TRANSITIONS_DEDICATED_HOMES = Object.freeze([
22
+ const STATES_TRANSITIONS_DEDICATED_HOMES = Object.freeze([
23
23
  'docs/states.md',
24
24
  'docs/transitions.md',
25
25
  'docs/states-transitions.md',
@@ -32,7 +32,7 @@ export const STATES_TRANSITIONS_ASK_MISSING =
32
32
  export const STATES_TRANSITIONS_ASK_INCOMPLETE =
33
33
  'The states heading is there, but it is not a map yet — no table or link.';
34
34
 
35
- export const STATES_TRANSITIONS_NEXT =
35
+ const STATES_TRANSITIONS_NEXT =
36
36
  'Add a short table (entity · states · allowed from → to) — or one link — on the domain doc. Names from the code. No flag soup.';
37
37
 
38
38
  function readMarkdown(abs) {