arkgate 4.5.5 → 4.5.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,14 +5,80 @@ in the immutable pre-2.0 archive linked below.
5
5
 
6
6
  ## Unreleased
7
7
 
8
+ ## 4.5.7 — 2026-08-12
9
+
10
+ **Patch** over **4.5.6**. Cursor hard write path: project `.cursor/hooks.json` `preToolUse`
11
+ gate for `Write` / `StrReplace` (failClosed), host matrix + doctor honesty, payload mapping
12
+ (`contents` / `StrReplace`), install/merge without wiping sibling hooks. Repair envelope may
13
+ emit; Write `updated_input` reinjection is **not** claimed. Codex/OpenCode stay advisory at
14
+ write. Shell/Tab/human edits still rely on required CI. **No required config migration.**
15
+
16
+ **Status: prepared** (not published).
17
+
18
+ ### Added
19
+
20
+ - **Cursor hard write (CH01):** install writes `.cursor/hooks.json` with
21
+ `preToolUse` matcher `Write|StrReplace`, `failClosed: true`, `--hook --hook-repair`,
22
+ `--root-env CURSOR_PROJECT_DIR`. Merge preserves sibling Cursor hooks.
23
+ - **Hook runtime:** Cursor payload normalization (`contents` → `content`, `StrReplace` → Edit)
24
+ and stdout `{ permission: "deny", agent_message, user_message }` on block.
25
+
26
+ ### Changed
27
+
28
+ - Host support matrix: Cursor `hard-write: true` for listed ops; soft hosts are Codex/OpenCode.
29
+ - Docs: `docs/ai-gates.md`, agent guide, README matrix, CLI help.
30
+
31
+ ## 4.5.6 — 2026-08-11
32
+
33
+ **Patch** over **4.5.5**. Field upgrade & multi-project MCP truth (Phase FX): registry-aware
34
+ package install, structured skip recovery, skill drift + opt-in refresh, post-upgrade checks,
35
+ MCP process package honesty, host selection notes, early whatsNew, inventory UX-copy quieting.
36
+ **No required config migration.** No new skill names, sensors, or scores. Freezes held.
37
+
38
+ **Status: published** (on npm `latest`; see
39
+ `docs/releases/4.5.6.md`).
40
+
41
+ ### Added
42
+
43
+ - **Registry-aware upgrade (FX01–FX02):** `ark upgrade --apply` no longer skips package install
44
+ solely because CLI version equals `node_modules/arkgate`. When the registry (injectable for
45
+ tests) is ahead, install runs. Skip paths emit structured JSON (`reasonCode`,
46
+ `installedVersion`, `cliVersion`, `registryLatest`, `suggestedInstallCmd`) plus human
47
+ recovery. Offline / registry-unknown stays honest (`REGISTRY_UNAVAILABLE`) without inventing
48
+ a bump.
49
+ - **Skill content drift honesty (FX03):** upgrade preview/JSON `skillDrift` counts
50
+ current/stale/customized/missing skills with sample paths and preserve notes.
51
+ - **Opt-in skill refresh (FX04):** `--refresh-skills` rewrites customized managed *skills* to
52
+ package templates with explicit consent. Conflicted assets still need `--accept-conflicts`.
53
+ Never silent overwrite of true user edits by default.
54
+ - **Post-upgrade verification block (FX05):** after apply, advisory `postUpgradeChecks` (pin↔CLI,
55
+ architecture verification, dual-truth, doctor/status/MCP notes). Always `notAScore`.
56
+ - **MCP multi-project process honesty (FX06):** every MCP tool context includes
57
+ `processPackage` (`processArkgateVersion`, `projectInstalledVersion`,
58
+ `processPackageMismatch` / `processStale`, `nextAction`). Prefer project-local CLI until
59
+ identity matched and versions align; multi-checkout recipe in agent-guide + `/ark-upgrade`.
60
+ - **Host selection honesty (FX07):** upgrade notes when detected active host ∉ managed
61
+ `--tools`/manifest hosts and suggests `--tools` expansion.
62
+ - **whatsNew early path (FX08):** suggested improvements on preview even when nothing to apply;
63
+ includes FX field-truth try/inspect items.
64
+ - **Inventory UX message quieting (FX09):** rules inventory downranks pure UX/error-message
65
+ string constants so they do not crowd business-rule pilots (no new sensors).
66
+
67
+ ### Changed
68
+
69
+ - `/ark-upgrade` skill + upgrade help document registry-aware install, `--refresh-skills`,
70
+ skill drift, post-upgrade checks, and multi-project MCP restart guidance.
71
+
72
+ **Freezes held:** no principle scores/ranks; no new skill names; no silent customized overwrite;
73
+ no silent multi-project MCP retarget; soft hosts stay advisory; no fake published claims.
74
+
8
75
  ## 4.5.5 — 2026-08-11
9
76
 
10
77
  **Patch** over **4.5.0**. Deep-module coach train plus upgrade **suggested improvements /
11
78
  what’s new** so consumers know what to try after install. **No required config migration.**
12
79
  No new skill names, sensors, or scores. Freezes held.
13
80
 
14
- **Status: prepared** (implementation only; not yet on npm `latest` remains `4.5.0` until
15
- the publication checklist completes).
81
+ **Status: published** (`arkgate@4.5.5` on npm `latest`; see `docs/releases/4.5.5.md`).
16
82
 
17
83
  ### Added
18
84
 
package/README.md CHANGED
@@ -16,9 +16,8 @@ and makes sure a “green” check means something real.
16
16
 
17
17
  </div>
18
18
 
