arkgate 4.6.6 → 4.7.0

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 (66) hide show
  1. package/CHANGELOG.md +139 -2
  2. package/README.md +22 -10
  3. package/SECURITY.md +1 -1
  4. package/bin/ark-check-runtime.mjs +21 -341
  5. package/bin/ark-mcp-runtime.mjs +71 -325
  6. package/bin/ark-shared.mjs +24 -158
  7. package/bin/lib/adapter-contract.mjs +17 -36
  8. package/bin/lib/analysis-engine.mjs +6 -6
  9. package/bin/lib/ark-run-doctor.mjs +144 -0
  10. package/bin/lib/ark-run-facts.mjs +472 -0
  11. package/bin/lib/ark-run-report.mjs +57 -0
  12. package/bin/lib/ark-run-sensors.mjs +309 -0
  13. package/bin/lib/check-args.mjs +173 -0
  14. package/bin/lib/check-config-detect.mjs +101 -0
  15. package/bin/lib/check-watch.mjs +80 -0
  16. package/bin/lib/config-contract.mjs +86 -11
  17. package/bin/lib/deep-module-coach.mjs +3 -0
  18. package/bin/lib/diagnostic-catalog.mjs +8 -0
  19. package/bin/lib/doctor-advisories.mjs +45 -8
  20. package/bin/lib/doctor-human.mjs +519 -0
  21. package/bin/lib/doctor-plan.mjs +62 -445
  22. package/bin/lib/extra-merge-teeth.mjs +187 -0
  23. package/bin/lib/github-enforcement.mjs +22 -9
  24. package/bin/lib/html-report-advisories.mjs +2 -0
  25. package/bin/lib/html-report-depth.mjs +22 -2
  26. package/bin/lib/html-report.mjs +40 -7
  27. package/bin/lib/mcp-hook-payload.mjs +328 -0
  28. package/bin/lib/package-manager.mjs +174 -0
  29. package/bin/lib/policy-delta-io.mjs +4 -0
  30. package/bin/lib/remediation.mjs +132 -0
  31. package/bin/lib/resolved-candidate-facts.mjs +67 -2
  32. package/bin/lib/rules-under-contract.mjs +37 -89
  33. package/bin/lib/snippet-analysis.mjs +43 -2
  34. package/bin/lib/status-command.mjs +28 -0
  35. package/bin/lib/status-manifest.mjs +23 -0
  36. package/bin/lib/team-parliament-io.mjs +4 -0
  37. package/dist/{configTypes-l6XiwiC1.d.ts → configTypes-CgJimx9o.d.ts} +17 -3
  38. package/dist/eslint/index.cjs +6 -2
  39. package/dist/eslint/index.d.ts +70 -2
  40. package/dist/eslint/index.js +6 -2
  41. package/dist/index.cjs +35 -35
  42. package/dist/index.d.ts +787 -272
  43. package/dist/index.js +35 -35
  44. package/docs/README.md +4 -3
  45. package/docs/agent-guide.md +21 -15
  46. package/docs/ai-gates.md +13 -0
  47. package/docs/configuration.md +24 -11
  48. package/docs/develop.md +12 -3
  49. package/docs/diagnostics.md +75 -0
  50. package/docs/enthusiast/README.md +4 -3
  51. package/docs/package-surface.md +17 -13
  52. package/docs/product-voice.md +6 -3
  53. package/docs/threat-model.md +1 -1
  54. package/docs/use.md +5 -4
  55. package/package.json +1 -1
  56. package/schemas/ark.config.schema.json +41 -2
  57. package/schemas/ark.resolved-candidate-facts.schema.json +1 -1
  58. package/schemas/ark.status-manifest.schema.json +47 -0
  59. package/server.json +2 -2
  60. package/templates/agent-skills/README.md +1 -1
  61. package/templates/agent-skills/ark-adopt/SKILL.md +23 -2
  62. package/templates/agent-skills/ark-place/SKILL.md +26 -2
  63. package/templates/agent-skills/ark-runtime/SKILL.md +66 -24
  64. package/templates/skills/ark-adopt.md +23 -2
  65. package/templates/skills/ark-place.md +26 -2
  66. package/templates/skills/ark-runtime.md +66 -24
package/CHANGELOG.md CHANGED
@@ -5,12 +5,149 @@ in the immutable pre-2.0 archive linked below.
5
5
 
6
6
  ## Unreleased
7
7
 
