arkgate 4.8.14 → 4.8.16

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 (103) hide show
  1. package/CHANGELOG.md +172 -0
  2. package/README.md +17 -7
  3. package/bin/ark-check-runtime.mjs +17 -49
  4. package/bin/ark-mcp-runtime.mjs +111 -2
  5. package/bin/ark-shared.mjs +142 -11
  6. package/bin/ark.mjs +24 -33
  7. package/bin/lib/adr-presence.mjs +97 -0
  8. package/bin/lib/agent-skills-package.mjs +179 -1
  9. package/bin/lib/analysis-completeness.mjs +38 -2
  10. package/bin/lib/analysis-engine.mjs +6 -6
  11. package/bin/lib/architecture-scan.mjs +43 -4
  12. package/bin/lib/ark-run-doctor.mjs +6 -0
  13. package/bin/lib/ark-run-report.mjs +6 -2
  14. package/bin/lib/arkrules-sensors.mjs +81 -0
  15. package/bin/lib/baseline-key.mjs +6 -1
  16. package/bin/lib/check-args.mjs +52 -2
  17. package/bin/lib/config-contract.mjs +65 -2
  18. package/bin/lib/diagnostic-catalog.mjs +5 -1
  19. package/bin/lib/doctor-advisories.mjs +120 -10
  20. package/bin/lib/doctor-green-cite.mjs +139 -0
  21. package/bin/lib/doctor-human.mjs +115 -67
  22. package/bin/lib/doctor-next-actions.mjs +23 -0
  23. package/bin/lib/doctor-plan.mjs +35 -2
  24. package/bin/lib/enforcement-honesty.mjs +47 -2
  25. package/bin/lib/field-install.mjs +47 -7
  26. package/bin/lib/first-run-help.mjs +9 -1
  27. package/bin/lib/host-support-matrix.mjs +9 -1
  28. package/bin/lib/html-report-depth.mjs +2 -0
  29. package/bin/lib/html-report.mjs +13 -2
  30. package/bin/lib/import-resolve.mjs +74 -13
  31. package/bin/lib/improvement-compass-doctor.mjs +8 -1
  32. package/bin/lib/improvement-compass-map.mjs +5 -1
  33. package/bin/lib/invariant-coverage.mjs +121 -0
  34. package/bin/lib/invariant-tests-path.mjs +212 -0
  35. package/bin/lib/layer-description.mjs +90 -8
  36. package/bin/lib/mcp-hook-payload.mjs +56 -0
  37. package/bin/lib/no-domain-frontend.mjs +91 -0
  38. package/bin/lib/package-manager.mjs +8 -0
  39. package/bin/lib/presets.mjs +3 -4
  40. package/bin/lib/product-copy.mjs +15 -0
  41. package/bin/lib/projected-governed-coverage.mjs +114 -0
  42. package/bin/lib/prototype-shortcuts.mjs +224 -0
  43. package/bin/lib/recommend-cli.mjs +54 -0
  44. package/bin/lib/remediation.mjs +15 -0
  45. package/bin/lib/resolved-candidate-facts.mjs +83 -66
  46. package/bin/lib/rules-under-contract.mjs +117 -2
  47. package/bin/lib/snippet-analysis.mjs +14 -8
  48. package/bin/lib/start-preview.mjs +24 -2
  49. package/bin/lib/states-transitions-presence.mjs +212 -0
  50. package/bin/lib/status-command.mjs +2 -0
  51. package/bin/lib/status-transition-catalog.mjs +410 -0
  52. package/bin/lib/team-parliament-io.mjs +10 -0
  53. package/bin/lib/violations.mjs +13 -1
  54. package/bin/lib/write-path-capabilities.mjs +20 -5
  55. package/bin/lib/write-path-detect.mjs +27 -2
  56. package/dist/{configTypes-j7so8B4O.d.ts → configTypes-VD0qcubY.d.ts} +21 -1
  57. package/dist/{diagnosticCatalog-DVx_2RmF.d.ts → diagnosticCatalog-KWvGLI1U.d.ts} +24 -3
  58. package/dist/eslint/index.cjs +4 -4
  59. package/dist/eslint/index.d.ts +1 -1
  60. package/dist/eslint/index.js +4 -4
  61. package/dist/index.cjs +34 -34
  62. package/dist/index.d.ts +71 -10
  63. package/dist/index.js +34 -34
  64. package/dist/nestjs/index.cjs +5 -5
  65. package/dist/nestjs/index.d.ts +3 -3
  66. package/dist/nestjs/index.js +5 -5
  67. package/dist/runtime/index.cjs +15 -15
  68. package/dist/runtime/index.d.ts +6 -6
  69. package/dist/runtime/index.js +15 -15
  70. package/dist/{types-tGhZUiGX.d.ts → types-BSzRy2X1.d.ts} +1 -1
  71. package/dist/{types-Djbs3KjE.d.ts → types-D5GT5ZT8.d.ts} +3 -1
  72. package/docs/README.md +1 -1
  73. package/docs/agent-guide.md +8 -4
  74. package/docs/ai-gates.md +10 -3
  75. package/docs/brownfield-adoption.md +4 -1
  76. package/docs/configuration.md +66 -10
  77. package/docs/develop.md +4 -1
  78. package/docs/diagnostics.md +48 -4
  79. package/docs/enthusiast/how-to-agent-gates.md +5 -0
  80. package/docs/enthusiast/how-to-pick-shape.md +1 -1
  81. package/docs/package-surface.md +7 -5
  82. package/docs/use.md +10 -3
  83. package/package.json +1 -1
  84. package/schemas/ark.config.schema.json +24 -3
  85. package/server.json +2 -2
  86. package/templates/agent-skills/README.md +1 -1
  87. package/templates/agent-skills/ark-adopt/SKILL.md +97 -1
  88. package/templates/agent-skills/ark-autopilot/SKILL.md +12 -3
  89. package/templates/agent-skills/ark-coverage/SKILL.md +2 -2
  90. package/templates/agent-skills/ark-explain/SKILL.md +6 -3
  91. package/templates/agent-skills/ark-explore/SKILL.md +33 -1
  92. package/templates/agent-skills/ark-place/SKILL.md +42 -3
  93. package/templates/agent-skills/ark-runtime/SKILL.md +4 -2
  94. package/templates/agent-skills/ark-upgrade/SKILL.md +1 -1
  95. package/templates/arkrules/DomainModel.json +14 -1
  96. package/templates/skills/ark-adopt.md +97 -1
  97. package/templates/skills/ark-autopilot.md +12 -3
  98. package/templates/skills/ark-coverage.md +2 -2
  99. package/templates/skills/ark-explain.md +6 -3
  100. package/templates/skills/ark-explore.md +33 -1
  101. package/templates/skills/ark-place.md +42 -3
  102. package/templates/skills/ark-runtime.md +4 -2
  103. package/templates/skills/ark-upgrade.md +1 -1