19
- > **ArkGate 4.5.5** is **prepared** (deep-module coach + upgrade what’s new). npm `latest` remains
20
- > **4.5.0** until publish. [4.5.5 notes](docs/releases/4.5.5.md) · [4.5.0](docs/releases/4.5.0.md) ·
21
- > [4.4.0](docs/releases/4.4.0.md) · [Docs hub](docs/README.md) · [Product voice](docs/product-voice.md)
19
+ > **ArkGate 4.5.6** is on npm `latest` field upgrade truth, multi-project MCP honesty, skill drift.
20
+ > [4.5.6 notes](docs/releases/4.5.6.md) · [4.5.5](docs/releases/4.5.5.md) · [4.5.0](docs/releases/4.5.0.md) · [Docs hub](docs/README.md) · [Product voice](docs/product-voice.md)
22
21
 
23
22
  ---
24
23
 
@@ -114,16 +113,16 @@ Details: [docs/use.md](docs/use.md).
114
113
  | Claude Code | **Hard** block for listed ops (PreToolUse `Write` / `Edit` / `MultiEdit`) when installed + trusted | Advisory; the agent must call it | **Required GitHub status context** running `arkgate-check --strict-merge` (alias `ark-check`) | Emitted on hook deny; host must re-inject (hard path when installed + trusted) |
115
114
  | Grok Build | **Hard** block for listed ops (PreToolUse `write` / `search_replace` (plus aliases)) when installed + trusted | Advisory; the agent must call it | **Required GitHub status context** running `arkgate-check --strict-merge` (alias `ark-check`) | Emitted on hook deny; host must re-inject (hard path when installed + trusted) |
116
115
  | Google Antigravity | **Hard** block for listed ops (PreToolUse `write_to_file` / `replace_file_content` / `multi_replace_file_content`) when installed + trusted | Advisory; the agent must call it | **Required GitHub status context** running `arkgate-check --strict-merge` (alias `ark-check`) | Emitted on hook deny; host must re-inject (hard path when installed + trusted) |
117
- | Cursor | **Advisory only** at write (no hard hook) | Advisory; the agent must call it | **Required GitHub status context** running `arkgate-check --strict-merge` (alias `ark-check`) | No hard-boundary payload |
118
- | OpenAI Codex | **Advisory / best-effort** at write (not equivalent to Claude/Grok hard block) | Advisory; the agent must call it | **Required GitHub status context** running `arkgate-check --strict-merge` (alias `ark-check`) | Envelope may emit (`--hook-repair`); reinjection **not** guaranteed (advisory host) |
116
+ | Cursor | **Hard** block for listed ops (preToolUse `Write` / `StrReplace`) when installed + trusted | Advisory; the agent must call it | **Required GitHub status context** running `arkgate-check --strict-merge` (alias `ark-check`) | Envelope may emit (`--hook-repair`); reinjection **not** guaranteed (advisory host) |
117
+ | OpenAI Codex | **Advisory / best-effort** at write (not equivalent to Claude/Grok/Cursor hard block) | Advisory; the agent must call it | **Required GitHub status context** running `arkgate-check --strict-merge` (alias `ark-check`) | Envelope may emit (`--hook-repair`); reinjection **not** guaranteed (advisory host) |
119
118
  | OpenCode | **Advisory / best-effort** at write (MCP + optional plugin; not a hard boundary) | Advisory; the agent must call it | **Required GitHub status context** running `arkgate-check --strict-merge` (alias `ark-check`) | No hard-boundary payload |
120
119
 
121
120
  **Read the CI column:** for every host, the repository-wide hard guarantee is a **required**
122
121
  GitHub **status context** that runs the CLI — not “CI file present,” and not the CLI binary name alone.
123
- Cursor/Codex/OpenCode never get a fake hard write claim.
122
+ Codex/OpenCode never get a fake hard write claim. Cursor hard write covers only listed
123
+ `preToolUse` ops when `.cursor/hooks.json` is installed and trusted — Shell/Tab/human edits still rely on CI.
124
124
 
125
125
  This table describes the supported profile **after its files are installed and the host loads/trusts them**. A hard local boundary covers only the listed hook operations; alternate tools, direct filesystem writes, and human edits still rely on CI. MCP validation is advisory because the agent must call it. The CI check blocks a merge only when the repository makes that status required. Repair **envelopes** may be emitted without reinjection being guaranteed; silent auto-apply never happens. Run `arkgate-check --doctor` (or `ark-check --doctor`) for the evidence actually detected in the current repository.
126
-
127
126
  <!-- arkgate-host-support:end -->
128
127
 
129
128
  #### Why the hard guarantee lives at the merge gate
