arkgate 4.8.17 → 4.8.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/CHANGELOG.md +109 -2
  2. package/README.md +12 -10
  3. package/bin/ark-mcp-runtime.mjs +3 -3
  4. package/bin/ark-shared.mjs +197 -20
  5. package/bin/ark.mjs +24 -18
  6. package/bin/lib/agent-gates.mjs +2 -0
  7. package/bin/lib/ci-and-commands.mjs +2 -2
  8. package/bin/lib/doctor-human.mjs +4 -2
  9. package/bin/lib/doctor-next-actions.mjs +152 -17
  10. package/bin/lib/doctor-plan.mjs +6 -8
  11. package/bin/lib/first-run-help.mjs +4 -1
  12. package/bin/lib/gate-files.mjs +22 -1
  13. package/bin/lib/host-support-matrix.mjs +1 -1
  14. package/bin/lib/html-report-depth.mjs +1 -1
  15. package/bin/lib/html-report.mjs +1 -1
  16. package/bin/lib/install-migrate.mjs +3 -4
  17. package/bin/lib/managed-upgrade.mjs +30 -5
  18. package/bin/lib/mcp-adoption.mjs +4 -1
  19. package/bin/lib/presets.mjs +15 -21
  20. package/bin/lib/scan-files.mjs +3 -26
  21. package/bin/lib/skill-install.mjs +45 -6
  22. package/bin/lib/start-preview.mjs +135 -8
  23. package/bin/lib/write-path-detect.mjs +3 -2
  24. package/dist/{diagnosticCatalog-DVABouBV.d.ts → diagnosticCatalog-D2v0ASN2.d.ts} +1 -1
  25. package/dist/index.cjs +1 -1
  26. package/dist/index.d.ts +2 -2
  27. package/dist/index.js +1 -1
  28. package/dist/nestjs/index.cjs +1 -1
  29. package/dist/nestjs/index.js +1 -1
  30. package/dist/runtime/index.cjs +1 -1
  31. package/dist/runtime/index.d.ts +1 -1
  32. package/dist/runtime/index.js +1 -1
  33. package/docs/README.md +2 -2
  34. package/docs/agent-guide.md +11 -2
  35. package/docs/ai-gates.md +11 -3
  36. package/docs/develop.md +1 -2
  37. package/docs/enthusiast/how-to-pick-shape.md +1 -1
  38. package/docs/use.md +10 -6
  39. package/package.json +1 -1
  40. package/server.json +2 -2
  41. package/templates/agent-skills/README.md +1 -1
  42. package/templates/agent-skills/ark-adopt/SKILL.md +4 -3
  43. package/templates/agent-skills/ark-explain/SKILL.md +3 -0
  44. package/templates/agent-skills/ark-place/SKILL.md +2 -0
  45. package/templates/agent-skills/ark-upgrade/SKILL.md +1 -0
  46. package/templates/hooks/opencode-ark-write-gate.mjs +2 -2
  47. package/templates/skills/ark-adopt.md +4 -3
  48. package/templates/skills/ark-explain.md +3 -0
  49. package/templates/skills/ark-place.md +2 -0
  50. package/templates/skills/ark-upgrade.md +1 -0