@@ -0,0 +1,139 @@
1
+ /**
2
+ * Golden rule: a doctor green / healthy claim must cite a file, config key,
3
+ * or test. Uncited greens are dishonest — demote or omit.
4
+ *
5
+ * Tooling only. Reuses existing doctor view fields (IT01 / ADR / CI
6
+ * residual shape). No new schema, RPC, or skill name.
7
+ */
8
+ import { REQUIRED_GATE_WORKFLOW } from './gate-files.mjs';
9
+ import { CI_MERGE_BOUNDARY_REL } from './ci-merge-boundary.mjs';
10
+
11
+ export const HEALTHY_CLAIM = 'Healthy — nothing to do.';
12
+ export const HEALTHY_KEEP = ' Keep write path + CI.';
13
+ export const UNCITE_SUFFIX =
14
+ 'not green until a file, config key, or test is named';
15
+
16
+ const CONFIG_KEY_DOT = /^[A-Za-z_][\w]*\.[A-Za-z_][\w.]*$/;
17
+ const CONFIG_KEY_BARE = new Set([
18
+ 'include',
19
+ 'exclude',
20
+ 'layers',
21
+ 'rules',
22
+ 'arkRules',
23
+ 'arkRun',
24
+ 'arkOrder',
25
+ 'coverage',
26
+ 'stewards',
27
+ 'schemaVersion',
28
+ ]);
29
+ const FILE_LIKE =
30
+ /(?:^|\/)(?:\.[A-Za-z][\w.-]*|[A-Za-z][\w.-]*\.(?:json|ya?ml|md|ts|mjs|js|cjs))$|\/|[.](?:json|ya?ml|md|ts|mjs|js|cjs)$/;
31
+ const TEST_LIKE = /(?:^|\/)tests?\/|(?:\.|\b)test\.(?:ts|mjs|js)\b/i;
32
+
33
+ /**
34
+ * True when a stranger can open this in about a minute.
35
+ * Generic words ("CI", "gates") are not cites.
36
+ */
37
+ export function isConcreteCite(value) {
38
+ if (typeof value !== 'string') return false;
39
+ const cite = value.trim();
40
+ if (cite.length < 3 || cite.length > 240) return false;
41
+ if (/\s/.test(cite)) return false;
42
+ if (cite === REQUIRED_GATE_WORKFLOW) return false;
43
+ return (
44
+ FILE_LIKE.test(cite) ||
45
+ TEST_LIKE.test(cite) ||
46
+ CONFIG_KEY_DOT.test(cite) ||
47
+ CONFIG_KEY_BARE.has(cite)
48
+ );
49
+ }
50
+
51
+ export function uniqueConcreteCites(cites) {
52
+ const out = [];
53
+ for (const raw of Array.isArray(cites) ? cites : [cites]) {
54
+ if (!isConcreteCite(raw)) continue;
55
+ const cite = String(raw).trim();
56
+ if (!out.includes(cite)) out.push(cite);
57
+ }
58
+ return out;
59
+ }
60
+
61
+ /**
62
+ * @returns {{ mark: 'ok', text: string } | { mark: 'warn', text: string }}
63
+ */
64
+ export function citeOrDemoteGreen(claim, cites) {
65
+ const named = typeof claim === 'string' && claim.trim() ? claim.trim() : 'This claim';
66
+ const list = uniqueConcreteCites(cites);
67
+ if (list.length > 0) {
68
+ return { mark: 'ok', text: `${named} (${list.join(' · ')})` };
69
+ }
70
+ return { mark: 'warn', text: `${named} — ${UNCITE_SUFFIX}` };
71
+ }
72
+
73
+ export function citedGreen(line, marks, claim, cites) {
74
+ const row = citeOrDemoteGreen(claim, cites);
75
+ line(row.mark === 'ok' ? marks.ok : marks.warn, row.text);
76
+ return row;
77
+ }
78
+
79
+ /** Backing artifacts already on the doctor view — no second scan. */
80
+ export function healthyCitesFromView(view) {
81
+ const cites = ['ark.config.json'];
82
+ if (view?.ciMergeBoundary) cites.push(CI_MERGE_BOUNDARY_REL);
83
+ const workflow =
84
+ view?.ciMergeBoundary?.ci?.workflowFile || view?.ciNotFailClosed?.workflowFile;
85
+ if (workflow) cites.push(workflow);
86
+ return uniqueConcreteCites(cites);
87
+ }
88
+
89
+ export function printHealthyHeadline(view, color) {
90
+ const row = citeOrDemoteGreen(HEALTHY_CLAIM, healthyCitesFromView(view));
91
+ if (row.mark === 'ok') {
92
+ console.log(color.green(`✔ ${row.text}`));
93
+ console.log(color.dim(HEALTHY_KEEP));
94
+ return row;
95
+ }
96
+ console.log(color.yellow(`! ${row.text}`));
97
+ return row;
98
+ }
99
+
100
+ export function displayedMissingGates(gatesMissing, view) {
101
+ const list = Array.isArray(gatesMissing) ? gatesMissing : [];
102
+ const hideGlob = Boolean(
103
+ view?.ciNotFailClosed?.workflowFile ||
104
+ view?.ciNotFailClosed?.error === 'ci-not-fail-closed' ||
105
+ view?.ciMergeBoundary?.ci?.workflowPresent
106
+ );
107
+ return hideGlob ? list.filter((item) => item !== REQUIRED_GATE_WORKFLOW) : list;
108
+ }
109
+
110
+ export function ciNotFailClosedNotice(view) {
111
+ const file = view?.ciNotFailClosed?.workflowFile;
112
+ if (!file) return null;
113
+ return `CI not fail-closed: ${file} — remove the skippable if:, or write .ark/adoption-stance.json with stance: advisory-only`;
114
+ }
115
+
116
+ export function ciMergeGreenCites(view) {
117
+ const cites = [];
118
+ if (view?.ciMergeBoundary) cites.push(CI_MERGE_BOUNDARY_REL);
119
+ const workflow = view?.ciMergeBoundary?.ci?.workflowFile || view?.ciNotFailClosed?.workflowFile;
120
+ if (workflow) cites.push(workflow);
121
+ return uniqueConcreteCites(cites);
122
+ }
123
+
124
+ export function foundGateCites(view) {
125
+ const cites = ['AGENTS.md'];
126
+ if (view?.ciMergeBoundary) cites.push(CI_MERGE_BOUNDARY_REL);
127
+ const workflow = view?.ciMergeBoundary?.ci?.workflowFile;
128
+ if (workflow) cites.push(workflow);
129
+ return uniqueConcreteCites(cites);
130
+ }
131
+
132
+ export function writePathGreenCites(writePath) {
133
+ return uniqueConcreteCites([
134
+ ...(Array.isArray(writePath?.capabilityEvidence?.['hard-write'])
135
+ ? writePath.capabilityEvidence['hard-write']
136
+ : []),
137
+ ...(Array.isArray(writePath?.evidence) ? writePath.evidence : []),
138
+ ]);
139
+ }
@@ -13,23 +13,16 @@ import { enforcementDoctorLines } from './enforcement-state.mjs';
13
13
  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
