arkgate 2.10.0 → 2.12.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 (55) hide show
  1. package/CHANGELOG.md +107 -0
  2. package/README.md +21 -12
  3. package/SECURITY.md +3 -4
  4. package/bin/ark-check.mjs +41 -16
  5. package/bin/ark-mcp.mjs +54 -10
  6. package/bin/ark.mjs +87 -24
  7. package/bin/lib/agent-gates.mjs +68 -2090
  8. package/bin/lib/architecture-scan.mjs +4 -1
  9. package/bin/lib/baseline-key.mjs +17 -0
  10. package/bin/lib/ci-and-commands.mjs +386 -0
  11. package/bin/lib/config-warnings.mjs +22 -0
  12. package/bin/lib/core-layers.mjs +7 -0
  13. package/bin/lib/core-ratchet.mjs +3 -7
  14. package/bin/lib/deploy-path.mjs +205 -0
  15. package/bin/lib/doctor-plan.mjs +29 -5
  16. package/bin/lib/gate-files.mjs +223 -0
  17. package/bin/lib/hook-templates.mjs +99 -0
  18. package/bin/lib/install-migrate.mjs +442 -0
  19. package/bin/lib/mcp-adoption.mjs +423 -0
  20. package/bin/lib/presets.mjs +3 -0
  21. package/bin/lib/safety-diagnostics.mjs +263 -0
  22. package/bin/lib/scan-files.mjs +51 -6
  23. package/bin/lib/skill-install.mjs +259 -0
  24. package/bin/lib/typescript-host.mjs +88 -0
  25. package/bin/lib/violations.mjs +3 -3
  26. package/bin/lib/write-path-detect.mjs +138 -0
  27. package/dist/index.cjs +103 -8
  28. package/dist/index.cjs.map +1 -1
  29. package/dist/index.d.cts +5 -3
  30. package/dist/index.d.ts +5 -3
  31. package/dist/index.js +103 -8
  32. package/dist/index.js.map +1 -1
  33. package/dist/nestjs/index.cjs +18 -5
  34. package/dist/nestjs/index.cjs.map +1 -1
  35. package/dist/nestjs/index.d.cts +1 -1
  36. package/dist/nestjs/index.d.ts +1 -1
  37. package/dist/nestjs/index.js +18 -5
  38. package/dist/nestjs/index.js.map +1 -1
  39. package/dist/runtime/index.cjs +103 -8
  40. package/dist/runtime/index.cjs.map +1 -1
  41. package/dist/runtime/index.d.cts +1 -1
  42. package/dist/runtime/index.d.ts +1 -1
  43. package/dist/runtime/index.js +103 -8
  44. package/dist/runtime/index.js.map +1 -1
  45. package/dist/{types-D6Q8WHes.d.cts → types-BZ17b9i5.d.cts} +5 -1
  46. package/dist/{types-D6Q8WHes.d.ts → types-BZ17b9i5.d.ts} +5 -1
  47. package/docs/agent-guide.md +12 -2
  48. package/docs/ai-gates.md +20 -2
  49. package/docs/package-surface.md +10 -3
  50. package/docs/production-hardening.md +5 -0
  51. package/package.json +5 -2
  52. package/server.json +2 -2
  53. package/templates/skills/ark-autopilot.md +77 -45
  54. package/templates/skills/ark-explain.md +2 -1
  55. package/templates/skills/ark-explore.md +135 -34
