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 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: prepared** (npm `latest` remains **4.8.16** until Actions `publish-npm`
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.17** is prepared on this tree; npm `latest` remains **4.8.16** until `publish-npm` for `v4.8.17`.
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.17 prepared](CHANGELOG.md) · [4.8.11 published](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)
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%). That lock is
80
- deliberate. Lock the shape with `--archetype <id>`, `--preset <name>`, or
81
- `--force`, or inspect ranked shapes with `npx arkgate-check --recommend`.
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
- | Prepared (4.8.17; not published) | [CHANGELOG](CHANGELOG.md) |
348
- | Current published (4.8.11 on npm `latest`) | [docs/releases/4.8.11.md](docs/releases/4.8.11.md) · [CHANGELOG](CHANGELOG.md) |
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) |
@@ -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
- firstCommand: `${arkCommand(root, 'ark', `start --apply --archetype ${result.archetype}`)}`,
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 the coverage·confidence gate may refuse apply. */
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% or weak shape), that lock is deliberate.',
1823
- 'Lock the shape: --archetype <id> · --preset <name> · --force',
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);
@@ -106,6 +106,8 @@ export {
106
106
  assessSkillCatalogParity,
107
107
  detectSkillGaps,
108
108
  skillGapsForActiveHost,
109
+ skillGapToolLabel,
110
+ SKILL_CANONICAL_TOOL,
109
111
  agentsMdSkillRefs,
110
112
  verifyHostSkillCatalog,
111
113
  printSkillAndCodexGapHints,
@@ -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
- if (enforceEmptyPlan) {
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).',
@@ -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) — run ark upgrade / --install-agent-gates</div>`
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) {
@@ -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
- // Established workspaces often have flat package source roots rather than a
633
- // directory named domain/application. The package manifest supplies a real,
634
- // reviewable role signal: a published library is a domain surface; an app or
635
- // CLI package coordinates work. This is deliberately limited to package roots
636
- // Ark already discovered, never a catch-all **/src/** fallback.
637
- const packagePatterns = (roles) => {
638
- if (!root) return [];
639
- return units
640
- .filter((unit) => roles.includes(unit.role))
641
- .flatMap((unit) => unit.sourceRoots.map((sourceRoot) => {
642
- const base = unit.root === '.'
643
- ? sourceRoot
644
- : sourceRoot === '.'
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']);
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((gap) => gap.tool === activeHost);
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.dim(
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
- console.log('Apply this plan with: arkgate start --apply');
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?.ok) {
212
- throw new Error('Refusing to apply a start plan that exceeds the compact setup budget.');
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);