arkgate 4.6.2 → 4.6.4

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,6 +5,51 @@ in the immutable pre-2.0 archive linked below.
5
5
 
6
6
  ## Unreleased
7
7
 
8
+ ## 4.6.4 — 2026-08-18
9
+
10
+ **Patch** over **4.6.3**. `ark upgrade` now tells Codex users how to activate the local
11
+ `apply_patch` boundary after upgrading: refresh the project hook, restart Codex/local Desktop,
12
+ trust the exact hook definition, and verify `doctor.writePath` after a governed patch.
13
+ **No required config migration.**
14
+
15
+ **Status: prepared** (not yet on npm `latest`; see `docs/releases/4.6.4.md`).
16
+
17
+ ### Changed
18
+
19
+ - **Upgrade JSON:** `whatsNew.items` includes stable id `codex-hard-write` with the exact
20
+ `--install-agent-gates --tools codex --force` command and evidence to inspect.
21
+ - **Upgrade human output:** **Suggested improvements** prints the same refresh/restart/trust/check
22
+ path on preview and apply, including nothing-to-apply previews.
23
+ - **Upgrade skill:** flat and Agent Skills guidance tells Codex to exercise a governed
24
+ `apply_patch` and keeps hosted/specialized/shell/direct/incomplete/human paths CI-backed.
25
+ - **Stale MCP fail-closed:** when process version no longer matches the project install,
26
+ `ark_identity` reports non-authoritative evidence and project tools return
27
+ `PROCESS_PACKAGE_STALE` until restart/retarget.
28
+ - **Global CLI handoff:** a modern stale global `ark upgrade` delegates the original invocation
29
+ to the project-local `node_modules/arkgate/bin/ark.mjs` instead of managing from the wrong PATH
30
+ version. Pre-4.6.4 globals still require one `npx arkgate upgrade` entry.
31
+
32
+ ## 4.6.3 — 2026-08-18
33
+
34
+ **Patch** over **4.6.2**. Codex CLI and local ChatGPT Desktop/App Server now get a
35
+ runtime-proven pre-write block for complete `apply_patch` calls. ArkGate accepts the current
36
+ `tool_input.command` payload, while incomplete, hosted, specialized, shell/direct, and human
37
+ write paths remain CI-backed. **No required config migration.**
38
+
39
+ **Status: published** (npm `latest` from signed tag `v4.6.3`; OIDC run `32167523804`;
40
+ see `docs/releases/4.6.3.md`).
41
+
42
+ ### Changed
43
+
44
+ - **Codex hook payload:** current `PreToolUse` `apply_patch` bodies are read from
45
+ `tool_input.command`; historical patch/input/content fields stay compatible.
46
+ - **Operation-scoped hard write:** a complete trusted and runtime-observed local patch can report
47
+ `hard:true` and exit `2` before disk mutation. Hook files alone stay unverified.
48
+ - **Honesty surfaces:** host matrix, doctor/status, `--require-write-hook codex`, onboarding,
49
+ upgrade self-service, skills, and public docs now share the same boundary.
50
+ - **All-path backstop:** required `arkgate-check --strict-merge` CI remains mandatory. MCP stays
51
+ advisory and repair reinjection is not claimed.
52
+
8
53
  ## 4.6.2 — 2026-08-16
9
54
 
10
55
  **Patch** over **4.6.1**. First-contact copy: a newcomer (human or coding agent) sees what
@@ -12,7 +57,7 @@ to do in a few lines — `arkgate` / `arkgate-check --help`, start wrap-up, doct
12
57
  #1, write-gate deny, SessionStart, MCP tool order, and the five doors. Same 13 skill names.
13
58
  **No required config migration.**
14
59
 
15
- **Status: prepared** (not yet on npm `latest`; see `docs/releases/4.6.2.md`).
60
+ **Status: published** (on npm `latest`; see `docs/releases/4.6.2.md`).
16
61
 
17
62
  ### Changed
18
63
 
package/README.md CHANGED
@@ -16,9 +16,9 @@ and makes sure a “green” check means something real.
16
16
 
17
17
  </div>
18
18
 
