flowviant 0.125.5 → 0.125.6

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.
Files changed (86) hide show
  1. package/bin/lib/agentInternalWait.mjs +1 -5
  2. package/bin/lib/agentPlan.mjs +13 -163
  3. package/bin/lib/agentRepositories.mjs +3 -1
  4. package/bin/lib/claudeInput.mjs +28 -9
  5. package/bin/lib/claudeMachineMcp.mjs +85 -0
  6. package/bin/lib/claudePersonal.mjs +6 -57
  7. package/bin/lib/claudePosture.mjs +6 -15
  8. package/bin/lib/claudeSettingsArg.mjs +67 -0
  9. package/bin/lib/claudeSubagentProgress.mjs +12 -28
  10. package/bin/lib/claudeSubagents.mjs +4 -2
  11. package/bin/lib/codexFencedMount.mjs +275 -0
  12. package/bin/lib/codexPersonal.mjs +1 -265
  13. package/bin/lib/codexProjectRead.mjs +5 -4
  14. package/bin/lib/codexResearch.mjs +9 -3
  15. package/bin/lib/codexSubagentEvents.mjs +11 -7
  16. package/bin/lib/codexSubagents.mjs +55 -52
  17. package/bin/lib/codexToml.mjs +328 -0
  18. package/bin/lib/conversationIds.mjs +10 -0
  19. package/bin/lib/fleet.mjs +2 -2
  20. package/bin/lib/git.mjs +32 -0
  21. package/bin/lib/heldTool.mjs +49 -0
  22. package/bin/lib/hooks/mcpAllow.mjs +11 -5
  23. package/bin/lib/hooks/orchestrator.mjs +20 -10
  24. package/bin/lib/intakeDraft.mjs +1 -1
  25. package/bin/lib/jsonlTail.mjs +52 -0
  26. package/bin/lib/mcpInventory.mjs +9 -5
  27. package/bin/lib/mcpReasons.mjs +20 -0
  28. package/bin/lib/mcpReportBounds.mjs +10 -0
  29. package/bin/lib/mcpStdio.mjs +35 -0
  30. package/bin/lib/mergeBatch.mjs +6 -24
  31. package/bin/lib/mergeOrder.mjs +18 -0
  32. package/bin/lib/modelJson.mjs +173 -0
  33. package/bin/lib/nativeAgentTools.mjs +72 -0
  34. package/bin/lib/orchestrator.mjs +212 -133
  35. package/bin/lib/orchestratorClient.mjs +17 -21
  36. package/bin/lib/orchestratorHooks.mjs +21 -4
  37. package/bin/lib/orchestratorNative.mjs +58 -0
  38. package/bin/lib/orchestratorPrompt.mjs +7 -4
  39. package/bin/lib/orchestratorProtocol.mjs +25 -3
  40. package/bin/lib/postureTools.mjs +58 -13
  41. package/bin/lib/prWorkflow.mjs +13 -2
  42. package/bin/lib/projectRepositories.mjs +2 -2
  43. package/bin/lib/projectToolRuntimes.mjs +92 -245
  44. package/bin/lib/projectTools.mjs +59 -19
  45. package/bin/lib/repositoryDelivery.mjs +6 -4
  46. package/bin/lib/repositoryMerge.mjs +8 -12
  47. package/bin/lib/repositoryPlaces.mjs +2 -2
  48. package/bin/lib/repositoryPrompts.mjs +42 -0
  49. package/bin/lib/repositoryTool.mjs +9 -32
  50. package/bin/lib/researchUse.mjs +39 -0
  51. package/bin/lib/resumeMarker.mjs +43 -0
  52. package/bin/lib/runTurn.mjs +34 -52
  53. package/bin/lib/runtimeAntigravity.mjs +1 -1
  54. package/bin/lib/runtimeCapabilities.mjs +6 -16
  55. package/bin/lib/runtimeClaude.mjs +47 -20
  56. package/bin/lib/runtimeCodex.mjs +9 -6
  57. package/bin/lib/runtimeEvents.mjs +13 -2
  58. package/bin/lib/runtimeOfferings.mjs +9 -4
  59. package/bin/lib/runtimes.mjs +3 -1
  60. package/bin/lib/subagentBounds.mjs +20 -0
  61. package/bin/lib/trace.mjs +30 -5
  62. package/bin/lib/turnDeclaration.mjs +1 -1
  63. package/bin/lib/turnDiagnostics.mjs +9 -12
  64. package/bin/lib/turnReceipts.mjs +64 -0
  65. package/bin/lib/turnSubagents.mjs +91 -0
  66. package/bin/lib/turnUsage.mjs +9 -6
  67. package/bin/lib/versionOrder.mjs +11 -1
  68. package/bin/lib/work.mjs +6 -6
  69. package/bin/lib/workAgentMerges.mjs +5 -7
  70. package/bin/lib/workAgentPrecheck.mjs +5 -5
  71. package/bin/lib/workAgentRecovery.mjs +2 -1
  72. package/bin/lib/workAgentResume.mjs +121 -0
  73. package/bin/lib/workAgentTools.mjs +111 -0
  74. package/bin/lib/workAgentTurnExecution.mjs +62 -246
  75. package/bin/lib/workAgentTurnOutcome.mjs +30 -3
  76. package/bin/lib/workAgentTurnShape.mjs +90 -0
  77. package/bin/lib/workAgentTurns.mjs +24 -10
  78. package/bin/lib/workArtifactReconcile.mjs +3 -2
  79. package/bin/lib/workMergeBatch.mjs +7 -5
  80. package/bin/lib/workPlaceReports.mjs +3 -3
  81. package/bin/lib/workPlaces.mjs +14 -0
  82. package/bin/lib/workSessionRuntime.mjs +2 -5
  83. package/bin/lib/workSessionTurns.mjs +12 -63
  84. package/bin/lib/workShip.mjs +8 -18
  85. package/bin/lib/workTurnShape.mjs +6 -4
  86. package/package.json +1 -1
