arkgate 4.8.13 → 4.8.15

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 (126) hide show
  1. package/CHANGELOG.md +219 -4
  2. package/README.md +33 -22
  3. package/SECURITY.md +5 -3
  4. package/bin/ark-check-runtime.mjs +25 -57
  5. package/bin/ark-mcp-runtime.mjs +111 -2
  6. package/bin/ark-shared.mjs +140 -11
  7. package/bin/ark.mjs +7 -25
  8. package/bin/lib/adr-presence.mjs +97 -0
  9. package/bin/lib/agent-projection-formatters.mjs +2 -0
  10. package/bin/lib/agent-skills-package.mjs +241 -8
  11. package/bin/lib/analysis-completeness.mjs +38 -2
  12. package/bin/lib/analysis-engine.mjs +6 -6
  13. package/bin/lib/architecture-scan.mjs +33 -5
  14. package/bin/lib/ark-order-doctor.mjs +7 -1
  15. package/bin/lib/ark-order-report.mjs +2 -1
  16. package/bin/lib/ark-run-doctor.mjs +6 -0
  17. package/bin/lib/ark-run-report.mjs +6 -2
  18. package/bin/lib/arkrules-sensors.mjs +81 -0
  19. package/bin/lib/baseline-key.mjs +4 -1
  20. package/bin/lib/check-args.mjs +52 -2
  21. package/bin/lib/ci-and-commands.mjs +7 -2
  22. package/bin/lib/config-contract.mjs +64 -1
  23. package/bin/lib/design-smells.mjs +21 -1
  24. package/bin/lib/diagnostic-catalog.mjs +5 -3
  25. package/bin/lib/doctor-advisories.mjs +109 -17
  26. package/bin/lib/doctor-human.mjs +20 -6
  27. package/bin/lib/doctor-next-actions.mjs +14 -0
  28. package/bin/lib/doctor-plan.mjs +32 -2
  29. package/bin/lib/enforcement-honesty.mjs +47 -2
  30. package/bin/lib/first-run-help.mjs +19 -3
  31. package/bin/lib/gate-files.mjs +40 -3
  32. package/bin/lib/host-support-matrix.mjs +9 -1
  33. package/bin/lib/html-report-depth.mjs +2 -0
  34. package/bin/lib/html-report.mjs +13 -2
  35. package/bin/lib/import-resolve.mjs +74 -13
  36. package/bin/lib/improvement-compass-doctor.mjs +6 -1
  37. package/bin/lib/improvement-compass-map.mjs +3 -1
  38. package/bin/lib/install-migrate.mjs +23 -0
  39. package/bin/lib/layer-description.mjs +90 -8
  40. package/bin/lib/mcp-hook-payload.mjs +57 -1
  41. package/bin/lib/no-domain-frontend.mjs +91 -0
  42. package/bin/lib/presets.mjs +3 -4
  43. package/bin/lib/product-copy.mjs +19 -0
  44. package/bin/lib/projected-governed-coverage.mjs +114 -0
  45. package/bin/lib/recommend-cli.mjs +54 -0
  46. package/bin/lib/remediation.mjs +6 -2
  47. package/bin/lib/resolved-candidate-facts.mjs +225 -100
  48. package/bin/lib/rules-under-contract.mjs +117 -2
  49. package/bin/lib/scan-files.mjs +39 -0
  50. package/bin/lib/snippet-analysis.mjs +14 -8
  51. package/bin/lib/start-preview.mjs +8 -2
  52. package/bin/lib/states-transitions-presence.mjs +212 -0
  53. package/bin/lib/status-command.mjs +2 -0
  54. package/bin/lib/status-transition-catalog.mjs +410 -0
  55. package/bin/lib/team-parliament-io.mjs +10 -0
  56. package/bin/lib/upgrade-whats-new.mjs +3 -3
  57. package/bin/lib/violations.mjs +43 -1
  58. package/bin/lib/write-path-capabilities.mjs +20 -5
  59. package/bin/lib/write-path-detect.mjs +27 -2
  60. package/dist/{configTypes-j7so8B4O.d.ts → configTypes-Dt3DpVbd.d.ts} +19 -0
  61. package/dist/{diagnosticCatalog-DA565Lja.d.ts → diagnosticCatalog-BEg85XlE.d.ts} +3 -3
  62. package/dist/eslint/index.cjs +4 -4
  63. package/dist/eslint/index.d.ts +1 -1
  64. package/dist/eslint/index.js +4 -4
  65. package/dist/index.cjs +31 -31
  66. package/dist/index.d.ts +126 -19
  67. package/dist/index.js +31 -31
  68. package/dist/nestjs/index.cjs +5 -5
  69. package/dist/nestjs/index.d.ts +3 -3
  70. package/dist/nestjs/index.js +5 -5
  71. package/dist/runtime/index.cjs +15 -15
  72. package/dist/runtime/index.d.ts +6 -6
  73. package/dist/runtime/index.js +15 -15
  74. package/dist/{types-Djbs3KjE.d.ts → types-CN9tVMPz.d.ts} +3 -1
  75. package/dist/{types-tGhZUiGX.d.ts → types-TBiv0WHL.d.ts} +1 -1
  76. package/docs/README.md +8 -6
  77. package/docs/agent-guide.md +35 -17
  78. package/docs/ai-gates.md +13 -4
  79. package/docs/arkorder.md +11 -4
  80. package/docs/brownfield-adoption.md +4 -1
  81. package/docs/configuration.md +63 -14
  82. package/docs/develop.md +4 -1
  83. package/docs/diagnostics.md +30 -10
  84. package/docs/enthusiast/README.md +6 -1
  85. package/docs/enthusiast/how-to-agent-gates.md +5 -0
  86. package/docs/enthusiast/how-to-gallery-starter.md +2 -1
  87. package/docs/enthusiast/how-to-pick-shape.md +1 -1
  88. package/docs/package-surface.md +10 -6
  89. package/docs/product-voice.md +32 -6
  90. package/docs/threat-model.md +2 -2
  91. package/docs/typescript-support.md +3 -3
  92. package/docs/use.md +23 -11
  93. package/package.json +1 -1
  94. package/schemas/ark.config.schema.json +23 -2
  95. package/server.json +2 -2
  96. package/templates/agent-skills/README.md +7 -4
  97. package/templates/agent-skills/ark-adopt/SKILL.md +91 -6
  98. package/templates/agent-skills/ark-architect/SKILL.md +5 -18
  99. package/templates/agent-skills/ark-autopilot/SKILL.md +14 -6
  100. package/templates/agent-skills/ark-contract/SKILL.md +9 -20
  101. package/templates/agent-skills/ark-coverage/SKILL.md +13 -9
  102. package/templates/agent-skills/ark-explain/SKILL.md +11 -5
  103. package/templates/agent-skills/ark-explore/SKILL.md +39 -5
  104. package/templates/agent-skills/ark-fix/SKILL.md +15 -20
  105. package/templates/agent-skills/ark-loop/SKILL.md +14 -20
  106. package/templates/agent-skills/ark-order/SKILL.md +200 -0
  107. package/templates/agent-skills/ark-place/SKILL.md +49 -11
  108. package/templates/agent-skills/ark-runtime/SKILL.md +21 -6
  109. package/templates/agent-skills/ark-think/SKILL.md +24 -126
  110. package/templates/agent-skills/ark-upgrade/SKILL.md +14 -3
  111. package/templates/arkrules/DomainModel.json +14 -1
  112. package/templates/skills/ark-adopt.md +91 -6
  113. package/templates/skills/ark-architect.md +5 -18
  114. package/templates/skills/ark-autopilot.md +14 -6
  115. package/templates/skills/ark-contract.md +9 -20
  116. package/templates/skills/ark-coverage.md +13 -9
  117. package/templates/skills/ark-explain.md +11 -5
  118. package/templates/skills/ark-explore.md +39 -5
  119. package/templates/skills/ark-fix.md +15 -20
  120. package/templates/skills/ark-loop.md +14 -20
  121. package/templates/skills/ark-order.md +200 -0
  122. package/templates/skills/ark-place.md +49 -11
  123. package/templates/skills/ark-runtime.md +21 -6
  124. package/templates/skills/ark-think.md +24 -126
  125. package/templates/skills/ark-upgrade.md +14 -3
  126. package/templates/tests/ark-adoption-gaps.test.ts +5 -4
