arkgate 2.13.0 → 3.0.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 (72) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/README.md +37 -22
  3. package/bin/ark-check.mjs +62 -4
  4. package/bin/ark-mcp.mjs +108 -1
  5. package/bin/ark-shared.mjs +204 -149
  6. package/bin/ark.mjs +90 -25
  7. package/bin/lib/adapter-contract.mjs +93 -0
  8. package/bin/lib/agent-gates.mjs +1 -0
  9. package/bin/lib/analysis-engine.mjs +1171 -0
  10. package/bin/lib/architecture-scan.mjs +84 -135
  11. package/bin/lib/ci-and-commands.mjs +31 -0
  12. package/bin/lib/config-warnings.mjs +7 -205
  13. package/bin/lib/field-install.mjs +67 -10
  14. package/bin/lib/gate-files.mjs +42 -3
  15. package/bin/lib/graph-cycles.mjs +4 -54
  16. package/bin/lib/hook-templates.mjs +33 -1
  17. package/bin/lib/host-support-matrix.mjs +7 -1
  18. package/bin/lib/install-migrate.mjs +54 -16
  19. package/bin/lib/presets.mjs +42 -2
  20. package/bin/lib/safety-diagnostics.mjs +18 -17
  21. package/bin/lib/scan-files.mjs +12 -1
  22. package/bin/lib/skill-install.mjs +8 -1
  23. package/bin/lib/source-policy.mjs +36 -0
  24. package/bin/lib/start-preview.mjs +271 -0
  25. package/bin/lib/ts-resolve.mjs +11 -2
  26. package/bin/lib/write-path-capabilities.mjs +4 -0
  27. package/compat/nestjs.cjs +2 -0
  28. package/compat/nestjs.d.ts +2 -0
  29. package/compat/nestjs.js +1 -0
  30. package/compat/runtime.cjs +2 -0
  31. package/compat/runtime.d.ts +2 -0
  32. package/compat/runtime.js +1 -0
  33. package/dist/configContract-BxSIwVRo.d.cts +259 -0
  34. package/dist/configContract-BxSIwVRo.d.ts +259 -0
  35. package/dist/eslint/index.cjs +125 -48
  36. package/dist/eslint/index.d.cts +7 -1
  37. package/dist/eslint/index.d.ts +7 -1
  38. package/dist/eslint/index.js +125 -48
  39. package/dist/index.cjs +1248 -3302
  40. package/dist/index.d.cts +359 -483
  41. package/dist/index.d.ts +359 -483
  42. package/dist/index.js +1231 -3248
  43. package/docs/agent-guide.md +28 -16
  44. package/docs/ai-gates.md +30 -7
  45. package/docs/migrate-from-ark-runtime-kernel.md +2 -3
  46. package/docs/package-surface.md +8 -13
  47. package/docs/production-hardening.md +17 -4
  48. package/docs/typescript-support.md +27 -0
  49. package/package.json +33 -11
  50. package/schemas/ark.analysis-result.schema.json +91 -0
  51. package/server.json +2 -2
  52. package/templates/skills/ark-architect.md +3 -2
  53. package/dist/configContract-iBLxx5Tz.d.cts +0 -53
  54. package/dist/configContract-iBLxx5Tz.d.ts +0 -53
  55. package/dist/eslint/index.cjs.map +0 -1
  56. package/dist/eslint/index.js.map +0 -1
  57. package/dist/index.cjs.map +0 -1
  58. package/dist/index.js.map +0 -1
  59. package/dist/nestjs/index.cjs +0 -2606
  60. package/dist/nestjs/index.cjs.map +0 -1
  61. package/dist/nestjs/index.d.cts +0 -23
  62. package/dist/nestjs/index.d.ts +0 -23
  63. package/dist/nestjs/index.js +0 -2582
  64. package/dist/nestjs/index.js.map +0 -1
  65. package/dist/runtime/index.cjs +0 -4014
  66. package/dist/runtime/index.cjs.map +0 -1
  67. package/dist/runtime/index.d.cts +0 -3
  68. package/dist/runtime/index.d.ts +0 -3
  69. package/dist/runtime/index.js +0 -3925
  70. package/dist/runtime/index.js.map +0 -1
  71. package/dist/types-BxBwnBpC.d.cts +0 -1041
  72. package/dist/types-Wcs_l1_J.d.ts +0 -1041