@@ -10,6 +10,7 @@ import { ensureRealSubdir, realDirState } from './ownedDirs.mjs';
10
10
  import { closeSync, constants, copyFileSync, existsSync, fstatSync, openSync, readFileSync, readSync, lstatSync, mkdirSync, readdirSync, realpathSync, rmSync, unlinkSync, writeFileSync } from 'node:fs';
11
11
  import { dirname, isAbsolute, join, resolve } from 'node:path';
12
12
  import { createRequire } from 'node:module';
13
+ import { versionAtLeast } from './versionOrder.mjs';
13
14
 
14
15
  const MAX_FILES = 20_000;
15
16
  const MAX_BYTES = 128 * 1024 * 1024;
@@ -51,6 +52,11 @@ export function copyResearchView(source, destination, budget = { files: 0, bytes
51
52
  }
52
53
  }
53
54
 
55
+ /** The helper both the research fence and project-scoped reads need
56
+ * (codexProjectRead.mjs): one floor, so neither runs on a Codex the other
57
+ * refuses. */
58
+ export const CODEX_HELPER_MIN = '0.159.2';
59
+
54
60
  /** Resolve the actual sandbox helper without granting the person's bin/home.
55
61
  * Support native installs and the npm wrapper's platform package. Unknown
56
62
  * installations refuse; granting the whole home to find a helper is no fence.
@@ -147,9 +153,9 @@ export async function runIsolatedResearch(opts, run, {
147
153
  if (process.platform !== 'linux') throw new Error('Isolated Codex research requires Linux or WSL.');
148
154
  const original = realpathSync(opts.cwd);
149
155
  const helper = resolveHelper();
150
- const version = readVersion(helper).match(/codex-cli (\d+)\.(\d+)\.(\d+)/);
151
- if (!version || version.slice(1).map(Number).reduce((n, v) => n * 1000 + v, 0) < 159002)
152
- throw new Error('Isolated research needs Codex CLI 0.159.2 or newer. Restart after updating Codex.');
156
+ const version = readVersion(helper).match(/codex-cli (\d+\.\d+\.\d+)/);
157
+ if (!version || !versionAtLeast(version[1], CODEX_HELPER_MIN))
158
+ throw new Error(`Isolated research needs Codex CLI ${CODEX_HELPER_MIN} or newer. Restart after updating Codex.`);
153
159
  // Both publication parents must be real directories in this checkout, or
154
160
  // absent — never a link, dangling or not (ownedDirs.mjs).
155
161
  for (const dir of [join(original, '.flowviant'), join(original, '.flowviant/artifacts')])
@@ -1,24 +1,28 @@
1
- const SUBAGENT_LIMIT = 32;
2
- const THREAD_ID = /^[a-zA-Z0-9-]{16,64}$/;
1
+ import { CODEX_CHILD_THREAD_RE } from './conversationIds.mjs';
2
+ import { NATIVE_AGENT_TOOLS } from './nativeAgentTools.mjs';
3
+ import { SUBAGENT_CAP } from './subagentBounds.mjs';
4
+
3
5
  const snake = s => String(s ?? '').replace(/([a-z])([A-Z])/g, '$1_$2').toLowerCase();
6
+ // A collab item as exec (`collab_tool_call`) or a rollout (`CollabAgentToolCall`) spells it.
7
+ const isCollabItem = item => ['collab_tool_call', 'CollabAgentToolCall'].includes(item?.type ?? item?.item_type);
4
8
  export const statusOf = (s) => ({ pending_init: 'waiting', running: 'running', completed: 'done',
5
9
  interrupted: 'stopped', errored: 'stopped', shutdown: 'stopped', not_found: 'stopped',
6
10
  waiting: 'waiting', asked: 'asked' })[snake(typeof s === 'string' ? s : s && Object.keys(s)[0])];
7
11
 
8
12
  export function codexSpawnResult(item) {
9
- if (!['collab_tool_call', 'CollabAgentToolCall'].includes(item?.type ?? item?.item_type) || snake(item.tool) !== 'spawn_agent' || !item.id) return null;
13
+ if (!isCollabItem(item) || snake(item.tool) !== 'spawn_agent' || !item.id) return null;
10
14
  if (['failed', 'errored'].includes(snake(item.status))) return { callId: item.id, success: false };
11
15
  const nativeId = item.receiver_thread_ids?.[0];
12
16
  return nativeId ? { callId: item.id, nativeId, success: true } : null;
13
17
  }
14
18
 
15
19
  export function codexCollabUpdates(item) {
16
- if (!['collab_tool_call', 'CollabAgentToolCall'].includes(item?.type ?? item?.item_type)) return [];
20
+ if (!isCollabItem(item)) return [];
17
21
  const tool = snake(item.tool);
18
22
  const states = item.agents_states ?? {};
19
- const ids = [...new Set([...(item.receiver_thread_ids ?? []), ...Object.keys(states)])].slice(0, SUBAGENT_LIMIT);
23
+ const ids = [...new Set([...(item.receiver_thread_ids ?? []), ...Object.keys(states)])].slice(0, SUBAGENT_CAP);
20
24
  return ids.flatMap(id => {
21
- if (typeof id !== 'string' || !THREAD_ID.test(id)) return [];
25
+ if (typeof id !== 'string' || !CODEX_CHILD_THREAD_RE.test(id)) return [];
22
26
  const state = states[id];
23
27
  const receiver = item.receiver_agents?.find(a => a.thread_id === id);
24
28
  const status = statusOf(state?.status ?? state);
@@ -27,7 +31,7 @@ export function codexCollabUpdates(item) {
27
31
  ...(receiver?.agent_path || receiver?.agent_nickname || item.title ? { title: receiver?.agent_path ?? receiver?.agent_nickname ?? item.title } : {}),
28
32
  ...(receiver?.agent_role || item.agent_type ? { kind: receiver?.agent_role ?? item.agent_type } : {}),
29
33
  } : {}),
30
- ...(status ? { status } : ['close_agent', 'interrupt_agent'].includes(tool) ? { status: 'stopped' } : {}),
34
+ ...(status ? { status } : NATIVE_AGENT_TOOLS.codex.stop.includes(tool) ? { status: 'stopped' } : {}),
31
35
  ...(typeof message === 'string' ? { now: message, message } : {}),
32
36
  }];
33
37
  });
@@ -3,15 +3,22 @@
3
3
  * rollouts named by this turn's native spawn receipts, never a global latest.
4
4
  * rust-v0.160.0: exec_events.rs CollabToolCallItem; core/agent/child_config.rs
5
5
  * reapplies the live turn's approval and permission profile after role config.
6
+ * V2 rollout shapes measured on codex-cli 0.163.0 (codexSubagents.test.mjs).
6
7
  */