package/CHANGELOG.md CHANGED
@@ -4,6 +4,113 @@ All notable changes to ArkGate (`arkgate`; formerly `ark-runtime-kernel`) are do
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## 2.12.0 — 2026-07-10
8
+
9
+ ### Fixed
10
+
11
+ - **Install agent gates on temp roots:** skip rewriting the developer's real
12
+ `~/.codex/config.toml` when the project root is a temp/upgrade scratch and
13
+ `CODEX_HOME` is unset. Home MCP wire failures no longer fail an otherwise
14
+ successful repo gate install (sandbox/EPERM). Explicit `CODEX_HOME` and
15
+ `--codex-home` still wire as before.
16
+ - **Q1 coverage floors (broad include, 80/85/95):** Vitest thresholds statements/lines **≥80%**,
17
+ branches/functions **≥85%** on the **full product unit surface** (`src/**` + `bin/lib/**` +
18
+ `bin/ark-shared.mjs`; only process-entry shells excluded — no cherry-picked enforcement-core
19
+ include). Per-path critical floors: write-path-detect / auto-patch / prepare-write /
20
+ safety-diagnostics / baseline-key / graph-cycles at **≥95%** branch. Real branch-driving tests
21
+ under `tests/unit/static-check/` (critical + surface/topup/seam suites). Two consecutive
22
+ green `npm run test:coverage` captures (stmts/lines **92.71%**, branches **85%**, functions
23
+ **94.76%**; critical modules all **≥95%** branch).
24
+ - **agent-gates modularization:** thin facade (`bin/lib/agent-gates.mjs` ~100 LOC) re-exports
25
+ `gate-files`, `skill-install`, `ci-and-commands`, `mcp-adoption`, `install-migrate`,
26
+ `typescript-host`, `hook-templates`, `write-path-detect`, plus field/codex helpers.
27
+ `detectDeployPathQuality` extracted to `bin/lib/deploy-path.mjs` so `mcp-adoption.mjs` stays
28
+ under the 600 LOC module budget. Import hygiene on extract modules; `loadTypeScript` uses
29
+ `__arkCheckCli` for the nested arkgate TypeScript fallback.
30
+ - **Deny→repair CI proof:** `tests/unit/static-check/writePathDetect.test.ts` drives
31
+ shipped `bin/ark-mcp.mjs --hook --hook-repair` and asserts `ARK_REPAIR_JSON` /
32
+ `ARK_AUTOPATCH_JSON` on deny (exit 2); reject-only without repair flag still supported.
33
+ - **Dogfood write path repair:** local Claude/Grok hooks use `--hook-repair`; doctor
34
+ reports `writePath.mode = repair` on this tree.
35
+ - **Self-hosted AGENTS.md:** `--install-agent-gates --force` no longer overwrites library
36
+ mother-repo Identity (`skipped-self-hosted`).
37
+ - **hexagonal-order-api:** `safety.allowInMemory` for ephemeral demo kernel; prefer
38
+ `arkgate/runtime` imports; `npm run check` green under `--strict-config`.
39
+ - **multi-app / monorepo rules:** deny App→Persistence, Presentation→Domain, and
40
+ Persistence→Presentation (parity with crud-product starter).
41
+ - **Generated CI Node default lags local npm (again):** when a project had no
42
+ `.nvmrc` / `engines.node`, the Ark architecture gate workflow defaulted to
43
+ Node 22. Lockfiles written on Node 24/26 then failed `npm ci` with
44
+ "Missing: … from lock file" before `ark-check` ran — CI green, Ark red.
45
+ Detection order is now `.nvmrc` / `.node-version` → `engines.node` → **highest
46
+ `node-version` from sibling workflows** (excludes `ark-check.yml` so a stale
47
+ gate cannot re-pin itself) → default **24**. Refresh existing gates with
48
+ `ark-check --install-agent-gates --force` (or edit `node-version` in
49
+ `.github/workflows/ark-check.yml`).
50
+
51
+ ### Changed
52
+
53
+ - **Hook templates extracted** to `bin/lib/hook-templates.mjs` (agent-gates seam).
54
+ - **Write-path detect extracted** to `bin/lib/write-path-detect.mjs` (doctor W5; re-exported
55
+ from agent-gates).
56
+ - **Coverage thresholds** raised to Q1 floors on the broad include set: statements/lines **≥80**,
57
+ branches/functions **≥85**, critical write/safety modules **≥95%** branch (see Fixed above).
58
+ - **`/ark-explore` skill:** decision-grade recon — field path (run starters/checks),
59
+ installed hooks vs install templates, coupling via fan-in/exports (not LOC alone),
60
+ ranked “así te lo re-soluciono” rows only when residual changes action; ENFORCE /
61
+ empty plan treated as baseline, not the story. **v2.1:** output modes (recon vs
62
+ dual-plan seed, no multi-week roadmaps by default); path-correct vs design-correct
63
+ + semantic false-green; success signals and kill-switches on bets (anti-vanity).
64
+ - **`/ark-autopilot` skill:** explore-first (decision-grade), **dual plan** —
65
+ A remediation from `--plan` + B pattern/evolution bets (never auto-apply B as
66
+ mechanical-safe); empty plan no longer means “healthy” without explore/B.
67
+ - **Day-zero origin first:** `ark start` / `ark init` freeze `.ark/reports/origin.*`
68
+ immediately after `ark.config.json` exists and **before** agent docs, skills, and CI
69
+ templates. Later `--report` still shows evolution vs that snapshot.
70
+
71
+ ## 2.11.0 — 2026-07-10
72
+
73
+ Fail-closed enforcement hardening: `--strict` now combines contract coverage, installed-gate
74
+ checks, write-hook presence, and bypass diagnostics in one CI profile. The GitHub Action runs
75
+ the exact checked-out revision by default, and runtime workflows cancel timed-out work
76
+ cooperatively.
77
+
78
+ ### Added
79
+
80
+ - **Strict CI profile:** `arkgate-check --strict` enables strict config validation, requires
81
+ generated gates plus a PreToolUse write hook, and fails on configured safety diagnostics.
82
+ - **Bypass diagnostics:** new `dynamicImportAllowlist` and `safety` config fields detect
83
+ non-literal dynamic imports, TypeScript suppression directives, explicit `any` casts,
84
+ production InMemory defaults, and disabled or omitted peer isolation. `--doctor --json`
85
+ exposes the same evidence under `doctor.safety`.
86
+ - **Release-quality CI:** JavaScript syntax validation, enforced coverage thresholds, and a
87
+ Node 18/20/22/24 compatibility matrix now run before merge.
88
+
89
+ ### Changed
90
+
91
+ - **Pinned GitHub Action execution:** `uses: pedroknigge/arkgate@<tag-or-SHA>` now runs that
92
+ checked-out ArkGate source. The `version` input remains available only as an explicit exact
93
+ npm compatibility override.
94
+ - **Complete MCP contract:** `ark://manifest` exposes every configured file layer separately
95
+ from runtime intent layers, plus reviewed dynamic-import and safety policy.
96
+ - **Workflow cancellation contract:** workflow steps receive an `AbortSignal` as their third
97
+ argument. `timeoutMs` aborts that signal, clears the active step, and rejects duplicate step
98
+ names before execution can corrupt compensation order.
99
+ - **Filesystem confinement:** source scans follow internal symlinks once and reject symlinks
100
+ that escape the project root.
101
+
102
+ ### Fixed
103
+
104
+ - **Baseline duplicate honesty:** repeated violations now receive stable per-occurrence keys,
105
+ so adding a second identical violation is new debt instead of being hidden by one baseline
106
+ entry.
107
+ - **Write-hook duplicate honesty:** proposed writes compare violation counts, preventing a new
108
+ duplicate from being mistaken for an already-existing violation.
109
+ - **CLI argument safety:** unknown flags and missing flag values fail with usage guidance
110
+ instead of silently weakening enforcement or throwing an internal error.
111
+ - **Action gate detection:** repositories using the ArkGate composite Action satisfy the CI
112
+ gate check without needing a separate literal `ark-check` command.
113
+
7
114
  ## 2.10.0 — 2026-07-10