8
+ ## 4.7.0 — 2026-08-25
9
+
10
+ **Minor** over **4.6.7**. Ships **ArkRun**: an opt-in extra on schema `1.2` for kernel
11
+ usage and complete declarations, plus companion `@arkgate/runtime` DX. Absence is
12
+ silent (Layers / ArkRules verdicts unchanged). In-memory stores remain
13
+ reference-only. **No required config migration.** Does not close Z09 / K01.
14
+
15
+ **Status: prepared** (see `docs/releases/4.7.0.md`). npm `latest` remains **4.6.7**
16
+ until publish.
17
+
18
+ ### Added
19
+
20
+ - **`arkRun` extra on `ark.config.json` schema `1.2` (RN02):** optional inline
21
+ `{ mode, compositionRoots, managedLayers, requireDeclarations }`. `1.1` and
22
+ earlier configs migrate in memory; absence is silent (Layers / ArkRules
23
+ verdicts unchanged). Unknown keys, unknown `managedLayers` names, and empty
24
+ `compositionRoots` in `enforced` mode fail closed (`ARKRUN_MISSING_ROOT`).
25
+ Advisory → enforced is a strengthening policy delta; demotion or deletion is
26
+ weakening and needs the existing hash-bound ack. ESLint envelope is RN06
27
+ (landed below); CI extra teeth landed in RN07. Does not close Z09 / K01.
28
+
29
+ - **ArkRun resolver facts on resolved-candidate-facts schema `1.2` (RN03):**
30
+ additive optional `arkRunKernelCalls`, `arkRunManagedNews`, and
31
+ `arkRunCompositionRootHits`. `1.0`/`1.1` payloads stay loadable (empty
32
+ arrays). Syntax evidence only — sensors consume these in RN04. Absence of
33
+ `arkRun` still leaves Layers / ArkRules verdicts unchanged. Does not close
34
+ Z09 / K01.
35
+
36
+ - **ArkRun tier-1 sensors (RN04):** when `arkRun` is present, closed sensors
37
+ emit `ARKRUN_MISSING_ROOT`, `ARKRUN_KERNEL_IN_DOMAIN`, `ARKRUN_DIRECT_NEW`,
38
+ `ARKRUN_UNDECLARED_EMIT`, `ARKRUN_UNDECLARED_HANDLE`, `ARKRUN_UNDECLARED_DEPEND`,
39
+ and `ARKRUN_TRANSPORT_BYPASS`. Advisory findings never flip `valid`; enforced
40
+ blocks. Absence of the extra is still silent on Layers / ArkRules verdicts.
41
+ Optional `arkRunDeclarations` facts stay additive on schema `1.2`. Dual-depth
42
+ catalog nextAction is RN05 (landed below). Does not close Z09 / K01.
43
+
44
+ - **ArkRun diagnostic catalog dual-depth (RN05):** closed `ARKRUN_*` catalog
45
+ entries have dual-depth remediation: casual `enthusiastHint` plus engineer
46
+ `nextAction` (target interpolates the call-site literal or specifier). Adding
47
+ an existing declaration-list string is `mechanical-safe` (`arkrun-declaration-list`)
48
+ only when that literal is already present; other ArkRun findings stay
49
+ `judgment`. Sensors, adapter fallback, and CLI remediation share
50
+ `deterministicNextAction`. Does not close Z09 / K01.
51
+
52
+ - **ArkRun ESLint envelope (RN06):** `arkgate/eslint` recommended config adds
53
+ `ark/no-arkrun-kernel-in-domain`, `ark/no-arkrun-direct-new`, and
54
+ `ark/no-arkrun-transport-bypass`. Same `ARKRUN_*` sensors as ark-check for
55
+ the import / `new` envelope; silent when `arkRun` is absent. Missing-root and
56
+ undeclared-* stay CLI/MCP/preflight. Does not close Z09 / K01.
57
+
58
+ - **ArkRun extra-teeth parity (RN07):** CLI `--strict-merge`, MCP `ark_check` /
59
+ snippet write, PreToolUse hook, atomic preflight, and CI share one ArkRun
60
+ verdict. Enforced extra teeth arm only when the layer plane is classified
61
+ (same ≥50% governed / ≥1 populated-layer floor as ArkRules); advisory and
62
+ absence stay silent on `valid`. Doctor/status `arkRun` section landed in
63
+ RN08 below. Does not close Z09 / K01.
64
+
65
+ - **ArkRun doctor / status / report (RN08):** `ark-check --doctor`, HTML
66
+ `--report`, and `ark status` / MCP `ark_status` expose an `arkRun` section
67
+ that is always `notAScore`. Residual is a finding-id count, never a score or
68
+ LLM verdict. `mergePlanes.arkRun` states whether the extra can fail merge;
69
+ advisory and absence never arm extra teeth. Report parity requires
70
+ `data-advisory="arkRun"`. Does not close Z09 / K01.
71
+
72
+ - **ArkRun companion branding (RN09):** `@arkgate/runtime` README and public
73
+ docs brand the kernel **ArkRun**. `createStrictArkKernel` stays the factory
74
+ (per-instance; no process-wide singleton). Kernel implementation stays out of
75
+ the `arkgate` tarball. Branding is not a production-durability claim. Does not
76
+ close Z09 / K01.
77
+
78
+ - **ArkRun interaction declarations (RN10):** `@arkgate/runtime` `register()`
79
+ accepts `uses` / `reactsTo` / `raises` / `sends` plus optional tooling-only
80
+ `extendedInfo`. `getDependencyInformationPackage()` returns a JSON-serializable
81
+ snapshot of ids, lifetime, and declarations — never factories, live instances,
82
+ or input DTOs. Companion registrations may omit declarations for local
83
+ experiments; enforced `arkRun` on the gate still requires them. Does not close
84
+ Z09 / K01.
85
+
86
+ - **ArkRun transport ports (RN11):** `@arkgate/runtime` `send()` is one call site
87
+ for `local` / `localBlocking` / `broker`. `ephemeral` defaults true (await local
88
+ recording or adapter accept — not a durability claim). Missing broker adapter
89
+ falls back to in-process local delivery, not cloud portability. No cloud SDKs
90
+ ship in the package. Does not close Z09 / K01.
91
+
92
+ - **ArkRun dev inspector (RN12):** `@arkgate/runtime` `startInspector()` /
93
+ `startArkRunInspector()` is opt-in. Default bind is `127.0.0.1`; `NODE_ENV=production`
94
+ vetoes start; public hosts (`0.0.0.0`, `::`) are rejected. HTTP is lazy-loaded.
95
+ `GET /snapshot` and `GET /events` (SSE) serve the information package plus
96
+ transport facts (no factories, no shipped cloud SDKs). Does not close Z09 / K01.
97
+
98
+ - **ArkRun graph slices (RN13):** `@arkgate/runtime` `requestGraph()` slices the
99
+ information package into `process` (raises / reactsTo / sends) or `technical`
100
+ (`uses`) graphs. Optional `nodeIds`, `degreesOfSeparation`, and include/exclude
101
+ query keep a neighborhood. `formatArkRunGraphMermaid()` / `graph.mermaid` is a
102
+ helper string, never a score. Inspector `GET /graph` serves the same slice.
103
+ Does not close Z09 / K01.
104
+
105
+ - **ArkRun skip corpus (RN14):** `tests/fixtures/arkrun-skip-corpus/` is the
106
+ executable proof: Application `new`, same-layer peer import, and homemade
107
+ `EventEmitter` stay green when `arkRun` is absent (Layers / ArkRules match
108
+ schema `1.1`) and fail write path, CLI, MCP, and `--strict-merge` when the
109
+ extra is enforced. Does not close Z09 / K01.
110
+
111
+ - **ArkRun skill-body deepen (RN15):** `/ark-runtime`, `/ark-place`, and
112
+ `/ark-adopt` teach the extra vs companion (advisory adopt, kernel-only
113
+ scaffold, composition-root wiring). Frozen **13** names — no `/ark-run`.
114
+ Skills never enforce; doctor `arkRun` stays `notAScore`. Agent Skills layout
115
+ stays 1:1 with `templates/skills`. Does not close Z09 / K01.
116
+
117
+ ## 4.6.7 — 2026-08-24
118
+
119
+ **Patch** over **4.6.6**. Production-hardening: CODEOWNERS, eval/pack honesty, CLI extracts,
120
+ spawn timeouts, and HTML list cap. **No required config migration.** Does not close Z09.
121
+
122
+ **Status: published** (on npm `latest`; see `docs/releases/4.6.7.md`).
123
+
124
+ ### Changed
125
+
126
+ - **CODEOWNERS:** `/src/`, `/bin/`, and `/schemas/` owned by `@pedroknigge`. GitHub still
127
+ needs `require_code_owner_reviews` (or an approving-review count) for that file to
128
+ enforce; the in-tree list is the product control.
129
+
130
+ - **Eval comparative fixture:** `saas-dashboard/without-ark` is a real Presentation→Domain
131
+ **value** import. Type-only was non-blocking and made the nightly oracle go false-green.
132
+
133
+ - **npm pack JSON:** `scripts/npm-pack-report.mjs` strips ANSI and parses JSON lines that
134
+ actually have `filename`. Empty `[]` / `{}` stay empty; missing filename still throws.
135
+
136
+ - **CLI extracts:** hook payload, package-manager helpers, and check args/config/watch live
137
+ in `bin/lib/`. Module-budget maxima were not raised.
138
+
139
+ - **git/gh timeouts:** `SPAWN_TIMEOUT_MS = 8000` on git/gh `spawnSync`. Timeout is
140
+ fail-closed (`status !== 0`).
141
+
142
+ - **HTML violation cap:** beginner and full showcase lists share a cap of 12 plus
143
+ `+N more (T total)`. KPI tiles still use the full array.
144
+
8
145
  ## 4.6.6 — 2026-08-22