package/CHANGELOG.md CHANGED
@@ -4,6 +4,45 @@ All notable changes to ArkGate (`arkgate`; formerly `ark-runtime-kernel`) are do
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ No changes are scheduled after the prepared stable release.
8
+
9
+ ## 3.0.0 — 2026-07-13
10
+
11
+ ### Added
12
+
13
+ - **Compact active-host onboarding:** `ark start` now asks for the active host on a TTY (or
14
+ detects it non-interactively), writes at most five project files / 25 KB, and uses one
15
+ package/MCP-backed router instead of copied per-host skill packs. It does not alter
16
+ `package.json` unless `--install` is explicit; host removal and re-addition are previewed,
17
+ safe, and reversible.
18
+
19
+ - **Canonical analysis engine bundle:** graph policy, cycle evaluation, and configuration
20
+ diagnostics now have one Kernel implementation shared by the library, CLI, and MCP. A documented
21
+ standalone CLI bundle preserves the package's self-hosted boundary and is protected by a CI drift
22
+ check and Kernel/bundle parity fixtures.
23
+ - **Symbol-aware semantic analysis:** one Kernel extractor now resolves forbidden ambient
24
+ capabilities through local symbols, aliases, `globalThis`, static keys, and destructuring, and
25
+ classifies TS/JS dependency forms across ESM, CommonJS, type-only, and unresolved dynamic edges.
26
+ CLI, safety diagnostics, and AICodeGate consume the same generated implementation. The supported
27
+ soundness envelope is documented and guarded by a labeled adversarial corpus plus TypeScript
28
+ 5/6/7 and mutation matrices.
29
+ - **Versioned adapter parity:** CLI JSON, MCP structured results, write hooks, ESLint, and the
30
+ GitHub Action now expose the same `ark.analysis-result` v1 diagnostics. A generated JSON Schema,
31
+ committed compatibility fixture, exact golden corpus, and mandatory CI parity job prevent
32
+ adapter drift; source-policy decisions no longer live privately inside ESLint.
33
+ - **Runtime package isolation:** the next-major `arkgate` root now contains only gate APIs. The
34
+ optional runtime and NestJS adapter build independently as experimental `@arkgate/runtime`;
35
+ deprecated subpath shims contain no implementation. The non-atomic store is now presented as
36
+ `InMemoryEventBuffer`, with production recovery and durability requirements made explicit.
37
+
38
+ ### Fixed
39
+
40
+ - **Deterministic offline setup tests:** `ark start` fixtures that do not exercise installation now
41
+ pass `--no-install`, preventing a published current version from turning unit tests into registry
42
+ installs.
43
+ - **Node 26 watch fallback:** `ark-check --watch` falls back to bounded polling when recursive
44
+ `fs.watch` fails asynchronously with `EMFILE`, instead of crashing the watcher process.
45
+
7
46
  ## 2.13.0 — 2026-07-11
8
47
 
9
48
  - Added the stable, deterministic analysis IR and public in-memory API: `loadContract`,
package/README.md CHANGED
@@ -16,6 +16,9 @@ and makes sure a “green” check means something real.
16
16
 
17
17
  </div>
18
18
 
19
+ > **ArkGate 3.0** passed its independent beta-exit audit. The npm badge reports the version
20
+ > currently available in the registry.
21
+
19
22
  ---
20
23
 
21
24
  ## The only flow (humans and agents)
@@ -23,8 +26,10 @@ and makes sure a “green” check means something real.
23
26
  If you remember nothing else:
24
27
 