package/CHANGELOG.md CHANGED
@@ -96,6 +96,114 @@ 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.19 — 2026-09-20
100
+
101
+ **Patch** over **4.8.18**. Daily accumulate (2026-09-20 ART) of the five
102
+ ships that landed after 4.8.18 published: pre-hook write gate first
103
+ ([#280](https://github.com/pedroknigge/arkgate/pull/280)), honest
104
+ workspace evidence + playground exclude
105
+ ([#286](https://github.com/pedroknigge/arkgate/pull/286)), skill stamp
106
+ refresh ([#285](https://github.com/pedroknigge/arkgate/pull/285)),
107
+ honest start docs and install-fail next step
108
+ ([#287](https://github.com/pedroknigge/arkgate/pull/287)), and honest
109
+ recommend label when `uiHeavy` is true
110
+ ([#289](https://github.com/pedroknigge/arkgate/pull/289)).
111
+ **Write. Check. Ship.** **No required config migration.** No
112
+ `schemaVersion` bump. Does not close `K01` / `Z09`. This mother
113
+ `ark.config.json` still does **not** turn `arkOrder` on.
114
+
115
+ **Status: published** (npm `latest` is **4.8.19**).
116
+
117
+ ### Changed
118
+ - Write-gate order: install and trust the host pre-hook first. MCP
119
+ `ark_prepare_write` is fallback when that path is missing or fail-open.
120
+ Compact Cursor now requires the fail-closed hook (MCP alone is not
121
+ enough). Doctor / start / host matrix name the fallback
122
+ ([#280](https://github.com/pedroknigge/arkgate/pull/280)).
123
+
124
+ ### Fixed
125
+ - Stranger-path `--recommend` treats `pnpm-workspace.yaml` (even without
126
+ `package.json#workspaces`) as workspace evidence, and no longer says
127
+ "not a workspace monorepo" on a workspace tree. Start / recommend skip
128
+ playground, fixture, and scaffold trees the same way the check walk
129
+ does ([#286](https://github.com/pedroknigge/arkgate/pull/286)).
130
+ - Managed upgrade refreshes a skill's `arkVersion` / `arkgate@X.Y.Z`
131
+ description stamp when the body is unchanged (`state: current`).
132
+ Frozen redirect stubs no longer keep a stale picker version after a
133
+ pin bump. Reuses `planSkillInstall` `stamp-refresh`;
134
+ `summary.metadataRefresh` counts those writes
135
+ ([#285](https://github.com/pedroknigge/arkgate/pull/285) /
136
+ [#284](https://github.com/pedroknigge/arkgate/issues/284)).
137
+ - Stranger start path no longer leads with `npm install -D arkgate`
138
+ (hard-fail on `workspace:*`). Docs lead with `npx arkgate start`;
139
+ `start --apply` already pins with `pnpm add -w` / `yarn add -W`.
140
+ pnpm `minimumReleaseAge` recovery is a one-liner. Doctor after a
141
+ failed install leads with resolve the package, not "make CI required"
142
+ ([#287](https://github.com/pedroknigge/arkgate/pull/287) /
143
+ [#268](https://github.com/pedroknigge/arkgate/issues/268)).
144
+ - `start` / `--recommend` no longer labels a UI monorepo as "API server
145
+ without UI in this repository" when `uiHeavy` is true. The one-minute
146
+ label becomes "API-heavy monorepo with UI packages". Default monorepo
147
+ start parks `web/**` / `web/src/**` on PresentationAdapters
148
+ ([#289](https://github.com/pedroknigge/arkgate/pull/289) /
149
+ [#288](https://github.com/pedroknigge/arkgate/issues/288)).
150
+
151
+ ## 4.8.18 — 2026-09-18
152
+
153
+ **Patch** over **4.8.17**. Daily accumulate (2026-09-18 ART) of the six
154
+ ships that landed after 4.8.17 published: monorepo DomainModel globs
155
+ ([#269](https://github.com/pedroknigge/arkgate/pull/269)), packed
156
+ README latest-truth
157
+ ([#270](https://github.com/pedroknigge/arkgate/issues/270) /
158
+ [#272](https://github.com/pedroknigge/arkgate/pull/272)), deep pstack
159
+ audit ([#273](https://github.com/pedroknigge/arkgate/pull/273)), doctor
160
+ #1 dual-match honesty
161
+ ([#275](https://github.com/pedroknigge/arkgate/pull/275)), compact-start
162
+ size refuse ([#274](https://github.com/pedroknigge/arkgate/pull/274)),
163
+ and stale-skill Guiar
164
+ ([#278](https://github.com/pedroknigge/arkgate/pull/278)).
165
+ **Write. Check. Ship.** **No required config migration.** No
166
+ `schemaVersion` bump. Does not close `K01` / `Z09`. This mother
167
+ `ark.config.json` still does **not** turn `arkOrder` on.
168
+
169
+ **Status: published** (npm `latest` is **4.8.18**).
170
+
171
+ ### Added
172
+ - Doctor and `ark-check` warn when installed `/ark-*` skills are behind
173
+ this package (canonical `.agents/skills` included, even with no host
174
+ marker dir). Primary next action is skills-only refresh
175
+ (`--install-agent-gates --skills-only --force`) — not a full re-adopt
176
+ ([#278](https://github.com/pedroknigge/arkgate/pull/278)).
177
+ - Maintainer pstack audit report (core + add-ons) at
178
+ `docs/audit/2026-09-17-pstack-deep-product-audit.md`
179
+ ([#273](https://github.com/pedroknigge/arkgate/pull/273)). Report
180
+ only. No product code.
181
+
182
+ ### Fixed
183
+ - Monorepo `arkgate start` no longer puts whole-app roots (`api/**`,
184
+ `client/**`) on DomainModel next to the finer globs. Domain stays
185
+ package-scoped (`packages/*/src/**`, `**/domain/**`). When dual-match
186
+ is huge, doctor #1 is “fix overlapping layer globs” with one example,
187
+ not only “make CI required”
188
+ ([#269](https://github.com/pedroknigge/arkgate/pull/269)).
189
+ - Packed README / docs hub no longer pin npm `latest` to an older
190
+ version than this tarball. `check:package-files` and the
191
+ packed-artifact path refuse that waiting-room pin
192
+ ([#270](https://github.com/pedroknigge/arkgate/issues/270) /
193
+ [#272](https://github.com/pedroknigge/arkgate/pull/272)).
194
+ - Doctor #1 no longer treats intentional Domain+Tooling file+glob
195
+ dual-lists as a glob leak, and overlapping-glob copy names the tree's
196
+ real roots instead of a canned `api/**` sermon
197
+ ([#275](https://github.com/pedroknigge/arkgate/pull/275)).
198
+ - `doctorResidentWarm` CI ceiling is 800ms after docs-only main already
199
+ missed 680ms (p95 697–707ms). Same miss as #270 / the audit tip
200
+ ([#275](https://github.com/pedroknigge/arkgate/pull/275)).
201
+ - Cold `arkgate start --apply` now refuses a plan that is too big for
202
+ compact start **before** any “writing…” copy, names the file/byte
203
+ numbers, and prints one next action (`arkgate-check --init`).
204
+ `--force` does not unlock the size lock
205
+ ([#274](https://github.com/pedroknigge/arkgate/pull/274)).
206
+
99
207
  ## 4.8.17 — 2026-09-17
100
208
 
101
209
  **Patch** over **4.8.16**. Daily accumulate of the two ships that landed
@@ -107,8 +215,7 @@ policy-delta-ack-match confidence retarget
107
215
  `schemaVersion` bump. Does not close `K01` / `Z09`. This mother
108
216
  `ark.config.json` still does **not** turn `arkOrder` on.
109
217
 
110
- **Status: prepared** (npm `latest` remains **4.8.16** until Actions `publish-npm`
111
- runs with `tag=v4.8.17` and `dry_run=false`).
218
+ **Status: published** (npm `latest` is **4.8.17**).
112
219
 
113
220
  ### Added
114
221
  - 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.19** 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.19](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
 
@@ -66,19 +66,21 @@ Full map: **[docs/README.md](docs/README.md)**
66
66
  ## Start in one minute
67
67
 
68
68
  ```bash
69
- npm install -D arkgate typescript
70
69
  npx arkgate start # preview files + commands
71
70
  npx arkgate start --apply # compact config + host router + CI plan
72
71
  npx --package=arkgate arkgate-check --doctor # status — one next step
73
72
  npx --package=arkgate arkgate-check --doctor --all # full details
74
73
  ```
75
74
 
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.
75
+ `start --apply` pins arkgate with this repo's package manager (`pnpm add -w` / `yarn add -W` at a workspace root). Do not run `npm install -D arkgate` at a `workspace:*` root — npm rejects that protocol. After a local install, `npx arkgate-check --doctor` also works. `arkgate-check` is a command in the `arkgate` package, not its own npm package.
77
76
 
78
77
  `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`.
78
+ shape confidence is weak (below 0.6 with coverage under 80%), or when the
79
+ planned write is too big for compact start (more than 8 gate files or 32 KB).
80
+ That lock is deliberate. Coverage/shape: lock with `--archetype <id>`,
81
+ `--preset <name>`, or `--force`. Size: `--force` does not unlock. Next:
82
+ `npx arkgate-check --init`. Or inspect ranked shapes with
83
+ `npx arkgate-check --recommend`.
82
84
 
83
85
  That is the product. Stuck? Run status (`--doctor`) and do action **#1**.
84
86
 
@@ -201,7 +203,7 @@ Codex hard write covers only a complete local `apply_patch`; Cursor covers only
201
203
  installed + trusted, while shell/direct
202
204
  filesystem writes, hosted or specialized opt-out paths, and human edits still rely on CI.
203
205
 
204
- This table describes the supported profile **after its files are installed and the host loads/trusts them**. A hard local boundary covers only the listed hook operations; alternate tools, direct filesystem writes, and human edits still rely on CI. MCP validation is advisory because the agent must call it. The CI check blocks a merge only when the repository makes that status required. Repair **envelopes** may be emitted without reinjection being guaranteed; silent auto-apply never happens. Run `arkgate-check --doctor` (or `ark-check --doctor`) for the evidence actually detected in the current repository.
206
+ This table describes the supported profile **after its files are installed and the host loads/trusts them**. A hard local boundary covers only the listed hook operations; alternate tools, direct filesystem writes, and human edits still rely on CI. **Write-gate order:** install and trust the host pre-hook first. MCP prepare is fallback when that path is missing or fail-open. MCP validation is advisory because the agent must call it. The CI check blocks a merge only when the repository makes that status required. Repair **envelopes** may be emitted without reinjection being guaranteed; silent auto-apply never happens. Run `arkgate-check --doctor` (or `ark-check --doctor`) for the evidence actually detected in the current repository.
205
207
  <!-- arkgate-host-support:end -->
206
208
 
207
209
  #### Why required CI is the hard line
@@ -344,8 +346,8 @@ Compact starters leave the extra off. Details: [ArkOrder](docs/arkorder.md).
344
346
  | Config · package surface · TS | [configuration](docs/configuration.md) · [package-surface](docs/package-surface.md) · [typescript-support](docs/typescript-support.md) |
345
347
  | Brownfield | [docs/brownfield-adoption.md](docs/brownfield-adoption.md) |
346
348
  | 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) |
349
+ | Current published (4.8.19 on npm `latest`) | [CHANGELOG](CHANGELOG.md) |
350
+ | Prior published (4.8.11) | [docs/releases/4.8.11.md](docs/releases/4.8.11.md) · [CHANGELOG](CHANGELOG.md) |
349
351
  | Prior published (4.8.10) | [docs/releases/4.8.10.md](docs/releases/4.8.10.md) · [CHANGELOG](CHANGELOG.md) |
350
352
  | Prior published (4.8.9) | [docs/releases/4.8.9.md](docs/releases/4.8.9.md) · [CHANGELOG](CHANGELOG.md) |
351
353
  | Prior published (4.8.7) | [docs/releases/4.8.7.md](docs/releases/4.8.7.md) · [CHANGELOG](CHANGELOG.md) |
@@ -2037,8 +2037,8 @@ export async function runArkMcp({ hookInput } = {}) {
2037
2037
  'When the matched layer has layers[].description, the JSON includes description; the field is omitted when absent. ' +
2038
2038
  'When the matched layer has layers[].trustBoundary (public|auth|admin|internal), the JSON includes trustBoundary; omitted when absent. ' +
2039
2039
  'When the matched layer has layers[].owners, the JSON includes owners; omitted when absent. ' +
2040
- 'Call BEFORE writing a new file. ' +
2041
- 'Prefer ark_prepare_write when you already have the source snippet (place+validate+autoPatch in one call).',
2040
+ 'Call BEFORE writing a new file to learn the layer. ' +
2041
+ 'Prefer ark_prepare_write only as fallback when the host has no hard pre-hook (or that hook is missing / fail-open) and you already have the source snippet.',
2042
2042
  inputSchema: {
2043
2043
  type: 'object',
2044
2044
  properties: {
@@ -2061,7 +2061,7 @@ export async function runArkMcp({ hookInput } = {}) {
2061
2061
  'constrain (layer, mayImport, mustNotImport, forbiddenGlobals) + validate source + optional ' +
2062
2062
  'mechanical-safe autoPatch + judgmentBrief when judgment is needed + contentHash for host commit. ' +
2063
2063
  'Also returns the versioned new/worsened designDelta for the proposed full file. ' +
2064
- 'Composes ark_place + write-gate — call BEFORE Write/Edit when you have the snippet. ' +
2064
+ 'Composes ark_place + write-gate. Fallback when the host has no trusted hard pre-hook (or that hook is fail-open). When a hard hook is installed, the host already gates the write; this tool stays available but is not the primary gate. ' +
2065
2065
  'When the matched layer has layers[].description, the JSON includes description; the field is omitted when absent. ' +
2066
2066
  'When the matched layer has layers[].trustBoundary (public|auth|admin|internal), the JSON includes trustBoundary; omitted when absent. ' +
2067
2067
  'When the matched layer has layers[].owners, the JSON includes owners; omitted when absent. ' +
@@ -875,12 +875,61 @@ const SKIP_DIR_NAMES = new Set([
875
875
  '__pycache__',
876
876
  ]);
877
877
 
878
+ /**
879
+ * Non-product trees — same convention as the governed check walk
880
+ * (`isSkippedSourceDir` in scan-files). Start/recommend/discovery must not
881
+ * treat playground, fixtures, scaffolds, or tests as production architecture.
882
+ */
883
+ export const NON_PRODUCT_SOURCE_DIR_NAMES = new Set([
884
+ 'bench',
885
+ 'benches',
886
+ 'benchmark',
887
+ 'benchmarks',
888
+ 'docs',
889
+ 'documentation',
890
+ 'example',
891
+ 'examples',
892
+ 'fixture',
893
+ 'fixtures',
894
+ 'testdata',
895
+ 'playground',
896
+ 'scaffold',
897
+ 'scaffolds',
898
+ '__tests__',
899
+ '__mocks__',
900
+ 'e2e',
901
+ 'test',
902
+ 'tests',
903
+ ]);
904
+
905
+ export function isNonProductSourceDirName(name) {
906
+ return NON_PRODUCT_SOURCE_DIR_NAMES.has(name);
907
+ }
908
+
909
+ export function isNonProductRelativePath(relPath) {
910
+ return String(relPath)
911
+ .split(/[/\\]/)
912
+ .filter(Boolean)
913
+ .some((token) => NON_PRODUCT_SOURCE_DIR_NAMES.has(token.toLowerCase()));
914
+ }
915
+
916
+ /** Check-walk skip: vendor/build plus non-product fixture trees. */
917
+ export function isSkippedSourceDir(name) {
918
+ return (
919
+ name === 'node_modules' ||
920
+ name === 'dist' ||
921
+ name === 'coverage' ||
922
+ NON_PRODUCT_SOURCE_DIR_NAMES.has(name)
923
+ );
924
+ }
925
+
878
926
  function dirHasTsSources(dir, maxDepth) {
879
927
  try {
880
928
  for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
881
929
  if (entry.isFile() && /\.(tsx?|jsx?|mts|cts)$/i.test(entry.name)) return true;
882
930
  if (!entry.isDirectory()) continue;
883
931
  if (SKIP_DIR_NAMES.has(entry.name) || entry.name.startsWith('.')) continue;
932
+ if (isSkippedSourceDir(entry.name)) continue;
884
933
  if (maxDepth > 0 && dirHasTsSources(path.join(dir, entry.name), maxDepth - 1)) return true;
885
934
  }
886
935
  } catch {
@@ -918,8 +967,14 @@ export function detectTsPackageRoots(root, options = {}) {
918
967
  for (const entry of entries) {
919
968
  if (!entry.isDirectory()) continue;
920
969
  if (SKIP_DIR_NAMES.has(entry.name) || entry.name.startsWith('.')) continue;
921
- // Skip agent skill asset trees (not app code).
922
- if (entry.name === 'skills' || entry.name === 'templates' || entry.name === 'fixtures') continue;
970
+ // Skip agent skill asset trees and non-product fixture/playground/test trees.
971
+ if (
972
+ entry.name === 'skills' ||
973
+ entry.name === 'templates' ||
974
+ isSkippedSourceDir(entry.name)
975
+ ) {
976
+ continue;
977
+ }
923
978
  const childRel = rel ? `${rel}/${entry.name}` : entry.name;
924
979
  visit(path.join(abs, entry.name), childRel, depth + 1);
925
980
  }
@@ -944,7 +999,7 @@ export function resolveIncludeRoots(root) {
944
999
  const workspaces = detectWorkspaces(root);
945
1000
  const tsRoots = detectTsPackageRoots(root);
946
1001
  if (workspaces.length === 0) {
947
- if (tsRoots.length > 0) return tsRoots.filter((r) => r !== '.');
1002
+ if (tsRoots.length > 0) return tsRoots.filter((r) => r !== '.' && !isNonProductRelativePath(r));
948
1003
  return [];
949
1004
  }
950
1005
  // Workspaces present: keep them, add TS package roots not covered by a workspace prefix
@@ -960,7 +1015,7 @@ export function resolveIncludeRoots(root) {
960
1015
  if (fs.existsSync(path.join(root, tr, 'package.json'))) merged.add(tr);
961
1016
  }
962
1017
  }
963
- return [...merged];
1018
+ return [...merged].filter((entry) => !isNonProductRelativePath(entry));
964
1019
  }
965
1020
 
966
1021
  function isSourceFile(name) {
@@ -982,18 +1037,96 @@ function readJsonc(file) {
982
1037
 
983
1038
  function packageRole(rel, pkg) {
984
1039
  const tokens = rel.toLowerCase().split('/');
985
- if (tokens.some((token) => ['docs', 'documentation', 'website', 'examples', 'example', 'tests', 'test', 'fixtures'].includes(token))) {
986
- return tokens.some((token) => token.startsWith('doc') || token === 'website')
987
- ? 'docs'
988
- : tokens.some((token) => token.startsWith('test') || token === 'fixtures')
989
- ? 'test'
990
- : 'example';
1040
+ if (tokens.some((token) => ['docs', 'documentation', 'website'].includes(token))) {
1041
+ return 'docs';
1042
+ }
1043
+ if (
1044
+ tokens.some((token) =>
1045
+ [
1046
+ 'tests',
1047
+ 'test',
1048
+ 'fixtures',
1049
+ 'fixture',
1050
+ 'testdata',
1051
+ 'e2e',
1052
+ '__tests__',
1053
+ 'playground',
1054
+ 'scaffold',
1055
+ 'scaffolds',
1056
+ ].includes(token)
1057
+ )
1058
+ ) {
1059
+ return 'test';
1060
+ }
1061
+ if (tokens.some((token) => ['examples', 'example'].includes(token))) {
1062
+ return 'example';
991
1063
  }
992
1064
  if (pkg?.bin) return 'cli';
993
1065
  if (pkg?.exports || pkg?.main || pkg?.module || pkg?.types || pkg?.typings) return 'library';
994
1066
  return 'application';
995
1067
  }
996
1068
 
1069
+ /** Path tokens already treated as UI app folders in discoverRepoUnits (root scan). */
1070
+ export const UI_PACKAGE_PATH_TOKENS = new Set(['web', 'frontend', 'client', 'ui']);
1071
+
1072
+ const UI_PACKAGE_DEP_NAMES =
1073
+ /^(react|react-dom|vue|svelte|preact|solid-js|next|@angular\/core|@sveltejs\/kit)$/i;
1074
+
1075
+ /** True when a relative source path sits under a conventional UI package folder. */
1076
+ export function sourcePathLooksLikeUi(relPath) {
1077
+ return String(relPath ?? '')
1078
+ .toLowerCase()
1079
+ .split(/[/\\]/)
1080
+ .filter((token) => token && token !== '.')
1081
+ .some((token) => UI_PACKAGE_PATH_TOKENS.has(token));
1082
+ }
1083
+
1084
+ /**
1085
+ * Application/CLI unit that is a UI surface (folder name or UI framework deps).
1086
+ * Used so monorepo start does not park `web/**` on ApplicationOrchestration.
1087
+ */
1088
+ export function isUiPackageUnit(unit) {
1089
+ if (!unit || typeof unit !== 'object') return false;
1090
+ if (sourcePathLooksLikeUi(unit.root)) return true;
1091
+ const deps = unit.productionDeps ?? {};
1092
+ return Object.keys(deps).some((name) => UI_PACKAGE_DEP_NAMES.test(String(name).split('/')[0]));
1093
+ }
1094
+
1095
+ /** Source root of an application unit that should land on Presentation, not Application. */
1096
+ export function isUiApplicationSource(unit, sourceRoot) {
1097
+ if (!unit) return sourcePathLooksLikeUi(sourceRoot);
1098
+ const base =
1099
+ unit.root === '.'
1100
+ ? sourceRoot
1101
+ : sourceRoot === '.' || !sourceRoot
1102
+ ? unit.root
1103
+ : `${unit.root}/${sourceRoot}`;
1104
+ return sourcePathLooksLikeUi(base) || isUiPackageUnit(unit);
1105
+ }
1106
+
1107
+ /**
1108
+ * Monorepo start globs for package units by role. `uiMode: 'ui'` keeps only UI
1109
+ * application roots (web/, frontend/, react/vue/svelte); `'not-ui'` drops them
1110
+ * so they are not parked on ApplicationOrchestration (#288).
1111
+ */
1112
+ export function monorepoRoleSourceGlobs(units, roles, { root, domainScoped = false, uiMode } = {}) {
1113
+ if (!root || !Array.isArray(units)) return [];
1114
+ return units
1115
+ .filter((unit) => roles.includes(unit.role) && !(domainScoped && unit.root === '.'))
1116
+ .flatMap((unit) =>
1117
+ (unit.sourceRoots ?? []).map((sourceRoot) => {
1118
+ const base =
1119
+ unit.root === '.' ? sourceRoot : sourceRoot === '.' ? unit.root : `${unit.root}/${sourceRoot}`;
1120
+ if (!base || base === '.') return null;
1121
+ const uiSource = isUiApplicationSource(unit, sourceRoot);
1122
+ if (uiMode === 'ui' && !uiSource) return null;
1123
+ if (uiMode === 'not-ui' && uiSource) return null;
1124
+ return `${base}/**`;
1125
+ })
1126
+ )
1127
+ .filter((pattern) => pattern && (!domainScoped || /^(packages|libs)\//.test(pattern)));
1128
+ }
1129
+
997
1130
  function entrypointDirs(pkg) {
998
1131
  const values = [];
999
1132
  const add = (value) => {
@@ -1025,9 +1158,18 @@ export function discoverRepoUnits(root) {
1025
1158
  for (const name of ['tsconfig.json', 'jsconfig.json']) {
1026
1159
  const config = readJsonc(path.join(abs, name));
1027
1160
  const rootDir = config?.compilerOptions?.rootDir;
1028
- if (typeof rootDir === 'string') roots.add(normalizeRel(rootDir));
1161
+ if (typeof rootDir === 'string' && !/\.(json|jsonc)$/i.test(rootDir)) {
1162
+ const normalizedRootDir = normalizeRel(rootDir);
1163
+ if (!isNonProductRelativePath(normalizedRootDir)) roots.add(normalizedRootDir);
1164
+ }
1029
1165
  for (const ref of config?.references ?? []) {
1030
- if (typeof ref?.path === 'string') roots.add(normalizeRel(ref.path).replace(/^\.\//, ''));
1166
+ if (typeof ref?.path !== 'string') continue;
1167
+ const relRef = normalizeRel(ref.path).replace(/^\.\//, '');
1168
+ // Project references often point at tsconfig.app.json — that is not a source root.
1169
+ if (!relRef || relRef === '.' || /\.(json|jsonc)$/i.test(relRef)) continue;
1170
+ if (isNonProductRelativePath(relRef)) continue;
1171
+ const absRef = path.join(abs, relRef);
1172
+ if (fs.statSync(absRef, { throwIfNoEntry: false })?.isDirectory()) roots.add(relRef);
1031
1173
  }
1032
1174
  }
1033
1175
  for (const dir of entrypointDirs(pkg)) {
@@ -1067,6 +1209,7 @@ function walkSourceFiles(dir, files = [], depth = 0) {
1067
1209
  // `.github/workflows/` CI dir must not read as an app "workflows"/saga signal, and Ark's
1068
1210
  // own installed `.claude`/`.codex` dirs must not perturb a re-run's recommendation.
1069
1211
  if (entry.name === 'node_modules' || entry.name === 'dist' || entry.name.startsWith('.')) continue;
1212
+ if (isSkippedSourceDir(entry.name)) continue;
1070
1213
  walkSourceFiles(path.join(dir, entry.name), files, depth + 1);
1071
1214
  }
1072
1215
  return files;
@@ -1105,6 +1248,7 @@ function dirExistsAnywhere(root, names) {
1105
1248
  // Skip node_modules/dist and ALL dot-dirs so `.github/workflows/` (CI YAML) can't be read
1106
1249
  // as an app "workflows"/saga signal, and Ark's own `.claude`/`.codex` don't self-perturb.
1107
1250
  if (entry.name === 'node_modules' || entry.name === 'dist' || entry.name.startsWith('.')) continue;
1251
+ if (isSkippedSourceDir(entry.name)) continue;
1108
1252
  if (names.has(entry.name)) return true;
1109
1253
  const child = rel === '.' ? entry.name : `${rel}/${entry.name}`;
1110
1254
  if (child.split('/').length < 8) queue.push(child);
@@ -1139,7 +1283,14 @@ export function collectRepoShapeSignals(root) {
1139
1283
  const pkg = readPackageJson(root);
1140
1284
  const repoUnits = discoverRepoUnits(root);
1141
1285
  const workspaceDirs = detectWorkspaces(root);
1142
- const workspaces = workspaceDirs.length > 0;
1286
+ // Manifest presence is workspace evidence even when globs parse to no dirs
1287
+ // (pnpm-workspace.yaml without package.json#workspaces is the common case).
1288
+ const workspaces =
1289
+ workspaceDirs.length > 0 ||
1290
+ isPnpmWorkspaceRoot(root) ||
1291
+ isNpmYarnWorkspaceRoot(root) ||
1292
+ fs.existsSync(path.join(root, 'rush.json')) ||
1293
+ fs.existsSync(path.join(root, 'lerna.json'));
1143
1294
  // Include frontend/web/client — common Next monorepo app folders.
1144
1295
  const candidateScanDirs = repoUnits
1145
1296
  .filter((unit) => !['docs', 'example', 'test'].includes(unit.role))
@@ -1362,7 +1513,10 @@ export function collectRepoShapeSignals(root) {
1362
1513
  }
1363
1514
 
1364
1515
  const SIGNAL_WHY = {
1365
- workspaces: (signals) => `workspace roots declared (${signals.workspaceDirs.join(', ')})`,
1516
+ workspaces: (signals) =>
1517
+ signals.workspaceDirs?.length
1518
+ ? `workspace roots declared (${signals.workspaceDirs.join(', ')})`
1519
+ : 'workspace manifest present (pnpm-workspace.yaml, package.json workspaces, rush, or lerna)',
1366
1520
  tinyTree: (signals) => `few source files (${signals.sourceFileCount})`,
1367
1521
  ui: () => 'UI directories or multiple TSX files present',
1368
1522
  uiHeavy: () => 'substantial UI surface (multiple TSX files)',
@@ -1397,8 +1551,24 @@ const SIGNAL_WHY = {
1397
1551
  expressLike: () => 'HTTP framework dependency (express/fastify/hono/…)',
1398
1552
  };
1399
1553
 
1554
+ /**
1555
+ * Playbook labels that claim "without UI" are only honest when uiHeavy is false.
1556
+ * Mirror of the #282 workspace negative-copy overlay: do not invent a new
1557
+ * archetype — rewrite the one-minute label so it cannot contradict the sensors.
1558
+ */
1559
+ export function honestArchetypeLabel(label, signals) {
1560
+ if (typeof label !== 'string' || !signals?.uiHeavy) return label;
1561
+ if (!/without UI/i.test(label)) return label;
1562
+ if (signals.workspaces && signals.apiSurface) return 'API-heavy monorepo with UI packages';
1563
+ if (signals.apiSurface) return 'API-heavy repository with a UI surface';
1564
+ return 'Repository with a UI surface';
1565
+ }
1566
+
1400
1567
  const NEGATIVE_SIGNAL_WHY = {
1401
- workspaces: () => 'not a workspace monorepo (penalized for this shape)',
1568
+ workspaces: (signals) =>
1569
+ signals.workspaces
1570
+ ? `workspace monorepo (${signals.workspaceDirs?.join(', ') || 'manifest present'}; this shape prefers a single package)`
1571
+ : 'not a workspace monorepo (penalized for this shape)',
1402
1572
  cli: () => 'CLI bin entry present (penalized for this shape)',
1403
1573
  ui: () => 'UI directories present (penalized for this shape)',
1404
1574
  uiHeavy: () => 'heavy UI surface (penalized for this shape)',
@@ -1582,7 +1752,7 @@ export function scoreArchetypes(signals, playbook) {
1582
1752
  );
1583
1753
  scored.push({
1584
1754
  id,
1585
- label: def.label,
1755
+ label: honestArchetypeLabel(def.label, signals),
1586
1756
  preset: resolvePreset(def, signals),
1587
1757
  score,
1588
1758
  maxPositive: maxPositive || 1,
@@ -1718,6 +1888,7 @@ export function buildArchitectureRecommendation(root, options = {}) {
1718
1888
  })),
1719
1889
  workspaces: signals.workspaces,
1720
1890
  ui: signals.ui,
1891
+ uiHeavy: signals.uiHeavy,
1721
1892
  apiSurface: signals.apiSurface,
1722
1893
  persistence: signals.persistence,
1723
1894
  jobs: signals.jobs,
@@ -1740,7 +1911,12 @@ export function buildArchitectureRecommendation(root, options = {}) {
1740
1911
  // a thin slice and can mis-flag framework internals, so steer these to the adoption flow.
1741
1912
  mature: signals.sourceFileCount >= MATURE_REPO_FILE_THRESHOLD,
1742
1913
  initCommand: `${arkCommand(root, 'ark', `init --archetype ${result.archetype} --yes`)}`,
1743
- firstCommand: `${arkCommand(root, 'ark', `start --apply --archetype ${result.archetype}`)}`,
1914
+ // Greenfield: compact start. Mature: `--init` writes the rules file without the
1915
+ // compact-start size lock that cold `start --apply` can hit on large trees.
1916
+ firstCommand:
1917
+ signals.sourceFileCount >= MATURE_REPO_FILE_THRESHOLD
1918
+ ? arkCommand(root, 'ark-check', '--init')
1919
+ : `${arkCommand(root, 'ark', `start --apply --archetype ${result.archetype}`)}`,
1744
1920
  adoptCommand: arkCommand(root, 'ark-check', '--recommend --write-plan'),
1745
1921
  recommendCommand: arkCommand(root, 'ark-check', '--recommend'),
1746
1922
  checkCommand: arkCommand(root, 'ark-check', '--root . --config ark.config.json --strict-config'),
@@ -1817,10 +1993,11 @@ export function resolveStartInitPreset(root, rec = {}, archetype = rec.archetype
1817
1993
  return preset ?? null;
1818
1994
  }
1819
1995
 
1820
- /** One-minute / preview footer when the coverage·confidence gate may refuse apply. */
1996
+ /** One-minute / preview footer when apply may refuse (coverage·shape or compact size). */
1821
1997
  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',
1998
+ 'If apply refuses (coverage below 50%, weak shape, or too big for compact start), that lock is deliberate.',
1999
+ 'Coverage/shape: --archetype <id> · --preset <name> · --force',
2000
+ 'Too big for compact start: --force does not unlock. Next: arkgate-check --init',
1824
2001
  'Inspect ranked shapes: arkgate-check --recommend',
1825
2002
  ].join('\n');
1826
2003
 
package/bin/ark.mjs CHANGED
@@ -24,9 +24,12 @@ 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,
32
+ runStartPackageInstall,
30
33
  } from './lib/start-preview.mjs';
31
34
  import { runUpgradeCommand } from './lib/upgrade-command.mjs';
32
35
  import { detectActiveAgentHost } from './lib/skill-install.mjs';
@@ -209,11 +212,6 @@ function cliVersion() {
209
212
 
210
213
  // packageInstallArgv is imported from ark-shared (workspace-aware -w / -W).
211
214
 
212
- function runCommand(command, commandArgs, cwd) {
213
- const result = spawnSync(command, commandArgs, { cwd, stdio: 'inherit', encoding: 'utf8' });
214
- return result.status ?? 1;
215
- }
216
-
217
215
  function runArkCheck(args, options = {}) {
218
216
  const result = spawnSync(process.execPath, [arkCheck, ...args], {
219
217
  cwd: options.cwd,
@@ -280,13 +278,16 @@ export function ensureProjectArkgateDependency(root, opts = {}) {
280
278
  }
281
279
  const pinned = pinArkgateDevDependency(root);
282
280
  let installStatus = null;
281
+ let hostOutput = '';
283
282
  // Only run the package manager after a successful pin change — avoid surprise
284
283
  // network on every start when arkgate is already listed.
285
284
  if (runPm && pinned.changed) {
286
285
  const [command, commandArgs] = packageInstallArgv(root, pinned.version);
287
- installStatus = runCommand(command, commandArgs, root);
286
+ const install = runStartPackageInstall(command, commandArgs, root);
287
+ installStatus = install.status;
288
+ hostOutput = `${install.stdout}\n${install.stderr}`;
288
289
  }
289
- return { pinned, installStatus };
290
+ return { pinned, installStatus, hostOutput };
290
291
  }
291
292
 
292
293
  async function resolveArchetypeInteractive(rl, root) {
@@ -470,6 +471,10 @@ async function start(args) {
470
471
  }
471
472
  return 2;
472
473
  }
474
+ if (!evaluateStartSetupBudgetGate(preview.setupBudget).ok) {
475
+ emitStartSetupBudgetRefuse(preview, args.json);
476
+ return 2;
477
+ }
473
478
  }
474
479
  if (args.json) console.log(JSON.stringify(preview, null, 2));
475
480
  else if (!args.apply) renderStartPreview(preview);
@@ -497,16 +502,16 @@ async function start(args) {
497
502
  const installCommand = `${command} ${commandArgs.join(' ')}`;
498
503
  if (!args.json) console.log(`Installing package: ${installCommand}`);
499
504
  // Keep stdout clean for --json consumers (package managers are chatty on stdout).
500
- const status = args.json
501
- ? (spawnSync(command, commandArgs, {
502
- cwd: args.root,
503
- stdio: ['ignore', 'pipe', 'pipe'],
504
- encoding: 'utf8',
505
- }).status ?? 1)
506
- : runCommand(command, commandArgs, args.root);
507
- if (status !== 0) {
508
- console.error(formatStartPackageInstallFailure({ exitStatus: status, installCommand }));
509
- return status;
505
+ const install = runStartPackageInstall(command, commandArgs, args.root);
506
+ if (install.status !== 0) {
507
+ console.error(
508
+ formatStartPackageInstallFailure({
509
+ exitStatus: install.status,
510
+ installCommand,
511
+ hostOutput: `${install.stdout}\n${install.stderr}`,
512
+ })
513
+ );
514
+ return install.status;
510
515
  }
511
516
  }
512
517
  }
@@ -566,7 +571,7 @@ async function start(args) {
566
571
  // 2b) Pin arkgate as a project devDependency so CI/npx do not depend on a stale global.
567
572
  // Default install=true; only --no-install skips. (installExplicit tracks user override for copy.)
568
573
  if (args.install && fs.existsSync(path.join(root, 'package.json'))) {
569
- const { pinned, installStatus } = ensureProjectArkgateDependency(root, {
574
+ const { pinned, installStatus, hostOutput } = ensureProjectArkgateDependency(root, {
570
575
  install: true,
571
576
  runPackageManager: !args.skipPackageManager,
572
577
  });
@@ -578,6 +583,7 @@ async function start(args) {
578
583
  formatStartPackageInstallFailure({
579
584
  exitStatus: installStatus,
580
585
  installCommand: `${command} ${commandArgs.join(' ')}`,
586
+ hostOutput,
581
587
  })
582
588
  );
583
589
  }
@@ -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,