@@ -209,8 +208,8 @@ for real systems. Details: [docs/production-hardening.md](docs/production-harden
209
208
  | Config · package surface · TS | [configuration](docs/configuration.md) · [package-surface](docs/package-surface.md) · [typescript-support](docs/typescript-support.md) |
210
209
  | Brownfield | [docs/brownfield-adoption.md](docs/brownfield-adoption.md) |
211
210
  | Security | [SECURITY.md](SECURITY.md) |
212
- | Prepared (4.5.5) | [docs/releases/4.5.5.md](docs/releases/4.5.5.md) · [CHANGELOG](CHANGELOG.md) |
213
- | Current published (4.5.0 on npm `latest`) | [docs/releases/4.5.0.md](docs/releases/4.5.0.md) |
211
+ | Current release (4.5.6 on npm `latest`) | [docs/releases/4.5.6.md](docs/releases/4.5.6.md) · [CHANGELOG](CHANGELOG.md) |
212
+ | Prior (4.5.0) | [docs/releases/4.5.0.md](docs/releases/4.5.0.md) |
214
213
  | Prior (4.4.0) | [docs/releases/4.4.0.md](docs/releases/4.4.0.md) |
215
214
  | Prior (4.3.0) | [docs/releases/4.3.0.md](docs/releases/4.3.0.md) |
216
215
  | Prior (4.2.1) | [docs/releases/4.2.1.md](docs/releases/4.2.1.md) |
@@ -322,9 +322,10 @@ function usage() {
322
322
  'transition. Weakening or judgment-required findings fail unless --policy-ack names',
323
323
  'every finding and is bound to both policy hashes. Use --policy-base/--policy-base-ref',
324
324
  'for an explicit comparison; ARK_POLICY_BASE_REF is the CI environment equivalent.',
325
- 'Add --require-write-hook claude|grok to validate a hard local write boundary for that',
326
- 'specific host. Cursor and Codex expose advisory MCP tools plus the shared CI check;',
327
- 'merge blocking requires repository policy to make that status required.',
325
+ 'Add --require-write-hook claude|grok|antigravity|cursor to validate a hard local write',
326
+ 'boundary for that specific host. Codex and OpenCode expose advisory MCP (plus best-effort',
327
+ 'hooks where applicable) and the shared CI check; merge blocking requires repository policy',
328
+ 'to make that status required.',
328
329
  '',
329
330
  '--require-gates implies --strict-config and fails when the Ark contract in AGENTS.md,',
330
331
  'the project-rooted Ark server in .mcp.json, or fail-closed CI is missing/invalid.',
@@ -332,7 +333,8 @@ function usage() {
332
333
  '',
333
334
  '--install-agent-gates writes AGENTS.md, .mcp.json, and the CI workflow for every',
334
335
  'project, plus tool-specific templates. Known tools: claude, cursor, codex, grok',
335
- '(Claude/Grok hard-write hooks; Cursor/Codex advisory MCP; shared CI check for all) and',
336
+ '(Claude/Grok/Antigravity/Cursor hard-write hooks when covered; Codex advisory MCP;',
337
+ 'shared CI check for all) and',
336
338
  'windsurf, cline, copilot, kiro, roo, continue, gemini',
337
339
  '(instruction-tier rule files derived from the same contract).',
338
340
  'It also installs the /ark-* skills shipped in templates/skills/ into each',
@@ -6,6 +6,10 @@ import readline from 'node:readline';
6
6
  import { createHash, randomUUID } from 'node:crypto';
7
7
  import { spawnSync } from 'node:child_process';
8
8
  import { fileURLToPath } from 'node:url';
9
+ import {
10
+ buildProcessPackageHonesty,
11
+ readProjectInstalledArkgateVersion,
12
+ } from './lib/mcp-process-package.mjs';
9
13
  import {
10
14
  DEFAULT_INTENT_PREFIXES,
11
15
  DEFAULT_LAYER_DIRECTORIES,
@@ -350,6 +354,8 @@ function mapAntigravityToolCall(toolCall) {
350
354
  * Grok Build: { toolName, toolInput: { file_path, content | old_string/new_string } }
351
355
  * (aliases Write/Edit/MultiEdit → write/search_replace; matcher keeps both)
352
356
  * Antigravity: { toolCall: { name, args: { TargetFile, CodeContent, … } } }
357
+ * Cursor: { tool_name, tool_input, hook_event_name?, workspace_roots? }
358
+ * Write uses `contents`; StrReplace maps to Edit (path/old_string/new_string).
353
359
  */
354
360
  function normalizeHookPayload(payload, grokHookEvent = Boolean(process.env.GROK_HOOK_EVENT)) {
355
361
  const antigravityStyle =
@@ -366,17 +372,25 @@ function normalizeHookPayload(payload, grokHookEvent = Boolean(process.env.GROK_
366
372
  toolInput: { ...(mapped?.toolInput ?? {}), file_path: filePath },
367
373
  grokStyle: true, // decision JSON on stdout (deny)
368
374
  antigravityStyle: true,
375
+ cursorStyle: false,
369
376
  operation: mapped?.operation ?? mapped?.toolName ?? null,
370
377
  };
371
378
  }
372
379
 
373
380
  const rawName = payload?.tool_name ?? payload?.toolName ?? '';
374
- const toolInput = payload?.tool_input ?? payload?.toolInput ?? {};
381
+ const toolInputRaw = payload?.tool_input ?? payload?.toolInput ?? {};
382
+ const toolInput =
383
+ toolInputRaw && typeof toolInputRaw === 'object' ? { ...toolInputRaw } : {};
384
+ // Cursor Write uses `contents`; Claude/Grok use `content`.
385
+ if (toolInput.content == null && typeof toolInput.contents === 'string') {
386
+ toolInput.content = toolInput.contents;
387
+ }
375
388
  const nameMap = {
376
389
  Write: 'Write',
377
390
  write: 'Write',
378
391
  Edit: 'Edit',
379
392
  search_replace: 'Edit',
393
+ StrReplace: 'Edit',
380
394
  MultiEdit: 'MultiEdit',
381
395
  ApplyPatch: 'ApplyPatch',
382
396
  apply_patch: 'ApplyPatch',
@@ -387,6 +401,15 @@ function normalizeHookPayload(payload, grokHookEvent = Boolean(process.env.GROK_
387
401
  const toolName = nameMap[rawName] ?? rawName;
388
402
  const filePath =
389
403
  toolInput.file_path ?? toolInput.filePath ?? toolInput.path ?? toolInput.target_file;
404
+ const cursorStyle =
405
+ Boolean(process.env.CURSOR_PROJECT_DIR) ||
406
+ Boolean(process.env.CURSOR_VERSION) ||
407
+ (payload != null &&
408
+ typeof payload === 'object' &&
409
+ (payload.hook_event_name === 'preToolUse' ||
410
+ Array.isArray(payload.workspace_roots) ||
411
+ rawName === 'StrReplace' ||
412
+ (rawName === 'Write' && typeof toolInputRaw?.contents === 'string')));
390
413
  return {
391
414
  toolName,
392
415
  toolInput: { ...toolInput, file_path: filePath },
@@ -395,7 +418,8 @@ function normalizeHookPayload(payload, grokHookEvent = Boolean(process.env.GROK_
395
418
  grokHookEvent ||
396
419
  (payload != null && typeof payload === 'object' && 'toolName' in payload),
397
420
  antigravityStyle: false,
398
- operation: null,
421
+ cursorStyle,
422
+ operation: rawName === 'StrReplace' ? 'StrReplace' : null,
399
423
  };
400
424
  }
401
425
 
@@ -550,7 +574,7 @@ function designDeltaViolations(delta) {
550
574
  * generated code are inserted literally, never interpreted as replacement patterns.
551
575
  */
552
576
  function proposedSource(toolName, toolInput) {
553
- if (toolName === 'Write') return toolInput.content;
577
+ if (toolName === 'Write') return toolInput.content ?? toolInput.contents;
554
578
 
555
579
  let text = '';
556
580
  try {
@@ -606,18 +630,30 @@ function emitAntigravityAllow(output, antigravityStyle) {
606
630
  output.stdout(`${JSON.stringify({ decision: 'allow' })}\n`);
607
631
  }
608
632
 
633
+ /** Cursor preToolUse accepts explicit allow; exit 0 alone also works. */
634
+ function emitCursorAllow(output, cursorStyle) {
635
+ if (!cursorStyle) return;
636
+ output.stdout(`${JSON.stringify({ permission: 'allow' })}\n`);
637
+ }
638
+
639
+ function emitHostAllow(output, { antigravityStyle, cursorStyle }) {
640
+ emitAntigravityAllow(output, antigravityStyle);
641
+ emitCursorAllow(output, cursorStyle);
642
+ }
643
+
609
644
  function runHookPayload(payload, gate, config, args, ts, attemptContext, output = processHookOutput()) {
610
- const { toolName, toolInput, grokStyle, antigravityStyle, operation } = normalizeHookPayload(
611
- payload,
612
- attemptContext?.grokHookEvent ?? Boolean(process.env.GROK_HOOK_EVENT)
613
- );
645
+ const { toolName, toolInput, grokStyle, antigravityStyle, cursorStyle, operation } =
646
+ normalizeHookPayload(
647
+ payload,
648
+ attemptContext?.grokHookEvent ?? Boolean(process.env.GROK_HOOK_EVENT)
649
+ );
614
650
  if (toolName === 'ApplyPatch') {
615
651
  const patch = toolInput.patch ?? toolInput.input ?? toolInput.content;
616
652
  const parsedPatch = codexPatchWrites(patch, args.root);
617
653
  // Codex ApplyPatch is only preflighted when Ark can reconstruct every file operation.
618
654
  // An incomplete reconstruction must not be mislabeled as atomic or hard enforcement.
619
655
  if (!parsedPatch.complete) {
620
- emitAntigravityAllow(output, antigravityStyle);
656
+ emitHostAllow(output, { antigravityStyle, cursorStyle });
621
657
  return;
622
658
  }
623
659
  const patchWrites = parsedPatch.writes;
@@ -683,7 +719,7 @@ function runHookPayload(payload, gate, config, args, ts, attemptContext, output
683
719
  );
684
720
  }
685
721
  if (changes.length === 0) {
686
- emitAntigravityAllow(output, antigravityStyle);
722
+ emitHostAllow(output, { antigravityStyle, cursorStyle });
687
723
  return;
688
724
  }
689
725
  result = prepareChangeFromRoot({
@@ -741,7 +777,7 @@ function runHookPayload(payload, gate, config, args, ts, attemptContext, output
741
777
  ? evaluateWriteDesignDelta({ root: args.root, config, changes, ts })
742
778
  : null;
743
779
  if (result.valid && (designDelta?.valid ?? true)) {
744
- emitAntigravityAllow(output, antigravityStyle);
780
+ emitHostAllow(output, { antigravityStyle, cursorStyle });
745
781
  return;
746
782
  }
747
783
  const message = [
@@ -775,23 +811,23 @@ function runHookPayload(payload, gate, config, args, ts, attemptContext, output
775
811
  const filePath = toolInput.file_path;
776
812
  if (!['Write', 'Edit', 'MultiEdit'].includes(toolName)) {
777
813
  // Non-file tools: fail-open. Antigravity still needs an explicit allow decision.
778
- emitAntigravityAllow(output, antigravityStyle);
814
+ emitHostAllow(output, { antigravityStyle, cursorStyle });
779
815
  return;
780
816
  }
781
817
  if (typeof filePath !== 'string' || !SOURCE_FILE.test(filePath) || filePath.endsWith('.d.ts')) {
782
- emitAntigravityAllow(output, antigravityStyle);
818
+ emitHostAllow(output, { antigravityStyle, cursorStyle });
783
819
  return;
784
820
  }
785
821
  const rel = path.relative(args.root, path.resolve(filePath));
786
822
  const segments = rel.split(path.sep);
787
823
  if (segments[0] === '..' || segments.includes('node_modules')) {
788
- emitAntigravityAllow(output, antigravityStyle);
824
+ emitHostAllow(output, { antigravityStyle, cursorStyle });
789
825
  return;
790
826
  }
791
827
 
792
828
  const source = proposedSource(toolName, toolInput);
793
829
  if (typeof source !== 'string') {
794
- emitAntigravityAllow(output, antigravityStyle);
830
+ emitHostAllow(output, { antigravityStyle, cursorStyle });
795
831
  return;
796
832
  }
797
833
 
@@ -829,7 +865,7 @@ function runHookPayload(payload, gate, config, args, ts, attemptContext, output
829
865
  })
830
866
  : null;
831
867
  if (result.valid && (designDelta?.valid ?? true)) {
832
- emitAntigravityAllow(output, antigravityStyle);
868
+ emitHostAllow(output, { antigravityStyle, cursorStyle });
833
869
  return;
834
870
  }
835
871
 
@@ -860,7 +896,7 @@ function runHookPayload(payload, gate, config, args, ts, attemptContext, output
860
896
  return false;
861
897
  });
862
898
  if (newViolations.length === 0 && (designDelta?.valid ?? true)) {
863
- emitAntigravityAllow(output, antigravityStyle);
899
+ emitHostAllow(output, { antigravityStyle, cursorStyle });
864
900
  return;
865
901
  }
866
902
  const combinedViolations = [...newViolations, ...designDeltaViolations(designDelta)];
@@ -922,7 +958,13 @@ function runHookPayload(payload, gate, config, args, ts, attemptContext, output
922
958
  enforcement: hookEnforcement(
923
959
  args.root,
924
960
  attemptContext?.host ??
925
- (antigravityStyle ? 'antigravity' : grokStyle ? 'grok' : 'claude'),
961
+ (antigravityStyle
962
+ ? 'antigravity'
963
+ : cursorStyle
964
+ ? 'cursor'
965
+ : grokStyle
966
+ ? 'grok'
967
+ : 'claude'),
926
968
  attemptContext?.operation ??
927
969
  operation ??
928
970
  (antigravityStyle
@@ -931,11 +973,15 @@ function runHookPayload(payload, gate, config, args, ts, attemptContext, output
931
973
  : toolName === 'MultiEdit'
932
974
  ? 'multi_replace_file_content'
933
975
  : 'write_to_file'
934
- : grokStyle
976
+ : cursorStyle
935
977
  ? toolName === 'Edit'
936
- ? 'search_replace'
937
- : 'write'
938
- : toolName),
978
+ ? 'StrReplace'
979
+ : 'Write'
980
+ : grokStyle
981
+ ? toolName === 'Edit'
982
+ ? 'search_replace'
983
+ : 'write'
984
+ : toolName),
939
985
  toolName === 'Write' || Boolean(attemptContext?.completePatch)
940
986
  ),
941
987
  ...(layer ? { layer } : {}),
@@ -967,6 +1013,16 @@ function runHookPayload(payload, gate, config, args, ts, attemptContext, output
967
1013
  }) + '\n'
968
1014
  );
969
1015
  }
1016
+ // Cursor preToolUse: permission deny + agent_message (exit 2 also blocks).
1017
+ if (cursorStyle) {
1018
+ output.stdout(
1019
+ JSON.stringify({
1020
+ permission: 'deny',
1021
+ agent_message: message,
1022
+ user_message: `ArkGate blocked write to ${rel}`,
1023
+ }) + '\n'
1024
+ );
1025
+ }
970
1026
  output.status(2);
971
1027
  }
972
1028
 
@@ -1847,11 +1903,25 @@ export async function runArkMcp({ hookInput } = {}) {
1847
1903
  return currentBinding;
1848
1904
  }
1849
1905
 
1906
+ /**
1907
+ * FX06 — process package vs project install honesty (multi-project field truth).
1908
+ * Process arkgateVersion is startup-loaded; after consumer `npm i arkgate@newer`,
1909
+ * long-lived MCP can report a stale process version until restart.
1910
+ * Algorithm lives in bin/lib/mcp-process-package.mjs for unit coverage.
1911
+ */
1912
+ function processPackageHonesty() {
1913
+ return buildProcessPackageHonesty({
1914
+ processVersion: typeof ark.version === 'string' ? ark.version : null,
1915
+ root: resolvedRoot,
1916
+ });
1917
+ }
1918
+
1850
1919
  function contextFor(binding) {
1851
1920
  return {
1852
1921
  projectIdentity,
1853
1922
  binding,
1854
1923
  authoritative: binding.authoritative,
1924
+ processPackage: processPackageHonesty(),
1855
1925
  };
1856
1926
  }
1857
1927
 
@@ -722,35 +722,14 @@ export function packageInstallArgv(root, versionSpec = 'latest') {
722
722
  return ['npm', ['install', '-D', pkgSpec]];
723
723
  }
724
724
 
725
- /**
726
- * Whether an install of arkgate@latest can be skipped because node_modules already
727
- * resolves the same version as this CLI package.
728
- *
729
- * @param {string} root
730
- * @param {string} [cliVersion] this binary's package version
731
- * @returns {{ skip: boolean, installedVersion: string|null, reason: string }}
732
- */
733
- export function shouldSkipArkgateInstall(root, cliVersion) {
734
- const pkgPath = path.join(root, 'node_modules', 'arkgate', 'package.json');
735
- if (!fs.existsSync(pkgPath)) {
736
- return { skip: false, installedVersion: null, reason: 'not-installed' };
737
- }
738
- let installedVersion = null;
739
- try {
740
- installedVersion = JSON.parse(fs.readFileSync(pkgPath, 'utf8')).version ?? null;
741
- } catch {
742
- return { skip: false, installedVersion: null, reason: 'unreadable' };
743
- }
744
- if (
745
- typeof cliVersion === 'string' &&
746
- cliVersion &&
747
- installedVersion &&
748
- installedVersion === cliVersion
749
- ) {
750
- return { skip: true, installedVersion, reason: 'already-current' };
751
- }
752
- return { skip: false, installedVersion, reason: 'version-differs' };
753
- }
725
+ // FX01–FX02: registry-aware skip lives in upgrade-package-decision (injectable probe).
726
+ export {
727
+ shouldSkipArkgateInstall,
728
+ buildPackageInstallSkipPayload,
729
+ formatPackageInstallDecisionHuman,
730
+ compareSemverCore,
731
+ probeRegistryArkgateLatest,
732
+ } from './lib/upgrade-package-decision.mjs';
754
733
 
755
734
  /** Package-manager aware "install a dev dependency" hint (e.g. for a missing typescript). */
756
735
  export function installDevHint(root, pkg) {
package/bin/ark.mjs CHANGED
@@ -77,6 +77,7 @@ function parseArgs(argv) {
77
77
  installExplicit: false,
78
78
  apply: false,
79
79
  acceptConflicts: false,
80
+ refreshSkills: false,
80
81
  planDigest: undefined,
81
82
  json: false,
82
83
  internalApply: false,
@@ -118,6 +119,7 @@ function parseArgs(argv) {
118
119
  }
119
120
  else if (arg === '--apply') args.apply = true;
120
121
  else if (arg === '--accept-conflicts') args.acceptConflicts = true;
122
+ else if (arg === '--refresh-skills') args.refreshSkills = true;
121
123
  else if (arg === '--plan-digest') args.planDigest = requireValue(arg, i++);
122
124
  else if (arg === '--json') args.json = true;
123
125
  else if (arg === '--internal-apply') args.internalApply = true;
@@ -153,7 +155,7 @@ function usage() {
153
155
  ark start [--root <project>] [--tools <host>] [--require-write-hook <host>] [--install] [--apply] [--json]
154
156
  ark init [--root <project>] [--preset hexagonal|layered|feature-sliced|monorepo|ui-surface|vertical-slice|ddd-bounded-contexts|clean-architecture|onion-architecture]
155
157
  [--archetype <playbook-id>] [--tools <list>] [--require-write-hook <host>] [--yes] [--force] [--no-strict]
156
- ark upgrade [--root <project>] [--tools <list>] [--apply] [--plan-digest <sha256>] [--accept-conflicts] [--json] [--no-install] [--no-strict]
158
+ ark upgrade [--root <project>] [--tools <list>] [--apply] [--plan-digest <sha256>] [--accept-conflicts] [--refresh-skills] [--json] [--no-install] [--no-strict]
157
159
  ark preflight --changes <change-set.json> [--change-map <map.json>] [--root <project>] [--config ark.config.json] [--manifest <manifest.json>] [--tsconfig <tsconfig.json>] [--json]
158
160
  ark status [--root <project>] [--config ark.config.json] [--json]
159
161
  [--expected-root <abs>] [--expected-project-id sha256:…] [--tools <host>]
@@ -164,8 +166,10 @@ Commands:
164
166
  start New here? Analyze and preview the complete setup. Read-only unless --apply.
165
167
  init Configure Ark project enforcement with explicit prompts.
166
168
  upgrade Preview identity-proven Ark-managed asset updates. With package install,
167
- --apply bumps to @latest and recomputes the preview; a second explicit
168
- --apply --no-install applies those exact bytes and verifies them.
169
+ --apply bumps toward registry latest when behind (not only when CLI ≠ pin)
170
+ and recomputes the preview; a second explicit --apply --no-install applies
171
+ those exact bytes and verifies them. --refresh-skills opts in to rewrite
172
+ customized managed skills to package templates (never silent default).
169
173
  (alias: ark update)
170
174
  preflight Validate one atomic create/update/delete set without writing project files.
171
175
  status Unified session/project manifest (identity, activation, last check, rules).
@@ -203,8 +207,8 @@ Options:
203
207
  --remove-host <host>
204
208
  Preview or apply removal of that compact host integration; re-add it with --tools <host>.
205
209
  --require-write-hook <host>
206
- Require and verify a hard local write hook for Claude or Grok. Cursor/Codex are
207
- advisory-write plus hard CI merge only; impossible requests fail before any write.
210
+ Require and verify a hard local write hook for Claude, Grok, Antigravity, or Cursor.
211
+ Codex/OpenCode are advisory-write plus hard CI merge only; impossible requests fail before any write.
208
212
 
209
213
  Interactive mode (TTY, no --yes): asks what application shape you are building and maps it to a preset.
210
214
  Non-interactive (no TTY): uses the same defaults as --yes — never calls readline on a null interface.
@@ -26,9 +26,11 @@ export {
26
26
  antigravityHooks,
27
27
  claudeSettings,
28
28
  codexProjectConfig,
29
+ cursorHooks,
29
30
  grokHooks,
30
31
  grokProjectConfig,
31
32
  mergeAntigravityArkHook,
33
+ mergeCursorArkHook,
32
34
  mergeOpencodeArkMcp,
33
35
  opencodeProjectConfig,
34
36
  } from './hook-templates.mjs';
@@ -370,7 +370,8 @@ ${steps}
370
370
  ${placementBody}
371
371
 
372
372
  The project is only considered Ark-enforced when its host-appropriate write path is configured
373
- and the CI check passes. Only Claude/Grok provide a hard local write boundary; Cursor/Codex use
373
+ and the CI check passes. Only Claude/Grok/Antigravity/Cursor provide a hard local write boundary
374
+ (Cursor: Write/StrReplace when .cursor/hooks.json is trusted); Codex/OpenCode use
374
375
  advisory MCP plus CI. The experimental runtime is not required.
375
376
  `;
376
377
  }
@@ -127,7 +127,7 @@ const FAIL_CLOSED_ARK_FLAG = /(?:^|\s)--(?:strict|strict-merge|require-gates)(?=
127
127
  const COMPACT_HOST_FILES = {
128
128
  claude: ['.claude/settings.json'],
129
129
  grok: ['.grok/config.toml', '.grok/hooks/ark-write-gate.json'],
130
- cursor: ['.cursor/mcp.json'],
130
+ cursor: ['.cursor/mcp.json', '.cursor/hooks.json'],
131
131
  codex: ['.codex/hooks.json', '.codex/config.toml'],
132
132
  windsurf: ['.windsurf/rules/ark.md'],
133
133
  cline: ['.clinerules/ark.md'],
@@ -1,20 +1,22 @@
1
1
  // Generated from hook-templates.source.mjs — run npm run generate:packaged-tooling.
2
- import{execCommandParts as i,execRunner as s}from"../ark-shared.mjs";const c="arkgate-mcp";function l(o){const r=s(o);return`${JSON.stringify({hooks:{SessionStart:[{hooks:[{type:"command",command:`${r} ${c} --session-context --root . --root-env CLAUDE_PROJECT_DIR --config ark.config.json`}]}],PreToolUse:[{matcher:"Write|Edit|MultiEdit",hooks:[{type:"command",command:`${r} ${c} --hook --hook-repair --fail-on-new-smells --root . --root-env CLAUDE_PROJECT_DIR --config ark.config.json`}]}]}},null,2)}
3
- `}function p(o){const r=s(o);return`${JSON.stringify({hooks:{SessionStart:[{hooks:[{type:"command",timeout:30,command:`${r} ${c} --session-context --root . --root-env CODEX_PROJECT_DIR --config ark.config.json`}]}],PreToolUse:[{matcher:"ApplyPatch|apply_patch|Write|Edit|MultiEdit",hooks:[{type:"command",timeout:30,command:`${r} ${c} --hook --hook-repair --fail-on-new-smells --root . --root-env CODEX_PROJECT_DIR --config ark.config.json`}]}]}},null,2)}
4
- `}function f(o){const{command:r,args:e}=i(o,c,["--root",".","--config","ark.config.json"]),t=a=>a.replace(/\\/g,"\\\\").replace(/"/g,'\\"'),n=e.map(a=>`"${t(a)}"`).join(", ");return`# Generated by ark-check --install-agent-gates (Codex project scope).
2
+ import{execCommandParts as l,execRunner as i}from"../ark-shared.mjs";const s="arkgate-mcp";function h(e){const r=i(e);return`${JSON.stringify({version:1,hooks:{preToolUse:[{command:`${r} ${s} --hook --hook-repair --fail-on-new-smells --root . --root-env CURSOR_PROJECT_DIR --config ark.config.json`,matcher:"Write|StrReplace",failClosed:!0,timeout:30}]}},null,2)}
3
+ `}function y(e){const r=i(e);return`${JSON.stringify({hooks:{SessionStart:[{hooks:[{type:"command",command:`${r} ${s} --session-context --root . --root-env CLAUDE_PROJECT_DIR --config ark.config.json`}]}],PreToolUse:[{matcher:"Write|Edit|MultiEdit",hooks:[{type:"command",command:`${r} ${s} --hook --hook-repair --fail-on-new-smells --root . --root-env CLAUDE_PROJECT_DIR --config ark.config.json`}]}]}},null,2)}
4
+ `}function $(e){const r=i(e);return`${JSON.stringify({hooks:{SessionStart:[{hooks:[{type:"command",timeout:30,command:`${r} ${s} --session-context --root . --root-env CODEX_PROJECT_DIR --config ark.config.json`}]}],PreToolUse:[{matcher:"ApplyPatch|apply_patch|Write|Edit|MultiEdit",hooks:[{type:"command",timeout:30,command:`${r} ${s} --hook --hook-repair --fail-on-new-smells --root . --root-env CODEX_PROJECT_DIR --config ark.config.json`}]}]}},null,2)}
5
+ `}function O(e){const{command:r,args:o}=l(e,s,["--root",".","--config","ark.config.json"]),t=a=>a.replace(/\\/g,"\\\\").replace(/"/g,'\\"'),c=o.map(a=>`"${t(a)}"`).join(", ");return`# Generated by ark-check --install-agent-gates (Codex project scope).
5
6
  # CONFIGURED ON DISK \u2014 RUNTIME NOT VERIFIED.
6
7
  # Restart Codex, then call ark_identity with expectedRoot before trusting MCP verdicts.
7
8
  [mcp_servers.ark]
8
9
  command = "${t(r)}"
9
- args = [${n}]
10
- `}function g(o){const{command:r,args:e}=i(o,c,["--root",".","--config","ark.config.json"]),t=e.map(n=>`"${n.replace(/\\/g,"\\\\").replace(/"/g,'\\"')}"`).join(", ");return`# Generated by ark-check --install-agent-gates (Grok Build project scope).
10
+ args = [${c}]
11
+ `}function R(e){const{command:r,args:o}=l(e,s,["--root",".","--config","ark.config.json"]),t=o.map(c=>`"${c.replace(/\\/g,"\\\\").replace(/"/g,'\\"')}"`).join(", ");return`# Generated by ark-check --install-agent-gates (Grok Build project scope).
11
12
  # Restart Grok (or /mcps \u2192 refresh) after changes. Also loads repo-root .mcp.json.
12
13
  [mcp_servers.ark]
13
14
  command = "${r.replace(/\\/g,"\\\\").replace(/"/g,'\\"')}"
14
15
  args = [${t}]
15
- `}function u(o){const r=s(o);return`${JSON.stringify({hooks:{SessionStart:[{hooks:[{type:"command",timeout:30,command:`${r} ${c} --session-context --root . --root-env GROK_WORKSPACE_ROOT,CLAUDE_PROJECT_DIR --config ark.config.json`}]}],PreToolUse:[{matcher:"Write|Edit|MultiEdit|write|search_replace",hooks:[{type:"command",timeout:30,command:`${r} ${c} --hook --hook-repair --fail-on-new-smells --root . --root-env GROK_WORKSPACE_ROOT,CLAUDE_PROJECT_DIR --config ark.config.json`}]}]}},null,2)}
16
- `}function k(o){const r=s(o);return`${JSON.stringify({"ark-write-gate":{PreToolUse:[{matcher:"write_to_file|replace_file_content|multi_replace_file_content",hooks:[{type:"command",timeout:30,command:`${r} ${c} --hook --hook-repair --fail-on-new-smells --root . --config ark.config.json`}]}]}},null,2)}
17
- `}function d(o){const{command:r,args:e}=i(o,c,["--root",".","--config","ark.config.json"]);return`${JSON.stringify({$schema:"https://opencode.ai/config.json",mcp:{ark:{type:"local",command:[r,...e],enabled:!0}}},null,2)}
18
- `}function h(o,r){let e,t;try{e=o&&o.trim()?JSON.parse(o):{},t=JSON.parse(r)}catch{return null}if(!e||typeof e!="object"||Array.isArray(e)||!t||typeof t!="object"||Array.isArray(t))return null;const n=t["ark-write-gate"];if(!n||typeof n!="object")return null;const a={...e,"ark-write-gate":n};return`${JSON.stringify(a,null,2)}
19
- `}function y(o,r){let e,t;try{e=o&&o.trim()?JSON.parse(o):{},t=JSON.parse(r)}catch{return null}if(!e||typeof e!="object"||Array.isArray(e))return null;const n={...e};!n.$schema&&t.$schema&&(n.$schema=t.$schema);const a=e.mcp&&typeof e.mcp=="object"&&!Array.isArray(e.mcp)?{...e.mcp}:{};return a.ark=t.mcp.ark,n.mcp=a,`${JSON.stringify(n,null,2)}
20
- `}export{c as PREFERRED_MCP_BIN,k as antigravityHooks,l as claudeSettings,p as codexHooks,f as codexProjectConfig,u as grokHooks,g as grokProjectConfig,h as mergeAntigravityArkHook,y as mergeOpencodeArkMcp,d as opencodeProjectConfig};
16
+ `}function _(e){const r=i(e);return`${JSON.stringify({hooks:{SessionStart:[{hooks:[{type:"command",timeout:30,command:`${r} ${s} --session-context --root . --root-env GROK_WORKSPACE_ROOT,CLAUDE_PROJECT_DIR --config ark.config.json`}]}],PreToolUse:[{matcher:"Write|Edit|MultiEdit|write|search_replace",hooks:[{type:"command",timeout:30,command:`${r} ${s} --hook --hook-repair --fail-on-new-smells --root . --root-env GROK_WORKSPACE_ROOT,CLAUDE_PROJECT_DIR --config ark.config.json`}]}]}},null,2)}
17
+ `}function A(e){const r=i(e);return`${JSON.stringify({"ark-write-gate":{PreToolUse:[{matcher:"write_to_file|replace_file_content|multi_replace_file_content",hooks:[{type:"command",timeout:30,command:`${r} ${s} --hook --hook-repair --fail-on-new-smells --root . --config ark.config.json`}]}]}},null,2)}
18
+ `}function j(e){const{command:r,args:o}=l(e,s,["--root",".","--config","ark.config.json"]);return`${JSON.stringify({$schema:"https://opencode.ai/config.json",mcp:{ark:{type:"local",command:[r,...o],enabled:!0}}},null,2)}
19
+ `}function E(e,r){let o,t;try{o=e&&e.trim()?JSON.parse(e):{},t=JSON.parse(r)}catch{return null}if(!o||typeof o!="object"||Array.isArray(o)||!t||typeof t!="object"||Array.isArray(t))return null;const c=t.hooks,p=(Array.isArray(c?.preToolUse)?c.preToolUse:[]).find(n=>n&&typeof n=="object"&&typeof n.command=="string"&&/arkgate-mcp|ark-mcp/.test(n.command)&&/\s--hook(?:\s|$)/.test(` ${n.command} `));if(!p)return null;const m=o.hooks&&typeof o.hooks=="object"&&!Array.isArray(o.hooks)?{...o.hooks}:{},f=Array.isArray(m.preToolUse)?[...m.preToolUse]:[],k=n=>n&&typeof n=="object"&&typeof n.command=="string"&&/arkgate-mcp|ark-mcp/.test(n.command)&&/\s--hook(?:\s|$)/.test(` ${n.command} `)&&(typeof n.matcher!="string"||/Write/.test(n.matcher)||/StrReplace/.test(n.matcher)),g=f.filter(n=>!k(n));m.preToolUse=[...g,p];const u={...o,version:o.version??t.version??1,hooks:m};return`${JSON.stringify(u,null,2)}
20
+ `}function S(e,r){let o,t;try{o=e&&e.trim()?JSON.parse(e):{},t=JSON.parse(r)}catch{return null}if(!o||typeof o!="object"||Array.isArray(o)||!t||typeof t!="object"||Array.isArray(t))return null;const c=t["ark-write-gate"];if(!c||typeof c!="object")return null;const a={...o,"ark-write-gate":c};return`${JSON.stringify(a,null,2)}
21
+ `}function C(e,r){let o,t;try{o=e&&e.trim()?JSON.parse(e):{},t=JSON.parse(r)}catch{return null}if(!o||typeof o!="object"||Array.isArray(o))return null;const c={...o};!c.$schema&&t.$schema&&(c.$schema=t.$schema);const a=o.mcp&&typeof o.mcp=="object"&&!Array.isArray(o.mcp)?{...o.mcp}:{};return a.ark=t.mcp.ark,c.mcp=a,`${JSON.stringify(c,null,2)}
22
+ `}export{s as PREFERRED_MCP_BIN,A as antigravityHooks,y as claudeSettings,$ as codexHooks,O as codexProjectConfig,h as cursorHooks,_ as grokHooks,R as grokProjectConfig,S as mergeAntigravityArkHook,E as mergeCursorArkHook,C as mergeOpencodeArkMcp,j as opencodeProjectConfig};