arkgate 4.8.18 → 4.8.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +52 -0
- package/README.md +5 -6
- package/bin/ark-mcp-runtime.mjs +3 -3
- package/bin/ark-shared.mjs +187 -16
- package/bin/ark.mjs +18 -18
- package/bin/lib/ci-and-commands.mjs +2 -2
- package/bin/lib/doctor-human.mjs +2 -0
- package/bin/lib/doctor-next-actions.mjs +35 -0
- package/bin/lib/doctor-plan.mjs +6 -8
- package/bin/lib/gate-files.mjs +22 -1
- package/bin/lib/host-support-matrix.mjs +1 -1
- package/bin/lib/html-report-depth.mjs +1 -1
- package/bin/lib/install-migrate.mjs +3 -4
- package/bin/lib/managed-upgrade.mjs +30 -5
- package/bin/lib/mcp-adoption.mjs +4 -1
- package/bin/lib/presets.mjs +14 -14
- package/bin/lib/scan-files.mjs +3 -26
- package/bin/lib/skill-install.mjs +3 -2
- package/bin/lib/start-preview.mjs +60 -5
- package/bin/lib/write-path-detect.mjs +3 -2
- package/dist/{diagnosticCatalog-BNxKdcN4.d.ts → diagnosticCatalog-D2v0ASN2.d.ts} +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/nestjs/index.cjs +1 -1
- package/dist/nestjs/index.js +1 -1
- package/dist/runtime/index.cjs +1 -1
- package/dist/runtime/index.d.ts +1 -1
- package/dist/runtime/index.js +1 -1
- package/docs/README.md +1 -1
- package/docs/agent-guide.md +3 -1
- package/docs/ai-gates.md +7 -3
- package/docs/develop.md +1 -2
- package/docs/use.md +1 -2
- package/package.json +1 -1
- package/server.json +2 -2
- package/templates/agent-skills/README.md +1 -1
- package/templates/agent-skills/ark-place/SKILL.md +2 -0
- package/templates/hooks/opencode-ark-write-gate.mjs +2 -2
- package/templates/skills/ark-place.md +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -96,6 +96,58 @@ 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
|
+
|
|
99
151
|
## 4.8.18 — 2026-09-18
|
|
100
152
|
|
|
101
153
|
**Patch** over **4.8.17**. Daily accumulate (2026-09-18 ART) of the six
|
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.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.
|
|
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,14 +66,13 @@ 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-
|
|
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
78
|
shape confidence is weak (below 0.6 with coverage under 80%), or when the
|
|
@@ -204,7 +203,7 @@ Codex hard write covers only a complete local `apply_patch`; Cursor covers only
|
|
|
204
203
|
installed + trusted, while shell/direct
|
|
205
204
|
filesystem writes, hosted or specialized opt-out paths, and human edits still rely on CI.
|
|
206
205
|
|
|
207
|
-
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.
|
|
208
207
|
<!-- arkgate-host-support:end -->
|
|
209
208
|
|
|
210
209
|
#### Why required CI is the hard line
|
|
@@ -347,7 +346,7 @@ Compact starters leave the extra off. Details: [ArkOrder](docs/arkorder.md).
|
|
|
347
346
|
| Config · package surface · TS | [configuration](docs/configuration.md) · [package-surface](docs/package-surface.md) · [typescript-support](docs/typescript-support.md) |
|
|
348
347
|
| Brownfield | [docs/brownfield-adoption.md](docs/brownfield-adoption.md) |
|
|
349
348
|
| Security | [SECURITY.md](SECURITY.md) |
|
|
350
|
-
| Current published (4.8.
|
|
349
|
+
| Current published (4.8.19 on npm `latest`) | [CHANGELOG](CHANGELOG.md) |
|
|
351
350
|
| Prior published (4.8.11) | [docs/releases/4.8.11.md](docs/releases/4.8.11.md) · [CHANGELOG](CHANGELOG.md) |
|
|
352
351
|
| Prior published (4.8.10) | [docs/releases/4.8.10.md](docs/releases/4.8.10.md) · [CHANGELOG](CHANGELOG.md) |
|
|
353
352
|
| Prior published (4.8.9) | [docs/releases/4.8.9.md](docs/releases/4.8.9.md) · [CHANGELOG](CHANGELOG.md) |
|
package/bin/ark-mcp-runtime.mjs
CHANGED
|
@@ -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
|
|
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
|
|
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. ' +
|
package/bin/ark-shared.mjs
CHANGED
|
@@ -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
|
|
922
|
-
if (
|
|
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'
|
|
986
|
-
return
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
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')
|
|
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
|
|
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
|
-
|
|
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) =>
|
|
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: () =>
|
|
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,
|
package/bin/ark.mjs
CHANGED
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
formatStartPackageInstallFailure,
|
|
30
30
|
planStart,
|
|
31
31
|
renderStartPreview,
|
|
32
|
+
runStartPackageInstall,
|
|
32
33
|
} from './lib/start-preview.mjs';
|
|
33
34
|
import { runUpgradeCommand } from './lib/upgrade-command.mjs';
|
|
34
35
|
import { detectActiveAgentHost } from './lib/skill-install.mjs';
|
|
@@ -211,11 +212,6 @@ function cliVersion() {
|
|
|
211
212
|
|
|
212
213
|
// packageInstallArgv is imported from ark-shared (workspace-aware -w / -W).
|
|
213
214
|
|
|
214
|
-
function runCommand(command, commandArgs, cwd) {
|
|
215
|
-
const result = spawnSync(command, commandArgs, { cwd, stdio: 'inherit', encoding: 'utf8' });
|
|
216
|
-
return result.status ?? 1;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
215
|
function runArkCheck(args, options = {}) {
|
|
220
216
|
const result = spawnSync(process.execPath, [arkCheck, ...args], {
|
|
221
217
|
cwd: options.cwd,
|
|
@@ -282,13 +278,16 @@ export function ensureProjectArkgateDependency(root, opts = {}) {
|
|
|
282
278
|
}
|
|
283
279
|
const pinned = pinArkgateDevDependency(root);
|
|
284
280
|
let installStatus = null;
|
|
281
|
+
let hostOutput = '';
|
|
285
282
|
// Only run the package manager after a successful pin change — avoid surprise
|
|
286
283
|
// network on every start when arkgate is already listed.
|
|
287
284
|
if (runPm && pinned.changed) {
|
|
288
285
|
const [command, commandArgs] = packageInstallArgv(root, pinned.version);
|
|
289
|
-
|
|
286
|
+
const install = runStartPackageInstall(command, commandArgs, root);
|
|
287
|
+
installStatus = install.status;
|
|
288
|
+
hostOutput = `${install.stdout}\n${install.stderr}`;
|
|
290
289
|
}
|
|
291
|
-
return { pinned, installStatus };
|
|
290
|
+
return { pinned, installStatus, hostOutput };
|
|
292
291
|
}
|
|
293
292
|
|
|
294
293
|
async function resolveArchetypeInteractive(rl, root) {
|
|
@@ -503,16 +502,16 @@ async function start(args) {
|
|
|
503
502
|
const installCommand = `${command} ${commandArgs.join(' ')}`;
|
|
504
503
|
if (!args.json) console.log(`Installing package: ${installCommand}`);
|
|
505
504
|
// Keep stdout clean for --json consumers (package managers are chatty on stdout).
|
|
506
|
-
const
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
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;
|
|
516
515
|
}
|
|
517
516
|
}
|
|
518
517
|
}
|
|
@@ -572,7 +571,7 @@ async function start(args) {
|
|
|
572
571
|
// 2b) Pin arkgate as a project devDependency so CI/npx do not depend on a stale global.
|
|
573
572
|
// Default install=true; only --no-install skips. (installExplicit tracks user override for copy.)
|
|
574
573
|
if (args.install && fs.existsSync(path.join(root, 'package.json'))) {
|
|
575
|
-
const { pinned, installStatus } = ensureProjectArkgateDependency(root, {
|
|
574
|
+
const { pinned, installStatus, hostOutput } = ensureProjectArkgateDependency(root, {
|
|
576
575
|
install: true,
|
|
577
576
|
runPackageManager: !args.skipPackageManager,
|
|
578
577
|
});
|
|
@@ -584,6 +583,7 @@ async function start(args) {
|
|
|
584
583
|
formatStartPackageInstallFailure({
|
|
585
584
|
exitStatus: installStatus,
|
|
586
585
|
installCommand: `${command} ${commandArgs.join(' ')}`,
|
|
586
|
+
hostOutput,
|
|
587
587
|
})
|
|
588
588
|
);
|
|
589
589
|
}
|
|
@@ -201,7 +201,7 @@ const AGENT_CONTRACT = {
|
|
|
201
201
|
],
|
|
202
202
|
// Cursor-only guidance: the write-time validate_code tool is available in
|
|
203
203
|
// Cursor's runtime but has no equivalent in a plain AGENTS.md read.
|
|
204
|
-
cursorValidateStep: `
|
|
204
|
+
cursorValidateStep: `The write gate is the host pre-hook when \`.cursor/hooks.json\` is installed, trusted, and \`failClosed\`. Call \`ark_prepare_write\` / \`validate_code\` only as fallback if that hook is missing or fail-open.`,
|
|
205
205
|
};
|
|
206
206
|
|
|
207
207
|
/**
|
|
@@ -413,7 +413,7 @@ Contener · Guiar · Ordenar — contain the write, guide the next step, order l
|
|
|
413
413
|
1. Status anytime: \`${doctorCmd}\` — what's wrong and what to do first.
|
|
414
414
|
2. Read the **Improvement compass** section (not a score). Name residual lenses in plain language when present (SoC, DIP, domain, …). Out-of-scope lenses (performance, app security tooling, full resilience) stay honest — do not invent Ark enforcement for them.
|
|
415
415
|
3. CLI-first: if the local CLI already resolved this project root, do not wait on MCP “still connecting”. Identity handshake is optional in that case. When you do trust MCP evidence: call \`ark_identity\` with \`project.expectedRoot\` set to this project's exact absolute root, then reuse that root plus the returned \`projectIdentity.projectId\` on every Ark MCP call. A descendant path is authoritative only with that matching id. Missing tool, non-\`matched\` binding, or wrong root means this is not proven to be the right project: restart the host and use the local CLI meanwhile.
|
|
416
|
-
4. Day to day: call \`ark_manifest\` with the same project expectation (or skip if CLI already bound the root); place new files with \`ark_place\` (filePath required — fail-closed);
|
|
416
|
+
4. Day to day: call \`ark_manifest\` with the same project expectation (or skip if CLI already bound the root); place new files with \`ark_place\` (filePath required — fail-closed); run \`${checkCmd}\`. The host pre-hook is the write gate when installed; MCP \`ark_prepare_write\` is fallback if that hook is missing or fail-open. The \`ark://manifest\` resource is compatibility-only and always unverified. On a deny, fix the illegal import — do not weaken \`ark.config.json\`.
|
|
417
417
|
5. If MCP is unavailable: inspect \`ark.config.json\` and run \`${checkCmd}\`. The host-readable projection is AGENTS.md and CLAUDE.md (same schema).
|
|
418
418
|
|
|
419
419
|
**Single door when residual remains:**
|
package/bin/lib/doctor-human.mjs
CHANGED
|
@@ -147,6 +147,8 @@ export function printDoctorCompactHuman(view) {
|
|
|
147
147
|
if (skippableCi) line(warn, skippableCi);
|
|
148
148
|
else if (writePathHonesty?.nativeFailClosed === false) {
|
|
149
149
|
line(warn, writePathHonesty.message || 'Write hook is fail-open — if the checker cannot run, the write still lands.');
|
|
150
|
+
} else if (listedMissing.length === 0 && writePath.gap?.id === 'write-path-mcp-only') {
|
|
151
|
+
line(warn, writePath.gap.message || 'Fell back to MCP prepare; required CI is the merge boundary.');
|
|
150
152
|
} else if (listedMissing.length === 0 && (writePath.gap || writePathHonesty?.softWriteHost)) {
|
|
151
153
|
line(warn, 'Local writes are advisory; required CI is the merge boundary.');
|
|
152
154
|
}
|
|
@@ -9,6 +9,36 @@ import { mergePostGreenTopActions } from './post-green-path.mjs';
|
|
|
9
9
|
import { ADOPTED_NOT, NOT_ADOPTED_NEXT_ACTION } from './adoption-stance.mjs';
|
|
10
10
|
import { REQUIRED_GATE_WORKFLOW } from './gate-files.mjs';
|
|
11
11
|
|
|
12
|
+
/** Package pin may exist; local resolve still failed. Lead with install, not CI. */
|
|
13
|
+
export const PACKAGE_UNRESOLVED_NEXT_ACTION =
|
|
14
|
+
'Install arkgate in this project first (npx --package=arkgate, or pnpm add -D arkgate -w / yarn add -D arkgate -W at a workspace root), then re-run --doctor';
|
|
15
|
+
|
|
16
|
+
export function packageUnresolvedNextAction(ctx) {
|
|
17
|
+
if (ctx?.packageInstalled !== false || ctx?.selfHost === true) return null;
|
|
18
|
+
const code = ctx?.packageVersionTruth?.code;
|
|
19
|
+
if (code === 'PACKAGE_PIN_SELF_HOST' || code === 'PACKAGE_PIN_ABSENT') return null;
|
|
20
|
+
// Failed start --apply: pin is in package.json, node_modules did not resolve.
|
|
21
|
+
if (code === 'PACKAGE_PIN_MATCHES' || code === 'PACKAGE_PIN_BEHIND_CLI' || ctx?.packagePinned === true) {
|
|
22
|
+
return PACKAGE_UNRESOLVED_NEXT_ACTION;
|
|
23
|
+
}
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Compact doctor JSON #1 — same rank as collectDoctorNextActions when package is missing. */
|
|
28
|
+
export function preferredDoctorPrimaryNextAction({
|
|
29
|
+
adopted,
|
|
30
|
+
packageInstalled,
|
|
31
|
+
selfHost,
|
|
32
|
+
packageVersionTruth,
|
|
33
|
+
postGreenPath,
|
|
34
|
+
dualTruthNext,
|
|
35
|
+
} = {}) {
|
|
36
|
+
return (
|
|
37
|
+
packageUnresolvedNextAction({ packageInstalled, selfHost, packageVersionTruth }) ||
|
|
38
|
+
(adopted === 'not-adopted' ? NOT_ADOPTED_NEXT_ACTION : postGreenPath?.action ?? dualTruthNext ?? null)
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
12
42
|
function missingGateFiles(ctx) {
|
|
13
43
|
const list = Array.isArray(ctx.gatesMissing) ? ctx.gatesMissing : [];
|
|
14
44
|
if (ctx.ciNotFailClosed) {
|
|
@@ -119,6 +149,8 @@ export function collectDoctorNextActions(ctx) {
|
|
|
119
149
|
if (notAdopted || ctx.adopted === ADOPTED_NOT || ctx.adopted == null) {
|
|
120
150
|
actions.push(ctx.notAdoptedNextAction || NOT_ADOPTED_NEXT_ACTION);
|
|
121
151
|
}
|
|
152
|
+
const resolvePkg = packageUnresolvedNextAction(ctx);
|
|
153
|
+
if (resolvePkg) actions.push(resolvePkg);
|
|
122
154
|
const nudge = ctx.stewardNudge;
|
|
123
155
|
if (
|
|
124
156
|
nudge &&
|
|
@@ -259,6 +291,9 @@ export function collectDoctorNextActions(ctx) {
|
|
|
259
291
|
if (ctx.designFitness.designWeak && unique.length === 0 && ctx.postGreenPath) {
|
|
260
292
|
unique.push(ctx.postGreenPath.action);
|
|
261
293
|
}
|
|
294
|
+
if (resolvePkg) {
|
|
295
|
+
return [resolvePkg, ...unique.filter((a) => a !== resolvePkg)];
|
|
296
|
+
}
|
|
262
297
|
if (overlapAction) {
|
|
263
298
|
return [overlapAction, ...unique.filter((a) => a !== overlapAction)];
|
|
264
299
|
}
|