arkgate 4.8.17 → 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 +57 -2
- package/README.md +10 -7
- package/bin/ark-shared.mjs +10 -4
- package/bin/ark.mjs +6 -0
- package/bin/lib/agent-gates.mjs +2 -0
- package/bin/lib/doctor-human.mjs +2 -2
- package/bin/lib/doctor-next-actions.mjs +117 -17
- package/bin/lib/first-run-help.mjs +4 -1
- package/bin/lib/html-report.mjs +1 -1
- 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-DVABouBV.d.ts → diagnosticCatalog-BNxKdcN4.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 +8 -1
- package/docs/ai-gates.md +4 -0
- package/docs/enthusiast/how-to-pick-shape.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 +4 -3
- package/templates/agent-skills/ark-explain/SKILL.md +3 -0
- package/templates/agent-skills/ark-upgrade/SKILL.md +1 -0
- package/templates/skills/ark-adopt.md +4 -3
- package/templates/skills/ark-explain.md +3 -0
- package/templates/skills/ark-upgrade.md +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -96,6 +96,62 @@ in the immutable pre-2.0 archive linked below.
|
|
|
96
96
|
Existing configs keep whatever URL they already have. Editor completion
|
|
97
97
|
matches the 4.x line you just installed.
|
|
98
98
|
|
|
99
|
+
## 4.8.18 — 2026-09-18
|
|
100
|
+
|
|
101
|
+
**Patch** over **4.8.17**. Daily accumulate (2026-09-18 ART) of the six
|
|
102
|
+
ships that landed after 4.8.17 published: monorepo DomainModel globs
|
|
103
|
+
([#269](https://github.com/pedroknigge/arkgate/pull/269)), packed
|
|
104
|
+
README latest-truth
|
|
105
|
+
([#270](https://github.com/pedroknigge/arkgate/issues/270) /
|
|
106
|
+
[#272](https://github.com/pedroknigge/arkgate/pull/272)), deep pstack
|
|
107
|
+
audit ([#273](https://github.com/pedroknigge/arkgate/pull/273)), doctor
|
|
108
|
+
#1 dual-match honesty
|
|
109
|
+
([#275](https://github.com/pedroknigge/arkgate/pull/275)), compact-start
|
|
110
|
+
size refuse ([#274](https://github.com/pedroknigge/arkgate/pull/274)),
|
|
111
|
+
and stale-skill Guiar
|
|
112
|
+
([#278](https://github.com/pedroknigge/arkgate/pull/278)).
|
|
113
|
+
**Write. Check. Ship.** **No required config migration.** No
|
|
114
|
+
`schemaVersion` bump. Does not close `K01` / `Z09`. This mother
|
|
115
|
+
`ark.config.json` still does **not** turn `arkOrder` on.
|
|
116
|
+
|
|
117
|
+
**Status: published** (npm `latest` is **4.8.18**).
|
|
118
|
+
|
|
119
|
+
### Added
|
|
120
|
+
- Doctor and `ark-check` warn when installed `/ark-*` skills are behind
|
|
121
|
+
this package (canonical `.agents/skills` included, even with no host
|
|
122
|
+
marker dir). Primary next action is skills-only refresh
|
|
123
|
+
(`--install-agent-gates --skills-only --force`) — not a full re-adopt
|
|
124
|
+
([#278](https://github.com/pedroknigge/arkgate/pull/278)).
|
|
125
|
+
- Maintainer pstack audit report (core + add-ons) at
|
|
126
|
+
`docs/audit/2026-09-17-pstack-deep-product-audit.md`
|
|
127
|
+
([#273](https://github.com/pedroknigge/arkgate/pull/273)). Report
|
|
128
|
+
only. No product code.
|
|
129
|
+
|
|
130
|
+
### Fixed
|
|
131
|
+
- Monorepo `arkgate start` no longer puts whole-app roots (`api/**`,
|
|
132
|
+
`client/**`) on DomainModel next to the finer globs. Domain stays
|
|
133
|
+
package-scoped (`packages/*/src/**`, `**/domain/**`). When dual-match
|
|
134
|
+
is huge, doctor #1 is “fix overlapping layer globs” with one example,
|
|
135
|
+
not only “make CI required”
|
|
136
|
+
([#269](https://github.com/pedroknigge/arkgate/pull/269)).
|
|
137
|
+
- Packed README / docs hub no longer pin npm `latest` to an older
|
|
138
|
+
version than this tarball. `check:package-files` and the
|
|
139
|
+
packed-artifact path refuse that waiting-room pin
|
|
140
|
+
([#270](https://github.com/pedroknigge/arkgate/issues/270) /
|
|
141
|
+
[#272](https://github.com/pedroknigge/arkgate/pull/272)).
|
|
142
|
+
- Doctor #1 no longer treats intentional Domain+Tooling file+glob
|
|
143
|
+
dual-lists as a glob leak, and overlapping-glob copy names the tree's
|
|
144
|
+
real roots instead of a canned `api/**` sermon
|
|
145
|
+
([#275](https://github.com/pedroknigge/arkgate/pull/275)).
|
|
146
|
+
- `doctorResidentWarm` CI ceiling is 800ms after docs-only main already
|
|
147
|
+
missed 680ms (p95 697–707ms). Same miss as #270 / the audit tip
|
|
148
|
+
([#275](https://github.com/pedroknigge/arkgate/pull/275)).
|
|
149
|
+
- Cold `arkgate start --apply` now refuses a plan that is too big for
|
|
150
|
+
compact start **before** any “writing…” copy, names the file/byte
|
|
151
|
+
numbers, and prints one next action (`arkgate-check --init`).
|
|
152
|
+
`--force` does not unlock the size lock
|
|
153
|
+
([#274](https://github.com/pedroknigge/arkgate/pull/274)).
|
|
154
|
+
|
|
99
155
|
## 4.8.17 — 2026-09-17
|
|
100
156
|
|
|
101
157
|
**Patch** over **4.8.16**. Daily accumulate of the two ships that landed
|
|
@@ -107,8 +163,7 @@ policy-delta-ack-match confidence retarget
|
|
|
107
163
|
`schemaVersion` bump. Does not close `K01` / `Z09`. This mother
|
|
108
164
|
`ark.config.json` still does **not** turn `arkOrder` on.
|
|
109
165
|
|
|
110
|
-
**Status:
|
|
111
|
-
runs with `tag=v4.8.17` and `dry_run=false`).
|
|
166
|
+
**Status: published** (npm `latest` is **4.8.17**).
|
|
112
167
|
|
|
113
168
|
### Added
|
|
114
169
|
- A weaken, new layer, or new allow edge must name a short ADR /
|
package/README.md
CHANGED
|
@@ -39,7 +39,7 @@ Works with Cursor, Claude, Codex, and Grok.
|
|
|
39
39
|
|
|
40
40
|
</div>
|
|
41
41
|
|
|
42
|
-
> **ArkGate 4.8.
|
|
42
|
+
> **ArkGate 4.8.18** is on npm `latest`.
|
|
43
43
|
> Write. Check. Ship. Adopted = required GitHub
|
|
44
44
|
> status running `arkgate-check --strict-merge`, or an explicit `advisory-only` stance.
|
|
45
45
|
> Status is compact (`arkgate-check --doctor`; `--all` for Details). Optional **ArkRun**
|
|
@@ -47,7 +47,7 @@ Works with Cursor, Claude, Codex, and Grok.
|
|
|
47
47
|
> (`arkgate/order`) stops rewriting a big product choice — like the billing plan —
|
|
48
48
|
> as if it were a seat count. Change those choices through a valve, not a generic update.
|
|
49
49
|
> `@arkgate/runtime` is deprecated.
|
|
50
|
-
> [4.8.
|
|
50
|
+
> [4.8.18](CHANGELOG.md) · [4.8.11](docs/releases/4.8.11.md) · [4.8.10](docs/releases/4.8.10.md) · [4.8.9](docs/releases/4.8.9.md) · [4.8.8](docs/releases/4.8.8.md) · [4.8.7](docs/releases/4.8.7.md) · [4.8.6](docs/releases/4.8.6.md) · [4.8.5](docs/releases/4.8.5.md) · [4.8.4](docs/releases/4.8.4.md) · [4.8.3](docs/releases/4.8.3.md) · [4.8.2](docs/releases/4.8.2.md) · [4.8.1](docs/releases/4.8.1.md) · [4.8.0](docs/releases/4.8.0.md) · [Docs hub](docs/README.md) · [Voice](docs/product-voice.md)
|
|
51
51
|
|
|
52
52
|
---
|
|
53
53
|
|
|
@@ -76,9 +76,12 @@ npx --package=arkgate arkgate-check --doctor --all # full details
|
|
|
76
76
|
`arkgate-check` is a command in the `arkgate` package, not its own npm package. After a local install, `npx arkgate-check --doctor` also works.
|
|
77
77
|
|
|
78
78
|
`start --apply` refuses when projected governed coverage is below 50% or
|
|
79
|
-
shape confidence is weak (below 0.6 with coverage under 80%)
|
|
80
|
-
|
|
81
|
-
|
|
79
|
+
shape confidence is weak (below 0.6 with coverage under 80%), or when the
|
|
80
|
+
planned write is too big for compact start (more than 8 gate files or 32 KB).
|
|
81
|
+
That lock is deliberate. Coverage/shape: lock with `--archetype <id>`,
|
|
82
|
+
`--preset <name>`, or `--force`. Size: `--force` does not unlock. Next:
|
|
83
|
+
`npx arkgate-check --init`. Or inspect ranked shapes with
|
|
84
|
+
`npx arkgate-check --recommend`.
|
|
82
85
|
|
|
83
86
|
That is the product. Stuck? Run status (`--doctor`) and do action **#1**.
|
|
84
87
|
|
|
@@ -344,8 +347,8 @@ Compact starters leave the extra off. Details: [ArkOrder](docs/arkorder.md).
|
|
|
344
347
|
| Config · package surface · TS | [configuration](docs/configuration.md) · [package-surface](docs/package-surface.md) · [typescript-support](docs/typescript-support.md) |
|
|
345
348
|
| Brownfield | [docs/brownfield-adoption.md](docs/brownfield-adoption.md) |
|
|
346
349
|
| Security | [SECURITY.md](SECURITY.md) |
|
|
347
|
-
|
|
|
348
|
-
|
|
|
350
|
+
| Current published (4.8.18 on npm `latest`) | [CHANGELOG](CHANGELOG.md) |
|
|
351
|
+
| Prior published (4.8.11) | [docs/releases/4.8.11.md](docs/releases/4.8.11.md) · [CHANGELOG](CHANGELOG.md) |
|
|
349
352
|
| Prior published (4.8.10) | [docs/releases/4.8.10.md](docs/releases/4.8.10.md) · [CHANGELOG](CHANGELOG.md) |
|
|
350
353
|
| Prior published (4.8.9) | [docs/releases/4.8.9.md](docs/releases/4.8.9.md) · [CHANGELOG](CHANGELOG.md) |
|
|
351
354
|
| Prior published (4.8.7) | [docs/releases/4.8.7.md](docs/releases/4.8.7.md) · [CHANGELOG](CHANGELOG.md) |
|
package/bin/ark-shared.mjs
CHANGED
|
@@ -1740,7 +1740,12 @@ export function buildArchitectureRecommendation(root, options = {}) {
|
|
|
1740
1740
|
// a thin slice and can mis-flag framework internals, so steer these to the adoption flow.
|
|
1741
1741
|
mature: signals.sourceFileCount >= MATURE_REPO_FILE_THRESHOLD,
|
|
1742
1742
|
initCommand: `${arkCommand(root, 'ark', `init --archetype ${result.archetype} --yes`)}`,
|
|
1743
|
-
|
|
1743
|
+
// Greenfield: compact start. Mature: `--init` writes the rules file without the
|
|
1744
|
+
// compact-start size lock that cold `start --apply` can hit on large trees.
|
|
1745
|
+
firstCommand:
|
|
1746
|
+
signals.sourceFileCount >= MATURE_REPO_FILE_THRESHOLD
|
|
1747
|
+
? arkCommand(root, 'ark-check', '--init')
|
|
1748
|
+
: `${arkCommand(root, 'ark', `start --apply --archetype ${result.archetype}`)}`,
|
|
1744
1749
|
adoptCommand: arkCommand(root, 'ark-check', '--recommend --write-plan'),
|
|
1745
1750
|
recommendCommand: arkCommand(root, 'ark-check', '--recommend'),
|
|
1746
1751
|
checkCommand: arkCommand(root, 'ark-check', '--root . --config ark.config.json --strict-config'),
|
|
@@ -1817,10 +1822,11 @@ export function resolveStartInitPreset(root, rec = {}, archetype = rec.archetype
|
|
|
1817
1822
|
return preset ?? null;
|
|
1818
1823
|
}
|
|
1819
1824
|
|
|
1820
|
-
/** One-minute / preview footer when
|
|
1825
|
+
/** One-minute / preview footer when apply may refuse (coverage·shape or compact size). */
|
|
1821
1826
|
export const START_APPLY_REFUSE_FOOTER = [
|
|
1822
|
-
'If apply refuses (coverage below 50
|
|
1823
|
-
'
|
|
1827
|
+
'If apply refuses (coverage below 50%, weak shape, or too big for compact start), that lock is deliberate.',
|
|
1828
|
+
'Coverage/shape: --archetype <id> · --preset <name> · --force',
|
|
1829
|
+
'Too big for compact start: --force does not unlock. Next: arkgate-check --init',
|
|
1824
1830
|
'Inspect ranked shapes: arkgate-check --recommend',
|
|
1825
1831
|
].join('\n');
|
|
1826
1832
|
|
package/bin/ark.mjs
CHANGED
|
@@ -24,6 +24,8 @@ import { pinArkgateDevDependency, FALSE_GREEN_GAP_ID } from './lib/field-install
|
|
|
24
24
|
import { validateHardWriteRequest } from './lib/enforcement-profiles.mjs';
|
|
25
25
|
import {
|
|
26
26
|
applyStartPreview,
|
|
27
|
+
emitStartSetupBudgetRefuse,
|
|
28
|
+
evaluateStartSetupBudgetGate,
|
|
27
29
|
formatStartPackageInstallFailure,
|
|
28
30
|
planStart,
|
|
29
31
|
renderStartPreview,
|
|
@@ -470,6 +472,10 @@ async function start(args) {
|
|
|
470
472
|
}
|
|
471
473
|
return 2;
|
|
472
474
|
}
|
|
475
|
+
if (!evaluateStartSetupBudgetGate(preview.setupBudget).ok) {
|
|
476
|
+
emitStartSetupBudgetRefuse(preview, args.json);
|
|
477
|
+
return 2;
|
|
478
|
+
}
|
|
473
479
|
}
|
|
474
480
|
if (args.json) console.log(JSON.stringify(preview, null, 2));
|
|
475
481
|
else if (!args.apply) renderStartPreview(preview);
|
package/bin/lib/agent-gates.mjs
CHANGED
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).
|
|
@@ -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/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) {
|
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);
|