flowviant 0.125.4 → 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.
- package/bin/lib/agentInternalWait.mjs +1 -5
- package/bin/lib/agentPlan.mjs +13 -163
- package/bin/lib/agentRepositories.mjs +3 -1
- package/bin/lib/claudeInput.mjs +28 -9
- package/bin/lib/claudeMachineMcp.mjs +85 -0
- package/bin/lib/claudePersonal.mjs +6 -6
- package/bin/lib/claudePosture.mjs +6 -15
- package/bin/lib/claudeSettingsArg.mjs +67 -0
- package/bin/lib/claudeSubagentProgress.mjs +12 -28
- package/bin/lib/claudeSubagents.mjs +4 -2
- package/bin/lib/codexFencedMount.mjs +275 -0
- package/bin/lib/codexPersonal.mjs +1 -265
- package/bin/lib/codexProjectRead.mjs +5 -4
- package/bin/lib/codexResearch.mjs +9 -3
- package/bin/lib/codexSubagentEvents.mjs +11 -7
- package/bin/lib/codexSubagents.mjs +55 -52
- package/bin/lib/codexToml.mjs +328 -0
- package/bin/lib/conversationIds.mjs +10 -0
- package/bin/lib/fleet.mjs +2 -2
- package/bin/lib/git.mjs +32 -0
- package/bin/lib/heldTool.mjs +49 -0
- package/bin/lib/hooks/mcpAllow.mjs +11 -5
- package/bin/lib/hooks/orchestrator.mjs +20 -10
- package/bin/lib/intakeDraft.mjs +1 -1
- package/bin/lib/jsonlTail.mjs +52 -0
- package/bin/lib/mcpInventory.mjs +9 -5
- package/bin/lib/mcpReasons.mjs +20 -0
- package/bin/lib/mcpReportBounds.mjs +10 -0
- package/bin/lib/mcpStdio.mjs +35 -0
- package/bin/lib/mergeBatch.mjs +6 -24
- package/bin/lib/mergeOrder.mjs +18 -0
- package/bin/lib/modelJson.mjs +173 -0
- package/bin/lib/nativeAgentTools.mjs +72 -0
- package/bin/lib/orchestrator.mjs +212 -133
- package/bin/lib/orchestratorClient.mjs +17 -21
- package/bin/lib/orchestratorHooks.mjs +21 -4
- package/bin/lib/orchestratorNative.mjs +58 -0
- package/bin/lib/orchestratorPrompt.mjs +7 -4
- package/bin/lib/orchestratorProtocol.mjs +25 -3
- package/bin/lib/postureTools.mjs +58 -13
- package/bin/lib/prWorkflow.mjs +13 -2
- package/bin/lib/projectRepositories.mjs +2 -2
- package/bin/lib/projectToolRuntimes.mjs +92 -245
- package/bin/lib/projectTools.mjs +59 -19
- package/bin/lib/repositoryDelivery.mjs +6 -4
- package/bin/lib/repositoryMerge.mjs +8 -12
- package/bin/lib/repositoryPlaces.mjs +2 -2
- package/bin/lib/repositoryPrompts.mjs +42 -0
- package/bin/lib/repositoryTool.mjs +9 -32
- package/bin/lib/researchUse.mjs +39 -0
- package/bin/lib/resumeMarker.mjs +43 -0
- package/bin/lib/runTurn.mjs +38 -52
- package/bin/lib/runtimeAntigravity.mjs +1 -1
- package/bin/lib/runtimeCapabilities.mjs +6 -16
- package/bin/lib/runtimeClaude.mjs +62 -19
- package/bin/lib/runtimeCodex.mjs +9 -6
- package/bin/lib/runtimeEvents.mjs +13 -2
- package/bin/lib/runtimeOfferings.mjs +9 -4
- package/bin/lib/runtimes.mjs +3 -1
- package/bin/lib/subagentBounds.mjs +20 -0
- package/bin/lib/trace.mjs +30 -5
- package/bin/lib/turnDeclaration.mjs +1 -1
- package/bin/lib/turnDiagnostics.mjs +9 -12
- package/bin/lib/turnReceipts.mjs +64 -0
- package/bin/lib/turnSubagents.mjs +91 -0
- package/bin/lib/turnUsage.mjs +9 -6
- package/bin/lib/versionOrder.mjs +11 -1
- package/bin/lib/work.mjs +6 -6
- package/bin/lib/workAgentMerges.mjs +5 -7
- package/bin/lib/workAgentPrecheck.mjs +5 -5
- package/bin/lib/workAgentRecovery.mjs +2 -1
- package/bin/lib/workAgentResume.mjs +121 -0
- package/bin/lib/workAgentTools.mjs +111 -0
- package/bin/lib/workAgentTurnExecution.mjs +62 -246
- package/bin/lib/workAgentTurnOutcome.mjs +30 -3
- package/bin/lib/workAgentTurnShape.mjs +90 -0
- package/bin/lib/workAgentTurns.mjs +24 -10
- package/bin/lib/workArtifactReconcile.mjs +3 -2
- package/bin/lib/workMergeBatch.mjs +7 -5
- package/bin/lib/workPlaceReports.mjs +3 -3
- package/bin/lib/workPlaces.mjs +14 -0
- package/bin/lib/workSessionRuntime.mjs +2 -5
- package/bin/lib/workSessionTurns.mjs +12 -63
- package/bin/lib/workShip.mjs +8 -18
- package/bin/lib/workTurnShape.mjs +6 -4
- package/package.json +1 -1
|
@@ -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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const
|
|
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 || !
|
|
28
|
-
|
|
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) ?? (
|
|
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
|
|
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 (!
|
|
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
|
|
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 (!
|
|
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 {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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 (
|
|
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)
|
|
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
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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 (!
|
|
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,
|
|
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 =
|
|
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;
|
package/bin/lib/git.mjs
CHANGED
|
@@ -333,6 +333,38 @@ export function usableBaseRef(repoRoot, ref) {
|
|
|
333
333
|
return ref;
|
|
334
334
|
}
|
|
335
335
|
|
|
336
|
+
/**
|
|
337
|
+
* The BASE REF a repository lands on: its configured branch when a human set
|
|
338
|
+
* one (`origin/<branch>`, or the local branch where there is no origin), else
|
|
339
|
+
* the detected one. One home (2026-10-18, the SOLID pass); was written in
|
|
340
|
+
* repositoryMerge, mergeBatch, projectRepositories and fleet, so a change to
|
|
341
|
+
* how a configured base resolves needed four edits that had to agree.
|
|
342
|
+
*/
|
|
343
|
+
export function baseRefFor(repoRoot, baseBranch) {
|
|
344
|
+
return baseBranch ? usableBaseRef(repoRoot, `origin/${baseBranchName(baseBranch)}`) : detectBaseRef(repoRoot);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Is `a` an ancestor of `b`? A failed probe (an unknown commit included) is
|
|
349
|
+
* no. One home (2026-10-18, the SOLID pass); was a closure in repositoryMerge,
|
|
350
|
+
* mergeBatch and repositoryDelivery.
|
|
351
|
+
*/
|
|
352
|
+
export function isAncestor(a, b, cwd) {
|
|
353
|
+
try { git(['merge-base', '--is-ancestor', a, b], cwd); return true; } catch { return false; }
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* A repository's base, MEASURED NOW: fetch origin when there is one, then the
|
|
358
|
+
* base ref and the commit it names. One home (2026-10-18, the SOLID pass);
|
|
359
|
+
* was a `fetch` closure in repositoryMerge and mergeBatch. `net` is the
|
|
360
|
+
* caller's (its tests inject one).
|
|
361
|
+
*/
|
|
362
|
+
export function measureBase(repoRoot, baseBranch, net = gitNet) {
|
|
363
|
+
if (gitOrNull(['remote', 'get-url', 'origin'], repoRoot)) net(['fetch', 'origin', '--quiet'], repoRoot, 60_000);
|
|
364
|
+
const baseRef = baseRefFor(repoRoot, baseBranch);
|
|
365
|
+
return { baseRef, baseTip: git(['rev-parse', baseRef], repoRoot) };
|
|
366
|
+
}
|
|
367
|
+
|
|
336
368
|
/** True when the repository has at least one commit (HEAD resolves). */
|
|
337
369
|
export function hasCommits(repoRoot) {
|
|
338
370
|
try { git(['rev-parse', '--verify', '--quiet', 'HEAD^{commit}'], repoRoot); return true; } catch { return false; }
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A HELD TURN'S LOOPBACK TOOL, DAEMON SIDE (2026-10-18, the SOLID pass): the
|
|
3
|
+
* run-scoped capability, its one gate, the loopback listen, the env pair and
|
|
4
|
+
* the argv that starts the tool's child.
|
|
5
|
+
*
|
|
6
|
+
* A held turn's CLI never receives the machine token. A card tool it may
|
|
7
|
+
* call is a server on 127.0.0.1 that answers only a POST to `/` bearing a
|
|
8
|
+
* capability minted for this one run, and a bare 403 to anything else. That
|
|
9
|
+
* gate is a security boundary, and it was written out twice, two days apart
|
|
10
|
+
* (repositoryTool.mjs 2026-10-14, orchestrator.mjs 2026-10-15), with the
|
|
11
|
+
* listen, the env pair and the self-argv beside it. Each tool keeps its own
|
|
12
|
+
* body reader, limits, refusals and close order; the child half is
|
|
13
|
+
* mcpStdio.mjs plus each tool's own client.
|
|
14
|
+
*
|
|
15
|
+
* `heldToolArgv` keeps the `import.meta.url` form: launchCommand.mjs's
|
|
16
|
+
* `selfArgv` reads process.argv[1], which under the suite is the test file.
|
|
17
|
+
*/
|
|
18
|
+
import { createServer } from 'node:http';
|
|
19
|
+
import { randomBytes } from 'node:crypto';
|
|
20
|
+
import { fileURLToPath } from 'node:url';
|
|
21
|
+
import { sameSecret } from './grant.mjs';
|
|
22
|
+
import { runningCompiledBinary } from './launchCommand.mjs';
|
|
23
|
+
|
|
24
|
+
/** One run's capability: the only credential the tool's child ever holds. */
|
|
25
|
+
export const mintHeldCapability = () => randomBytes(32).toString('hex');
|
|
26
|
+
|
|
27
|
+
/** The argv that starts this flowviant again as `verb`, under node or the compiled binary. */
|
|
28
|
+
export function heldToolArgv(verb) {
|
|
29
|
+
return runningCompiledBinary() ? [verb] : [fileURLToPath(new URL('../cli.mjs', import.meta.url)), verb];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Listen on a loopback port for one held tool. `respond(req, res)` answers
|
|
34
|
+
* only a request that passed the gate; `isClosed()` shuts the gate before the
|
|
35
|
+
* server stops. Returns the server (the caller closes it, in its own order),
|
|
36
|
+
* the capability, `<envPrefix>_URL`/`_CAPABILITY`, and the child's argv.
|
|
37
|
+
*/
|
|
38
|
+
export async function listenHeldTool({ envPrefix, verb, requestTimeoutMs, capability = mintHeldCapability(), isClosed, respond }) {
|
|
39
|
+
const server = createServer(async (req, res) => {
|
|
40
|
+
if (isClosed() || req.method !== 'POST' || req.url !== '/' || !sameSecret(req.headers.authorization, `Bearer ${capability}`)) {
|
|
41
|
+
res.writeHead(403).end(); return;
|
|
42
|
+
}
|
|
43
|
+
await respond(req, res);
|
|
44
|
+
});
|
|
45
|
+
server.requestTimeout = requestTimeoutMs;
|
|
46
|
+
await new Promise((resolve, reject) => { server.once('error', reject); server.listen(0, '127.0.0.1', resolve); });
|
|
47
|
+
const env = { [`${envPrefix}_URL`]: `http://127.0.0.1:${server.address().port}/`, [`${envPrefix}_CAPABILITY`]: capability };
|
|
48
|
+
return { server, capability, env, args: heldToolArgv(verb) };
|
|
49
|
+
}
|