flowviant 0.102.2 → 0.104.0
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/cli.mjs +87 -474
- package/bin/lib/admission.mjs +19 -3
- package/bin/lib/agentPlan.mjs +2 -2
- package/bin/lib/agentTaskKinds.mjs +93 -0
- package/bin/lib/agySessions.mjs +158 -0
- package/bin/lib/artifactPolicy.mjs +44 -0
- package/bin/lib/artifactPreview.mjs +52 -0
- package/bin/lib/artifactReporter.mjs +163 -0
- package/bin/lib/artifacts.mjs +47 -227
- package/bin/lib/authReject.mjs +16 -4
- package/bin/lib/authproxy.mjs +47 -304
- package/bin/lib/boxIdentity.mjs +245 -0
- package/bin/lib/childEnv.mjs +11 -9
- package/bin/lib/claude.mjs +14 -851
- package/bin/lib/claudeAuth.mjs +3 -3
- package/bin/lib/claudePosture.mjs +367 -0
- package/bin/lib/claudeSessions.mjs +411 -0
- package/bin/lib/claudeStream.mjs +207 -0
- package/bin/lib/cliLimit.mjs +34 -0
- package/bin/lib/commandSpecs.mjs +173 -0
- package/bin/lib/config.mjs +11 -42
- package/bin/lib/credentialDate.mjs +22 -0
- package/bin/lib/credentials.mjs +21 -26
- package/bin/lib/daemonLogging.mjs +55 -0
- package/bin/lib/daemonState.mjs +64 -0
- package/bin/lib/deploy.mjs +148 -647
- package/bin/lib/deployCheckout.mjs +265 -0
- package/bin/lib/deployConfig.mjs +142 -0
- package/bin/lib/deployRunner.mjs +311 -0
- package/bin/lib/deployWire.mjs +41 -0
- package/bin/lib/desktopContract.mjs +56 -105
- package/bin/lib/env.mjs +49 -484
- package/bin/lib/fleet.mjs +184 -2396
- package/bin/lib/fleetJobs.mjs +120 -0
- package/bin/lib/fleetPost.mjs +40 -0
- package/bin/lib/fleetReports.mjs +392 -0
- package/bin/lib/fleetRoster.mjs +470 -0
- package/bin/lib/fleetStartup.mjs +196 -0
- package/bin/lib/fleetWake.mjs +45 -0
- package/bin/lib/fleetWire.mjs +46 -0
- package/bin/lib/git.mjs +34 -3
- package/bin/lib/gitMergeIdentity.mjs +51 -0
- package/bin/lib/grant.mjs +5 -3
- package/bin/lib/help.mjs +3 -125
- package/bin/lib/holder.mjs +482 -0
- package/bin/lib/instance.mjs +124 -46
- package/bin/lib/knowledge.mjs +16 -799
- package/bin/lib/knowledgeFetch.mjs +38 -0
- package/bin/lib/knowledgeFileSync.mjs +112 -0
- package/bin/lib/knowledgeFiles.mjs +487 -0
- package/bin/lib/knowledgeLibrary.mjs +249 -0
- package/bin/lib/landed.mjs +2 -1
- package/bin/lib/listeners.mjs +99 -141
- package/bin/lib/localSessions.mjs +30 -555
- package/bin/lib/login.mjs +3 -2
- package/bin/lib/loginPath.mjs +3 -5
- package/bin/lib/machineDisconnect.mjs +203 -0
- package/bin/lib/machineEnv.mjs +22 -0
- package/bin/lib/machineLimits.mjs +61 -0
- package/bin/lib/machineListing.mjs +338 -0
- package/bin/lib/machines.mjs +78 -455
- package/bin/lib/machinesCommand.mjs +214 -0
- package/bin/lib/mcp-cli.mjs +2 -1
- package/bin/lib/patchRevert.mjs +60 -0
- package/bin/lib/prWorkflow.mjs +157 -0
- package/bin/lib/preflight.mjs +2 -1
- package/bin/lib/pressurePolicy.mjs +104 -0
- package/bin/lib/preview.mjs +2 -272
- package/bin/lib/previewFramePolicy.mjs +112 -0
- package/bin/lib/previewGatePolicy.mjs +236 -0
- package/bin/lib/previewRegistry.mjs +235 -0
- package/bin/lib/printable.mjs +2 -2
- package/bin/lib/procRegistry.mjs +28 -18
- package/bin/lib/procScan.mjs +146 -0
- package/bin/lib/processes.mjs +29 -37
- package/bin/lib/projectToolRuntimes.mjs +138 -0
- package/bin/lib/projectTools.mjs +13 -88
- package/bin/lib/prompts.mjs +28 -42
- package/bin/lib/repoPath.mjs +26 -0
- package/bin/lib/resources.mjs +7 -69
- package/bin/lib/resumeLost.mjs +107 -0
- package/bin/lib/runTurn.mjs +324 -0
- package/bin/lib/runtimeAntigravity.mjs +230 -0
- package/bin/lib/runtimeCapabilities.mjs +334 -0
- package/bin/lib/runtimeClaude.mjs +126 -0
- package/bin/lib/runtimeCodex.mjs +217 -0
- package/bin/lib/runtimeDetection.mjs +121 -0
- package/bin/lib/runtimeEvents.mjs +482 -0
- package/bin/lib/runtimeOfferings.mjs +4 -1
- package/bin/lib/runtimes.mjs +42 -1402
- package/bin/lib/safeFileName.mjs +74 -0
- package/bin/lib/shipMerge.mjs +33 -2
- package/bin/lib/standDownExit.mjs +181 -0
- package/bin/lib/standDownGate.mjs +48 -0
- package/bin/lib/startProject.mjs +279 -0
- package/bin/lib/trace.mjs +3 -3
- package/bin/lib/turnLock.mjs +68 -0
- package/bin/lib/turnProfile.mjs +77 -0
- package/bin/lib/uninstall.mjs +55 -45
- package/bin/lib/uninstallCopies.mjs +112 -0
- package/bin/lib/update.mjs +30 -17
- package/bin/lib/uplinkScrub.mjs +265 -0
- package/bin/lib/vault.mjs +22 -151
- package/bin/lib/vaultDiff.mjs +183 -0
- package/bin/lib/versionOrder.mjs +42 -0
- package/bin/lib/views.mjs +35 -12
- package/bin/lib/wikiRunner.mjs +630 -0
- package/bin/lib/work.mjs +188 -3037
- package/bin/lib/workAdoptCarry.mjs +94 -0
- package/bin/lib/workAgentCheck.mjs +278 -0
- package/bin/lib/workAgentMerges.mjs +54 -142
- package/bin/lib/workAgentPlans.mjs +14 -12
- package/bin/lib/workAgentPrecheck.mjs +393 -0
- package/bin/lib/workAgentPublish.mjs +177 -0
- package/bin/lib/workAgentReview.mjs +8 -666
- package/bin/lib/workAgentTurnBegun.mjs +109 -0
- package/bin/lib/workAgentTurnExecution.mjs +604 -0
- package/bin/lib/workAgentTurnOutcome.mjs +207 -0
- package/bin/lib/workAgentTurnReports.mjs +189 -0
- package/bin/lib/workAgentTurns.mjs +92 -897
- package/bin/lib/workAttachments.mjs +93 -0
- package/bin/lib/workBrain.mjs +53 -0
- package/bin/lib/workChildren.mjs +110 -0
- package/bin/lib/workCommandAudit.mjs +62 -0
- package/bin/lib/workDiffs.mjs +2 -1
- package/bin/lib/workNarration.mjs +152 -0
- package/bin/lib/workPlaceReports.mjs +426 -0
- package/bin/lib/workPlaces.mjs +228 -0
- package/bin/lib/workPreviews.mjs +31 -9
- package/bin/lib/workProcesses.mjs +4 -5
- package/bin/lib/workPullRequests.mjs +52 -126
- package/bin/lib/workReportQueue.mjs +254 -0
- package/bin/lib/workRetire.mjs +185 -0
- package/bin/lib/workSessionGroups.mjs +166 -0
- package/bin/lib/workSessionRuntime.mjs +164 -0
- package/bin/lib/workSessionTokens.mjs +68 -0
- package/bin/lib/workSessionTurns.mjs +892 -0
- package/bin/lib/workShip.mjs +9 -29
- package/bin/lib/workToolLog.mjs +76 -0
- package/package.json +3 -2
- package/bin/lib/patch.mjs +0 -363
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WHAT EACH CLI'S STREAM SAID, IN THE DAEMON'S VOCABULARY — the event parsers.
|
|
3
|
+
*
|
|
4
|
+
* Split out of runtimes.mjs (2026-09-26, SOLID F045). The registry answers "how
|
|
5
|
+
* do you drive this CLI"; this file answers "what did its output mean", and the
|
|
6
|
+
* two change for different reasons: a vendor renaming an event or a tool is an
|
|
7
|
+
* edit here, a vendor renaming a flag is an edit to the registry. Claude's tool
|
|
8
|
+
* calls (the one-line activity and the structured tool event), Codex's JSONL
|
|
9
|
+
* ThreadEvents and Antigravity's stream-json all live here, beside the
|
|
10
|
+
* `label`/`full` convention and the thinking marker every one of them speaks.
|
|
11
|
+
*
|
|
12
|
+
* Pure: no state, no I/O. The registry hands `parseCodexLine` and
|
|
13
|
+
* `parseAgyLine` to its adapters as their `parse`; claudeStream.mjs owns Claude's
|
|
14
|
+
* own stream loop and calls `humanizeClaudeTool`/`toolEventOf` from it.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/** Truncate for a one-line activity label. */
|
|
18
|
+
const oneLine = (s, n = 140) =>
|
|
19
|
+
String(s ?? '').replace(/\s+/g, ' ').trim().slice(0, n);
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* `label` IS THE READOUT; `full` IS THE RELAY (2026-09-16) — the convention
|
|
23
|
+
* every prose activity in this daemon follows, stated once here because three
|
|
24
|
+
* parsers produce one and three consumers read them.
|
|
25
|
+
*
|
|
26
|
+
* `label` is one collapsed, clamped line, because its first two consumers are a
|
|
27
|
+
* terminal console and a one-line pulse that is overwritten every two seconds,
|
|
28
|
+
* and neither can show a paragraph. The turn TRACE is a third consumer with the
|
|
29
|
+
* opposite need: it is scrollback, and clipping a sentence at 140 characters
|
|
30
|
+
* there is the product summarizing its own agent.
|
|
31
|
+
*
|
|
32
|
+
* So a prose activity may ALSO carry `full`: the text EXACTLY as the CLI
|
|
33
|
+
* emitted it, uncollapsed and unclipped. Nothing derives it and nothing but the
|
|
34
|
+
* trace reads it — `label` is untouched, so the console and the pulse are
|
|
35
|
+
* byte-identical to before. Absent means there was no fuller text than the
|
|
36
|
+
* label (a tool line, a bare thinking marker), and the trace falls back to the
|
|
37
|
+
* label — which is what every pre-0.87.0 daemon does for everything.
|
|
38
|
+
*
|
|
39
|
+
* THE MARKER BELOW IS THE label-ONLY CASE, and one definition with four
|
|
40
|
+
* readers: claudeStream.mjs writes it, this file's codex parser falls back to it, and
|
|
41
|
+
* TWO collapses key on it — `trace.mjs` for the turn trace, `wikiRunner.mjs`
|
|
42
|
+
* for the wiki sweep's feed. A collapse keyed on a string typed out four times would
|
|
43
|
+
* silently stop collapsing the first time somebody reworded one of them, and a
|
|
44
|
+
* collapse that stops collapsing fails no test — it just fills a feed.
|
|
45
|
+
*
|
|
46
|
+
* It exists at all because the CLIs do not hand over the thinking itself:
|
|
47
|
+
* measured 2026-09-16 across three real transcripts, 93 thinking blocks, every
|
|
48
|
+
* one of them empty (signature only), and a live probe with MAX_THINKING_TOKENS
|
|
49
|
+
* and `--include-partial-messages` returned an empty `thinking_delta` and a
|
|
50
|
+
* zero-length complete block. So the marker is the honest whole of what is
|
|
51
|
+
* known — "it is reasoning, not hung" — and the day text does arrive it rides
|
|
52
|
+
* `full` and this marker is simply not used.
|
|
53
|
+
*/
|
|
54
|
+
export const THINK_MARKER = 'thinking…';
|
|
55
|
+
|
|
56
|
+
const shortPath = (p, cwd) => {
|
|
57
|
+
const s = String(p ?? '');
|
|
58
|
+
return cwd && s.startsWith(cwd) ? s.slice(cwd.length).replace(/^\//, '') : s;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// ── Claude Code ────────────────────────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Tool-call → one line of activity. Claude's tool names, unchanged from when
|
|
65
|
+
* this lived in claude.mjs; `kind` is the daemon's own vocabulary and every
|
|
66
|
+
* runtime's parser must speak it (`read` is what the wiki file counter counts,
|
|
67
|
+
* `write` carries `path` so distinct pages can be counted).
|
|
68
|
+
*/
|
|
69
|
+
export function humanizeClaudeTool(name, input = {}, cwd = '') {
|
|
70
|
+
switch (name) {
|
|
71
|
+
case 'Read':
|
|
72
|
+
return { kind: 'read', label: `read ${shortPath(input.file_path, cwd)}` };
|
|
73
|
+
case 'Write':
|
|
74
|
+
case 'Edit': {
|
|
75
|
+
const p = String(input.file_path ?? '');
|
|
76
|
+
const tail = p.split('/').slice(-2).join('/');
|
|
77
|
+
return { kind: 'write', path: p, label: `${name === 'Write' ? '+ page' : '~ page'} ${tail}` };
|
|
78
|
+
}
|
|
79
|
+
case 'Grep':
|
|
80
|
+
return {
|
|
81
|
+
kind: 'search',
|
|
82
|
+
label: `grep ${JSON.stringify(input.pattern ?? '')}${input.path ? ` in ${shortPath(input.path, cwd)}` : ''}`,
|
|
83
|
+
};
|
|
84
|
+
case 'Glob':
|
|
85
|
+
return { kind: 'glob', label: `glob ${input.pattern ?? ''}` };
|
|
86
|
+
case 'LS':
|
|
87
|
+
return { kind: 'list', label: `ls ${shortPath(input.path ?? '.', cwd)}` };
|
|
88
|
+
case 'Bash':
|
|
89
|
+
// `command` rides beside the display label, VERBATIM (capped): the label
|
|
90
|
+
// is a 60-char readout for a console and the rail, and truncation is
|
|
91
|
+
// fine there — but the admin's command audit exists to answer "what
|
|
92
|
+
// actually ran on our box", and an ellipsis is exactly where the part
|
|
93
|
+
// that matters would hide.
|
|
94
|
+
return {
|
|
95
|
+
kind: 'bash',
|
|
96
|
+
command: String(input.command ?? '').slice(0, 2000),
|
|
97
|
+
label: `$ ${oneLine(input.command, 60)}`,
|
|
98
|
+
};
|
|
99
|
+
case 'TodoWrite': {
|
|
100
|
+
// The CLI's own todo list — the plan the transcript's PLAN card renders.
|
|
101
|
+
// The label narrates the change ("plan: 2 of 5 — cut over /api/session");
|
|
102
|
+
// the structured items ride through toolEventOf below.
|
|
103
|
+
const todos = Array.isArray(input.todos) ? input.todos : [];
|
|
104
|
+
if (todos.length === 0) return null;
|
|
105
|
+
const done = todos.filter((t) => t?.status === 'completed').length;
|
|
106
|
+
const active = todos.find((t) => t?.status === 'in_progress');
|
|
107
|
+
return {
|
|
108
|
+
kind: 'plan',
|
|
109
|
+
label: `plan: ${done} of ${todos.length}${active?.content ? ` — ${oneLine(active.content, 60)}` : ''}`,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
default:
|
|
113
|
+
return null; // other tools: silent
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Count '\n' without materializing a split — a Write's content can be a
|
|
118
|
+
* multi-MB generated file, and `split('\n')` re-allocates all of it as line
|
|
119
|
+
* strings on the synchronous stream path the narrator and wake socket share.
|
|
120
|
+
* Numbers cannot leak, so counting runs on the RAW string. */
|
|
121
|
+
export function countLines(s) {
|
|
122
|
+
const str = String(s ?? '');
|
|
123
|
+
if (!str) return 0;
|
|
124
|
+
let n = 1;
|
|
125
|
+
let i = -1;
|
|
126
|
+
while ((i = str.indexOf('\n', i + 1)) !== -1) n++;
|
|
127
|
+
return n;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** How much raw string the scrubber sees before any cap is applied. A secret
|
|
131
|
+
* can only surface in the first ~capped chars of a field; giving the scrub a
|
|
132
|
+
* window this much larger means a secret would have to be longer than the
|
|
133
|
+
* window minus the cap to straddle out of it — no real credential is. */
|
|
134
|
+
const SCRUB_WINDOW = 8_192;
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Tool-call → ONE STRUCTURED EVENT for the transcript's tool cards — the
|
|
138
|
+
* relay's durable form, where `humanizeClaudeTool` above is its one-line live
|
|
139
|
+
* form. Same source (the CLI's own tool_use input), zero inference: every
|
|
140
|
+
* field is something the CLI emitted, and a tool this doesn't know renders as
|
|
141
|
+
* nothing rather than as a guess.
|
|
142
|
+
*
|
|
143
|
+
* Wire vocabulary (compact keys — this rides a 1.5s-throttled POST):
|
|
144
|
+
* t: read|edit|write|grep|glob|bash|task|plan
|
|
145
|
+
* p: path (worktree-relative) q: pattern/description c: command
|
|
146
|
+
* a/d: line counts added/deleted (from the input's own strings)
|
|
147
|
+
* dl: a few "-/+" prefixed preview lines of an Edit
|
|
148
|
+
* items: the plan's todos, x = text, s = done|active|open
|
|
149
|
+
*
|
|
150
|
+
* SCRUB BEFORE CAP — the order is load-bearing (review, 2026-09-01). The
|
|
151
|
+
* caller passes its `envScrub`; every string is scrubbed over a bounded
|
|
152
|
+
* window FIRST and capped LAST, because the reverse order had two failures:
|
|
153
|
+
* a secret straddling the cap boundary was cut into a prefix the
|
|
154
|
+
* exact-substring scrub could no longer match (a partial credential on the
|
|
155
|
+
* wire), and a scrub REPLACEMENT that grew a string past the cap tripped the
|
|
156
|
+
* server's field limits. `scrub` defaults to identity so this stays testable
|
|
157
|
+
* without a vault.
|
|
158
|
+
*/
|
|
159
|
+
export function toolEventOf(name, input = {}, cwd = '', scrub = (s) => s) {
|
|
160
|
+
// Window → scrub → cap. The window bounds what a multi-MB input costs; the
|
|
161
|
+
// cap is applied AFTER the scrub so a replacement cannot overflow it.
|
|
162
|
+
const clean = (v, cap) => scrub(String(v ?? '').slice(0, SCRUB_WINDOW)).slice(0, cap);
|
|
163
|
+
const rel = (p) => clean(shortPath(p, cwd), 300);
|
|
164
|
+
// The first k lines of a side, from a scrubbed bounded prefix — never a
|
|
165
|
+
// full split of the raw string.
|
|
166
|
+
const firstLines = (s, k) =>
|
|
167
|
+
scrub(String(s ?? '').slice(0, SCRUB_WINDOW)).split('\n', k).slice(0, k);
|
|
168
|
+
switch (name) {
|
|
169
|
+
case 'Read':
|
|
170
|
+
return { t: 'read', p: rel(input.file_path) };
|
|
171
|
+
case 'Write':
|
|
172
|
+
return { t: 'write', p: rel(input.file_path), a: countLines(input.content) };
|
|
173
|
+
case 'Edit': {
|
|
174
|
+
const oldS = String(input.old_string ?? '');
|
|
175
|
+
const newS = String(input.new_string ?? '');
|
|
176
|
+
// A MINI-DIFF, not the diff: the first lines of each side, enough to
|
|
177
|
+
// recognise the change at a glance. The real diff lives in git.
|
|
178
|
+
const dl = [
|
|
179
|
+
...firstLines(oldS, 2).map((l) => `- ${l}`),
|
|
180
|
+
...firstLines(newS, 3).map((l) => `+ ${l}`),
|
|
181
|
+
].map((l) => l.slice(0, 160));
|
|
182
|
+
return { t: 'edit', p: rel(input.file_path), a: countLines(newS), d: countLines(oldS), dl };
|
|
183
|
+
}
|
|
184
|
+
case 'Grep':
|
|
185
|
+
return {
|
|
186
|
+
t: 'grep',
|
|
187
|
+
q: clean(input.pattern, 200),
|
|
188
|
+
...(input.path ? { p: rel(input.path) } : {}),
|
|
189
|
+
};
|
|
190
|
+
case 'Glob':
|
|
191
|
+
return { t: 'glob', q: clean(input.pattern, 200) };
|
|
192
|
+
case 'Bash':
|
|
193
|
+
return { t: 'bash', c: clean(input.command, 200) };
|
|
194
|
+
case 'Task':
|
|
195
|
+
return { t: 'task', q: clean(input.description, 200) };
|
|
196
|
+
case 'TodoWrite': {
|
|
197
|
+
const todos = Array.isArray(input.todos) ? input.todos.slice(0, 20) : [];
|
|
198
|
+
const items = todos
|
|
199
|
+
.map((td) => ({
|
|
200
|
+
x: clean(td?.content, 120),
|
|
201
|
+
s: td?.status === 'completed' ? 'done' : td?.status === 'in_progress' ? 'active' : 'open',
|
|
202
|
+
}))
|
|
203
|
+
.filter((i) => i.x);
|
|
204
|
+
return items.length ? { t: 'plan', items } : null;
|
|
205
|
+
}
|
|
206
|
+
default:
|
|
207
|
+
return null;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* THE ACTIVITY KINDS `toolEventOf` ALSO ANSWERS — the dedupe rule, stated once.
|
|
213
|
+
*
|
|
214
|
+
* A Claude tool call goes down BOTH paths in claudeStream.mjs: `humanizeClaudeTool`
|
|
215
|
+
* makes a one-line activity and `toolEventOf` makes a structured event, from
|
|
216
|
+
* the same `tool_use`. A consumer taking both — the agent turn's trace — would
|
|
217
|
+
* otherwise render every read twice, once as a sentence and once as a card.
|
|
218
|
+
*
|
|
219
|
+
* It is the KINDS the structured builder answers, not every tool kind: `LS`
|
|
220
|
+
* produces a `list` activity and no tool event, so dropping `list` would delete
|
|
221
|
+
* it from the trace entirely. And it is only safe to apply on the runtimes
|
|
222
|
+
* whose stream reaches `onToolEvent` at all — codex and agy have their own
|
|
223
|
+
* parsers, never call it, and would go silent. Both halves are pinned in
|
|
224
|
+
* trace.test.mjs, because the two functions are edited independently and the
|
|
225
|
+
* failure is invisible: a dropped prose line looks exactly like a quiet turn.
|
|
226
|
+
*/
|
|
227
|
+
export const CLAUDE_TOOL_PROSE_KINDS = new Set([
|
|
228
|
+
'read',
|
|
229
|
+
'write',
|
|
230
|
+
'search',
|
|
231
|
+
'glob',
|
|
232
|
+
'bash',
|
|
233
|
+
'plan',
|
|
234
|
+
]);
|
|
235
|
+
|
|
236
|
+
// ── Codex ──────────────────────────────────────────────────────────────────
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Codex item → activity, in the daemon's vocabulary.
|
|
240
|
+
*
|
|
241
|
+
* The event names and item types below are not guesses: they were read off the
|
|
242
|
+
* shipped 0.147 binary (`ThreadStarted`/`TurnCompleted`/`ItemCompleted`, items
|
|
243
|
+
* `agent_message` / `reasoning` / `command_execution` / `file_change` /
|
|
244
|
+
* `mcp_tool_call` / `web_search` / `todo_list`). Unknown item types return null
|
|
245
|
+
* and stay silent rather than printing a raw JSON blob into someone's console.
|
|
246
|
+
*/
|
|
247
|
+
function humanizeCodexItem(item = {}, cwd = '') {
|
|
248
|
+
switch (item.item_type ?? item.type) {
|
|
249
|
+
// `full` rides beside the label wherever the item carries more than the
|
|
250
|
+
// label can hold — see the `label`/`full` note above. Codex is the runtime
|
|
251
|
+
// that actually SENDS reasoning text today, so its `think` is the one place
|
|
252
|
+
// in this daemon where a real thought reaches the trace whole.
|
|
253
|
+
case 'agent_message': {
|
|
254
|
+
const text = String(item.text ?? item.message ?? '');
|
|
255
|
+
return { kind: 'say', label: oneLine(text), ...(text ? { full: text } : {}) };
|
|
256
|
+
}
|
|
257
|
+
case 'reasoning': {
|
|
258
|
+
const text = String(item.text ?? '');
|
|
259
|
+
return {
|
|
260
|
+
kind: 'think',
|
|
261
|
+
label: oneLine(text) || THINK_MARKER,
|
|
262
|
+
...(text.trim() ? { full: text } : {}),
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
case 'command_execution':
|
|
266
|
+
return {
|
|
267
|
+
kind: 'bash',
|
|
268
|
+
command: String(item.command ?? '').slice(0, 2000),
|
|
269
|
+
label: `$ ${oneLine(item.command, 60)}`,
|
|
270
|
+
};
|
|
271
|
+
case 'file_change': {
|
|
272
|
+
// `changes` is a list of touched paths; the daemon counts distinct files,
|
|
273
|
+
// so emit one activity per path rather than one for the batch.
|
|
274
|
+
const first = (item.changes ?? [])[0] ?? {};
|
|
275
|
+
const p = String(first.path ?? '');
|
|
276
|
+
const tail = p.split('/').slice(-2).join('/');
|
|
277
|
+
const verb = first.kind === 'add' ? '+' : first.kind === 'delete' ? '-' : '~';
|
|
278
|
+
return { kind: 'write', path: p, label: `${verb} ${tail || 'file'}` };
|
|
279
|
+
}
|
|
280
|
+
case 'mcp_tool_call':
|
|
281
|
+
return { kind: 'tool', label: `${item.server ?? 'mcp'}.${item.tool ?? ''}` };
|
|
282
|
+
case 'web_search':
|
|
283
|
+
return { kind: 'search', label: `search ${oneLine(item.query, 60)}` };
|
|
284
|
+
default:
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Codex `--json` emits JSONL of ThreadEvents. Returns `{ activity, text,
|
|
291
|
+
* threadId }` — `text` accumulates the agent's own words, because the turn
|
|
292
|
+
* loop reads its sentinels (NOTHING / BLOCKED:<id> / DONE) out of exactly
|
|
293
|
+
* that; `threadId` surfaces once, off the lifecycle event, for callers that
|
|
294
|
+
* need to resume THIS conversation later (runTurn's onThreadId).
|
|
295
|
+
*/
|
|
296
|
+
export function parseCodexLine(line, cwd) {
|
|
297
|
+
let ev;
|
|
298
|
+
try {
|
|
299
|
+
ev = JSON.parse(line);
|
|
300
|
+
} catch {
|
|
301
|
+
return null; // not every line is JSON (warnings go to stderr, but be safe)
|
|
302
|
+
}
|
|
303
|
+
switch (ev.type) {
|
|
304
|
+
// The conversation's own id, announced before any item (`ThreadStarted` on
|
|
305
|
+
// the shipped 0.147 binary, like the measured events below). Surfaced so a
|
|
306
|
+
// SESSION turn can resume this exact thread next time: `resume --last` is
|
|
307
|
+
// a machine-global guess, and on a box running two tabs — or a tab plus a
|
|
308
|
+
// dispatch — it resumes someone else's conversation. No activity and no
|
|
309
|
+
// text: nothing here is the model speaking.
|
|
310
|
+
case 'thread.started':
|
|
311
|
+
return { activity: null, text: '', threadId: String(ev.thread_id ?? '') || null };
|
|
312
|
+
case 'item.completed': {
|
|
313
|
+
const item = ev.item ?? {};
|
|
314
|
+
const activity = humanizeCodexItem(item, cwd);
|
|
315
|
+
// Only the agent's MESSAGES are sentinel-bearing text. Reasoning is not:
|
|
316
|
+
// a model that muses "I could output NOTHING here" must not end the turn.
|
|
317
|
+
const isMessage = (item.item_type ?? item.type) === 'agent_message';
|
|
318
|
+
const text = isMessage ? `${item.text ?? item.message ?? ''}\n` : '';
|
|
319
|
+
// `answer` is the SAME message, surfaced alone: `text` accumulates every
|
|
320
|
+
// message plus every error and stderr line, so a caller that needs "what
|
|
321
|
+
// the agent said LAST" (the agent lane's final JSON object) cannot recover
|
|
322
|
+
// it from `out`. Claude's stream has this for free under
|
|
323
|
+
// `answerFromResult`; codex needed it said.
|
|
324
|
+
return { activity, text, ...(isMessage ? { answer: String(item.text ?? item.message ?? '') } : {}) };
|
|
325
|
+
}
|
|
326
|
+
// THE TURN'S OWN TOKEN COUNT (2026-09-24). `codex exec --json` closes a
|
|
327
|
+
// turn with `turn.completed` carrying `usage` — the codex twin of Claude's
|
|
328
|
+
// `result.usage`, and until now dropped on the floor, so a codex agent
|
|
329
|
+
// reported no spend at all. Mapped onto the daemon's four counters:
|
|
330
|
+
// OpenAI's `input_tokens` INCLUDES the cached part, Claude's excludes it,
|
|
331
|
+
// so the cached share is subtracted out rather than counted twice. Codex
|
|
332
|
+
// has no cache-creation figure; zero is what it reported, not a guess.
|
|
333
|
+
// Output already includes reasoning tokens on OpenAI's side.
|
|
334
|
+
case 'turn.completed': {
|
|
335
|
+
const u = ev.usage;
|
|
336
|
+
if (!u || typeof u !== 'object') return null;
|
|
337
|
+
const n = (v) => (Number.isFinite(Number(v)) && Number(v) > 0 ? Math.floor(Number(v)) : 0);
|
|
338
|
+
const cached = n(u.cached_input_tokens);
|
|
339
|
+
return {
|
|
340
|
+
activity: null,
|
|
341
|
+
text: '',
|
|
342
|
+
// `usageFromResult`'s shape (claudeStream.mjs), so every consumer of
|
|
343
|
+
// `onUsage` reads one vocabulary whichever CLI counted it.
|
|
344
|
+
usage: {
|
|
345
|
+
input: Math.max(0, n(u.input_tokens) - cached),
|
|
346
|
+
output: n(u.output_tokens),
|
|
347
|
+
cacheCreate: 0,
|
|
348
|
+
cacheRead: cached,
|
|
349
|
+
},
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
case 'turn.failed': {
|
|
353
|
+
// An error is prose too, and the sentence that explains a failed turn is
|
|
354
|
+
// routinely longer than a 140-char label — a stack-shaped message loses
|
|
355
|
+
// its cause exactly where somebody is reading to find it.
|
|
356
|
+
//
|
|
357
|
+
// THE LABEL KEEPS ITS OWN `??`, not `msg || …`, and the difference is one
|
|
358
|
+
// input: an EXPLICITLY EMPTY message. `??` lets `''` through as an empty
|
|
359
|
+
// label, which every consumer swallows (`if (!label)`); `||` would print
|
|
360
|
+
// "turn failed" and fire a pulse where this daemon printed nothing. That
|
|
361
|
+
// may well be the better readout, but it is a change to the console and
|
|
362
|
+
// the pulse, and `full` shipped on the promise that neither moved — so it
|
|
363
|
+
// is argued for on its own day, not smuggled in beside a relay field.
|
|
364
|
+
const msg = String(ev.error?.message ?? '');
|
|
365
|
+
return {
|
|
366
|
+
activity: {
|
|
367
|
+
kind: 'error',
|
|
368
|
+
label: oneLine(ev.error?.message ?? 'turn failed'),
|
|
369
|
+
...(msg.trim() ? { full: msg } : {}),
|
|
370
|
+
},
|
|
371
|
+
// THE FAILURE'S OWN WORDS REACH `text` (2026-09-24), the rule the bare
|
|
372
|
+
// `error` arm below already keeps. This returned '' — so a codex turn
|
|
373
|
+
// that failed on a usage limit handed the limit matcher nothing, the
|
|
374
|
+
// agent landed in Stuck as "produced no output", and every other codex
|
|
375
|
+
// agent kept spending into the same wall.
|
|
376
|
+
text: msg.trim() ? `${msg}\n` : '',
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
// A bare `error` event — the shape an auth failure arrives in ("401
|
|
380
|
+
// Unauthorized: Missing bearer…", observed against 0.147.0 with no
|
|
381
|
+
// credentials). It used to fall through to `default` and be dropped, which
|
|
382
|
+
// meant a signed-out Codex produced an EMPTY turn: no sentinel, so the
|
|
383
|
+
// driver nudged twice and reported `stalled`, and the thread said the agent
|
|
384
|
+
// gave up rather than that the CLI is not signed in. The message goes into
|
|
385
|
+
// `text` so it reaches the operator's console AND the usage-limit
|
|
386
|
+
// classifier, which reads exactly this stream.
|
|
387
|
+
case 'error': {
|
|
388
|
+
// Same split as the arm above: `??` for the label (untouched behaviour),
|
|
389
|
+
// `msg` for the relay field only.
|
|
390
|
+
const msg = String(ev.message ?? '');
|
|
391
|
+
return {
|
|
392
|
+
activity: {
|
|
393
|
+
kind: 'error',
|
|
394
|
+
label: oneLine(ev.message ?? 'error'),
|
|
395
|
+
...(msg.trim() ? { full: msg } : {}),
|
|
396
|
+
},
|
|
397
|
+
text: `${ev.message ?? ''}\n`,
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
default:
|
|
401
|
+
return null; // turn.started / item.started / item.updated
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// ── Antigravity ────────────────────────────────────────────────────────────
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* `agy --output-format stream-json` emits `{event: init|step_update|result}`.
|
|
409
|
+
* Tool names read off a live 1.1.12 session rather than guessed.
|
|
410
|
+
*/
|
|
411
|
+
function humanizeAgyTool(name, p = {}, cwd = '') {
|
|
412
|
+
const path = p.TargetFile ?? p.AbsolutePath ?? p.DirectoryPath ?? p.File ?? '';
|
|
413
|
+
const tail = String(path).split('/').slice(-2).join('/');
|
|
414
|
+
switch (name) {
|
|
415
|
+
case 'view_file':
|
|
416
|
+
case 'read_resource':
|
|
417
|
+
return { kind: 'read', label: `read ${shortPath(path, cwd)}` };
|
|
418
|
+
case 'write_to_file':
|
|
419
|
+
case 'replace_file_content':
|
|
420
|
+
case 'multi_replace_file_content':
|
|
421
|
+
return {
|
|
422
|
+
kind: 'write',
|
|
423
|
+
path: String(path),
|
|
424
|
+
label: `${name === 'write_to_file' ? '+ page' : '~ page'} ${tail || 'file'}`,
|
|
425
|
+
};
|
|
426
|
+
case 'grep_search':
|
|
427
|
+
return { kind: 'search', label: `grep ${oneLine(p.Query ?? p.SearchTerm ?? '', 60)}` };
|
|
428
|
+
case 'find_by_name':
|
|
429
|
+
return { kind: 'glob', label: `find ${oneLine(p.Pattern ?? '', 60)}` };
|
|
430
|
+
case 'list_dir':
|
|
431
|
+
return { kind: 'list', label: `ls ${shortPath(path, cwd)}` };
|
|
432
|
+
case 'run_command':
|
|
433
|
+
return {
|
|
434
|
+
kind: 'bash',
|
|
435
|
+
command: String(p.CommandLine ?? '').slice(0, 2000),
|
|
436
|
+
label: `$ ${oneLine(p.CommandLine, 60)}`,
|
|
437
|
+
};
|
|
438
|
+
case 'call_mcp_tool':
|
|
439
|
+
return { kind: 'tool', label: `mcp.${p.ToolName ?? ''}` };
|
|
440
|
+
default:
|
|
441
|
+
return null;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
export function parseAgyLine(line, cwd) {
|
|
446
|
+
let ev;
|
|
447
|
+
try {
|
|
448
|
+
ev = JSON.parse(line);
|
|
449
|
+
} catch {
|
|
450
|
+
return null;
|
|
451
|
+
}
|
|
452
|
+
if (ev.event === 'step_update') {
|
|
453
|
+
const su = ev.step_update ?? {};
|
|
454
|
+
const ti = su.tool_info;
|
|
455
|
+
if (!ti) return null;
|
|
456
|
+
const err = ti.error?.message;
|
|
457
|
+
// Same rule as codex's error lines: the label is the console's, `full` is
|
|
458
|
+
// the trace's, and a tool error's message is exactly the kind of sentence a
|
|
459
|
+
// 140-char clamp cuts the cause out of.
|
|
460
|
+
if (err)
|
|
461
|
+
return {
|
|
462
|
+
activity: { kind: 'error', label: oneLine(err), full: String(err) },
|
|
463
|
+
text: '',
|
|
464
|
+
};
|
|
465
|
+
// Each tool is reported twice — once ACTIVE, once DONE — so only the
|
|
466
|
+
// terminal state emits, otherwise every action appears in the thread twice.
|
|
467
|
+
if (su.state && su.state !== 'DONE') return null;
|
|
468
|
+
return { activity: humanizeAgyTool(ti.name, ti.parameters ?? {}, cwd), text: '' };
|
|
469
|
+
}
|
|
470
|
+
if (ev.event === 'result') {
|
|
471
|
+
const r = ev.result ?? {};
|
|
472
|
+
// The final answer is the ONLY sentinel-bearing text: agy has no incremental
|
|
473
|
+
// assistant-message event, so a turn's whole verdict arrives here at once.
|
|
474
|
+
return {
|
|
475
|
+
activity: r.error
|
|
476
|
+
? { kind: 'error', label: oneLine(r.error), full: String(r.error) }
|
|
477
|
+
: null,
|
|
478
|
+
text: `${r.response ?? ''}${r.error ? `\n${r.error}` : ''}\n`,
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
return null;
|
|
482
|
+
}
|
|
@@ -4,6 +4,7 @@ import { readFileSync, readdirSync, statSync } from 'node:fs';
|
|
|
4
4
|
import { homedir } from 'node:os';
|
|
5
5
|
import { join } from 'node:path';
|
|
6
6
|
import { readBaseTools } from './projectTools.mjs';
|
|
7
|
+
import { mcpRefusal } from './projectToolRuntimes.mjs';
|
|
7
8
|
|
|
8
9
|
const TTL = 5 * 60_000;
|
|
9
10
|
let cachedAt = 0;
|
|
@@ -68,7 +69,9 @@ export function runtimeOfferings(repoRoot, baseRef, { refresh = false } = {}) {
|
|
|
68
69
|
const snapshot = repoCached.snapshot;
|
|
69
70
|
if (out.rts.codex) out.rts.codex = [...new Set([...out.rts.codex, ...snapshot.skills.map((s) => s.path.split('/')[2])])].slice(0, 80);
|
|
70
71
|
if (out.rtp.codex) out.rtp.codex = [...new Map([...out.rtp.codex, ...snapshot.tools
|
|
71
|
-
|
|
72
|
+
// A repo server Codex refuses (projectToolRuntimes.mjs owns which) is
|
|
73
|
+
// one this machine cannot connect for Codex, like a broken one.
|
|
74
|
+
.filter((t) => t.state !== 'ready' || mcpRefusal('codex', t.config) != null)
|
|
72
75
|
.map((t) => ({ n: t.name, s: 'failed' }))].map((server) => [server.n, server])).values()].slice(0, 80);
|
|
73
76
|
} catch { /* keep local measurements */ }
|
|
74
77
|
return out;
|