arkgate 2.9.2 → 2.11.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 (50) hide show
  1. package/CHANGELOG.md +87 -0
  2. package/README.md +12 -2
  3. package/SECURITY.md +3 -4
  4. package/bin/ark-check.mjs +41 -16
  5. package/bin/ark-mcp.mjs +335 -111
  6. package/bin/ark.mjs +35 -5
  7. package/bin/lib/agent-gates.mjs +161 -8
  8. package/bin/lib/architecture-scan.mjs +23 -1
  9. package/bin/lib/auto-patch.mjs +264 -0
  10. package/bin/lib/baseline-key.mjs +17 -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/doctor-plan.mjs +83 -5
  15. package/bin/lib/port-proof.mjs +309 -0
  16. package/bin/lib/prepare-write.mjs +130 -0
  17. package/bin/lib/remediation.mjs +21 -0
  18. package/bin/lib/safety-diagnostics.mjs +263 -0
  19. package/bin/lib/scan-files.mjs +51 -6
  20. package/bin/lib/violations.mjs +3 -3
  21. package/dist/index.cjs +115 -11
  22. package/dist/index.cjs.map +1 -1
  23. package/dist/index.d.cts +5 -3
  24. package/dist/index.d.ts +5 -3
  25. package/dist/index.js +115 -11
  26. package/dist/index.js.map +1 -1
  27. package/dist/nestjs/index.cjs +18 -5
  28. package/dist/nestjs/index.cjs.map +1 -1
  29. package/dist/nestjs/index.d.cts +1 -1
  30. package/dist/nestjs/index.d.ts +1 -1
  31. package/dist/nestjs/index.js +18 -5
  32. package/dist/nestjs/index.js.map +1 -1
  33. package/dist/runtime/index.cjs +115 -11
  34. package/dist/runtime/index.cjs.map +1 -1
  35. package/dist/runtime/index.d.cts +1 -1
  36. package/dist/runtime/index.d.ts +1 -1
  37. package/dist/runtime/index.js +115 -11
  38. package/dist/runtime/index.js.map +1 -1
  39. package/dist/{types-D6Q8WHes.d.cts → types-BZ17b9i5.d.cts} +5 -1
  40. package/dist/{types-D6Q8WHes.d.ts → types-BZ17b9i5.d.ts} +5 -1
  41. package/docs/agent-guide.md +15 -1
  42. package/docs/ai-gates.md +63 -5
  43. package/docs/enthusiast/how-to-agent-gates.md +8 -0
  44. package/docs/enthusiast/reference-commands.md +1 -1
  45. package/docs/package-surface.md +2 -2
  46. package/docs/production-hardening.md +5 -0
  47. package/package.json +6 -2
  48. package/server.json +2 -2
  49. package/templates/skills/ark-explain.md +1 -1
  50. package/templates/skills/ark-loop.md +2 -1