9
146
 
10
147
  **Patch** over **4.6.5**. Phase AL corrective honesty plus a slimmer public docs surface.
11
148
  **No required config migration.** Does not close Z09. AL05 stays parked.
12
149
 
13
- **Status: current** (shipping as `arkgate@4.6.6`; see `docs/releases/4.6.6.md`).
150
+ **Status: published** (see `docs/releases/4.6.6.md`).
14
151
 
15
152
  ### Changed
16
153
 
@@ -49,7 +186,7 @@ in the immutable pre-2.0 archive linked below.
49
186
  **Patch** over **4.6.4**. Adoption, placement, doctor, upgrade, and write-path honesty for
50
187
  existing Next.js trees and multi-host teams. **No required config migration.**
51
188
 
52
- **Status: published** (on npm `latest` until 4.6.6 lands; see `docs/releases/4.6.5.md`).
189
+ **Status: published** (see `docs/releases/4.6.5.md`).
53
190
 
54
191
  ### Changed
55
192
 
package/README.md CHANGED
@@ -16,11 +16,11 @@ and makes sure a “green” check means something real.
16
16
 
17
17
  </div>
18
18
 
19
- > **ArkGate 4.6.6** is current (this train). **4.6.5** remains npm `latest` until publish.
19
+ > **ArkGate 4.7.0** is prepared on this tree. **4.6.7** remains npm `latest` until publish.
20
20
  > A tree is **adopted** only with a required GitHub status running `arkgate-check --strict-merge`,