7
8
  import { open } from 'node:fs/promises';
8
9
  import { codexRolloutFile } from './runtimeLimits.mjs';
10
+ import { CODEX_CHILD_THREAD_RE } from './conversationIds.mjs';
11
+ import { followJsonl } from './jsonlTail.mjs';
9
12
  import { codexCollabUpdates, codexSpawnResult, statusOf } from './codexSubagentEvents.mjs';
13
+ import { NATIVE_AGENT_TOOLS } from './nativeAgentTools.mjs';
14
+ import { SUBAGENT_CAP } from './subagentBounds.mjs';
10
15
 
11
- export const SUBAGENT_LIMIT = 32;
12
- const READ_BYTES = 256 * 1024;
13
- const INITIAL_SCAN = 4 * 1024 * 1024;
14
- const THREAD_ID = /^[a-zA-Z0-9-]{16,64}$/;
16
+ const CODEX = NATIVE_AGENT_TOOLS.codex;
17
+ // A rollout shows a superset of what the gate sees: send_message and the
18
+ // stop tools too.
19
+ const COLLAB_CALLS = [...CODEX.spawn, ...CODEX.followup, ...CODEX.message, ...CODEX.stop];
20
+ // The calls whose output names a child's receipt.
21
+ const RECEIPT_CALLS = [...CODEX.spawn, ...CODEX.followup];
15
22
 
