@mrciphersmith/keryx 0.3.40 → 0.3.42

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/README.md CHANGED
@@ -95,6 +95,13 @@ keryx auth login <provider> # subscription login (device code / OAuth) or API k
95
95
  Inside a running session, `/provider` reopens the same add/reconfigure wizard
96
96
  and `/connect` switches between providers you already configured.
97
97
 
98
+ ChatGPT / Codex model discovery reads the current stable Codex version from npm
99
+ metadata and caches it for one day. **Test** in `/connect` (or
100
+ `keryx providers test openai-codex`) always checks for a newer version before
101
+ listing models. This does not install Codex or poll npm in the background.
102
+ If npm is unavailable, a previously discovered version is reused; without a
103
+ cached version, discovery reports the problem instead of guessing a version.
104
+
98
105
  Once you have more than one provider connected, `keryx routing` (and, inside a
99
106
  session, `/routing`) maps a task category — `review`, `subagents`, and a
100
107
  catalogue of others — to a specific model, so reviews and subagent spawns can
@@ -480,7 +487,7 @@ What is in it today:
480
487
  bounded parallel scheduling, and an offline fleet report over a recorded event
481
488
  log (`keryx agents monitor <events-file>`).
482
489
  - **Vendor CLIs as child agents — off by default.** keryx can hand a bounded,