21
21
  > or `.ark/adoption-stance.json` `stance: "advisory-only"`. Doctor is compact (`--doctor --all`
22
- > for Details). [4.6.6 notes](docs/releases/4.6.6.md) · [4.6.5](docs/releases/4.6.5.md) ·
23
- > [Docs hub](docs/README.md) · [Product voice](docs/product-voice.md)
22
+ > for Details). [4.7.0 notes](docs/releases/4.7.0.md) · [4.6.7](docs/releases/4.6.7.md) ·
23
+ > [4.6.6](docs/releases/4.6.6.md) · [Docs hub](docs/README.md) · [Product voice](docs/product-voice.md)
24
24
 
25
25
  ---
26
26
 
@@ -78,11 +78,12 @@ A machine-readable architecture file (`ark.config.json`) plus enforcement:
78
78
  |-------|----------------|--------|
79
79
  | **Layers** (always) | Who may import whom — imports, placement, purity, isolation | `ark.config.json` layers + rules |
80
80
  | **ArkRules** (opt-in; structure rules inside a layer) | Habits *inside* a layer — structure sensors + domain invariants as data | `arkRules` → `arkrules/<Layer>.json` |
81
+ | **ArkRun** (opt-in extra) | Kernel usage + complete declarations | `arkRun` on schema `1.2` |
81
82
 
