flowviant 0.116.0 → 0.117.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/README.md +31 -12
- package/bin/cli.mjs +13 -17
- package/bin/lib/admission.mjs +7 -14
- package/bin/lib/agentInternalWait.mjs +83 -0
- package/bin/lib/agentPlan.mjs +56 -24
- package/bin/lib/agentPublish.mjs +12 -13
- package/bin/lib/agentTaskKinds.mjs +4 -7
- package/bin/lib/agySessions.mjs +3 -3
- package/bin/lib/artifactPreview.mjs +92 -18
- package/bin/lib/artifactReporter.mjs +16 -8
- package/bin/lib/artifacts.mjs +64 -60
- package/bin/lib/authproxy.mjs +2 -1
- package/bin/lib/childEnv.mjs +9 -4
- package/bin/lib/claude.mjs +10 -20
- package/bin/lib/claudePersonal.mjs +71 -6
- package/bin/lib/claudePosture.mjs +119 -42
- package/bin/lib/claudeSessions.mjs +7 -7
- package/bin/lib/claudeStream.mjs +7 -11
- package/bin/lib/cliLimit.mjs +2 -2
- package/bin/lib/codexHome.mjs +17 -0
- package/bin/lib/codexPersonal.mjs +44 -3
- package/bin/lib/codexRelay.mjs +21 -5
- package/bin/lib/codexResearch.mjs +180 -0
- package/bin/lib/config.mjs +37 -24
- package/bin/lib/conversationIds.mjs +51 -0
- package/bin/lib/credentials.mjs +2 -7
- package/bin/lib/daemonLocks.mjs +88 -0
- package/bin/lib/daemonState.mjs +5 -5
- package/bin/lib/deploy.mjs +6 -9
- package/bin/lib/deployCheckout.mjs +26 -22
- package/bin/lib/deployRunner.mjs +54 -18
- package/bin/lib/deployWire.mjs +32 -23
- package/bin/lib/desktopContract.mjs +3 -5
- package/bin/lib/env.mjs +104 -35
- package/bin/lib/fleet.mjs +7 -9
- package/bin/lib/fleetPost.mjs +172 -25
- package/bin/lib/fleetReports.mjs +130 -170
- package/bin/lib/fleetStartup.mjs +6 -6
- package/bin/lib/fleetWire.mjs +42 -0
- package/bin/lib/git.mjs +111 -61
- package/bin/lib/gitMergeIdentity.mjs +2 -8
- package/bin/lib/grant.mjs +23 -7
- package/bin/lib/holder.mjs +108 -81
- package/bin/lib/hooks/readGuard.mjs +41 -7
- package/bin/lib/install.mjs +80 -33
- package/bin/lib/instance.mjs +36 -83
- package/bin/lib/intakeDraft.mjs +4 -1
- package/bin/lib/knowledgeFileSync.mjs +31 -0
- package/bin/lib/knowledgeFiles.mjs +28 -83
- package/bin/lib/landed.mjs +22 -29
- package/bin/lib/launchCommand.mjs +91 -0
- package/bin/lib/listeners.mjs +0 -5
- package/bin/lib/localSessions.mjs +52 -39
- package/bin/lib/login.mjs +22 -12
- package/bin/lib/loginPath.mjs +22 -17
- package/bin/lib/machineListing.mjs +6 -3
- package/bin/lib/mcp-cli.mjs +9 -10
- package/bin/lib/noWindowEnv.mjs +23 -3
- package/bin/lib/ownedDirs.mjs +99 -0
- package/bin/lib/pinnedCapture.mjs +383 -0
- package/bin/lib/prWorkflow.mjs +94 -17
- package/bin/lib/preflight.mjs +8 -25
- package/bin/lib/preview.mjs +19 -25
- package/bin/lib/previewGatePolicy.mjs +34 -9
- package/bin/lib/previewRegistry.mjs +4 -11
- package/bin/lib/printable.mjs +70 -7
- package/bin/lib/procRegistry.mjs +33 -13
- package/bin/lib/procScan.mjs +2 -3
- package/bin/lib/procStat.mjs +76 -0
- package/bin/lib/processGroup.mjs +48 -0
- package/bin/lib/processes.mjs +10 -24
- package/bin/lib/projectToolRuntimes.mjs +101 -16
- package/bin/lib/projectTools.mjs +50 -32
- package/bin/lib/prompts.mjs +41 -19
- package/bin/lib/repoPath.mjs +19 -0
- package/bin/lib/repoState.mjs +17 -19
- package/bin/lib/runTurn.mjs +268 -210
- package/bin/lib/runtimeAntigravity.mjs +16 -5
- package/bin/lib/runtimeCapabilities.mjs +140 -20
- package/bin/lib/runtimeClaude.mjs +55 -5
- package/bin/lib/runtimeCodex.mjs +245 -205
- package/bin/lib/runtimeDetection.mjs +2 -2
- package/bin/lib/runtimeEvents.mjs +16 -10
- package/bin/lib/runtimeLimits.mjs +5 -6
- package/bin/lib/runtimeOfferings.mjs +24 -11
- package/bin/lib/runtimes.mjs +10 -0
- package/bin/lib/shipSweep.mjs +5 -3
- package/bin/lib/shot.mjs +91 -17
- package/bin/lib/sleep.mjs +11 -0
- package/bin/lib/startProject.mjs +5 -5
- package/bin/lib/statePaths.mjs +40 -0
- package/bin/lib/tty.mjs +25 -15
- package/bin/lib/turnLock.mjs +7 -14
- package/bin/lib/turnProfile.mjs +1 -1
- package/bin/lib/ui.mjs +0 -3
- package/bin/lib/uninstall.mjs +30 -26
- package/bin/lib/update.mjs +79 -27
- package/bin/lib/uplinkScrub.mjs +296 -31
- package/bin/lib/vault.mjs +78 -10
- package/bin/lib/vaultDiff.mjs +5 -2
- package/bin/lib/views.mjs +37 -12
- package/bin/lib/wikiRunner.mjs +40 -61
- package/bin/lib/work.mjs +17 -7
- package/bin/lib/workAdoptCarry.mjs +2 -4
- package/bin/lib/workAgentCheck.mjs +104 -57
- package/bin/lib/workAgentMerges.mjs +62 -57
- package/bin/lib/workAgentPlans.mjs +71 -139
- package/bin/lib/workAgentPrecheck.mjs +71 -138
- package/bin/lib/workAgentPublish.mjs +6 -8
- package/bin/lib/workAgentTurnBegun.mjs +15 -11
- package/bin/lib/workAgentTurnExecution.mjs +46 -53
- package/bin/lib/workAgentTurnReports.mjs +9 -37
- package/bin/lib/workAgentTurns.mjs +4 -3
- package/bin/lib/workAttachments.mjs +66 -11
- package/bin/lib/workBrain.mjs +9 -4
- package/bin/lib/workCappedTurn.mjs +109 -0
- package/bin/lib/workChildren.mjs +5 -5
- package/bin/lib/workCommandAudit.mjs +11 -2
- package/bin/lib/workDiffs.mjs +52 -6
- package/bin/lib/workIntake.mjs +35 -60
- package/bin/lib/workPlaceReports.mjs +7 -4
- package/bin/lib/workPlaces.mjs +96 -30
- package/bin/lib/workPreviews.mjs +47 -112
- package/bin/lib/workProcesses.mjs +8 -35
- package/bin/lib/workPullRequests.mjs +37 -56
- package/bin/lib/workReportQueue.mjs +100 -148
- package/bin/lib/workRetire.mjs +2 -1
- package/bin/lib/workSessionRuntime.mjs +38 -37
- package/bin/lib/workSessionTurns.mjs +111 -189
- package/bin/lib/workShip.mjs +43 -23
- package/bin/lib/workTurnShape.mjs +163 -0
- package/npm-shrinkwrap.json +59 -0
- package/package.json +3 -2
- package/bin/lib/fleetJobs.mjs +0 -120
- package/bin/lib/patchRevert.mjs +0 -60
|
@@ -6,16 +6,19 @@
|
|
|
6
6
|
* Split out of work.mjs (2026-09-26, SOLID F037). The queue is the skip-guard
|
|
7
7
|
* that stops a side-effecting turn being re-run over a dropped 200, so its
|
|
8
8
|
* delivery rules (ok / terminal / reject / retry) have exactly one home; the
|
|
9
|
-
* session lane settles through `settleWorkTurn
|
|
10
|
-
* `settleShip
|
|
11
|
-
*
|
|
12
|
-
*
|
|
9
|
+
* session lane settles through `settleWorkTurn` and the ship lane through
|
|
10
|
+
* `settleShip`; the plan, check and intake lanes, which have no queue, settle
|
|
11
|
+
* through fleetPost.mjs's `postBestEffort` (it closed over nothing of this
|
|
12
|
+
* queue's and moved there, audit 2026-10-10 F136). The attempts counter is
|
|
13
|
+
* the session lane's, handed in by name, because a terminal delivery is what
|
|
14
|
+
* may forget it.
|
|
13
15
|
*/
|
|
14
|
-
import { FLEET_URL
|
|
16
|
+
import { FLEET_URL } from './config.mjs';
|
|
15
17
|
import { git } from './git.mjs';
|
|
16
18
|
import { sweepMergedBranch } from './shipSweep.mjs';
|
|
17
19
|
import { note } from './ui.mjs';
|
|
18
|
-
import { fleetEndpoint } from './fleetWire.mjs';
|
|
20
|
+
import { fleetEndpoint, isConsideredRefusal } from './fleetWire.mjs';
|
|
21
|
+
import { fleetRequest } from './fleetPost.mjs';
|
|
19
22
|
|
|
20
23
|
export function createWorkReportQueue({
|
|
21
24
|
repoRoot,
|
|
@@ -58,137 +61,102 @@ export function createWorkReportQueue({
|
|
|
58
61
|
* hammered a refused body forever.
|
|
59
62
|
* - 'retry' — network errors, 408, 429 and 5xx: nothing was decided. */
|
|
60
63
|
const postSettle = async (url, body, terminalStatuses) => {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
'Content-Type': 'application/json',
|
|
68
|
-
},
|
|
69
|
-
signal: AbortSignal.timeout(30_000),
|
|
70
|
-
body: JSON.stringify(body),
|
|
71
|
-
});
|
|
72
|
-
if (res.ok) return 'ok';
|
|
73
|
-
if (terminalStatuses.includes(res.status)) return 'terminal';
|
|
74
|
-
if (res.status >= 400 && res.status < 500 && res.status !== 408 && res.status !== 429)
|
|
75
|
-
return 'reject';
|
|
76
|
-
return 'retry';
|
|
77
|
-
} catch {
|
|
78
|
-
return 'retry';
|
|
79
|
-
}
|
|
64
|
+
const r = await fleetRequest(url, { body, timeoutMs: 30_000 });
|
|
65
|
+
if (r.error) return 'retry';
|
|
66
|
+
if (r.ok) return 'ok';
|
|
67
|
+
if (terminalStatuses.includes(r.status)) return 'terminal';
|
|
68
|
+
if (isConsideredRefusal(r.status)) return 'reject';
|
|
69
|
+
return 'retry';
|
|
80
70
|
};
|
|
81
71
|
|
|
82
|
-
/**
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
if (Number.isFinite(secs)) return Math.max(0, Math.min(secs * 1000, 60_000));
|
|
90
|
-
const at = Date.parse(h);
|
|
91
|
-
return Number.isFinite(at) ? Math.max(0, Math.min(at - Date.now(), 60_000)) : fallbackMs;
|
|
92
|
-
};
|
|
72
|
+
/** How long a REJECTED report sits out before re-offering its body — long
|
|
73
|
+
* enough that a deploy-skew 400 costs a handful of POSTs a day, short
|
|
74
|
+
* enough that a server fix picks the report up the same morning. */
|
|
75
|
+
const REJECT_RETRY_MS = 10 * 60 * 1000;
|
|
76
|
+
// Keyed `${queue}:${id}` — a turn id and a session id are different
|
|
77
|
+
// namespaces, and one queue's backoff must never hold the other's report.
|
|
78
|
+
const reportBackoff = new Map(); // `work:<turnId>` | `ship:<sessionId>` -> earliest next attempt
|
|
93
79
|
|
|
94
80
|
/**
|
|
95
|
-
* A
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
* swallowed: unsettled, and the server expires the job so the asker is
|
|
103
|
-
* told, never spun.
|
|
81
|
+
* WHAT A DELIVERED SHIP REPORT SETS OFF — on BOTH paths, the immediate settle
|
|
82
|
+
* and the per-poll flush (audit 2026-10-10, F158). The flush used to run only
|
|
83
|
+
* the sweep, so a ship whose first ship-done POST hit a 502 left the rail
|
|
84
|
+
* rendering the whole pre-ship diff, the Repository block counting a deleted
|
|
85
|
+
* branch and card closes waiting on the 3-minute fetch. These follow the
|
|
86
|
+
* MERGE, not the report's punctuality: an action that changes what the
|
|
87
|
+
* machine would measure must cause a new measurement.
|
|
104
88
|
*/
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
await new Promise((r) => setTimeout(r, retryAfterMs(res, 2_000 * (i + 1))));
|
|
128
|
-
continue;
|
|
129
|
-
}
|
|
130
|
-
if (res.status >= 500) {
|
|
131
|
-
if (i === attempts - 1) return false;
|
|
132
|
-
await new Promise((r) => setTimeout(r, 2_000 * (i + 1)));
|
|
133
|
-
continue;
|
|
134
|
-
}
|
|
135
|
-
return true; // any other 4xx — the server's considered answer
|
|
136
|
-
}
|
|
137
|
-
return false;
|
|
89
|
+
const shipDelivered = (sessionId) => {
|
|
90
|
+
// The report has landed, so the idempotency path no longer needs the
|
|
91
|
+
// branch to exist. See `sweepMergedSessionBranch`.
|
|
92
|
+
sweepMergedSessionBranch(sessionId);
|
|
93
|
+
// AND THE DIFFSTAT IS NOW WRONG BY DEFINITION. A ship folds base in,
|
|
94
|
+
// merges the tip out and deletes the branch, so a fresh measurement reads
|
|
95
|
+
// `ahead: 0` with an empty diffstat — and without this the rail keeps
|
|
96
|
+
// rendering the ENTIRE pre-ship diff while the transcript two hundred
|
|
97
|
+
// pixels away says "Shipped to main". The ship button re-arms over a
|
|
98
|
+
// branch that is already merged. Same rule the kill path just learned:
|
|
99
|
+
// an action that changes what the machine would measure must cause a new
|
|
100
|
+
// measurement, and the 60s sweep is not that.
|
|
101
|
+
void reportPlaceWorktrees(sessionId).catch(() => {});
|
|
102
|
+
burstListeners(sessionId);
|
|
103
|
+
// …and the REPO picture changed too: the session branch is gone and base
|
|
104
|
+
// moved. Without this the Repository block keeps counting a branch the
|
|
105
|
+
// ship just deleted.
|
|
106
|
+
onRepoChanged();
|
|
107
|
+
// The ship's push moved the local origin/<base> ref — observe now, so
|
|
108
|
+
// the landed report (and anything trailered a ship carried) lands on
|
|
109
|
+
// this beat rather than the next 3-minute fetch.
|
|
110
|
+
void landed.observe().catch(() => {});
|
|
138
111
|
};
|
|
139
112
|
|
|
140
|
-
/**
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
113
|
+
/** EACH QUEUE, DESCRIBED ONCE — its endpoint, the explicit statuses under
|
|
114
|
+
* which the server will never re-offer the job, the id the body carries,
|
|
115
|
+
* and what a delivery (on time or late) forgets or sets off. The settle
|
|
116
|
+
* and the flush both deliver through `deliver`, so the two paths cannot
|
|
117
|
+
* drift apart again. */
|
|
118
|
+
const QUEUES = {
|
|
119
|
+
work: {
|
|
120
|
+
name: 'work',
|
|
121
|
+
url: WORK_DONE_URL,
|
|
122
|
+
terminal: [403, 404],
|
|
123
|
+
pending: pendingWorkReports,
|
|
124
|
+
idKey: 'turnId',
|
|
125
|
+
delivered: (id) => workAttempts.delete(id),
|
|
126
|
+
},
|
|
127
|
+
ship: {
|
|
128
|
+
name: 'ship',
|
|
129
|
+
url: SHIP_DONE_URL,
|
|
130
|
+
terminal: [403, 409],
|
|
131
|
+
pending: pendingShipReports,
|
|
132
|
+
idKey: 'sessionId',
|
|
133
|
+
delivered: shipDelivered,
|
|
134
|
+
},
|
|
157
135
|
};
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
|
|
136
|
+
const backoffKey = (q, id) => `${q.name}:${id}`;
|
|
137
|
+
|
|
138
|
+
/** POST one queue's body and apply the outcome: 'retry' and 'reject' keep
|
|
139
|
+
* it queued (the skip-guard) and only 'reject' backs off; 'ok' and
|
|
140
|
+
* 'terminal' forget it — and its backoff — and run the queue's delivered
|
|
141
|
+
* effects. */
|
|
142
|
+
const deliver = async (q, id, body) => {
|
|
143
|
+
const r = await postSettle(q.url, body, q.terminal);
|
|
161
144
|
if (r === 'retry' || r === 'reject') {
|
|
162
|
-
|
|
163
|
-
if (r === 'reject') reportBackoff.set(
|
|
145
|
+
q.pending.set(id, body);
|
|
146
|
+
if (r === 'reject') reportBackoff.set(backoffKey(q, id), Date.now() + REJECT_RETRY_MS);
|
|
164
147
|
} else {
|
|
165
|
-
|
|
166
|
-
reportBackoff.delete(
|
|
167
|
-
|
|
168
|
-
// branch to exist. See `sweepMergedSessionBranch`.
|
|
169
|
-
sweepMergedSessionBranch(sessionId);
|
|
170
|
-
// AND THE DIFFSTAT IS NOW WRONG BY DEFINITION. A ship folds base in,
|
|
171
|
-
// merges the tip out and deletes the branch, so a fresh measurement reads
|
|
172
|
-
// `ahead: 0` with an empty diffstat — and without this the rail keeps
|
|
173
|
-
// rendering the ENTIRE pre-ship diff while the transcript two hundred
|
|
174
|
-
// pixels away says "Shipped to main". The ship button re-arms over a
|
|
175
|
-
// branch that is already merged. Same rule the kill path just learned:
|
|
176
|
-
// an action that changes what the machine would measure must cause a new
|
|
177
|
-
// measurement, and the 60s sweep is not that.
|
|
178
|
-
void reportPlaceWorktrees(sessionId).catch(() => {});
|
|
179
|
-
burstListeners(sessionId);
|
|
180
|
-
// …and the REPO picture changed too: the session branch is gone and base
|
|
181
|
-
// moved. Without this the Repository block keeps counting a branch the
|
|
182
|
-
// ship just deleted.
|
|
183
|
-
onRepoChanged();
|
|
184
|
-
// The ship's push moved the local origin/<base> ref — observe now, so
|
|
185
|
-
// the landed report (and anything trailered a ship carried) lands on
|
|
186
|
-
// this beat rather than the next 3-minute fetch.
|
|
187
|
-
void landed.observe().catch(() => {});
|
|
148
|
+
q.pending.delete(id);
|
|
149
|
+
reportBackoff.delete(backoffKey(q, id));
|
|
150
|
+
q.delivered(id);
|
|
188
151
|
}
|
|
189
152
|
return r;
|
|
190
153
|
};
|
|
191
154
|
|
|
155
|
+
const settleWorkTurn = (turnId, payload) =>
|
|
156
|
+
deliver(QUEUES.work, turnId, { [QUEUES.work.idKey]: turnId, ...payload });
|
|
157
|
+
const settleShip = (sessionId, payload) =>
|
|
158
|
+
deliver(QUEUES.ship, sessionId, { [QUEUES.ship.idKey]: sessionId, ...payload });
|
|
159
|
+
|
|
192
160
|
/** See `shipSweep.mjs`. Bound to this manager's repo, base and report queue. */
|
|
193
161
|
const sweepMergedSessionBranch = (sessionId) =>
|
|
194
162
|
sweepMergedBranch(sessionId, {
|
|
@@ -210,30 +178,15 @@ export function createWorkReportQueue({
|
|
|
210
178
|
if (pendingWorkReports.size === 0 && pendingShipReports.size === 0) return;
|
|
211
179
|
flushingReports = true;
|
|
212
180
|
try {
|
|
213
|
-
for (const
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
workAttempts.delete(id);
|
|
223
|
-
reportBackoff.delete(id);
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
for (const [id, body] of [...pendingShipReports]) {
|
|
227
|
-
if ((reportBackoff.get(id) ?? 0) > Date.now()) continue;
|
|
228
|
-
const r = await postSettle(SHIP_DONE_URL, body, [403, 409]);
|
|
229
|
-
if (r === 'reject') reportBackoff.set(id, Date.now() + REJECT_RETRY_MS);
|
|
230
|
-
else if (r !== 'retry') {
|
|
231
|
-
pendingShipReports.delete(id);
|
|
232
|
-
reportBackoff.delete(id);
|
|
233
|
-
// Delivered late is still delivered — same sweep as the immediate
|
|
234
|
-
// path, and it must be here too or a report that needed a retry
|
|
235
|
-
// would leave its branch behind forever.
|
|
236
|
-
sweepMergedSessionBranch(id);
|
|
181
|
+
for (const q of [QUEUES.work, QUEUES.ship]) {
|
|
182
|
+
for (const [id, body] of [...q.pending]) {
|
|
183
|
+
// A rejected body sits out its backoff; the queued entry itself
|
|
184
|
+
// stays — it is the skip-guard against re-running a turn whose side
|
|
185
|
+
// effects already happened. Delivered late is still delivered: the
|
|
186
|
+
// same effects as the immediate path (a ship's sweep, re-measure,
|
|
187
|
+
// repo refresh and landed observation).
|
|
188
|
+
if ((reportBackoff.get(backoffKey(q, id)) ?? 0) > Date.now()) continue;
|
|
189
|
+
await deliver(q, id, body);
|
|
237
190
|
}
|
|
238
191
|
}
|
|
239
192
|
} finally {
|
|
@@ -244,7 +197,6 @@ export function createWorkReportQueue({
|
|
|
244
197
|
return {
|
|
245
198
|
pendingWorkReports,
|
|
246
199
|
pendingShipReports,
|
|
247
|
-
postBestEffort,
|
|
248
200
|
REJECT_RETRY_MS,
|
|
249
201
|
settleWorkTurn,
|
|
250
202
|
settleShip,
|
package/bin/lib/workRetire.mjs
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
import { existsSync, readdirSync } from 'node:fs';
|
|
12
12
|
import { join } from 'node:path';
|
|
13
13
|
import { git, isSafePathSegment } from './git.mjs';
|
|
14
|
+
import { PERSON_PLACE_PREFIX } from './workPlaces.mjs';
|
|
14
15
|
|
|
15
16
|
export function createWorkRetire({
|
|
16
17
|
repoRoot,
|
|
@@ -169,7 +170,7 @@ export function createWorkRetire({
|
|
|
169
170
|
// Only a PERSON's manual place. An agent's own worktree (`a-<id>`) is
|
|
170
171
|
// deliberately not touched: its branch is the reviewable unit, and
|
|
171
172
|
// moving it under a review would change what somebody is deciding about.
|
|
172
|
-
if (!place.startsWith(
|
|
173
|
+
if (!place.startsWith(PERSON_PLACE_PREFIX) || !isSafePathSegment(place)) continue;
|
|
173
174
|
if (placeLocks.has(place)) continue;
|
|
174
175
|
const wt = join(baseDir, 'sessions', place);
|
|
175
176
|
try {
|
|
@@ -1,32 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* WHICH CLI A TAB SPEAKS
|
|
3
|
-
*
|
|
2
|
+
* WHICH CLI A TAB SPEAKS — the runtime pin, and agy's cwd registry the first
|
|
3
|
+
* turn learns its conversation from.
|
|
4
4
|
*
|
|
5
|
-
* Split out of work.mjs (2026-09-26, SOLID F037).
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
5
|
+
* Split out of work.mjs (2026-09-26, SOLID F037). None of it is about running
|
|
6
|
+
* a turn: the session lane asks `sessionRuntime` before it spawns.
|
|
7
|
+
*
|
|
8
|
+
* THE ID SHAPES MOVED to a leaf (audit 2026-10-10, F157): conversationIds.mjs
|
|
9
|
+
* owns every CLI's conversation-id guard and marker name, because the
|
|
10
|
+
* machine readers below the lanes (runtimeLimits, the session scanners) need
|
|
11
|
+
* them too and a lane module is not theirs to import. The two guards the
|
|
12
|
+
* agent lane imports from here are re-exported unchanged.
|
|
10
13
|
*/
|
|
11
14
|
import { existsSync, readFileSync, realpathSync, writeFileSync } from 'node:fs';
|
|
12
15
|
import { join } from 'node:path';
|
|
13
16
|
import { homedir } from 'node:os';
|
|
14
|
-
import { canRun, RUNTIMES } from './runtimes.mjs';
|
|
17
|
+
import { canRun, RUNTIMES, runtimeById, runtimeLabel } from './runtimes.mjs';
|
|
15
18
|
import { detectRuntimes } from './runtimeDetection.mjs';
|
|
19
|
+
import { AGY_CONV_RE, CODEX_THREAD_RE } from './conversationIds.mjs';
|
|
16
20
|
|
|
17
|
-
|
|
18
|
-
* The shape a codex thread id must have before it is written to disk or —
|
|
19
|
-
* decisive — pushed into argv as `resume <id>`. Conservative on purpose:
|
|
20
|
-
* alphanumeric plus dash/underscore, never a leading dash (an argv that
|
|
21
|
-
* parses as a flag), never whitespace. Anything else is dropped and the
|
|
22
|
-
* session simply runs fresh in its own worktree.
|
|
23
|
-
*/
|
|
24
|
-
export const CODEX_THREAD_RE = /^[0-9a-zA-Z][0-9a-zA-Z_-]{7,63}$/;
|
|
25
|
-
|
|
26
|
-
/** agy conversation ids are plain UUIDs (the db filename IS the identity —
|
|
27
|
-
* measured: a renamed copy fails "trajectory not found"). Guarded the same
|
|
28
|
-
* way as the codex id: it rides in argv as `--conversation <id>`. */
|
|
29
|
-
export const AGY_CONV_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
21
|
+
export { AGY_CONV_RE, CODEX_THREAD_RE };
|
|
30
22
|
|
|
31
23
|
export function createWorkSessionRuntime({ sessionMetaPath }) {
|
|
32
24
|
/**
|
|
@@ -52,15 +44,19 @@ export function createWorkSessionRuntime({ sessionMetaPath }) {
|
|
|
52
44
|
* { mismatch: { pin, runtime } } (labels, for the caller's sentence).
|
|
53
45
|
*
|
|
54
46
|
* SESSION-CAPABLE means rt.mcp is truthy — the session tools ride a real
|
|
55
|
-
* per-invocation MCP config — OR the runtime runs tabs PLAIN
|
|
56
|
-
* no MCP at all, no cards, no streaming;
|
|
57
|
-
* the daemon's own report and ship-time
|
|
58
|
-
* whole.
|
|
59
|
-
*
|
|
60
|
-
*
|
|
47
|
+
* per-invocation MCP config — OR the runtime's row runs tabs PLAIN
|
|
48
|
+
* (`tab: 'plain'`, Antigravity's): no MCP at all, no cards, no streaming;
|
|
49
|
+
* the final answer is delivered by the daemon's own report and ship-time
|
|
50
|
+
* reconciliation keeps the ledger whole. The row says so, and the turn's
|
|
51
|
+
* shape reads the same fact (workTurnShape.mjs). `pickRuntimeFor('build')`
|
|
52
|
+
* is still the WRONG question here — it says yes to the mediated DISPATCH
|
|
53
|
+
* path without saying how a tab would speak, and a session pinned by it
|
|
54
|
+
* once threw in mcpFor on every turn.
|
|
61
55
|
*/
|
|
62
|
-
const sessionCapable = (rid) =>
|
|
63
|
-
|
|
56
|
+
const sessionCapable = (rid) => {
|
|
57
|
+
const rt = runtimeById(rid);
|
|
58
|
+
return (Boolean(rt?.mcp) || rt?.tab === 'plain') && canRun(rt, 'build');
|
|
59
|
+
};
|
|
64
60
|
const sessionRuntime = (wt, jobRuntime, sessionId) => {
|
|
65
61
|
// SCOPED: two tabs standing in one directory may run different CLIs, and an
|
|
66
62
|
// unscoped pin would hand the second one the first one's runtime.
|
|
@@ -73,12 +69,17 @@ export function createWorkSessionRuntime({ sessionMetaPath }) {
|
|
|
73
69
|
/* unreadable marker — re-pin below */
|
|
74
70
|
}
|
|
75
71
|
}
|
|
76
|
-
|
|
72
|
+
// A marker naming no runtime this daemon declares — torn, hand-edited, or
|
|
73
|
+
// a prototype key like `constructor` — is no pin at all: it is re-picked
|
|
74
|
+
// exactly like a missing one (audit 2026-10-10, F125). It used to enter
|
|
75
|
+
// this branch on truthiness and settle every turn as "unsupported
|
|
76
|
+
// constructor".
|
|
77
|
+
if (pinned && runtimeById(pinned)) {
|
|
77
78
|
if (jobRuntime && jobRuntime !== pinned) {
|
|
78
79
|
return {
|
|
79
80
|
mismatch: {
|
|
80
|
-
pin:
|
|
81
|
-
runtime:
|
|
81
|
+
pin: runtimeLabel(pinned),
|
|
82
|
+
runtime: runtimeLabel(jobRuntime),
|
|
82
83
|
},
|
|
83
84
|
};
|
|
84
85
|
}
|
|
@@ -86,18 +87,18 @@ export function createWorkSessionRuntime({ sessionMetaPath }) {
|
|
|
86
87
|
// the caller, not silently re-picked: re-picking would hand the held
|
|
87
88
|
// context to a different brain, which is the exact substitution the pin
|
|
88
89
|
// exists to prevent.
|
|
89
|
-
if (!sessionCapable(pinned)) return { unsupported:
|
|
90
|
+
if (!sessionCapable(pinned)) return { unsupported: runtimeLabel(pinned) };
|
|
90
91
|
const installed = detectRuntimes().find((r) => r.id === pinned)?.installed;
|
|
91
|
-
return installed ? { id: pinned } : { missing:
|
|
92
|
+
return installed ? { id: pinned } : { missing: runtimeLabel(pinned) };
|
|
92
93
|
}
|
|
93
94
|
// First turn, and the server named the brain: that IS the pick, gated the
|
|
94
95
|
// same two ways as a pin — not session-capable and not installed both
|
|
95
96
|
// settle honestly via the caller's existing paths, never substituted.
|
|
96
97
|
if (jobRuntime) {
|
|
97
98
|
if (!sessionCapable(jobRuntime))
|
|
98
|
-
return { unsupported:
|
|
99
|
+
return { unsupported: runtimeLabel(jobRuntime) };
|
|
99
100
|
const installed = detectRuntimes().find((r) => r.id === jobRuntime)?.installed;
|
|
100
|
-
if (!installed) return { missing:
|
|
101
|
+
if (!installed) return { missing: runtimeLabel(jobRuntime) };
|
|
101
102
|
if (marker) {
|
|
102
103
|
try {
|
|
103
104
|
writeFileSync(marker, jobRuntime);
|
|
@@ -114,7 +115,7 @@ export function createWorkSessionRuntime({ sessionMetaPath }) {
|
|
|
114
115
|
// when the server names it, never handed out as a default.
|
|
115
116
|
const rows = detectRuntimes();
|
|
116
117
|
const okFor = (rid) =>
|
|
117
|
-
Boolean(
|
|
118
|
+
Boolean(runtimeById(rid)?.mcp) &&
|
|
118
119
|
sessionCapable(rid) &&
|
|
119
120
|
Boolean(rows.find((r) => r.id === rid)?.installed);
|
|
120
121
|
const id = okFor('claude') ? 'claude' : (Object.keys(RUNTIMES).find(okFor) ?? null);
|