package/CHANGELOG.md CHANGED
@@ -4,6 +4,93 @@ All notable changes to ArkGate (`arkgate`; formerly `ark-runtime-kernel`) are do
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## 2.11.0 — 2026-07-10
8
+
9
+ Fail-closed enforcement hardening: `--strict` now combines contract coverage, installed-gate
10
+ checks, write-hook presence, and bypass diagnostics in one CI profile. The GitHub Action runs
11
+ the exact checked-out revision by default, and runtime workflows cancel timed-out work
12
+ cooperatively.
13
+
14
+ ### Added
15
+
16
+ - **Strict CI profile:** `arkgate-check --strict` enables strict config validation, requires
17
+ generated gates plus a PreToolUse write hook, and fails on configured safety diagnostics.
18
+ - **Bypass diagnostics:** new `dynamicImportAllowlist` and `safety` config fields detect
19
+ non-literal dynamic imports, TypeScript suppression directives, explicit `any` casts,
20
+ production InMemory defaults, and disabled or omitted peer isolation. `--doctor --json`
21
+ exposes the same evidence under `doctor.safety`.
22
+ - **Release-quality CI:** JavaScript syntax validation, enforced coverage thresholds, and a
23
+ Node 18/20/22/24 compatibility matrix now run before merge.
24
+
25
+ ### Changed
26
+
27
+ - **Pinned GitHub Action execution:** `uses: pedroknigge/arkgate@<tag-or-SHA>` now runs that
28
+ checked-out ArkGate source. The `version` input remains available only as an explicit exact
29
+ npm compatibility override.
30
+ - **Complete MCP contract:** `ark://manifest` exposes every configured file layer separately
31
+ from runtime intent layers, plus reviewed dynamic-import and safety policy.
32
+ - **Workflow cancellation contract:** workflow steps receive an `AbortSignal` as their third
33
+ argument. `timeoutMs` aborts that signal, clears the active step, and rejects duplicate step
34
+ names before execution can corrupt compensation order.
35
+ - **Filesystem confinement:** source scans follow internal symlinks once and reject symlinks
36
+ that escape the project root.
37
+
38
+ ### Fixed
39
+
40
+ - **Baseline duplicate honesty:** repeated violations now receive stable per-occurrence keys,
41
+ so adding a second identical violation is new debt instead of being hidden by one baseline
42
+ entry.
43
+ - **Write-hook duplicate honesty:** proposed writes compare violation counts, preventing a new
44
+ duplicate from being mistaken for an already-existing violation.
45
+ - **CLI argument safety:** unknown flags and missing flag values fail with usage guidance
46
+ instead of silently weakening enforcement or throwing an internal error.
47
+ - **Action gate detection:** repositories using the ArkGate composite Action satisfy the CI
48
+ gate check without needing a separate literal `ark-check` command.
49
+
50
+ ## 2.10.0 — 2026-07-10
51
+
52
+ Track W — **Constrained write → verified repair**: write-boundary autoPatch, prepare_write,
53
+ loop-cost measurement, opt-in hook repair payloads, doctor write-path awareness, and a
54
+ proof-gated port-inject transform (judgment for auto-apply).
55
+
56
+ ### Added
57
+
58
+ - **W1 write-boundary autoPatch:** `validate_code` and PreToolUse `--hook` may return
59
+ additive `autoPatch: { source, remediationKind, confidence, valid }` for mechanical-safe
60
+ **import type** rewrites (`import-type-from-pure-type-module`, `import-type-of-type-exports`).
61
+ Post-patch revalidation must be green or the patch is discarded (never silent write).
62
+ Implementation: `bin/lib/auto-patch.mjs`.
63
+ - **W2 `ark_prepare_write` MCP tool:** place + constrain + validate + optional autoPatch +
64
+ judgmentBrief + contentHash in one call (`bin/lib/prepare-write.mjs`). Composes
65
+ `ark_place` + write gate — not a second contract.
66
+ - **W3 loop-cost eval harness:** `eval/loop-cost-run.mjs` / `npm run eval:loop-cost`
67
+ records turns-to-green, optional tokens, CHEATED (fixture-measured). Baseline
68
+ `eval/loop-cost-baseline.json` (medianTurnsTypeOnly=1, cheatedRate=0).
69
+ - **W4 opt-in hook repair payload:** `--hook-repair` / `ARK_HOOK_REPAIR=1` on deny emits
70
+ `ARK_REPAIR_JSON` + `ARK_AUTOPATCH_JSON` (stderr) and optional Grok `autoPatch` (stdout).
71
+ Default `--hook` remains hard-block prose only. Install templates (Claude/Grok) include
72
+ `--hook-repair`. Never silent write.
73
+ - **W5 doctor write-path awareness:** `ark-check --doctor` (JSON + human) surfaces
74
+ `writePath.mode` (`repair` | `reject-only` | `mcp-only` | `none`) and
75
+ `prepareWrite` / `autoPatch` flags from installed hooks/MCP. Reject-only gap is
76
+ additive (info) with install fix.
77
+ - **W6 port-proof inject binding (eval-gated):** prove+transform for
78
+ `port-proof-inject-binding` — single named value import used only as
79
+ `binding.method(...)` inside function declarations. Removes the import, emits a
80
+ port type, injects the binding as a parameter (call sites preserved). **Judgment for
81
+ auto-apply** (call arity changes; not write-path autoPatch). Fail-closed static proof;
82
+ rest params refuse apply. Labeled eval case. Implementation: `bin/lib/port-proof.mjs` +
83
+ scan flag `portProofEligible`.
84
+
85
+ ### Changed
86
+
87
+ - **Write gate type-only edges:** `import type` / `export type` no longer hard-block
88
+ LAYER_IMPORT / infra heuristics on the write path (erased at runtime). Value imports
89
+ and peerIsolation still deny. ark-check plan continues to surface type placement debt.
90
+ - **`ark_prepare_write` isError:** always `isError` when proposed source is invalid
91
+ (autoPatch is additive recovery, not soft-success).
92
+ - **`resolveImportFileAbs`:** confines disk reads under project root (no path escape).
93
+
7
94
  ## 2.9.2 — 2026-07-09