483
- **read-only** task (or, for `claude-cli` only, a reviewed write — see below)
490
+ **read-only** task (or, for `claude-cli` and `codex-cli`, a reviewed write — see below)
484
491
  to a coding CLI you already have installed (`codex exec`,
485
492
  `claude -p`, or Google's Antigravity CLI, `agy -p --output-format
486
493
  stream-json`) and host it as a child of the same harness: a disposable git
@@ -502,16 +509,20 @@ What is in it today:
502
509
  - **A write from an external agent you review before it lands.**
503
510
  `keryx agents external run claude-cli --task "…" --write` runs `claude` in a
504
511
  throwaway worktree with only `Read Grep Glob Edit Write` (no shell, no network,
505
- no MCP). Its diff is captured, secret-redacted, hashed and stored as a pending
512
+ no MCP); `codex-cli --write` runs `codex` in the same kind of worktree under an
513
+ OS sandbox (as measured: writes outside the worktree fail, a DNS lookup fails, `.git` read-only)
514
+ that still leaves it a shell able to read files your account can read, so review
515
+ its output too. Its diff is captured, secret-redacted, hashed and stored as a pending
506
516
  review — nothing reaches your checkout. `keryx agents external review <run-id>`
507
517
  shows it; `apply <run-id>` needs a real terminal, shows the diff and asks you to
508
518
  type the first 12 hex digits of the patch hash, then creates a NEW local branch
509
519
  `external/<run-id>` with one commit. Your current branch and working tree are
510
520
  never touched, nothing is pushed and no pull request is opened;
511
521
  `discard <run-id>` drops it. There is no flag that skips the confirmation. Only
512
- `claude-cli` can write (`codex-cli` and `antigravity-cli` refuse `--write`), the
513
- review is a human reading the diff (a model review is not built), and a full run
514
- through the installed CLI is not recorded yet. In the TUI: `/external-diff` and a
522
+ `claude-cli` and `codex-cli` can write (`antigravity-cli` refuses `--write`), the
523
+ review is a human reading the diff (a model review is not built), and neither
524
+ has a long live history (`codex` was verified in a scratch repository on
525
+ `codex` 0.159.2; versions outside 0.159.2 up to 0.160.0 are refused). In the TUI: `/external-diff` and a
515
526
  sidebar row while a diff waits. See the [external agent write
516
527
  guide](docs/docs/guides/external-agent-write.md).
517
528
  - **Completion you can audit.** The completion gate blocks on missing evidence: a
@@ -941,7 +952,7 @@ untouched.
941
952
  | No bundled embedding runtime | No semantic ranking in memory search | Lexical memory search remains fully available |
942
953
  | ripgrep is external | `keryx ctx rg` needs `rg` on `PATH` | Install ripgrep, or let the agent read files directly |
943
954
  | Model commands need a credential | Four of the five commands above exit non-zero without one; `wiki enrich` exits `0` and marks the affected pages skipped | Everything else runs deterministically offline |
944
- | External agents are read-only, except `claude-cli` write mode, which has no full live run recorded | A delegated CLI can read and search; only `claude-cli` can write, and only as a stored diff a human reviews (`keryx agents external review\|apply\|discard`) that lands as a new local branch — never on your checkout, never pushed. `codex-cli` and `antigravity-cli` refuse `--write` ("write mode is claude-only in this release"). A model review is not built. The parent gets the child's result and nothing before it — supervision of a running external child is not implemented. `claude` and `agy` read-only runs are live-verified; a write run through the installed CLI is not yet | Use keryx's own child agents for work that must mutate the tree, or the [external agent write guide](docs/docs/guides/external-agent-write.md) for a reviewed `claude-cli` diff |
955
+ | External agents are read-only, except `claude-cli` and `codex-cli` write mode, which have no long live history | A delegated CLI can read and search; only `claude-cli` and `codex-cli` can write, and only as a stored diff a human reviews (`keryx agents external review\|apply\|discard`) that lands as a new local branch — never on your checkout, never pushed. `claude` is confined by a tool allow-list (no shell, no network); `codex` by an OS sandbox that still leaves it a shell that can read files your user can read (it cannot send them, but they can show up in the run output), and is refused when older than 0.159.0 — live-verified in a scratch repository on `codex` 0.159.0. `antigravity-cli` refuses `--write`. A model review is not built. The parent gets the child's result and nothing before it — supervision of a running external child is not implemented. `claude` and `agy` read-only runs are live-verified | Use keryx's own child agents for work that must mutate the tree, or the [external agent write guide](docs/docs/guides/external-agent-write.md) for a reviewed `claude-cli` or `codex-cli` diff |
945
956
  | A dispatched `flow-next` task is "done" by checks, not review | `task done` means normal end + a commit on the trigger branch + `keryx health gate` passing in the worktree; nobody has read the diff | Review and merge the `trigger/<flow>-<task>` branch yourself |
946
957
  | `trust` dispatch needs Linux + a working bubblewrap | The hardened unattended sandbox (network off, home hidden, allow-listed env) is bwrap-only; without it — or on macOS — a `trust` dispatch refuses before starting | Use `permissionMode: "ask"` (read-only), or run triggers on a Linux host with bwrap |
947
958
  | `dispatch.network: true` is the host's full network | The agent's commands then reach the internet and every host loopback service; the model call never needs it (it is made outside the sandbox) | Leave `network` off; review the `trigger/*` branch before installing or building it |
package/dist/cli.js CHANGED
@@ -14119,7 +14119,7 @@ var package_default;
14119
14119
  var init_package = __esm(() => {
14120
14120
  package_default = {
14121
14121
  name: "@mrciphersmith/keryx",
14122
- version: "0.3.40",
14122
+ version: "0.3.42",
14123
14123
  description: "Version-controlled project context for AI coding agents: code graph, architecture wiki, project memory, relevant tests, quality signals, and task flows.",
14124
14124
  private: false,
14125
14125
  publishConfig: {
@@ -24043,6 +24043,90 @@ var init_openai_subscription = __esm(() => {
24043
24043
  };
24044
24044
  });
24045
24045
 
24046
+ // src/lib/oauth/codex-catalog-version.ts
24047
+ import { join as join2 } from "path";
24048
+ function validVersion(value) {
24049
+ return typeof value === "string" && value.length <= 64 && /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/.test(value);
24050
+ }
24051
+ function loadCache(file, now) {
24052
+ try {
24053
+ const content = readConfigFile(file);
24054
+ if (!content.ok)
24055
+ return;
24056
+ const cache = JSON.parse(content.text);
24057
+ if (typeof cache !== "object" || cache === null)
24058
+ return;
24059
+ const record = cache;
24060
+ if (!validVersion(record.version) || typeof record.fetchedAt !== "number" || !Number.isFinite(record.fetchedAt) || record.fetchedAt < 0 || record.fetchedAt > now)
24061
+ return;
24062
+ return { version: record.version, fetchedAt: record.fetchedAt };
24063
+ } catch {
24064
+ return;
24065
+ }
24066
+ }
24067
+ async function readMetadata(response) {
24068
+ const declared = response.headers.get("content-length");
24069
+ if (declared !== null && Number(declared) > RESPONSE_LIMIT_BYTES)
24070
+ return;
24071
+ if (response.body === null)
24072
+ return;
24073
+ const reader = response.body.getReader();
24074
+ const chunks = [];
24075
+ let size = 0;
24076
+ try {
24077
+ while (true) {
24078
+ const chunk = await reader.read();
24079
+ if (chunk.done)
24080
+ break;
24081
+ size += chunk.value.byteLength;
24082
+ if (size > RESPONSE_LIMIT_BYTES) {
24083
+ await reader.cancel().catch(() => {});
24084
+ return;
24085
+ }
24086
+ chunks.push(chunk.value);
24087
+ }
24088
+ return JSON.parse(Buffer.concat(chunks).toString("utf8"));
24089
+ } finally {
24090
+ reader.releaseLock();
24091
+ }
24092
+ }
24093
+ async function resolveCodexCatalogVersion(fetchFn, opts = {}) {
24094
+ if (opts.signal?.aborted)
24095
+ return;
24096
+ const now = (opts.now ?? Date.now)();
24097
+ const file = join2(keryxConfigDir(opts.configDir), CACHE_FILE);
24098
+ const cached = loadCache(file, now);
24099
+ if (!opts.forceRefresh && cached !== undefined && now - cached.fetchedAt < CODEX_VERSION_CACHE_TTL_MS)
24100
+ return cached.version;
24101
+ try {
24102
+ const timeout = AbortSignal.timeout(2000);
24103
+ const response = await fetchFn(CODEX_VERSION_REGISTRY_URL, {
24104
+ headers: { Accept: "application/json" },
24105
+ credentials: "omit",
24106
+ redirect: "error",
24107
+ signal: opts.signal === undefined ? timeout : AbortSignal.any([opts.signal, timeout])
24108
+ });
24109
+ if (response.ok) {
24110
+ const body = await readMetadata(response);
24111
+ if (typeof body === "object" && body !== null && "name" in body && body.name === "@openai/codex" && "version" in body && validVersion(body.version) && !opts.signal?.aborted) {
24112
+ const version = body.version;
24113
+ try {
24114
+ ensureKeryxConfigDir(opts.configDir);
24115
+ writeOwnerOnlyFileAtomic(file, JSON.stringify({ version, fetchedAt: now }));
24116
+ } catch {}
24117
+ return version;
24118
+ }
24119
+ }
24120
+ } catch {}
24121
+ return opts.signal?.aborted ? undefined : cached?.version;
24122
+ }
24123
+ var CODEX_VERSION_REGISTRY_URL = "https://registry.npmjs.org/@openai%2Fcodex/latest", CODEX_VERSION_CACHE_TTL_MS, CACHE_FILE = "codex-catalog-version.json", RESPONSE_LIMIT_BYTES;
24124
+ var init_codex_catalog_version = __esm(() => {
24125
+ init_config_dir();
24126
+ CODEX_VERSION_CACHE_TTL_MS = 24 * 60 * 60 * 1000;
24127
+ RESPONSE_LIMIT_BYTES = 64 * 1024;
24128
+ });
24129
+
24046
24130
  // src/commands/subscription-models.ts
24047
24131
  async function fetchOpenAiCodexModels(fetchFn, opts = {}) {
24048
24132
  const controller = new AbortController;
@@ -24051,7 +24135,17 @@ async function fetchOpenAiCodexModels(fetchFn, opts = {}) {
24051
24135
  try {
24052
24136
  const grant = await ensureOpenAiCodexGrant({ fetch: fetchFn, signal: controller.signal, ...opts.configDir !== undefined ? { configDir: opts.configDir } : {} });
24053
24137
  authenticated = true;
24054
- const response = await fetchFn(`${OPENAI_CODEX_PICKER.baseUrl}/models?client_version=${CODEX_CATALOG_CLIENT_VERSION}`, {
24138
+ const clientVersion = await resolveCodexCatalogVersion(fetchFn, {
24139
+ signal: controller.signal,
24140
+ ...opts.refreshCatalogVersion !== undefined ? { forceRefresh: opts.refreshCatalogVersion } : {},
24141
+ ...opts.configDir !== undefined ? { configDir: opts.configDir } : {}
24142
+ });
24143
+ if (clientVersion === undefined)
24144
+ return { models: [], source: "fallback", failure: {
24145
+ kind: "unreachable",
24146
+ detail: "could not discover the Codex catalog version from npm; no cached version is available"
24147
+ } };
24148
+ const response = await fetchFn(`${OPENAI_CODEX_PICKER.baseUrl}/models?client_version=${clientVersion}`, {
24055
24149
  headers: { Authorization: `Bearer ${grant.access}`, "ChatGPT-Account-ID": grant.accountId, originator: "keryx", Accept: "application/json" },
24056
24150
  signal: controller.signal,
24057
24151
  redirect: "error"
@@ -24075,9 +24169,10 @@ async function fetchOpenAiCodexModels(fetchFn, opts = {}) {
24075
24169
  clearTimeout(timer);
24076
24170
  }
24077
24171
  }
24078
- var OPENAI_CODEX_PICKER, CODEX_CATALOG_CLIENT_VERSION = "0.157.0";
24172
+ var OPENAI_CODEX_PICKER;
24079
24173
  var init_subscription_models = __esm(() => {
24080
24174
  init_openai_subscription();
24175
+ init_codex_catalog_version();
24081
24176
  OPENAI_CODEX_PICKER = {
24082
24177
  name: "openai-codex",
24083
24178
  label: "ChatGPT / Codex",
@@ -26678,7 +26773,10 @@ function providerApiKey(provider, env = process.env) {
26678
26773
  }
26679
26774
  async function testProviderConnection(provider, fetchFn = globalThis.fetch, env = process.env, profiles) {
26680
26775
  if (provider.name === "openai-codex")
26681
- return fetchOpenAiCodexModels(fetchFn, { ...profiles?.dir !== undefined ? { configDir: profiles.dir } : {} });
26776
+ return fetchOpenAiCodexModels(fetchFn, {
26777
+ refreshCatalogVersion: true,
26778
+ ...profiles?.dir !== undefined ? { configDir: profiles.dir } : {}
26779
+ });
26682
26780
  const apiKey = providerApiKey(provider, envWithSavedApiKeys(env, profiles?.dir)) ?? provider.apiKey;
26683
26781
  return fetchOpenAiCompatModelsDetailed(fetchFn, provider, apiKey, {
26684
26782
  timeoutMs: MODELS_FETCH_TIMEOUT_MS,
@@ -31306,7 +31404,7 @@ var init_wiki_freshness = () => {};
31306
31404
 
31307
31405
  // src/harness/tool/builtin/interactive-tools.ts
31308
31406
  import { readdir as readdir9 } from "fs/promises";
31309
- import { basename, dirname, isAbsolute, join as join2, relative, resolve as resolve2, sep } from "path";
31407
+ import { basename, dirname, isAbsolute, join as join3, relative, resolve as resolve2, sep } from "path";
31310
31408
  import { lstatSync as lstatSync5, realpathSync as realpathSync3 } from "fs";
31311
31409
  async function readFromLine(file, startLine, cap) {
31312
31410
  const reader = file.stream().getReader();
@@ -31391,7 +31489,7 @@ function realpathThroughAncestors(absolute) {
31391
31489
  for (;; ) {
31392
31490
  try {
31393
31491
  const real = realpathSync3(current);
31394
- return missing.length === 0 ? real : join2(real, ...missing.reverse());
31492
+ return missing.length === 0 ? real : join3(real, ...missing.reverse());
31395
31493
  } catch {
31396
31494
  try {
31397
31495
  lstatSync5(current);
@@ -51270,13 +51368,15 @@ permission bridge.
51270
51368
  A line-stream agent (\`claude-cli\`, \`codex-cli\`) is a DIFFERENT thing:
51271
51369
  \`run\` refuses it outright ("speaks the one-way line stream. Delegate to it
51272
51370
  from \`keryx shell\` with /delegate"). There is no ACP permission bridge and
51273
- no patch capture for it \u2014 this release implements \`worktree-write\` for the
51274
- ACP transport only; a dispatch that asks for \`worktree-write\` on a
51275
- line-stream agent is REFUSED fail-closed with a distinguishable
51276
- \`not-implemented\` reason everywhere keryx dispatches one (the internal
51277
- \`runtime\` block, and \`keryx shell\`'s /delegate) \u2014 never silently accepted
51278
- and its writes discarded. It stays \`read-only\`, contained by the same
51279
- disposable worktree as every external run.
51371
+ no patch capture for it in those paths \u2014 \`worktree-write\` is implemented for
51372
+ the ACP transport, and for \`claude-cli\` and \`codex-cli\` only through the
51373
+ operator's own \`keryx agents external run <id> --write\`, which captures the
51374
+ diff for human review. A dispatch that asks for \`worktree-write\` on a
51375
+ line-stream agent anywhere else (the internal \`runtime\` block, and
51376
+ \`keryx shell\`'s /delegate) is REFUSED fail-closed with a distinguishable
51377
+ \`not-implemented\` reason \u2014 never silently accepted and its writes
51378
+ discarded. There it stays \`read-only\`, contained by the same disposable
51379
+ worktree as every external run.
51280
51380
 
51281
51381
  See \`docs/docs/guides/acp-client.md\` for the full ACP contract, including the
51282
51382
  output size bounds (a stderr-only flood and an output flood both end the run
@@ -52592,7 +52692,7 @@ var init_github = __esm(() => {
52592
52692
 
52593
52693
  // src/harness/tool/metaproject-adapter.ts
52594
52694
  import { lstat as lstat8, readdir as readdir21, readFile as readFile60, realpath as realpath11, stat as stat15 } from "fs/promises";
52595
- import { isAbsolute as isAbsolute2, join as join3, relative as relative2, resolve as resolve3 } from "path";
52695
+ import { isAbsolute as isAbsolute2, join as join4, relative as relative2, resolve as resolve3 } from "path";
52596
52696
  function isMissingExecutable(cause) {
52597
52697
  const message = cause instanceof Error ? cause.message : String(cause);
52598
52698
  return /Executable not found|\bENOENT\b|not found in \$?PATH/i.test(message);
@@ -52633,7 +52733,7 @@ function portableMemoryPath(candidate) {
52633
52733
  return !isAbsolute2(normalized) && normalized !== ".." && !normalized.startsWith("../") && !normalized.includes("/../") ? normalized : null;
52634
52734
  }
52635
52735
  function confineToWiki(cwd, candidate) {
52636
- const wikiRoot = join3(cwd, ".metaproject", "wiki");
52736
+ const wikiRoot = join4(cwd, ".metaproject", "wiki");
52637
52737
  const target = resolve3(wikiRoot, candidate);
52638
52738
  const rel = relative2(wikiRoot, target);
52639
52739
  if (rel === "") {
@@ -52676,7 +52776,7 @@ async function projectRemovalTrail(cwd, query) {
52676
52776
  async function detectMemoryStoreUnreadable(cwd) {
52677
52777
  const root = memoryRoot(cwd);
52678
52778
  for (const { folder } of MEMORY_TYPES) {
52679
- const dir = join3(root, folder);
52779
+ const dir = join4(root, folder);
52680
52780
  try {
52681
52781
  await readdir21(dir);
52682
52782
  } catch (error) {
@@ -52690,7 +52790,7 @@ async function detectMemoryStoreUnreadable(cwd) {
52690
52790
  return null;
52691
52791
  }
52692
52792
  function confineToSkills(cwd, candidate) {
52693
- const skillsRoot = join3(cwd, ".metaproject", "skills", "gdskills");
52793
+ const skillsRoot = join4(cwd, ".metaproject", "skills", "gdskills");
52694
52794
  const target = resolve3(cwd, candidate);
52695
52795
  const rel = relative2(skillsRoot, target);
52696
52796
  if (rel === "") {
@@ -52705,7 +52805,7 @@ async function parseCatalogSummaries(cwd) {
52705
52805
  const summaries = new Map;
52706
52806
  let content;
52707
52807
  try {
52708
- const bytes = await readContainedFile(join3(cwd, ".metaproject", "skills"), join3(cwd, ".metaproject", "skills", "catalog.md"), { maxBytes: 512 * 1024, requireRegularFile: true });
52808
+ const bytes = await readContainedFile(join4(cwd, ".metaproject", "skills"), join4(cwd, ".metaproject", "skills", "catalog.md"), { maxBytes: 512 * 1024, requireRegularFile: true });
52709
52809
  content = bytes.toString("utf8");
52710
52810
  } catch {
52711
52811
  return summaries;
@@ -52726,7 +52826,7 @@ async function parseCatalogSummaries(cwd) {
52726
52826
  return summaries;
52727
52827
  }
52728
52828
  async function walkSkillCatalog(cwd) {
52729
- const root = join3(cwd, ".metaproject", "skills", "gdskills");
52829
+ const root = join4(cwd, ".metaproject", "skills", "gdskills");
52730
52830
  const entries = [];
52731
52831
  const rootInfo = await lstat8(root).catch(() => null);
52732
52832
  if (!rootInfo || !rootInfo.isDirectory() || rootInfo.isSymbolicLink())
@@ -52744,7 +52844,7 @@ async function walkSkillCatalog(cwd) {
52744
52844
  for (const categoryDir of categoryDirs) {
52745
52845
  if (!categoryDir.isDirectory() && !categoryDir.isSymbolicLink())
52746
52846
  continue;
52747
- const categoryPath = join3(root, categoryDir.name);
52847
+ const categoryPath = join4(root, categoryDir.name);
52748
52848
  const categoryReal = await realpath11(categoryPath).catch(() => null);
52749
52849
  const categoryInfo = await stat15(categoryPath).catch(() => null);
52750
52850
  if (!categoryReal || !categoryInfo?.isDirectory() || !isPathInside(rootReal, categoryReal))
@@ -52756,12 +52856,12 @@ async function walkSkillCatalog(cwd) {
52756
52856
  continue;
52757
52857
  }
52758
52858
  for (const skillDir of skillDirs) {
52759
- const skillPath = join3(categoryPath, skillDir.name);
52859
+ const skillPath = join4(categoryPath, skillDir.name);
52760
52860
  const skillReal = await realpath11(skillPath).catch(() => null);
52761
52861
  const skillInfo = await stat15(skillPath).catch(() => null);
52762
52862
  if (!skillReal || !skillInfo?.isDirectory() || !isPathInside(rootReal, skillReal))
52763
52863
  continue;
52764
- const skillMdPath = join3(skillPath, "SKILL.md");
52864
+ const skillMdPath = join4(skillPath, "SKILL.md");
52765
52865
  let content;
52766
52866
  try {
52767
52867
  const bytes = await readContainedFile(root, skillMdPath, { maxBytes: 512 * 1024, requireRegularFile: true });
@@ -53027,7 +53127,7 @@ function createMetaprojectAdapter(cwd, overrides = {}) {
53027
53127
  exists = false;
53028
53128
  }
53029
53129
  if (!exists) {
53030
- const relativePath = toPosix(relative2(join3(cwd, ".metaproject", "wiki"), target));
53130
+ const relativePath = toPosix(relative2(join4(cwd, ".metaproject", "wiki"), target));
53031
53131
  const registry = await readSectionRegistryState(cwd);
53032
53132
  if (registry.state === "unreadable") {
53033
53133
  return {
@@ -53068,7 +53168,7 @@ function createMetaprojectAdapter(cwd, overrides = {}) {
53068
53168
  };
53069
53169
  }
53070
53170
  try {
53071
- const content = (await readContainedFile(join3(cwd, ".metaproject", "wiki"), target, {
53171
+ const content = (await readContainedFile(join4(cwd, ".metaproject", "wiki"), target, {
53072
53172
  maxBytes: 8 * 1024 * 1024,
53073
53173
  requireRegularFile: true
53074
53174
  })).toString("utf8");
@@ -53112,7 +53212,7 @@ function createMetaprojectAdapter(cwd, overrides = {}) {
53112
53212
  }
53113
53213
  let hasWikiIndex;
53114
53214
  try {
53115
- await readFile60(join3(cwd, ".metaproject", "wiki", "index.md"), "utf8");
53215
+ await readFile60(join4(cwd, ".metaproject", "wiki", "index.md"), "utf8");
53116
53216
  hasWikiIndex = true;
53117
53217
  } catch {
53118
53218
  hasWikiIndex = false;
@@ -53387,7 +53487,7 @@ function createMetaprojectAdapter(cwd, overrides = {}) {
53387
53487
  const byName = catalog.find((entry) => entry.name === input.name);
53388
53488
  if (byName !== undefined) {
53389
53489
  try {
53390
- const content = (await readContainedFile(join3(cwd, ".metaproject", "skills", "gdskills"), join3(cwd, byName.path), {
53490
+ const content = (await readContainedFile(join4(cwd, ".metaproject", "skills", "gdskills"), join4(cwd, byName.path), {
53391
53491
  maxBytes: 512 * 1024,
53392
53492
  requireRegularFile: true
53393
53493
  })).toString("utf8");
@@ -53400,12 +53500,12 @@ function createMetaprojectAdapter(cwd, overrides = {}) {
53400
53500
  if (confined === null) {
53401
53501
  return { name: input.name, path: "", content: "", found: false };
53402
53502
  }
53403
- const byPath = catalog.find((entry) => join3(cwd, entry.path) === confined);
53503
+ const byPath = catalog.find((entry) => join4(cwd, entry.path) === confined);
53404
53504
  if (byPath === undefined) {
53405
53505
  return { name: input.name, path: "", content: "", found: false };
53406
53506
  }
53407
53507
  try {
53408
- const content = (await readContainedFile(join3(cwd, ".metaproject", "skills", "gdskills"), confined, {
53508
+ const content = (await readContainedFile(join4(cwd, ".metaproject", "skills", "gdskills"), confined, {
53409
53509
  maxBytes: 512 * 1024,
53410
53510
  requireRegularFile: true
53411
53511
  })).toString("utf8");
@@ -85041,14 +85141,14 @@ var init_registry3 = __esm(() => {
85041
85141
  label: "Codex",
85042
85142
  binary: "codex",
85043
85143
  detect: ["--version"],
85044
- versionPattern: "codex-cli (\\d+\\.\\d+\\.\\d+)",
85144
+ versionPattern: "codex-cli (\\d+\\.\\d+\\.\\d+(?:-[0-9A-Za-z.]+)?)",
85045
85145
  knownGoodRange: { min: "0.147.0" },
85046
85146
  sandboxModes: ["read-only", "worktree-write"],
85047
85147
  streamingInput: false,
85048
85148
  resumable: true,
85049
85149
  reportsCost: false,
85050
85150
  budgetFlag: false,
85051
- notes: "Resume requires NOT passing `--ephemeral`: an ephemeral thread fails resume with " + "`no rollout found for thread id \u2026 (code -32600)`. `codex exec resume` also takes a " + "narrower flag set than `codex exec` \u2014 no `-s`, no `-C` \u2014 so its sandbox level cannot " + "be re-asserted and it must be spawned with cwd already set to the worktree."
85151
+ notes: "Resume requires NOT passing `--ephemeral`: an ephemeral thread fails resume with " + "`no rollout found for thread id \u2026 (code -32600)`. `codex exec resume` also takes a " + "narrower flag set than `codex exec` \u2014 no `-s`, no `-C` \u2014 so a read-only resume inherits " + "its sandbox, a write run re-asserts its confinement as `-c` overrides, and it must be " + "spawned with cwd already set to the worktree. Write mode needs codex 0.159.2 up to, but not including, 0.160.0."
85052
85152
  },
85053
85153
  {
85054
85154
  id: "claude-cli",
@@ -85098,10 +85198,27 @@ var init_registry3 = __esm(() => {
85098
85198
  });
85099
85199
 
85100
85200
  // src/harness/external/dispatch.ts
85201
+ function codecWriteVersionRefusal(entry, version) {
85202
+ const verified = CODEC_WRITE_VERIFIED_VERSIONS[entry.id];
85203
+ if (verified === undefined)
85204
+ return;
85205
+ const range = `\`${entry.binary}\` ${verified.min} up to, but not including, ${verified.before}`;
85206
+ const prefix = `${entry.label} write mode needs ${range}, the versions whose sandbox confinement was measured; `;
85207
+ if (version === undefined)
85208
+ return `${prefix}the installed version could not be read`;
85209
+ if (!/^\d+\.\d+\.\d+$/.test(version))
85210
+ return `${prefix}found ${version}, a pre-release`;
85211
+ if (compareVersions(version, verified.min) < 0)
85212
+ return `${prefix}found ${version}, which is older`;
85213
+ if (compareVersions(version, verified.before) >= 0) {
85214
+ return `${prefix}found ${version}, which is newer and not yet verified: update keryx, or use \`--task\` without \`--write\``;
85215
+ }
85216
+ return;
85217
+ }
85101
85218
  function implementedSandboxModesFor(entry, options = {}) {
85102
85219
  if (transportOf(entry) === "acp")
85103
85220
  return IMPLEMENTED_ACP_SANDBOX_MODES;
85104
- return entry.id === CODEC_WRITE_AGENT_ID && options.ownsWriteCapture === true ? IMPLEMENTED_ACP_SANDBOX_MODES : IMPLEMENTED_SANDBOX_MODES;
85221
+ return CODEC_WRITE_AGENT_IDS.includes(entry.id) && options.ownsWriteCapture === true ? IMPLEMENTED_ACP_SANDBOX_MODES : IMPLEMENTED_SANDBOX_MODES;
85105
85222
  }
85106
85223
  function validateRuntimeBlock(runtime, allowedActions, options = {}) {
85107
85224
  if (runtime === undefined || runtime.kind === "keryx") {
@@ -85143,7 +85260,7 @@ function validateRuntimeBlock(runtime, allowedActions, options = {}) {
85143
85260
  return {
85144
85261
  ok: false,
85145
85262
  code: "not-implemented",
85146
- reason: `sandbox "${sandbox}" is not implemented in this release; only ${implemented.join(", ")} is available` + (sandbox === "worktree-write" && transportOf(entry) !== "acp" ? `; write mode is claude-only in this release (${CODEC_WRITE_AGENT_ID})` : "")
85263
+ reason: `sandbox "${sandbox}" is not implemented in this release; only ${implemented.join(", ")} is available` + (sandbox === "worktree-write" && transportOf(entry) !== "acp" ? `; write mode is available for ${CODEC_WRITE_AGENT_IDS.join(", ")} only` + (entry.id === "antigravity-cli" ? ": antigravity-cli's edit tool writes outside the worktree" : "") : "")
85147
85264
  };
85148
85265
  }
85149
85266
  if (sandbox === "read-only") {
@@ -85169,12 +85286,16 @@ function readRuntimeBlock(dispatch) {
85169
85286
  return;
85170
85287
  return raw;
85171
85288
  }
85172
- var READ_ONLY_FORBIDDEN_ACTIONS, IMPLEMENTED_SANDBOX_MODES, IMPLEMENTED_ACP_SANDBOX_MODES, CODEC_WRITE_AGENT_ID = "claude-cli";
85289
+ var READ_ONLY_FORBIDDEN_ACTIONS, IMPLEMENTED_SANDBOX_MODES, IMPLEMENTED_ACP_SANDBOX_MODES, CODEC_WRITE_AGENT_IDS, CODEC_WRITE_VERIFIED_VERSIONS;
85173
85290
  var init_dispatch = __esm(() => {
85174
85291
  init_registry3();
85175
85292
  READ_ONLY_FORBIDDEN_ACTIONS = ["write", "network", "spawn-subagent"];
85176
85293
  IMPLEMENTED_SANDBOX_MODES = ["read-only"];
85177
85294
  IMPLEMENTED_ACP_SANDBOX_MODES = ["read-only", "worktree-write"];
85295
+ CODEC_WRITE_AGENT_IDS = ["claude-cli", "codex-cli"];
85296
+ CODEC_WRITE_VERIFIED_VERSIONS = {
85297
+ "codex-cli": { min: "0.159.2", before: "0.160.0" }
85298
+ };
85178
85299
  });
85179
85300
 
85180
85301
  // src/harness/external/env-deny.ts
@@ -85708,7 +85829,7 @@ var init_prompt2 = __esm(() => {
85708
85829
  EXTERNAL_WRITE_RUNTIME_DIRECTIVE = [
85709
85830
  "# Runtime directive",
85710
85831
  "",
85711
- "You are running non-interactively as a bounded child agent. Produce the work itself as " + "your final message, in the requested output schema. Do not ask questions, do not offer a " + "choice of modes, do not route to another skill, orchestrator, or flow, do not delegate to " + "another agent. You may create and modify files, only inside your current working " + "directory, using the Read, Grep, Glob, Edit and Write tools; no shell is available. Do not " + "touch .git, .github, .claude or .metaproject. Your changes are captured as a patch that a " + "human reviews before anything lands, so summarise what you changed in the final message."
85832
+ "You are running non-interactively as a bounded child agent. Produce the work itself as " + "your final message, in the requested output schema. Do not ask questions, do not offer a " + "choice of modes, do not route to another skill, orchestrator, or flow, do not delegate to " + "another agent. You may create and modify files, only inside your current working " + "directory, using your file reading, searching and editing tools. Do not use a shell to reach " + "outside it. Do not touch .git, .github, .claude or .metaproject. Your changes are captured as a patch that a " + "human reviews before anything lands, so summarise what you changed in the final message."
85712
85833
  ].join(`
85713
85834
  `);
85714
85835
  DIFF_INTRO_COMPLETE = "The operator's uncommitted changes are below. Your worktree is checked out at HEAD and " + "does not contain them, so this diff is the only place they exist for you.";
@@ -88240,6 +88361,8 @@ function buildCodexArgv(input) {
88240
88361
  "--ignore-user-config",
88241
88362
  "--skip-git-repo-check"
88242
88363
  ];
88364
+ if (input.sandbox === "worktree-write")
88365
+ argv.push(...CODEX_WRITE_CONFINEMENT_FLAGS);
88243
88366
  if (input.resultSchemaPath !== undefined)
88244
88367
  argv.push("--output-schema", input.resultSchemaPath);
88245
88368
  if (input.model !== undefined)
@@ -88247,8 +88370,9 @@ function buildCodexArgv(input) {
88247
88370
  argv.push(input.prompt);
88248
88371
  return argv;
88249
88372
  }
88250
- function buildCodexResumeArgv(sessionRef, message) {
88251
- return ["codex", "exec", "resume", sessionRef, "--json", "--ignore-user-config", "--skip-git-repo-check", message];
88373
+ function buildCodexResumeArgv(sessionRef, message, input) {
88374
+ const confinement = input?.sandbox === "worktree-write" ? ["-c", `sandbox_mode="${CODEX_SANDBOX_MODES["worktree-write"]}"`, ...CODEX_WRITE_CONFINEMENT_FLAGS] : [];
88375
+ return ["codex", "exec", "resume", sessionRef, "--json", "--ignore-user-config", "--skip-git-repo-check", ...confinement, message];
88252
88376
  }
88253
88377
  function parseCodexEvents(line) {
88254
88378
  const record = readJsonObject2(line);
@@ -88403,12 +88527,21 @@ function brief2(text, max = 200) {
88403
88527
  const flat = text.replace(/\s+/g, " ").trim();
88404
88528
  return flat.length <= max ? flat : `${flat.slice(0, max - 1)}\u2026`;
88405
88529
  }
88406
- var CODEX_SANDBOX_MODES, RECOGNISED_TYPES, NARRATED_FAILURE_LINE, CLI_USAGE_PATTERNS, AUTH_PATTERNS2, LIMIT_PATTERNS2, codexCliCodec;
88530
+ var CODEX_SANDBOX_MODES, CODEX_WRITE_CONFINEMENT_FLAGS, RECOGNISED_TYPES, NARRATED_FAILURE_LINE, CLI_USAGE_PATTERNS, AUTH_PATTERNS2, LIMIT_PATTERNS2, codexCliCodec;
88407
88531
  var init_codex_cli = __esm(() => {
88408
88532
  CODEX_SANDBOX_MODES = {
88409
88533
  "read-only": "read-only",
88410
88534
  "worktree-write": "workspace-write"
88411
88535
  };
88536
+ CODEX_WRITE_CONFINEMENT_FLAGS = [
88537
+ "--ignore-rules",
88538
+ "-c",
88539
+ "sandbox_workspace_write.exclude_slash_tmp=true",
88540
+ "-c",
88541
+ "sandbox_workspace_write.exclude_tmpdir_env_var=true",
88542
+ "-c",
88543
+ "sandbox_workspace_write.network_access=false"
88544
+ ];
88412
88545
  RECOGNISED_TYPES = new Set([
88413
88546
  "thread.started",
88414
88547
  "turn.started",
@@ -88438,7 +88571,7 @@ var init_codex_cli = __esm(() => {
88438
88571
  parseLine: parseCodexLine,
88439
88572
  parseEvents: parseCodexEvents,
88440
88573
  classifyFailure: classifyCodexFailure,
88441
- buildResumeArgv: (sessionRef, message) => buildCodexResumeArgv(sessionRef, message),
88574
+ buildResumeArgv: buildCodexResumeArgv,
88442
88575
  isRecognisedLine: isRecognisedCodexLine
88443
88576
  };
88444
88577
  });
@@ -88552,15 +88685,21 @@ async function runExternalChild(input, deps) {
88552
88685
  if (!isAcp && codec === undefined) {
88553
88686
  return refuse2("Error", `no codec is registered for external agent "${entry.id}"`);
88554
88687
  }
88688
+ let detectedVersion;
88555
88689
  if (deps.detect !== undefined) {
88556
88690
  const availability = resolveAvailability(entry, await deps.detect(entry.binary, entry.detect));
88557
88691
  if (availability.state === "binary-missing") {
88558
88692
  return refuse2("Denied", `${entry.label} is not installed (\`${entry.binary}\` not on PATH)`);
88559
88693
  }
88694
+ if (availability.state === "available")
88695
+ detectedVersion = availability.version;
88560
88696
  if (availability.state === "available" && availability.verdict.state !== "in-range") {
88561
88697
  deps.onWarning?.(`${entry.label} version ${availability.version ?? "unknown"} is outside the range this build's fixtures were recorded against`);
88562
88698
  }
88563
88699
  }
88700
+ const versionRefusal = sandbox === "worktree-write" ? codecWriteVersionRefusal(entry, detectedVersion) : undefined;
88701
+ if (versionRefusal !== undefined)
88702
+ return refuse2("Denied", versionRefusal);
88564
88703
  const schemaPrep = await prepareResultSchema();
88565
88704
  if (!schemaPrep.ok)
88566
88705
  return refuse2("Error", schemaPrep.reason);
@@ -112748,7 +112887,7 @@ var init_jev_rules = __esm(() => {
112748
112887
 
112749
112888
  // src/commands/review-jev-rules.ts
112750
112889
  import { readFile as readFile112, readdir as readdir41, stat as stat25 } from "fs/promises";
112751
- import { basename as basename2, dirname as dirname2, isAbsolute as isAbsolute3, join as join4, relative as relative3 } from "path";
112890
+ import { basename as basename2, dirname as dirname2, isAbsolute as isAbsolute3, join as join5, relative as relative3 } from "path";
112752
112891
  function rejectUnknownJevRulesFlags(args) {
112753
112892
  const unknown = args.filter((arg) => arg.startsWith("--") && !JEV_RULES_FLAGS.includes(arg.split("=")[0]));
112754
112893
  if (unknown.length > 0) {
@@ -112790,7 +112929,7 @@ async function walkMatching(root, suffixes) {
112790
112929
  return;
112791
112930
  }
112792
112931
  for (const entry of entries) {
112793
- const full = join4(dir, entry.name);
112932
+ const full = join5(dir, entry.name);
112794
112933
  if (entry.isDirectory()) {
112795
112934
  await walk(full);
112796
112935
  continue;
@@ -112818,7 +112957,7 @@ function stripKeryxManagedBlock(text) {
112818
112957
  }
112819
112958
  async function discoverRuleSources(cwd, explicitPaths) {
112820
112959
  const discovered = [];
112821
- for (const root of [join4(cwd, ".metaproject", "rules"), join4(cwd, "rules"), join4(cwd, ".claude", "rules")]) {
112960
+ for (const root of [join5(cwd, ".metaproject", "rules"), join5(cwd, "rules"), join5(cwd, ".claude", "rules")]) {
112822
112961
  for (const file of await walkMatching(root, [".md", ".mdc"])) {
112823
112962
  const text = await readIfExists(file);
112824
112963
  if (text === undefined)
@@ -112827,7 +112966,7 @@ async function discoverRuleSources(cwd, explicitPaths) {
112827
112966
  }
112828
112967
  }
112829
112968
  for (const name of ["CLAUDE.md", "AGENTS.md"]) {
112830
- const text = await readIfExists(join4(cwd, name));
112969
+ const text = await readIfExists(join5(cwd, name));
112831
112970
  if (text === undefined)
112832
112971
  continue;
112833
112972
  const body = stripKeryxManagedBlock(text);
@@ -112836,8 +112975,8 @@ async function discoverRuleSources(cwd, explicitPaths) {
112836
112975
  break;
112837
112976
  }
112838
112977
  const skillRoots = [
112839
- [join4(cwd, ".metaproject", "project-skills"), "project-skill"],
112840
- [join4(cwd, ".metaproject", "skills", "gdskills"), "gdskill"]
112978
+ [join5(cwd, ".metaproject", "project-skills"), "project-skill"],
112979
+ [join5(cwd, ".metaproject", "skills", "gdskills"), "gdskill"]
112841
112980
  ];
112842
112981
  for (const [root, kind] of skillRoots) {
112843
112982
  for (const skillMd of await walkMatching(root, ["SKILL.md"])) {
@@ -112862,7 +113001,7 @@ async function discoverRuleSources(cwd, explicitPaths) {
112862
113001
  const explicitFiles = [];
112863
113002
  const explicitDirSources = [];
112864
113003
  for (const raw of explicitPaths) {
112865
- const resolved = isAbsolute3(raw) ? raw : join4(cwd, raw);
113004
+ const resolved = isAbsolute3(raw) ? raw : join5(cwd, raw);
112866
113005
  const stats = await stat25(resolved).catch(() => {
112867
113006
  return;
112868
113007
  });
@@ -112905,11 +113044,11 @@ async function discoverRuleSources(cwd, explicitPaths) {
112905
113044
  return { sources: [...byPath.values()].sort((a, b) => a.path.localeCompare(b.path)), excluded: excluded.sort((a, b) => a.path.localeCompare(b.path)) };
112906
113045
  }
112907
113046
  async function fixturePrPort(dir) {
112908
- const raw = await readFile112(join4(dir, "pr.json"), "utf8");
113047
+ const raw = await readFile112(join5(dir, "pr.json"), "utf8");
112909
113048
  return createFixtureConformPrPort({ pr: JSON.parse(raw) });
112910
113049
  }
112911
113050
  async function fixtureJevFetch(dir) {
112912
- const raw = await readFile112(join4(dir, "jev-responses.json"), "utf8");
113051
+ const raw = await readFile112(join5(dir, "jev-responses.json"), "utf8");
112913
113052
  const responses = JSON.parse(raw);
112914
113053
  let index = 0;
112915
113054
  const fn = async () => {
@@ -115269,7 +115408,7 @@ var init_jev_scenarios = __esm(() => {
115269
115408
 
115270
115409
  // src/commands/review-jev-scenarios.ts
115271
115410
  import { readFile as readFile121, readdir as readdir43 } from "fs/promises";
115272
- import { join as join5, relative as relative4 } from "path";
115411
+ import { join as join6, relative as relative4 } from "path";
115273
115412
  function symbolsByPathFromRegions(regions) {
115274
115413
  const byPath = new Map;
115275
115414
  for (const region of regions) {
@@ -115322,7 +115461,7 @@ async function walkMarkdown2(root) {
115322
115461
  return;
115323
115462
  }
115324
115463
  for (const entry of entries) {
115325
- const full = join5(dir, entry.name);
115464
+ const full = join6(dir, entry.name);
115326
115465
  if (entry.isDirectory()) {
115327
115466
  await walk(full);
115328
115467
  continue;
@@ -115336,19 +115475,19 @@ async function walkMarkdown2(root) {
115336
115475
  }
115337
115476
  async function discoverScenarioSources(cwd) {
115338
115477
  const scenarios = [];
115339
- for (const file of await walkMarkdown2(join5(cwd, ".metaproject", "wiki", "user-scenarios"))) {
115478
+ for (const file of await walkMarkdown2(join6(cwd, ".metaproject", "wiki", "user-scenarios"))) {
115340
115479
  const text = await readIfExists2(file);
115341
115480
  if (text === undefined)
115342
115481
  continue;
115343
115482
  scenarios.push(scenarioFromWikiPage(relative4(cwd, file), text));
115344
115483
  }
115345
- for (const file of await walkMarkdown2(join5(cwd, "docs", "requirements"))) {
115484
+ for (const file of await walkMarkdown2(join6(cwd, "docs", "requirements"))) {
115346
115485
  const text = await readIfExists2(file);
115347
115486
  if (text === undefined)
115348
115487
  continue;
115349
115488
  scenarios.push(...scenariosFromPrd(relative4(cwd, file), text));
115350
115489
  }
115351
- const readmeLikeFiles = [join5(cwd, "README.md"), ...await walkMarkdown2(join5(cwd, "docs", "docs"))];
115490
+ const readmeLikeFiles = [join6(cwd, "README.md"), ...await walkMarkdown2(join6(cwd, "docs", "docs"))];
115352
115491
  for (const file of readmeLikeFiles) {
115353
115492
  const text = await readIfExists2(file);
115354
115493
  if (text === undefined)
@@ -115358,11 +115497,11 @@ async function discoverScenarioSources(cwd) {
115358
115497
  return scenarios;
115359
115498
  }
115360
115499
  async function fixturePrPort4(dir) {
115361
- const raw = await readFile121(join5(dir, "pr.json"), "utf8");
115500
+ const raw = await readFile121(join6(dir, "pr.json"), "utf8");
115362
115501
  return createFixtureConformPrPort({ pr: JSON.parse(raw) });
115363
115502
  }
115364
115503
  async function fixtureJevFetch5(dir) {
115365
- const raw = await readFile121(join5(dir, "jev-responses.json"), "utf8");
115504
+ const raw = await readFile121(join6(dir, "jev-responses.json"), "utf8");
115366
115505
  const responses = JSON.parse(raw);
115367
115506
  let index = 0;
115368
115507
  const fn = async () => {
@@ -115987,7 +116126,7 @@ var init_jev_docs = __esm(() => {
115987
116126
 
115988
116127
  // src/commands/review-jev-docs.ts
115989
116128
  import { readFile as readFile123, readdir as readdir44 } from "fs/promises";
115990
- import { join as join6, relative as relative5 } from "path";
116129
+ import { join as join7, relative as relative5 } from "path";
115991
116130
  function messageOf4(error) {
115992
116131
  return error instanceof Error ? error.message : String(error);
115993
116132
  }
@@ -116049,7 +116188,7 @@ async function walkMatching2(root, suffixes) {
116049
116188
  return;
116050
116189
  }
116051
116190
  for (const entry of entries) {
116052
- const full = join6(dir, entry.name);
116191
+ const full = join7(dir, entry.name);
116053
116192
  if (entry.isDirectory()) {
116054
116193
  await walk(full);
116055
116194
  continue;
@@ -116109,7 +116248,7 @@ async function walkAllDocFiles(dir) {
116109
116248
  return out;
116110
116249
  }
116111
116250
  for (const entry of entries) {
116112
- const full = join6(dir, entry.name);
116251
+ const full = join7(dir, entry.name);
116113
116252
  if (entry.isDirectory()) {
116114
116253
  if (INCLUDE_WALK_PRUNE_DIRS.has(entry.name))
116115
116254
  continue;
@@ -116123,19 +116262,19 @@ async function walkAllDocFiles(dir) {
116123
116262
  async function resolveIncludeGlob(cwd, glob) {
116124
116263
  const rootDir = includeGlobRootDir(glob);
116125
116264
  const regex = includeGlobToRegExp(glob);
116126
- const candidates = await walkAllDocFiles(join6(cwd, rootDir));
116265
+ const candidates = await walkAllDocFiles(join7(cwd, rootDir));
116127
116266
  return candidates.filter((file) => regex.test(relative5(cwd, file)));
116128
116267
  }
116129
116268
  async function discoverDocFiles(cwd, includeGlobs = []) {
116130
116269
  const files = new Set;
116131
- for (const file of await walkMatching2(join6(cwd, "docs"), [".md"]))
116270
+ for (const file of await walkMatching2(join7(cwd, "docs"), [".md"]))
116132
116271
  files.add(file);
116133
116272
  for (const name of await matchingFileNames(cwd, /^readme/i)) {
116134
116273
  if (/^changelog/i.test(name))
116135
116274
  continue;
116136
- files.add(join6(cwd, name));
116275
+ files.add(join7(cwd, name));
116137
116276
  }
116138
- for (const file of await walkMatching2(join6(cwd, ".metaproject", "wiki"), [".md"]))
116277
+ for (const file of await walkMatching2(join7(cwd, ".metaproject", "wiki"), [".md"]))
116139
116278
  files.add(file);
116140
116279
  for (const glob of includeGlobs) {
116141
116280
  for (const file of await resolveIncludeGlob(cwd, glob))
@@ -116163,11 +116302,11 @@ function changedCommandNamesFromRegions(regions) {
116163
116302
  return names;
116164
116303
  }
116165
116304
  async function fixturePrPort5(dir) {
116166
- const raw = await readFile123(join6(dir, "pr.json"), "utf8");
116305
+ const raw = await readFile123(join7(dir, "pr.json"), "utf8");
116167
116306
  return createFixtureConformPrPort({ pr: JSON.parse(raw) });
116168
116307
  }
116169
116308
  async function fixtureJevFetch6(dir) {
116170
- const raw = await readFile123(join6(dir, "jev-responses.json"), "utf8");
116309
+ const raw = await readFile123(join7(dir, "jev-responses.json"), "utf8");
116171
116310
  const responses = JSON.parse(raw);
116172
116311
  let index = 0;
116173
116312
  const fn = async () => {
@@ -116585,7 +116724,7 @@ var init_jev_comments = __esm(() => {
116585
116724
 
116586
116725
  // src/commands/review-jev-comments.ts
116587
116726
  import { readFile as readFile125 } from "fs/promises";
116588
- import { join as join7 } from "path";
116727
+ import { join as join8 } from "path";
116589
116728
  function rejectUnknownJevCommentsFlags(args) {
116590
116729
  const unknown = args.filter((arg) => arg.startsWith("--") && !JEV_COMMENTS_FLAGS.includes(arg.split("=")[0]));
116591
116730
  if (unknown.length > 0) {
@@ -116671,15 +116810,15 @@ function createFixtureJevCommentsFactsPort(facts) {
116671
116810
  };
116672
116811
  }
116673
116812
  async function fixturePrPort6(dir) {
116674
- const raw = await readFile125(join7(dir, "pr.json"), "utf8");
116813
+ const raw = await readFile125(join8(dir, "pr.json"), "utf8");
116675
116814
  return createFixtureConformPrPort({ pr: JSON.parse(raw) });
116676
116815
  }
116677
116816
  async function fixtureFactsPort(dir) {
116678
- const raw = await readFile125(join7(dir, "git-facts.json"), "utf8").catch(() => "{}");
116817
+ const raw = await readFile125(join8(dir, "git-facts.json"), "utf8").catch(() => "{}");
116679
116818
  return createFixtureJevCommentsFactsPort(JSON.parse(raw));
116680
116819
  }
116681
116820
  async function fixtureJevFetch7(dir) {
116682
- const raw = await readFile125(join7(dir, "jev-responses.json"), "utf8");
116821
+ const raw = await readFile125(join8(dir, "jev-responses.json"), "utf8");
116683
116822
  const responses = JSON.parse(raw);
116684
116823
  let index = 0;
116685
116824
  const fn = async () => {
@@ -116693,7 +116832,7 @@ async function fixtureJevFetch7(dir) {
116693
116832
  return fn;
116694
116833
  }
116695
116834
  function advisoryLabelsPath(cwd, repo, number) {
116696
- return join7(cwd, ".metaproject", "data", "review-jev-comments", `labels__${repo.replace(/\//g, "__")}__${number}.json`);
116835
+ return join8(cwd, ".metaproject", "data", "review-jev-comments", `labels__${repo.replace(/\//g, "__")}__${number}.json`);
116697
116836
  }
116698
116837
  async function readCommentAdvisoryLabels(cwd, repo, number) {
116699
116838
  const file = advisoryLabelsPath(cwd, repo, number);
@@ -127961,7 +128100,7 @@ var init_narrate = __esm(() => {
127961
128100
  // src/commands/memory.ts
127962
128101
  import { randomUUID as randomUUID34 } from "crypto";
127963
128102
  import { readdir as readdir45 } from "fs/promises";
127964
- import { join as join8 } from "path";
128103
+ import { join as join9 } from "path";
127965
128104
  function getService() {
127966
128105
  service4 ??= createMemoryService();
127967
128106
  return service4;
@@ -128068,7 +128207,7 @@ async function runIndex(args) {
128068
128207
  async function detectMemoryStoreUnreadable2(cwd) {
128069
128208
  const root = memoryRoot(cwd);
128070
128209
  for (const { folder } of MEMORY_TYPES) {
128071
- const dir = join8(root, folder);
128210
+ const dir = join9(root, folder);
128072
128211
  try {
128073
128212
  await readdir45(dir);
128074
128213
  } catch (error) {
@@ -152693,7 +152832,7 @@ function referencesHash(entry) {
152693
152832
  }
152694
152833
  return hash.digest("hex");
152695
152834
  }
152696
- function loadCache(cachePath) {
152835
+ function loadCache2(cachePath) {
152697
152836
  if (!existsSync51(cachePath))
152698
152837
  return {};
152699
152838
  try {
@@ -152821,7 +152960,7 @@ async function runStocktake(root, options = {}) {
152821
152960
  const now = options.now ?? (() => new Date);
152822
152961
  const { entries: catalog, unreadable } = loadSkillCatalogWithDiagnostics(root, { scope });
152823
152962
  const cachePath = options.cachePath ?? path295.join(root, ".metaproject", "data", "skills", "stocktake", "cache.json");
152824
- const cache = loadCache(cachePath);
152963
+ const cache = loadCache2(cachePath);
152825
152964
  const fingerprint = catalogFingerprint(catalog);
152826
152965
  let hits = 0;
152827
152966
  let misses = 0;
@@ -159572,7 +159711,7 @@ var init_post = __esm(() => {
159572
159711
 
159573
159712
  // src/commands/review-bot.ts
159574
159713
  import { readFile as readFile158 } from "fs/promises";
159575
- import { join as join9 } from "path";
159714
+ import { join as join10 } from "path";
159576
159715
  function reject(args, valued, boolean, usage) {
159577
159716
  const problems = [];
159578
159717
  for (let index = 0;index < args.length; index += 1) {
@@ -159601,7 +159740,7 @@ async function portFor(args) {
159601
159740
  return createGhPort();
159602
159741
  let pull;
159603
159742
  try {
159604
- pull = JSON.parse(await readFile158(join9(fixtures, "pull.json"), "utf8"));
159743
+ pull = JSON.parse(await readFile158(join10(fixtures, "pull.json"), "utf8"));
159605
159744
  } catch {
159606
159745
  pull = {};
159607
159746
  }
@@ -161041,7 +161180,7 @@ var init_floor = __esm(() => {
161041
161180
 
161042
161181
  // src/commands/review.ts
161043
161182
  import { mkdir as mkdir66, readdir as readdir58, readFile as readFile162 } from "fs/promises";
161044
- import path317, { join as join10 } from "path";
161183
+ import path317, { join as join11 } from "path";
161045
161184
  function reviewerList(args) {
161046
161185
  return (optionValue(args, "--reviewers") ?? "").split(",").map((item) => item.trim()).filter(Boolean);
161047
161186
  }
@@ -161716,7 +161855,7 @@ async function resolvePort(args) {
161716
161855
  }
161717
161856
  const files = {};
161718
161857
  for (const key of ["pull", "pull-comments", "pull-reviews", "issue-comments"]) {
161719
- const file = join10(fixtures, `${key}.json`);
161858
+ const file = join11(fixtures, `${key}.json`);
161720
161859
  try {
161721
161860
  files[key] = JSON.parse(await readFile162(file, "utf8"));
161722
161861
  } catch {
@@ -161726,13 +161865,13 @@ async function resolvePort(args) {
161726
161865
  return createFixturePort(files);
161727
161866
  }
161728
161867
  async function fixtureCiPort(dir) {
161729
- const runInfo = JSON.parse(await readFile162(join10(dir, "ci-run-info.json"), "utf8"));
161730
- const log = await readFile162(join10(dir, "ci-failed-log.txt"), "utf8").catch(() => "");
161731
- const history = await readFile162(join10(dir, "ci-history.json"), "utf8").then((raw) => JSON.parse(raw)).catch(() => ({}));
161732
- const attempts = await readFile162(join10(dir, "ci-attempts.json"), "utf8").then((raw) => JSON.parse(raw)).catch(() => ({}));
161733
- const changedFiles = await readFile162(join10(dir, "ci-changed-files.json"), "utf8").then((raw) => JSON.parse(raw)).catch(() => ({}));
161734
- const runsByHeadSha = await readFile162(join10(dir, "ci-runs-by-head-sha.json"), "utf8").then((raw) => JSON.parse(raw)).catch(() => ({}));
161735
- const related = await readFile162(join10(dir, "ci-related-runs.json"), "utf8").then((raw) => JSON.parse(raw)).catch(() => ({}));
161868
+ const runInfo = JSON.parse(await readFile162(join11(dir, "ci-run-info.json"), "utf8"));
161869
+ const log = await readFile162(join11(dir, "ci-failed-log.txt"), "utf8").catch(() => "");
161870
+ const history = await readFile162(join11(dir, "ci-history.json"), "utf8").then((raw) => JSON.parse(raw)).catch(() => ({}));
161871
+ const attempts = await readFile162(join11(dir, "ci-attempts.json"), "utf8").then((raw) => JSON.parse(raw)).catch(() => ({}));
161872
+ const changedFiles = await readFile162(join11(dir, "ci-changed-files.json"), "utf8").then((raw) => JSON.parse(raw)).catch(() => ({}));
161873
+ const runsByHeadSha = await readFile162(join11(dir, "ci-runs-by-head-sha.json"), "utf8").then((raw) => JSON.parse(raw)).catch(() => ({}));
161874
+ const related = await readFile162(join11(dir, "ci-related-runs.json"), "utf8").then((raw) => JSON.parse(raw)).catch(() => ({}));
161736
161875
  return createFixtureCiPort({
161737
161876
  runs: { [runInfo.runId]: runInfo, ...related.runs },
161738
161877
  logs: { [runInfo.runId]: log, ...related.logs },
@@ -161743,7 +161882,7 @@ async function fixtureCiPort(dir) {
161743
161882
  });
161744
161883
  }
161745
161884
  async function fixtureJevFetch9(dir) {
161746
- const body = await readFile162(join10(dir, "jev-response.json"), "utf8");
161885
+ const body = await readFile162(join11(dir, "jev-response.json"), "utf8");
161747
161886
  const fn = async () => new Response(body, { status: 200, headers: { "content-type": "application/json" } });
161748
161887
  return fn;
161749
161888
  }
@@ -161912,12 +162051,12 @@ async function runCiTriageEval(cwd, args, evalFile) {
161912
162051
  }
161913
162052
  }
161914
162053
  async function fixtureConformJevFetch(dir) {
161915
- const body = await readFile162(join10(dir, "jev-response.json"), "utf8");
162054
+ const body = await readFile162(join11(dir, "jev-response.json"), "utf8");
161916
162055
  const fn = async () => new Response(body, { status: 200, headers: { "content-type": "application/json" } });
161917
162056
  return fn;
161918
162057
  }
161919
162058
  async function fixtureConformPrPortFrom(dir) {
161920
- const pr = JSON.parse(await readFile162(join10(dir, "pr.json"), "utf8"));
162059
+ const pr = JSON.parse(await readFile162(join11(dir, "pr.json"), "utf8"));
161921
162060
  return createFixtureConformPrPort({ pr });
161922
162061
  }
161923
162062
  function accumulateJevUsage(acc, usage) {
@@ -161998,7 +162137,7 @@ async function explainConformVerdicts(cwd, refPath, verdicts, threshold, fixture
161998
162137
  return {};
161999
162138
  if (fixturesDir !== undefined) {
162000
162139
  try {
162001
- return JSON.parse(await readFile162(join10(fixturesDir, "explain-response.json"), "utf8"));
162140
+ return JSON.parse(await readFile162(join11(fixturesDir, "explain-response.json"), "utf8"));
162002
162141
  } catch {
162003
162142
  return {};
162004
162143
  }
@@ -162046,7 +162185,7 @@ function parsePositiveIntFlag(args, flag, fallback) {
162046
162185
  }
162047
162186
  async function readReportFindings2(reportDir) {
162048
162187
  try {
162049
- const raw = JSON.parse(await readFile162(join10(reportDir, "findings.json"), "utf8"));
162188
+ const raw = JSON.parse(await readFile162(join11(reportDir, "findings.json"), "utf8"));
162050
162189
  return Array.isArray(raw) ? raw : [];
162051
162190
  } catch {
162052
162191
  return [];
@@ -162118,7 +162257,7 @@ async function runConform(args) {
162118
162257
  const fetchFn = fixturesDir === undefined ? globalThis.fetch : await fixtureConformJevFetch(fixturesDir);
162119
162258
  const acc = { jevCalls: 0, inputTokens: 0, outputTokens: 0, costUsd: 0, sawUsage: false };
162120
162259
  const clauses = await resolveConformClauseTags(cwd, refPath, docText, rawClauses, fetchFn, model, acc);
162121
- await writeFileAtomic(join10(cwd, CONFORM_RECENTS_PATH), `${JSON.stringify(withRecentDoc(await readRecentConformDocs2(cwd), refPath), null, 2)}
162260
+ await writeFileAtomic(join11(cwd, CONFORM_RECENTS_PATH), `${JSON.stringify(withRecentDoc(await readRecentConformDocs2(cwd), refPath), null, 2)}
162122
162261
  `, { mode: 384 }).catch(() => {});
162123
162262
  const notCheckable = clauses.filter((c) => !c.checkable);
162124
162263
  const checkable = clauses.filter((c) => c.checkable);
@@ -162150,7 +162289,7 @@ async function runConform(args) {
162150
162289
  hunkTotalRegions = hunkResult.totalRegions;
162151
162290
  } else if (reportDir !== undefined) {
162152
162291
  target = { kind: "report", label: reportDir };
162153
- const reportMarkdown = await readFile162(join10(reportDir, "report.md"), "utf8").catch(() => "");
162292
+ const reportMarkdown = await readFile162(join11(reportDir, "report.md"), "utf8").catch(() => "");
162154
162293
  const findings = await readReportFindings2(reportDir);
162155
162294
  const facts = computeReportConformFacts(reportMarkdown, findings);
162156
162295
  const redacted = reportRedactedStateText(reportMarkdown);
@@ -162191,7 +162330,7 @@ async function runConform(args) {
162191
162330
  }
162192
162331
  async function readRecentConformDocs2(cwd) {
162193
162332
  try {
162194
- const raw = JSON.parse(await readFile162(join10(cwd, CONFORM_RECENTS_PATH), "utf8"));
162333
+ const raw = JSON.parse(await readFile162(join11(cwd, CONFORM_RECENTS_PATH), "utf8"));
162195
162334
  return Array.isArray(raw) ? raw : [];
162196
162335
  } catch {
162197
162336
  return [];
@@ -162440,7 +162579,7 @@ async function runStack(args) {
162440
162579
  console.log(renderStackScopingMarkdown(detected, decisions));
162441
162580
  }
162442
162581
  async function stackScopingForInstalledReviewers(cwd, detected) {
162443
- const reviewRoot = join10(cwd, ".metaproject", "skills", "gdskills", "review");
162582
+ const reviewRoot = join11(cwd, ".metaproject", "skills", "gdskills", "review");
162444
162583
  let entries;
162445
162584
  try {
162446
162585
  entries = await readdir58(reviewRoot, { withFileTypes: true });
@@ -162452,7 +162591,7 @@ async function stackScopingForInstalledReviewers(cwd, detected) {
162452
162591
  if (!entry.isDirectory()) {
162453
162592
  continue;
162454
162593
  }
162455
- const skillMdPath = join10(reviewRoot, entry.name, "SKILL.md");
162594
+ const skillMdPath = join11(reviewRoot, entry.name, "SKILL.md");
162456
162595
  let content;
162457
162596
  try {
162458
162597
  content = await readFile162(skillMdPath, "utf8");
@@ -179345,7 +179484,7 @@ async function runCommand4(args, deps, log) {
179345
179484
  maxExternalDepth: DEFAULT_MAX_EXTERNAL_DEPTH,
179346
179485
  onWarning: (warning) => console.error(`warning: ${plain(warning)}`)
179347
179486
  };
179348
- if (write && id === CODEC_WRITE_AGENT_ID) {
179487
+ if (write && CODEC_WRITE_AGENT_IDS.includes(id)) {
179349
179488
  let handle;
179350
179489
  let interrupted = false;
179351
179490
  const onSigint = () => {
@@ -179619,7 +179758,7 @@ function printExternalHelp() {
179619
179758
  { flag: "--no-probe", desc: "Skip detection entirely; every entry reports `not-probed`." },
179620
179759
  { flag: "--task", desc: "run: what the agent should do. It runs in a disposable worktree; your tree is never touched." },
179621
179760
  { flag: "--unattended", desc: "run: refuse every permission that would need a human (also implied without a TTY)." },
179622
- { flag: "--write", desc: "run: writes land in the disposable worktree and leave as a never-applied patch. An ACP agent advertises fs.writeTextFile; for a line-stream agent only claude is supported (Edit and Write inside the worktree)." },
179761
+ { flag: "--write", desc: "run: writes land in the disposable worktree and leave as a never-applied patch. An ACP agent advertises fs.writeTextFile; for a line-stream agent only claude-cli and codex-cli are supported. claude gets Edit and Write inside the worktree and no shell; codex runs under its OS sandbox (writes confined to the worktree, network closed as far as measured) and keeps a shell that can read any file you can read. codex needs version 0.159.2 or newer but older than 0.160.0. antigravity-cli is refused: its edit tool writes outside the worktree." },
179623
179762
  { flag: "--timeout", desc: "run: wall-clock ceiling in ms (default: externalAgents.defaultTimeoutMs)." }
179624
179763
  ]);
179625
179764
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrciphersmith/keryx",
3
- "version": "0.3.40",
3
+ "version": "0.3.42",
4
4
  "description": "Version-controlled project context for AI coding agents: code graph, architecture wiki, project memory, relevant tests, quality signals, and task flows.",
5
5
  "private": false,
6
6
  "publishConfig": {
@@ -165,7 +165,7 @@
165
165
  "minLength": 1
166
166
  },
167
167
  "sandbox": {
168
- "description": "Permission level for the external child. `worktree-write` is schema-valid but refused at runtime in the read-only release, with a reason distinguishable from an unsupported-by-this-agent refusal.",
168
+ "description": "Permission level for the external child. `worktree-write` is schema-valid but refused at runtime except for an ACP agent and, through the operator's `--write` command only, `claude-cli` and `codex-cli`, with a reason distinguishable from an unsupported-by-this-agent refusal.",
169
169
  "type": "string",
170
170
  "enum": ["read-only", "worktree-write"]
171
171
  },