arkgate 4.8.16 → 4.8.18
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 +81 -0
- package/README.md +10 -7
- package/bin/ark-check-runtime.mjs +5 -1
- package/bin/ark-mcp-runtime.mjs +14 -7
- package/bin/ark-shared.mjs +10 -4
- package/bin/ark.mjs +6 -0
- package/bin/lib/adr-path.mjs +116 -0
- package/bin/lib/adr-presence.mjs +3 -2
- package/bin/lib/agent-gates.mjs +2 -0
- package/bin/lib/analysis-engine.mjs +6 -6
- package/bin/lib/doctor-human.mjs +2 -2
- package/bin/lib/doctor-next-actions.mjs +117 -17
- package/bin/lib/first-run-help.mjs +6 -2
- package/bin/lib/html-report.mjs +1 -1
- package/bin/lib/policy-delta-io.mjs +19 -10
- package/bin/lib/presets.mjs +15 -21
- package/bin/lib/skill-install.mjs +42 -4
- package/bin/lib/start-preview.mjs +75 -3
- package/dist/{diagnosticCatalog-KWvGLI1U.d.ts → diagnosticCatalog-BNxKdcN4.d.ts} +7 -1
- package/dist/index.cjs +9 -9
- package/dist/index.d.ts +2 -2
- package/dist/index.js +11 -11
- package/dist/nestjs/index.cjs +1 -1
- package/dist/nestjs/index.js +1 -1
- package/dist/runtime/index.cjs +6 -6
- package/dist/runtime/index.d.ts +1 -1
- package/dist/runtime/index.js +6 -6
- package/docs/README.md +2 -2
- package/docs/agent-guide.md +9 -2
- package/docs/ai-gates.md +4 -0
- package/docs/configuration.md +6 -3
- package/docs/enthusiast/how-to-agent-gates.md +3 -1
- package/docs/enthusiast/how-to-pick-shape.md +1 -1
- package/docs/package-surface.md +1 -1
- package/docs/use.md +9 -4
- 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 +8 -5
- package/templates/agent-skills/ark-explain/SKILL.md +3 -0
- package/templates/agent-skills/ark-explore/SKILL.md +3 -1
- package/templates/agent-skills/ark-upgrade/SKILL.md +1 -0
- package/templates/skills/ark-adopt.md +8 -5
- package/templates/skills/ark-explain.md +3 -0
- package/templates/skills/ark-explore.md +3 -1
- package/templates/skills/ark-upgrade.md +1 -0
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 {
|
|
@@ -467,7 +467,7 @@ export function printDoctorDetailsHuman(view) {
|
|
|
467
467
|
if (remMiss + remStale > 0) {
|
|
468
468
|
line(
|
|
469
469
|
warn,
|
|
470
|
-
`${remMiss} missing / ${remStale} content-behind-package /ark-* skill(s) for ${remainingGaps.map((g) => g.tool).join(', ')}`
|
|
470
|
+
`${remMiss} missing / ${remStale} content-behind-package /ark-* skill(s) for ${remainingGaps.map((g) => skillGapToolLabel(g.tool)).join(', ')}`
|
|
471
471
|
);
|
|
472
472
|
}
|
|
473
473
|
const codexHomeGap = detectCodexHomeGap(root);
|
|
@@ -2,7 +2,7 @@
|
|
|
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';
|
|
@@ -17,6 +17,99 @@ function missingGateFiles(ctx) {
|
|
|
17
17
|
return list;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
/** Ignore a handful of expected Next API dual-matches. */
|
|
21
|
+
const DUAL_MATCH_REPAIR_FLOOR = 20;
|
|
22
|
+
/** Absolute pile-up even when the tree is huge. */
|
|
23
|
+
const DUAL_MATCH_REPAIR_ABSOLUTE = 50;
|
|
24
|
+
/** Share of in-scope files that match two+ layers. */
|
|
25
|
+
const DUAL_MATCH_REPAIR_SHARE = 0.1;
|
|
26
|
+
|
|
27
|
+
function isWildcardPattern(pattern) {
|
|
28
|
+
return /[*?]/.test(String(pattern ?? ''));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function posixRel(file) {
|
|
32
|
+
return String(file ?? '').split(/\\/).join('/');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function layerPatterns(cov, layerName) {
|
|
36
|
+
const rows = Array.isArray(cov?.layers) ? cov.layers : [];
|
|
37
|
+
const row = rows.find((layer) => layer?.name === layerName);
|
|
38
|
+
return Array.isArray(row?.patterns) ? row.patterns : [];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function fileMatchesWildcardPatterns(file, patterns) {
|
|
42
|
+
const rel = posixRel(file);
|
|
43
|
+
return patterns.some((pattern) => isWildcardPattern(pattern) && globToRegExp(pattern).test(rel));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Domain overlap is a glob leak when Domain matched the sample via a wildcard.
|
|
48
|
+
* Exact-file Domain listings under another layer glob (mother generated CLI)
|
|
49
|
+
* are intentional dual-lists, not a repair. No layer patterns → keep the
|
|
50
|
+
* Domain-sample shortcut so older #269 fixtures still fire.
|
|
51
|
+
*/
|
|
52
|
+
function domainSampleIsGlobLeak(row, cov) {
|
|
53
|
+
if (!(row?.layers ?? []).includes('DomainModel')) return false;
|
|
54
|
+
const patterns = layerPatterns(cov, 'DomainModel');
|
|
55
|
+
if (patterns.length === 0) return true;
|
|
56
|
+
return fileMatchesWildcardPatterns(row.file, patterns);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Overlapping roots from matching Domain wildcards, else sample path prefixes. */
|
|
60
|
+
export function overlappingRootsFromCoverage(cov) {
|
|
61
|
+
const samples = Array.isArray(cov?.dualMembership?.samples) ? cov.dualMembership.samples : [];
|
|
62
|
+
const domainPatterns = layerPatterns(cov, 'DomainModel');
|
|
63
|
+
const fromPatterns = [];
|
|
64
|
+
for (const row of samples) {
|
|
65
|
+
const rel = posixRel(row?.file);
|
|
66
|
+
for (const pattern of domainPatterns) {
|
|
67
|
+
if (isWildcardPattern(pattern) && globToRegExp(pattern).test(rel)) {
|
|
68
|
+
fromPatterns.push(pattern);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (fromPatterns.length > 0) {
|
|
73
|
+
return [...new Set(fromPatterns)].slice(0, 2);
|
|
74
|
+
}
|
|
75
|
+
const derived = [];
|
|
76
|
+
for (const row of samples) {
|
|
77
|
+
const first = posixRel(row?.file).split('/').filter(Boolean)[0];
|
|
78
|
+
if (first) derived.push(`${first}/**`);
|
|
79
|
+
}
|
|
80
|
+
return [...new Set(derived)].slice(0, 2);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Dual-match is a lying layer map when it is large — especially Domain
|
|
85
|
+
* overlapping Presentation/Application after over-broad monorepo start globs.
|
|
86
|
+
* Intentional file+glob dual-lists (exact Domain files under Tooling `bin/**`)
|
|
87
|
+
* do not steal doctor #1.
|
|
88
|
+
*/
|
|
89
|
+
export function dualMatchNeedsGlobRepair(cov) {
|
|
90
|
+
const count = Number(cov?.dualMembership?.count) || 0;
|
|
91
|
+
if (count < DUAL_MATCH_REPAIR_FLOOR) return false;
|
|
92
|
+
const total = Number(cov?.totalFiles ?? cov?.governed?.totalFiles) || 0;
|
|
93
|
+
const samples = Array.isArray(cov?.dualMembership?.samples) ? cov.dualMembership.samples : [];
|
|
94
|
+
const domainOverlap = samples.some((row) => domainSampleIsGlobLeak(row, cov));
|
|
95
|
+
if (domainOverlap) return true;
|
|
96
|
+
if (total > 0 && count / total >= DUAL_MATCH_REPAIR_SHARE) return true;
|
|
97
|
+
return count >= DUAL_MATCH_REPAIR_ABSOLUTE;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function overlappingGlobNextAction(cov) {
|
|
101
|
+
const count = Number(cov?.dualMembership?.count) || 0;
|
|
102
|
+
const sample = Array.isArray(cov?.dualMembership?.samples) ? cov.dualMembership.samples[0] : null;
|
|
103
|
+
const layers = Array.isArray(sample?.layers) ? sample.layers.filter(Boolean) : [];
|
|
104
|
+
const example =
|
|
105
|
+
sample?.file && layers.length > 1
|
|
106
|
+
? `${sample.file} matches ${layers.join(' + ')}`
|
|
107
|
+
: 'the same files match two layer globs';
|
|
108
|
+
const roots = overlappingRootsFromCoverage(cov);
|
|
109
|
+
const rootPhrase = roots.length > 0 ? roots.join(', ') : 'the overlapping globs';
|
|
110
|
+
return `Fix overlapping layer globs — ${count} files match more than one layer (e.g. ${example}). Narrow overlapping roots like ${rootPhrase}. Then /ark-adopt`;
|
|
111
|
+
}
|
|
112
|
+
|
|
20
113
|
export function collectDoctorNextActions(ctx) {
|
|
21
114
|
const actions = [];
|
|
22
115
|
const missingFiles = missingGateFiles(ctx);
|
|
@@ -37,9 +130,26 @@ export function collectDoctorNextActions(ctx) {
|
|
|
37
130
|
if (ctx.layerOwners?.required && ctx.layerOwners.nextAction) {
|
|
38
131
|
actions.push(ctx.layerOwners.nextAction);
|
|
39
132
|
}
|
|
133
|
+
const humanSkillGaps = skillGapsForActiveHost(ctx.skillGaps ?? []);
|
|
134
|
+
const legacyCodex = humanSkillGaps.some((g) => g.tool === 'codex' && g.legacyPromptsOnly);
|
|
135
|
+
const remainingGaps = humanSkillGaps.filter(
|
|
136
|
+
(g) => !(g.tool === 'codex' && (g.legacyPromptsOnly || g.legacyAdvisory))
|
|
137
|
+
);
|
|
138
|
+
const remMiss = remainingGaps.reduce((s, g) => s + g.missing, 0);
|
|
139
|
+
const remStale = remainingGaps.reduce((s, g) => s + g.stale, 0);
|
|
140
|
+
if (legacyCodex) {
|
|
141
|
+
actions.push('install Codex SKILL.md catalog (--install-agent-gates --skills-only --tools codex --force)');
|
|
142
|
+
}
|
|
143
|
+
if (remMiss > 0) {
|
|
144
|
+
actions.push('install missing /ark-* skills (--install-agent-gates --skills-only --force)');
|
|
145
|
+
} else if (remStale > 0) {
|
|
146
|
+
actions.push('refresh stale /ark-* skills (--install-agent-gates --skills-only --force) — gates are installed, catalog is stale');
|
|
147
|
+
}
|
|
40
148
|
const enforceEmptyPlan =
|
|
41
149
|
ctx.operatingMode === 'enforce' && planAEmpty && gatesInstalled && !notAdopted;
|
|
42
|
-
|
|
150
|
+
// Stale/missing doors outrank the Shape nudge — a colleague on an old catalog
|
|
151
|
+
// must see skills-only refresh as #1, not leftover-design explore.
|
|
152
|
+
if (enforceEmptyPlan && remMiss === 0 && remStale === 0 && !legacyCodex) {
|
|
43
153
|
actions.push(
|
|
44
154
|
ctx.postGreenPath?.action ||
|
|
45
155
|
'/ark-explore, then one small refactor with /ark-autopilot and your OK'
|
|
@@ -70,6 +180,8 @@ export function collectDoctorNextActions(ctx) {
|
|
|
70
180
|
if (ctx.coverageHonesty.greenIsNotEnforcement && ctx.coverageHonesty.worseThanNoGate) {
|
|
71
181
|
actions.push('raise governed coverage above a minority slice before treating green as enforcement');
|
|
72
182
|
}
|
|
183
|
+
const overlapAction = dualMatchNeedsGlobRepair(ctx.cov) ? overlappingGlobNextAction(ctx.cov) : null;
|
|
184
|
+
if (overlapAction) actions.push(overlapAction);
|
|
73
185
|
if (ctx.cov.suggestions.length > 0) actions.push('classify the ungoverned directories (/ark-adopt)');
|
|
74
186
|
if (ctx.packageVersionTruth?.dualTruth) {
|
|
75
187
|
actions.push(
|
|
@@ -100,21 +212,6 @@ export function collectDoctorNextActions(ctx) {
|
|
|
100
212
|
: 'Remove the skippable if:, or write .ark/adoption-stance.json with stance: advisory-only')
|
|
101
213
|
);
|
|
102
214
|
}
|
|
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
215
|
if (ctx.codexHomeGap && ctx.codexConcernActive && ctx.codexHomeGap.duplicateHome) {
|
|
119
216
|
actions.push(
|
|
120
217
|
'remove duplicate Codex home /ark-* skills (project .agents/skills is enough): --install-agent-gates --skills-only --prune-home-duplicates'
|
|
@@ -162,6 +259,9 @@ export function collectDoctorNextActions(ctx) {
|
|
|
162
259
|
if (ctx.designFitness.designWeak && unique.length === 0 && ctx.postGreenPath) {
|
|
163
260
|
unique.push(ctx.postGreenPath.action);
|
|
164
261
|
}
|
|
262
|
+
if (overlapAction) {
|
|
263
|
+
return [overlapAction, ...unique.filter((a) => a !== overlapAction)];
|
|
264
|
+
}
|
|
165
265
|
if (notAdopted) {
|
|
166
266
|
const next = ctx.notAdoptedNextAction || NOT_ADOPTED_NEXT_ACTION;
|
|
167
267
|
return [next, ...unique.filter((a) => a !== next)];
|
|
@@ -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).
|
|
@@ -271,7 +273,8 @@ export function checkUsageAll() {
|
|
|
271
273
|
'This merge profile never depends on an editor/agent hook.',
|
|
272
274
|
'When a Git merge base is available, --strict-merge classifies the ark.config.json',
|
|
273
275
|
'transition. Weakening or judgment-required findings fail unless --policy-ack names',
|
|
274
|
-
'every finding
|
|
276
|
+
'every finding, is bound to both policy hashes, and adrPath points at a short note',
|
|
277
|
+
'under docs/adr/ or docs/decisions/. Use --policy-base/--policy-base-ref',
|
|
275
278
|
'for an explicit comparison; ARK_POLICY_BASE_REF is the CI environment equivalent.',
|
|
276
279
|
'The same merge profile blocks new UI business-rule files (domain-logic-in-ui) created',
|
|
277
280
|
'versus that base; leftover design on existing files stays green. Missing base skips',
|
|
@@ -302,7 +305,8 @@ export function checkUsageAll() {
|
|
|
302
305
|
'rule file. Existing files are never overwritten without --force, so re-running',
|
|
303
306
|
'after an update only adds what is missing. --skills-only restricts the write to',
|
|
304
307
|
'just the /ark-* skills (safe to --force-refresh — it leaves a customized AGENTS.md,',
|
|
305
|
-
'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.',
|
|
306
310
|
'Pass --tools to pick which tool configs to write; otherwise they are auto-detected',
|
|
307
311
|
'from their config directories (.claude/, .cursor/, .codex/, .grok/, .windsurf/,',
|
|
308
312
|
'.clinerules/, .kiro/, .roo/, .continue/, .gemini/; copilot is explicit-only).',
|
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) {
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
loadInvariantCoverageInputs,
|
|
10
10
|
} from './invariant-coverage-io.mjs';
|
|
11
11
|
import { evaluateInvariantCoverage } from './invariant-coverage.mjs';
|
|
12
|
+
import { attachPolicyAdrNote } from './adr-path.mjs';
|
|
12
13
|
|
|
13
14
|
function readJsonFile(filePath, label) {
|
|
14
15
|
if (!fs.existsSync(filePath)) throw new Error(`${label} not found: ${filePath}`);
|
|
@@ -196,14 +197,22 @@ export function analyzePolicyTransition({
|
|
|
196
197
|
candidateInvariantCoverage = evaluated.coverage;
|
|
197
198
|
}
|
|
198
199
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
200
|
+
const acknowledgement = readPolicyAcknowledgement(root, acknowledgementPath);
|
|
201
|
+
return attachPolicyAdrNote(
|
|
202
|
+
analyzePolicyDelta({
|
|
203
|
+
baseConfig: base.config,
|
|
204
|
+
candidateConfig,
|
|
205
|
+
acknowledgement,
|
|
206
|
+
baseSource: base.source,
|
|
207
|
+
candidateSource: path.isAbsolute(configPath) ? configPath : path.join(root, configPath),
|
|
208
|
+
...(baseArkRules ? { baseArkRules } : {}),
|
|
209
|
+
candidateArkRules,
|
|
210
|
+
...(candidateInvariantCoverage ? { candidateInvariantCoverage } : {}),
|
|
211
|
+
}),
|
|
212
|
+
{
|
|
213
|
+
root,
|
|
214
|
+
acknowledgement,
|
|
215
|
+
failClosed: Boolean(strictMerge || acknowledgementPath),
|
|
216
|
+
}
|
|
217
|
+
);
|
|
209
218
|
}
|
package/bin/lib/presets.mjs
CHANGED
|
@@ -629,27 +629,21 @@ export const ARCHITECTURE_PRESETS = {
|
|
|
629
629
|
}
|
|
630
630
|
// Turborepo: apps/ + packages/; Nx enterprise: apps/ + libs/ (+ packages/).
|
|
631
631
|
if (include.length === 0) include = ['packages', 'apps', 'libs'];
|
|
632
|
-
//
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
return base === '.' ? null : `${base}/**`;
|
|
648
|
-
}))
|
|
649
|
-
.filter(Boolean);
|
|
650
|
-
};
|
|
651
|
-
const librarySourcePatterns = packagePatterns(['library']);
|
|
652
|
-
const applicationSourcePatterns = packagePatterns(['application', 'cli']);
|
|
632
|
+
// Library Domain bags stay under packages/ or libs/ — never api/** or client/**.
|
|
633
|
+
const roleGlobs = (roles, domainScoped) =>
|
|
634
|
+
!root
|
|
635
|
+
? []
|
|
636
|
+
: units
|
|
637
|
+
.filter((unit) => roles.includes(unit.role) && !(domainScoped && unit.root === '.'))
|
|
638
|
+
.flatMap((unit) =>
|
|
639
|
+
(unit.sourceRoots ?? []).map((sourceRoot) => {
|
|
640
|
+
const base = unit.root === '.' ? sourceRoot : sourceRoot === '.' ? unit.root : `${unit.root}/${sourceRoot}`;
|
|
641
|
+
return base === '.' ? null : `${base}/**`;
|
|
642
|
+
})
|
|
643
|
+
)
|
|
644
|
+
.filter((pattern) => pattern && (!domainScoped || /^(packages|libs)\//.test(pattern)));
|
|
645
|
+
const librarySourcePatterns = roleGlobs(['library'], true);
|
|
646
|
+
const applicationSourcePatterns = roleGlobs(['application', 'cli'], false);
|
|
653
647
|
return presetWithOverlays(
|
|
654
648
|
{
|
|
655
649
|
include,
|
|
@@ -261,8 +261,22 @@ export const SKILL_TOOL_TARGETS = {
|
|
|
261
261
|
windsurf: (name) => `.windsurf/workflows/${name}.md`,
|
|
262
262
|
cline: (name) => `.clinerules/workflows/${name}.md`,
|
|
263
263
|
copilot: (name) => `.github/prompts/${name}.prompt.md`,
|
|
264
|
+
// Catalog-only gap: `.agents/skills` exists without a host marker that reads it.
|
|
265
|
+
agents: (name) => canonicalSkillPath(name),
|
|
264
266
|
};
|
|
265
267
|
|
|
268
|
+
/** Synthetic detectSkillGaps tool when only the canonical catalog is present. */
|
|
269
|
+
export const SKILL_CANONICAL_TOOL = 'agents';
|
|
270
|
+
|
|
271
|
+
export function skillGapToolLabel(tool) {
|
|
272
|
+
return tool === SKILL_CANONICAL_TOOL ? SKILL_CANONICAL_DIR : String(tool ?? '');
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export function skillTargetIsCanonical(tool) {
|
|
276
|
+
const target = SKILL_TOOL_TARGETS[tool];
|
|
277
|
+
return typeof target === 'function' && target('ark-place') === canonicalSkillPath('ark-place');
|
|
278
|
+
}
|
|
279
|
+
|
|
266
280
|
/** Hosts whose catalog is the project `.agents/skills` tree (write once). */
|
|
267
281
|
export function usesCanonicalSkillCatalog(tool) {
|
|
268
282
|
return (
|
|
@@ -1086,10 +1100,32 @@ export function detectSkillGaps(root) {
|
|
|
1086
1100
|
if (fs.existsSync(path.join(root, '.cursor'))) detected.push('cursor');
|
|
1087
1101
|
if (fs.existsSync(path.join(root, '.codex'))) detected.push('codex');
|
|
1088
1102
|
if (fs.existsSync(path.join(root, '.grok'))) detected.push('grok');
|
|
1103
|
+
// Same markers as resolveTools — Antigravity/OpenCode read or link the catalog.
|
|
1104
|
+
if (
|
|
1105
|
+
fs.existsSync(path.join(root, '.agents', 'hooks.json')) ||
|
|
1106
|
+
fs.existsSync(path.join(root, '.agents', 'mcp_config.json'))
|
|
1107
|
+
) {
|
|
1108
|
+
detected.push('antigravity');
|
|
1109
|
+
}
|
|
1110
|
+
if (
|
|
1111
|
+
fs.existsSync(path.join(root, 'opencode.json')) ||
|
|
1112
|
+
fs.existsSync(path.join(root, 'opencode.jsonc')) ||
|
|
1113
|
+
fs.existsSync(path.join(root, '.opencode'))
|
|
1114
|
+
) {
|
|
1115
|
+
detected.push('opencode');
|
|
1116
|
+
}
|
|
1089
1117
|
if (fs.existsSync(path.join(root, '.windsurf'))) detected.push('windsurf');
|
|
1090
1118
|
if (fs.statSync(path.join(root, '.clinerules'), { throwIfNoEntry: false })?.isDirectory()) {
|
|
1091
1119
|
detected.push('cline');
|
|
1092
1120
|
}
|
|
1121
|
+
// One-catalog trees may have `.agents/skills` and no host marker dir.
|
|
1122
|
+
// Without this, a colleague who `npm install`s a new pin keeps stale doors silently.
|
|
1123
|
+
if (
|
|
1124
|
+
fs.existsSync(path.join(root, SKILL_CANONICAL_DIR)) &&
|
|
1125
|
+
!detected.some((tool) => skillTargetIsCanonical(tool))
|
|
1126
|
+
) {
|
|
1127
|
+
detected.push(SKILL_CANONICAL_TOOL);
|
|
1128
|
+
}
|
|
1093
1129
|
const version = arkPackageVersion();
|
|
1094
1130
|
const gaps = [];
|
|
1095
1131
|
for (const tool of detected) {
|
|
@@ -1150,7 +1186,9 @@ export function detectSkillGaps(root) {
|
|
|
1150
1186
|
export function skillGapsForActiveHost(skillGaps, env = process.env) {
|
|
1151
1187
|
const activeHost = detectActiveAgentHost(env);
|
|
1152
1188
|
if (!activeHost) return skillGaps ?? [];
|
|
1153
|
-
return (skillGaps ?? []).filter(
|
|
1189
|
+
return (skillGaps ?? []).filter(
|
|
1190
|
+
(gap) => gap.tool === activeHost || gap.tool === SKILL_CANONICAL_TOOL
|
|
1191
|
+
);
|
|
1154
1192
|
}
|
|
1155
1193
|
|
|
1156
1194
|
/**
|
|
@@ -1174,7 +1212,7 @@ export function printSkillAndCodexGapHints(root, opts) {
|
|
|
1174
1212
|
);
|
|
1175
1213
|
const missingTotal = remaining.reduce((sum, gap) => sum + gap.missing, 0);
|
|
1176
1214
|
const staleTotal = remaining.reduce((sum, gap) => sum + gap.stale, 0);
|
|
1177
|
-
const tools = remaining.map((gap) => gap.tool).join(', ');
|
|
1215
|
+
const tools = remaining.map((gap) => skillGapToolLabel(gap.tool)).join(', ');
|
|
1178
1216
|
if (legacyCodex) {
|
|
1179
1217
|
console.log(
|
|
1180
1218
|
color.yellow(
|
|
@@ -1200,9 +1238,9 @@ export function printSkillAndCodexGapHints(root, opts) {
|
|
|
1200
1238
|
}
|
|
1201
1239
|
if (staleTotal > 0) {
|
|
1202
1240
|
console.log(
|
|
1203
|
-
color.
|
|
1241
|
+
color.yellow(
|
|
1204
1242
|
`${staleTotal} /ark-* skill(s) content behind this Ark package for ${tools}. ` +
|
|
1205
|
-
`Refresh: ${arkCommand(root, 'ark-check', '--install-agent-gates --skills-only --force')}`
|
|
1243
|
+
`Refresh skills only (do not re-adopt): ${arkCommand(root, 'ark-check', '--install-agent-gates --skills-only --force')}`
|
|
1206
1244
|
)
|
|
1207
1245
|
);
|
|
1208
1246
|
}
|
|
@@ -122,6 +122,73 @@ export function setupBudget(changes) {
|
|
|
122
122
|
};
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
+
/** One next write when compact start is the wrong size. `--force` does not unlock. */
|
|
126
|
+
export const START_SETUP_BUDGET_NEXT = 'arkgate-check --init';
|
|
127
|
+
|
|
128
|
+
function formatSetupKb(bytes) {
|
|
129
|
+
return `${Math.max(1, Math.round(Number(bytes) / 1024))} KB`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Compact-start size lock. `--force` unlocks coverage/shape only — never this.
|
|
134
|
+
* @param {object|null|undefined} setupBudget
|
|
135
|
+
* @param {{ force?: boolean }} [options]
|
|
136
|
+
*/
|
|
137
|
+
export function evaluateStartSetupBudgetGate(setupBudget, { force = false } = {}) {
|
|
138
|
+
if (!setupBudget || setupBudget.ok) {
|
|
139
|
+
return { ok: true, forceIgnored: Boolean(force) };
|
|
140
|
+
}
|
|
141
|
+
const reasons = [];
|
|
142
|
+
const maxFiles = setupBudget.maxFiles ?? 8;
|
|
143
|
+
const maxBytes = setupBudget.maxBytes ?? 32 * 1024;
|
|
144
|
+
const gateFiles =
|
|
145
|
+
typeof setupBudget.gateFiles === 'number' ? setupBudget.gateFiles : setupBudget.files;
|
|
146
|
+
if (typeof gateFiles === 'number' && gateFiles > maxFiles) {
|
|
147
|
+
reasons.push(`${gateFiles} gate files (max ${maxFiles})`);
|
|
148
|
+
}
|
|
149
|
+
if (typeof setupBudget.bytes === 'number' && setupBudget.bytes >= maxBytes) {
|
|
150
|
+
reasons.push(`${formatSetupKb(setupBudget.bytes)} (max ${formatSetupKb(maxBytes)})`);
|
|
151
|
+
}
|
|
152
|
+
if (reasons.length === 0) {
|
|
153
|
+
reasons.push('the planned write is larger than compact start allows');
|
|
154
|
+
}
|
|
155
|
+
return {
|
|
156
|
+
ok: false,
|
|
157
|
+
reasons,
|
|
158
|
+
nextAction: START_SETUP_BUDGET_NEXT,
|
|
159
|
+
forceBypasses: false,
|
|
160
|
+
setupBudget,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Human refuse / preview warning. Avoids the first-run phrase "Compact setup budget".
|
|
166
|
+
* @param {object|null|undefined} setupBudget
|
|
167
|
+
* @param {{ applying?: boolean }} [options]
|
|
168
|
+
*/
|
|
169
|
+
export function formatStartSetupBudgetRefuse(setupBudget, { applying = true } = {}) {
|
|
170
|
+
const gate = evaluateStartSetupBudgetGate(setupBudget);
|
|
171
|
+
if (gate.ok) return '';
|
|
172
|
+
const facts = gate.reasons.map((reason) => ` • ${reason}`).join('\n');
|
|
173
|
+
const next = `--force does not unlock this. Next: ${gate.nextAction}`;
|
|
174
|
+
if (applying) {
|
|
175
|
+
return `Refusing ark start --apply: this plan is too big for compact start.\n${facts}\n${next}`;
|
|
176
|
+
}
|
|
177
|
+
return `This plan is too big for compact start.\n${facts}\nApply will refuse. ${next}`;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Apply-path emit: JSON envelope or human refuse. Call before any “writing…” copy. */
|
|
181
|
+
export function emitStartSetupBudgetRefuse(preview, json) {
|
|
182
|
+
const gate = evaluateStartSetupBudgetGate(preview.setupBudget);
|
|
183
|
+
if (json) {
|
|
184
|
+
console.log(
|
|
185
|
+
JSON.stringify({ ok: false, error: 'start-setup-budget-gate', ...gate, preview }, null, 2)
|
|
186
|
+
);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
console.error(formatStartSetupBudgetRefuse(preview.setupBudget, { applying: true }));
|
|
190
|
+
}
|
|
191
|
+
|
|
125
192
|
function commands(root, args, helpers) {
|
|
126
193
|
if (args.removeHost) {
|
|
127
194
|
return [`ark start --root ${root} --tools ${args.removeHost} --apply`];
|
|
@@ -160,6 +227,7 @@ export function formatStartPackageInstallFailure({ exitStatus, installCommand })
|
|
|
160
227
|
*/
|
|
161
228
|
export function renderStartPreview(preview, options = {}) {
|
|
162
229
|
const applying = options.applying === true;
|
|
230
|
+
const budgetOk = preview.setupBudget?.ok !== false;
|
|
163
231
|
if (applying) {
|
|
164
232
|
console.log(
|
|
165
233
|
preview.changes.length === 0
|
|
@@ -170,7 +238,11 @@ export function renderStartPreview(preview, options = {}) {
|
|
|
170
238
|
console.log('Ark start preview — no files were changed.');
|
|
171
239
|
}
|
|
172
240
|
if (!applying) {
|
|
173
|
-
|
|
241
|
+
if (budgetOk) {
|
|
242
|
+
console.log('Apply this plan with: arkgate start --apply');
|
|
243
|
+
} else {
|
|
244
|
+
console.log(formatStartSetupBudgetRefuse(preview.setupBudget, { applying: false }));
|
|
245
|
+
}
|
|
174
246
|
}
|
|
175
247
|
if (preview.analysis) {
|
|
176
248
|
console.log(`Your project looks like: ${preview.analysis.label}.`);
|
|
@@ -208,8 +280,8 @@ export function renderStartPreview(preview, options = {}) {
|
|
|
208
280
|
}
|
|
209
281
|
|
|
210
282
|
export function applyStartPreview(root, preview) {
|
|
211
|
-
if (!preview.setupBudget
|
|
212
|
-
throw new Error(
|
|
283
|
+
if (!evaluateStartSetupBudgetGate(preview.setupBudget).ok) {
|
|
284
|
+
throw new Error(formatStartSetupBudgetRefuse(preview.setupBudget, { applying: true }));
|
|
213
285
|
}
|
|
214
286
|
for (const change of preview.changes) {
|
|
215
287
|
const target = path.join(root, change.path);
|
|
@@ -392,7 +392,7 @@ declare function createAdapterResult(input: {
|
|
|
392
392
|
}): CurrentAdapterResult;
|
|
393
393
|
|
|
394
394
|
/** ArkGate library version — single source of truth. */
|
|
395
|
-
declare const version = "4.8.
|
|
395
|
+
declare const version = "4.8.18";
|
|
396
396
|
|
|
397
397
|
/**
|
|
398
398
|
* AI Code Gate (basic).
|
|
@@ -1971,6 +1971,12 @@ type PolicyDeltaAcknowledgement = {
|
|
|
1971
1971
|
candidatePolicyHash: string;
|
|
1972
1972
|
findingIds: readonly string[];
|
|
1973
1973
|
reason: string;
|
|
1974
|
+
/**
|
|
1975
|
+
* Relative decision-note path under a conventional home (`docs/adr/`,
|
|
1976
|
+
* `docs/decisions/`, or the AP01 single-file homes). Tooling checks the file.
|
|
1977
|
+
* Hash matching does not require it.
|
|
1978
|
+
*/
|
|
1979
|
+
adrPath?: string;
|
|
1974
1980
|
};
|
|
1975
1981
|
type ClassifyArkPolicyDeltaOptions = {
|
|
1976
1982
|
baseArkRules?: EffectiveArkRules;
|