25
28
  ```text
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
29
+ 1. npx arkgate start ← read-only preview: files + commands + projected coverage
30
+ npx arkgate start --apply ← apply exactly the previewed mutations
31
+ 2. Compact router ← MCP/AGENTS routes place, validate, and architecture checks
32
+ /ark-autopilot ← optional full guided workflow after its skill pack is installed
28
33
  3. npx arkgate-check --doctor ← “where am I?” anytime (one status screen)
29
34
  ```
30
35
 
@@ -36,8 +41,8 @@ If you remember nothing else:
36
41
  | New ArkGate version | `/ark-upgrade` |
37
42
 
38
43
  **Everything else is optional.** You do not need to learn “modes”, 11 skills, or the runtime
39
- kernel to get value. Agents that are unsure should **only** run `/ark-autopilot` (or the three
40
- commands above).
44
+ kernel to get value. The compact router is enough for normal work; install `/ark-autopilot` only
45
+ when you want the full guided workflow.
41
46
 
42
47
  ---
43
48
 
@@ -65,12 +70,13 @@ It is **not** a web framework, ORM, or job runner.
65
70
 
66
71
  ## Who it’s for
67
72
 
68
- Same start for almost everyone: **`npx arkgate start` → `/ark-autopilot`**.
73
+ Same start for almost everyone: **`npx arkgate start` → compact router** (then
74
+ `/ark-autopilot` when you opt into the full skill pack).
69
75
 
70
76
  | You are… | Same start, then… |
71
77
  |----------|-------------------|
72
- | Builder with AI | Stay on autopilot until doctor is happy |
73
- | Tech lead on a messy monorepo | Autopilot (or deeper `/ark-adopt` if you want a focused brownfield pass) |
78
+ | Builder with AI | Use the compact router and doctor; add autopilot when you want guided remediation |
79
+ | Tech lead on a messy monorepo | Add autopilot (or deeper `/ark-adopt` for a focused brownfield pass) |
74
80
  | Power user | Same flow; use `ark-check --plan` / `--coverage` when you want the raw sensor |
75
81
 
76
82
  **Not for:** no TypeScript, “just one lint rule”, or looking for an app framework.