16
23
  export function createCodexSubagentReader({ since = Date.now(), env = process.env, onSubagent, onActivity, onToolEvent, onSpawnResult,
17
24
  findFile = codexRolloutFile } = {}) {
@@ -24,8 +31,9 @@ export function createCodexSubagentReader({ since = Date.now(), env = process.en
24
31
  let stopped = false;
25
32
  let chain = Promise.resolve();
26
33
  const report = update => {
27
- if (!update?.id || !THREAD_ID.test(update.id)) return;
28
- if (!readers.has(update.id) && readers.size >= SUBAGENT_LIMIT + 1) return;
34
+ if (!update?.id || !CODEX_CHILD_THREAD_RE.test(update.id)) return;
35
+ // The root thread plus the run's children.
36
+ if (!readers.has(update.id) && readers.size >= SUBAGENT_CAP + 1) return;
29
37
  if (!readers.has(update.id))
30
38
  readers.set(update.id, { offset: null, rest: Buffer.alloc(0), seen: new Set(), live: false });
31
39
  const { message, ...record } = update;
@@ -35,7 +43,7 @@ export function createCodexSubagentReader({ since = Date.now(), env = process.en
35
43
  targets.set(path, id);
36
44
  if (targets.size > 128) targets.delete(targets.keys().next().value);
37
45
  };
38
- const targetId = value => targets.get(value) ?? (THREAD_ID.test(value ?? '') ? value : null);
46
+ const targetId = value => targets.get(value) ?? (CODEX_CHILD_THREAD_RE.test(value ?? '') ? value : null);
39
47
 
40
48
  function observe(ev, owner) {
41
49
  const at = Date.parse(ev?.timestamp ?? '');
@@ -68,40 +76,52 @@ export function createCodexSubagentReader({ since = Date.now(), env = process.en
68
76
  if (command) step(p.call_id, Array.isArray(command) ? command.join(' ') : command);
69
77
  return;
70
78
  }
71
- if (owner !== root && name === 'request_user_input') {
79
+ if (owner !== root && CODEX.ask.includes(name)) {
72
80
  questions.set(p.call_id, owner);
73
81
  if (questions.size > 128) questions.delete(questions.keys().next().value);
74
82
  report({ id: owner, status: 'asked' }); return;
75
83
  }
76
- if (!['spawn_agent', 'send_input', 'send_message', 'followup_task', 'resume_agent', 'close_agent', 'interrupt_agent'].includes(name)) return;
84
+ if (!COLLAB_CALLS.includes(name)) return;
77
85
  let args;
78
86
  try { args = JSON.parse(p.arguments); } catch { return; }
79
87
  calls.set(p.call_id, { name, args });
80
88
  if (calls.size > 128) calls.delete(calls.keys().next().value);
81
89
  const id = targetId(args.target ?? args.id);
82
90
  if (id && name !== 'spawn_agent') {
83
- const stopped = name === 'close_agent' || name === 'interrupt_agent';
91
+ const stopped = CODEX.stop.includes(name);
84
92
  report({ id, ...(stopped ? { status: 'stopped' } : name === 'followup_task' ? { status: 'running' } : {}) });
85
93
  }
86
94
  }
87
95
  if (ev.type === 'response_item' && p.type === 'function_call_output') {
88
96
  if (questions.has(p.call_id)) { report({ id: questions.get(p.call_id), status: 'running' }); questions.delete(p.call_id); }
89
97
  const call = calls.get(p.call_id);
90
- if (!['spawn_agent', 'followup_task', 'resume_agent', 'send_input'].includes(call?.name)) return;
91
- let output;
98
+ if (!RECEIPT_CALLS.includes(call?.name)) return;
99
+ let output = null;
92
100
  try {
93
101
  const body = Array.isArray(p.output) ? p.output.map(b => b.text ?? '').join('') : p.output;
94
102
  output = typeof body === 'string' ? JSON.parse(body) : body;
95
- } catch { onSpawnResult?.({ callId: p.call_id, success: false }); return; }
96
- const id = output?.agent_id ?? output?.thread_id;
97
- const receipt = id ?? output?.agent_path ?? (call.name !== 'spawn_agent' && (output?.submission_id || output?.status) ? call.args.target ?? call.args.id : null);
98
- onSpawnResult?.({ callId: p.call_id, success: Boolean(receipt), ...(receipt ? { nativeId: receipt } : {}) });
103
+ } catch { /* the tool's own words */ }
104
+ // ONLY AN EXPLICIT FAILURE FREES A SLOT (2026-10-17). These tools answer
105
+ // JSON on success (their output schemas), so plain text is the native
106
+ // error ("collab spawn failed: …"); V2 followup_task declares no output,
107
+ // and a JSON answer naming no receipt is unknown. Neither is a failure.
108
+ if (!output || typeof output !== 'object') {
109
+ if (call.name !== 'followup_task') onSpawnResult?.({ callId: p.call_id, success: false });
110
+ return;
111
+ }
112
+ // V2 (measured on 0.163) answers only the canonical task path; the
113
+ // thread arrived first, on the spawn's SubAgentActivity.
114
+ const path = output.task_name ?? output.agent_path;
115
+ const id = output.agent_id ?? output.thread_id ?? (typeof path === 'string' ? targets.get(path) : undefined);
116
+ const receipt = call.name === 'spawn_agent' ? output.agent_id ?? output.thread_id ?? path
117
+ : output.submission_id || output.status ? call.args.target ?? call.args.id : null;
118
+ if (receipt) onSpawnResult?.({ callId: p.call_id, success: true, nativeId: receipt });
99
119
  if (call.name !== 'spawn_agent') return;
100
120
  if (!id) return;
101
121
  const args = call.args;
102
122
  const title = args.task_name ?? args.description ?? output.agent_nickname ?? '';
103
123
  if (args.task_name) rememberTarget(args.task_name, id);
104
- if (output.agent_path) rememberTarget(output.agent_path, id);
124
+ if (typeof path === 'string') rememberTarget(path, id);
105
125
  report({ id, kind: args.agent_type ?? '', title, brief: args.message ?? '', status: 'running', now: '' });
106
126
  return;
107
127
  }
@@ -112,7 +132,13 @@ export function createCodexSubagentReader({ since = Date.now(), env = process.en
112
132
  if (ev.type !== 'event_msg') return;
113
133
  const activityState = item => {
114
134
  const kind = String(item.kind ?? '').toLowerCase();
115
- if (item.agent_thread_id) report({ id: item.agent_thread_id, title: item.agent_path,
135
+ if (!item.agent_thread_id) return;
136
+ if (item.agent_path) rememberTarget(item.agent_path, item.agent_thread_id);
137
+ // A start carries the spawn's own call id (measured on 0.163): the
138
+ // controller binds the native thread to the record that call reserved.
139
+ const callId = kind === 'started' ? item.call_id ?? item.id : null;
140
+ report({ id: item.agent_thread_id, title: item.agent_path,
141
+ ...(typeof callId === 'string' && callId ? { callId } : {}),
116
142
  ...(['started', 'completed', 'interrupted'].includes(kind) ? {
117
143
  status: kind === 'completed' ? 'done' : kind === 'interrupted' ? 'stopped' : 'running',
118
144
  } : {}),
@@ -163,41 +189,18 @@ export function createCodexSubagentReader({ since = Date.now(), env = process.en
163
189
  if (!file) return;
164
190
  state.file = file;
165
191
  fh = await open(file, 'r');
166
- const { size } = await fh.stat();
167
192
  const initial = state.offset === null;
168
193
  const initialEvents = [];
169
- if (initial) {
170
- state.offset = Math.max(0, size - INITIAL_SCAN);
171
- state.skip = state.offset > 0;
172
- }
173
- if (size < state.offset) { state.offset = 0; state.rest = Buffer.alloc(0); }
174
- // Bounded work per poll, including an immediately completed child.
175
- const end = Math.min(size, state.offset + INITIAL_SCAN);
176
- while (state.offset < end) {
177
- const buffer = Buffer.alloc(Math.min(READ_BYTES, end - state.offset));
178
- const { bytesRead } = await fh.read(buffer, 0, buffer.length, state.offset);
179
- if (!bytesRead) break;
180
- state.offset += bytesRead;
181
- const data = Buffer.concat([state.rest, buffer.subarray(0, bytesRead)]);
182
- let start = 0, nl;
183
- while ((nl = data.indexOf(10, start)) >= 0) {
184
- const line = data.subarray(start, nl).toString('utf8');
185
- start = nl + 1;
186
- if (state.skip) { state.skip = false; continue; }
187
- try {
188
- const ev = JSON.parse(line);
189
- if (initial && id !== root) initialEvents.push(ev);
190
- else if (id === root || state.live || ev.type === 'session_meta') observe(ev, id);
191
- else if (ev.type === 'event_msg' && ['task_started', 'turn_started'].includes(ev.payload?.type)) {
192
- state.live = true; observe(ev, id);
193
- }
194
- } catch { /* non-JSON/unknown native events */ }
195
- }
196
- // Keep bytes across UTF-8 and partial-line boundaries. Oversized lines
197
- // are discarded through their newline rather than reparsed as a suffix.
198
- state.rest = data.subarray(start);
199
- if (state.rest.length > READ_BYTES) { state.rest = Buffer.alloc(0); state.skip = true; }
200
- }
194
+ await followJsonl(fh, state, line => {
195
+ try {
196
+ const ev = JSON.parse(line);
197
+ if (initial && id !== root) initialEvents.push(ev);
198
+ else if (id === root || state.live || ev.type === 'session_meta') observe(ev, id);
199
+ else if (ev.type === 'event_msg' && ['task_started', 'turn_started'].includes(ev.payload?.type)) {
200
+ state.live = true; observe(ev, id);
201
+ }
202
+ } catch { /* non-JSON/unknown native events */ }
203
+ });
201
204
  if (initial && id !== root) {
202
205
  // A fork copies the parent's old EventMsgs. Only the child's newest
203
206
  // native turn belongs to this card; inherited history is never replayed.
@@ -219,7 +222,7 @@ export function createCodexSubagentReader({ since = Date.now(), env = process.en
219
222
  };
220
223
  return {
221
224
  thread(id) {
222
- if (!THREAD_ID.test(id ?? '') || root) return;
225
+ if (!CODEX_CHILD_THREAD_RE.test(id ?? '') || root) return;
223
226
  root = id;
224
227
  readers.set(id, { offset: null, rest: Buffer.alloc(0), seen: new Set(), live: false });
225
228
  if (!stopped) { timer = setInterval(() => void flush(), 1_000); timer.unref?.(); }
@@ -0,0 +1,328 @@
1
+ /**
2
+ * CODEX'S config.toml SYNTAX, IN ONE IMPORT-FREE HOME (2026-10-18, the SOLID
3
+ * pass): the bounded reader, the value writer and the key quoting every Codex
4
+ * module writes with. Split out because codexPersonal.mjs (the login carry)
5
+ * took its string quoter `toml` from projectToolRuntimes.mjs (the runtime
6
+ * table), which imported codexPersonal.mjs back for the reader, and
7
+ * codexFencedMount.mjs imported both — a three-module cycle that loaded only
8
+ * because every use across it sat inside a function. MEASURED on scratch
9
+ * copies: one top-level `toml(...)` in codexPersonal.mjs made importing
10
+ * projectToolRuntimes.mjs throw "Cannot access 'toml' before
11
+ * initialization", so which module a caller imported first decided whether
12
+ * the daemon started; and the uplink scrubber, which wants only the provider
13
+ * secrets, loaded the runtime tool table and the fenced mount through it. The
14
+ * one bare-key quoter had two copies (`bareOr`, `bareKeyOr`); it is `bareKey`.
15
+ * The string quoter had a third, orchestratorHooks.mjs's `quote` for the
16
+ * Codex hook rows' `-c` values; it is `toml`.
17
+ *
18
+ * A LEAF: no imports. codexPersonal.mjs, codexFencedMount.mjs and
19
+ * projectToolRuntimes.mjs import it, never each other for it
20
+ * (codexToml.test.mjs).
21
+ */
22
+
23
+ export const toml = (value) => JSON.stringify(String(value));
24
+
25
+ /** A table header's dotted keys, unquoted — null for one this reader cannot
26
+ * split, which is then nobody's to strip. */
27
+ export function headerKeys(section) {
28
+ const key = /\s*(?:"((?:[^"\\]|\\.)*)"|'([^']*)'|([A-Za-z0-9_-]+))\s*(?:\.|$)/y;
29
+ const keys = [];
30
+ for (let i = 0; i < section.length; ) {
31
+ key.lastIndex = i;
32
+ const m = key.exec(section);
33
+ if (!m || key.lastIndex === i) return null;
34
+ try {
35
+ keys.push(m[1] !== undefined ? JSON.parse(`"${m[1]}"`) : m[2] ?? m[3]);
36
+ } catch {
37
+ return null;
38
+ }
39
+ i = key.lastIndex;
40
+ }
41
+ return keys;
42
+ }
43
+
44
+ /** The dotted keys a `key = …` / `key.sub = …` line defines, unquoted, or
45
+ * null for a line that defines none this reader can split. */
46
+ export function lineKeys(line) {
47
+ const key = /\s*(?:"((?:[^"\\]|\\.)*)"|'([^']*)'|([A-Za-z0-9_-]+))\s*([.=])/y;
48
+ const keys = [];
49
+ for (let i = 0; ; ) {
50
+ key.lastIndex = i;
51
+ const m = key.exec(line);
52
+ if (!m) return null;
53
+ try {
54
+ keys.push(m[1] !== undefined ? JSON.parse(`"${m[1]}"`) : m[2] ?? m[3]);
55
+ } catch {
56
+ return null;
57
+ }
58
+ if (m[4] === '=') return keys;
59
+ i = key.lastIndex;
60
+ }
61
+ }
62
+
63
+ /** Arrays and inline tables nest no deeper than this in a file we read. */
64
+ const MAX_DEPTH = 32;
65
+
66
+ /** A number, date or time, kept as written so it is carried as written. */
67
+ export class TomlRaw {
68
+ constructor(text) {
69
+ this.text = text;
70
+ }
71
+ }
72
+ export const newTable = () => Object.create(null);
73
+ export const isTable = (v) => v !== null && typeof v === 'object' && !Array.isArray(v) && !(v instanceof TomlRaw);
74
+
75
+ const BARE = /[A-Za-z0-9_-]+/y;
76
+ // First match wins, so the forms that begin like an integer come first.
77
+ const SCALAR =
78
+ /[+-]?(?:inf|nan)|0x[0-9A-Fa-f](?:_?[0-9A-Fa-f])*|0o[0-7](?:_?[0-7])*|0b[01](?:_?[01])*|\d{4}-\d{2}-\d{2}(?:[Tt ]\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:[Zz]|[+-]\d{2}:\d{2})?)?|\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?|[+-]?\d(?:_?\d)*(?:\.\d(?:_?\d)*)?(?:[eE][+-]?\d(?:_?\d)*)?/y;
79
+
80
+ /**
81
+ * A BOUNDED TOML READER — the repo carries no TOML library, and the one file
82
+ * it reads is Codex's own config. Whole documents: tables, arrays of tables
83
+ * (the daemon's own `[[skills.config]]` in an agent's home), dotted and quoted
84
+ * keys, the four string forms, arrays, inline tables, booleans, and numbers,
85
+ * dates and times kept as written. Anything else throws, and the caller
86
+ * carries nothing — a partial read is never a license to guess.
87
+ */
88
+ export function parseCodexToml(text) {
89
+ const src = String(text).replace(/^/, '');
90
+ const n = src.length;
91
+ let i = 0;
92
+ const fail = (why) => {
93
+ throw new Error(`config.toml: ${why} at offset ${i}`);
94
+ };
95
+ const ws = () => {
96
+ while (i < n && (src[i] === ' ' || src[i] === '\t')) i++;
97
+ };
98
+ const comment = () => {
99
+ if (src[i] === '#') while (i < n && src[i] !== '\n') i++;
100
+ };
101
+ const newline = () => {
102
+ if (src[i] === '\n') return (i += 1), true;
103
+ if (src[i] === '\r' && src[i + 1] === '\n') return (i += 2), true;
104
+ return false;
105
+ };
106
+ /** Blank lines, comments and whitespace, as between array items. */
107
+ const gap = () => {
108
+ for (;;) {
109
+ ws();
110
+ comment();
111
+ if (!newline()) return;
112
+ }
113
+ };
114
+ const endOfLine = () => {
115
+ ws();
116
+ comment();
117
+ if (i < n && !newline()) fail('expected the end of the line');
118
+ };
119
+ const hexChar = (len) => {
120
+ const h = src.slice(i, i + len);
121
+ if (h.length !== len || !/^[0-9A-Fa-f]+$/.test(h)) fail('bad escape');
122
+ i += len;
123
+ const cp = parseInt(h, 16);
124
+ if (cp > 0x10ffff || (cp >= 0xd800 && cp <= 0xdfff)) fail('bad escape');
125
+ return String.fromCodePoint(cp);
126
+ };
127
+ const escape = () => {
128
+ const e = src[i + 1];
129
+ i += 2;
130
+ switch (e) {
131
+ case 'b': return '\b';
132
+ case 't': return '\t';
133
+ case 'n': return '\n';
134
+ case 'f': return '\f';
135
+ case 'r': return '\r';
136
+ case 'e': return '\x1b';
137
+ case '"': return '"';
138
+ case '\\': return '\\';
139
+ case 'x': return hexChar(2);
140
+ case 'u': return hexChar(4);
141
+ case 'U': return hexChar(8);
142
+ default: return fail('bad escape');
143
+ }
144
+ };
145
+ const basicString = () => {
146
+ i += 1;
147
+ let s = '';
148
+ for (;;) {
149
+ if (i >= n) fail('unterminated string');
150
+ const ch = src[i];
151
+ if (ch === '"') return (i += 1), s;
152
+ if (ch === '\n' || ch === '\r') fail('newline in a string');
153
+ if (ch === '\\') s += escape();
154
+ else (s += ch), (i += 1);
155
+ }
156
+ };
157
+ const literalString = () => {
158
+ const end = src.indexOf("'", i + 1);
159
+ if (end < 0) fail('unterminated string');
160
+ const s = src.slice(i + 1, end);
161
+ if (/[\r\n]/.test(s)) fail('newline in a string');
162
+ i = end + 1;
163
+ return s;
164
+ };
165
+ const LINE_END_BACKSLASH = /\\[ \t]*\r?\n/y;
166
+ const multiline = (quote, escapes) => {
167
+ i += 3;
168
+ newline(); // a newline straight after the opening quotes is not content
169
+ let s = '';
170
+ for (;;) {
171
+ if (i >= n) fail('unterminated string');
172
+ if (src.startsWith(quote.repeat(3), i)) {
173
+ let q = 3;
174
+ while (q < 5 && src[i + q] === quote) q++; // up to two belong to the content
175
+ s += quote.repeat(q - 3);
176
+ i += q;
177
+ return s;
178
+ }
179
+ if (escapes && src[i] === '\\') {
180
+ LINE_END_BACKSLASH.lastIndex = i;
181
+ if (LINE_END_BACKSLASH.test(src)) {
182
+ i = LINE_END_BACKSLASH.lastIndex;
183
+ while (i < n && /[ \t\r\n]/.test(src[i])) i++;
184
+ } else s += escape();
185
+ } else (s += src[i]), (i += 1);
186
+ }
187
+ };
188
+ const key = () => {
189
+ const parts = [];
190
+ for (;;) {
191
+ ws();
192
+ if (src[i] === '"') {
193
+ if (src.startsWith('"""', i)) fail('a multi-line key');
194
+ parts.push(basicString());
195
+ } else if (src[i] === "'") {
196
+ if (src.startsWith("'''", i)) fail('a multi-line key');
197
+ parts.push(literalString());
198
+ } else {
199
+ BARE.lastIndex = i;
200
+ const m = BARE.exec(src);
201
+ if (!m) fail('expected a key');
202
+ parts.push(m[0]);
203
+ i = BARE.lastIndex;
204
+ }
205
+ ws();
206
+ if (src[i] !== '.') return parts;
207
+ i += 1;
208
+ }
209
+ };
210
+ const assign = (target, path, v) => {
211
+ let t = target;
212
+ for (const k of path.slice(0, -1)) {
213
+ if (!Object.hasOwn(t, k)) t[k] = newTable();
214
+ else if (!isTable(t[k])) fail(`"${k}" is not a table`);
215
+ t = t[k];
216
+ }
217
+ const last = path.at(-1);
218
+ if (Object.hasOwn(t, last)) fail(`"${last}" is defined twice`);
219
+ t[last] = v;
220
+ };
221
+ let depth = 0;
222
+ const nested = (open) => {
223
+ if (++depth > MAX_DEPTH) fail('nested too deep');
224
+ try {
225
+ return open();
226
+ } finally {
227
+ depth -= 1;
228
+ }
229
+ };
230
+ const array = () => {
231
+ i += 1;
232
+ const out = [];
233
+ for (;;) {
234
+ gap();
235
+ if (src[i] === ']') return (i += 1), out;
236
+ out.push(value());
237
+ gap();
238
+ if (src[i] === ',') i += 1;
239
+ else if (src[i] === ']') return (i += 1), out;
240
+ else fail('expected , or ]');
241
+ }
242
+ };
243
+ const inlineTable = () => {
244
+ i += 1;
245
+ const t = newTable();
246
+ for (;;) {
247
+ gap();
248
+ if (src[i] === '}') return (i += 1), t;
249
+ keyValue(t);
250
+ gap();
251
+ if (src[i] === ',') i += 1;
252
+ else if (src[i] === '}') return (i += 1), t;
253
+ else fail('expected , or }');
254
+ }
255
+ };
256
+ const word = (w) => src.startsWith(w, i) && !/[A-Za-z0-9_-]/.test(src[i + w.length] ?? '');
257
+ const value = () => {
258
+ const ch = src[i];
259
+ if (ch === '"') return src.startsWith('"""', i) ? multiline('"', true) : basicString();
260
+ if (ch === "'") return src.startsWith("'''", i) ? multiline("'", false) : literalString();
261
+ if (ch === '[') return nested(array);
262
+ if (ch === '{') return nested(inlineTable);
263
+ if (word('true')) return (i += 4), true;
264
+ if (word('false')) return (i += 5), false;
265
+ SCALAR.lastIndex = i;
266
+ const m = SCALAR.exec(src);
267
+ if (!m) fail('a value this reader does not know');
268
+ i = SCALAR.lastIndex;
269
+ if (/[A-Za-z0-9_.:+-]/.test(src[i] ?? '')) fail('a value this reader does not know');
270
+ return new TomlRaw(m[0]);
271
+ };
272
+ const keyValue = (target) => {
273
+ const path = key();
274
+ if (src[i] !== '=') fail('expected =');
275
+ i += 1;
276
+ ws();
277
+ assign(target, path, value());
278
+ };
279
+
280
+ const root = newTable();
281
+ let current = root;
282
+ for (;;) {
283
+ gap();
284
+ if (i >= n) return root;
285
+ if (src[i] !== '[') {
286
+ keyValue(current);
287
+ endOfLine();
288
+ continue;
289
+ }
290
+ const many = src[i + 1] === '[';
291
+ i += many ? 2 : 1;
292
+ const path = key();
293
+ if (many ? !src.startsWith(']]', i) : src[i] !== ']') fail('an unclosed table header');
294
+ i += many ? 2 : 1;
295
+ endOfLine();
296
+ let t = root;
297
+ for (const k of path.slice(0, -1)) {
298
+ if (!Object.hasOwn(t, k)) t[k] = newTable();
299
+ const v = Array.isArray(t[k]) ? t[k].at(-1) : t[k];
300
+ if (!isTable(v)) fail(`"${k}" is not a table`);
301
+ t = v;
302
+ }
303
+ const last = path.at(-1);
304
+ if (many) {
305
+ if (!Object.hasOwn(t, last)) t[last] = [];
306
+ if (!Array.isArray(t[last])) fail(`"${last}" is not an array of tables`);
307
+ current = newTable();
308
+ t[last].push(current);
309
+ } else {
310
+ if (!Object.hasOwn(t, last)) t[last] = newTable();
311
+ else if (!isTable(t[last])) fail(`"${last}" is not a table`);
312
+ current = t[last];
313
+ }
314
+ }
315
+ }
316
+
317
+ /** A key as TOML writes it: bare where it can be, else quoted. */
318
+ export const bareKey = (k) => (/^[A-Za-z0-9_-]+$/.test(k) ? k : toml(k));
319
+
320
+ /** One value as TOML, as a `-c` override's value is parsed. */
321
+ export function tomlValue(v) {
322
+ if (typeof v === 'string') return toml(v);
323
+ if (typeof v === 'boolean') return String(v);
324
+ if (v instanceof TomlRaw) return v.text;
325
+ if (Array.isArray(v)) return `[${v.map(tomlValue).join(',')}]`;
326
+ if (isTable(v)) return `{${Object.keys(v).map((k) => `${bareKey(k)}=${tomlValue(v[k])}`).join(',')}}`;
327
+ throw new Error('not a TOML value');
328
+ }
@@ -35,6 +35,16 @@ export const CLAUDE_SESSION_RE = /^[A-Za-z0-9_-]{8,64}$/;
35
35
  */
36
36
  export const CODEX_THREAD_RE = /^[0-9a-zA-Z][0-9a-zA-Z_-]{7,63}$/;
37
37
 
38
+ /**
39
+ * A Codex native CHILD's thread id as the subagent lane follows it
40
+ * (codexSubagentEvents.mjs, codexSubagents.mjs). Its measured value,
41
+ * unchanged when its two copies moved here (2026-10-18). The rollout lookup
42
+ * it then reaches still guards with CODEX_THREAD_RE (runtimeLimits.mjs
43
+ * codexRolloutFile). Making the two one shape is a behaviour change for the
44
+ * owner to rule on.
45
+ */
46
+ export const CODEX_CHILD_THREAD_RE = /^[a-zA-Z0-9-]{16,64}$/;
47
+
38
48
  /** agy conversation ids are plain UUIDs (the db filename IS the identity —
39
49
  * measured: a renamed copy fails "trajectory not found"). Guarded the same
40
50
  * way as the codex id: it rides in argv as `--conversation <id>`. */
package/bin/lib/fleet.mjs CHANGED
@@ -38,7 +38,7 @@ import { compareVersions } from './versionOrder.mjs';
38
38
  import { emitMachineEvent } from './daemonLogging.mjs';
39
39
  import { writeDaemonState } from './daemonState.mjs';
40
40
  import { maintenanceTick, readMaintenance } from './maintenance.mjs';
41
- import { repoRootOrDie, detectBaseRef, baseBranchName, excludeInWorktree, usableBaseRef } from './git.mjs';
41
+ import { repoRootOrDie, detectBaseRef, excludeInWorktree, baseRefFor } from './git.mjs';
42
42
  import { c, info, note, ok, warn, fail } from './ui.mjs';
43
43
  import { sleep } from './sleep.mjs';
44
44
  import { reapOrphanPreviews } from './previewRegistry.mjs';
@@ -576,7 +576,7 @@ export async function runFleetDaemon({ afterLock = null } = {}) {
576
576
  const servedBaseBranch = Array.isArray(roster.repositories)
577
577
  ? repositories.list().find((r) => r.repoRoot === repoRoot)?.baseBranch : roster.baseBranch;
578
578
  if (typeof servedBaseBranch === 'string' && servedBaseBranch.trim()) {
579
- const want = usableBaseRef(repoRoot, `origin/${baseBranchName(servedBaseBranch.trim())}`);
579
+ const want = baseRefFor(repoRoot, servedBaseBranch.trim());
580
580
  if (want !== baseRef) {
581
581
  note(`base · ${want} ${c.dim('(set for this project)')}`);
582
582
  baseRef = want;