19
- > **ArkGate 4.6.1** is on npm `latest` — five doors + a lock on the constitution.
20
- > **4.6.2** is prepared on this tree (first-contact copy; not yet `latest`).
21
- > [4.6.2 notes](docs/releases/4.6.2.md) · [4.6.1](docs/releases/4.6.1.md) · [4.6.0](docs/releases/4.6.0.md) · [Docs hub](docs/README.md) · [Product voice](docs/product-voice.md)
19
+ > **ArkGate 4.6.3** is on npm `latest` — Codex CLI/local Desktop hard-block complete trusted `apply_patch`.
20
+ > **4.6.4 is prepared** `ark upgrade` tells Codex how to refresh, trust, restart, and verify it.
21
+ > [4.6.4 notes](docs/releases/4.6.4.md) · [4.6.3](docs/releases/4.6.3.md) · [4.6.2](docs/releases/4.6.2.md) · [4.6.1](docs/releases/4.6.1.md) · [Docs hub](docs/README.md) · [Product voice](docs/product-voice.md)
22
22
 
23
23
  ---
24
24
 
@@ -117,14 +117,15 @@ Details: [docs/use.md](docs/use.md).
117
117
  | 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) |
118
118
  | 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) |
119
119
  | 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) |
120
- | 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) |
121
- | 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) |
120
+ | 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 |
121
+ | OpenAI Codex | **Hard** block for listed ops (PreToolUse `apply_patch` in Codex CLI and local ChatGPT Desktop/App Server) 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 |
122
122
  | 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 |
123
123
 
124
124
  **Read the CI column:** for every host, the repository-wide hard guarantee is a **required**
125
125
  GitHub **status context** that runs the CLI — not “CI file present,” and not the CLI binary name alone.
126
- Codex/OpenCode never get a fake hard write claim. Cursor hard write covers only listed
127
- `preToolUse` ops when `.cursor/hooks.json` is installed and trusted Shell/Tab/human edits still rely on CI.
126
+ Codex hard write covers only a complete local `apply_patch`; Cursor covers only listed
127
+ `preToolUse` ops. In both cases the project hook must be installed + trusted, while shell/direct
128
+ filesystem writes, hosted or specialized opt-out paths, and human edits still rely on CI.
128
129
 
129
130
  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.
130
131
  <!-- arkgate-host-support:end -->
@@ -133,10 +134,11 @@ This table describes the supported profile **after its files are installed and t
133
134
 
134
135
  The split above is a deliberate trade-off, not a gap. ArkGate validates at the earliest boundary
135
136
  each host offers and enforces at the earliest boundary a repository can make non-bypassable: the
136
- required merge status. Hard hooks (Claude Code, Grok Build, Google Antigravity) deny the listed
137
- write operations at write time; advisory surfaces (MCP, rules, OpenCode plugins) coach the agent
138
- while it works. But any local boundary can be routed around — another tool, a direct filesystem
139
- write, a human edit so the only guarantee ArkGate claims for every path is the
137
+ required merge status. Hard hooks (Claude Code, Grok Build, Google Antigravity, Cursor, and
138
+ Codex’s complete local `apply_patch`) deny their listed write operations at write time; advisory
139
+ surfaces (MCP, rules, OpenCode plugins) coach the agent while it works. But any local boundary can
140
+ be routed aroundanother tool, a hosted/specialized path, a direct filesystem write, or a human
141
+ edit — so the only guarantee ArkGate claims for every path is the
140
142
  `arkgate-check --strict-merge` check, and only when the repository makes that status required.
141
143
  Local checks optimize feedback speed; the merge gate owns correctness.
142
144
 
