flowviant 0.103.0 → 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 +183 -2383
- 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 +2 -2
- 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/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 +17 -17
- package/bin/lib/wikiRunner.mjs +630 -0
- package/bin/lib/work.mjs +188 -3034
- 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 +91 -898
- 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,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CODEX, AS A RUNTIME — its registry row: argv per profile (the kernel
|
|
3
|
+
* sandbox postures), efforts, the env-carried MCP token, and its parser.
|
|
4
|
+
*
|
|
5
|
+
* Split out of runtimes.mjs (2026-09-26, SOLID F045). Each vendor's argv moves
|
|
6
|
+
* on its own vendor's release schedule; the registry (runtimes.mjs) assembles
|
|
7
|
+
* the rows and owns the rule that reads them, and the JSONL parser this row
|
|
8
|
+
* hands out as `parse` lives with the other stream parsers in
|
|
9
|
+
* runtimeEvents.mjs.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { SAFE } from './config.mjs';
|
|
13
|
+
import { parseCodexLine } from './runtimeEvents.mjs';
|
|
14
|
+
|
|
15
|
+
function codexMcp(token, mcpUrl) {
|
|
16
|
+
return {
|
|
17
|
+
dir: null, // nothing written — the token rides in the environment
|
|
18
|
+
args: [
|
|
19
|
+
'-c',
|
|
20
|
+
`mcp_servers.flowviant.url="${mcpUrl}"`,
|
|
21
|
+
'-c',
|
|
22
|
+
'mcp_servers.flowviant.bearer_token_env_var="FLOWVIANT_MCP_TOKEN"',
|
|
23
|
+
],
|
|
24
|
+
env: { FLOWVIANT_MCP_TOKEN: token },
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const CODEX_RUNTIME = {
|
|
29
|
+
id: 'codex',
|
|
30
|
+
label: 'Codex',
|
|
31
|
+
vendor: 'OpenAI',
|
|
32
|
+
bin: 'codex',
|
|
33
|
+
install: 'npm i -g @openai/codex',
|
|
34
|
+
login: 'codex login',
|
|
35
|
+
live: false,
|
|
36
|
+
/**
|
|
37
|
+
* BUILD AND CONSULT. See `args()` below for how consult is expressed — the
|
|
38
|
+
* short version is that Codex keeps the promise at the kernel rather than in
|
|
39
|
+
* a verb allowlist, and for a consult's actual threat (exfiltration driven
|
|
40
|
+
* by an injected question) that is sufficient and arguably stronger.
|
|
41
|
+
*
|
|
42
|
+
* ALL THREE, and `wiki` is the one worth pausing on: Codex expresses it
|
|
43
|
+
* MORE strictly than Claude does. A cartographer must read the repo and
|
|
44
|
+
* write only the vault; Claude's WIKI_PERM cannot path-scope Write and says
|
|
45
|
+
* so, leaning on the worktree reset as a backstop. Codex's permission
|
|
46
|
+
* profiles enforce the same rule in the kernel — measured: repo readable,
|
|
47
|
+
* vault writable, repo writes refused, network off.
|
|
48
|
+
*
|
|
49
|
+
* Enforcement was verified on Linux (bubblewrap + seccomp). macOS Seatbelt
|
|
50
|
+
* and Windows are UNTESTED; if this daemon starts running there, re-verify
|
|
51
|
+
* before trusting the consult posture on those platforms.
|
|
52
|
+
*/
|
|
53
|
+
profiles: ['build', 'consult', 'wiki', 'plan'],
|
|
54
|
+
/** `model_reasoning_effort` tops out at `xhigh` — there is no `max`, and a
|
|
55
|
+
* queued or skewed job naming it would be an argument Codex refuses. */
|
|
56
|
+
efforts: ['low', 'medium', 'high', 'xhigh'],
|
|
57
|
+
mcp: codexMcp,
|
|
58
|
+
/**
|
|
59
|
+
* Codex has NO system-prompt flag. The contract therefore rides inside the
|
|
60
|
+
* prompt, fenced and placed first, and this is a genuine weakening worth
|
|
61
|
+
* stating plainly: a system prompt is a rule, and a prompt preamble is a
|
|
62
|
+
* strong suggestion the model may drift from over a long turn. It is the
|
|
63
|
+
* best available; AGENTS.md was the alternative and is worse, because it is
|
|
64
|
+
* a FILE IN THE WORKTREE — one `git add -A` from being committed into the
|
|
65
|
+
* user's repository, which is not a risk worth taking for a slightly
|
|
66
|
+
* stickier instruction.
|
|
67
|
+
*
|
|
68
|
+
* Agent turns additionally pass reviewed base instructions as a developer
|
|
69
|
+
* config override when the personal config has no developer instruction;
|
|
70
|
+
* the prompt always carries them too. Workbench tabs keep their normal
|
|
71
|
+
* checkout discovery.
|
|
72
|
+
*
|
|
73
|
+
* `--skip-git-repo-check` is deliberately NOT passed: a task always builds
|
|
74
|
+
* in a git worktree, and if it somehow is not one, failing loudly beats
|
|
75
|
+
* silently editing files nobody can diff.
|
|
76
|
+
*
|
|
77
|
+
* THE PROMPT IS LAST, and that is load-bearing rather than tidy. Codex takes
|
|
78
|
+
* it as a trailing POSITIONAL, so every flag — including the two `-c` MCP
|
|
79
|
+
* overrides, which the caller hands in rather than appending — has to be
|
|
80
|
+
* placed before it. Appending them after the positional is the kind of argv
|
|
81
|
+
* that parses today and stops parsing on some future clap upgrade.
|
|
82
|
+
*/
|
|
83
|
+
args({ prompt, system, model, effort, resume, resumeThreadId, profile = 'build', vaultDir, mcp = [], resultSchemaArgs = [], adoptResumeId, agentTools }) {
|
|
84
|
+
// Adoption resumes a conversation in ITS OWN CLI's store (claude forks,
|
|
85
|
+
// agy moves) — codex has no adoptable store wired yet. Reaching here
|
|
86
|
+
// with an adopt id is a wiring mistake upstream, and it fails loudly on
|
|
87
|
+
// purpose: quietly dropping the flag would answer that session's held
|
|
88
|
+
// context with a different brain.
|
|
89
|
+
if (adoptResumeId) throw new Error("codex has no adoptable terminal store — an adopt id can't reach this builder");
|
|
90
|
+
const a = ['exec'];
|
|
91
|
+
// BY ID when the caller knows WHICH conversation this is — a Workbench
|
|
92
|
+
// tab's held context, captured off thread.started and stored with its
|
|
93
|
+
// worktree. `--last` resumes the machine's most recent codex conversation,
|
|
94
|
+
// which is only safe on the dispatch path (one lane, one turn at a time,
|
|
95
|
+
// in its own worktree); for a session it is a machine-global guess that
|
|
96
|
+
// two tabs — or a tab plus a dispatch — would cross-resume.
|
|
97
|
+
if (resumeThreadId) a.push('resume', resumeThreadId);
|
|
98
|
+
else if (resume) a.push('resume', '--last');
|
|
99
|
+
a.push('--json');
|
|
100
|
+
if (agentTools) {
|
|
101
|
+
a.push('-c', 'project_doc_max_bytes=0');
|
|
102
|
+
// A person's existing developer instructions keep their authority.
|
|
103
|
+
// The base snapshot is also present in the per-turn prompt preamble.
|
|
104
|
+
if (!agentTools.personalDeveloperInstructions)
|
|
105
|
+
a.push('-c', `developer_instructions=${JSON.stringify(agentTools.instructions)}`);
|
|
106
|
+
}
|
|
107
|
+
if (model) a.push('--model', model);
|
|
108
|
+
// Effort is a config value on Codex rather than a flag.
|
|
109
|
+
if (effort) a.push('-c', `model_reasoning_effort="${effort}"`);
|
|
110
|
+
|
|
111
|
+
if (profile === 'consult') {
|
|
112
|
+
// A CONSULT, EXPRESSED THE ONLY WAY CODEX CAN EXPRESS IT — and it is a
|
|
113
|
+
// different shape from Claude's, which is the whole reason `profile` is
|
|
114
|
+
// a promise rather than a flag list.
|
|
115
|
+
//
|
|
116
|
+
// Claude gets a VERB allowlist: Read, Grep, Glob and a handful of
|
|
117
|
+
// read-only Bash forms, with nothing that reaches the network. Codex has
|
|
118
|
+
// no such thing — it has no file-read tool at all, so reading the repo
|
|
119
|
+
// IS command execution (`cat`, `rg`). Removing the shell leaves the
|
|
120
|
+
// model with exactly ["update_plan","request_user_input"], which cannot
|
|
121
|
+
// answer a question about a codebase. You get both capabilities or
|
|
122
|
+
// neither.
|
|
123
|
+
//
|
|
124
|
+
// So the promise is kept one layer down instead. `read-only` is
|
|
125
|
+
// kernel-enforced (bubblewrap + seccomp on Linux): writes fail, and a
|
|
126
|
+
// direct-IP connect fails with "Operation not permitted" — socket() is
|
|
127
|
+
// denied, not merely DNS. An injected command still RUNS and still
|
|
128
|
+
// cannot take the repository anywhere, which is the threat a consult
|
|
129
|
+
// actually has: its prompt is steered by a question any project editor
|
|
130
|
+
// can type. Arguably a stronger guarantee than the allowlist, being
|
|
131
|
+
// below the agent rather than inside it.
|
|
132
|
+
a.push('--sandbox', 'read-only');
|
|
133
|
+
|
|
134
|
+
// THE HOLE THE SANDBOX DOES NOT COVER. `web_search` ships in `codex
|
|
135
|
+
// exec`'s default tool list even without --search, and it executes
|
|
136
|
+
// SERVER-SIDE at OpenAI — no local sandbox touches it. An injected turn
|
|
137
|
+
// could pack repo contents into a query and egress them straight past
|
|
138
|
+
// everything above. Both spellings, because the two config systems
|
|
139
|
+
// disagree about which one is live.
|
|
140
|
+
a.push('-c', 'tools.web_search=false', '-c', 'web_search="disabled"');
|
|
141
|
+
|
|
142
|
+
// Sub-agents would be a second turn whose posture nobody here chose.
|
|
143
|
+
a.push('-c', 'features.multi_agent=false', '-c', 'features.goals=false');
|
|
144
|
+
|
|
145
|
+
// HERMETIC. Without these a user's ~/.codex/config.toml, a project
|
|
146
|
+
// `.rules` execpolicy file, or their own MCP servers can widen a posture
|
|
147
|
+
// we are asserting on their behalf — silently, and on the one turn whose
|
|
148
|
+
// prompt comes from someone else's typing.
|
|
149
|
+
a.push('--ignore-user-config', '--ignore-rules');
|
|
150
|
+
} else if (profile === 'plan') {
|
|
151
|
+
// A PLANNING SESSION. Read-only on the filesystem, exactly like a
|
|
152
|
+
// consult — the writes it makes go through the control plane, not
|
|
153
|
+
// through this box — so the kernel sandbox is the same one, and for the
|
|
154
|
+
// same reason: this turn's prompt is steered by anything a project
|
|
155
|
+
// editor can type.
|
|
156
|
+
//
|
|
157
|
+
// Everything the consult branch above closes stays closed, and the
|
|
158
|
+
// reasoning is unchanged, so it is not restated: web_search egresses
|
|
159
|
+
// server-side at OpenAI where no local sandbox reaches it, sub-agents
|
|
160
|
+
// would be a turn whose posture nobody here chose, and a user's own
|
|
161
|
+
// config or MCP servers must not widen a posture we are asserting on
|
|
162
|
+
// their behalf.
|
|
163
|
+
//
|
|
164
|
+
// What differs from a consult is the ONE thing this profile exists for:
|
|
165
|
+
// an MCP config IS passed, carrying the plan principal's token. That
|
|
166
|
+
// token's whole tool set is the five plan tools — the server refuses
|
|
167
|
+
// anything else on it — so the control plane being open here does not
|
|
168
|
+
// widen what a hijacked turn could reach beyond the plan it is already
|
|
169
|
+
// sitting in.
|
|
170
|
+
a.push('--sandbox', 'read-only');
|
|
171
|
+
a.push('-c', 'tools.web_search=false', '-c', 'web_search="disabled"');
|
|
172
|
+
a.push('-c', 'features.multi_agent=false', '-c', 'features.goals=false');
|
|
173
|
+
a.push('--ignore-user-config', '--ignore-rules');
|
|
174
|
+
} else if (profile === 'wiki' && vaultDir) {
|
|
175
|
+
// THE CARTOGRAPHER, AND THIS ONE IS STRICTER THAN CLAUDE'S.
|
|
176
|
+
//
|
|
177
|
+
// A wiki turn reads the whole repo and writes ONLY the vault. Claude
|
|
178
|
+
// cannot actually express that: WIKI_PERM hands it Write/Edit and its own
|
|
179
|
+
// comment admits "Write/Edit can't be path-scoped here; the worktree
|
|
180
|
+
// reset is the backstop" — i.e. the cartographer CAN scribble on the
|
|
181
|
+
// checkout and we clean up afterwards.
|
|
182
|
+
//
|
|
183
|
+
// Codex's permission profiles take a per-path filesystem map, so the
|
|
184
|
+
// rule is enforced by the kernel instead of apologised for. Measured:
|
|
185
|
+
// repo readable, vault writable, repo writes fail "Read-only file
|
|
186
|
+
// system", and curl returns 000 — the network is off, which is the half
|
|
187
|
+
// WIKI_PERM was really protecting (its comment: "Command execution is
|
|
188
|
+
// the line: it enables network exfil").
|
|
189
|
+
//
|
|
190
|
+
// The table is set WHOLE rather than as a dotted key, and that is not
|
|
191
|
+
// style: `-c permissions.wiki.filesystem."<path>"="write"` splits the
|
|
192
|
+
// dotted path on the dots INSIDE the path, and every real vault lives
|
|
193
|
+
// under `~/.flowviant/vaults/…`. It fails with "filesystem path must be
|
|
194
|
+
// absolute", which reads like a path problem and is a parsing one.
|
|
195
|
+
a.push('-c', 'permissions.flowviantwiki.extends=":read-only"');
|
|
196
|
+
a.push('-c', `permissions.flowviantwiki.filesystem={"${vaultDir}"="write"}`);
|
|
197
|
+
a.push('-P', 'flowviantwiki');
|
|
198
|
+
a.push('--ignore-user-config', '--ignore-rules');
|
|
199
|
+
} else {
|
|
200
|
+
// The daemon's build posture, mapped: SAFE keeps writes inside the
|
|
201
|
+
// workspace, the default lets the agent run its own tests and git
|
|
202
|
+
// commands. Neither asks a human — there is no human on this end of the
|
|
203
|
+
// pipe. Deliberately NOT hermetic: a build is work the user asked for by
|
|
204
|
+
// @mentioning this CLI, and their own config is theirs to apply.
|
|
205
|
+
a.push('--sandbox', SAFE ? 'workspace-write' : 'danger-full-access');
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
a.push(...mcp, ...resultSchemaArgs);
|
|
209
|
+
a.push(`${system}\n\n---\n\n${prompt}`);
|
|
210
|
+
return a;
|
|
211
|
+
},
|
|
212
|
+
parse: parseCodexLine,
|
|
213
|
+
/** `codex exec --output-schema <file>` constrains the FINAL message to a
|
|
214
|
+
* JSON Schema. Only needed on the mediated path; the direct path reports
|
|
215
|
+
* through MCP tool calls, which are already structured. */
|
|
216
|
+
resultSchema: (path) => ['--output-schema', path],
|
|
217
|
+
};
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WHAT THIS MACHINE HAS — which declared CLIs are installed here, and, of
|
|
3
|
+
* those, which one runs a job nobody @mentioned.
|
|
4
|
+
*
|
|
5
|
+
* Split out of runtimes.mjs (2026-09-26, SOLID F045). The registry is a
|
|
6
|
+
* statement about the CLIs (their argv, their profiles); this file is a
|
|
7
|
+
* measurement of the box (a `--version` exec per CLI, cached with a TTL), and a
|
|
8
|
+
* probe changing is not a reason to edit a table of argv builders.
|
|
9
|
+
*
|
|
10
|
+
* `pickRuntimeFor` lives HERE rather than beside `canRun` because it is the one
|
|
11
|
+
* reader that joins the registry's promise with this box's measurement: the
|
|
12
|
+
* registry imports nothing machine-stateful, so the dependency runs one way
|
|
13
|
+
* (this file → runtimes.mjs) and never in a cycle.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { execFileSync } from 'node:child_process';
|
|
17
|
+
import { RUNTIMES, canRun, drivableHere } from './runtimes.mjs';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* WHICH RUNTIME RUNS A JOB THAT NOBODY @MENTIONED.
|
|
21
|
+
*
|
|
22
|
+
* Building a task has an author: you @mentioned a CLI, and that is the only
|
|
23
|
+
* dispatch in this product. The other turns have none — the wiki sweep, the
|
|
24
|
+
* re-ground, the plan check, the quick edit and the consult are all started by
|
|
25
|
+
* the daemon or the server, and until now every one of them took `runTurn`'s
|
|
26
|
+
* default parameter value and ran Claude. That was not a decision; it was five
|
|
27
|
+
* call sites omitting an argument, and it only looked correct while Claude was
|
|
28
|
+
* the only runtime and preflight refused to start without it.
|
|
29
|
+
*
|
|
30
|
+
* A PROFILE is a promise about what is IMPOSSIBLE during the turn, not a flag
|
|
31
|
+
* list — flag lists are per-vendor, promises are not, and the goal is that every
|
|
32
|
+
* runtime behaves the same way predictably. A runtime declares the profiles it
|
|
33
|
+
* can actually express; one that cannot express a profile does not get that job,
|
|
34
|
+
* rather than getting it with weaker guarantees nobody wrote down.
|
|
35
|
+
*
|
|
36
|
+
* Claude first when it qualifies — not favouritism, and worth saying plainly:
|
|
37
|
+
* these prompts were written and tuned against it, so it is the known-good
|
|
38
|
+
* answer and anything else is a substitution. When it is absent, any runtime
|
|
39
|
+
* that can express the profile runs the job, which is the whole point.
|
|
40
|
+
*/
|
|
41
|
+
export function pickRuntimeFor(profile, { detected } = {}) {
|
|
42
|
+
const rows = detected ?? detectRuntimes();
|
|
43
|
+
const ok = (id) =>
|
|
44
|
+
canRun(RUNTIMES[id], profile) && Boolean(rows.find((d) => d.id === id)?.installed);
|
|
45
|
+
if (ok('claude')) return 'claude';
|
|
46
|
+
return Object.keys(RUNTIMES).find(ok) ?? null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Which of these is on this machine, asked at most once per DETECT_TTL_MS.
|
|
51
|
+
*
|
|
52
|
+
* `--version` rather than `which`: a binary on PATH that cannot execute (a
|
|
53
|
+
* broken install, a wrong-arch download, a shell alias pointing at nothing) is
|
|
54
|
+
* not a runtime you can dispatch to, and reporting it as one sends work into a
|
|
55
|
+
* hole. 5s is generous for a version print and short enough that three missing
|
|
56
|
+
* CLIs cannot stall a roster poll.
|
|
57
|
+
*
|
|
58
|
+
* Reported to the server on the roster poll so the app can stop saying "we have
|
|
59
|
+
* not looked". It is a statement about THIS MACHINE and nothing else — no
|
|
60
|
+
* account, no quota, no entitlement. Flowviant relays; it does not enforce.
|
|
61
|
+
*/
|
|
62
|
+
let detectedCache = null;
|
|
63
|
+
let detectedAt = 0;
|
|
64
|
+
// The cache EXPIRES rather than living for the process: pickRuntimeFor's whole
|
|
65
|
+
// premise is "a CLI can be installed while the daemon runs", and both it and
|
|
66
|
+
// the roster poll read this cache — a forever-cache made that comment a lie
|
|
67
|
+
// (nothing after preflight ever passed refresh, so a mid-run install was
|
|
68
|
+
// invisible until restart, to the server included). 5 minutes keeps the probes
|
|
69
|
+
// (3 sync --version execs) off the hot path while an install still surfaces on
|
|
70
|
+
// the next poll or job.
|
|
71
|
+
const DETECT_TTL_MS = 5 * 60 * 1000;
|
|
72
|
+
/**
|
|
73
|
+
* THE POLL'S `runtimes` VALUE — the CLIs this machine can drive, comma-joined,
|
|
74
|
+
* and '' when a detection that RAN found none (2026-09-26, SOLID F015).
|
|
75
|
+
*
|
|
76
|
+
* Three states on the wire, like `skills`: absent = nothing was measured (an
|
|
77
|
+
* older daemon, or a detection that threw); '' = measured, and none; a list =
|
|
78
|
+
* measured. Omitting the param after an empty detection — the old spelling —
|
|
79
|
+
* read as "said nothing", so the server kept advertising the last CLI a
|
|
80
|
+
* machine lost.
|
|
81
|
+
*/
|
|
82
|
+
export function runtimesReport(detected) {
|
|
83
|
+
return detected
|
|
84
|
+
.filter((r) => r.dispatchable)
|
|
85
|
+
.map((r) => r.id)
|
|
86
|
+
.join(',');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function detectRuntimes({ refresh = false } = {}) {
|
|
90
|
+
if (detectedCache && !refresh && Date.now() - detectedAt < DETECT_TTL_MS) return detectedCache;
|
|
91
|
+
detectedAt = Date.now();
|
|
92
|
+
detectedCache = Object.values(RUNTIMES).map((rt) => {
|
|
93
|
+
let version = null;
|
|
94
|
+
try {
|
|
95
|
+
version = String(
|
|
96
|
+
execFileSync(rt.bin, ['--version'], {
|
|
97
|
+
timeout: 5000,
|
|
98
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
99
|
+
})
|
|
100
|
+
)
|
|
101
|
+
.trim()
|
|
102
|
+
.split('\n')[0]
|
|
103
|
+
.slice(0, 40);
|
|
104
|
+
} catch {
|
|
105
|
+
version = null;
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
id: rt.id,
|
|
109
|
+
installed: version !== null,
|
|
110
|
+
version,
|
|
111
|
+
// Installed and drivable are different questions, and conflating them is
|
|
112
|
+
// how a user ends up @mentioning something that silently never starts.
|
|
113
|
+
// THREE questions, in fact — see `drivableHere`: the CLI can be installed,
|
|
114
|
+
// and this module can know how to spawn it, and the worker this daemon is
|
|
115
|
+
// running can still be unable to drive it.
|
|
116
|
+
dispatchable: version !== null && drivableHere(rt),
|
|
117
|
+
blocked: rt.blocked ?? null,
|
|
118
|
+
};
|
|
119
|
+
});
|
|
120
|
+
return detectedCache;
|
|
121
|
+
}
|