arkgate 4.8.17 → 4.8.19
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 +109 -2
- package/README.md +12 -10
- package/bin/ark-mcp-runtime.mjs +3 -3
- package/bin/ark-shared.mjs +197 -20
- package/bin/ark.mjs +24 -18
- package/bin/lib/agent-gates.mjs +2 -0
- package/bin/lib/ci-and-commands.mjs +2 -2
- package/bin/lib/doctor-human.mjs +4 -2
- package/bin/lib/doctor-next-actions.mjs +152 -17
- package/bin/lib/doctor-plan.mjs +6 -8
- package/bin/lib/first-run-help.mjs +4 -1
- package/bin/lib/gate-files.mjs +22 -1
- package/bin/lib/host-support-matrix.mjs +1 -1
- package/bin/lib/html-report-depth.mjs +1 -1
- package/bin/lib/html-report.mjs +1 -1
- package/bin/lib/install-migrate.mjs +3 -4
- package/bin/lib/managed-upgrade.mjs +30 -5
- package/bin/lib/mcp-adoption.mjs +4 -1
- package/bin/lib/presets.mjs +15 -21
- package/bin/lib/scan-files.mjs +3 -26
- package/bin/lib/skill-install.mjs +45 -6
- package/bin/lib/start-preview.mjs +135 -8
- package/bin/lib/write-path-detect.mjs +3 -2
- package/dist/{diagnosticCatalog-DVABouBV.d.ts → diagnosticCatalog-D2v0ASN2.d.ts} +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/nestjs/index.cjs +1 -1
- package/dist/nestjs/index.js +1 -1
- package/dist/runtime/index.cjs +1 -1
- package/dist/runtime/index.d.ts +1 -1
- package/dist/runtime/index.js +1 -1
- package/docs/README.md +2 -2
- package/docs/agent-guide.md +11 -2
- package/docs/ai-gates.md +11 -3
- package/docs/develop.md +1 -2
- package/docs/enthusiast/how-to-pick-shape.md +1 -1
- package/docs/use.md +10 -6
- package/package.json +1 -1
- package/server.json +2 -2
- package/templates/agent-skills/README.md +1 -1
- package/templates/agent-skills/ark-adopt/SKILL.md +4 -3
- package/templates/agent-skills/ark-explain/SKILL.md +3 -0
- package/templates/agent-skills/ark-place/SKILL.md +2 -0
- package/templates/agent-skills/ark-upgrade/SKILL.md +1 -0
- package/templates/hooks/opencode-ark-write-gate.mjs +2 -2
- package/templates/skills/ark-adopt.md +4 -3
- package/templates/skills/ark-explain.md +3 -0
- package/templates/skills/ark-place.md +2 -0
- package/templates/skills/ark-upgrade.md +1 -0
|
@@ -201,7 +201,7 @@ const AGENT_CONTRACT = {
|
|
|
201
201
|
],
|
|
202
202
|
// Cursor-only guidance: the write-time validate_code tool is available in
|
|
203
203
|
// Cursor's runtime but has no equivalent in a plain AGENTS.md read.
|
|
204
|
-
cursorValidateStep: `
|
|
204
|
+
cursorValidateStep: `The write gate is the host pre-hook when \`.cursor/hooks.json\` is installed, trusted, and \`failClosed\`. Call \`ark_prepare_write\` / \`validate_code\` only as fallback if that hook is missing or fail-open.`,
|
|
205
205
|
};
|
|
206
206
|
|
|
207
207
|
/**
|
|
@@ -413,7 +413,7 @@ Contener · Guiar · Ordenar — contain the write, guide the next step, order l
|
|
|
413
413
|
1. Status anytime: \`${doctorCmd}\` — what's wrong and what to do first.
|
|
414
414
|
2. Read the **Improvement compass** section (not a score). Name residual lenses in plain language when present (SoC, DIP, domain, …). Out-of-scope lenses (performance, app security tooling, full resilience) stay honest — do not invent Ark enforcement for them.
|
|
415
415
|
3. CLI-first: if the local CLI already resolved this project root, do not wait on MCP “still connecting”. Identity handshake is optional in that case. When you do trust MCP evidence: call \`ark_identity\` with \`project.expectedRoot\` set to this project's exact absolute root, then reuse that root plus the returned \`projectIdentity.projectId\` on every Ark MCP call. A descendant path is authoritative only with that matching id. Missing tool, non-\`matched\` binding, or wrong root means this is not proven to be the right project: restart the host and use the local CLI meanwhile.
|
|
416
|
-
4. Day to day: call \`ark_manifest\` with the same project expectation (or skip if CLI already bound the root); place new files with \`ark_place\` (filePath required — fail-closed);
|
|
416
|
+
4. Day to day: call \`ark_manifest\` with the same project expectation (or skip if CLI already bound the root); place new files with \`ark_place\` (filePath required — fail-closed); run \`${checkCmd}\`. The host pre-hook is the write gate when installed; MCP \`ark_prepare_write\` is fallback if that hook is missing or fail-open. The \`ark://manifest\` resource is compatibility-only and always unverified. On a deny, fix the illegal import — do not weaken \`ark.config.json\`.
|
|
417
417
|
5. If MCP is unavailable: inspect \`ark.config.json\` and run \`${checkCmd}\`. The host-readable projection is AGENTS.md and CLAUDE.md (same schema).
|
|
418
418
|
|
|
419
419
|
**Single door when residual remains:**
|
package/bin/lib/doctor-human.mjs
CHANGED
|
@@ -11,7 +11,7 @@ import { printDoctorAdvisories, printCompactExtraDoctorLines } from './doctor-ad
|
|
|
11
11
|
import { designDeltaDoctorLines } from './design-delta.mjs';
|
|
12
12
|
import { enforcementDoctorLines } from './enforcement-state.mjs';
|
|
13
13
|
import { analysisIncompleteStatement } from './analysis-completeness.mjs';
|
|
14
|
-
import { skillGapsForActiveHost, detectCodexHomeGap, codexConcernIsActive } from './agent-gates.mjs';
|
|
14
|
+
import { skillGapsForActiveHost, detectCodexHomeGap, codexConcernIsActive, skillGapToolLabel } from './agent-gates.mjs';
|
|
15
15
|
import { agentHomeConcernIsActive } from './agent-homes.mjs';
|
|
16
16
|
import { layerGuidanceLine } from './layer-description.mjs';
|
|
17
17
|
import {
|
|
@@ -147,6 +147,8 @@ export function printDoctorCompactHuman(view) {
|
|
|
147
147
|
if (skippableCi) line(warn, skippableCi);
|
|
148
148
|
else if (writePathHonesty?.nativeFailClosed === false) {
|
|
149
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?.id === 'write-path-mcp-only') {
|
|
151
|
+
line(warn, writePath.gap.message || 'Fell back to MCP prepare; required CI is the merge boundary.');
|
|
150
152
|
} else if (listedMissing.length === 0 && (writePath.gap || writePathHonesty?.softWriteHost)) {
|
|
151
153
|
line(warn, 'Local writes are advisory; required CI is the merge boundary.');
|
|
152
154
|
}
|
|
@@ -467,7 +469,7 @@ export function printDoctorDetailsHuman(view) {
|
|
|
467
469
|
if (remMiss + remStale > 0) {
|
|
468
470
|
line(
|
|
469
471
|
warn,
|
|
470
|
-
`${remMiss} missing / ${remStale} content-behind-package /ark-* skill(s) for ${remainingGaps.map((g) => g.tool).join(', ')}`
|
|
472
|
+
`${remMiss} missing / ${remStale} content-behind-package /ark-* skill(s) for ${remainingGaps.map((g) => skillGapToolLabel(g.tool)).join(', ')}`
|
|
471
473
|
);
|
|
472
474
|
}
|
|
473
475
|
const codexHomeGap = detectCodexHomeGap(root);
|
|
@@ -2,13 +2,43 @@
|
|
|
2
2
|
* Rank doctor next actions from already-computed facts (no I/O).
|
|
3
3
|
* Lets the human printer show light + #1 before honesty/compass sections.
|
|
4
4
|
*/
|
|
5
|
-
import { arkCommand } from '../ark-shared.mjs';
|
|
5
|
+
import { arkCommand, globToRegExp } from '../ark-shared.mjs';
|
|
6
6
|
import { skillGapsForActiveHost } from './agent-gates.mjs';
|
|
7
7
|
import { agentHomeConcernIsActive, agentHomeRefreshCommand } from './agent-homes.mjs';
|
|
8
8
|
import { mergePostGreenTopActions } from './post-green-path.mjs';
|
|
9
9
|
import { ADOPTED_NOT, NOT_ADOPTED_NEXT_ACTION } from './adoption-stance.mjs';
|
|
10
10
|
import { REQUIRED_GATE_WORKFLOW } from './gate-files.mjs';
|
|
11
11
|
|
|
12
|
+
/** Package pin may exist; local resolve still failed. Lead with install, not CI. */
|
|
13
|
+
export const PACKAGE_UNRESOLVED_NEXT_ACTION =
|
|
14
|
+
'Install arkgate in this project first (npx --package=arkgate, or pnpm add -D arkgate -w / yarn add -D arkgate -W at a workspace root), then re-run --doctor';
|
|
15
|
+
|
|
16
|
+
export function packageUnresolvedNextAction(ctx) {
|
|
17
|
+
if (ctx?.packageInstalled !== false || ctx?.selfHost === true) return null;
|
|
18
|
+
const code = ctx?.packageVersionTruth?.code;
|
|
19
|
+
if (code === 'PACKAGE_PIN_SELF_HOST' || code === 'PACKAGE_PIN_ABSENT') return null;
|
|
20
|
+
// Failed start --apply: pin is in package.json, node_modules did not resolve.
|
|
21
|
+
if (code === 'PACKAGE_PIN_MATCHES' || code === 'PACKAGE_PIN_BEHIND_CLI' || ctx?.packagePinned === true) {
|
|
22
|
+
return PACKAGE_UNRESOLVED_NEXT_ACTION;
|
|
23
|
+
}
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Compact doctor JSON #1 — same rank as collectDoctorNextActions when package is missing. */
|
|
28
|
+
export function preferredDoctorPrimaryNextAction({
|
|
29
|
+
adopted,
|
|
30
|
+
packageInstalled,
|
|
31
|
+
selfHost,
|
|
32
|
+
packageVersionTruth,
|
|
33
|
+
postGreenPath,
|
|
34
|
+
dualTruthNext,
|
|
35
|
+
} = {}) {
|
|
36
|
+
return (
|
|
37
|
+
packageUnresolvedNextAction({ packageInstalled, selfHost, packageVersionTruth }) ||
|
|
38
|
+
(adopted === 'not-adopted' ? NOT_ADOPTED_NEXT_ACTION : postGreenPath?.action ?? dualTruthNext ?? null)
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
12
42
|
function missingGateFiles(ctx) {
|
|
13
43
|
const list = Array.isArray(ctx.gatesMissing) ? ctx.gatesMissing : [];
|
|
14
44
|
if (ctx.ciNotFailClosed) {
|
|
@@ -17,6 +47,99 @@ function missingGateFiles(ctx) {
|
|
|
17
47
|
return list;
|
|
18
48
|
}
|
|
19
49
|
|
|
50
|
+
/** Ignore a handful of expected Next API dual-matches. */
|
|
51
|
+
const DUAL_MATCH_REPAIR_FLOOR = 20;
|
|
52
|
+
/** Absolute pile-up even when the tree is huge. */
|
|
53
|
+
const DUAL_MATCH_REPAIR_ABSOLUTE = 50;
|
|
54
|
+
/** Share of in-scope files that match two+ layers. */
|
|
55
|
+
const DUAL_MATCH_REPAIR_SHARE = 0.1;
|
|
56
|
+
|
|
57
|
+
function isWildcardPattern(pattern) {
|
|
58
|
+
return /[*?]/.test(String(pattern ?? ''));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function posixRel(file) {
|
|
62
|
+
return String(file ?? '').split(/\\/).join('/');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function layerPatterns(cov, layerName) {
|
|
66
|
+
const rows = Array.isArray(cov?.layers) ? cov.layers : [];
|
|
67
|
+
const row = rows.find((layer) => layer?.name === layerName);
|
|
68
|
+
return Array.isArray(row?.patterns) ? row.patterns : [];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function fileMatchesWildcardPatterns(file, patterns) {
|
|
72
|
+
const rel = posixRel(file);
|
|
73
|
+
return patterns.some((pattern) => isWildcardPattern(pattern) && globToRegExp(pattern).test(rel));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Domain overlap is a glob leak when Domain matched the sample via a wildcard.
|
|
78
|
+
* Exact-file Domain listings under another layer glob (mother generated CLI)
|
|
79
|
+
* are intentional dual-lists, not a repair. No layer patterns → keep the
|
|
80
|
+
* Domain-sample shortcut so older #269 fixtures still fire.
|
|
81
|
+
*/
|
|
82
|
+
function domainSampleIsGlobLeak(row, cov) {
|
|
83
|
+
if (!(row?.layers ?? []).includes('DomainModel')) return false;
|
|
84
|
+
const patterns = layerPatterns(cov, 'DomainModel');
|
|
85
|
+
if (patterns.length === 0) return true;
|
|
86
|
+
return fileMatchesWildcardPatterns(row.file, patterns);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Overlapping roots from matching Domain wildcards, else sample path prefixes. */
|
|
90
|
+
export function overlappingRootsFromCoverage(cov) {
|
|
91
|
+
const samples = Array.isArray(cov?.dualMembership?.samples) ? cov.dualMembership.samples : [];
|
|
92
|
+
const domainPatterns = layerPatterns(cov, 'DomainModel');
|
|
93
|
+
const fromPatterns = [];
|
|
94
|
+
for (const row of samples) {
|
|
95
|
+
const rel = posixRel(row?.file);
|
|
96
|
+
for (const pattern of domainPatterns) {
|
|
97
|
+
if (isWildcardPattern(pattern) && globToRegExp(pattern).test(rel)) {
|
|
98
|
+
fromPatterns.push(pattern);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (fromPatterns.length > 0) {
|
|
103
|
+
return [...new Set(fromPatterns)].slice(0, 2);
|
|
104
|
+
}
|
|
105
|
+
const derived = [];
|
|
106
|
+
for (const row of samples) {
|
|
107
|
+
const first = posixRel(row?.file).split('/').filter(Boolean)[0];
|
|
108
|
+
if (first) derived.push(`${first}/**`);
|
|
109
|
+
}
|
|
110
|
+
return [...new Set(derived)].slice(0, 2);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Dual-match is a lying layer map when it is large — especially Domain
|
|
115
|
+
* overlapping Presentation/Application after over-broad monorepo start globs.
|
|
116
|
+
* Intentional file+glob dual-lists (exact Domain files under Tooling `bin/**`)
|
|
117
|
+
* do not steal doctor #1.
|
|
118
|
+
*/
|
|
119
|
+
export function dualMatchNeedsGlobRepair(cov) {
|
|
120
|
+
const count = Number(cov?.dualMembership?.count) || 0;
|
|
121
|
+
if (count < DUAL_MATCH_REPAIR_FLOOR) return false;
|
|
122
|
+
const total = Number(cov?.totalFiles ?? cov?.governed?.totalFiles) || 0;
|
|
123
|
+
const samples = Array.isArray(cov?.dualMembership?.samples) ? cov.dualMembership.samples : [];
|
|
124
|
+
const domainOverlap = samples.some((row) => domainSampleIsGlobLeak(row, cov));
|
|
125
|
+
if (domainOverlap) return true;
|
|
126
|
+
if (total > 0 && count / total >= DUAL_MATCH_REPAIR_SHARE) return true;
|
|
127
|
+
return count >= DUAL_MATCH_REPAIR_ABSOLUTE;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function overlappingGlobNextAction(cov) {
|
|
131
|
+
const count = Number(cov?.dualMembership?.count) || 0;
|
|
132
|
+
const sample = Array.isArray(cov?.dualMembership?.samples) ? cov.dualMembership.samples[0] : null;
|
|
133
|
+
const layers = Array.isArray(sample?.layers) ? sample.layers.filter(Boolean) : [];
|
|
134
|
+
const example =
|
|
135
|
+
sample?.file && layers.length > 1
|
|
136
|
+
? `${sample.file} matches ${layers.join(' + ')}`
|
|
137
|
+
: 'the same files match two layer globs';
|
|
138
|
+
const roots = overlappingRootsFromCoverage(cov);
|
|
139
|
+
const rootPhrase = roots.length > 0 ? roots.join(', ') : 'the overlapping globs';
|
|
140
|
+
return `Fix overlapping layer globs — ${count} files match more than one layer (e.g. ${example}). Narrow overlapping roots like ${rootPhrase}. Then /ark-adopt`;
|
|
141
|
+
}
|
|
142
|
+
|
|
20
143
|
export function collectDoctorNextActions(ctx) {
|
|
21
144
|
const actions = [];
|
|
22
145
|
const missingFiles = missingGateFiles(ctx);
|
|
@@ -26,6 +149,8 @@ export function collectDoctorNextActions(ctx) {
|
|
|
26
149
|
if (notAdopted || ctx.adopted === ADOPTED_NOT || ctx.adopted == null) {
|
|
27
150
|
actions.push(ctx.notAdoptedNextAction || NOT_ADOPTED_NEXT_ACTION);
|
|
28
151
|
}
|
|
152
|
+
const resolvePkg = packageUnresolvedNextAction(ctx);
|
|
153
|
+
if (resolvePkg) actions.push(resolvePkg);
|
|
29
154
|
const nudge = ctx.stewardNudge;
|
|
30
155
|
if (
|
|
31
156
|
nudge &&
|
|
@@ -37,9 +162,26 @@ export function collectDoctorNextActions(ctx) {
|
|
|
37
162
|
if (ctx.layerOwners?.required && ctx.layerOwners.nextAction) {
|
|
38
163
|
actions.push(ctx.layerOwners.nextAction);
|
|
39
164
|
}
|
|
165
|
+
const humanSkillGaps = skillGapsForActiveHost(ctx.skillGaps ?? []);
|
|
166
|
+
const legacyCodex = humanSkillGaps.some((g) => g.tool === 'codex' && g.legacyPromptsOnly);
|
|
167
|
+
const remainingGaps = humanSkillGaps.filter(
|
|
168
|
+
(g) => !(g.tool === 'codex' && (g.legacyPromptsOnly || g.legacyAdvisory))
|
|
169
|
+
);
|
|
170
|
+
const remMiss = remainingGaps.reduce((s, g) => s + g.missing, 0);
|
|
171
|
+
const remStale = remainingGaps.reduce((s, g) => s + g.stale, 0);
|
|
172
|
+
if (legacyCodex) {
|
|
173
|
+
actions.push('install Codex SKILL.md catalog (--install-agent-gates --skills-only --tools codex --force)');
|
|
174
|
+
}
|
|
175
|
+
if (remMiss > 0) {
|
|
176
|
+
actions.push('install missing /ark-* skills (--install-agent-gates --skills-only --force)');
|
|
177
|
+
} else if (remStale > 0) {
|
|
178
|
+
actions.push('refresh stale /ark-* skills (--install-agent-gates --skills-only --force) — gates are installed, catalog is stale');
|
|
179
|
+
}
|
|
40
180
|
const enforceEmptyPlan =
|
|
41
181
|
ctx.operatingMode === 'enforce' && planAEmpty && gatesInstalled && !notAdopted;
|
|
42
|
-
|
|
182
|
+
// Stale/missing doors outrank the Shape nudge — a colleague on an old catalog
|
|
183
|
+
// must see skills-only refresh as #1, not leftover-design explore.
|
|
184
|
+
if (enforceEmptyPlan && remMiss === 0 && remStale === 0 && !legacyCodex) {
|
|
43
185
|
actions.push(
|
|
44
186
|
ctx.postGreenPath?.action ||
|
|
45
187
|
'/ark-explore, then one small refactor with /ark-autopilot and your OK'
|
|
@@ -70,6 +212,8 @@ export function collectDoctorNextActions(ctx) {
|
|
|
70
212
|
if (ctx.coverageHonesty.greenIsNotEnforcement && ctx.coverageHonesty.worseThanNoGate) {
|
|
71
213
|
actions.push('raise governed coverage above a minority slice before treating green as enforcement');
|
|
72
214
|
}
|
|
215
|
+
const overlapAction = dualMatchNeedsGlobRepair(ctx.cov) ? overlappingGlobNextAction(ctx.cov) : null;
|
|
216
|
+
if (overlapAction) actions.push(overlapAction);
|
|
73
217
|
if (ctx.cov.suggestions.length > 0) actions.push('classify the ungoverned directories (/ark-adopt)');
|
|
74
218
|
if (ctx.packageVersionTruth?.dualTruth) {
|
|
75
219
|
actions.push(
|
|
@@ -100,21 +244,6 @@ export function collectDoctorNextActions(ctx) {
|
|
|
100
244
|
: 'Remove the skippable if:, or write .ark/adoption-stance.json with stance: advisory-only')
|
|
101
245
|
);
|
|
102
246
|
}
|
|
103
|
-
const humanSkillGaps = skillGapsForActiveHost(ctx.skillGaps);
|
|
104
|
-
const legacyCodex = humanSkillGaps.some((g) => g.tool === 'codex' && g.legacyPromptsOnly);
|
|
105
|
-
const remainingGaps = humanSkillGaps.filter(
|
|
106
|
-
(g) => !(g.tool === 'codex' && (g.legacyPromptsOnly || g.legacyAdvisory))
|
|
107
|
-
);
|
|
108
|
-
const remMiss = remainingGaps.reduce((s, g) => s + g.missing, 0);
|
|
109
|
-
const remStale = remainingGaps.reduce((s, g) => s + g.stale, 0);
|
|
110
|
-
if (legacyCodex) {
|
|
111
|
-
actions.push('install Codex SKILL.md catalog (--install-agent-gates --skills-only --tools codex --force)');
|
|
112
|
-
}
|
|
113
|
-
if (remMiss > 0) {
|
|
114
|
-
actions.push('install missing /ark-* skills (--install-agent-gates --skills-only --force)');
|
|
115
|
-
} else if (remStale > 0) {
|
|
116
|
-
actions.push('refresh stale /ark-* skills (--install-agent-gates --skills-only --force) — gates are installed, catalog is stale');
|
|
117
|
-
}
|
|
118
247
|
if (ctx.codexHomeGap && ctx.codexConcernActive && ctx.codexHomeGap.duplicateHome) {
|
|
119
248
|
actions.push(
|
|
120
249
|
'remove duplicate Codex home /ark-* skills (project .agents/skills is enough): --install-agent-gates --skills-only --prune-home-duplicates'
|
|
@@ -162,6 +291,12 @@ export function collectDoctorNextActions(ctx) {
|
|
|
162
291
|
if (ctx.designFitness.designWeak && unique.length === 0 && ctx.postGreenPath) {
|
|
163
292
|
unique.push(ctx.postGreenPath.action);
|
|
164
293
|
}
|
|
294
|
+
if (resolvePkg) {
|
|
295
|
+
return [resolvePkg, ...unique.filter((a) => a !== resolvePkg)];
|
|
296
|
+
}
|
|
297
|
+
if (overlapAction) {
|
|
298
|
+
return [overlapAction, ...unique.filter((a) => a !== overlapAction)];
|
|
299
|
+
}
|
|
165
300
|
if (notAdopted) {
|
|
166
301
|
const next = ctx.notAdoptedNextAction || NOT_ADOPTED_NEXT_ACTION;
|
|
167
302
|
return [next, ...unique.filter((a) => a !== next)];
|
package/bin/lib/doctor-plan.mjs
CHANGED
|
@@ -13,7 +13,7 @@ import * as arkShared from '../ark-shared.mjs';
|
|
|
13
13
|
import { summarizeRulesUnderContract } from './rules-under-contract.mjs';
|
|
14
14
|
import { describePackageVersionDualTruth } from './field-install.mjs';
|
|
15
15
|
import { detectAgentHomeGaps } from './agent-homes.mjs';
|
|
16
|
-
import { collectDoctorNextActions } from './doctor-next-actions.mjs';
|
|
16
|
+
import { collectDoctorNextActions, preferredDoctorPrimaryNextAction } from './doctor-next-actions.mjs';
|
|
17
17
|
import { printDoctorCompactHuman, printDoctorDetailsHuman } from './doctor-human.mjs';
|
|
18
18
|
import { collectLayerOwnerResidual, layerGuidanceLine, placementDescriptionFields } from './layer-description.mjs';
|
|
19
19
|
import { collectAdrPresenceResidual, printAdrPresenceHint } from './adr-presence.mjs';
|
|
@@ -653,6 +653,7 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
|
|
|
653
653
|
const statusTransitionCatalog = collectStatusTransitionCatalogResidual({ root, config, files, statesTransitions });
|
|
654
654
|
const noDomainFrontend = collectNoDomainFrontendResidual({ config, coverage: cov, designSmells });
|
|
655
655
|
const { invariantCoverageRoots, invariantTestsPath } = collectInvariantCoverageResiduals({ adopted: isAdopted(adopted) || options.requireGates === true, coverage: config?.coverage, config, root });
|
|
656
|
+
const selfHost = packageVersionTruth?.selfHost === true || packageVersionTruth?.code === 'PACKAGE_PIN_SELF_HOST';
|
|
656
657
|
const { coverageHonesty, baselineHonesty, writePathHonesty, productHonesty } =
|
|
657
658
|
computeDoctorEnforcementHonesty({
|
|
658
659
|
governedPercent: cov.governed.percent,
|
|
@@ -681,13 +682,10 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
|
|
|
681
682
|
: rulesUnderContract?.active === true || arkRun?.active === true || doctorAdvisories.arkOrder?.active === true
|
|
682
683
|
? { active: true, extraMergeTeeth: arkRun?.extraMergeTeeth === true || doctorAdvisories.arkOrder?.extraMergeTeeth === true }
|
|
683
684
|
: null,
|
|
684
|
-
primaryNextAction:
|
|
685
|
-
adopted === 'not-adopted' ? NOT_ADOPTED_NEXT_ACTION : postGreenPath?.action ?? dualTruthNext,
|
|
685
|
+
primaryNextAction: preferredDoctorPrimaryNextAction({ adopted, packageInstalled, selfHost, packageVersionTruth, postGreenPath, dualTruthNext }),
|
|
686
686
|
operatingMode,
|
|
687
687
|
packageInstalled,
|
|
688
|
-
selfHost
|
|
689
|
-
packageVersionTruth?.selfHost === true ||
|
|
690
|
-
packageVersionTruth?.code === 'PACKAGE_PIN_SELF_HOST',
|
|
688
|
+
selfHost,
|
|
691
689
|
nativeFailClosed: writePath.nativeFailClosed,
|
|
692
690
|
nativeFailClosedPolicy: writePath.nativeFailClosedPolicy,
|
|
693
691
|
adopted,
|
|
@@ -752,7 +750,7 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
|
|
|
752
750
|
adoptionStance: adopted,
|
|
753
751
|
...(adopted === 'not-adopted'
|
|
754
752
|
? {
|
|
755
|
-
primaryNextAction: NOT_ADOPTED_NEXT_ACTION,
|
|
753
|
+
primaryNextAction: productHonesty.primaryNextAction || NOT_ADOPTED_NEXT_ACTION,
|
|
756
754
|
...(postGreenPath ? DESIGN_WEAK_HONESTY_FLAGS : {}),
|
|
757
755
|
}
|
|
758
756
|
: postGreenPath
|
|
@@ -908,7 +906,7 @@ export function runDoctor(root, config, files, rules, violations, asJson, option
|
|
|
908
906
|
coverageHonesty,
|
|
909
907
|
cov,
|
|
910
908
|
packageVersionTruth,
|
|
911
|
-
dualTruthNext,
|
|
909
|
+
dualTruthNext, packageInstalled, selfHost,
|
|
912
910
|
activeCount,
|
|
913
911
|
writePath,
|
|
914
912
|
gatesMissing,
|
|
@@ -12,6 +12,7 @@ ${NORTH_STAR_ONE_LINE}
|
|
|
12
12
|
arkgate start preview (no writes)
|
|
13
13
|
arkgate start --apply write host + CI setup
|
|
14
14
|
(refuses weak coverage/shape; lock with --archetype/--preset/--force)
|
|
15
|
+
(refuses a plan too big for compact start; next: arkgate-check --init)
|
|
15
16
|
arkgate-check --doctor status — one next step
|
|
16
17
|
(if missing: npx --package=arkgate arkgate-check --doctor)
|
|
17
18
|
|
|
@@ -70,6 +71,7 @@ Options:
|
|
|
70
71
|
--yes Non-interactive defaults: create config if needed, install gate templates, run strict check.
|
|
71
72
|
(Also the implicit default when stdin/stdout are not a TTY — agents never hang on prompts.)
|
|
72
73
|
--force Allow generated files to overwrite existing files.
|
|
74
|
+
Unlocks weak coverage/shape on start --apply. Does not unlock a plan too big for compact start.
|
|
73
75
|
--no-strict Skip the final strict ark-check run.
|
|
74
76
|
--install Pin and install arkgate as a project devDependency (default for start).
|
|
75
77
|
--no-install Skip adding/installing arkgate as a project devDependency (start/upgrade).
|
|
@@ -303,7 +305,8 @@ export function checkUsageAll() {
|
|
|
303
305
|
'rule file. Existing files are never overwritten without --force, so re-running',
|
|
304
306
|
'after an update only adds what is missing. --skills-only restricts the write to',
|
|
305
307
|
'just the /ark-* skills (safe to --force-refresh — it leaves a customized AGENTS.md,',
|
|
306
|
-
'settings, and CI workflow untouched).',
|
|
308
|
+
'settings, and CI workflow untouched). Doctor / ark-check warn when an already-installed',
|
|
309
|
+
'catalog is behind this package; that next action is --skills-only --force, not re-adopt.',
|
|
307
310
|
'Pass --tools to pick which tool configs to write; otherwise they are auto-detected',
|
|
308
311
|
'from their config directories (.claude/, .cursor/, .codex/, .grok/, .windsurf/,',
|
|
309
312
|
'.clinerules/, .kiro/, .roo/, .continue/, .gemini/; copilot is explicit-only).',
|
package/bin/lib/gate-files.mjs
CHANGED
|
@@ -152,9 +152,30 @@ export function isCompactRouterAgentsContent(text) {
|
|
|
152
152
|
return typeof text === 'string' && COMPACT_ROUTER.test(text);
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
+
function hasCursorFailClosedHook(root) {
|
|
156
|
+
try {
|
|
157
|
+
const parsed = readJson(path.join(root, '.cursor', 'hooks.json'));
|
|
158
|
+
const entries = Array.isArray(parsed?.hooks?.preToolUse) ? parsed.hooks.preToolUse : [];
|
|
159
|
+
return entries.some(
|
|
160
|
+
(entry) =>
|
|
161
|
+
entry &&
|
|
162
|
+
typeof entry === 'object' &&
|
|
163
|
+
entry.failClosed === true &&
|
|
164
|
+
typeof entry.command === 'string' &&
|
|
165
|
+
/(?:^|\s)--hook(?:\s|$)/.test(entry.command)
|
|
166
|
+
);
|
|
167
|
+
} catch {
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function hasCursorCompactRegistration(root) {
|
|
173
|
+
return hasArkMcpRegistration(root, '.cursor/mcp.json') && hasCursorFailClosedHook(root);
|
|
174
|
+
}
|
|
175
|
+
|
|
155
176
|
function hasCompactHostRegistration(root, host) {
|
|
156
177
|
if (host === 'none') return hasArkMcpRegistration(root);
|
|
157
|
-
if (host === 'cursor') return
|
|
178
|
+
if (host === 'cursor') return hasCursorCompactRegistration(root);
|
|
158
179
|
if (host === 'codex') return hasCodexCompactRegistration(root);
|
|
159
180
|
const files = COMPACT_HOST_FILES[host];
|
|
160
181
|
return Boolean(files) && files.every((relativePath) => fs.existsSync(path.join(root, relativePath)));
|
|
@@ -197,7 +197,7 @@ Codex hard write covers only a complete local \`apply_patch\`; Cursor covers onl
|
|
|
197
197
|
installed + trusted, while shell/direct
|
|
198
198
|
filesystem writes, hosted or specialized opt-out paths, and human edits still rely on CI.
|
|
199
199
|
|
|
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.`;
|
|
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. **Write-gate order:** install and trust the host pre-hook first. MCP prepare is fallback when that path is missing or fail-open. 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.`;
|
|
201
201
|
}
|
|
202
202
|
|
|
203
203
|
/**
|
|
@@ -224,7 +224,7 @@ export function writePathModeHint(mode) {
|
|
|
224
224
|
case 'reject-only':
|
|
225
225
|
return 'Hard write boundary without repair payload; edits can be blocked without guided re-entry.';
|
|
226
226
|
case 'mcp-only':
|
|
227
|
-
return '
|
|
227
|
+
return 'Fell back to MCP prepare — advisory prepare-write/autoPatch; no hard PreToolUse for this host.';
|
|
228
228
|
case 'none':
|
|
229
229
|
return 'No hard write boundary or advisory MCP for the active host (or host is unknown in this process).';
|
|
230
230
|
default:
|
package/bin/lib/html-report.mjs
CHANGED
|
@@ -905,7 +905,7 @@ export function renderHtmlReport({
|
|
|
905
905
|
);
|
|
906
906
|
} else {
|
|
907
907
|
skillsParts.push(
|
|
908
|
-
`<div class="pill warn">${actionableSkillGaps.length} skill gap(s) —
|
|
908
|
+
`<div class="pill warn">${actionableSkillGaps.length} skill gap(s) — refresh skills only: ark-check --install-agent-gates --skills-only --force</div>`
|
|
909
909
|
);
|
|
910
910
|
}
|
|
911
911
|
if (legacyAdvisoryOnly) {
|
|
@@ -200,10 +200,9 @@ export function buildManagedAssetCatalog({ root, tools, compact = false, skillsO
|
|
|
200
200
|
})
|
|
201
201
|
);
|
|
202
202
|
if (selectedTools.has('cursor')) {
|
|
203
|
-
|
|
204
|
-
// whole-file for managed upgrade/manifest (json-merge is install-time only via
|
|
205
|
-
// mergeCursorArkHook below — same pattern as Antigravity hooks).
|
|
203
|
+
// Write-gate order: pre-hook first, MCP prepare as fallback + identity.
|
|
206
204
|
add('.cursor/hooks.json', cursorHooks(root));
|
|
205
|
+
add('.cursor/mcp.json', mcpJson(root));
|
|
207
206
|
if (!compact) add('.cursor/rules/ark.mdc', cursorRule(root));
|
|
208
207
|
}
|
|
209
208
|
if (selectedTools.has('claude')) add('.claude/settings.json', claudeSettings(root));
|
|
@@ -213,8 +212,8 @@ export function buildManagedAssetCatalog({ root, tools, compact = false, skillsO
|
|
|
213
212
|
if (!compact) add('docs/ark-codex-config.toml', codexTomlSnippet(root));
|
|
214
213
|
}
|
|
215
214
|
if (selectedTools.has('grok')) {
|
|
216
|
-
add('.grok/config.toml', grokProjectConfig(root));
|
|
217
215
|
add('.grok/hooks/ark-write-gate.json', grokHooks(root));
|
|
216
|
+
add('.grok/config.toml', grokProjectConfig(root));
|
|
218
217
|
}
|
|
219
218
|
if (selectedTools.has('antigravity')) {
|
|
220
219
|
add('.agents/hooks.json', antigravityHooks(root));
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
arkPackageVersion,
|
|
18
18
|
detectActiveAgentHost,
|
|
19
19
|
normalizeToolsList,
|
|
20
|
+
planSkillInstall,
|
|
20
21
|
skillContentIdentity,
|
|
21
22
|
} from './skill-install.mjs';
|
|
22
23
|
|
|
@@ -396,8 +397,11 @@ function summaryFor(assets, manifestChanged) {
|
|
|
396
397
|
for (const asset of assets) states[asset.state] = (states[asset.state] ?? 0) + 1;
|
|
397
398
|
const applying = assets.filter((asset) => asset.willApply);
|
|
398
399
|
const wouldWrite = applying.length;
|
|
399
|
-
//
|
|
400
|
-
|
|
400
|
+
// Same-body stamp drift (arkVersion / description prefix) stays state=current
|
|
401
|
+
// and is scheduled via planSkillInstall stamp-refresh — not a second pipeline.
|
|
402
|
+
const metadataRefresh = applying.filter(
|
|
403
|
+
(asset) => asset.state === 'current' && asset.reason === 'stamp-refresh'
|
|
404
|
+
).length;
|
|
401
405
|
const customizedPreserved = assets.filter((asset) => asset.state === 'customized').length;
|
|
402
406
|
const fileChanges = applying.length;
|
|
403
407
|
return {
|
|
@@ -477,9 +481,23 @@ export function planManagedUpgrade(root, options = {}) {
|
|
|
477
481
|
refreshSkills &&
|
|
478
482
|
catalogAsset.kind === 'skill' &&
|
|
479
483
|
classified.state === 'customized';
|
|
484
|
+
// HS01 / #284: identity-current skill whose arkVersion / description prefix
|
|
485
|
+
// lagged the package — reuse planSkillInstall stamp-refresh, do not reclassify.
|
|
486
|
+
const stampRefresh =
|
|
487
|
+
!unparsedScope &&
|
|
488
|
+
catalogAsset.kind === 'skill' &&
|
|
489
|
+
classified.state === 'current' &&
|
|
490
|
+
currentScoped != null &&
|
|
491
|
+
planSkillInstall({
|
|
492
|
+
existingContent: currentScoped,
|
|
493
|
+
targetContent: desiredScoped,
|
|
494
|
+
packageVersion: catalog.version ?? arkPackageVersion(),
|
|
495
|
+
scope: 'repo',
|
|
496
|
+
}).reason === 'stamp-refresh';
|
|
480
497
|
const canApply =
|
|
481
498
|
classified.state === 'stale' ||
|
|
482
499
|
skillRefresh ||
|
|
500
|
+
stampRefresh ||
|
|
483
501
|
(classified.state === 'missing' && (!recorded || accepted)) ||
|
|
484
502
|
(classified.state === 'conflicted' && accepted);
|
|
485
503
|
const blocked = classified.requiresConsent && !accepted && !skillRefresh;
|
|
@@ -490,7 +508,11 @@ export function planManagedUpgrade(root, options = {}) {
|
|
|
490
508
|
kind: catalogAsset.kind,
|
|
491
509
|
scope: catalogAsset.scope,
|
|
492
510
|
...classified,
|
|
493
|
-
...(unparsedScope
|
|
511
|
+
...(unparsedScope
|
|
512
|
+
? { reason: 'unparsed managed TOML scope preserved' }
|
|
513
|
+
: stampRefresh
|
|
514
|
+
? { reason: 'stamp-refresh' }
|
|
515
|
+
: {}),
|
|
494
516
|
action: canApply ? (currentScoped == null ? 'create' : 'update') : 'none',
|
|
495
517
|
willApply: canApply,
|
|
496
518
|
blocked,
|
|
@@ -970,8 +992,11 @@ export function renderManagedUpgrade(plan, options = {}) {
|
|
|
970
992
|
const customizedPreserved = summary.customizedPreserved ?? summary.states?.customized ?? 0;
|
|
971
993
|
const blocked = summary.blocked ?? 0;
|
|
972
994
|
console.log(
|
|
973
|
-
`Managed assets: ${managedAssets}; would write: ${wouldWrite}
|
|
974
|
-
|
|
995
|
+
`Managed assets: ${managedAssets}; would write: ${wouldWrite}` +
|
|
996
|
+
(summary.metadataRefresh
|
|
997
|
+
? `; metadata refresh: ${summary.metadataRefresh}`
|
|
998
|
+
: '') +
|
|
999
|
+
`; customized preserved: ${customizedPreserved}; blocked conflicts/deletions: ${blocked}.`
|
|
975
1000
|
);
|
|
976
1001
|
const honesty =
|
|
977
1002
|
plan.selfService ??
|
package/bin/lib/mcp-adoption.mjs
CHANGED
|
@@ -191,7 +191,10 @@ export function collectAdoptionGaps(root, config, coverage) {
|
|
|
191
191
|
host: 'cursor',
|
|
192
192
|
dir: '.cursor',
|
|
193
193
|
skill: (n) => path.join(root, '.cursor', 'commands', `${n}.md`),
|
|
194
|
-
extras: [
|
|
194
|
+
extras: [
|
|
195
|
+
['.cursor/hooks.json', 'write-gate hook'],
|
|
196
|
+
['.cursor/mcp.json', 'MCP config (fallback)'],
|
|
197
|
+
],
|
|
195
198
|
toolsFlag: 'cursor',
|
|
196
199
|
},
|
|
197
200
|
{
|
package/bin/lib/presets.mjs
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
DEFAULT_DOMAIN_FORBIDDEN_GLOBALS,
|
|
11
11
|
discoverRepoUnits,
|
|
12
12
|
DEFAULT_INTENT_PREFIXES,
|
|
13
|
+
monorepoRoleSourceGlobs,
|
|
13
14
|
resolveIncludeRoots,
|
|
14
15
|
} from '../ark-shared.mjs';
|
|
15
16
|
import { withArkConfigMetadata } from './config-contract.mjs';
|
|
@@ -629,27 +630,19 @@ export const ARCHITECTURE_PRESETS = {
|
|
|
629
630
|
}
|
|
630
631
|
// Turborepo: apps/ + packages/; Nx enterprise: apps/ + libs/ (+ packages/).
|
|
631
632
|
if (include.length === 0) include = ['packages', 'apps', 'libs'];
|
|
632
|
-
//
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
const
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
? unit.root
|
|
646
|
-
: `${unit.root}/${sourceRoot}`;
|
|
647
|
-
return base === '.' ? null : `${base}/**`;
|
|
648
|
-
}))
|
|
649
|
-
.filter(Boolean);
|
|
650
|
-
};
|
|
651
|
-
const librarySourcePatterns = packagePatterns(['library']);
|
|
652
|
-
const applicationSourcePatterns = packagePatterns(['application', 'cli']);
|
|
633
|
+
// Library Domain bags stay under packages/ or libs/ — never api/** or client/**.
|
|
634
|
+
const librarySourcePatterns = monorepoRoleSourceGlobs(units, ['library'], {
|
|
635
|
+
root,
|
|
636
|
+
domainScoped: true,
|
|
637
|
+
});
|
|
638
|
+
const applicationSourcePatterns = monorepoRoleSourceGlobs(units, ['application', 'cli'], {
|
|
639
|
+
root,
|
|
640
|
+
uiMode: 'not-ui',
|
|
641
|
+
});
|
|
642
|
+
const uiApplicationSourcePatterns = monorepoRoleSourceGlobs(units, ['application', 'cli'], {
|
|
643
|
+
root,
|
|
644
|
+
uiMode: 'ui',
|
|
645
|
+
});
|
|
653
646
|
return presetWithOverlays(
|
|
654
647
|
{
|
|
655
648
|
include,
|
|
@@ -695,6 +688,7 @@ export const ARCHITECTURE_PRESETS = {
|
|
|
695
688
|
'**/http/**',
|
|
696
689
|
'**/routes/**',
|
|
697
690
|
'**/hooks/**',
|
|
691
|
+
...uiApplicationSourcePatterns,
|
|
698
692
|
// No bare **/lib/** — NEW-ADOPT-LIB-AS-PRESENTATION / NEW-APP-VACUUM-LIB.
|
|
699
693
|
],
|
|
700
694
|
optional: true,
|
package/bin/lib/scan-files.mjs
CHANGED
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import fs from 'node:fs';
|
|
6
6
|
import path from 'node:path';
|
|
7
|
-
import { isScanExcludedRelative } from '../ark-shared.mjs';
|
|
7
|
+
import { isScanExcludedRelative, isSkippedSourceDir } from '../ark-shared.mjs';
|
|
8
|
+
|
|
9
|
+
export { isSkippedSourceDir };
|
|
8
10
|
|
|
9
11
|
export const SOURCE_FILE_NAME = /\.[cm]?[tj]sx?$/;
|
|
10
12
|
|
|
@@ -18,31 +20,6 @@ export function isGovernableSourceFile(name) {
|
|
|
18
20
|
return SOURCE_FILE_NAME.test(name) && !name.endsWith('.d.ts') && !TEST_FILE_NAME.test(name);
|
|
19
21
|
}
|
|
20
22
|
|
|
21
|
-
export function isSkippedSourceDir(name) {
|
|
22
|
-
return (
|
|
23
|
-
name === 'node_modules' ||
|
|
24
|
-
name === 'dist' ||
|
|
25
|
-
name === 'coverage' ||
|
|
26
|
-
name === 'bench' ||
|
|
27
|
-
name === 'benches' ||
|
|
28
|
-
name === 'benchmark' ||
|
|
29
|
-
name === 'benchmarks' ||
|
|
30
|
-
name === 'docs' ||
|
|
31
|
-
name === 'documentation' ||
|
|
32
|
-
name === 'example' ||
|
|
33
|
-
name === 'examples' ||
|
|
34
|
-
name === 'fixture' ||
|
|
35
|
-
name === 'fixtures' ||
|
|
36
|
-
name === 'playground' ||
|
|
37
|
-
name === '__tests__' ||
|
|
38
|
-
name === '__mocks__' ||
|
|
39
|
-
name === 'e2e' ||
|
|
40
|
-
// Top-level style Nest/Jest folders (not "testing" helpers inside src)
|
|
41
|
-
name === 'test' ||
|
|
42
|
-
name === 'tests'
|
|
43
|
-
);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
23
|
function isInsideRoot(root, target) {
|
|
47
24
|
const rel = path.relative(root, target);
|
|
48
25
|
return rel === '' || (!rel.startsWith(`..${path.sep}`) && rel !== '..' && !path.isAbsolute(rel));
|