@@ -18,10 +18,19 @@ import {
18
18
  } from './reshape-decisions.mjs';
19
19
  import { printParseHealthSection, summarizeParseHealth } from './parse-health.mjs';
20
20
  import { detectGraphBlindSpots, printGraphBlindSection } from './graph-blind.mjs';
21
- import { summarizeRulesUnderContract } from './rules-under-contract.mjs';
21
+ import {
22
+ formatArkRulesDoctorLines,
23
+ summarizeRulesUnderContract,
24
+ } from './rules-under-contract.mjs';
22
25
  import { collectStewardNudge } from './team-parliament-io.mjs';
23
- import { formatArkRunDoctorLines, summarizeArkRunSection } from './ark-run-doctor.mjs';
24
26
  import {
27
+ ARKRUN_FIRST_CONTACT_NEXT,
28
+ ARKRUN_ONE_BREATH,
29
+ formatArkRunDoctorLines,
30
+ summarizeArkRunSection,
31
+ } from './ark-run-doctor.mjs';
32
+ import {
33
+ ARKORDER_FIRST_CONTACT_NEXT,
25
34
  ARKORDER_ONE_BREATH,
26
35
  formatArkOrderDoctorLines,
27
36
  summarizeArkOrderSection,
@@ -79,22 +88,77 @@ export function attachExtraDoctorSections(rulesUnderContract, config, classifica
79
88
  return { arkRun, arkOrder, mergePlanes };
80
89
  }
81
90
 
91
+ function arkRulesDoctorMark(section, warn) {
92
+ if (Array.isArray(section?.loadErrors) && section.loadErrors.length > 0) return warn;
93
+ if (section?.emptyInvariantCatalog === true) return warn;
94
+ if ((Number(section?.uncoveredInvariants) || 0) > 0) return warn;
95
+ return ' ';
96
+ }
97
+
82
98
  export function printCompactExtraDoctorLines(advisories, io) {
99
+ const owners = advisories?.layerOwners;
100
+ if (owners?.required && owners.ask) {
101
+ console.log('');
102
+ io.line(io.warn, owners.ask);
103
+ if (owners.nextAction) io.line(' ', `Next: ${owners.nextAction}`);
104
+ }
105
+ const adr = advisories?.adrPresence;
106
+ if (adr?.missing && adr.ask) {
107
+ console.log('');
108
+ io.line(io.warn, adr.ask);
109
+ if (adr.nextAction) io.line(' ', `Next: ${adr.nextAction}`);
110
+ }
111
+ const catalog = advisories?.statusTransitionCatalog;
112
+ if (catalog?.ask) {
113
+ console.log('');
114
+ io.line(io.warn, catalog.ask);
115
+ if (catalog.nextAction) io.line(' ', `Next: ${catalog.nextAction}`);
116
+ } else {
117
+ const states = advisories?.statesTransitions;
118
+ if (states?.ask) {
119
+ console.log('');
120
+ io.line(io.warn, states.ask);
121
+ if (states.nextAction) io.line(' ', `Next: ${states.nextAction}`);
122
+ }
123
+ }
124
+ const noDomain = advisories?.noDomainFrontend;
125
+ if (noDomain?.ask) {
126
+ console.log('');
127
+ io.line(io.warn, noDomain.ask);
128
+ if (noDomain.nextAction) io.line(' ', `Next: ${noDomain.nextAction}`);
129
+ }
130
+ const rulesUnderContract = advisories?.rulesUnderContract;
131
+ const arkRulesLines = formatArkRulesDoctorLines(rulesUnderContract);
132
+ if (arkRulesLines.length > 0) {
133
+ console.log('');
134
+ const mark = arkRulesDoctorMark(rulesUnderContract, io.warn);
135
+ for (const text of arkRulesLines) io.line(mark, text);
136
+ }
83
137
  const arkRun = advisories?.arkRun;
84
138
  if (arkRun?.active === true && arkRun.notAScore === true) {
85
139
  console.log('');
86
140
  const residual = Number(arkRun.residual?.count) || 0;
87
- io.line(residual > 0 ? io.warn : ' ', `ArkRun: ${arkRun.mode || 'on'} · residual=${residual} · not a score`);
141
+ const mark = residual > 0 ? io.warn : ' ';
142
+ io.line(mark, ARKRUN_ONE_BREATH);
143
+ io.line(mark, `ArkRun: ${arkRun.mode || 'on'} · residual=${residual} · not a score`);
144
+ if (residual > 0) io.line(mark, ARKRUN_FIRST_CONTACT_NEXT);
88
145
  }
89
146
  const arkOrder = advisories?.arkOrder;
90
- if (arkOrder?.active === true && arkOrder.notAScore === true) {
147
+ if (arkOrder && arkOrder.notAScore === true) {
91
148
  console.log('');
92
- const residual = Number(arkOrder.residual?.count) || 0;
93
- const keys =
94
- Array.isArray(arkOrder.xiKeys) && arkOrder.xiKeys.length > 0 ? arkOrder.xiKeys.join(', ') : 'unnamed';
95
- const mark = residual > 0 ? io.warn : ' ';
96
- io.line(mark, ARKORDER_ONE_BREATH);
97
- io.line(mark, `ArkOrder: ${arkOrder.mode || 'on'} · xiKeys=${keys} · residual=${residual} · not a score`);
149
+ if (arkOrder.active === true) {
150
+ const residual = Number(arkOrder.residual?.count) || 0;
151
+ const keys =
152
+ Array.isArray(arkOrder.xiKeys) && arkOrder.xiKeys.length > 0
153
+ ? arkOrder.xiKeys.join(', ')
154
+ : 'unnamed';
155
+ const mark = residual > 0 ? io.warn : ' ';
156
+ io.line(mark, ARKORDER_ONE_BREATH);
157
+ io.line(mark, `ArkOrder: ${arkOrder.mode || 'on'} · xiKeys=${keys} · residual=${residual} · not a score`);
158
+ } else {
159
+ io.line(' ', ARKORDER_ONE_BREATH);
160
+ io.line(' ', ARKORDER_FIRST_CONTACT_NEXT);
161
+ }
98
162
  }
99
163
  }
100
164
 
@@ -119,16 +183,36 @@ export function computeDoctorAdvisories(root, config, cov, rules, files, ts, par
119
183
  root,
120
184
  decisionMemory
121
185
  );
122
- // Prefer architecture facts paths when available; coverage I/O still walks test roots.
186
+ // Prefer architecture facts paths when available; still union the doctor walk
187
+ // so an empty-catalog residual can see Domain files the facts subset missed.
188
+ const normalizeDoctorFile = (entry) => {
189
+ const raw =
190
+ typeof entry === 'string'
191
+ ? entry
192
+ : typeof entry?.path === 'string'
193
+ ? entry.path
194
+ : '';
195
+ const path = raw.replace(/\\/g, '/').replace(/^\.\//, '');
196
+ return path ? { path } : null;
197
+ };
198
+ const walkFiles = Array.isArray(files) ? files.map(normalizeDoctorFile).filter(Boolean) : [];
199
+ const factFiles = Array.isArray(facts?.files)
200
+ ? facts.files.map(normalizeDoctorFile).filter(Boolean)
201
+ : [];
202
+ const seen = new Set();
203
+ const mergedFiles = [];
204
+ for (const entry of [...factFiles, ...walkFiles]) {
205
+ if (seen.has(entry.path)) continue;
206
+ seen.add(entry.path);
207
+ mergedFiles.push(entry);
208
+ }
123
209
  const factPaths =
124
- facts ??
125
- (Array.isArray(files)
210
+ facts || walkFiles.length > 0
126
211
  ? {
127
- files: files.map((f) => ({
128
- path: typeof f === 'string' ? f.replace(/\\/g, '/').replace(/^\.\//, '') : f?.path,
129
- })).filter((f) => f.path),
212
+ ...(facts && typeof facts === 'object' ? facts : {}),
213
+ files: mergedFiles,
130
214
  }
131
- : undefined);
215
+ : undefined;
132
216
  const classification = classificationFromCoverage(cov);
133
217
  const rulesUnderContract = summarizeRulesUnderContract(root, config, factPaths, classification);
134
218
  const { arkRun, arkOrder } = attachExtraDoctorSections(
@@ -171,6 +255,14 @@ export function printDoctorAdvisories(advisories, io) {
171
255
  io.line(io.warn, nudge.ask);
172
256
  if (nudge.nextAction) io.line(' ', io.color.dim(`Next: ${nudge.nextAction}`));
173
257
  }
258
+ const rulesUnderContract = advisories.rulesUnderContract;
259
+ const arkRulesLines = formatArkRulesDoctorLines(rulesUnderContract);
260
+ if (arkRulesLines.length > 0) {
261
+ console.log('');
262
+ console.log(io.color.bold('ArkRules (not a score)'));
263
+ const mark = arkRulesDoctorMark(rulesUnderContract, io.warn);
264
+ for (const text of arkRulesLines) io.line(mark, text);
265
+ }
174
266
  const arkRun = advisories.arkRun;
175
267
  if (arkRun && arkRun.notAScore === true) {
176
268
  console.log('');
@@ -4,7 +4,7 @@
4
4
  */
5
5
  import path from 'node:path';
6
6
  import { arkCommand } from '../ark-shared.mjs';
7
- import { operatingModeTitle } from './product-copy.mjs';
7
+ import { NORTH_STAR_ONE_LINE, operatingModeTitle } from './product-copy.mjs';
8
8
  import { isDoctorHealthyNothingToDo } from './post-green-path.mjs';
9
9
  import { printParseHealthSection } from './parse-health.mjs';
10
10
  import { printDoctorAdvisories, printCompactExtraDoctorLines } from './doctor-advisories.mjs';
@@ -14,6 +14,7 @@ import { analysisIncompleteStatement } from './analysis-completeness.mjs';
14
14
  import { skillGapsForActiveHost, detectCodexHomeGap, codexConcernIsActive } from './agent-gates.mjs';
15
15
  import { agentHomeConcernIsActive } from './agent-homes.mjs';
16
16
  import { REQUIRED_GATE_WORKFLOW } from './gate-files.mjs';
17
+ import { layerGuidanceLine } from './layer-description.mjs';
17
18
 
18
19
  function displayedMissingGates(gatesMissing, view) {
19
20
  const list = Array.isArray(gatesMissing) ? gatesMissing : [];
@@ -73,6 +74,7 @@ export function printDoctorCompactHuman(view) {
73
74
  const skippableCi = ciNotFailClosedNotice(view);
74
75
 
75
76
  console.log(color.bold(`Ark doctor — ${path.basename(path.resolve(root)) || '.'}`));
77
+ console.log(color.dim(NORTH_STAR_ONE_LINE));
76
78
  if (!analysisComplete) line(warn, analysisIncompleteStatement(completeness));
77
79
  printParseHealthSection(doctorAdvisories.parseHealth, { color, warn, line });
78
80
 
@@ -138,13 +140,15 @@ export function printDoctorCompactHuman(view) {
138
140
  : warn;
139
141
  line(govMark, `Governed: ${cov.governed.percent}% (${cov.governed.classifiedFiles}/${cov.governed.totalFiles} files)`);
140
142
  for (const row of cov.layers ?? []) {
141
- if (row.description) line(' ', `${row.name} — ${row.description}`);
143
+ const guidance = layerGuidanceLine(row);
144
+ if (guidance) line(' ', `${row.name} — ${guidance}`);
142
145
  }
143
146
 
144
147
  const hostRed =
145
148
  listedMissing.length > 0 ||
146
149
  Boolean(writePath.gap) ||
147
150
  writePathHonesty?.softWriteHost === true ||
151
+ writePathHonesty?.nativeFailClosed === false ||
148
152
  Boolean(skippableCi);
149
153
  if (hostRed) {
150
154
  console.log('');
@@ -152,7 +156,9 @@ export function printDoctorCompactHuman(view) {
152
156
  if (writePath.activeHost) line(' ', `Active host: ${writePath.activeHost}`);
153
157
  if (listedMissing.length > 0) line(bad, `Missing gates: ${listedMissing.join(', ')}`);
154
158
  if (skippableCi) line(warn, skippableCi);
155
- else if (listedMissing.length === 0 && (writePath.gap || writePathHonesty?.softWriteHost)) {
159
+ else if (writePathHonesty?.nativeFailClosed === false) {
160
+ line(warn, writePathHonesty.message || 'Write hook is fail-open — if the checker cannot run, the write still lands.');
161
+ } else if (listedMissing.length === 0 && (writePath.gap || writePathHonesty?.softWriteHost)) {
156
162
  line(warn, 'Local writes are advisory; required CI is the merge boundary.');
157
163
  }
158
164
  }
@@ -164,7 +170,15 @@ export function printDoctorCompactHuman(view) {
164
170
  line(warn, nudge.ask);
165
171
  }
166
172
 
167
- printCompactExtraDoctorLines(doctorAdvisories, { line, warn });
173
+ printCompactExtraDoctorLines(
174
+ {
175
+ ...doctorAdvisories,
176
+ layerOwners: view.layerOwners,
177
+ adrPresence: view.adrPresence,
178
+ statesTransitions: view.statesTransitions, statusTransitionCatalog: view.statusTransitionCatalog, noDomainFrontend: view.noDomainFrontend,
179
+ },
180
+ { line, warn }
181
+ );
168
182
 
169
183
  if (violations.length === 0) {
170
184
  if (!analysisComplete) {
@@ -248,11 +262,11 @@ export function printDoctorDetailsHuman(view) {
248
262
  );
249
263
  }
250
264
  if (cov.suggestions.length === 0 && cov.emptyLayers.length === 0) line(ok, 'Every layer classifies files; no empty layers');
251
- const captioned = (cov.layers ?? []).filter((row) => row.description);
265
+ const captioned = (cov.layers ?? []).filter((row) => layerGuidanceLine(row));
252
266
  if (captioned.length > 0) {
253
267
  console.log('');
254
268
  console.log(color.bold('Layers'));
255
- for (const row of captioned) line(' ', `${row.name} — ${row.description}`);
269
+ for (const row of captioned) line(' ', `${row.name} — ${layerGuidanceLine(row)}`);
256
270
  }
257
271
 
258
272
  if (packageVersionTruth?.dualTruth) {
@@ -34,6 +34,9 @@ export function collectDoctorNextActions(ctx) {
34
34
  ) {
35
35
  actions.push(nudge.nextAction);
36
36
  }
37
+ if (ctx.layerOwners?.required && ctx.layerOwners.nextAction) {
38
+ actions.push(ctx.layerOwners.nextAction);
39
+ }
37
40
  const enforceEmptyPlan =
38
41
  ctx.operatingMode === 'enforce' && planAEmpty && gatesInstalled && !notAdopted;
39
42
  if (enforceEmptyPlan) {
@@ -42,6 +45,17 @@ export function collectDoctorNextActions(ctx) {
42
45
  '/ark-explore, then one small refactor with /ark-autopilot and your OK'
43
46
  );
44
47
  }
48
+ if (ctx.adrPresence?.missing && ctx.adrPresence.nextAction) {
49
+ actions.push(ctx.adrPresence.nextAction);
50
+ }
51
+ if (ctx.statusTransitionCatalog?.nextAction) {
52
+ actions.push(ctx.statusTransitionCatalog.nextAction);
53
+ } else if (ctx.statesTransitions?.nextAction) {
54
+ actions.push(ctx.statesTransitions.nextAction);
55
+ }
56
+ if (ctx.noDomainFrontend?.nextAction) {
57
+ actions.push(ctx.noDomainFrontend.nextAction);
58
+ }
45
59
  if (!ctx.analysisComplete) actions.push('restore complete analysis, then rerun ark-check --doctor');
46
60
  if (ctx.designSmells.length > 0 && ctx.postGreenPath) actions.push(ctx.postGreenPath.action);
47
61
  if (ctx.coverageHonesty.greenIsNotEnforcement && ctx.coverageHonesty.worseThanNoGate) {
@@ -15,7 +15,12 @@ import { describePackageVersionDualTruth } from './field-install.mjs';
15
15
  import { detectAgentHomeGaps } from './agent-homes.mjs';
16
16
  import { collectDoctorNextActions } from './doctor-next-actions.mjs';
17
17
  import { printDoctorCompactHuman, printDoctorDetailsHuman } from './doctor-human.mjs';
18
- import { placementDescriptionFields } from './layer-description.mjs';
18
+ import { collectLayerOwnerResidual, layerGuidanceLine, placementDescriptionFields } from './layer-description.mjs';
19
+ import { collectAdrPresenceResidual, printAdrPresenceHint } from './adr-presence.mjs';
20
+ import { collectStatesTransitionsResidual } from './states-transitions-presence.mjs';
21
+ import { collectStatusTransitionCatalogResidual } from './status-transition-catalog.mjs';
22
+ import { collectNoDomainFrontendResidual } from './no-domain-frontend.mjs';
23
+ export { printAdrPresenceHint };
19
24
  export { printDoctorCompactHuman, printDoctorDetailsHuman };
20
25
  export { summarizeRulesUnderContract };
21
26
 
@@ -173,7 +178,8 @@ export function runCoverage(root, config, files, rules, asJson) {
173
178
  console.log(` ${pad('Layer')} Files`);
174
179
  for (const row of layerRows) {
175
180
  const flag = row.files === 0 ? ' (pattern matches nothing)' : '';
176
- const caption = row.description ? ` ${row.description}` : '';
181
+ const guidance = layerGuidanceLine(row);
182
+ const caption = guidance ? ` ${guidance}` : '';
177
183
  console.log(` ${pad(row.name)} ${String(row.files).padStart(5)}${flag}${caption}`);
178
184
  }
179
185
  console.log(` ${pad('(unclassified)')} ${String(unclassified.length).padStart(5)}`);
@@ -616,6 +622,7 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
616
622
  options.facts ?? options.architectureFacts,
617
623
  activeViolations
618
624
  );
625
+ const layerOwners = collectLayerOwnerResidual(config);
619
626
  const rulesUnderContract = doctorAdvisories.rulesUnderContract;
620
627
  const arkRun = doctorAdvisories.arkRun;
621
628
  // Single residual expression (nextPilot || extractionCard) — HTML report uses the same.
@@ -636,6 +643,13 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
636
643
  github: githubForBoundary,
637
644
  ci: ciMergeBoundary?.ci,
638
645
  });
646
+ const adrPresence = collectAdrPresenceResidual({
647
+ root,
648
+ demanded: options.requireGates === true || adopted === 'required-merge',
649
+ });
650
+ const statesTransitions = collectStatesTransitionsResidual({ root });
651
+ const statusTransitionCatalog = collectStatusTransitionCatalogResidual({ root, config, files, statesTransitions });
652
+ const noDomainFrontend = collectNoDomainFrontendResidual({ config, coverage: cov, designSmells });
639
653
  const { coverageHonesty, baselineHonesty, writePathHonesty, productHonesty } =
640
654
  computeDoctorEnforcementHonesty({
641
655
  governedPercent: cov.governed.percent,
@@ -671,6 +685,8 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
671
685
  selfHost:
672
686
  packageVersionTruth?.selfHost === true ||
673
687
  packageVersionTruth?.code === 'PACKAGE_PIN_SELF_HOST',
688
+ nativeFailClosed: writePath.nativeFailClosed,
689
+ nativeFailClosedPolicy: writePath.nativeFailClosedPolicy,
674
690
  adopted,
675
691
  ciMergeBoundary,
676
692
  github: githubForBoundary,
@@ -763,6 +779,10 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
763
779
  governed: cov.governed,
764
780
  coverageHonesty,
765
781
  layers: cov.layers,
782
+ ...(layerOwners ? { layerOwners } : {}),
783
+ ...(adrPresence ? { adrPresence } : {}),
784
+ ...(statesTransitions ? { statesTransitions } : {}),
785
+ ...(statusTransitionCatalog ? { statusTransitionCatalog } : {}), ...(noDomainFrontend ? { noDomainFrontend } : {}),
766
786
  emptyLayers: cov.emptyLayers,
767
787
  layersWithoutRules: cov.layersWithoutRules,
768
788
  ungovernedDirs: cov.suggestions.length,
@@ -827,6 +847,8 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
827
847
  mcpPresent: writePath.mcpPresent,
828
848
  evidence: writePath.evidence,
829
849
  honesty: writePathHonesty,
850
+ nativeFailClosed: writePath.nativeFailClosed ?? null,
851
+ nativeFailClosedPolicy: writePath.nativeFailClosedPolicy ?? 'unsupported',
830
852
  ...(writePath.sessionNote ? { sessionNote: writePath.sessionNote } : {}),
831
853
  ...(writePath.gap
832
854
  ? {
@@ -902,12 +924,20 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
902
924
  operatingMode,
903
925
  adopted,
904
926
  stewardNudge: doctorAdvisories.stewardNudge,
927
+ layerOwners,
928
+ adrPresence,
929
+ statesTransitions,
930
+ statusTransitionCatalog, noDomainFrontend,
905
931
  });
906
932
  const humanView = {
907
933
  root,
908
934
  analysisComplete,
909
935
  completeness,
910
936
  doctorAdvisories,
937
+ layerOwners,
938
+ adrPresence,
939
+ statesTransitions,
940
+ statusTransitionCatalog, noDomainFrontend,
911
941
  operatingMode,
912
942
  designFitness,
913
943
  adopted,
@@ -180,6 +180,8 @@ export function buildBaselineHonesty(input = {}) {
180
180
  * packagePinAbsent?: boolean,
181
181
  * selfHost?: boolean,
182
182
  * motherCli?: boolean,
183
+ * nativeFailClosed?: boolean | null,
184
+ * nativeFailClosedPolicy?: 'required' | 'unsupported' | 'none',
183
185
  * }} [extras]
184
186
  */
185
187
  export function buildWritePathHonesty(activeHost, hardWriteActive = false, extras = {}) {
@@ -202,7 +204,21 @@ export function buildWritePathHonesty(activeHost, hardWriteActive = false, extra
202
204
  const hardAllowed = packageInstalled && !pinAbsentForUser;
203
205
  const effectiveHard =
204
206
  Boolean(hardWriteActive) && hardCapable && !softWriteHost && hardAllowed;
205
- const message = doctorWritePathHonestyMessage(host, effectiveHard);
207
+ const nativeFailClosedPolicy =
208
+ extras.nativeFailClosedPolicy === 'required' ||
209
+ extras.nativeFailClosedPolicy === 'none' ||
210
+ extras.nativeFailClosedPolicy === 'unsupported'
211
+ ? extras.nativeFailClosedPolicy
212
+ : HOST_SUPPORT_MATRIX[host]?.nativeFailClosed ?? (hardCapable ? 'unsupported' : 'none');
213
+ const nativeFailClosed =
214
+ extras.nativeFailClosed === true ? true : extras.nativeFailClosed === false ? false : null;
215
+ const failOpen = nativeFailClosedPolicy === 'required' && nativeFailClosed === false;
216
+ const honestyMessage = doctorWritePathHonestyMessage(host, effectiveHard);
217
+ const failOpenMessage =
218
+ `${HOST_SUPPORT_MATRIX[host]?.label ?? 'This host'}: the write hook is fail-open. ` +
219
+ 'If the checker cannot run, the write still lands. Set failClosed: true on the write hook — ' +
220
+ 'same idea as a file permission: no checker, no write. Required CI is the shared merge line.';
221
+ const message = failOpen ? failOpenMessage : honestyMessage;
206
222
 
207
223
  /** @type {Record<string, unknown>} */
208
224
  const out = {
@@ -212,6 +228,8 @@ export function buildWritePathHonesty(activeHost, hardWriteActive = false, extra
212
228
  hardWriteSupported: hardCapable,
213
229
  hardWriteActive: effectiveHard,
214
230
  hardWriteUnverified: hardCapable && !effectiveHard,
231
+ nativeFailClosed,
232
+ nativeFailClosedPolicy,
215
233
  hardMergeBoundary:
216
234
  'required-github-status-context (CLI: arkgate-check --strict-merge / ark-check --strict-merge)',
217
235
  packageInstalled,
@@ -385,6 +403,16 @@ export function buildProductHonesty(input = {}) {
385
403
  });
386
404
  }
387
405
 
406
+ if (write?.nativeFailClosed === false && write?.nativeFailClosedPolicy === 'required') {
407
+ reasons.push({
408
+ id: 'native-fail-open',
409
+ bucket: 'environment',
410
+ message:
411
+ write.message ||
412
+ 'The write hook is fail-open. If the checker cannot run, the write still lands. Set failClosed: true on the write hook.',
413
+ });
414
+ }
415
+
388
416
  const adopted =
389
417
  typeof input.adopted === 'string'
390
418
  ? input.adopted
@@ -442,7 +470,7 @@ export function buildProductHonesty(input = {}) {
442
470
  }
443
471
 
444
472
  // EH05: environment residual deny-list (future reason ids stay architecture debt by default).
445
- const ENVIRONMENT_REASON_IDS = new Set(['soft-write-host']);
473
+ const ENVIRONMENT_REASON_IDS = new Set(['soft-write-host', 'native-fail-open']);
446
474
 
447
475
  const environmentResiduals = reasons.filter((r) => ENVIRONMENT_REASON_IDS.has(r.id));
448
476
  const architectureReasons = reasons.filter((r) => !ENVIRONMENT_REASON_IDS.has(r.id));
@@ -457,6 +485,8 @@ export function buildProductHonesty(input = {}) {
457
485
 
458
486
  const softWriteOnly =
459
487
  !unfinished && environmentResiduals.some((r) => r.id === 'soft-write-host');
488
+ const failOpenOnly =
489
+ !unfinished && environmentResiduals.some((r) => r.id === 'native-fail-open');
460
490
  const hostLabel = (() => {
461
491
  const h = typeof write?.activeHost === 'string' ? write.activeHost.trim().toLowerCase() : '';
462
492
  if (h === 'codex') return 'Codex';
@@ -493,6 +523,10 @@ export function buildProductHonesty(input = {}) {
493
523
  'This tree acked advisory-only in .ark/adoption-stance.json. That is not a required GitHub merge status.';
494
524
  } else if (softWriteOnly) {
495
525
  primaryMessage = `${hostLabel} local writes stay advisory/bypassable; architecture contract on this slice is ready. Hard merge boundary is a required GitHub status context running arkgate-check --strict-merge (alias ark-check --strict-merge).`;
526
+ } else if (failOpenOnly) {
527
+ primaryMessage =
528
+ write?.message ||
529
+ `${hostLabel} write hook is fail-open — if the checker cannot run, the write still lands. Set failClosed: true. Required CI is the shared merge line.`;
496
530
  } else if (wholeTreeGoverned) {
497
531
  primaryMessage =
498
532
  'No residual honesty blockers on this slice — still not a numeric architecture score; re-doctor after material change.';
@@ -509,6 +543,10 @@ export function buildProductHonesty(input = {}) {
509
543
  headline = wholeTreeGoverned
510
544
  ? `Architecture contract ready; ${hostLabel} local writes are advisory`
511
545
  : `Contract residual clear; ${hostLabel} local writes are advisory`;
546
+ } else if (!unfinished && failOpenOnly) {
547
+ headline = wholeTreeGoverned
548
+ ? `Architecture contract ready; ${hostLabel} write hook is fail-open`
549
+ : `Contract residual clear; ${hostLabel} write hook is fail-open`;
512
550
  } else if (!unfinished && adopted === 'advisory-only-acked') {
513
551
  headline = 'Advisory-only adoption — merge status is not required';
514
552
  } else if (!unfinished) {
@@ -536,6 +574,9 @@ export function buildProductHonesty(input = {}) {
536
574
  primaryNextAction =
537
575
  input.stewardNudge?.nextAction ||
538
576
  '/ark-adopt (ask, then update stewards[] — do not invent names)';
577
+ } else if (!primaryNextAction && failOpenOnly) {
578
+ primaryNextAction =
579
+ 'Set failClosed: true on the write hook, then re-run doctor. Required CI is the shared merge line.';
539
580
  } else if (!primaryNextAction && softWriteOnly) {
540
581
  primaryNextAction =
541
582
  'Confirm the GitHub required status context name runs arkgate-check --strict-merge (or ark-check --strict-merge). Soft-write hosts stay advisory at local write; the required status is the hard merge boundary.';
@@ -613,6 +654,8 @@ export function computeDoctorEnforcementHonesty({
613
654
  adoptionStance,
614
655
  emptyStewards,
615
656
  stewardNudge,
657
+ nativeFailClosed,
658
+ nativeFailClosedPolicy,
616
659
  } = {}) {
617
660
  const coverageHonesty = buildCoverageHonesty({
618
661
  percent: governedPercent,
@@ -632,6 +675,8 @@ export function computeDoctorEnforcementHonesty({
632
675
  packagePinAbsent: packageVersionTruth?.code === 'PACKAGE_PIN_ABSENT',
633
676
  selfHost,
634
677
  motherCli,
678
+ nativeFailClosed,
679
+ nativeFailClosedPolicy,
635
680
  });
636
681
  // Prefer explicit blocking count; fall back to activeViolations only when callers
637
682
  // already pass blocking-only totals (legacy tests). Type-only must not invent debt.
@@ -2,12 +2,16 @@
2
2
  * First-run CLI help (setup + check). Encyclopedia text stays behind --help --all.
3
3
  */
4
4
 
5
+ import { NORTH_STAR_ONE_LINE } from './product-copy.mjs';
6
+
5
7
  export function setupUsage() {
6
8
  return `arkgate (alias ark) — Write. Check. Ship.
7
9
  When the agent writes a bad import, the write doesn’t land. Same check fails the PR.
10
+ ${NORTH_STAR_ONE_LINE}
8
11
 
9
12
  arkgate start preview (no writes)
10
13
  arkgate start --apply write host + CI setup
14
+ (refuses weak coverage/shape; lock with --archetype/--preset/--force)
11
15
  arkgate-check --doctor status — one next step
12
16
 
13
17
  Stuck? Run status (--doctor). Do #1.
@@ -30,6 +34,7 @@ Every flag: arkgate --help --all
30
34
  export function setupUsageAll() {
31
35
  return `arkgate (alias ark) — Write. Check. Ship.
32
36
  When the agent writes a bad import, the write doesn’t land. Same check fails the PR.
37
+ ${NORTH_STAR_ONE_LINE}
33
38
 
34
39
  Usage:
35
40
  arkgate start [--root <project>] [--tools <host>] [--require-write-hook <host>] [--install] [--apply] [--json]
@@ -158,9 +163,13 @@ export async function withSensorsPartialModeHonesty(args, run) {
158
163
 
159
164
  export function checkUsage() {
160
165
  return [
161
- 'arkgate-check (alias ark-check) — the architecture check.',
166
+ 'arkgate-check (alias ark-check) — Write. Check. Ship.',
167
+ 'When the agent writes a bad import, the write doesn’t land. Same check fails the PR.',
168
+ NORTH_STAR_ONE_LINE,
162
169
  '',
163
170
  ' arkgate-check --doctor where you are: one status light, one next action',
171
+ ' arkgate-check --local --base <ref> optional local / multi-worktree cheap check',
172
+ ' arkgate-check --changed --base <ref> local / pre-push: checks touched files only',
164
173
  ' arkgate-check --strict-merge CI / merge gate (required GitHub status)',
165
174
  ' arkgate-check --sensors which sensors can ever be enforced (does not run analysis)',
166
175
  '',
@@ -170,11 +179,13 @@ export function checkUsage() {
170
179
 
171
180
  export function checkUsageAll() {
172
181
  return [
173
- 'arkgate-check (alias ark-check) — the architecture check.',
182
+ 'arkgate-check (alias ark-check) — Write. Check. Ship.',
183
+ 'When the agent writes a bad import, the write doesn’t land. Same check fails the PR.',
184
+ NORTH_STAR_ONE_LINE,
174
185
  '',
175
186
  'Usage: arkgate-check | ark-check (identical bins; product name ArkGate)',
176
187
  ' arkgate-check --version',
177
- ' arkgate-check --root <project> --config <ark.config.json> [--manifest <ark.manifest.json>] [--tsconfig <tsconfig.json>] [--strict-merge | --strict | --strict-config] [--policy-base <file> | --policy-base-ref <git-ref>] [--policy-ack <file>] [--fail-on-new-smells --base-ref <git-ref>] [--contract-diff] [--contract-session] [--changed] [--against <git-ref>] [--base <git-ref>] [--persona touch|contributor|agent|steward] [--author <id>] [--require-gates] [--require-write-hook <host>] [--json] [--baseline [file]] [--report [file.html]] [--no-cache]',
188
+ ' arkgate-check --root <project> --config <ark.config.json> [--manifest <ark.manifest.json>] [--tsconfig <tsconfig.json>] [--strict-merge | --strict | --strict-config] [--policy-base <file> | --policy-base-ref <git-ref>] [--policy-ack <file>] [--fail-on-new-smells --base-ref <git-ref>] [--contract-diff] [--contract-session] [--local] [--changed] [--against <git-ref>] [--base <git-ref>] [--persona touch|contributor|agent|steward] [--author <id>] [--require-gates] [--require-write-hook <host>] [--json] [--baseline [file]] [--report [file.html]] [--no-cache]',
178
189
  ' ark-check --doctor [--json] [--all] [--resident] [--fail-on-new-smells --base-ref <git-ref>] compact first screen; --all prints Details; resident JSON falls back cold',
179
190
  ' ark-check --coverage [--json] per-layer file counts + full unclassified list (report only, exit 0)',
180
191
  ' ark-check --plan [--json] classified remediation plan (mechanical-safe / judgment / deferred) + goal; report only',
@@ -216,6 +227,9 @@ export function checkUsageAll() {
216
227
  '',
217
228
  'Team parliament: law files (ark.config / arkrules / .ark-baseline.json) cannot ship in',
218
229
  'the same diff as product source. --changed --base <ref> checks touched files only.',
230
+ '--local (or ARK_CHECK_LOCAL=1) is the same cheap path: it turns on --changed and',
231
+ 'stays per worktree root. It cannot combine with --strict-merge. Write hooks stay',
232
+ 'on the lexical snippet path; they do not run a full-tree check.',
219
233
  '--against <ref> ratchets new keys vs that ref\'s baseline. --contract-session is a',
220
234
  'steward law-only PR. Loosen / baseline-grow need stewards[] + --author when set.',
221
235
  '',
@@ -270,6 +284,8 @@ export function checkUsageAll() {
270
284
  '--require-gates implies --strict-config and fails when the Ark contract in AGENTS.md,',
271
285
  'the project-rooted Ark server in .mcp.json, or fail-closed CI is missing/invalid.',
272
286
  'Included but unclassified source files therefore stay red instead of false-green.',
287
+ 'If gates are required and there is no short decision note yet, it points at docs/adr/',
288
+ '(or docs/decisions/) — a hint, not a fail. Off without --require-gates.',
273
289
  '',
274
290
  '--install-agent-gates writes AGENTS.md, .mcp.json, and the CI workflow for every',
275
291
  'project, plus tool-specific templates. Known tools: claude, cursor, codex, grok, antigravity',
@@ -257,6 +257,42 @@ export function hasArkMcpRegistration(root, relativePath = '.mcp.json') {
257
257
  }
258
258
  }
259
259
 
260
+ /**
261
+ * Shared MCP JSON destinations written by install-agent-gates (catalog + host copies).
262
+ * Upsert `mcpServers.ark` without clobbering sibling servers — same contract as
263
+ * mergeOpencodeArkMcp / mergeCursorArkHook.
264
+ */
265
+ export const MCP_JSON_GATE_FILES = ['.mcp.json', '.cursor/mcp.json', '.agents/mcp_config.json'];
266
+
267
+ /**
268
+ * Upsert mcpServers.ark into an existing MCP JSON document.
269
+ * Preserves sibling servers and unknown top-level keys. Returns null if unreadable.
270
+ */
271
+ export function mergeArkMcpJson(existingText, generatedText) {
272
+ let existing;
273
+ let generated;
274
+ try {
275
+ existing = existingText && existingText.trim() ? JSON.parse(existingText) : {};
276
+ generated = JSON.parse(generatedText);
277
+ } catch {
278
+ return null;
279
+ }
280
+ if (!existing || typeof existing !== 'object' || Array.isArray(existing)) return null;
281
+ if (!generated || typeof generated !== 'object' || Array.isArray(generated)) return null;
282
+ const generatedArk = generated.mcpServers?.ark;
283
+ if (!generatedArk || typeof generatedArk !== 'object' || Array.isArray(generatedArk)) {
284
+ return null;
285
+ }
286
+ const servers =
287
+ existing.mcpServers &&
288
+ typeof existing.mcpServers === 'object' &&
289
+ !Array.isArray(existing.mcpServers)
290
+ ? { ...existing.mcpServers }
291
+ : {};
292
+ const next = { ...existing, mcpServers: { ...servers, ark: generatedArk } };
293
+ return `${JSON.stringify(next, null, 2)}\n`;
294
+ }
295
+
260
296
  function commandArkMcpArgs(command) {
261
297
  if (typeof command !== 'string') return null;
262
298
  const words = [];
@@ -747,12 +783,13 @@ export function writeTemplate(root, relativePath, content, force) {
747
783
  return { relativePath, status: 'skipped-self-hosted' };
748
784
  }
749
785
  if (existing && !isArkAgentsContent(existing)) {
750
- // Never clobber a project-owned AGENTS.md — even with --force.
751
- // If Ark section not present yet, merge once; subsequent runs leave it alone.
786
+ // Never replace a project-owned AGENTS.md — even with --force.
787
+ // Merge the Ark section once when missing so start / install-agent-gates
788
+ // leave a contract doctor accepts (issue #210). Subsequent runs leave it.
752
789
  const hasArkSection =
753
790
  /#\s*Ark(Gate)?\s+Enforcement\b/.test(existing) ||
754
791
  /ark\.config\.json is authoritative/i.test(existing);
755
- if (force && isArkAgentsContent(content) && !hasArkSection) {
792
+ if (isArkAgentsContent(content) && !hasArkSection) {
756
793
  try {
757
794
  const merged = `${existing.replace(/\s*$/, '')}\n\n---\n\n${content}`;
758
795
  ensureDirForFile(fullPath);
@@ -35,6 +35,11 @@ function hostProfile(label, hookPath, hookSurface, hookOperations, hardWrite, re
35
35
  'repair-envelope-emitted': repairEnvelopeEmitted,
36
36
  'repair-reinjection-guaranteed': repairReinjectionGuaranteed,
37
37
  }),
38
+ // Host-native "deny when the checker cannot run" flag (FS-permission analog).
39
+ // required = schema field must be true for hard-write evidence (Cursor failClosed).
40
+ // unsupported = host has no documented flag; hook-crash behavior is host-owned.
41
+ // none = advisory host; no local write block.
42
+ nativeFailClosed: extras.nativeFailClosed ?? (hardWrite ? 'unsupported' : 'none'),
38
43
  // EH07 minimum ops matrix (hard=false for soft hosts on every listed op).
39
44
  operationCoverage: Object.freeze(
40
45
  extras.operationCoverage ||
@@ -83,6 +88,7 @@ export const HOST_SUPPORT_MATRIX = Object.freeze({
83
88
  false,
84
89
  {
85
90
  repairEnvelopeEmitted: true,
91
+ nativeFailClosed: 'required',
86
92
  operationCoverage: {
87
93
  Write: true,
88
94
  StrReplace: true,
@@ -186,7 +192,9 @@ ${rows}
186
192
  **Read the CI column:** for every host, the repository-wide hard guarantee is a **required**
187
193
  GitHub **status context** that runs the CLI — not “CI file present,” and not the CLI binary name alone.
188
194
  Codex hard write covers only a complete local \`apply_patch\`; Cursor covers only listed
189
- \`preToolUse\` ops. In both cases the project hook must be installed + trusted, while shell/direct
195
+ \`preToolUse\` ops with \`failClosed: true\`. A Cursor hook without that flag is fail-open
196
+ (if the checker cannot run, the write still lands). In both cases the project hook must be
197
+ installed + trusted, while shell/direct
190
198
  filesystem writes, hosted or specialized opt-out paths, and human edits still rely on CI.
191
199
 
192
200
  This table describes the supported profile **after its files are installed and the host loads/trusts them**. A hard local boundary covers only the listed hook operations; alternate tools, direct filesystem writes, and human edits still rely on CI. MCP validation is advisory because the agent must call it. The CI check blocks a merge only when the repository makes that status required. Repair **envelopes** may be emitted without reinjection being guaranteed; silent auto-apply never happens. Run \`arkgate-check --doctor\` (or \`ark-check --doctor\`) for the evidence actually detected in the current repository.`;