tickmarkr 2.4.1 → 2.4.2

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.
@@ -3,7 +3,7 @@ import { readdirSync, readFileSync, realpathSync, statSync } from "node:fs";
3
3
  import { homedir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  import { parseWorkerResult } from "./prompt.js";
6
- import { channelsFromConfig, declareInputBox, MODEL_ID_RE, shq, TokenUsageSchema } from "./types.js";
6
+ import { channelsFromConfig, declareInputBox, MODEL_ID_RE, promptFitsArgv, shq, TokenUsageSchema } from "./types.js";
7
7
  // SPEND-01/SPEND-11: claude writes a per-session JSONL to ~/.claude/projects/<slug>/ where slug is the
8
8
  // realpath'd cwd with every non-alphanumeric char replaced by "-" (verified 114/114 — 36-DIAGNOSIS.md).
9
9
  // The old `/`-only formula missed the "." in `.tickmarkr/worktrees/…` — ENOENT on every worktree dispatch.
@@ -239,7 +239,12 @@ export const claudeCode = {
239
239
  // live check ate the prompt), and --prompt-suggestions takes an OPTIONAL value — appended directly
240
240
  // before the prompt it would swallow it the same way. So the setting's value is always followed by
241
241
  // another flag, never by the prompt positional.
242
- interactiveCommand: (promptFile, model) => `claude --model ${shq(model)} --strict-mcp-config --mcp-config '{"mcpServers":{}}' --settings '{"promptSuggestionEnabled":false}' --prompt-suggestions false --permission-mode bypassPermissions "$(cat ${shq(promptFile)})"`,
242
+ // OBS-931: the same ONE-argv-string hazard as codex (OBS-930) — over promptArgvCeiling() the TUI
243
+ // launch would E2BIG on Linux, so it returns null → worker-mode-fallback → the headless form.
244
+ // resumeCommand keeps the shape: its contract returns a string (composer delivery is 2.4.3 work).
245
+ interactiveCommand: (promptFile, model) => promptFitsArgv(promptFile)
246
+ ? `claude --model ${shq(model)} --strict-mcp-config --mcp-config '{"mcpServers":{}}' --settings '{"promptSuggestionEnabled":false}' --prompt-suggestions false --permission-mode bypassPermissions "$(cat ${shq(promptFile)})"`
247
+ : null,
243
248
  trustDialog: CLAUDE_TRUST_DIALOG,
244
249
  inputBox: CLAUDE_INPUT_BOX,
245
250
  // A resumed attempt lands in the same painted editor, so it carries the same ghost-text suppression
@@ -4,6 +4,7 @@ export declare function readCodexModelsCache(path?: string): {
4
4
  fetchedAt?: string;
5
5
  };
6
6
  export declare const CODEX_TRUST_DIALOG: TrustDialog;
7
+ export declare const CODEX_INPUT_BOX: import("./types.js").InputBox;
7
8
  export declare function seedCodexTrust(repoRoot: string, configPath?: string): TrustVerdict;
8
9
  export declare function hasCodexTrustedProject(text: string, root: string): boolean;
9
10
  export declare function codexConfigMcpServerNames(configPath?: string): string[];
@@ -3,7 +3,7 @@ import { homedir } from "node:os";
3
3
  import { dirname, join } from "node:path";
4
4
  import { probeVersion } from "./claude-code.js";
5
5
  import { parseWorkerResult } from "./prompt.js";
6
- import { channelsFromConfig, MODEL_ID_RE, shq, TokenUsageSchema } from "./types.js";
6
+ import { channelsFromConfig, declareInputBox, MODEL_ID_RE, promptFitsArgv, shq, TokenUsageSchema } from "./types.js";
7
7
  // SPEND-07: codex writes per-session JSONL to ~/.codex/sessions/YYYY/MM/DD/rollout-*.jsonl — date-partitioned,
8
8
  // NOT cwd-keyed. session_meta.payload.cwd is FILE-SCOPED (one codex exec per cwd). token_count events carry
9
9
  // per-turn DELTAS in payload.info.last_token_usage; we read POST-HOC (never the pane, never the trailer).
@@ -95,6 +95,56 @@ export const CODEX_TRUST_DIALOG = {
95
95
  fingerprint: "Do you trust the contents of this directory?",
96
96
  key: "Enter",
97
97
  };
98
+ // OBS-930 / OBS-136: the codex TUI's composer, CAPTURED (codex 0.153.4, herdr pane read, 2026-09-05
99
+ // 15:48Z — tests/fixtures/codex-input-box, provenance in its README), never guessed:
100
+ // (background row)
101
+ // › Ask Codex to do anything ← U+203A, ASCII space, then the DIM placeholder (empty) or the draft
102
+ // (background row)
103
+ // gpt-5.6-luna medium · /private/tmp/tkr-obs930-smoke ← footer: <model> <effort> · <cwd>
104
+ // Two facts a fingerprint alone would get wrong: (1) a submitted turn is echoed into the transcript
105
+ // with the SAME caret (`› You are a smoke test.`), and so is the trust dialog's cursor (`› 1. Yes,
106
+ // continue`) — only the composer is followed by the footer row, so the footer is the anchor, exactly
107
+ // as claude's editor is anchored by its rules; (2) an EMPTY composer paints its placeholder as text,
108
+ // so "empty" is a closed allowlist of captured placeholders (0.153.4 above; 0.144.5's
109
+ // `Run /review on my current changes` from tests/fixtures/codex-mcp-spinner). An unknown placeholder
110
+ // reads as occupied and a submit onto it fails closed by name (OBS-140) — a fixture-capture chore,
111
+ // never a drive-by widening. `match` is THE COMPOSER IS PAINTED (true while empty, true mid-turn);
112
+ // `emptyMatch` is painted AND carrying nothing — the only positive evidence a submit registered.
113
+ const CODEX_ANSI_SGR_RE = /\u001B\[[0-9;]*m/g;
114
+ const CODEX_CARET_RE = /^› /;
115
+ const CODEX_PLACEHOLDER_RE = /^› (?:Ask Codex to do anything|Run \/review on my current changes)$/;
116
+ const CODEX_FOOTER_RE = /^\S+(?: \S+)? · \S/;
117
+ // A wrapped or multi-line draft grows the composer downward before the footer.
118
+ // ponytail: a fixed window, not a parser — raise it if a real capture ever shows a taller composer.
119
+ const CODEX_MAX_COMPOSER_ROWS = 8;
120
+ function matchesCodexComposer(paneText, empty) {
121
+ const lines = paneText.replace(CODEX_ANSI_SGR_RE, "").split("\n").map((l) => l.trim());
122
+ const caret = empty ? CODEX_PLACEHOLDER_RE : CODEX_CARET_RE;
123
+ return lines.some((line, i) => {
124
+ if (!caret.test(line))
125
+ return false;
126
+ for (let below = i + 1; below < lines.length && below <= i + CODEX_MAX_COMPOSER_ROWS; below++) {
127
+ if (CODEX_FOOTER_RE.test(lines[below]))
128
+ return true;
129
+ // only the composer's own rows may sit between the caret row and the footer: the background
130
+ // rows (blank in a text read) and a draft's continuation rows — never another caret row
131
+ if (lines[below] !== "" && CODEX_CARET_RE.test(lines[below]))
132
+ return false;
133
+ }
134
+ return false;
135
+ });
136
+ }
137
+ export const CODEX_INPUT_BOX = declareInputBox("codex", {
138
+ fingerprint: "› ",
139
+ match: (paneText) => matchesCodexComposer(paneText, false),
140
+ emptyMatch: (paneText) => matchesCodexComposer(paneText, true),
141
+ // As for claude (OBS-342): a fresh codex worker slot is a shell awaiting its launch line; every
142
+ // later delivery is a TUI turn awaiting this composer.
143
+ firstDeliveryIsLaunch: true,
144
+ // The 2026-09-05 capture painted the composer ~10 s after the trust answer with MCP suppressed;
145
+ // claude's bound, kept for the same cold-start reasons.
146
+ readinessTimeoutMs: 30_000,
147
+ });
98
148
  // v1.22 T5 / OBS-16: codex keys trust on absolute path under [projects."<root>"] trust_level="trusted"
99
149
  // in ~/.codex/config.toml (CODEX_HOME relocates the dir). Worktrees inherit parent-project trust when
100
150
  // the REPO ROOT is trusted — seed the root once, cover every future worktree. Idempotent: a second
@@ -186,14 +236,24 @@ export const codex = {
186
236
  channels: (cfg) => channelsFromConfig("codex", cfg),
187
237
  // v1.65 T3: every flag the command builder below hardcodes (incl. codexMcpSuppressionFlags' -c/
188
238
  // --disable and GITDIR_WRITABLE's -c) — all listed by top-level `codex --help`, verified 2026-07-22.
189
- hardcodedFlags: { binary: "codex", flags: ["--sandbox", "--model", "-c", "--disable", "--dangerously-bypass-hook-trust"] },
239
+ hardcodedFlags: { binary: "codex", flags: ["--sandbox", "-a", "-s", "--model", "-c", "--disable", "--dangerously-bypass-hook-trust"] },
190
240
  // --sandbox workspace-write is the autonomous sandbox mode (codex v0.144.1+)
191
241
  // MCP suppression built per dispatch (config can change between runs) — see codexMcpSuppressionFlags.
192
242
  // CODEX_HOOK_TRUST (OBS-125) clears the per-worktree "Hooks need review" gate while keeping the sandbox.
193
243
  headlessCommand: (promptFile, model) => `codex exec --sandbox workspace-write ${CODEX_HOOK_TRUST} ${codexMcpSuppressionFlags()} ${GITDIR_WRITABLE} --model ${shq(model)} - < ${shq(promptFile)}`,
194
- // OBS-889: Codex's TUI has no file/stdin prompt form. Returning null makes the daemon journal
195
- // worker-mode-fallback before it runs the argv-safe headless command in the visible pane.
196
- interactiveCommand: () => null,
244
+ // OBS-930: the visible pane runs the REAL TUI. Codex's TUI takes its prompt only as the [PROMPT]
245
+ // positional (`codex --help`, 0.153.4 — no file/stdin form), so the launch inlines the file exactly
246
+ // as the claude adapter does: the prompt is the LAST positional and every flag value is followed by
247
+ // a flag, never by the prompt. The argv hazard that once forbade this (OBS-889: `countLiveSuites`
248
+ // matched a suite word 140 KB into a finished worker's argv) is closed on the counter side — the
249
+ // census reads a command's first four tokens only — and those four never carry a suite word here.
250
+ // Same sandbox, hook trust and MCP suppression as the headless form; `-a never` is the TUI's
251
+ // autonomous approval policy (exec has no approvals to configure).
252
+ // OBS-930 (Linux): the inlined prompt is ONE argv string and Linux caps one at 131072 bytes, so a
253
+ // prompt over promptArgvCeiling() returns null → worker-mode-fallback → the headless form (types.ts).
254
+ interactiveCommand: (promptFile, model) => promptFitsArgv(promptFile)
255
+ ? `codex -a never -s workspace-write ${CODEX_HOOK_TRUST} ${codexMcpSuppressionFlags()} ${GITDIR_WRITABLE} --model ${shq(model)} "$(cat ${shq(promptFile)})"`
256
+ : null,
197
257
  invoke(task, _cwd, a, ctx) {
198
258
  return { command: this.headlessCommand(ctx.promptFile, a.model) };
199
259
  },
@@ -202,6 +262,7 @@ export const codex = {
202
262
  // "Do you trust this directory?" (OBS-16). doctor-only side effect.
203
263
  trust: (repoRoot) => seedCodexTrust(repoRoot),
204
264
  trustDialog: CODEX_TRUST_DIALOG,
265
+ inputBox: CODEX_INPUT_BOX,
205
266
  // v1.5 MODEL-01: file read only (no `codex models` subcommand exists, verified 2026-07-10).
206
267
  // Already fails OPEN to [] internally — advisory detection, unlike gates' fail-closed.
207
268
  listModels: async () => readCodexModelsCache().models,
@@ -185,5 +185,9 @@ export declare function channelKey(c: {
185
185
  model: string;
186
186
  }): string;
187
187
  export declare function shq(s: string): string;
188
+ export declare const PROMPT_ARGV_CEILING_LINUX = 120000;
189
+ export declare const PROMPT_ARGV_CEILING_DEFAULT = 900000;
190
+ export declare function promptArgvCeiling(platform?: string): number;
191
+ export declare function promptFitsArgv(promptFile: string, platform?: string): boolean;
188
192
  export declare const QUOTA_RE: RegExp;
189
193
  export declare const MODEL_ID_RE: RegExp;
@@ -1,3 +1,4 @@
1
+ import { statSync } from "node:fs";
1
2
  import { z } from "zod";
2
3
  // SPEND-01/06: normalized token counts — the measurable fact. NO cost field, ever: CLIs report
3
4
  // cost:0 on sub plans and notional list prices on others (LIVE-CHECK finding 3); money is Phase 18's
@@ -232,6 +233,38 @@ export function channelKey(c) {
232
233
  export function shq(s) {
233
234
  return `'${s.replaceAll("'", `'\\''`)}'`;
234
235
  }
236
+ // OBS-930 (Linux) / OBS-931: a TUI launch inlines the prompt file as ONE argv string — "$(cat prompt)"
237
+ // as the last positional. Linux caps a single argv string at MAX_ARG_STRLEN = PAGE_SIZE × 32 =
238
+ // 131072 bytes (E2BIG: CI run 33979013874, ubuntu, `codex: Argument list too long` on a 140 KB
239
+ // prompt); darwin enforces only the 1 MB total ARG_MAX, which is why the macOS export proof passed.
240
+ // A real worker prompt is 60–150 KB (OBS-889 measured 149,417 bytes), so on a Linux host the launch
241
+ // fails in production, not only in the test. Ceilings are named BY PLATFORM — never a probe of the
242
+ // running kernel at dispatch time:
243
+ // linux 120_000 — the cap is per STRING and every flag is its own argv entry, so only the prompt
244
+ // counts against it; "$(cat …)" strips nothing but trailing newlines, so the
245
+ // file's byte size IS the string's. 131072 − 120000 leaves ~11 KB of headroom.
246
+ // others 900_000 — under the 1 MB total that darwin/BSD enforce across argv + envp.
247
+ // Over the ceiling the adapter returns null: the daemon journals worker-mode-fallback
248
+ // {reason:"adapter"} and runs the headless form in the visible pane — the pre-OBS-930 behaviour,
249
+ // now only for oversized prompts. An unreadable file is "not proven oversized" and keeps the TUI
250
+ // rendering: the daemon writes the prompt before it builds the launch, a missing file fails the
251
+ // same way in either form, and docs-truth renders the command against a placeholder path.
252
+ // OBS-931 (2.4.3): the real fix is prompt delivery through the composer for large prompts.
253
+ export const PROMPT_ARGV_CEILING_LINUX = 120_000;
254
+ export const PROMPT_ARGV_CEILING_DEFAULT = 900_000;
255
+ export function promptArgvCeiling(platform = process.platform) {
256
+ return platform === "linux" ? PROMPT_ARGV_CEILING_LINUX : PROMPT_ARGV_CEILING_DEFAULT;
257
+ }
258
+ export function promptFitsArgv(promptFile, platform = process.platform) {
259
+ let bytes;
260
+ try {
261
+ bytes = statSync(promptFile).size;
262
+ }
263
+ catch {
264
+ return true;
265
+ }
266
+ return bytes <= promptArgvCeiling(platform);
267
+ }
235
268
  // Quota exhaustion is detected from CLI errors, never predicted (spec §4).
236
269
  // ZAI coding-plan exhaustion text: "Insufficient balance or no resource package. Please recharge."
237
270
  // Anchor the distinctive full phrase, not the two-word "insufficient balance" fragment — that fires
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tickmarkr",
3
- "version": "2.4.1",
3
+ "version": "2.4.2",
4
4
  "description": "Spec in, verified work out.",
5
5
  "type": "module",
6
6
  "license": "MIT",