82
- Absence of ArkRules changes no inter-layer verdict. Label residual **`[Layer]`** vs **`[ArkRules]`**.
83
- Details: [configuration](docs/configuration.md#arkrules-intra-layer-opt-in) · [use path](docs/use.md).
83
+ Absence of ArkRules or ArkRun changes no inter-layer verdict. Label residual **`[Layer]`** vs **`[ArkRules]`**.
84
+ Details: [configuration](docs/configuration.md) · [use path](docs/use.md).
84
85
 
85
- **Not** a web framework, ORM, or job runner. Optional experimental runtime is separate and not required for the gate.
86
+ **Not** a web framework, ORM, or job runner. Optional **ArkRun** extra and companion kernel (`@arkgate/runtime`) are separate and not required for the gate. In-memory stores are not production durability.
86
87
 
87
88
  **Name note:** npm package `arkgate` — not affiliated with the separate Archgate CLI project.
88
89
 
@@ -192,9 +193,18 @@ More: [docs/develop.md](docs/develop.md) · skills install: [docs/agent-guide.md
192
193
 
193
194
  ---
194
195
 
195
- ## Optional experimental runtime
196
+ ## Optional ArkRun kernel
196
197
 
197
- Gates need **no** app runtime. The experimental `@arkgate/runtime` companion is separate and is not a production-readiness claim.
198
+ Gates need **no** app runtime. The experimental **ArkRun** companion (`@arkgate/runtime`) is separate
199
+ and is not a production-readiness claim. `createStrictArkKernel` is the factory: each call creates
200
+ an isolated instance (no process-wide singleton). Managed components declare `uses` / `reactsTo` /
201
+ `raises` / `sends`; `getDependencyInformationPackage()` is a JSON snapshot and never leaks factories.
202
+ `requestGraph()` slices that snapshot into process or technical graphs (`nodeIds`,
203
+ `degreesOfSeparation`, include/exclude query) with a Mermaid helper. `send()` is local /
204
+ localBlocking / broker (broker falls back to in-process local; `ephemeral`
205
+ defaults true; no cloud SDKs in the package). Opt-in `startInspector()` binds `127.0.0.1`,
206
+ refuses `NODE_ENV=production`, and lazy-loads HTTP for JSON snapshots, SSE, and `/graph`. The kernel is
207
+ not bundled in the `arkgate` tarball.
198
208
 
199
209
  ### Durability stance
200
210
 
@@ -217,8 +227,10 @@ for real systems. Details: [docs/production-hardening.md](docs/production-harden
217
227
  | Config · package surface · TS | [configuration](docs/configuration.md) · [package-surface](docs/package-surface.md) · [typescript-support](docs/typescript-support.md) |
218
228
  | Brownfield | [docs/brownfield-adoption.md](docs/brownfield-adoption.md) |
219
229
  | Security | [SECURITY.md](SECURITY.md) |
220
- | Current release (4.6.6) | [docs/releases/4.6.6.md](docs/releases/4.6.6.md) · [CHANGELOG](CHANGELOG.md) |
221
- | Prior published (4.6.5 on npm `latest` until 4.6.6 lands) | [docs/releases/4.6.5.md](docs/releases/4.6.5.md) |
230
+ | Current tree (4.7.0 prepared) | [docs/releases/4.7.0.md](docs/releases/4.7.0.md) · [CHANGELOG](CHANGELOG.md) |
231
+ | Current published (4.6.7 on npm `latest`) | [docs/releases/4.6.7.md](docs/releases/4.6.7.md) |
232
+ | Prior published (4.6.6) | [docs/releases/4.6.6.md](docs/releases/4.6.6.md) |
233
+ | Prior published (4.6.5) | [docs/releases/4.6.5.md](docs/releases/4.6.5.md) |
222
234
  | Prior published (4.6.3) | [docs/releases/4.6.3.md](docs/releases/4.6.3.md) |
223
235
  | Prior (4.6.2) | [docs/releases/4.6.2.md](docs/releases/4.6.2.md) |
224
236
  | Prior (4.6.1) | [docs/releases/4.6.1.md](docs/releases/4.6.1.md) |
package/SECURITY.md CHANGED
@@ -18,7 +18,7 @@ Preferred path: use GitHub's private vulnerability reporting for this repository
18
18
  - reproduction steps
19
19
  - expected impact
20
20
  - whether the issue affects `arkgate-check` / `ark-check`, `arkgate-mcp` / `ark-mcp`,
21
- generated agent gates, the GitHub Action, or the optional runtime kernel
21
+ generated agent gates, the GitHub Action, or the optional ArkRun kernel
22
22
 
23
23
  If private vulnerability reporting is unavailable, open a minimal public issue asking for
24
24
  a private security contact without including exploit details.
@@ -7,7 +7,6 @@ import { fileURLToPath } from 'node:url';
7
7
  const __arkCheckCli = fileURLToPath(new URL('./ark-check.mjs', import.meta.url));
8
8
 
9
9
  import {
10
- DEFAULT_DOMAIN_FORBIDDEN_GLOBALS,
11
10
  DEFAULT_INTENT_PREFIXES,
12
11
  DEFAULT_LAYER_DIRECTORIES,
13
12
  DEFAULT_RULES,
@@ -80,7 +79,6 @@ import {
80
79
  CONCENTRATION_MIN_VIOLATIONS,
81
80
  } from './lib/violations.mjs';
82
81
  import {
83
- suggestLayerForDir,
84
82
  detectBestFitModel,
85
83
  dirSegmentsFromGlob,
86
84
  } from './lib/suggestions.mjs';
@@ -126,174 +124,9 @@ import {
126
124
  resolveEffectiveProjectRoot,
127
125
  } from './lib/project-root.mjs';
128
126
  import { demoteArkRuleTeethUnderClassificationFloor } from './lib/rules-under-contract.mjs';
129
-
130
- function resolveDesignDeltaBaseRef(root, explicit, env = process.env) {
131
- const flag = typeof explicit === 'string' ? explicit.trim() : '';
132
- if (flag) return flag;
133
- const envRef = normalizePolicyBaseRef(env.ARK_POLICY_BASE_REF);
134
- if (envRef) return envRef;
135
- const githubBase = typeof env.GITHUB_BASE_REF === 'string' ? env.GITHUB_BASE_REF.trim() : '';
136
- if (githubBase) return `origin/${githubBase}`;
137
- return discoverLocalBaseRef(root) || undefined;
138
- }
139
-
140
- function parseArgs(argv) {
141
- const args = {
142
- root: process.cwd(),
143
- config: 'ark.config.json',
144
- manifest: undefined,
145
- printConfig: undefined,
146
- tsconfig: undefined,
147
- json: false,
148
- strictConfig: false,
149
- strictMerge: false,
150
- requireGates: false,
151
- requireWriteHook: undefined,
152
- init: false,
153
- installAgentGates: false,
154
- compact: false,
155
- tools: undefined,
156
- force: false,
157
- skillsOnly: false,
158
- baseline: undefined,
159
- policyBase: undefined,
160
- policyBaseRef: undefined,
161
- policyAck: undefined, failOnNewSmells: false, baseRef: undefined,
162
- contractSession: false,
163
- contractDiff: false,
164
- changed: false,
165
- against: undefined,
166
- base: undefined,
167
- persona: undefined,
168
- author: undefined,
169
- failUngoverned: false,
170
- updateBaseline: false,
171
- noCache: false,
172
- resident: false,
173
- coverage: false,
174
- migrateCommands: false,
175
- doctor: false,
176
- plan: false,
177
- recommend: false,
178
- writePlan: false,
179
- listPolicyPacks: false,
180
- applyPolicyPack: undefined,
181
- watch: false,
182
- beginner: false,
183
- openReport: false,
184
- noOpenReport: false,
185
- version: false,
186
- help: false,
187
- all: false,
188
- followConfigRoot: false,
189
- };
190
- const requireValue = (flag, index) => {
191
- const value = argv[index + 1];
192
- if (value === undefined || value.startsWith('-')) {
193
- throw new Error(`Missing value for ${flag}. Run arkgate-check --help for usage.`);
194
- }
195
- return value;
196
- };
197
- for (let i = 2; i < argv.length; i += 1) {
198
- const arg = argv[i];
199
- if (arg === '--json') args.json = true;
200
- else if (arg === '--strict' || arg === '--strict-merge') {
201
- args.strictConfig = true;
202
- args.requireGates = true;
203
- args.strictMerge = true;
204
- }
205
- else if (arg === '--strict-config') args.strictConfig = true;
206
- else if (arg === '--require-gates') {
207
- args.requireGates = true;
208
- args.strictConfig = true;
209
- }
210
- else if (arg === '--require-write-hook') {
211
- args.requireWriteHook = requireValue(arg, i++).trim().toLowerCase();
212
- }
213
- else if (arg === '--init') args.init = true;
214
- else if (arg === '--preset') args.preset = requireValue(arg, i++);
215
- else if (arg === '--install-agent-gates') args.installAgentGates = true;
216
- else if (arg === '--compact') args.compact = true;
217
- else if (arg === '--tools') {
218
- // Consume the next arg only when it isn't another flag (same rule as --baseline),
219
- // so `--tools --force` can't silently eat --force as a "tool name".
220
- const next = argv[i + 1];
221
- if (next !== undefined && !next.startsWith('-')) {
222
- i += 1;
223
- args.tools = next
224
- .split(',')
225
- .map((tool) => tool.trim().toLowerCase())
226
- .filter(Boolean);
227
- } else {
228
- args.tools = []; // flag without a value — rejected in runInstallAgentGates
229
- }
230
- }
231
- else if (arg === '--force') args.force = true;
232
- else if (arg === '--follow-config-root') args.followConfigRoot = true;
233
- else if (arg === '--skills-only') args.skillsOnly = true;
234
- else if (arg === '--coverage') args.coverage = true;
235
- else if (arg === '--doctor') args.doctor = true;
236
- else if (arg === '--plan') args.plan = true;
237
- else if (arg === '--rules-inventory') args.rulesInventory = true;
238
- else if (arg === '--recommend') args.recommend = true;
239
- else if (arg === '--write-plan') args.writePlan = true;
240
- else if (arg === '--list-policy-packs') args.listPolicyPacks = true;
241
- else if (arg === '--apply-policy-pack') args.applyPolicyPack = requireValue(arg, i++);
242
- else if (arg === '--suggest-include') args.suggestInclude = true;
243
- else if (arg === '--adopt-contract') args.adoptContract = true;
244
- else if (arg === '--migrate-contract') args.migrateContract = true;
245
- else if (arg === '--ratchet-cores') args.ratchetCores = true;
246
- else if (arg === '--write') args.write = true;
247
- else if (arg === '--watch') args.watch = true;
248
- else if (arg === '--beginner') args.beginner = true;
249
- else if (arg === '--codex-home') args.codexHome = true;
250
- else if (arg === '--claude-home') args.claudeHome = true;
251
- else if (arg === '--grok-home') args.grokHome = true;
252
- else if (arg === '--agent-homes') {
253
- args.agentHomes = true;
254
- args.codexHome = true;
255
- args.claudeHome = true;
256
- args.grokHome = true;
257
- }
258
- else if (arg === '--migrate-commands') args.migrateCommands = true;
259
- else if (arg === '--no-cache') args.noCache = true;
260
- else if (arg === '--resident') args.resident = true;
261
- else if (arg === '--report') {
262
- const next = argv[i + 1];
263
- args.report = next && !next.startsWith('-') ? argv[++i] : 'ark-report.html';
264
- }
265
- else if (arg === '--reset-origin') args.resetOrigin = true;
266
- else if (arg === '--no-archive') args.noArchive = true;
267
- else if (arg === '--open') args.openReport = true;
268
- else if (arg === '--no-open') args.noOpenReport = true;
269
- else if (arg === '--baseline' || arg === '--update-baseline') {
270
- if (arg === '--update-baseline') args.updateBaseline = true;
271
- // optional path value: consume the next arg only when it isn't another flag
272
- const next = argv[i + 1];
273
- args.baseline = next && !next.startsWith('-') ? argv[++i] : '.ark-baseline.json';
274
- }
275
- else if (arg === '--policy-base') args.policyBase = requireValue(arg, i++);
276
- else if (arg === '--policy-base-ref') args.policyBaseRef = requireValue(arg, i++);
277
- else if (arg === '--policy-ack') args.policyAck = requireValue(arg, i++); else if (arg === '--fail-on-new-smells') args.failOnNewSmells = true; else if (arg === '--base-ref') args.baseRef = requireValue(arg, i++);
278
- else if (arg === '--contract-session') args.contractSession = true;
279
- else if (arg === '--contract-diff') args.contractDiff = true;
280
- else if (arg === '--changed') args.changed = true;
281
- else if (arg === '--against') args.against = requireValue(arg, i++);
282
- else if (arg === '--base') args.base = requireValue(arg, i++);
283
- else if (arg === '--persona') args.persona = requireValue(arg, i++);
284
- else if (arg === '--author') args.author = requireValue(arg, i++);
285
- else if (arg === '--root') args.root = path.resolve(requireValue(arg, i++));
286
- else if (arg === '--config') args.config = requireValue(arg, i++);
287
- else if (arg === '--manifest') args.manifest = requireValue(arg, i++);
288
- else if (arg === '--print-config') args.printConfig = requireValue(arg, i++);
289
- else if (arg === '--tsconfig') args.tsconfig = requireValue(arg, i++);
290
- else if (arg === '--help' || arg === '-h') args.help = true;
291
- else if (arg === '--all') args.all = true;
292
- else if (arg === '--version' || arg === '-V') args.version = true;
293
- else throw new Error(`Unknown argument: ${arg}. Run arkgate-check --help for usage.`);
294
- }
295
- return args;
296
- }
127
+ import { parseArgs, resolveDesignDeltaBaseRef } from './lib/check-args.mjs';
128
+ import { detectConfig, proposeForUncovered } from './lib/check-config-detect.mjs';
129
+ import { runWatchMode } from './lib/check-watch.mjs';
297
130
 
298
131
  /** Path shown to humans: project-relative when inside root, absolute otherwise (no `../../..`). */
299
132
  function displayPathFromRoot(root, absPath) {
@@ -317,101 +150,6 @@ function readConfig(root, configPath) {
317
150
  return parseArkConfigJson(fs.readFileSync(fullPath, 'utf8'), fullPath).config;
318
151
  }
319
152
 
320
- /**
321
- * Infer an ark.config.json from the directories that actually exist in the project,
322
- * using the same layer→directory conventions as the eleven-layer template. A directory
323
- * only counts when it contains at least one source file, so an empty scaffold dir can't
324
- * produce a layer whose pattern matches nothing (which --strict-config would fail).
325
- */
326
- function detectConfig(root) {
327
- const srcDir = fs.existsSync(path.join(root, 'src')) ? 'src' : '.';
328
- const layers = [];
329
-
330
- for (const entry of DEFAULT_INTENT_PREFIXES) {
331
- const directories = (DEFAULT_LAYER_DIRECTORIES[entry.layer] ?? []).filter(
332
- (directory) => walk(path.join(root, srcDir, directory), [], { root }).length > 0
333
- );
334
- if (directories.length === 0) continue;
335
- layers.push({
336
- name: entry.layer,
337
- patterns: directories.map((directory) => `${normalize(path.join(srcDir, directory))}/**`),
338
- intentPrefixes: entry.prefixes,
339
- ...(entry.layer === 'DomainModel'
340
- ? { forbiddenGlobals: DEFAULT_DOMAIN_FORBIDDEN_GLOBALS }
341
- : {}),
342
- });
343
- }
344
-
345
- const names = new Set(layers.map((layer) => layer.name));
346
- const rules = DEFAULT_RULES.filter((rule) => names.has(rule.from) && names.has(rule.to));
347
-
348
- return { srcDir, config: { include: [srcDir], layers, rules } };
349
- }
350
-
351
- /** Top-level directories under srcDir not covered by any detected layer pattern. */
352
- function uncoveredDirectories(root, srcDir, layers) {
353
- const base = path.join(root, srcDir);
354
- if (!fs.existsSync(base)) return [];
355
- return fs
356
- .readdirSync(base, { withFileTypes: true })
357
- .filter(
358
- (entry) =>
359
- entry.isDirectory() &&
360
- entry.name !== 'node_modules' &&
361
- entry.name !== 'dist' &&
362
- !entry.name.startsWith('.')
363
- )
364
- .map((entry) => entry.name)
365
- .filter((name) => {
366
- const prefix = `${normalize(path.join(srcDir, name))}/`;
367
- return !layers.some((layer) =>
368
- layer.patterns.some((pattern) => pattern.startsWith(prefix))
369
- );
370
- });
371
- }
372
-
373
- // detectWorkspaces: shared implementation in ark-shared.mjs (npm/pnpm/rush/lerna +
374
- // conventional multi-package roots).
375
-
376
- // Deny every "upward" edge for an ordered layer list (index 0 = outermost/top,
377
- // which may import everything below it). Inner/lower layers must not import outer
378
- // ones — the shared shape behind linear layered and feature-sliced layouts.
379
-
380
- function proposeForUncovered(root, srcDir, layers) {
381
- const proposals = [];
382
- for (const top of uncoveredDirectories(root, srcDir, layers)) {
383
- const direct = suggestLayerForDir(top);
384
- if (direct) {
385
- proposals.push({ dir: `${srcDir}/${top}`, ...direct });
386
- continue;
387
- }
388
- let children = [];
389
- try {
390
- children = fs
391
- .readdirSync(path.join(root, srcDir, top), { withFileTypes: true })
392
- .filter((e) => e.isDirectory() && e.name !== 'node_modules' && !e.name.startsWith('.'))
393
- .map((e) => e.name);
394
- } catch {
395
- /* not a readable directory — treat as unrecognized below */
396
- }
397
- if (children.length > 0) {
398
- // Descend: propose per child so a mixed `lib/` yields lib/repositories → Persistence
399
- // AND flags lib/db as unrecognized, instead of dropping the parts Ark can't place.
400
- for (const child of children) {
401
- const hit = suggestLayerForDir(child);
402
- proposals.push(
403
- hit
404
- ? { dir: `${srcDir}/${top}/${child}`, ...hit }
405
- : { dir: `${srcDir}/${top}/${child}`, unrecognized: true }
406
- );
407
- }
408
- } else {
409
- proposals.push({ dir: `${srcDir}/${top}`, unrecognized: true });
410
- }
411
- }
412
- return proposals;
413
- }
414
-
415
153
  function printInitNextSteps(root) {
416
154
  console.log('');
417
155
  console.log('Next steps:');
@@ -1733,6 +1471,7 @@ async function main() {
1733
1471
  score: fitness.score,
1734
1472
  mode: fitness.mode,
1735
1473
  improvementCompass: reportCompass,
1474
+ arkRun: designDepth?.arkRun ?? null,
1736
1475
  }),
1737
1476
  leftoverDesignWork: designDepth?.designFitness?.designWeak === true,
1738
1477
  };
@@ -1755,7 +1494,17 @@ async function main() {
1755
1494
  adoption: adoptionForReport,
1756
1495
  designDepth,
1757
1496
  advisories: {
1758
- ...computeDoctorAdvisories(root, config, coverage, rules, files, ts, parseHealth),
1497
+ ...computeDoctorAdvisories(
1498
+ root,
1499
+ config,
1500
+ coverage,
1501
+ rules,
1502
+ files,
1503
+ ts,
1504
+ parseHealth,
1505
+ undefined,
1506
+ activeViolations
1507
+ ),
1759
1508
  // Doctor parity: always emit improvement compass when doctor would (reportParity).
1760
1509
  ...(designDepth?.improvementCompass
1761
1510
  ? { improvementCompass: designDepth.improvementCompass }
@@ -1764,6 +1513,7 @@ async function main() {
1764
1513
  ...(designDepth?.deepModuleCoach
1765
1514
  ? { deepModuleCoach: designDepth.deepModuleCoach }
1766
1515
  : {}),
1516
+ ...(designDepth?.arkRun ? { arkRun: designDepth.arkRun } : {}),
1767
1517
  },
1768
1518
  };
1769
1519
  const html = args.beginner
@@ -1964,87 +1714,17 @@ async function main() {
1964
1714
  }
1965
1715
 
1966
1716
  if (args.watch) {
1967
- await runWatchMode(args);
1717
+ await runWatchMode(args, {
1718
+ cliPath: __arkCheckCli,
1719
+ loadConfig: readConfig,
1720
+ dim: color.dim,
1721
+ });
1968
1722
  return;
1969
1723
  }
1970
1724
 
1971
1725
  process.exitCode = designCheck.exitCode(observedOk && (!args.strictMerge || analysisComplete) ? 0 : 1);
1972
1726
  }
1973
1727
 
1974
- async function runWatchMode(args) {
1975
- const argv = process.argv.slice(2).filter((token) => token !== '--watch');
1976
- let debounce;
1977
- const rerun = () => {
1978
- clearTimeout(debounce);
1979
- debounce = setTimeout(() => {
1980
- const result = spawnSync(process.execPath, [__arkCheckCli, ...argv], {
1981
- cwd: args.root,
1982
- stdio: 'inherit',
1983
- env: process.env,
1984
- });
1985
- process.exitCode = result.status ?? 1;
1986
- }, 300);
1987
- };
1988
-
1989
- let config;
1990
- try {
1991
- config = readConfig(args.root, args.config);
1992
- } catch (error) {
1993
- console.error(error instanceof Error ? error.message : String(error));
1994
- process.exitCode = 2;
1995
- return;
1996
- }
1997
-
1998
- for (const entry of config.include ?? []) {
1999
- const target = path.join(args.root, entry);
2000
- if (!fs.existsSync(target)) continue;
2001
- try {
2002
- const watcher = fs.watch(target, { recursive: true }, rerun);
2003
- watcher.on('error', () => {
2004
- watcher.close();
2005
- watchByPolling(target, rerun);
2006
- });
2007
- } catch {
2008
- watchByPolling(target, rerun);
2009
- }
2010
- }
2011
-
2012
- console.log(color.dim('Watching governed paths for changes… (Ctrl+C to stop)'));
2013
- await new Promise(() => {});
2014
- }
2015
-
2016
- function watchByPolling(target, onChange) {
2017
- let previous = watchFingerprint(target);
2018
- setInterval(() => {
2019
- const current = watchFingerprint(target);
2020
- if (current === previous) return;
2021
- previous = current;
2022
- onChange();
2023
- }, 250);
2024
- }
2025
-
2026
- function watchFingerprint(target) {
2027
- const pending = [target];
2028
- const entries = [];
2029
- while (pending.length > 0) {
2030
- const current = pending.pop();
2031
- let stat;
2032
- try {
2033
- stat = fs.statSync(current);
2034
- } catch {
2035
- continue;
2036
- }
2037
- entries.push(`${current}:${stat.mtimeMs}:${stat.size}`);
2038
- if (!stat.isDirectory()) continue;
2039
- try {
2040
- for (const name of fs.readdirSync(current)) pending.push(path.join(current, name));
2041
- } catch {
2042
- // A concurrent delete is represented by the next fingerprint.
2043
- }
2044
- }
2045
- return entries.sort().join('|');
2046
- }
2047
-
2048
1728
  main().catch((error) => {
2049
1729
  console.error(
2050
1730
  process.env.ARK_DEBUG_STACK === '1' && error instanceof Error