- import { REQUIRED_GATE_WORKFLOW } from './gate-files.mjs';
17
-
18
- function displayedMissingGates(gatesMissing, view) {
19
- const list = Array.isArray(gatesMissing) ? gatesMissing : [];
20
- const hideGlob = Boolean(
21
- view?.ciNotFailClosed?.workflowFile ||
22
- view?.ciNotFailClosed?.error === 'ci-not-fail-closed' ||
23
- view?.ciMergeBoundary?.ci?.workflowPresent
24
- );
25
- return hideGlob ? list.filter((item) => item !== REQUIRED_GATE_WORKFLOW) : list;
26
- }
27
-
28
- function ciNotFailClosedNotice(view) {
29
- const file = view?.ciNotFailClosed?.workflowFile;
30
- if (!file) return null;
31
- return `CI not fail-closed: ${file} — remove the skippable if:, or write .ark/adoption-stance.json with stance: advisory-only`;
32
- }
16
+ import { layerGuidanceLine } from './layer-description.mjs';
17
+ import {
18
+ citedGreen,
19
+ ciMergeGreenCites,
20
+ ciNotFailClosedNotice,
21
+ displayedMissingGates,
22
+ foundGateCites,
23
+ printHealthyHeadline,
24
+ writePathGreenCites,
25
+ } from './doctor-green-cite.mjs';
33
26
 