@@ -110,8 +116,9 @@ Full checklist (CI, MCP, Codex, imports): **[docs/migrate-from-ark-runtime-kerne
110
116
 
111
117
  ```bash
112
118
  npm install -D arkgate typescript
113
- npx arkgate start # contract → day-zero origin gates + plan
114
- # in agent:
119
+ npx arkgate start # read-only preview: exact mutations + projected coverage
120
+ npx arkgate start --apply # apply the compact contract → active-host router → CI plan
121
+ # optional, after installing the full skill pack:
115
122
  # /ark-autopilot
116
123
  npx arkgate-check --doctor # status light + next action
117
124
  ```
@@ -121,7 +128,7 @@ Aliases `ark` / `ark-check` / `ark-mcp` still work. **npm / pnpm / yarn**. No in
121
128
  <details>
122
129
  <summary>What <code>/ark-autopilot</code> does under the hood (optional detail)</summary>
123
130
 
124
- 1. Setup if needed (`ark start` — contract, then **day-zero origin**, then gates).
131
+ 1. Setup if needed (`ark start` — compact contract + active-host router + CI gate; create an HTML/origin report explicitly when you need one).
125
132
  2. **Explore pass** (decision-grade map of *this* product; field path when demos exist).
126
133
  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.
127
134
  4. Apply A → re-check; judgment only when you ask for full apply.
@@ -150,8 +157,9 @@ npx arkgate-check --coverage
150
157
  Install once: `npx arkgate-check --install-agent-gates`
151
158
  (`--tools claude,cursor,codex,grok` to pick hosts.)
152
159
 
153
- **Default is always `/ark-autopilot`.** The rest are escapes, not a second curriculum.
154
- Generated `AGENTS.md` includes a **skill routing table** (trigger → skill). Skills are
160
+ **The compact router is the default; `/ark-autopilot` is the full guided option.** The rest are
161
+ escapes, not a second curriculum. Full-install `AGENTS.md` includes a **skill routing table**
162
+ (trigger → skill). Skills are
155
163
  **dual-engine** (CLI sensor + read real source) and end with a fixed **completion contract**;
156
164
  critical handoffs say **STOP** and name the next skill (hosts must follow — markdown cannot chain calls).
157
165
  When the host allows it, skills may **fan out parallel subagents** (disjoint scopes);
@@ -211,16 +219,18 @@ ark.config.json
211
219
  [configuration and editor guide](docs/configuration.md).
212
220
  - **Frameworks:** Nest / Next / express / library layouts get sensible globs on init so day-one coverage is real.
213
221
  - **Brownfield:** baseline ratchet, refuse to freeze a wrong contract, `/ark-adopt` for mature trees.
214
- - **Agents:** skills install into Claude / Cursor / Codex / Grok; `ark start` freezes **day-zero origin** under `.ark/reports/` **before** agent docs/CI templates.
222
+ - **Agents:** `ark start` asks for (or detects) one active host and writes one compact router, not copied skill packs, in at most five project files and 25 KB. Use `ark-check --install-agent-gates --skills-only --tools <host>` later when you explicitly want the full `/ark-*` skill set. Reports are opt-in with `ark-check --report`.
215
223
  - **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.
216
224
  - **Fail-closed CI (2.11):** `--strict-merge` combines config coverage, shared gate-file
217
225
  presence, and bypass diagnostics for dynamic imports, TypeScript suppressions, explicit `any`
218
226
  casts, InMemory runtime defaults, and disabled peer isolation. `--strict` is a compatibility
219
227
  alias. Neither requires an editor hook; use `--require-write-hook claude|grok` when that local
220
228
  guarantee is part of the check.
221
- - **Trust / coverage (2.12):** package unit-test floors on the broad product surface
229
+ - **Trust / coverage (3.0 release baseline):** package unit-test floors on the broad product surface
222
230
  (statements/lines **≥80%**, branches/functions **≥85%**; enforcement-critical modules **≥95%**
223
- branch). Explore dual-plan + day-zero origin first (see above). Roadmap next: Q2 repair dogfood matrix.
231
+ branch). The V05 beta-exit audit passed with 12 pinned public adoptions, 97% median governed
232
+ coverage, and zero P0/P1 findings. Stable publication remains the signed-tag, GitHub Release,
233
+ and provenance-backed npm workflow documented in [the 3.0.0 release notes](docs/releases/3.0.0.md).
224
234
  - **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)).
225
235
 
226
236
  ### Why not only ESLint / dependency-cruiser / Nx?
@@ -242,7 +252,11 @@ ark.config.json
242
252
  ## Common commands
243
253
 
244
254
  ```bash
245
- npx arkgate start # guided setup: contract → origin → gates → plan
255
+ npx arkgate start # guided read-only preview
256
+ npx arkgate start --apply # apply the compact active-host setup (≤5 files)
257
+ npx arkgate start --tools codex --apply # select the host explicitly
258
+ npx arkgate start --install --apply # also add arkgate to package.json (explicit only)
259
+ npx arkgate start --remove-host codex # preview compact-host removal; add --apply to confirm
246
260
  npx arkgate-check --doctor # health + Adoption gaps (not just fitness)
247
261
  npx arkgate-check --doctor --json # machine-readable doctor.adoption
248
262
  npx arkgate-check --strict # fail-closed CI + installed-gate/safety checks
@@ -266,21 +280,21 @@ CI (example):
266
280
 
267
281
  Gates need **no app code changes**. The runtime API is currently **experimental** and is not a
268
282
  production-readiness claim. If you want to evaluate runtime intent/event contracts, use the
269
- opt-in subpath (preferred):
283
+ separate experimental package:
270
284
 
271
285
  ```ts
272
- import { createStrictArkKernelFromConfig } from 'arkgate/runtime';
286
+ import { createStrictArkKernelFromConfig } from '@arkgate/runtime';
273
287
  // see docs/production-hardening.md and docs/package-surface.md