@@ -214,8 +216,11 @@ for real systems. Details: [docs/production-hardening.md](docs/production-harden
214
216
  | Config · package surface · TS | [configuration](docs/configuration.md) · [package-surface](docs/package-surface.md) · [typescript-support](docs/typescript-support.md) |
215
217
  | Brownfield | [docs/brownfield-adoption.md](docs/brownfield-adoption.md) |
216
218
  | Security | [SECURITY.md](SECURITY.md) |
217
- | Current release (4.6.1 on npm `latest`) | [docs/releases/4.6.1.md](docs/releases/4.6.1.md) · [CHANGELOG](CHANGELOG.md) |
218
- | Prepared (4.6.2 — not yet `latest`) | [docs/releases/4.6.2.md](docs/releases/4.6.2.md) |
219
+ | Current release (4.6.3 on npm `latest`) | [docs/releases/4.6.3.md](docs/releases/4.6.3.md) · [CHANGELOG](CHANGELOG.md) |
220
+ | Prepared patch (4.6.4) | [docs/releases/4.6.4.md](docs/releases/4.6.4.md) |
221
+ | Prior (4.6.2) | [docs/releases/4.6.2.md](docs/releases/4.6.2.md) |
222
+ | Prior (4.6.1) | [docs/releases/4.6.1.md](docs/releases/4.6.1.md) |
223
+ | Prior (4.6.0) | [docs/releases/4.6.0.md](docs/releases/4.6.0.md) |
219
224
  | Prior (4.5.7) | [docs/releases/4.5.7.md](docs/releases/4.5.7.md) |
220
225
  | Prior (4.5.0) | [docs/releases/4.5.0.md](docs/releases/4.5.0.md) |
221
226
  | Prior (4.4.0) | [docs/releases/4.4.0.md](docs/releases/4.4.0.md) |
@@ -358,6 +358,7 @@ function mapAntigravityToolCall(toolCall) {
358
358
  * Antigravity: { toolCall: { name, args: { TargetFile, CodeContent, … } } }
359
359
  * Cursor: { tool_name, tool_input, hook_event_name?, workspace_roots? }
360
360
  * Write uses `contents`; StrReplace maps to Edit (path/old_string/new_string).
361
+ * Codex: { tool_name: "apply_patch", tool_input: { command: "*** Begin Patch..." } }
361
362
  */
362
363
  function normalizeHookPayload(payload, grokHookEvent = Boolean(process.env.GROK_HOOK_EVENT)) {
363
364
  const antigravityStyle =
@@ -669,7 +670,10 @@ function runHookPayload(payload, gate, config, args, ts, attemptContext, output
669
670
  attemptContext?.grokHookEvent ?? Boolean(process.env.GROK_HOOK_EVENT)
670
671
  );
671
672
  if (toolName === 'ApplyPatch') {
672
- const patch = toolInput.patch ?? toolInput.input ?? toolInput.content;
673
+ // Current Codex CLI/Desktop hook schema uses tool_input.command. Keep the
674
+ // historical fields for older clients and existing integration fixtures.
675
+ const patch =
676
+ toolInput.command ?? toolInput.patch ?? toolInput.input ?? toolInput.content;
673
677
  const parsedPatch = codexPatchWrites(patch, args.root);
674
678
  // Codex ApplyPatch is only preflighted when Ark can reconstruct every file operation.
675
679
  // An incomplete reconstruction must not be mislabeled as atomic or hard enforcement.
@@ -1956,11 +1960,14 @@ export async function runArkMcp({ hookInput } = {}) {
1956
1960
  }
1957
1961
 
1958
1962
  function contextFor(binding) {
1963
+ const processPackage = processPackageHonesty();
1959
1964
  return {
1960
1965
  projectIdentity,
1961
1966
  binding,
1962
- authoritative: binding.authoritative,
1963
- processPackage: processPackageHonesty(),
1967
+ // A correctly bound project is still non-authoritative when this long-lived
1968
+ // process loaded a different package version than the project now resolves.
1969
+ authoritative: binding.authoritative && !processPackage.processStale,
1970
+ processPackage,
1964
1971
  };
1965
1972
  }
1966
1973
 
@@ -2037,13 +2044,36 @@ export async function runArkMcp({ hookInput } = {}) {
2037
2044
  );
2038
2045
  }
2039
2046
 
2047
+ function staleProcessFailureResult(binding) {
2048
+ const processPackage = processPackageHonesty();
2049
+ return withProjectContext(
2050
+ {
2051
+ content: [
2052
+ {
2053
+ type: 'text',
2054
+ text: JSON.stringify({
2055
+ ok: false,
2056
+ error: {
2057
+ code: 'PROCESS_PACKAGE_STALE',
2058
+ message: processPackage.nextAction,
2059
+ },
2060
+ }),
2061
+ },
2062
+ ],
2063
+ isError: true,
2064
+ },
2065
+ binding
2066
+ );
2067
+ }
2068
+
2040
2069
  const TOOLS = [
2041
2070
  {
2042
2071
  name: 'ark_identity',
2043
2072
  description:
2044
2073
  'First call. Prove this MCP process is the right project: pass project.expectedRoot ' +
2045
2074
  '(exact absolute root) and reuse the returned projectId. Do this before any other Ark tool. ' +
2046
- 'A missing, unmatched, or different root means restart the host and use the local CLI.',
2075
+ 'A missing/unmatched root or processPackage.processStale means restart/retarget the host ' +
2076
+ 'and use the project-local CLI until identity and package versions align.',
2047
2077
  inputSchema: { type: 'object', properties: {} },
2048
2078
  },
2049
2079
  {
@@ -3001,6 +3031,13 @@ export async function runArkMcp({ hookInput } = {}) {
3001
3031
  reply(id, bindingFailureResult(binding));
3002
3032
  return;
3003
3033
  }
3034
+ // Keep ark_identity available so the host can diagnose the stale process,
3035
+ // but fail every project tool closed until the MCP server restarts on the
3036
+ // version installed for this root.
3037
+ if (params?.name !== 'ark_identity' && processPackageHonesty().processStale) {
3038
+ reply(id, staleProcessFailureResult(binding));
3039
+ return;
3040
+ }
3004
3041
  try {
3005
3042
  reply(id, withProjectContext(handler(params, binding), binding));
3006
3043
  } catch (error) {
package/bin/ark.mjs CHANGED
@@ -831,6 +831,7 @@ async function main() {
831
831
  packageInstallArgv,
832
832
  runArkCheck,
833
833
  cliVersion: cliVersion(),
834
+ rawArgv: process.argv.slice(2),
834
835
  shouldSkipArkgateInstall,
835
836
  });
836
837
  } catch (error) {
@@ -370,9 +370,10 @@ ${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/Antigravity/Cursor provide a hard local write boundary
374
- (Cursor: Write/StrReplace when .cursor/hooks.json is trusted); Codex/OpenCode use
375
- advisory MCP plus CI. The experimental runtime is not required.
373
+ and the CI check passes. Claude/Grok/Antigravity/Cursor provide hard local boundaries for their
374
+ listed operations. Codex provides a hard boundary for complete local apply_patch when
375
+ .codex/hooks.json is trusted and runtime-observed; hosted/specialized/direct-write paths remain
376
+ CI-backed. OpenCode uses advisory MCP plus CI. The experimental runtime is not required.
376
377
  `;
377
378
  }
378
379
 
@@ -88,8 +88,8 @@ Options:
88
88
  --remove-host <host>
89
89
  Preview or apply removal of that compact host integration; re-add it with --tools <host>.
90
90
  --require-write-hook <host>
91
- Require and verify a hard local write hook for Claude, Grok, Antigravity, or Cursor.
92
- Codex/OpenCode are advisory-write plus hard CI merge only; impossible requests fail before any write.
91
+ Require and verify a hard local write hook for Claude, Grok, Antigravity, Cursor, or Codex.
92
+ Codex covers complete local apply_patch only; OpenCode is advisory-write plus hard CI merge.
93
93
 
94
94
  Interactive mode (TTY, no --yes): asks what application shape you are building and maps it to a preset.
95
95
  Non-interactive (no TTY): uses the same defaults as --yes — never calls readline on a null interface.
@@ -183,10 +183,10 @@ export function checkUsageAll() {
183
183
  'transition. Weakening or judgment-required findings fail unless --policy-ack names',
184
184
  'every finding and is bound to both policy hashes. Use --policy-base/--policy-base-ref',
185
185
  'for an explicit comparison; ARK_POLICY_BASE_REF is the CI environment equivalent.',
186
- 'Add --require-write-hook claude|grok|antigravity|cursor to validate a hard local write',
187
- 'boundary for that specific host. Codex and OpenCode expose advisory MCP (plus best-effort',
188
- 'hooks where applicable) and the shared CI check; merge blocking requires repository policy',
189
- 'to make that status required.',
186
+ 'Add --require-write-hook claude|grok|antigravity|cursor|codex to validate a hard local',
187
+ 'write boundary for that specific host. Codex covers complete local apply_patch only;',
188
+ 'hosted/specialized/direct-write paths and OpenCode remain CI-backed. Merge blocking requires',
189
+ 'repository policy to make the shared CI status required.',
190
190
  '',
191
191
  '--require-gates implies --strict-config and fails when the Ark contract in AGENTS.md,',
192
192
  'the project-rooted Ark server in .mcp.json, or fail-closed CI is missing/invalid.',
@@ -194,7 +194,7 @@ export function checkUsageAll() {
194
194
  '',
195
195
  '--install-agent-gates writes AGENTS.md, .mcp.json, and the CI workflow for every',
196
196
  'project, plus tool-specific templates. Known tools: claude, cursor, codex, grok',
197
- '(Claude/Grok/Antigravity/Cursor hard-write hooks when covered; Codex advisory MCP;',
197
+ '(Claude/Grok/Antigravity/Cursor hard-write hooks when covered; Codex hard local apply_patch;',
198
198
  'shared CI check for all) and',
199
199
  'windsurf, cline, copilot, kiro, roo, continue, gemini',
200
200
  '(instruction-tier rule files derived from the same contract).',
@@ -17,7 +17,7 @@
17
17
  */
18
18
  function hostProfile(label, hookPath, hookSurface, hookOperations, hardWrite, repairPayload, extras = {}) {
19
19
  // EH07: repair envelope emission ≠ reinjection guarantee.
20
- // Codex hooks may emit --hook-repair JSON while reinjection stays host-dependent / not hard.
20
+ // Cursor/Codex hooks may emit --hook-repair JSON while reinjection stays host-dependent.
21
21
  const repairEnvelopeEmitted =
22
22
  extras.repairEnvelopeEmitted === true || repairPayload === true;
23
23
  const repairReinjectionGuaranteed = hardWrite === true && repairPayload === true;
@@ -94,15 +94,15 @@ export const HOST_SUPPORT_MATRIX = Object.freeze({
94
94
  codex: hostProfile(
95
95
  'OpenAI Codex',
96
96
  '.codex/hooks.json',
97
- 'Best-effort PreToolUse `apply_patch`; Code Mode hosts may bypass the event',
97
+ 'PreToolUse `apply_patch` in Codex CLI and local ChatGPT Desktop/App Server',
98
98
  ['apply_patch'],
99
- false,
99
+ true,
100
100
  false,
101
101
  {
102
- // Install writes --hook-repair; envelope can be emitted; reinjection is not guaranteed.
102
+ // Install writes --hook-repair; envelope can emit, but host reinjection is not guaranteed.
103
103
  repairEnvelopeEmitted: true,
104
104
  operationCoverage: {
105
- apply_patch: false,
105
+ apply_patch: true,
106
106
  shell: false,
107
107
  'pre-commit': false,
108
108
  },
@@ -152,15 +152,12 @@ export function renderHostSupportMatrixMarkdown() {
152
152
  const rows = HOST_SUPPORT_HOSTS.map((host) => {
153
153
  const profile = HOST_SUPPORT_MATRIX[host];
154
154
  const capabilities = profile.capabilities;
155
- // Fail-closed honesty: Codex/OpenCode never claim hard write; CI is required-status.
156
- // Cursor claims hard only for listed preToolUse ops when hooks are installed + trusted.
155
+ // Fail-closed honesty: hard hosts claim only listed operations when hooks are
156
+ // installed + trusted. OpenCode remains advisory; CI is required-status.
157
157
  // hookSurface already includes "PreToolUse/preToolUse …" — do not prefix again.
158
158
  let local;
159
159
  if (capabilities['hard-write']) {
160
160
  local = `**Hard** block for listed ops (${profile.hookSurface}) when installed + trusted`;
161
- } else if (host === 'codex') {
162
- local =
163
- '**Advisory / best-effort** at write (not equivalent to Claude/Grok/Cursor hard block)';
164
161
  } else if (host === 'opencode') {
165
162
  local =
166
163
  '**Advisory / best-effort** at write (MCP + optional plugin; not a hard boundary)';
@@ -172,7 +169,7 @@ export function renderHostSupportMatrixMarkdown() {
172
169
  if (capabilities['repair-reinjection-guaranteed']) {
173
170
  repair = 'Emitted on hook deny; host must re-inject (hard path when installed + trusted)';
174
171
  } else if (capabilities['repair-envelope-emitted']) {
175
- repair = 'Envelope may emit (`--hook-repair`); reinjection **not** guaranteed (advisory host)';
172
+ repair = 'Envelope may emit (`--hook-repair`); reinjection **not** guaranteed';
176
173
  } else {
177
174
  repair = 'No hard-boundary payload';
178
175
  }
@@ -188,8 +185,9 @@ ${rows}
188
185
 
189
186
  **Read the CI column:** for every host, the repository-wide hard guarantee is a **required**
190
187
  GitHub **status context** that runs the CLI — not “CI file present,” and not the CLI binary name alone.
191
- Codex/OpenCode never get a fake hard write claim. Cursor hard write covers only listed
192
- \`preToolUse\` ops when \`.cursor/hooks.json\` is installed and trusted Shell/Tab/human edits still rely on CI.
188
+ Codex hard write covers only a complete local \`apply_patch\`; Cursor covers only listed
189
+ \`preToolUse\` ops. In both cases the project hook must be installed + trusted, while shell/direct
190
+ filesystem writes, hosted or specialized opt-out paths, and human edits still rely on CI.
193
191
 
194
192
  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.`;
195
193
  }
@@ -226,8 +224,8 @@ export function doctorWritePathHonestyMessage(activeHost, hardWriteActive) {
226
224
  if (host === 'cursor' && !hardWriteActive) {
227
225
  return `Cursor: pre-write block is supported for Write/StrReplace when .cursor/hooks.json is installed + trusted; without runtime-observed hook evidence, the block is unverified. ${mergeBoundary}.`;
228
226
  }
229
- if (host === 'codex') {
230
- return `Codex: edits are warning only (not blocked) at write time. ${mergeBoundary}.`;
227
+ if (host === 'codex' && !hardWriteActive) {
228
+ return `Codex: a trusted PreToolUse hook can block complete local apply_patch calls in CLI and Desktop; without fresh runtime-observed apply_patch evidence, the block is unverified. Specialized/hosted paths and direct writes still rely on CI. ${mergeBoundary}.`;
231
229
  }
232
230
  if (host === 'opencode') {
233
231
  return `OpenCode: edits are warning only (not blocked). ${mergeBoundary}.`;
@@ -725,7 +725,7 @@ export function buildPostUpgradeChecks(root, options = {}) {
725
725
  neverGateInput: true,
726
726
  checks,
727
727
  mcpNote:
728
- 'If you used Ark MCP this session: restart/retarget MCP after package bump so process arkgateVersion matches project install; always pass project.expectedRoot + expectedProjectId (WI01). Prefer project-local CLI until identity matched and versions align.',
728
+ 'If you used Ark MCP this session: restart/retarget after the package bump. A stale process is non-authoritative and project tools return PROCESS_PACKAGE_STALE; always pass project.expectedRoot + expectedProjectId. Prefer project-local CLI until identity and package versions align.',
729
729
  };
730
730
  }
731
731
 
@@ -299,7 +299,7 @@ export function collectStatusFacts(options = {}) {
299
299
  ? writePath.activeHost.trim().toLowerCase()
300
300
  : String(activeHost).trim().toLowerCase();
301
301
  const matrix = HOST_SUPPORT_MATRIX[hostKey] ?? null;
302
- // Soft only when the host is known and matrix hard-write is false (Cursor/Codex/OpenCode).
302
+ // Soft only when the host is known and matrix hard-write is false (currently OpenCode).
303
303
  const softWriteHost = Boolean(matrix && matrix.capabilities?.['hard-write'] !== true);
304
304
  const writePathHonesty = buildWritePathHonesty(hostKey, hardWriteActive, {
305
305
  packageInstalled: writePath?.enforcementState?.localWrite?.installed !== false,
@@ -148,8 +148,9 @@ function staleCliRefuseMessage(detail) {
148
148
  * project's installed arkgate. Legacy global 2.x mutates skills; a newer
149
149
  * project pin must not be managed by an older outside-tree binary.
150
150
  *
151
- * Allow: no local install; CLI package root inside the project install tree
152
- * (realpath); running CLI version >= project installed version (including newer globals).
151
+ * Allow directly: no local install or CLI package root inside the project install tree
152
+ * (realpath). Any outside-tree modern CLI prefers a project-local handoff, even at the same
153
+ * or newer version, so one checkout never manages another through PATH state.
153
154
  *
154
155
  * @param {string} root project root for --root
155
156
  * @param {{
@@ -157,7 +158,7 @@ function staleCliRefuseMessage(detail) {
157
158
  * cliPackageRoot?: string|null,
158
159
  * projectPackageJsonPath?: string|null,
159
160
  * }} [options]
160
- * @returns {{ refuse: boolean, reason: string, message?: string, cliVersion?: string|null, projectVersion?: string|null, cliPackageRoot?: string|null, projectPackageRoot?: string|null }}
161
+ * @returns {{ refuse: boolean, handoff?: boolean, reason: string, message?: string, cliVersion?: string|null, projectVersion?: string|null, cliPackageRoot?: string|null, projectPackageRoot?: string|null }}
161
162
  */
162
163
  export function evaluateStaleUpgradeCli(root, options = {}) {
163
164
  const projectPkgPath =
@@ -272,7 +273,8 @@ export function evaluateStaleUpgradeCli(root, options = {}) {
272
273
 
273
274
  return {
274
275
  refuse: false,
275
- reason: 'outside-cli-ok',
276
+ handoff: true,
277
+ reason: 'outside-cli-project-local-preferred',
276
278
  cliVersion,
277
279
  projectVersion,
278
280
  cliPackageRoot,
@@ -325,6 +327,15 @@ function verify(root, json, arkCheck, runArkCheck) {
325
327
  return { exitCode: result.status ?? 1, stderr: result.stderr?.trim() || undefined };
326
328
  }
327
329
 
330
+ function runProjectLocalCli({ root, script, argv }) {
331
+ const result = spawnSync(process.execPath, [script, ...argv], {
332
+ cwd: root,
333
+ stdio: 'inherit',
334
+ env: { ...process.env, ARK_PROJECT_LOCAL_HANDOFF: '1' },
335
+ });
336
+ return result.status ?? 1;
337
+ }
338
+
328
339
  export function runUpgradeCommand(args, dependencies) {
329
340
  const root = args.root;
330
341
  // Fail closed before install/plan when PATH resolves a global/stale CLI older
@@ -339,6 +350,28 @@ export function runUpgradeCommand(args, dependencies) {
339
350
  cliVersion: dependencies?.cliVersion,
340
351
  cliPackageRoot: dependencies?.cliPackageRoot,
341
352
  });
353
+ if (staleGuard?.refuse || staleGuard?.handoff) {
354
+ const projectLocalScript =
355
+ typeof staleGuard.projectPackageRoot === 'string'
356
+ ? path.join(staleGuard.projectPackageRoot, 'bin', 'ark.mjs')
357
+ : null;
358
+ if (
359
+ process.env.ARK_PROJECT_LOCAL_HANDOFF !== '1' &&
360
+ Array.isArray(dependencies?.rawArgv) &&
361
+ projectLocalScript &&
362
+ fs.existsSync(projectLocalScript)
363
+ ) {
364
+ const handoff =
365
+ typeof dependencies.runProjectLocalCli === 'function'
366
+ ? dependencies.runProjectLocalCli
367
+ : runProjectLocalCli;
368
+ return handoff({
369
+ root,
370
+ script: projectLocalScript,
371
+ argv: dependencies.rawArgv,
372
+ });
373
+ }
374
+ }
342
375
  if (staleGuard?.refuse) {
343
376
  const message = staleGuard.message || 'Refusing ark upgrade: stale CLI.';
344
377
  if (args.json) {
@@ -65,6 +65,15 @@ export function buildUpgradeWhatsNewSuggestions() {
65
65
  why:
66
66
  'Project skills follow this pin. Shared homes stay on the newest ArkGate on the machine (additive; never downgrade). Orphan 2.x global skills stop coaching the wrong version.',
67
67
  },
68
+ {
69
+ id: 'codex-hard-write',
70
+ title: 'Codex complete local apply_patch block',
71
+ try: 'npx arkgate-check --install-agent-gates --tools codex --force',
72
+ inspect:
73
+ '.codex/hooks.json trust + doctor.writePath after a runtime-observed apply_patch',
74
+ why:
75
+ 'After upgrade, refresh the project hook, restart Codex or local Desktop, and trust the exact hook definition. Only a complete observed apply_patch is hard; hosted, specialized, shell/direct, incomplete, and human paths still rely on required CI.',
76
+ },
68
77
  {
69
78
  id: 'deep-module-coach',
70
79
  title: 'Deep-module coach (hot paths + deepening)',
@@ -129,7 +138,16 @@ export function buildUpgradeWhatsNewSuggestions() {
129
138
  try: 'ark_identity with project.expectedRoot; read processPackage on every tool',
130
139
  inspect: 'processPackage.processPackageMismatch / processStale + nextAction',
131
140
  why:
132
- 'One user, many checkouts: after package bump, restart MCP so process arkgateVersion matches install. Prefer project-local CLI until identity matched and versions align.',
141
+ 'One user, many checkouts: after package bump, restart MCP so process arkgateVersion matches install. A stale process is non-authoritative and project tools fail closed until restart.',
142
+ },
143
+ {
144
+ id: 'process-version-recovery',
145
+ title: 'Stale MCP / global CLI recovery',
146
+ try: 'npx arkgate upgrade --json',
147
+ inspect:
148
+ 'processPackage.processStale + PROCESS_PACKAGE_STALE; project-local CLI handoff for stale global upgrade',
149
+ why:
150
+ 'A long-lived MCP on an older package can no longer return authoritative project evidence. A newer global ArkGate that is older than this project automatically hands `ark upgrade` to the project-local CLI instead of staying stuck on PATH.',
133
151
  },
134
152
  ],
135
153
  };
@@ -219,6 +219,9 @@ function requiredWriteOperations(relativePath) {
219
219
  if (relativePath === '.cursor/hooks.json' || relativePath.startsWith('.cursor/hooks')) {
220
220
  return ['Write', 'StrReplace'];
221
221
  }
222
+ if (relativePath === '.codex/hooks.json' || relativePath.startsWith('.codex/hooks')) {
223
+ return ['apply_patch'];
224
+ }
222
225
  return ['Write', 'Edit', 'MultiEdit'];
223
226
  }
224
227
 
@@ -445,6 +448,7 @@ export function detectWritePathInventory(root) {
445
448
  const grokHook = hookEvidence(root, '.grok/hooks/ark-write-gate.json');
446
449
  const antigravityHook = hookEvidence(root, '.agents/hooks.json');
447
450
  const cursorHook = cursorHookEvidence(root);
451
+ const codexHook = hookEvidence(root, '.codex/hooks.json');
448
452
  const hosts = {
449
453
  claude: hostRecord(
450
454
  claudeHook.hard,
@@ -478,11 +482,11 @@ export function detectWritePathInventory(root) {
478
482
  [],
479
483
  merge
480
484
  ),
481
- // Codex 0.123+ emits PreToolUse for the native apply_patch handler, but some
482
- // Code Mode hosts execute deferred nested writes without dispatching that
483
- // project hook. Keep the installed hook as best-effort protection; do not
484
- // report a hard boundary that cannot be verified for every write surface.
485
- codex: hostRecord([], codexMcpEvidence(root), [], merge),
485
+ // Current Codex CLI and local ChatGPT Desktop/App Server synchronously run
486
+ // PreToolUse for apply_patch. Disk evidence remains unverified until a fresh
487
+ // covered invocation; hosted/specialized/direct-write paths stay outside scope.
488
+ // Repair envelopes may emit, but host reinjection is not guaranteed.
489
+ codex: hostRecord(codexHook.hard, codexMcpEvidence(root), [], merge),
486
490
  // OpenCode: MCP only (plugin hooks are incomplete / subagent-bypassable).
487
491
  opencode: hostRecord([], opencodeMcpEvidence(root), [], merge),
488
492
  };
@@ -111,9 +111,9 @@ export function detectWritePathCapabilities(root, explicitHost, attempt) {
111
111
  } else if (mode === 'mcp-only') {
112
112
  const honesty =
113
113
  activeHost === 'codex'
114
- ? 'Codex local write is advisory (MCP + best-effort hooks.json not a hard boundary; ' +
115
- 'not equivalent to Claude/Grok PreToolUse hard-write + repair). ' +
116
- 'The hard merge backstop is CI --strict-merge plus a required status check.'
114
+ ? 'Codex MCP is advisory because no complete Ark apply_patch hook was detected. ' +
115
+ 'Install and trust .codex/hooks.json for the operation-scoped pre-write block; ' +
116
+ 'CI --strict-merge plus a required status remains the all-path merge boundary.'
117
117
  : activeHost === 'opencode'
118
118
  ? 'OpenCode local write is advisory (MCP + optional experimental plugin — not a hard boundary; ' +
119
119
  'not equivalent to Claude/Grok/Antigravity PreToolUse hard-write). ' +