34
27
  function lineWith(ok, warn, bad, color) {
35
28
  return (mark, text) => console.log(` ${mark} ${text}`);
@@ -96,14 +89,13 @@ export function printDoctorCompactHuman(view) {
96
89
  : 'import rules check out. Keep host + CI.',
97
90
  };
98
91
  const modeTitle = operatingModeTitle(mode, designFitness.designWeak, stewardUnfinished);
99
- line(
100
- modeMark,
101
- `${modeTitle} ${
102
- designFitness.designWeak
103
- ? 'import rules check out; leftover design work remains.'
104
- : modeHelp[mode]
105
- }`
106
- );
92
+ const modeClaim = `${modeTitle} — ${
93
+ designFitness.designWeak
94
+ ? 'import rules check out; leftover design work remains.'
95
+ : modeHelp[mode]
96
+ }`;
97
+ if (modeMark === ok) citedGreen(line, { ok, warn }, modeClaim, ['ark.config.json']);
98
+ else line(modeMark, modeClaim);
107
99
  if (emptyScope) {
108
100
  line(
109
101
  bad,
@@ -113,17 +105,18 @@ export function printDoctorCompactHuman(view) {
113
105
 
114
106
  console.log('');
115
107
  if (ciMergeBoundary?.ci?.state) {
116
- line(
117
- ciMergeBoundary.ci.state === 'required' ? ok : warn,
118
- `CI merge: ${ciMergeBoundary.ci.state}`
119
- );
108
+ const mergeClaim = `CI merge: ${ciMergeBoundary.ci.state}`;
109
+ if (ciMergeBoundary.ci.state === 'required') {
110
+ citedGreen(line, { ok, warn }, mergeClaim, ciMergeGreenCites(view));
111
+ } else {
112
+ line(warn, mergeClaim);
113
+ }
120
114
  }
121
115
  if (adopted === 'advisory-only-acked') {
122
116
  line(warn, 'Adoption: advisory-only ack — not a required GitHub status.');
123
117
  }
124
118
  if (isDoctorHealthyNothingToDo(designFitness, uniqueActions, adopted)) {
125
- console.log(color.green('✔ Healthy — nothing to do.'));
126
- console.log(color.dim(' Keep write path + CI.'));
119
+ printHealthyHeadline(view, color);
127
120
  } else {
128
121
  console.log(color.bold('Primary next action'));
129
122
  console.log(` 1. ${uniqueActions[0]}`);
@@ -131,21 +124,20 @@ export function printDoctorCompactHuman(view) {
131
124
 
132
125
  console.log('');
133
126
  console.log(color.bold('Coverage'));
134
- const govMark =
135
- emptyScope || cov.governed.percent < 50
136
- ? bad
137
- : cov.governed.percent >= 80
138
- ? ok
139
- : warn;
140
- line(govMark, `Governed: ${cov.governed.percent}% (${cov.governed.classifiedFiles}/${cov.governed.totalFiles} files)`);
127
+ const govClaim = `Governed: ${cov.governed.percent}% (${cov.governed.classifiedFiles}/${cov.governed.totalFiles} files)`;
128
+ if (emptyScope || cov.governed.percent < 50) line(bad, govClaim);
129
+ else if (cov.governed.percent >= 80) citedGreen(line, { ok, warn }, govClaim, ['ark.config.json', 'include', 'layers']);
130
+ else line(warn, govClaim);
141
131
  for (const row of cov.layers ?? []) {
142
- if (row.description) line(' ', `${row.name} — ${row.description}`);
132
+ const guidance = layerGuidanceLine(row);
133
+ if (guidance) line(' ', `${row.name} — ${guidance}`);
143
134
  }
144
135
 
145
136
  const hostRed =
146
137
  listedMissing.length > 0 ||
147
138
  Boolean(writePath.gap) ||
148
139
  writePathHonesty?.softWriteHost === true ||
140
+ writePathHonesty?.nativeFailClosed === false ||
149
141
  Boolean(skippableCi);
150
142
  if (hostRed) {
151
143
  console.log('');
@@ -153,7 +145,9 @@ export function printDoctorCompactHuman(view) {
153
145
  if (writePath.activeHost) line(' ', `Active host: ${writePath.activeHost}`);
154
146
  if (listedMissing.length > 0) line(bad, `Missing gates: ${listedMissing.join(', ')}`);
155
147
  if (skippableCi) line(warn, skippableCi);
156
- else if (listedMissing.length === 0 && (writePath.gap || writePathHonesty?.softWriteHost)) {
148
+ else if (writePathHonesty?.nativeFailClosed === false) {
149
+ line(warn, writePathHonesty.message || 'Write hook is fail-open — if the checker cannot run, the write still lands.');
150
+ } else if (listedMissing.length === 0 && (writePath.gap || writePathHonesty?.softWriteHost)) {
157
151
  line(warn, 'Local writes are advisory; required CI is the merge boundary.');
158
152
  }
159
153
  }
@@ -165,7 +159,15 @@ export function printDoctorCompactHuman(view) {
165
159
  line(warn, nudge.ask);
166
160
  }
167
161
 
168
- printCompactExtraDoctorLines(doctorAdvisories, { line, warn });
162
+ printCompactExtraDoctorLines(
163
+ {
164
+ ...doctorAdvisories,
165
+ layerOwners: view.layerOwners,
166
+ adrPresence: view.adrPresence,
167
+ statesTransitions: view.statesTransitions, statusTransitionCatalog: view.statusTransitionCatalog, noDomainFrontend: view.noDomainFrontend, invariantTestsPath: view.invariantTestsPath, invariantCoverageRoots: view.invariantCoverageRoots,
168
+ },
169
+ { line, warn }
170
+ );
169
171
 
170
172
  if (violations.length === 0) {
171
173
  if (!analysisComplete) {
@@ -248,12 +250,17 @@ export function printDoctorDetailsHuman(view) {
248
250
  `Dual-match: ${cov.dualMembership.count} file(s) match multiple layers — ${cov.dualMembership.note ?? 'review overlapping globs'}`
249
251
  );
250
252
  }
251
- if (cov.suggestions.length === 0 && cov.emptyLayers.length === 0) line(ok, 'Every layer classifies files; no empty layers');
252
- const captioned = (cov.layers ?? []).filter((row) => row.description);
253
+ if (cov.suggestions.length === 0 && cov.emptyLayers.length === 0) {
254
+ citedGreen(line, { ok, warn }, 'Every layer classifies files; no empty layers', [
255
+ 'ark.config.json',
256
+ 'layers',
257
+ ]);
258
+ }
259
+ const captioned = (cov.layers ?? []).filter((row) => layerGuidanceLine(row));
253
260
  if (captioned.length > 0) {
254
261
  console.log('');
255
262
  console.log(color.bold('Layers'));
256
- for (const row of captioned) line(' ', `${row.name} — ${row.description}`);
263
+ for (const row of captioned) line(' ', `${row.name} — ${layerGuidanceLine(row)}`);
257
264
  }
258
265
 
259
266
  if (packageVersionTruth?.dualTruth) {
@@ -275,7 +282,8 @@ export function printDoctorDetailsHuman(view) {
275
282
  console.log('');
276
283
  console.log(color.bold('Design fitness'));
277
284
  if (designSmells.length === 0) {
278
- line(analysisComplete ? ok : warn, designFitness.label);
285
+ if (analysisComplete) citedGreen(line, { ok, warn }, designFitness.label, ['ark.config.json']);
286
+ else line(warn, designFitness.label);
279
287
  } else {
280
288
  line(designFitness.designWeak ? warn : warn, designFitness.label);
281
289
  for (const smell of designSmells.slice(0, 5)) {
@@ -342,15 +350,16 @@ export function printDoctorDetailsHuman(view) {
342
350
  } else if (designFitness.designWeak) {
343
351
  line(warn, `None on checked imports — import rules match the config; leftover design work remains (${modeTitle}). Not healthy finished.`);
344
352
  } else {
345
- line(ok, 'None — the code matches the contract on checked edges');
353
+ citedGreen(line, { ok, warn }, 'None — the code matches the contract on checked edges', [
354
+ 'ark.config.json',
355
+ ]);
346
356
  }
347
357
  } else {
348
358
  const typeNote = summary.typeOnlyCount > 0 ? ` (${summary.valueCount} value · ${summary.typeOnlyCount} type-only)` : '';
349
359
  const supNote = suppressed > 0 ? `, ${suppressed} frozen` : '';
350
- line(
351
- activeCount > 0 ? warn : ok,
352
- `${violations.length} total${typeNote}${supNote}${activeCount > 0 ? ` — ${activeCount} NOT baselined` : ''}`
353
- );
360
+ const violClaim = `${violations.length} total${typeNote}${supNote}${activeCount > 0 ? ` — ${activeCount} NOT baselined` : ''}`;
361
+ if (activeCount > 0) line(warn, violClaim);
362
+ else citedGreen(line, { ok, warn }, violClaim, ['.ark-baseline.json']);
354
363
  for (const edge of summary.edges.slice(0, 3)) line(' ', color.dim(`${edge.count} ${edge.edge}`));
355
364
  if (summary.concentrated && typeof summary.dominant === 'string' && summary.dominant.includes(' → ')) {
356
365
  line(warn, color.dim(`${Math.round(summary.dominantShare * 100)}% on one edge (${summary.dominant}) — likely a contract fix, not debt`));
@@ -374,7 +383,17 @@ export function printDoctorDetailsHuman(view) {
374
383
  : bad;
375
384
  line(' ', `Active host: ${writePath.activeHost}`);
376
385
  line(' ', `Supported profile: ${writePath.supportSummary}`);
377
- line(wpMark, `Mode: ${writePath.mode} — ${writePathLabels[writePath.mode] || writePath.mode}`);
386
+ const modeLine = `Mode: ${writePath.mode} — ${writePathLabels[writePath.mode] || writePath.mode}`;
387
+ if (wpMark === ok) {
388
+ citedGreen(
389
+ line,
390
+ { ok, warn },
391
+ modeLine,
392
+ writePathGreenCites(writePath)
393
+ );
394
+ } else {
395
+ line(wpMark, modeLine);
396
+ }
378
397
  if (writePathHonesty.message) line(warn, writePathHonesty.message);
379
398
  if (writePath.sessionNote) {
380
399
  line(warn, writePath.sessionNote);
@@ -384,14 +403,21 @@ export function printDoctorDetailsHuman(view) {
384
403
  const supportCaps = writePath.support?.capabilities || {};
385
404
  const repairReinjection = supportCaps['repair-reinjection-guaranteed'] === true;
386
405
  const repairEnvelope = supportCaps['repair-envelope-emitted'] === true || supportCaps['repair-payload'] === true;
387
- line(
388
- repairReinjection ? ok : warn,
389
- repairReinjection
390
- ? 'Repair: envelope + reinjection guaranteed on hard path when installed + trusted'
391
- : repairEnvelope
406
+ if (repairReinjection) {
407
+ citedGreen(
408
+ line,
409
+ { ok, warn },
410
+ 'Repair: envelope + reinjection guaranteed on hard path when installed + trusted',
411
+ writePathGreenCites(writePath)
412
+ );
413
+ } else {
414
+ line(
415
+ warn,
416
+ repairEnvelope
392
417
  ? 'Repair: envelope may emit (`--hook-repair`); reinjection not guaranteed (advisory host)'
393
418
  : 'Repair: no hard-boundary payload'
394
- );
419
+ );
420
+ }
395
421
  if (writePath.gap) {
396
422
  line(writePath.gap.severity === 'warn' ? warn : warn, writePath.gap.message);
397
423
  if (writePath.gap.fix) {
@@ -402,7 +428,12 @@ export function printDoctorDetailsHuman(view) {
402
428
  console.log('');
403
429
  console.log(color.bold('Gates & skills'));
404
430
  if (listedMissing.length === 0 && !skippableCi) {
405
- line(ok, 'Shared gate artifacts found on disk (AGENTS.md, .mcp.json, CI); runtime activation is reported separately');
431
+ citedGreen(
432
+ line,
433
+ { ok, warn },
434
+ 'Shared gate artifacts found on disk; runtime activation is reported separately',
435
+ foundGateCites(view)
436
+ );
406
437
  } else {
407
438
  if (listedMissing.length > 0) line(bad, `Missing gates: ${listedMissing.join(', ')}`);
408
439
  if (skippableCi) line(warn, skippableCi);
@@ -417,7 +448,11 @@ export function printDoctorDetailsHuman(view) {
417
448
  );
418
449
  const remMiss = remainingGaps.reduce((s, g) => s + g.missing, 0);
419
450
  const remStale = remainingGaps.reduce((s, g) => s + g.stale, 0);
420
- if (remMiss + remStale === 0 && !legacyCodex) line(ok, '/ark-* skills current for detected tools');
451
+ if (remMiss + remStale === 0 && !legacyCodex) {
452
+ citedGreen(line, { ok, warn }, '/ark-* skills current for detected tools', [
453
+ '.agents/skills',
454
+ ]);
455
+ }
421
456
  if (legacyCodex) {
422
457
  line(warn, 'Codex: legacy flat .codex/prompts only (not a loadable skill catalog)');
423
458
  }
@@ -467,10 +502,17 @@ export function printDoctorDetailsHuman(view) {
467
502
  console.log('');
468
503
  console.log(color.bold('Baseline'));
469
504
  if (!baseline.exists) {
470
- line(!analysisComplete || violations.length > 0 ? warn : ok, !analysisComplete ? 'No baseline — current violations were not fully evaluated' : violations.length > 0 ? 'No baseline — adopting a dirty repo? freeze with --update-baseline --force --contract-session --author <steward>' : 'No baseline (nothing to freeze)');
505
+ const none = !analysisComplete
506
+ ? 'No baseline — current violations were not fully evaluated'
507
+ : violations.length > 0
508
+ ? 'No baseline — adopting a dirty repo? freeze with --update-baseline --force --contract-session --author <steward>'
509
+ : 'No baseline (nothing to freeze)';
510
+ if (!analysisComplete || violations.length > 0) line(warn, none);
511
+ else line(' ', none);
471
512
  } else {
472
- const baseMark = !analysisComplete || baselineHonesty.dirtyBaselineRisk ? warn : ok;
473
- line(baseMark, `${baseline.keys.size} frozen key(s)${analysisComplete ? '' : ' — stale comparison not verified'}`);
513
+ const baseClaim = `${baseline.keys.size} frozen key(s)${analysisComplete ? '' : ' — stale comparison not verified'}`;
514
+ if (!analysisComplete || baselineHonesty.dirtyBaselineRisk) line(warn, baseClaim);
515
+ else citedGreen(line, { ok, warn }, baseClaim, ['.ark-baseline.json']);
474
516
  if (analysisComplete && baselineHonesty.dirtyBaselineRisk) {
475
517
  line(warn, baselineHonesty.message);
476
518
  }
@@ -481,7 +523,9 @@ export function printDoctorDetailsHuman(view) {
481
523
 
482
524
  console.log('');
483
525
  console.log(color.bold('Command runners'));
484
- if (staleRunners.length === 0) line(ok, 'Emitted commands match the package manager');
526
+ if (staleRunners.length === 0) {
527
+ citedGreen(line, { ok, warn }, 'Emitted commands match the package manager', ['package.json']);
528
+ }
485
529
  else {
486
530
  line(warn, `Stale runner in ${staleRunners.join(', ')}`);
487
531
  }
@@ -489,9 +533,11 @@ export function printDoctorDetailsHuman(view) {
489
533
  console.log('');
490
534
  console.log(color.bold('Adoption (separate from fitness score)'));
491
535
  if (adoption.gaps.length === 0 && !adoption.layerBalance) {
492
- line(
493
- ok,
494
- 'Hosts, MCP argv, core optionality, origin report, baseline policy, and deploy-path lint/types look complete'
536
+ citedGreen(
537
+ line,
538
+ { ok, warn },
539
+ 'Hosts, MCP argv, core optionality, origin report, baseline policy, and deploy-path lint/types look complete',
540
+ ['AGENTS.md', 'ark.config.json']
495
541
  );
496
542
  } else {
497
543
  for (const gap of adoption.gaps) {
@@ -540,7 +586,9 @@ export function printDoctorDetailsHuman(view) {
540
586
  ['Rules with peerIsolation: false', safety.disabledPeerIsolationRules],
541
587
  ];
542
588
  for (const [label, entries] of rows) {
543
- line(entries.length === 0 ? ok : warn, `${label}: ${entries.length}`);
589
+ const claim = `${label}: ${entries.length}`;
590
+ if (entries.length === 0) line(' ', claim);
591
+ else line(warn, claim);
544
592
  }
545
593
  }
546
594
  }
@@ -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,26 @@ 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
+ }
59
+ if (ctx.prototypeShortcuts?.nextAction) {
60
+ actions.push(ctx.prototypeShortcuts.nextAction);
61
+ }
62
+ if (ctx.invariantTestsPath?.missing && ctx.invariantTestsPath.nextAction) {
63
+ actions.push(ctx.invariantTestsPath.nextAction);
64
+ }
65
+ if (ctx.invariantCoverageRoots?.missing && ctx.invariantCoverageRoots.nextAction) {
66
+ actions.push(ctx.invariantCoverageRoots.nextAction);
67
+ }
45
68
  if (!ctx.analysisComplete) actions.push('restore complete analysis, then rerun ark-check --doctor');
46
69
  if (ctx.designSmells.length > 0 && ctx.postGreenPath) actions.push(ctx.postGreenPath.action);
47
70
  if (ctx.coverageHonesty.greenIsNotEnforcement && ctx.coverageHonesty.worseThanNoGate) {
@@ -15,7 +15,13 @@ 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
+ import { collectInvariantCoverageResiduals } from './invariant-tests-path.mjs';
24
+ export { printAdrPresenceHint };
19
25
  export { printDoctorCompactHuman, printDoctorDetailsHuman };
20
26
  export { summarizeRulesUnderContract };
21
27
 
@@ -68,6 +74,7 @@ import { writeCiMergeBoundary } from './ci-merge-boundary.mjs';
68
74
  import {
69
75
  classifyAdopted,
70
76
  githubEvidenceForCiMergeBoundary,
77
+ isAdopted,
71
78
  readAdoptionStance,
72
79
  NOT_ADOPTED_NEXT_ACTION,
73
80
  } from './adoption-stance.mjs';
@@ -173,7 +180,8 @@ export function runCoverage(root, config, files, rules, asJson) {
173
180
  console.log(` ${pad('Layer')} Files`);
174
181
  for (const row of layerRows) {
175
182
  const flag = row.files === 0 ? ' (pattern matches nothing)' : '';
176
- const caption = row.description ? ` ${row.description}` : '';
183
+ const guidance = layerGuidanceLine(row);
184
+ const caption = guidance ? ` ${guidance}` : '';
177
185
  console.log(` ${pad(row.name)} ${String(row.files).padStart(5)}${flag}${caption}`);
178
186
  }
179
187
  console.log(` ${pad('(unclassified)')} ${String(unclassified.length).padStart(5)}`);
@@ -616,6 +624,7 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
616
624
  options.facts ?? options.architectureFacts,
617
625
  activeViolations
618
626
  );
627
+ const layerOwners = collectLayerOwnerResidual(config);
619
628
  const rulesUnderContract = doctorAdvisories.rulesUnderContract;
620
629
  const arkRun = doctorAdvisories.arkRun;
621
630
  // Single residual expression (nextPilot || extractionCard) — HTML report uses the same.
@@ -636,6 +645,14 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
636
645
  github: githubForBoundary,
637
646
  ci: ciMergeBoundary?.ci,
638
647
  });
648
+ const adrPresence = collectAdrPresenceResidual({
649
+ root,
650
+ demanded: options.requireGates === true || adopted === 'required-merge',
651
+ });
652
+ const statesTransitions = collectStatesTransitionsResidual({ root });
653
+ const statusTransitionCatalog = collectStatusTransitionCatalogResidual({ root, config, files, statesTransitions });
654
+ const noDomainFrontend = collectNoDomainFrontendResidual({ config, coverage: cov, designSmells });
655
+ const { invariantCoverageRoots, invariantTestsPath } = collectInvariantCoverageResiduals({ adopted: isAdopted(adopted) || options.requireGates === true, coverage: config?.coverage, config, root });
639
656
  const { coverageHonesty, baselineHonesty, writePathHonesty, productHonesty } =
640
657
  computeDoctorEnforcementHonesty({
641
658
  governedPercent: cov.governed.percent,
@@ -671,6 +688,8 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
671
688
  selfHost:
672
689
  packageVersionTruth?.selfHost === true ||
673
690
  packageVersionTruth?.code === 'PACKAGE_PIN_SELF_HOST',
691
+ nativeFailClosed: writePath.nativeFailClosed,
692
+ nativeFailClosedPolicy: writePath.nativeFailClosedPolicy,
674
693
  adopted,
675
694
  ciMergeBoundary,
676
695
  github: githubForBoundary,
@@ -763,6 +782,10 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
763
782
  governed: cov.governed,
764
783
  coverageHonesty,
765
784
  layers: cov.layers,
785
+ ...(layerOwners ? { layerOwners } : {}),
786
+ ...(adrPresence ? { adrPresence } : {}),
787
+ ...(statesTransitions ? { statesTransitions } : {}),
788
+ ...(statusTransitionCatalog ? { statusTransitionCatalog } : {}), ...(noDomainFrontend ? { noDomainFrontend } : {}), ...(invariantTestsPath ? { invariantTestsPath } : {}), ...(invariantCoverageRoots ? { invariantCoverageRoots } : {}),
766
789
  emptyLayers: cov.emptyLayers,
767
790
  layersWithoutRules: cov.layersWithoutRules,
768
791
  ungovernedDirs: cov.suggestions.length,
@@ -827,6 +850,8 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
827
850
  mcpPresent: writePath.mcpPresent,
828
851
  evidence: writePath.evidence,
829
852
  honesty: writePathHonesty,
853
+ nativeFailClosed: writePath.nativeFailClosed ?? null,
854
+ nativeFailClosedPolicy: writePath.nativeFailClosedPolicy ?? 'unsupported',
830
855
  ...(writePath.sessionNote ? { sessionNote: writePath.sessionNote } : {}),
831
856
  ...(writePath.gap
832
857
  ? {
@@ -902,12 +927,20 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
902
927
  operatingMode,
903
928
  adopted,
904
929
  stewardNudge: doctorAdvisories.stewardNudge,
930
+ layerOwners,
931
+ adrPresence,
932
+ statesTransitions,
933
+ statusTransitionCatalog, noDomainFrontend, prototypeShortcuts: doctorAdvisories.prototypeShortcuts, invariantTestsPath, invariantCoverageRoots,
905
934
  });
906
935
  const humanView = {
907
936
  root,
908
937
  analysisComplete,
909
938
  completeness,
910
939
  doctorAdvisories,
940
+ layerOwners,
941
+ adrPresence,
942
+ statesTransitions,
943
+ statusTransitionCatalog, noDomainFrontend, prototypeShortcuts: doctorAdvisories.prototypeShortcuts, invariantTestsPath, invariantCoverageRoots,
911
944
  operatingMode,
912
945
  designFitness,
913
946
  adopted,