8
95
 
9
96
  Skill surface hardening: dual-engine, explore, STOP handoffs, AGENTS routing, subagent fan-out.
package/README.md CHANGED
@@ -151,6 +151,11 @@ critical handoffs say **STOP** and name the next skill (hosts must follow — ma
151
151
  When the host allows it, skills may **fan out parallel subagents** (disjoint scopes);
152
152
  otherwise they **fall back to sequential**.
153
153
 
154
+ **Write path (Track W):** Prefer MCP **`ark_prepare_write`** when you have a snippet (place +
155
+ constrain + validate + optional `autoPatch` + `judgmentBrief`). PreToolUse hooks with
156
+ `--hook-repair` emit machine-readable repair payloads on deny (still hard block; never silent
157
+ write). See [docs/ai-gates.md](docs/ai-gates.md).
158
+
154
159
  | Need | Skill |
155
160
  |------|--------|
156
161
  | Only the apply loop (plan already exists) | `/ark-loop` |
@@ -184,6 +189,10 @@ ark.config.json
184
189
  - **Frameworks:** Nest / Next / express / library layouts get sensible globs on init so day-one coverage is real.
185
190
  - **Brownfield:** baseline ratchet, refuse to freeze a wrong contract, `/ark-adopt` for mature trees.
186
191
  - **Agents:** skills install into Claude / Cursor / Codex / Grok; `ark start` freezes an origin report under `.ark/reports/`.
192
+ - **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.
193
+ - **Fail-closed CI (2.11):** `--strict` combines config coverage, required CI/MCP/write gates,
194
+ PreToolUse hook presence, and bypass diagnostics for dynamic imports, TypeScript suppressions,
195
+ explicit `any` casts, InMemory runtime defaults, and disabled peer isolation.
187
196
  - **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)).
188
197
 
189
198
  ### Why not only ESLint / dependency-cruiser / Nx?
@@ -208,6 +217,7 @@ ark.config.json
208
217
  npx arkgate start # guided setup + plan + origin report
209
218
  npx arkgate-check --doctor # health + Adoption gaps (not just fitness)
210
219
  npx arkgate-check --doctor --json # machine-readable doctor.adoption
220
+ npx arkgate-check --strict # fail-closed CI + installed-gate/safety checks
211
221
  npx arkgate-check --plan # safe-to-auto-fix vs your call
212
222
  npx arkgate-check --coverage # Governed: N%
213
223
  npx arkgate-check --report ark-report.html # showcase HTML + Adoption card + origin/latest
@@ -218,8 +228,8 @@ npx arkgate upgrade # package + gates/skills + MCP/Codex n
218
228
  CI (example):
219
229
 
220
230
  ```yaml
221
- - run: npx arkgate-check --root . --config ark.config.json --strict-config
222
- # or: uses: pedroknigge/arkgate@main
231
+ - run: npx arkgate-check --root . --config ark.config.json --strict
232
+ # or: uses: pedroknigge/arkgate@<tag-or-SHA> # runs that checked-out revision
223
233
  ```
224
234
 
225
235
  ---
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(