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.
- package/CHANGELOG.md +219 -4
- package/README.md +33 -22
- package/SECURITY.md +5 -3
- package/bin/ark-check-runtime.mjs +25 -57
- package/bin/ark-mcp-runtime.mjs +111 -2
- package/bin/ark-shared.mjs +140 -11
- package/bin/ark.mjs +7 -25
- package/bin/lib/adr-presence.mjs +97 -0
- package/bin/lib/agent-projection-formatters.mjs +2 -0
- package/bin/lib/agent-skills-package.mjs +241 -8
- package/bin/lib/analysis-completeness.mjs +38 -2
- package/bin/lib/analysis-engine.mjs +6 -6
- package/bin/lib/architecture-scan.mjs +33 -5
- package/bin/lib/ark-order-doctor.mjs +7 -1
- package/bin/lib/ark-order-report.mjs +2 -1
- package/bin/lib/ark-run-doctor.mjs +6 -0
- package/bin/lib/ark-run-report.mjs +6 -2
- package/bin/lib/arkrules-sensors.mjs +81 -0
- package/bin/lib/baseline-key.mjs +4 -1
- package/bin/lib/check-args.mjs +52 -2
- package/bin/lib/ci-and-commands.mjs +7 -2
- package/bin/lib/config-contract.mjs +64 -1
- package/bin/lib/design-smells.mjs +21 -1
- package/bin/lib/diagnostic-catalog.mjs +5 -3
- package/bin/lib/doctor-advisories.mjs +109 -17
- package/bin/lib/doctor-human.mjs +20 -6
- package/bin/lib/doctor-next-actions.mjs +14 -0
- package/bin/lib/doctor-plan.mjs +32 -2
- package/bin/lib/enforcement-honesty.mjs +47 -2
- package/bin/lib/first-run-help.mjs +19 -3
- package/bin/lib/gate-files.mjs +40 -3
- package/bin/lib/host-support-matrix.mjs +9 -1
- package/bin/lib/html-report-depth.mjs +2 -0
- package/bin/lib/html-report.mjs +13 -2
- package/bin/lib/import-resolve.mjs +74 -13
- package/bin/lib/improvement-compass-doctor.mjs +6 -1
- package/bin/lib/improvement-compass-map.mjs +3 -1
- package/bin/lib/install-migrate.mjs +23 -0
- package/bin/lib/layer-description.mjs +90 -8
- package/bin/lib/mcp-hook-payload.mjs +57 -1
- package/bin/lib/no-domain-frontend.mjs +91 -0
- package/bin/lib/presets.mjs +3 -4
- package/bin/lib/product-copy.mjs +19 -0
- package/bin/lib/projected-governed-coverage.mjs +114 -0
- package/bin/lib/recommend-cli.mjs +54 -0
- package/bin/lib/remediation.mjs +6 -2
- package/bin/lib/resolved-candidate-facts.mjs +225 -100
- package/bin/lib/rules-under-contract.mjs +117 -2
- package/bin/lib/scan-files.mjs +39 -0
- package/bin/lib/snippet-analysis.mjs +14 -8
- package/bin/lib/start-preview.mjs +8 -2
- package/bin/lib/states-transitions-presence.mjs +212 -0
- package/bin/lib/status-command.mjs +2 -0
- package/bin/lib/status-transition-catalog.mjs +410 -0
- package/bin/lib/team-parliament-io.mjs +10 -0
- package/bin/lib/upgrade-whats-new.mjs +3 -3
- package/bin/lib/violations.mjs +43 -1
- package/bin/lib/write-path-capabilities.mjs +20 -5
- package/bin/lib/write-path-detect.mjs +27 -2
- package/dist/{configTypes-j7so8B4O.d.ts → configTypes-Dt3DpVbd.d.ts} +19 -0
- package/dist/{diagnosticCatalog-DA565Lja.d.ts → diagnosticCatalog-BEg85XlE.d.ts} +3 -3
- package/dist/eslint/index.cjs +4 -4
- package/dist/eslint/index.d.ts +1 -1
- package/dist/eslint/index.js +4 -4
- package/dist/index.cjs +31 -31
- package/dist/index.d.ts +126 -19
- package/dist/index.js +31 -31
- package/dist/nestjs/index.cjs +5 -5
- package/dist/nestjs/index.d.ts +3 -3
- package/dist/nestjs/index.js +5 -5
- package/dist/runtime/index.cjs +15 -15
- package/dist/runtime/index.d.ts +6 -6
- package/dist/runtime/index.js +15 -15
- package/dist/{types-Djbs3KjE.d.ts → types-CN9tVMPz.d.ts} +3 -1
- package/dist/{types-tGhZUiGX.d.ts → types-TBiv0WHL.d.ts} +1 -1
- package/docs/README.md +8 -6
- package/docs/agent-guide.md +35 -17
- package/docs/ai-gates.md +13 -4
- package/docs/arkorder.md +11 -4
- package/docs/brownfield-adoption.md +4 -1
- package/docs/configuration.md +63 -14
- package/docs/develop.md +4 -1
- package/docs/diagnostics.md +30 -10
- package/docs/enthusiast/README.md +6 -1
- package/docs/enthusiast/how-to-agent-gates.md +5 -0
- package/docs/enthusiast/how-to-gallery-starter.md +2 -1
- package/docs/enthusiast/how-to-pick-shape.md +1 -1
- package/docs/package-surface.md +10 -6
- package/docs/product-voice.md +32 -6
- package/docs/threat-model.md +2 -2
- package/docs/typescript-support.md +3 -3
- package/docs/use.md +23 -11
- package/package.json +1 -1
- package/schemas/ark.config.schema.json +23 -2
- package/server.json +2 -2
- package/templates/agent-skills/README.md +7 -4
- package/templates/agent-skills/ark-adopt/SKILL.md +91 -6
- package/templates/agent-skills/ark-architect/SKILL.md +5 -18
- package/templates/agent-skills/ark-autopilot/SKILL.md +14 -6
- package/templates/agent-skills/ark-contract/SKILL.md +9 -20
- package/templates/agent-skills/ark-coverage/SKILL.md +13 -9
- package/templates/agent-skills/ark-explain/SKILL.md +11 -5
- package/templates/agent-skills/ark-explore/SKILL.md +39 -5
- package/templates/agent-skills/ark-fix/SKILL.md +15 -20
- package/templates/agent-skills/ark-loop/SKILL.md +14 -20
- package/templates/agent-skills/ark-order/SKILL.md +200 -0
- package/templates/agent-skills/ark-place/SKILL.md +49 -11
- package/templates/agent-skills/ark-runtime/SKILL.md +21 -6
- package/templates/agent-skills/ark-think/SKILL.md +24 -126
- package/templates/agent-skills/ark-upgrade/SKILL.md +14 -3
- package/templates/arkrules/DomainModel.json +14 -1
- package/templates/skills/ark-adopt.md +91 -6
- package/templates/skills/ark-architect.md +5 -18
- package/templates/skills/ark-autopilot.md +14 -6
- package/templates/skills/ark-contract.md +9 -20
- package/templates/skills/ark-coverage.md +13 -9
- package/templates/skills/ark-explain.md +11 -5
- package/templates/skills/ark-explore.md +39 -5
- package/templates/skills/ark-fix.md +15 -20
- package/templates/skills/ark-loop.md +14 -20
- package/templates/skills/ark-order.md +200 -0
- package/templates/skills/ark-place.md +49 -11
- package/templates/skills/ark-runtime.md +21 -6
- package/templates/skills/ark-think.md +24 -126
- package/templates/skills/ark-upgrade.md +14 -3
- 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 {
|
|
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
|
-
|
|
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
|
|
147
|
+
if (arkOrder && arkOrder.notAScore === true) {
|
|
91
148
|
console.log('');
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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;
|
|
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
|
-
|
|
128
|
-
|
|
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('');
|
package/bin/lib/doctor-human.mjs
CHANGED
|
@@ -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
|
-
|
|
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 (
|
|
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(
|
|
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
|
|
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
|
|
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) {
|
package/bin/lib/doctor-plan.mjs
CHANGED
|
@@ -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
|
|
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
|
|
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) —
|
|
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) —
|
|
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',
|
package/bin/lib/gate-files.mjs
CHANGED
|
@@ -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
|
|
751
|
-
//
|
|
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 (
|
|
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
|
|
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.`;
|