flowviant 0.125.5 → 0.125.7
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 +4 -5
- 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 +89 -10
- package/bin/lib/claudeMachineMcp.mjs +85 -0
- package/bin/lib/claudePersonal.mjs +6 -57
- package/bin/lib/claudePosture.mjs +6 -15
- package/bin/lib/claudeSettingsArg.mjs +67 -0
- package/bin/lib/claudeStream.mjs +30 -1
- package/bin/lib/claudeSubagentProgress.mjs +12 -28
- package/bin/lib/claudeSubagents.mjs +4 -2
- package/bin/lib/codexContract.mjs +52 -0
- 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/config.mjs +22 -1
- 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 +11 -6
- package/bin/lib/mcpReasons.mjs +26 -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 +221 -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/processGroup.mjs +41 -0
- package/bin/lib/projectRepositories.mjs +2 -2
- package/bin/lib/projectToolRuntimes.mjs +92 -245
- package/bin/lib/projectTools.mjs +60 -19
- package/bin/lib/prompts.mjs +47 -18
- package/bin/lib/reportJournal.mjs +61 -0
- 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/resumeLost.mjs +21 -1
- package/bin/lib/resumeMarker.mjs +43 -0
- package/bin/lib/runTurn.mjs +55 -52
- package/bin/lib/runtimeAntigravity.mjs +1 -1
- package/bin/lib/runtimeCapabilities.mjs +13 -18
- package/bin/lib/runtimeClaude.mjs +68 -24
- package/bin/lib/runtimeCodex.mjs +12 -6
- package/bin/lib/runtimeEvents.mjs +13 -2
- package/bin/lib/runtimeOfferings.mjs +12 -5
- package/bin/lib/runtimes.mjs +3 -1
- package/bin/lib/startProject.mjs +10 -0
- 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/update.mjs +18 -4
- package/bin/lib/versionOrder.mjs +11 -1
- package/bin/lib/work.mjs +9 -9
- 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 +82 -246
- package/bin/lib/workAgentTurnOutcome.mjs +30 -3
- package/bin/lib/workAgentTurnReports.mjs +16 -0
- package/bin/lib/workAgentTurnShape.mjs +90 -0
- package/bin/lib/workAgentTurns.mjs +28 -12
- package/bin/lib/workArtifactReconcile.mjs +3 -2
- package/bin/lib/workAttachments.mjs +27 -5
- package/bin/lib/workMergeBatch.mjs +7 -5
- package/bin/lib/workNarration.mjs +32 -0
- package/bin/lib/workPlaceReports.mjs +3 -3
- package/bin/lib/workPlaces.mjs +14 -0
- package/bin/lib/workReportQueue.mjs +18 -1
- package/bin/lib/workSessionRuntime.mjs +2 -5
- package/bin/lib/workSessionTurns.mjs +68 -80
- package/bin/lib/workShip.mjs +8 -18
- package/bin/lib/workTurnShape.mjs +6 -4
- package/bin/lib/worktreeDiff.mjs +29 -5
- package/package.json +1 -1
|
@@ -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
|
+
}
|
package/bin/lib/config.mjs
CHANGED
|
@@ -226,7 +226,28 @@ export const USER_AGENT = `flowviant/${VERSION}`;
|
|
|
226
226
|
* back. Sent on the poll as `di`; its ABSENCE is what an older daemon looks
|
|
227
227
|
* like, and the server hands preview work to nobody who cannot name themselves.
|
|
228
228
|
*/
|
|
229
|
-
export const DAEMON_INSTANCE = randomBytes(12).toString('hex');
|
|
229
|
+
export const DAEMON_INSTANCE = inheritedInstance() ?? randomBytes(12).toString('hex');
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* …EXCEPT ACROSS ITS OWN SELF-UPDATE (2026-10-19). A self-update re-execs
|
|
233
|
+
* only when the machine is idle (update.mjs), so the process it replaces
|
|
234
|
+
* holds nothing in flight — only its tabs' session leases, which no other
|
|
235
|
+
* daemon may take for 45 seconds. A successor with a new id waited out every
|
|
236
|
+
* one of them, so each unattended update left every tab unanswered for most
|
|
237
|
+
* of a minute. The successor named by `FLOWVIANT_REEXEC` takes the id its
|
|
238
|
+
* predecessor handed it; any other start still regenerates it.
|
|
239
|
+
*/
|
|
240
|
+
function inheritedInstance() {
|
|
241
|
+
const handed = process.env.FLOWVIANT_DAEMON_INSTANCE;
|
|
242
|
+
// TAKEN ONCE, THEN GONE FROM THIS PROCESS'S ENV. Every CLI turn spawns with
|
|
243
|
+
// `{ ...process.env }`, so a variable left here reached any `flowviant`
|
|
244
|
+
// started inside one — an agent running the daemon from a worktree — and
|
|
245
|
+
// two live processes would answer to one instance (the trap instance.mjs
|
|
246
|
+
// names for FLOWVIANT_REEXEC_FROM). `reexec` sets it afresh for its own
|
|
247
|
+
// successor, so the next update still hands it on.
|
|
248
|
+
delete process.env.FLOWVIANT_DAEMON_INSTANCE;
|
|
249
|
+
return process.env.FLOWVIANT_REEXEC === '1' && typeof handed === 'string' && /^[0-9a-f]{24}$/.test(handed) ? handed : null;
|
|
250
|
+
}
|
|
230
251
|
|
|
231
252
|
/**
|
|
232
253
|
* WHICH BOX, in words a person recognises — and nothing else.
|
|
@@ -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
|
+
}
|
|
@@ -17,10 +17,17 @@
|
|
|
17
17
|
* `touch`, an outside Write and WebFetch were refused exactly as without the
|
|
18
18
|
* hook.
|
|
19
19
|
*
|
|
20
|
-
* It decides MCP calls and nothing else: any other tool, and
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
20
|
+
* It decides MCP calls and nothing else: any other tool gets no decision, and
|
|
21
|
+
* the posture's list decides. A hook that fails gives no decision either, so a
|
|
22
|
+
* broken hook leaves MCP calls refused — closed.
|
|
23
|
+
*
|
|
24
|
+
* `mcp__flowviant` IS ONE OF THEM (2026-10-17). Only a card turn carries this
|
|
25
|
+
* hook, and a card turn mints no Flowviant server of its own, so a server of
|
|
26
|
+
* that name there is the person's own: since 0.125.5 their user and local
|
|
27
|
+
* servers ride the card (claudeMachineMcp.mjs `machineMcpServers`), and the
|
|
28
|
+
* owner keeps a local `flowviant` server on purpose — "its my way of having
|
|
29
|
+
* the board have a set of tools to manage the flowviant web page and board".
|
|
30
|
+
* A code card already admitted it under its bypass; a write-up card refused it.
|
|
24
31
|
*/
|
|
25
32
|
import { pathToFileURL } from 'node:url';
|
|
26
33
|
|
|
@@ -28,7 +35,6 @@ import { pathToFileURL } from 'node:url';
|
|
|
28
35
|
export function mcpAllowDecision(payload) {
|
|
29
36
|
const name = payload?.tool_name;
|
|
30
37
|
if (typeof name !== 'string' || !name.startsWith('mcp__')) return null;
|
|
31
|
-
if (name === 'mcp__flowviant' || name.startsWith('mcp__flowviant__')) return null;
|
|
32
38
|
return {
|
|
33
39
|
hookSpecificOutput: {
|
|
34
40
|
hookEventName: 'PreToolUse',
|
|
@@ -1,26 +1,36 @@
|
|
|
1
1
|
/** Native gate; a full pool refuses only this spawn, preserving the turn. */
|
|
2
2
|
import { fileURLToPath } from 'node:url';
|
|
3
|
-
import { callOrchestrator } from '../orchestratorClient.mjs';
|
|
3
|
+
import { callOrchestrator, OrchestratorRefusal } from '../orchestratorClient.mjs';
|
|
4
4
|
import { OrchestratorInputTooLarge, readOrchestratorInput } from '../orchestratorInput.mjs';
|
|
5
5
|
import { runningCompiledBinary } from '../launchCommand.mjs';
|
|
6
|
+
import { gatedSpawnEvent } from '../nativeAgentTools.mjs';
|
|
7
|
+
|
|
8
|
+
// __hook exits as soon as main returns. A large rewritten child prompt must
|
|
9
|
+
// reach the pipe before that exit, not stop partway through JSON.
|
|
10
|
+
const answer = output => new Promise((resolve, reject) => process.stdout.write(`${JSON.stringify(output)}\n`, error => error ? reject(error) : resolve()));
|
|
6
11
|
|
|
7
12
|
export async function main() {
|
|
13
|
+
let event = null;
|
|
8
14
|
try {
|
|
9
|
-
|
|
15
|
+
event = await readOrchestratorInput(process.stdin);
|
|
10
16
|
const result = await callOrchestrator('hook', event);
|
|
11
|
-
|
|
17
|
+
// A stop is recorded, never refused or continued (stop_hook_active or not).
|
|
18
|
+
const output = event.hook_event_name === 'SubagentStop' ? {}
|
|
19
|
+
: result.deny ? { hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny', permissionDecisionReason: result.deny } }
|
|
12
20
|
: result.updatedInput || result.context ? { hookSpecificOutput: { hookEventName: event.hook_event_name,
|
|
13
21
|
...(result.updatedInput ? { permissionDecision: 'allow', updatedInput: result.updatedInput } : {}),
|
|
14
22
|
...(result.context ? { additionalContext: result.context } : {}) } } : {};
|
|
15
|
-
|
|
16
|
-
// must reach the pipe before that exit, not stop partway through JSON.
|
|
17
|
-
await new Promise((resolve, reject) => process.stdout.write(`${JSON.stringify(output)}\n`, error => error ? reject(error) : resolve()));
|
|
23
|
+
await answer(output);
|
|
18
24
|
} catch (error) {
|
|
19
|
-
|
|
20
|
-
// never let a native spawn escape the shared admission pool.
|
|
21
|
-
process.stderr.write(error instanceof OrchestratorInputTooLarge ? `${error.message}\n`
|
|
25
|
+
process.stderr.write(error instanceof OrchestratorInputTooLarge || error instanceof OrchestratorRefusal ? `${error.message}\n`
|
|
22
26
|
: 'The run’s orchestration gate is unavailable. Keep the work in the orchestrator until it reconnects.\n');
|
|
23
|
-
|
|
27
|
+
// FAIL CLOSED ONLY WHERE A SPAWN COULD ESCAPE THE SHARED POOL (2026-10-17).
|
|
28
|
+
// Both CLIs read exit 2 as a refusal; after a tool ran (Codex replaces its
|
|
29
|
+
// output) or at a stop (the child keeps going) that refusal is false. An
|
|
30
|
+
// event too large to read is never a spawn: no model writes 24 MiB of input.
|
|
31
|
+
// The names are the controller's own list (nativeAgentTools.mjs).
|
|
32
|
+
if (gatedSpawnEvent(event)) { process.exitCode = 2; return; }
|
|
33
|
+
try { await answer({}); } catch { /* the CLI already stopped reading */ }
|
|
24
34
|
}
|
|
25
35
|
}
|
|
26
36
|
// Bun bundles this module at the virtual entrypoint too. Only cli.mjs may
|
package/bin/lib/intakeDraft.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** A ticket becomes one card only when the CLI supplied all four fields.
|
|
2
2
|
* Forgive a JSON fence or a sentence around it, as the plan reader does;
|
|
3
3
|
* refuse a half-card rather than landing plausible but unactionable work. */
|
|
4
|
-
import { candidateObjects } from './
|
|
4
|
+
import { candidateObjects } from './modelJson.mjs';
|
|
5
5
|
import { agentTaskTypeOf } from './agentTaskTypes.mjs';
|
|
6
6
|
import { scrub as envScrub } from './uplinkScrub.mjs';
|
|
7
7
|
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FOLLOWING A GROWING JSONL FILE, BOUNDED PER POLL (2026-10-18 SOLID pass).
|
|
3
|
+
*
|
|
4
|
+
* claudeSubagentProgress.mjs (Claude's agent_progress rows) and
|
|
5
|
+
* codexSubagents.mjs (each child's rollout) wrote this out by hand with the
|
|
6
|
+
* same bounds, so a tail-reading fix (truncation, a torn or an oversized
|
|
7
|
+
* line) would land in one reader and miss the other. A first read starts
|
|
8
|
+
* within the last `scan` bytes and skips its torn first line. A file that
|
|
9
|
+
* shrank restarts at 0. One poll reads at most `scan` bytes in `chunk`-sized
|
|
10
|
+
* reads. Bytes after the last newline carry to the next poll, so a UTF-8
|
|
11
|
+
* character or a line split across reads survives. A line longer than
|
|
12
|
+
* `chunk` is discarded through its newline rather than reparsed as a suffix.
|
|
13
|
+
*
|
|
14
|
+
* Each line goes to `onLine` as it is read, so the lines before a read that
|
|
15
|
+
* fails mid-poll are still handled: the offset has already moved past them.
|
|
16
|
+
* A leaf: it reads through the caller's open handle.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
export const TAIL_SCAN_BYTES = 4 * 1024 * 1024;
|
|
20
|
+
export const TAIL_CHUNK_BYTES = 256 * 1024;
|
|
21
|
+
|
|
22
|
+
/** A tail that has read nothing yet. */
|
|
23
|
+
export const newTail = () => ({ offset: null, rest: Buffer.alloc(0), skip: false });
|
|
24
|
+
|
|
25
|
+
export async function followJsonl(fh, tail, onLine, { scan = TAIL_SCAN_BYTES, chunk = TAIL_CHUNK_BYTES } = {}) {
|
|
26
|
+
const { size } = await fh.stat();
|
|
27
|
+
if (tail.offset === null) {
|
|
28
|
+
tail.offset = Math.max(0, size - scan);
|
|
29
|
+
tail.skip = tail.offset > 0;
|
|
30
|
+
}
|
|
31
|
+
if (size < tail.offset) { tail.offset = 0; tail.rest = Buffer.alloc(0); }
|
|
32
|
+
// Bounded work per poll, including an immediately completed child.
|
|
33
|
+
const end = Math.min(size, tail.offset + scan);
|
|
34
|
+
while (tail.offset < end) {
|
|
35
|
+
const buffer = Buffer.alloc(Math.min(chunk, end - tail.offset));
|
|
36
|
+
const { bytesRead } = await fh.read(buffer, 0, buffer.length, tail.offset);
|
|
37
|
+
if (!bytesRead) break;
|
|
38
|
+
tail.offset += bytesRead;
|
|
39
|
+
const data = Buffer.concat([tail.rest, buffer.subarray(0, bytesRead)]);
|
|
40
|
+
let start = 0, nl;
|
|
41
|
+
while ((nl = data.indexOf(10, start)) >= 0) {
|
|
42
|
+
const line = data.subarray(start, nl).toString('utf8');
|
|
43
|
+
start = nl + 1;
|
|
44
|
+
if (tail.skip) { tail.skip = false; continue; }
|
|
45
|
+
onLine(line);
|
|
46
|
+
}
|
|
47
|
+
// Keep bytes across UTF-8 and partial-line boundaries. Oversized lines
|
|
48
|
+
// are discarded through their newline rather than reparsed as a suffix.
|
|
49
|
+
tail.rest = data.subarray(start);
|
|
50
|
+
if (tail.rest.length > chunk) { tail.rest = Buffer.alloc(0); tail.skip = true; }
|
|
51
|
+
}
|
|
52
|
+
}
|
package/bin/lib/mcpInventory.mjs
CHANGED
|
@@ -57,7 +57,8 @@ function bound(servers) {
|
|
|
57
57
|
const seen = new Set();
|
|
58
58
|
return servers
|
|
59
59
|
.map((s) => ({ ...s, name: s.name.trim().slice(0, MCP_INVENTORY_NAME_MAX).trim() }))
|
|
60
|
-
|
|
60
|
+
// Only the person's own `flowviant` reaches here as theirs (2026-10-19).
|
|
61
|
+
.filter((s) => s.name && (s.name !== 'flowviant' || s.source === 'personal') && !seen.has(s.name) && seen.add(s.name))
|
|
61
62
|
.sort((a, b) => SOURCE_ORDER[a.source] - SOURCE_ORDER[b.source] || (a.name < b.name ? -1 : a.name > b.name ? 1 : 0))
|
|
62
63
|
.slice(0, MCP_INVENTORY_MAX_SERVERS)
|
|
63
64
|
.map((s) => {
|
|
@@ -91,16 +92,20 @@ export function claudeInventory(harvested, snapshot) {
|
|
|
91
92
|
|
|
92
93
|
/**
|
|
93
94
|
* Codex: its own config's servers, then each declaration. A declared server
|
|
94
|
-
* replaces a personal one of the same name on a card's turn
|
|
95
|
+
* that mounts replaces a personal one of the same name on a card's turn
|
|
95
96
|
* (projectToolRuntimes.mjs `isolatedCodexHome`), so the declaration is the
|
|
96
|
-
* row
|
|
97
|
+
* row; one that cannot mount (2026-10-17) leaves the person's server on the
|
|
98
|
+
* card, and that is the row. Codex names no tools without starting a server,
|
|
99
|
+
* so `tools` is null.
|
|
97
100
|
*/
|
|
98
101
|
export function codexInventory(rows, snapshot) {
|
|
99
102
|
const declared = declaredRows(snapshot, 'codex');
|
|
100
|
-
const
|
|
103
|
+
const mounts = new Set(declared.filter((d) => d.state === 'ready').map((d) => d.name));
|
|
104
|
+
const personal = (rows ?? []).filter((r) => !mounts.has(r.name));
|
|
105
|
+
const theirs = new Set(personal.map((r) => r.name));
|
|
101
106
|
const servers = [
|
|
102
|
-
...declared.map((d) => ({ name: d.name, source: 'project', status: 'declared', reason: d.reason, tools: null })),
|
|
103
|
-
...
|
|
107
|
+
...declared.filter((d) => !theirs.has(d.name)).map((d) => ({ name: d.name, source: 'project', status: 'declared', reason: d.reason, tools: null })),
|
|
108
|
+
...personal.map((r) => ({ name: r.name, source: 'personal', status: r.status, reason: r.reason, tools: null })),
|
|
104
109
|
];
|
|
105
110
|
return { reported: rows != null, servers: bound(servers) };
|
|
106
111
|
}
|