diff --git a/.github/workflows/real-model.yml b/.github/workflows/real-model.yml
index 712df50aa..62156e964 100644
--- a/.github/workflows/real-model.yml
+++ b/.github/workflows/real-model.yml
@@ -1058,12 +1058,12 @@ jobs:
# BOTH harness binaries: claude (ClaudeCodeHarness) drives the blessed behavioral gate; codex (CodexHarness)
# drives the informational behavioral proof + the deterministic real-binary proofs. FATAL install (a missing
# binary must RED the lane, never let a gating arm self-skip green — "skip ≠ pass"). Pins match the worker image
- # ARGs + the harness DefaultVersion consts the CLI surface is verified against (claude 2.1.193 / codex 0.142.2),
+ # ARGs + the harness DefaultVersion consts the CLI surface is verified against (claude 2.1.263 / codex 0.142.2),
# so a future CLI flag change is a visible lane break to fix, not a silent harness regression.
run: |
# One retry each to absorb a transient npm-registry blip (ECONNRESET / 5xx) without false-red-ing the lane; a
# genuinely missing/yanked binary still fails after the retry → reds (the install is fatal, not flaky).
- npm install -g '@anthropic-ai/claude-code@2.1.193' || { sleep 5; npm install -g '@anthropic-ai/claude-code@2.1.193'; }
+ npm install -g '@anthropic-ai/claude-code@2.1.263' || { sleep 5; npm install -g '@anthropic-ai/claude-code@2.1.263'; }
npm install -g '@openai/codex@0.142.2' || { sleep 5; npm install -g '@openai/codex@0.142.2'; }
claude --version
codex --version
@@ -1206,7 +1206,7 @@ jobs:
- name: Install the real coding-agent CLI (Claude Code)
# FATAL install (a missing binary must RED the lane, never let the gating arm self-skip green — "skip ≠ pass").
run: |
- npm install -g '@anthropic-ai/claude-code@2.1.193' || { sleep 5; npm install -g '@anthropic-ai/claude-code@2.1.193'; }
+ npm install -g '@anthropic-ai/claude-code@2.1.263' || { sleep 5; npm install -g '@anthropic-ai/claude-code@2.1.263'; }
claude --version
git --version
@@ -1333,7 +1333,7 @@ jobs:
- name: Install the real coding-agent CLI (Claude Code)
# FATAL install (a missing binary must RED the lane, never let the gating agent-feed arm self-skip green — "skip ≠ pass").
run: |
- npm install -g '@anthropic-ai/claude-code@2.1.193' || { sleep 5; npm install -g '@anthropic-ai/claude-code@2.1.193'; }
+ npm install -g '@anthropic-ai/claude-code@2.1.263' || { sleep 5; npm install -g '@anthropic-ai/claude-code@2.1.263'; }
claude --version
git --version
@@ -1460,7 +1460,7 @@ jobs:
# drives the hooks.json Stop hook. FATAL install (a missing binary must RED the lane, never let it self-skip
# green — "skip ≠ pass"). Pins match the injection lane's own pinned versions.
run: |
- npm install -g '@anthropic-ai/claude-code@2.1.193' || { sleep 5; npm install -g '@anthropic-ai/claude-code@2.1.193'; }
+ npm install -g '@anthropic-ai/claude-code@2.1.263' || { sleep 5; npm install -g '@anthropic-ai/claude-code@2.1.263'; }
npm install -g '@openai/codex@0.142.2' || { sleep 5; npm install -g '@openai/codex@0.142.2'; }
claude --version
codex --version
diff --git a/backend/Dockerfile.worker b/backend/Dockerfile.worker
index 74616eee8..b4667e53a 100644
--- a/backend/Dockerfile.worker
+++ b/backend/Dockerfile.worker
@@ -64,9 +64,14 @@ RUN SHA="${SOURCE_REVISION_ID:-$(git rev-parse HEAD 2>/dev/null || echo unknown)
# strings, so the harness-reported version can NEVER silently drift from what the worker actually installs — bump
# here and the test fails until the C# constants follow. `deploy/sync-local-harnesses.sh` installs these same pins
# locally, so a dev box matches the worker. Keep all three (this file · the harness consts · a local install) in lockstep.
-FROM node:20-bookworm-slim AS agent-cli
+#
+# The Node major must satisfy the STRICTEST `engines.node` of the two pinned CLIs: claude-code 2.1.263 declares
+# >=22 (2.1.193 declared >=18), codex 0.142.2 declares >=16. On node:20 npm still installs claude-code, but only
+# because EBADENGINE is a WARNING — an `engine-strict` npm, or a future release that actually uses node-22 syntax
+# in its install script, turns that warning into a failed image build. Track the floor rather than the warning.
+FROM node:22-bookworm-slim AS agent-cli
ARG CODEX_CLI_VERSION=0.142.2
-ARG CLAUDE_CODE_VERSION=2.1.193
+ARG CLAUDE_CODE_VERSION=2.1.263
RUN npm install -g "@openai/codex@${CODEX_CLI_VERSION}" "@anthropic-ai/claude-code@${CLAUDE_CODE_VERSION}"
# ── 3. Runtime: agent-execution + isolation deps + the Node runtime & CLIs + the published app ──
diff --git a/backend/src/CodeSpace.Core/Services/Agents/Harnesses/Claude/ClaudeCodeHarness.cs b/backend/src/CodeSpace.Core/Services/Agents/Harnesses/Claude/ClaudeCodeHarness.cs
index fcadb4350..b79a8b47a 100644
--- a/backend/src/CodeSpace.Core/Services/Agents/Harnesses/Claude/ClaudeCodeHarness.cs
+++ b/backend/src/CodeSpace.Core/Services/Agents/Harnesses/Claude/ClaudeCodeHarness.cs
@@ -105,7 +105,7 @@ public sealed class ClaudeCodeHarness : IAgentHarness, IAgentHarnessContractGene
private const string AnthropicProvider = "Anthropic";
/// The pinned Claude Code CLI version — MUST match CLAUDE_CODE_VERSION in backend/Dockerfile.worker (the single source of truth); a pin test fails if they drift.
- internal const string DefaultVersion = "2.1.193";
+ internal const string DefaultVersion = "2.1.263";
private const string DefaultCommand = "claude";
diff --git a/backend/src/CodeSpace.Core/Services/Agents/Harnesses/Claude/ClaudeTranscriptPath.cs b/backend/src/CodeSpace.Core/Services/Agents/Harnesses/Claude/ClaudeTranscriptPath.cs
index bac9f9e9b..2ed7051ab 100644
--- a/backend/src/CodeSpace.Core/Services/Agents/Harnesses/Claude/ClaudeTranscriptPath.cs
+++ b/backend/src/CodeSpace.Core/Services/Agents/Harnesses/Claude/ClaudeTranscriptPath.cs
@@ -6,14 +6,17 @@ namespace CodeSpace.Core.Services.Agents.Harnesses.Claude;
/// Reproduces Claude Code's transcript-file location so a CONTINUE can RESTORE a prior session's JSONL where the CLI
/// looks for it on --resume. Claude stores a session at
/// <CLAUDE_CONFIG_DIR>/projects/<sanitized-cwd>/<session-id>.jsonl. The sanitizer is a BYTE-FOR-BYTE
-/// port of the real claude 2.1.193 encoder (extracted from the binary): replace every char outside [A-Za-z0-9]
+/// port of the real claude 2.1.263 encoder (extracted from the binary): replace every char outside [A-Za-z0-9]
/// with -, and when the result exceeds 200 chars truncate to 200 and append -<base36 hash> of the
/// ORIGINAL cwd (so deep paths still map to a stable, collision-resistant dir). Pinned by ClaudeTranscriptPathTests
-/// against ground-truth pairs produced by the real algorithm.
+/// against ground-truth pairs produced by the real algorithm. Unchanged since the 2.1.193 pin this port was first taken
+/// from — only the minifier's symbol names moved (ab/Byu/hRe/pXe became RA/Te/gz/az), and
+/// defaultPath() still builds the segment as RA(cwd) under projects/.
///
-/// function ab(e){let t=e.replace(/[^a-zA-Z0-9]/g,"-");if(t.length<=200)return t;return `${t.slice(0,200)}-${Byu(e)}`}
-/// function Byu(e){return Math.abs(hRe(e)).toString(36)}
-/// function hRe(e){let t=0;for(let n=0;n<e.length;n++)t=(t<<5)-t+e.charCodeAt(n)|0;return t}
+/// function RA(e){let n=k(e);if(n.length<=az)return n;return `${n.slice(0,az)}-${Te(e)}`} // az=200
+/// function k(e){return e.replace(/[^a-zA-Z0-9]/g,"-")}
+/// function Te(e){return Math.abs(gz(e)).toString(36)}
+/// function gz(t){let e=0;for(let r=0;r<t.length;r++)e=(e<<5)-e+t.charCodeAt(r)|0;return e}
///
/// The sharpest P3 hazard: the cwd MUST be the RESOLVED real path the agent process runs in — on macOS
/// /var/… resolves to /private/var/…, and under bubblewrap it is the --chdir host path. Encoding the
diff --git a/backend/tests/CodeSpace.UnitTests/Agents/AgentNativeRecordPumpTests.cs b/backend/tests/CodeSpace.UnitTests/Agents/AgentNativeRecordPumpTests.cs
index 57d555887..8fd34163a 100644
--- a/backend/tests/CodeSpace.UnitTests/Agents/AgentNativeRecordPumpTests.cs
+++ b/backend/tests/CodeSpace.UnitTests/Agents/AgentNativeRecordPumpTests.cs
@@ -247,7 +247,7 @@ public async Task A_plane_that_refuses_to_open_or_to_write_leaves_the_parse_path
[Theory]
[InlineData("codex-cli", "0.142.2", 1, "codex-cli/v1")]
[InlineData("codex-cli", "0.142.2", null, "codex-cli/v1")]
- [InlineData("claude-code", "2.1.193", 2, "claude-code/v2")]
+ [InlineData("claude-code", "2.1.263", 2, "claude-code/v2")]
[InlineData("claude-code", "3.0.0", 2, "claude-code/v2")]
[InlineData("scripted", "9.9.9", 2, "scripted/v2")]
[InlineData("scripted", "2.0.0", null, "scripted/v1")]
diff --git a/backend/tests/CodeSpace.UnitTests/Workflows/ClaudeTranscriptPathTests.cs b/backend/tests/CodeSpace.UnitTests/Workflows/ClaudeTranscriptPathTests.cs
index a2ab00bec..cfd50ad14 100644
--- a/backend/tests/CodeSpace.UnitTests/Workflows/ClaudeTranscriptPathTests.cs
+++ b/backend/tests/CodeSpace.UnitTests/Workflows/ClaudeTranscriptPathTests.cs
@@ -6,7 +6,7 @@ namespace CodeSpace.UnitTests.Workflows;
///
/// 🟢 Unit: reproduces Claude Code's transcript-file location so a CONTINUE can
/// RESTORE a prior session's JSONL where the CLI looks for it on --resume. The known-pairs are the LOAD-BEARING
-/// pin: they are REAL ~/.claude/projects directory names observed on a machine running claude 2.1.193, so a
+/// pin: they are REAL ~/.claude/projects directory names observed on a machine running claude 2.1.263, so a
/// drift in the cwd→sanitized-dir encoding fails HERE at test time rather than as a silent failed real-CLI resume
/// (the sharpest P3 hazard — a mismatch lands the transcript under the wrong dir and --resume cold-starts with
/// no error).
@@ -15,14 +15,17 @@ namespace CodeSpace.UnitTests.Workflows;
public class ClaudeTranscriptPathTests
{
[Theory]
- // Ground truth — a byte-exact port of the real claude 2.1.193 `ab()`: replace every char outside [A-Za-z0-9] with
+ // Ground truth — a byte-exact port of the real claude 2.1.263 `RA()`: replace every char outside [A-Za-z0-9] with
// '-' (so '/', '.', AND '_' all become '-'); alphanumerics + existing '-' survive (they map to themselves).
[InlineData("/Users/mars/Projects/CodeSpace", "-Users-mars-Projects-CodeSpace")] // real ~/.claude/projects dir
[InlineData("/Users/mars/Projects/CodeSpace/backend/src/CodeSpace.Core", "-Users-mars-Projects-CodeSpace-backend-src-CodeSpace-Core")] // real dir; the '.' in CodeSpace.Core → '-'
[InlineData("/private/var/folders/z7/qrtkqj255vs6dg3wjfkgcn380000gn/T/codespace-agent-workspaces/05e4e233e0c5482985cbddd01d1a72a4",
"-private-var-folders-z7-qrtkqj255vs6dg3wjfkgcn380000gn-T-codespace-agent-workspaces-05e4e233e0c5482985cbddd01d1a72a4")] // resolved agent-workspace cwd (/private, not /var)
- [InlineData("/Users/john_doe/my_project", "-Users-john-doe-my-project")] // UNDERSCORE → '-' (the real binary does NOT preserve '_') — ground truth via the extracted ab()
+ [InlineData("/Users/john_doe/my_project", "-Users-john-doe-my-project")] // UNDERSCORE → '-' (the real binary does NOT preserve '_') — ground truth via the extracted RA()
[InlineData("/Users/a_b/x.y/z", "-Users-a-b-x-y-z")] // mixed '_' + '.' → '-'
+ // Observed live on 2.1.263: the real CLI, run from this cwd, created exactly this dir under its projects/ home.
+ // Covers the classes the pairs above miss — a SPACE and NON-ASCII (each UTF-16 unit → its own '-', so '项目' → '--').
+ [InlineData("/private/tmp/cs enc/my_project.v2/项目-x", "-private-tmp-cs-enc-my-project-v2----x")]
public void EncodeCwd_matches_the_real_claude_encoder(string cwd, string expected) =>
ClaudeTranscriptPath.EncodeCwd(cwd).ShouldBe(expected);
diff --git a/backend/tests/CodeSpace.UnitTests/Workflows/HarnessVersionPinTests.cs b/backend/tests/CodeSpace.UnitTests/Workflows/HarnessVersionPinTests.cs
index 1c98be67e..710ded2dd 100644
--- a/backend/tests/CodeSpace.UnitTests/Workflows/HarnessVersionPinTests.cs
+++ b/backend/tests/CodeSpace.UnitTests/Workflows/HarnessVersionPinTests.cs
@@ -1,5 +1,6 @@
using System;
using System.IO;
+using System.Linq;
using System.Text.RegularExpressions;
using CodeSpace.Core.Services.Agents.Harnesses.Claude;
using CodeSpace.Core.Services.Agents.Harnesses.Codex;
@@ -12,7 +13,9 @@ namespace CodeSpace.UnitTests.Workflows;
/// CODEX_CLI_VERSION / CLAUDE_CODE_VERSION ARG in backend/Dockerfile.worker (the version the
/// worker image actually installs). A bump in the Dockerfile that isn't mirrored into the C# constant (or vice
/// versa) FAILS here, so the harness-reported version can never silently drift from what the worker runs. The third
-/// surface — a developer's local install — is synced from the same ARG by deploy/sync-local-harnesses.sh.
+/// surface — a developer's local install — is synced from the same ARG by deploy/sync-local-harnesses.sh. The
+/// fourth is .github/workflows/real-model.yml's EXACT-pinned installs, the lanes that verify the CLI surface
+/// the harness argv targets; they must name the shipped version or the gate certifies a CLI nobody runs.
///
[Trait("Category", "Unit")]
public class HarnessVersionPinTests
@@ -25,6 +28,25 @@ public void Codex_default_version_matches_the_worker_dockerfile_pin() =>
public void Claude_default_version_matches_the_worker_dockerfile_pin() =>
DockerfileArg("CLAUDE_CODE_VERSION").ShouldBe(ClaudeCodeHarness.DefaultVersion);
+ ///
+ /// The FOURTH surface: real-model.yml's exact-pinned installs — the lanes whose whole job is to verify the
+ /// CLI surface the harness argv targets. An exact pin there that lags the worker's ARG means the gate certifies a
+ /// version the product does not ship, which is precisely the silent drift the other pins exist to prevent. Only
+ /// EXACT pins are asserted; the deliberately FLOATING @~2.1.0 lanes are excluded by the version pattern, so
+ /// they keep tracking the newest 2.1.x without failing here.
+ ///
+ [Fact]
+ public void Claude_exact_workflow_pins_match_the_worker_dockerfile_pin()
+ {
+ var pinned = DockerfileArg("CLAUDE_CODE_VERSION");
+ var matches = Regex.Matches(File.ReadAllText(LocateRealModelWorkflow()), @"@anthropic-ai/claude-code@(\d+\.\d+\.\d+)");
+
+ matches.Count.ShouldBeGreaterThan(0, "real-model.yml must keep at least one exact-pinned claude-code install — the lane that verifies the CLI surface");
+
+ foreach (var version in matches.Select(m => m.Groups[1].Value).Distinct())
+ version.ShouldBe(pinned, $"an exact '@anthropic-ai/claude-code@{version}' install in .github/workflows/real-model.yml lags CLAUDE_CODE_VERSION in backend/Dockerfile.worker — the lane would certify a CLI the worker image never installs");
+ }
+
private static string DockerfileArg(string name)
{
var content = File.ReadAllText(LocateWorkerDockerfile());
@@ -34,14 +56,20 @@ private static string DockerfileArg(string name)
return match.Groups[1].Value;
}
- private static string LocateWorkerDockerfile()
+ private static string LocateWorkerDockerfile() => LocateRepoFile("backend", "Dockerfile.worker");
+
+ private static string LocateRealModelWorkflow() => LocateRepoFile(".github", "workflows", "real-model.yml");
+
+ private static string LocateRepoFile(params string[] segments)
{
+ var relative = Path.Combine(segments);
+
for (var dir = new DirectoryInfo(AppContext.BaseDirectory); dir is not null; dir = dir.Parent)
{
- var candidate = Path.Combine(dir.FullName, "backend", "Dockerfile.worker");
+ var candidate = Path.Combine(dir.FullName, relative);
if (File.Exists(candidate)) return candidate;
}
- throw new FileNotFoundException("backend/Dockerfile.worker not found walking up from " + AppContext.BaseDirectory);
+ throw new FileNotFoundException($"{relative} not found walking up from {AppContext.BaseDirectory}");
}
}