8
115
 
9
116
  Track W — **Constrained write → verified repair**: write-boundary autoPatch, prepare_write,
package/README.md CHANGED
@@ -23,8 +23,8 @@ and makes sure a “green” check means something real.
23
23
  If you remember nothing else:
24
24
 
25
25
  ```text
26
- 1. npx arkgate start ← install shape + gates + origin report
27
- 2. /ark-autopilot ← in your agent: adopt, fix, leave gates on
26
+ 1. npx arkgate start ← walk tree contract **day-zero origin** → gates
27
+ 2. /ark-autopilot ← explore first, dual plan, safe fixes, leave gates on
28
28
  3. npx arkgate-check --doctor ← “where am I?” anytime (one status screen)
29
29
  ```
30
30
 
@@ -106,7 +106,7 @@ Full checklist (CI, MCP, Codex, imports): **[docs/migrate-from-ark-runtime-kerne
106
106
 
107
107
  ```bash
108
108
  npm install -D arkgate typescript
109
- npx arkgate start # setup + origin report
109
+ npx arkgate start # contract day-zero origin → gates + plan
110
110
  # in agent:
111
111
  # /ark-autopilot
112
112
  npx arkgate-check --doctor # status light + next action
@@ -117,11 +117,13 @@ Aliases `ark` / `ark-check` / `ark-mcp` still work. **npm / pnpm / yarn**. No in
117
117
  <details>
118
118
  <summary>What <code>/ark-autopilot</code> does under the hood (optional detail)</summary>
119
119
 
120
- 1. Setup if needed (`ark start`).
121
- 2. Origin architecture report (before picture in `.ark/reports/`).
122
- 3. Adoption: match contract to real folders, raise governed %.
123
- 4. Plan + safe auto-fixes; judgment when you ask for full apply.
124
- 5. Gates on + after report (evolution vs origin).
120
+ 1. Setup if needed (`ark start` — contract, then **day-zero origin**, then gates).
121
+ 2. **Explore pass** (decision-grade map of *this* product; field path when demos exist).
122
+ 3. **Dual plan:** **A** remediation from `--plan` (mechanical-safe only by default); **B** pattern/evolution bets (never auto-applied as mechanical-safe). Empty plan ≠ “healthy” without explore/B.
123
+ 4. Apply A re-check; judgment only when you ask for full apply.
124
+ 5. Gates on + latest report (evolution vs frozen origin).
125
+
126
+ Standalone recon without applying: `/ark-explore`.
125
127
 
126
128
  </details>
127
129
 
@@ -188,8 +190,14 @@ ark.config.json
188
190
  - **Presets:** hexagonal, layered, feature-sliced, monorepo, ui-surface, vertical-slice, ddd-bounded-contexts (+ aliases clean-architecture / onion-architecture). Layers start optional; doctor suggests tightening populated cores. Cross-slice / cross-context bans use optional `peerIsolation` rules.
189
191
  - **Frameworks:** Nest / Next / express / library layouts get sensible globs on init so day-one coverage is real.
190
192
  - **Brownfield:** baseline ratchet, refuse to freeze a wrong contract, `/ark-adopt` for mature trees.
191
- - **Agents:** skills install into Claude / Cursor / Codex / Grok; `ark start` freezes an origin report under `.ark/reports/`.
193
+ - **Agents:** skills install into Claude / Cursor / Codex / Grok; `ark start` freezes **day-zero origin** under `.ark/reports/` **before** agent docs/CI templates.
192
194
  - **Write protocol (2.10 / Track W):** mechanical-safe **autoPatch** on the write gate (`import type`); MCP **`ark_prepare_write`** (place + validate + patch + judgmentBrief); opt-in hook **`--hook-repair`** (`ARK_REPAIR_JSON`); doctor **`writePath`** (repair vs reject-only); loop-cost eval (`npm run eval:loop-cost`). Port-proof inject is **judgment** (arity change), not silent auto-apply.
195
+ - **Fail-closed CI (2.11):** `--strict` combines config coverage, required CI/MCP/write gates,
196
+ PreToolUse hook presence, and bypass diagnostics for dynamic imports, TypeScript suppressions,
197
+ explicit `any` casts, InMemory runtime defaults, and disabled peer isolation.
198
+ - **Trust / coverage (2.12):** package unit-test floors on the broad product surface
199
+ (statements/lines **≥80%**, branches/functions **≥85%**; enforcement-critical modules **≥95%**
200
+ branch). Explore dual-plan + day-zero origin first (see above). Roadmap next: Q2 repair dogfood matrix.
193
201
  - **TypeScript:** project compilers 5.x / 6.x / 7.x — gate falls back to a nested JS-API TypeScript when TS 7’s main export is version-only ([docs/typescript-support.md](docs/typescript-support.md)).
194
202
 
195
203
  ### Why not only ESLint / dependency-cruiser / Nx?
@@ -211,9 +219,10 @@ ark.config.json
211
219
  ## Common commands
212
220
 
213
221
  ```bash
214
- npx arkgate start # guided setup + plan + origin report
222
+ npx arkgate start # guided setup: contract origin → gates → plan
215
223
  npx arkgate-check --doctor # health + Adoption gaps (not just fitness)
216
224
  npx arkgate-check --doctor --json # machine-readable doctor.adoption
225
+ npx arkgate-check --strict # fail-closed CI + installed-gate/safety checks
217
226
  npx arkgate-check --plan # safe-to-auto-fix vs your call
218
227
  npx arkgate-check --coverage # Governed: N%
219
228
  npx arkgate-check --report ark-report.html # showcase HTML + Adoption card + origin/latest
@@ -224,8 +233,8 @@ npx arkgate upgrade # package + gates/skills + MCP/Codex n
224
233
  CI (example):
225
234
 
226
235
  ```yaml
227
- - run: npx arkgate-check --root . --config ark.config.json --strict-config
228
- # or: uses: pedroknigge/arkgate@main
236
+ - run: npx arkgate-check --root . --config ark.config.json --strict
237
+ # or: uses: pedroknigge/arkgate@<tag-or-SHA> # runs that checked-out revision
229
238
  ```
230
239
 
231
240
  ---
package/SECURITY.md CHANGED
@@ -28,10 +28,9 @@ a private security contact without including exploit details.
28
28
  ArkGate releases are GitHub-first:
29
29
 
30
30
  1. Changes land on GitHub and must pass CI plus the dedicated security workflow.
31
- 2. A GitHub Release is created from an annotated `vX.Y.Z` tag. Tag verification is
32
- **fail-closed** by default for unsigned tags unless
33
- `ARK_ALLOW_UNSIGNED_RELEASE_TAG=true` is set (CI publish sets this until GPG signing is
34
- wired). Signed tags can later require `ARK_REQUIRE_SIGNED_RELEASE_TAG=true`.
31
+ 2. A GitHub Release is created from a signed `vX.Y.Z` tag (`git tag -s`). The publish
32
+ workflow verifies the signature and fails closed for unsigned tags. The local override
33
+ exists only for explicit emergency use and is not enabled in CI.
35
34
  3. The manual `Publish npm` workflow verifies the tag, requires the GitHub Release to
36
35
  exist, reruns release checks, publishes npm with provenance, and uploads a SHA-256
37
36
  checksum for the npm tarball to the GitHub Release.
package/bin/ark-check.mjs CHANGED
@@ -43,6 +43,7 @@ import {
43
43
  arkPackageVersion,
44
44
  REQUIRED_GATE_FILES,
45
45
  codexPromptsDir,
46
+ detectWritePathCapabilities,
46
47
  } from './lib/agent-gates.mjs';
47
48
  import { syncBaselineIntoCheckSurfaces } from './lib/field-install.mjs';
48
49
  import {
@@ -64,6 +65,7 @@ import {
64
65
  import { runRatchetCores } from './lib/core-ratchet.mjs';
65
66
  import {
66
67
  baselineKey,
68
+ baselineOccurrenceKeys,
67
69
  readBaseline,
68
70
  summarizeViolations,
69
71
  writeBaseline,
@@ -99,6 +101,7 @@ function parseArgs(argv) {
99
101
  printConfig: undefined,
100
102
  tsconfig: undefined,
101
103
  json: false,
104
+ strict: false,
102
105
  strictConfig: false,
103
106
  requireGates: false,
104
107
  init: false,
@@ -122,13 +125,25 @@ function parseArgs(argv) {
122
125
  version: false,
123
126
  help: false,
124
127
  };
128
+ const requireValue = (flag, index) => {
129
+ const value = argv[index + 1];
130
+ if (value === undefined || value.startsWith('-')) {
131
+ throw new Error(`Missing value for ${flag}. Run ark-check --help for usage.`);
132
+ }
133
+ return value;
134
+ };
125
135
  for (let i = 2; i < argv.length; i += 1) {
126
136
  const arg = argv[i];
127
137
  if (arg === '--json') args.json = true;
138
+ else if (arg === '--strict') {
139
+ args.strict = true;
140
+ args.strictConfig = true;
141
+ args.requireGates = true;
142
+ }
128
143
  else if (arg === '--strict-config') args.strictConfig = true;
129
144
  else if (arg === '--require-gates') args.requireGates = true;
130
145
  else if (arg === '--init') args.init = true;
131
- else if (arg === '--preset') args.preset = argv[++i];
146
+ else if (arg === '--preset') args.preset = requireValue(arg, i++);
132
147
  else if (arg === '--install-agent-gates') args.installAgentGates = true;
133
148
  else if (arg === '--tools') {
134
149
  // Consume the next arg only when it isn't another flag (same rule as --baseline),
@@ -152,7 +167,7 @@ function parseArgs(argv) {
152
167
  else if (arg === '--recommend') args.recommend = true;
153
168
  else if (arg === '--write-plan') args.writePlan = true;
154
169
  else if (arg === '--list-policy-packs') args.listPolicyPacks = true;
155
- else if (arg === '--apply-policy-pack') args.applyPolicyPack = argv[++i];
170
+ else if (arg === '--apply-policy-pack') args.applyPolicyPack = requireValue(arg, i++);
156
171
  else if (arg === '--suggest-include') args.suggestInclude = true;
157
172
  else if (arg === '--adopt-contract') args.adoptContract = true;
158
173
  else if (arg === '--ratchet-cores') args.ratchetCores = true;
@@ -174,13 +189,14 @@ function parseArgs(argv) {
174
189
  const next = argv[i + 1];
175
190
  args.baseline = next && !next.startsWith('-') ? argv[++i] : '.ark-baseline.json';
176
191
  }
177
- else if (arg === '--root') args.root = path.resolve(argv[++i]);
178
- else if (arg === '--config') args.config = argv[++i];
179
- else if (arg === '--manifest') args.manifest = argv[++i];
180
- else if (arg === '--print-config') args.printConfig = argv[++i];
181
- else if (arg === '--tsconfig') args.tsconfig = argv[++i];
192
+ else if (arg === '--root') args.root = path.resolve(requireValue(arg, i++));
193
+ else if (arg === '--config') args.config = requireValue(arg, i++);
194
+ else if (arg === '--manifest') args.manifest = requireValue(arg, i++);
195
+ else if (arg === '--print-config') args.printConfig = requireValue(arg, i++);
196
+ else if (arg === '--tsconfig') args.tsconfig = requireValue(arg, i++);
182
197
  else if (arg === '--help' || arg === '-h') args.help = true;
183
198
  else if (arg === '--version' || arg === '-V') args.version = true;
199
+ else throw new Error(`Unknown argument: ${arg}. Run ark-check --help for usage.`);
184
200
  }
185
201
  return args;
186
202
  }
@@ -198,7 +214,7 @@ function usage() {
198
214
  return [
199
215
  'Usage: arkgate-check | ark-check (identical bins; product name ArkGate)',
200
216
  ' ark-check --version',
201
- ' ark-check --root <project> --config <ark.config.json> [--manifest <ark.manifest.json>] [--tsconfig <tsconfig.json>] [--strict-config] [--require-gates] [--json] [--baseline [file]] [--report [file.html]] [--no-cache]',
217
+ ' ark-check --root <project> --config <ark.config.json> [--manifest <ark.manifest.json>] [--tsconfig <tsconfig.json>] [--strict | --strict-config] [--require-gates] [--json] [--baseline [file]] [--report [file.html]] [--no-cache]',
202
218
  ' ark-check --coverage [--json] per-layer file counts + full unclassified list (report only, exit 0)',
203
219
  ' ark-check --plan [--json] classified remediation plan (mechanical-safe / judgment / deferred) + goal; report only',
204
220
  ' ark-check --recommend [--json] [--write-plan] application-shape plan; --write-plan emits ark-adoption-plan.json',
@@ -251,6 +267,8 @@ function usage() {
251
267
  '',
252
268
  'Config warnings are advisory by default and are included in JSON output.',
253
269
  'Use --strict-config to make config warnings fail the check.',
270
+ 'Use --strict for the fail-closed CI profile: --strict-config + --require-gates',
271
+ 'plus the security diagnostics surfaced by doctor.',
254
272
  '',
255
273
  '--require-gates fails the check when AGENTS.md, .mcp.json, or the generated CI',
256
274
  'workflow is missing, so "installed but never configured" is a red CI. Combine it',
@@ -301,6 +319,10 @@ function readConfig(root, configPath) {
301
319
  ...(raw.exclude ? { exclude: raw.exclude } : {}),
302
320
  ...(raw.excludeGenerated !== undefined ? { excludeGenerated: raw.excludeGenerated } : {}),
303
321
  ...(raw.cyclePolicy ? { cyclePolicy: raw.cyclePolicy } : {}),
322
+ ...(raw.dynamicImportAllowlist
323
+ ? { dynamicImportAllowlist: raw.dynamicImportAllowlist }
324
+ : {}),
325
+ ...(raw.safety ? { safety: raw.safety } : {}),
304
326
  };
305
327
  }
306
328
 
@@ -316,7 +338,7 @@ function detectConfig(root) {
316
338
 
317
339
  for (const entry of DEFAULT_INTENT_PREFIXES) {
318
340
  const directories = (DEFAULT_LAYER_DIRECTORIES[entry.layer] ?? []).filter(
319
- (directory) => walk(path.join(root, srcDir, directory)).length > 0
341
+ (directory) => walk(path.join(root, srcDir, directory), [], { root }).length > 0
320
342
  );
321
343
  if (directories.length === 0) continue;
322
344
  layers.push({
@@ -784,7 +806,7 @@ function runInit(args) {
784
806
  }
785
807
  // The starter profile only governs src/. Existing source elsewhere would make the
786
808
  // gate silently green, so surface it instead of pretending the project is covered.
787
- const outside = walk(args.root)
809
+ const outside = walk(args.root, [], { root: args.root })
788
810
  .map((file) => normalize(path.relative(args.root, file)))
789
811
  .filter((rel) => !rel.startsWith('src/') && !rel.split('/').some((s) => s.startsWith('.')));
790
812
  if (outside.length > 0) {
@@ -956,6 +978,9 @@ async function main() {
956
978
 
957
979
  if (args.requireGates) {
958
980
  const missing = missingGates(args.root);
981
+ if (args.strict && !detectWritePathCapabilities(args.root).hookPresent) {
982
+ missing.push('PreToolUse write hook');
983
+ }
959
984
  if (missing.length > 0) {
960
985
  const payload = {
961
986
  ok: false,
@@ -1032,7 +1057,7 @@ async function main() {
1032
1057
  );
1033
1058
  }
1034
1059
 
1035
- const { violations, warnings } = runArchitectureScan({
1060
+ const { violations, warnings, safety } = runArchitectureScan({
1036
1061
  root,
1037
1062
  config,
1038
1063
  manifest,
@@ -1046,6 +1071,7 @@ async function main() {
1046
1071
  runDoctor(root, config, files, rules, violations, args.json, {
1047
1072
  configPath: path.isAbsolute(args.config) ? args.config : path.join(root, args.config),
1048
1073
  configMissing: !fs.existsSync(path.isAbsolute(args.config) ? args.config : path.join(root, args.config)),
1074
+ safety,
1049
1075
  });
1050
1076
  return;
1051
1077
  }
@@ -1118,11 +1144,10 @@ async function main() {
1118
1144
  if (args.baseline) {
1119
1145
  const baseline = readBaseline(root, args.baseline);
1120
1146
  if (baseline.exists) {
1121
- suppressed = violations.filter((violation) => baseline.keys.has(baselineKey(violation)));
1122
- activeViolations = violations.filter(
1123
- (violation) => !baseline.keys.has(baselineKey(violation))
1124
- );
1125
- const currentKeys = new Set(violations.map(baselineKey));
1147
+ const occurrenceKeys = baselineOccurrenceKeys(violations);
1148
+ suppressed = violations.filter((_, index) => baseline.keys.has(occurrenceKeys[index]));
1149
+ activeViolations = violations.filter((_, index) => !baseline.keys.has(occurrenceKeys[index]));
1150
+ const currentKeys = new Set(occurrenceKeys);
1126
1151
  staleBaselineKeys = [...baseline.keys].filter((key) => !currentKeys.has(key)).length;
1127
1152
  } else {
1128
1153
  warnings.push(
package/bin/ark-mcp.mjs CHANGED
@@ -48,6 +48,7 @@ import {
48
48
  DEFAULT_LAYER_DIRECTORIES,
49
49
  DEFAULT_RULES,
50
50
  arkCommand,
51
+ globToRegExp,
51
52
  layerForFile,
52
53
  shouldShowNewHereNudge,
53
54
  detectWorkspaces,
@@ -273,18 +274,23 @@ function runHook(gate, config, args, ts) {
273
274
  // line numbers (edits shift them); simpler than full baselineKey (no file/layer fields
274
275
  // needed — this file is fixed).
275
276
  const violationKey = (violation) => `${violation.ruleId}|${violation.target ?? violation.message}`;
276
- let existingKeys = new Set();
277
+ let existingCounts = new Map();
277
278
  try {
278
279
  const current = fs.readFileSync(filePath, 'utf8');
279
- existingKeys = new Set(
280
- gate.validate(current, { layer, filePath }).violations.map(violationKey)
281
- );
280
+ for (const violation of gate.validate(current, { layer, filePath }).violations) {
281
+ const key = violationKey(violation);
282
+ existingCounts.set(key, (existingCounts.get(key) ?? 0) + 1);
283
+ }
282
284
  } catch {
283
285
  // New file: nothing pre-exists, every violation is new.
284
286
  }
285
- const newViolations = (result.violations ?? []).filter(
286
- (violation) => !existingKeys.has(violationKey(violation))
287
- );
287
+ const newViolations = (result.violations ?? []).filter((violation) => {
288
+ const key = violationKey(violation);
289
+ const remaining = existingCounts.get(key) ?? 0;
290
+ if (remaining === 0) return true;
291
+ existingCounts.set(key, remaining - 1);
292
+ return false;
293
+ });
288
294
  if (newViolations.length === 0) return;
289
295
 
290
296
  const lines = newViolations.map(
@@ -368,8 +374,14 @@ function runArkCheckJsonFromRoot(root, config, extraArgs, manifest) {
368
374
  const result = spawnSync(
369
375
  process.execPath,
370
376
  [arkCheckBin, '--root', root, '--config', config, ...manifestArgs, '--json', ...extraArgs],
371
- { encoding: 'utf8' }
377
+ { encoding: 'utf8', timeout: 120_000, maxBuffer: 20 * 1024 * 1024 }
372
378
  );
379
+ if (result.error) {
380
+ return {
381
+ data: null,
382
+ raw: `ark-check failed to execute: ${result.error.message}`,
383
+ };
384
+ }
373
385
  const stdout = result.stdout ?? '';
374
386
  try {
375
387
  return { data: JSON.parse(stdout), raw: stdout };
@@ -544,6 +556,18 @@ async function main() {
544
556
  name: layer.name,
545
557
  patterns: layer.patterns,
546
558
  })),
559
+ allowNonLiteralDynamicImport: (filePath) => {
560
+ if (!filePath || !Array.isArray(config.dynamicImportAllowlist)) return false;
561
+ const rel = path.relative(args.root, path.resolve(args.root, filePath)).split(path.sep).join('/');
562
+ return config.dynamicImportAllowlist.some((pattern) => {
563
+ if (typeof pattern !== 'string') return false;
564
+ try {
565
+ return globToRegExp(pattern).test(rel);
566
+ } catch {
567
+ return false;
568
+ }
569
+ });
570
+ },
547
571
  });
548
572
 
549
573
  if (args.hook) {
@@ -706,7 +730,10 @@ async function main() {
706
730
  // DomainModel there would tell the agent to create a second layer for the same
707
731
  // prefix, making longest-prefix resolution ambiguous.
708
732
  function suggestedLayers() {
709
- const activeNames = new Set(profile.layers.map((layer) => layer.name));
733
+ const activeNames = new Set([
734
+ ...configLayers.map((layer) => layer.name),
735
+ ...profile.layers.map((layer) => layer.name),
736
+ ]);
710
737
  const claimedPrefixes = new Set(
711
738
  profile.layers.flatMap((layer) =>
712
739
  (layer.prefixes ?? []).map((p) => (p.endsWith('.') ? p : `${p}.`))
@@ -732,13 +759,30 @@ async function main() {
732
759
  );
733
760
  }
734
761
  const suggestions = suggestedLayers();
762
+ const contractLayers = usedProjectConfig
763
+ ? configLayers.map((layer) => ({
764
+ ...layer,
765
+ prefixes: Array.isArray(layer.intentPrefixes) ? layer.intentPrefixes : [],
766
+ }))
767
+ : profile.layers;
735
768
  return JSON.stringify(
736
769
  {
737
770
  source: profile === ark.elevenLayerProfile ? 'strictDefaultElevenLayerProfile' : 'project',
738
771
  name: profile.name,
739
- layers: profile.layers,
772
+ // File placement contract: every configured layer, including layers that do not
773
+ // own intent prefixes (e.g. Tooling / FrameworkAdapters).
774
+ layers: contractLayers,
775
+ // Runtime/intent resolution profile kept explicit so consumers never have to infer
776
+ // why a prefix-less file layer is absent from intent resolution.
777
+ intentLayers: profile.layers,
740
778
  rules: profile.rules,
741
779
  ...(Object.keys(forbiddenGlobals).length > 0 ? { forbiddenGlobals } : {}),
780
+ ...(Array.isArray(config.dynamicImportAllowlist)
781
+ ? { dynamicImportAllowlist: config.dynamicImportAllowlist }
782
+ : {}),
783
+ ...(config.safety && typeof config.safety === 'object'
784
+ ? { safety: config.safety }
785
+ : {}),
742
786
  ...(suggestions.length > 0
743
787
  ? {
744
788
  suggestedLayers: suggestions,