274
288
  ```
275
289
 
276
- Root `import { } from 'arkgate'` still re-exports kernel symbols for compatibility
277
- in this major; use `arkgate/runtime` when evaluating the experimental surface.
290
+ The stable `arkgate` package does not bundle runtime implementation. The deprecated
291
+ `arkgate/runtime` forwarding shim requires `@arkgate/runtime` and is removed in ArkGate 4.
278
292
 
279
- NestJS: `arkgate/nestjs` (optional peer `@nestjs/common`).
293
+ NestJS: `@arkgate/runtime/nestjs` (optional peer `@nestjs/common`).
280
294
 
281
295
  ### Durability stance (built-in stores)
282
296
 
283
- The kernel’s default stores (`InMemoryOutboxStore`, `InMemoryAuditStore`,
297
+ The kernel’s default stores (`InMemoryEventBuffer`, `InMemoryAuditStore`,
284
298
  `InMemoryReadModelStore`, `InMemoryWorkflowStore`) are **reference in-memory only**:
285
299
  fine for tests, demos, and single-process local work — they **do not** survive restarts
286
300
  and are **not** production durability. Implement the store interfaces (or inject your own)
@@ -301,6 +315,7 @@ for real systems. Details: [docs/production-hardening.md](docs/production-harden
301
315
  | Agent / MCP tools | [docs/agent-guide.md](docs/agent-guide.md) |
302
316
  | Demos | [docs/demos/](docs/demos/) |
303
317
  | Examples | [examples/](examples/README.md) |
318
+ | Release 3.0.0 | [release notes and publication checklist](docs/releases/3.0.0.md) |
304
319
  | Roadmap | [ROADMAP.md](ROADMAP.md) · [Changelog](CHANGELOG.md) |
305
320
 
306
321
  ---
package/bin/ark-check.mjs CHANGED
@@ -42,6 +42,7 @@ import {
42
42
  checkArchitectureScriptSnippet,
43
43
  arkCheckCommand,
44
44
  arkPackageVersion,
45
+ compactRouterHost,
45
46
  REQUIRED_GATE_FILES,
46
47
  codexPromptsDir,
47
48
  detectWritePathCapabilities,
@@ -84,6 +85,7 @@ import {
84
85
  ARCHITECTURE_PRESETS,
85
86
  } from './lib/presets.mjs';
86
87
  import { loadArkConfigContract, parseArkConfigJson } from './lib/config-contract.mjs';
88
+ import { createAdapterResult } from './lib/adapter-contract.mjs';
87
89
 
88
90
  import {
89
91
  collectGovernedFiles,
@@ -110,6 +112,7 @@ function parseArgs(argv) {
110
112
  requireWriteHook: undefined,
111
113
  init: false,
112
114
  installAgentGates: false,
115
+ compact: false,
113
116
  tools: undefined,
114
117
  force: false,
115
118
  skillsOnly: false,
@@ -153,6 +156,7 @@ function parseArgs(argv) {
153
156
  else if (arg === '--init') args.init = true;
154
157
  else if (arg === '--preset') args.preset = requireValue(arg, i++);
155
158
  else if (arg === '--install-agent-gates') args.installAgentGates = true;
159
+ else if (arg === '--compact') args.compact = true;
156
160
  else if (arg === '--tools') {
157
161
  // Consume the next arg only when it isn't another flag (same rule as --baseline),
158
162
  // so `--tools --force` can't silently eat --force as a "tool name".
@@ -1038,7 +1042,13 @@ async function main() {
1038
1042
  // callers still get a clear signal from the exit code and the human-mode line.
1039
1043
  if (!args.json) {
1040
1044
  if (args.requireGates) {
1041
- console.log('Ark gates present (merge profile): ' + REQUIRED_GATE_FILES.join(', '));
1045
+ const compactHost = compactRouterHost(args.root);
1046
+ console.log(
1047
+ 'Ark gates present (merge profile): ' +
1048
+ (compactHost
1049
+ ? `AGENTS.md, compact host registration (${compactHost})`
1050
+ : REQUIRED_GATE_FILES.join(', '))
1051
+ );
1042
1052
  }
1043
1053
  if (writeRequest?.host) {
1044
1054
  console.log(`Ark hard-write hook present for ${writeRequest.host}.`);
@@ -1330,7 +1340,13 @@ async function main() {
1330
1340
  }
1331
1341
 
1332
1342
  if (args.json) {
1343
+ const adapterResult = createAdapterResult({
1344
+ valid: ok,
1345
+ violations: activeViolations.map(enrichViolationWithFixClass),
1346
+ warnings,
1347
+ });
1333
1348
  console.log(JSON.stringify({
1349
+ ...adapterResult,
1334
1350
  ok,
1335
1351
  violations: activeViolations.map(enrichViolationWithFixClass),
1336
1352
  suppressedViolations: suppressed.length,
@@ -1489,9 +1505,13 @@ async function runWatchMode(args) {
1489
1505
  const target = path.join(args.root, entry);
1490
1506
  if (!fs.existsSync(target)) continue;
1491
1507
  try {
1492
- fs.watch(target, { recursive: true }, rerun);
1508
+ const watcher = fs.watch(target, { recursive: true }, rerun);
1509
+ watcher.on('error', () => {
1510
+ watcher.close();
1511
+ watchByPolling(target, rerun);
1512
+ });
1493
1513
  } catch {
1494
- fs.watch(target, rerun);
1514
+ watchByPolling(target, rerun);
1495
1515
  }
1496
1516
  }
1497
1517
 
@@ -1499,7 +1519,45 @@ async function runWatchMode(args) {
1499
1519
  await new Promise(() => {});
1500
1520
  }
1501
1521
 
1522
+ function watchByPolling(target, onChange) {
1523
+ let previous = watchFingerprint(target);
1524
+ setInterval(() => {
1525
+ const current = watchFingerprint(target);
1526
+ if (current === previous) return;
1527
+ previous = current;
1528
+ onChange();
1529
+ }, 250);
1530
+ }
1531
+
1532
+ function watchFingerprint(target) {
1533
+ const pending = [target];
1534
+ const entries = [];
1535
+ while (pending.length > 0) {
1536
+ const current = pending.pop();
1537
+ let stat;
1538
+ try {
1539
+ stat = fs.statSync(current);
1540
+ } catch {
1541
+ continue;
1542
+ }
1543
+ entries.push(`${current}:${stat.mtimeMs}:${stat.size}`);
1544
+ if (!stat.isDirectory()) continue;
1545
+ try {
1546
+ for (const name of fs.readdirSync(current)) pending.push(path.join(current, name));
1547
+ } catch {
1548
+ // A concurrent delete is represented by the next fingerprint.
1549
+ }
1550
+ }
1551
+ return entries.sort().join('|');
1552
+ }
1553
+
1502
1554
  main().catch((error) => {
1503
- console.error(error instanceof Error ? error.message : String(error));
1555
+ console.error(
1556
+ process.env.ARK_DEBUG_STACK === '1' && error instanceof Error
1557
+ ? error.stack
1558
+ : error instanceof Error
1559
+ ? error.message
1560
+ : String(error)
1561
+ );
1504
1562
  process.exitCode = 2;
1505
1563
  });
package/bin/ark-mcp.mjs CHANGED
@@ -59,6 +59,7 @@ import { createImportTargetResolver } from './lib/import-resolve.mjs';
59
59
  import { validateWithAutoPatch, resolveImportFileAbs } from './lib/auto-patch.mjs';
60
60
  import { composePrepareWrite } from './lib/prepare-write.mjs';
61
61
  import { loadArkConfigContract } from './lib/config-contract.mjs';
62
+ import { ARK_ANALYSIS_RESULT_SCHEMA, createAdapterResult } from './lib/adapter-contract.mjs';
62
63
 
63
64
  const arkCheckBin = fileURLToPath(new URL('./ark-check.mjs', import.meta.url));
64
65
 
@@ -179,6 +180,8 @@ function normalizeHookPayload(payload) {
179
180
  Edit: 'Edit',
180
181
  search_replace: 'Edit',
181
182
  MultiEdit: 'MultiEdit',
183
+ ApplyPatch: 'ApplyPatch',
184
+ apply_patch: 'ApplyPatch',
182
185
  };
183
186
  const toolName = nameMap[rawName] ?? rawName;
184
187
  const filePath =
@@ -193,6 +196,70 @@ function normalizeHookPayload(payload) {
193
196
  };
194
197
  }
195
198
 
199
+ function applyCodexUpdatePatch(current, lines) {
200
+ let source = current.split('\n');
201
+ let cursor = 0;
202
+ const hunks = [];
203
+ let hunk = [];
204
+ for (const line of lines) {
205
+ if (line.startsWith('@@')) {
206
+ if (hunk.length > 0) hunks.push(hunk);
207
+ hunk = [];
208
+ } else if (/^[ +\-]/.test(line)) {
209
+ hunk.push(line);
210
+ }
211
+ }
212
+ if (hunk.length > 0) hunks.push(hunk);
213
+ for (const entries of hunks) {
214
+ const oldLines = entries.filter((line) => !line.startsWith('+')).map((line) => line.slice(1));
215
+ const newLines = entries.filter((line) => !line.startsWith('-')).map((line) => line.slice(1));
216
+ let found = -1;
217
+ for (let at = cursor; at <= source.length - oldLines.length; at += 1) {
218
+ if (oldLines.every((line, index) => source[at + index] === line)) {
219
+ found = at;
220
+ break;
221
+ }
222
+ }
223
+ if (found < 0) return null;
224
+ source.splice(found, oldLines.length, ...newLines);
225
+ cursor = found + newLines.length;
226
+ }
227
+ return source.join('\n');
228
+ }
229
+
230
+ function codexPatchWrites(patch, root) {
231
+ if (typeof patch !== 'string' || !patch.includes('*** Begin Patch')) return [];
232
+ const lines = patch.split('\n');
233
+ const writes = [];
234
+ for (let index = 0; index < lines.length; index += 1) {
235
+ const match = lines[index].match(/^\*\*\* (Add|Update|Delete) File: (.+)$/);
236
+ if (!match) continue;
237
+ const [, action, relativePath] = match;
238
+ const body = [];
239
+ for (index += 1; index < lines.length && !lines[index].startsWith('*** '); index += 1) {
240
+ body.push(lines[index]);
241
+ }
242
+ index -= 1;
243
+ if (action === 'Delete') continue;
244
+ const filePath = path.resolve(root, relativePath);
245
+ let content;
246
+ if (action === 'Add') {
247
+ content = body.filter((line) => line.startsWith('+')).map((line) => line.slice(1)).join('\n');
248
+ if (body.some((line) => line.startsWith('+'))) content += '\n';
249
+ } else {
250
+ let current;
251
+ try {
252
+ current = fs.readFileSync(filePath, 'utf8');
253
+ } catch {
254
+ continue;
255
+ }
256
+ content = applyCodexUpdatePatch(current, body);
257
+ }
258
+ if (typeof content === 'string') writes.push({ filePath, content });
259
+ }
260
+ return writes;
261
+ }
262
+
196
263
  /**
197
264
  * Compute the file content a Write/Edit/MultiEdit is about to produce. Edits are applied
198
265
  * to the CURRENT on-disk file so the gate judges the real post-edit state, not the edit
@@ -237,7 +304,27 @@ function runHook(gate, config, args, ts) {
237
304
  return;
238
305
  }
239
306
 
307
+ runHookPayload(payload, gate, config, args, ts);
308
+ }
309
+
310
+ function runHookPayload(payload, gate, config, args, ts) {
240
311
  const { toolName, toolInput, grokStyle } = normalizeHookPayload(payload);
312
+ if (toolName === 'ApplyPatch') {
313
+ const patch = toolInput.patch ?? toolInput.input ?? toolInput.content;
314
+ for (const write of codexPatchWrites(patch, args.root)) {
315
+ runHookPayload(
316
+ {
317
+ tool_name: 'Write',
318
+ tool_input: { file_path: write.filePath, content: write.content },
319
+ },
320
+ gate,
321
+ config,
322
+ args,
323
+ ts
324
+ );
325
+ }
326
+ return;
327
+ }
241
328
  const filePath = toolInput.file_path;
242
329
  if (!['Write', 'Edit', 'MultiEdit'].includes(toolName)) return;
243
330
  if (typeof filePath !== 'string' || !SOURCE_FILE.test(filePath) || filePath.endsWith('.d.ts')) {
@@ -298,6 +385,11 @@ function runHook(gate, config, args, ts) {
298
385
  return false;
299
386
  });
300
387
  if (newViolations.length === 0) return;
388
+ const normalizedRel = rel.split(path.sep).join('/');
389
+ const adapterResult = createAdapterResult({
390
+ valid: false,
391
+ violations: newViolations.map((violation) => ({ ...violation, file: normalizedRel })),
392
+ });
301
393
 
302
394
  const lines = newViolations.map(
303
395
  (violation) =>
@@ -339,9 +431,10 @@ function runHook(gate, config, args, ts) {
339
431
  if (repair) {
340
432
  // Structured envelope for any host that can re-inject. Never writes the file.
341
433
  const repairPayload = {
434
+ ...adapterResult,
342
435
  mode: 'repair',
343
436
  decision: 'deny',
344
- filePath: rel.split(path.sep).join('/'),
437
+ filePath: normalizedRel,
345
438
  ...(layer ? { layer } : {}),
346
439
  ...(autoPatch
347
440
  ? {
@@ -367,6 +460,7 @@ function runHook(gate, config, args, ts) {
367
460
  JSON.stringify({
368
461
  decision: 'deny',
369
462
  reason: message,
463
+ analysis: adapterResult,
370
464
  ...(repair && autoPatch ? { autoPatch } : {}),
371
465
  ...(repair ? { repair: true } : {}),
372
466
  }) + '\n'
@@ -616,6 +710,7 @@ async function main() {
616
710
  },
617
711
  required: ['source'],
618
712
  },
713
+ outputSchema: ARK_ANALYSIS_RESULT_SCHEMA,
619
714
  },
620
715
  {
621
716
  name: 'ark_check',
@@ -638,6 +733,7 @@ async function main() {
638
733
  },
639
734
  },
640
735
  },
736
+ outputSchema: ARK_ANALYSIS_RESULT_SCHEMA,
641
737
  },
642
738
  {
643
739
  name: 'ark_coverage',
@@ -826,12 +922,17 @@ async function main() {
826
922
  validate: validateOnce,
827
923
  resolveTargetAbs: resolveImportFileAbs,
828
924
  });
925
+ const adapterResult = createAdapterResult({
926
+ valid: result.valid,
927
+ violations: result.violations,
928
+ });
829
929
  return {
830
930
  content: [
831
931
  {
832
932
  type: 'text',
833
933
  text: JSON.stringify(
834
934
  {
935
+ ...adapterResult,
835
936
  valid: result.valid,
836
937
  violations: result.violations,
837
938
  ...(result.autoPatch ? { autoPatch: result.autoPatch } : {}),
@@ -842,6 +943,7 @@ async function main() {
842
943
  ),
843
944
  },
844
945
  ],
946
+ structuredContent: adapterResult,
845
947
  isError: !result.valid,
846
948
  };
847
949
  }
@@ -868,6 +970,11 @@ async function main() {
868
970
  }
869
971
  return {
870
972
  content: [{ type: 'text', text: JSON.stringify(data, null, 2) }],
973
+ structuredContent: {
974
+ schemaVersion: data.schemaVersion,
975
+ valid: data.valid,
976
+ diagnostics: data.diagnostics,
977
+ },
871
978
  isError: data.ok === false,
872
979
  };
873
980
  }