flowviant 0.115.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 +99 -145
- 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
|
@@ -12,53 +12,31 @@
|
|
|
12
12
|
* the credential (workSessionTokens.mjs), the runtime pin (workSessionRuntime
|
|
13
13
|
* .mjs), the adopt carry (workAdoptCarry.mjs), attachments
|
|
14
14
|
* (workAttachments.mjs), the live line (workNarration.mjs), the tool log
|
|
15
|
-
* (workToolLog.mjs)
|
|
15
|
+
* (workToolLog.mjs), the command audit (workCommandAudit.mjs) and — pure —
|
|
16
|
+
* what the turn IS, plain, plan, capture or build, with its prompt pair
|
|
17
|
+
* (workTurnShape.mjs, audit 2026-10-10 F159).
|
|
16
18
|
*/
|
|
17
19
|
import { existsSync, readFileSync, realpathSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
18
|
-
import { git
|
|
20
|
+
import { git } from './git.mjs';
|
|
19
21
|
import { processStartTime } from './procRegistry.mjs';
|
|
20
22
|
import { c, note, ok, warn } from './ui.mjs';
|
|
21
23
|
import { mcpFor } from './claude.mjs';
|
|
22
24
|
import { runTurn as defaultRunTurn } from './runTurn.mjs';
|
|
23
25
|
import { addUsage } from './turnUsage.mjs';
|
|
24
|
-
import {
|
|
25
|
-
SYSTEM_WORK,
|
|
26
|
-
SYSTEM_CAPTURE,
|
|
27
|
-
WORK_TURN_KICKOFF,
|
|
28
|
-
CAPTURE_TURN_KICKOFF,
|
|
29
|
-
SYSTEM_WORK_PLAIN,
|
|
30
|
-
WORK_TURN_KICKOFF_PLAIN,
|
|
31
|
-
withProjectContext,
|
|
32
|
-
} from './prompts.mjs';
|
|
33
26
|
import { knowledgeDirFor } from './knowledgeFiles.mjs';
|
|
34
27
|
import { scrub as envScrub } from './uplinkScrub.mjs';
|
|
35
|
-
import { recordSkills, recordMcpServers } from './runtimeCapabilities.mjs';
|
|
36
28
|
import { isTerminalSessionLive } from './claudeSessions.mjs';
|
|
37
29
|
import { isAgyConversationLive } from './agySessions.mjs';
|
|
30
|
+
import { adoptFence } from './localSessions.mjs';
|
|
38
31
|
import { resumeRetriesFresh } from './resumeLost.mjs';
|
|
39
32
|
import { turnLockedByLivePid } from './turnLock.mjs';
|
|
33
|
+
import { printable } from './printable.mjs';
|
|
40
34
|
import { brainFor } from './workBrain.mjs';
|
|
41
|
-
import {
|
|
42
|
-
import {
|
|
35
|
+
import { AGY_CONV_RE, CLAUDE_SESSION_RE, CODEX_THREAD_RE, RESUME_MARKERS } from './conversationIds.mjs';
|
|
36
|
+
import { sessionTurnKickoff, sessionTurnShape, sessionTurnSystem } from './workTurnShape.mjs';
|
|
43
37
|
import { createToolLog } from './workToolLog.mjs';
|
|
44
38
|
import { createCommandAudit } from './workCommandAudit.mjs';
|
|
45
|
-
import { APPROVAL_REFUSED, codexApprovalSentence, codexLoginFailure, codexLoginSentence, withRelay } from './codexRelay.mjs';
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* THE ONE SENTENCE A PLAN TURN'S SYSTEM PROMPT GAINS (0.97.0), composed here
|
|
49
|
-
* rather than in prompts.mjs because it is a MODIFIER on whichever contract
|
|
50
|
-
* the tab runs, not a contract of its own.
|
|
51
|
-
*
|
|
52
|
-
* The enforcement is not this sentence — it is `--permission-mode plan`, under
|
|
53
|
-
* which the CLI itself refuses every write (claudePosture.mjs, PLAN_MODE_PERM). What
|
|
54
|
-
* the sentence buys is the ANSWER: the plain contract above it says "edit
|
|
55
|
-
* freely, commit", and a turn told only that would spend itself discovering
|
|
56
|
-
* refusals. It also says not to reach for `ExitPlanMode` (measured: disabled
|
|
57
|
-
* under `-p`, and the probe turn then asked the person to leave plan mode
|
|
58
|
-
* themselves) — leaving plan mode is the person's switch in the tab.
|
|
59
|
-
*/
|
|
60
|
-
export const PLAN_TURN_SENTENCE =
|
|
61
|
-
'THIS IS A PLANNING TURN: the person switched this tab to plan mode, so read what you need, decide, and answer with the plan itself as your reply — change nothing (the CLI refuses every write this turn, whatever the mechanics above say about editing, committing or artifacts), and do not try to leave plan mode; they switch it off in the tab when they want the plan carried out.';
|
|
39
|
+
import { APPROVAL_REFUSED, codexApprovalSentence, codexLoginFailure, codexLoginSentence, SESSION_ANSWER_MAX, withRelay } from './codexRelay.mjs';
|
|
62
40
|
|
|
63
41
|
/**
|
|
64
42
|
* HOW LONG, AND HOW MANY, SETTLED TURNS THE LANE REMEMBERS (2026-09-29) — see
|
|
@@ -77,7 +55,7 @@ export function createWorkSessionTurns({
|
|
|
77
55
|
inPlace,
|
|
78
56
|
workChildren,
|
|
79
57
|
workAttempts,
|
|
80
|
-
|
|
58
|
+
notePlace,
|
|
81
59
|
placeOf,
|
|
82
60
|
placeWtFor,
|
|
83
61
|
sessionMetaPath,
|
|
@@ -191,17 +169,21 @@ export function createWorkSessionTurns({
|
|
|
191
169
|
workAnswering.add(job.id);
|
|
192
170
|
const place = job.place || job.sessionId;
|
|
193
171
|
/**
|
|
194
|
-
* VALIDATED AT THE TRUST BOUNDARY,
|
|
195
|
-
* the roster's map
|
|
196
|
-
*
|
|
172
|
+
* VALIDATED AT THE TRUST BOUNDARY, through the same door `learnPlaces`
|
|
173
|
+
* uses for the roster's map (`notePlace`, workPlaces.mjs) — in the same
|
|
174
|
+
* reconcile tick, BEFORE the preview jobs and the sweep read it. An
|
|
197
175
|
* unchecked value stored here reaches every `placeDir` consumer: the
|
|
198
176
|
* turn is spawned in it, `burstListeners` measures it, and a share would
|
|
199
177
|
* publicly tunnel a directory OUTSIDE the checkout — the exact traversal
|
|
200
178
|
* the preview feature's port attribution exists to prevent. Settled out
|
|
201
179
|
* loud rather than skipped, because a silently dropped turn strands the
|
|
202
180
|
* tab for the server's whole expiry window.
|
|
181
|
+
*
|
|
182
|
+
* Stored on success: remembered for every other beat — the sweep, ship,
|
|
183
|
+
* the preview re-check — so they all ask the same directory this turn
|
|
184
|
+
* runs in.
|
|
203
185
|
*/
|
|
204
|
-
if (
|
|
186
|
+
if (!notePlace(job.sessionId, place)) {
|
|
205
187
|
void settleTurn(job.id, {
|
|
206
188
|
ok: false,
|
|
207
189
|
answer:
|
|
@@ -210,9 +192,6 @@ export function createWorkSessionTurns({
|
|
|
210
192
|
releaseSlot();
|
|
211
193
|
continue;
|
|
212
194
|
}
|
|
213
|
-
// Remembered for every other beat — the sweep, ship, the preview
|
|
214
|
-
// re-check — so they all ask the same directory this turn runs in.
|
|
215
|
-
sessionPlaces.set(job.sessionId, place);
|
|
216
195
|
// A READER: other turns in this place run alongside it. See `inPlace`.
|
|
217
196
|
inPlace(place, false, async () => {
|
|
218
197
|
/** This turn's artifact snapshot — set once the place is resolved and
|
|
@@ -240,6 +219,20 @@ export function createWorkSessionTurns({
|
|
|
240
219
|
* out here beside `usage`, and spread wherever it is. Last write wins
|
|
241
220
|
* across a fresh retry: that spawn is the one that answered. */
|
|
242
221
|
let model = null;
|
|
222
|
+
/**
|
|
223
|
+
* WHAT THIS TURN SPENT AND DID, as every settle after a CLI may have
|
|
224
|
+
* run reports it (audit 2026-10-10, F164): its tool log when it has
|
|
225
|
+
* rows, its usage and its model, each only when measured. ONE
|
|
226
|
+
* null-safe spread for the empty adoption, the answer and the catch —
|
|
227
|
+
* the catch's own copy is where the 2026-09-26 ReferenceError lived,
|
|
228
|
+
* and a field added to one copy and not the others drops exactly when
|
|
229
|
+
* a turn fails.
|
|
230
|
+
*/
|
|
231
|
+
const spent = () => ({
|
|
232
|
+
...(toolLog?.ev?.length > 0 ? { tools: toolLog } : {}),
|
|
233
|
+
...(usage ? { usage } : {}),
|
|
234
|
+
...(model ? { model } : {}),
|
|
235
|
+
});
|
|
243
236
|
/** The `startingIn` key this turn claimed — released in the outer
|
|
244
237
|
* `finally`, and only by the turn that took it. */
|
|
245
238
|
let startClaim = null;
|
|
@@ -254,8 +247,11 @@ export function createWorkSessionTurns({
|
|
|
254
247
|
});
|
|
255
248
|
return;
|
|
256
249
|
}
|
|
250
|
+
// A person's name and their tab's, both member-written, printed on
|
|
251
|
+
// the operator's console: scrubbed (2026-10-10, the audit's F097;
|
|
252
|
+
// see printable.mjs).
|
|
257
253
|
note(
|
|
258
|
-
`${c.cyan('tab')} ${c.dim(`— ${job.askedByName || 'the owner'} in "${job.sessionName || 'a session'}"`)}`
|
|
254
|
+
`${c.cyan('tab')} ${c.dim(`— ${printable(job.askedByName) || 'the owner'} in "${printable(job.sessionName) || 'a session'}"`)}`
|
|
259
255
|
);
|
|
260
256
|
// ── ADOPTION: a tab born from a TERMINAL session ────────────────
|
|
261
257
|
// The server sends `adopt {id, cwd}` only while the session has no
|
|
@@ -297,28 +293,17 @@ export function createWorkSessionTurns({
|
|
|
297
293
|
// Inside the repo, outside the daemon's own worktrees: an adopt
|
|
298
294
|
// source is a HUMAN's checkout, and one of our directories showing
|
|
299
295
|
// up here means a stale or confused offer, not a session to fork.
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
try {
|
|
305
|
-
realRoot = realpathSync(repoRoot);
|
|
306
|
-
} catch {
|
|
307
|
-
/* keep the literal path */
|
|
308
|
-
}
|
|
309
|
-
try {
|
|
310
|
-
realBase = realpathSync(baseDir);
|
|
311
|
-
} catch {
|
|
312
|
-
/* keep the literal path */
|
|
313
|
-
}
|
|
314
|
-
if (!under(srcCwd, realRoot)) {
|
|
296
|
+
// The SAME fence the offer was made through (`adoptFence`,
|
|
297
|
+
// localSessions.mjs), so the two doors cannot disagree.
|
|
298
|
+
const fenced = adoptFence({ repoRoot, excludeDirs: [baseDir] }).check(srcCwd);
|
|
299
|
+
if (fenced === 'outside') {
|
|
315
300
|
await settleTurn(job.id, {
|
|
316
301
|
ok: false,
|
|
317
302
|
answer: "the terminal session's directory is outside this project's repository",
|
|
318
303
|
});
|
|
319
304
|
return;
|
|
320
305
|
}
|
|
321
|
-
if (
|
|
306
|
+
if (fenced === 'ours') {
|
|
322
307
|
await settleTurn(job.id, {
|
|
323
308
|
ok: false,
|
|
324
309
|
answer:
|
|
@@ -381,7 +366,7 @@ export function createWorkSessionTurns({
|
|
|
381
366
|
const startKey = lockPath || dir.wt;
|
|
382
367
|
if (startingIn.has(startKey) || turnLockedByLivePid(lockPath)) {
|
|
383
368
|
warn(
|
|
384
|
-
`a turn is already running in "${job.sessionName || job.sessionId}" — waiting for it to finish`
|
|
369
|
+
`a turn is already running in "${printable(job.sessionName) || job.sessionId}" — waiting for it to finish`
|
|
385
370
|
);
|
|
386
371
|
return;
|
|
387
372
|
}
|
|
@@ -432,43 +417,23 @@ export function createWorkSessionTurns({
|
|
|
432
417
|
});
|
|
433
418
|
return;
|
|
434
419
|
}
|
|
435
|
-
if (adopting && rt.id !== 'claude' && rt.id !== 'antigravity') {
|
|
436
|
-
// An adopt id names a conversation in ITS OWN CLI's store: claude
|
|
437
|
-
// forks it (--resume --fork-session), agy moves it
|
|
438
|
-
// (--conversation). Codex has no adoptable store yet, and its
|
|
439
|
-
// args builder backstops this with a loud throw — but a sentence
|
|
440
|
-
// here beats a stack there.
|
|
441
|
-
await settleTurn(job.id, {
|
|
442
|
-
ok: false,
|
|
443
|
-
answer:
|
|
444
|
-
'adopting this terminal session needs its own CLI on the machine — install it, then try again',
|
|
445
|
-
});
|
|
446
|
-
return;
|
|
447
|
-
}
|
|
448
420
|
/**
|
|
449
|
-
*
|
|
450
|
-
*
|
|
451
|
-
*
|
|
452
|
-
*
|
|
453
|
-
*
|
|
454
|
-
*
|
|
421
|
+
* WHAT THIS TURN IS — plain, plan, capture or build — decided ONCE,
|
|
422
|
+
* off the runtime's row and the job's own switches
|
|
423
|
+
* (workTurnShape.mjs, audit 2026-10-10 F159); everything below
|
|
424
|
+
* reads it instead of re-deciding. Its refusal (an adoption this
|
|
425
|
+
* CLI cannot take, then plan mode on a CLI that cannot spell it)
|
|
426
|
+
* settles here, in its words, in the order the lane always said
|
|
427
|
+
* them.
|
|
455
428
|
*/
|
|
456
|
-
const
|
|
457
|
-
if (
|
|
458
|
-
await settleTurn(job.id, {
|
|
459
|
-
ok: false,
|
|
460
|
-
answer: `plan mode runs on Claude Code only, and this tab is ${rt.id} — turn plan off in the tab, then send the message again`,
|
|
461
|
-
});
|
|
429
|
+
const shape = sessionTurnShape({ runtime: rt.id, adopting, job });
|
|
430
|
+
if (shape.refusal) {
|
|
431
|
+
await settleTurn(job.id, { ok: false, answer: shape.refusal });
|
|
462
432
|
return;
|
|
463
433
|
}
|
|
464
|
-
// A PLAIN tab (agy) mounts no MCP: no credential to mint
|
|
465
|
-
// to write. The trade is stated in SYSTEM_WORK_PLAIN — no cards, no
|
|
466
|
-
// streaming — and the honesty survives on the existing rails: the
|
|
467
|
-
// answer lands via work-turn-done, the rail says "no card yet", and
|
|
468
|
-
// ship-time reconciliation books every branch commit.
|
|
469
|
-
const plainTab = rt.id === 'antigravity';
|
|
434
|
+
// A PLAIN tab (agy) mounts no MCP: no credential to mint.
|
|
470
435
|
let mint = null;
|
|
471
|
-
if (!
|
|
436
|
+
if (!shape.plain) {
|
|
472
437
|
mint = await mintWorkToken(job.sessionId);
|
|
473
438
|
if (!mint) mint = await mintWorkToken(job.sessionId, true); // one transient blip ≠ a dead turn
|
|
474
439
|
// Another daemon on this credential holds the session. Return
|
|
@@ -528,11 +493,11 @@ export function createWorkSessionTurns({
|
|
|
528
493
|
*/
|
|
529
494
|
let claudeResumeId = null;
|
|
530
495
|
if (rt.id === 'claude') {
|
|
531
|
-
const convMarker = sessionMetaPath(dir.wt,
|
|
496
|
+
const convMarker = sessionMetaPath(dir.wt, RESUME_MARKERS.claude, job.sessionId);
|
|
532
497
|
if (convMarker && existsSync(convMarker)) {
|
|
533
498
|
try {
|
|
534
499
|
const v = readFileSync(convMarker, 'utf8').trim();
|
|
535
|
-
if (
|
|
500
|
+
if (CLAUDE_SESSION_RE.test(v)) claudeResumeId = v;
|
|
536
501
|
} catch {
|
|
537
502
|
/* unreadable marker — run fresh */
|
|
538
503
|
}
|
|
@@ -540,7 +505,7 @@ export function createWorkSessionTurns({
|
|
|
540
505
|
}
|
|
541
506
|
let codexResumeId = null;
|
|
542
507
|
if (rt.id === 'codex') {
|
|
543
|
-
const threadMarker = sessionMetaPath(dir.wt,
|
|
508
|
+
const threadMarker = sessionMetaPath(dir.wt, RESUME_MARKERS.codex, job.sessionId);
|
|
544
509
|
if (threadMarker && existsSync(threadMarker)) {
|
|
545
510
|
try {
|
|
546
511
|
const v = readFileSync(threadMarker, 'utf8').trim();
|
|
@@ -558,7 +523,7 @@ export function createWorkSessionTurns({
|
|
|
558
523
|
// dispatch sharing the machine could overwrite that between turns.
|
|
559
524
|
let agyConvId = null;
|
|
560
525
|
if (rt.id === 'antigravity' && !adopting) {
|
|
561
|
-
const convMarker = sessionMetaPath(dir.wt,
|
|
526
|
+
const convMarker = sessionMetaPath(dir.wt, RESUME_MARKERS.antigravity, job.sessionId);
|
|
562
527
|
if (convMarker && existsSync(convMarker)) {
|
|
563
528
|
try {
|
|
564
529
|
const v = readFileSync(convMarker, 'utf8').trim();
|
|
@@ -605,17 +570,13 @@ export function createWorkSessionTurns({
|
|
|
605
570
|
const adoptNote = adopting
|
|
606
571
|
? '[ADOPTED SESSION — this conversation was brought in from a terminal. Begin your reply with a 2-3 sentence recap of where it left off and what state carried over, then answer the message.]'
|
|
607
572
|
: '';
|
|
608
|
-
//
|
|
609
|
-
//
|
|
610
|
-
//
|
|
611
|
-
//
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
const mcp =
|
|
616
|
-
plainTab || planTurn
|
|
617
|
-
? { args: [], env: null, dir: null }
|
|
618
|
-
: mcpFor(rt.id, mint.token, getMcpUrl());
|
|
573
|
+
// Mounted only where the shape says (a plain tab and a plan turn run
|
|
574
|
+
// without one — see workTurnShape.mjs). A plan turn's credential is
|
|
575
|
+
// still minted above: its lease answers (held elsewhere, gone) are
|
|
576
|
+
// about the SESSION, and a plan turn needs them as much as any other.
|
|
577
|
+
const mcp = shape.mountMcp
|
|
578
|
+
? mcpFor(rt.id, mint.token, getMcpUrl())
|
|
579
|
+
: { args: [], env: null, dir: null };
|
|
619
580
|
// The tab's model/effort, if it named any. Spread into turnArgs so
|
|
620
581
|
// BOTH runTurn calls below carry it — the retry is the same turn on
|
|
621
582
|
// the same brain, not a quieter second opinion.
|
|
@@ -661,42 +622,20 @@ export function createWorkSessionTurns({
|
|
|
661
622
|
const message = [job.body, filesNote, adoptNote, carryNote]
|
|
662
623
|
.filter(Boolean)
|
|
663
624
|
.join('\n\n');
|
|
664
|
-
// A CAPTURE chat (the board's New task conversation): its own
|
|
665
|
-
// system prompt, its own kickoff, and the scratch planner's
|
|
666
|
-
// READ-ONLY permission profile — the prompt says stage-never-file
|
|
667
|
-
// and the profile is what makes "read-only" true rather than
|
|
668
|
-
// asserted. Server-flagged per job; a server too old to flag it
|
|
669
|
-
// simply runs an ordinary tab, which the web's version floor
|
|
670
|
-
// prevents ever being offered.
|
|
671
|
-
const captureTab = job.capture === true;
|
|
672
625
|
const turnArgs = {
|
|
673
|
-
//
|
|
674
|
-
//
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
:
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
askedByName: job.askedByName,
|
|
687
|
-
})
|
|
688
|
-
: WORK_TURN_KICKOFF({
|
|
689
|
-
sessionId: job.sessionId,
|
|
690
|
-
sessionName: job.sessionName,
|
|
691
|
-
message,
|
|
692
|
-
askedByName: job.askedByName,
|
|
693
|
-
}),
|
|
694
|
-
// ONE PROFILE (turnProfile.mjs): the capture chat's read-only
|
|
695
|
-
// `plan` list; or a plan turn's `plan-mode` — `--permission-mode
|
|
696
|
-
// plan` INSTEAD of the build posture, never beside
|
|
697
|
-
// `--dangerously-skip-permissions`, which silently wins
|
|
698
|
-
// (claudePosture.mjs, PLAN_MODE_PERM); else the build posture.
|
|
699
|
-
profile: captureTab ? 'plan' : planTurn ? 'plan-mode' : 'build',
|
|
626
|
+
// The kickoff under the turn's contract: a plain tab (or a plan
|
|
627
|
+
// turn) has no tools to name and no session id to pass, and asks
|
|
628
|
+
// for one complete report instead of a stream; a capture chat
|
|
629
|
+
// restates its posture.
|
|
630
|
+
prompt: sessionTurnKickoff(shape, {
|
|
631
|
+
sessionId: job.sessionId,
|
|
632
|
+
sessionName: job.sessionName,
|
|
633
|
+
message,
|
|
634
|
+
askedByName: job.askedByName,
|
|
635
|
+
}),
|
|
636
|
+
// ONE PROFILE (turnProfile.mjs), the shape's: `plan` for the
|
|
637
|
+
// capture chat, `plan-mode` for a plan turn, else `build`.
|
|
638
|
+
profile: shape.profile,
|
|
700
639
|
// The adopt turn resumes the TERMINAL conversation by forking it
|
|
701
640
|
// into this cwd (claude: --resume <id> --fork-session). After it
|
|
702
641
|
// speaks once, the fork lives natively here and turn 2+ is the
|
|
@@ -709,18 +648,14 @@ export function createWorkSessionTurns({
|
|
|
709
648
|
...(() => {
|
|
710
649
|
const knowledgeDir = knowledgeDirFor(repoRoot);
|
|
711
650
|
return {
|
|
712
|
-
//
|
|
713
|
-
//
|
|
714
|
-
// paragraph
|
|
715
|
-
//
|
|
716
|
-
system:
|
|
717
|
-
|
|
718
|
-
:
|
|
719
|
-
|
|
720
|
-
// ARTIFACTS (0.94.0): every tab but the read-only capture
|
|
721
|
-
// chat, and only while the server can show one.
|
|
722
|
-
{ knowledgeDir, artifacts: !captureTab && getArtifactsAccepted() }
|
|
723
|
-
),
|
|
651
|
+
// The shape's contract (a plan turn: the PLAIN one, no
|
|
652
|
+
// artifacts paragraph, the planning sentence appended). The
|
|
653
|
+
// artifacts paragraph also waits on the server — ARTIFACTS
|
|
654
|
+
// (0.94.0): only while it can show one, read here at spawn.
|
|
655
|
+
system: sessionTurnSystem(shape, {
|
|
656
|
+
knowledgeDir,
|
|
657
|
+
artifactsAccepted: getArtifactsAccepted(),
|
|
658
|
+
}),
|
|
724
659
|
// The directory is OUTSIDE a worktree's cwd; Claude Code is
|
|
725
660
|
// told it may read there (`--add-dir`) rather than left to
|
|
726
661
|
// refuse a read-only capture turn a path it was just handed.
|
|
@@ -729,12 +664,11 @@ export function createWorkSessionTurns({
|
|
|
729
664
|
})(),
|
|
730
665
|
// Present only when the tab named one — see brainFor.
|
|
731
666
|
...brain,
|
|
732
|
-
// The tab watches the CLI work
|
|
733
|
-
//
|
|
734
|
-
//
|
|
735
|
-
//
|
|
736
|
-
//
|
|
737
|
-
streamJson: true,
|
|
667
|
+
// The tab watches the CLI work (every CLI speaks events);
|
|
668
|
+
// `answerFromResult` keeps `out` — which IS the reply posted to
|
|
669
|
+
// the transcript — to the final result, so streamed prose is
|
|
670
|
+
// narrated once and posted once. Every line goes to the narrator
|
|
671
|
+
// above, throttled.
|
|
738
672
|
answerFromResult: true,
|
|
739
673
|
onActivity: (a) => {
|
|
740
674
|
narrator.line(a?.label);
|
|
@@ -756,16 +690,12 @@ export function createWorkSessionTurns({
|
|
|
756
690
|
onModel: (m) => {
|
|
757
691
|
model = m;
|
|
758
692
|
},
|
|
759
|
-
// What this CLI says it can be asked for by name
|
|
760
|
-
//
|
|
761
|
-
//
|
|
762
|
-
//
|
|
763
|
-
//
|
|
693
|
+
// What this CLI says it can be asked for by name, and which of
|
|
694
|
+
// its connectors need a sign-in, are harvested off this init by
|
|
695
|
+
// runTurn itself (`initHarvestFor`, runtimeCapabilities.mjs) —
|
|
696
|
+
// a tab's build turn is the machine's measurement; its fenced
|
|
697
|
+
// capture chat is not.
|
|
764
698
|
onInit: (i) => {
|
|
765
|
-
recordSkills(i.skills);
|
|
766
|
-
// The CLI's mounted MCP servers and connectors (0.97.0) — the
|
|
767
|
-
// same free fact off the same event; see recordMcpServers.
|
|
768
|
-
recordMcpServers(i.mcpServers);
|
|
769
699
|
// The conversation this turn is actually speaking under. Held
|
|
770
700
|
// and persisted after the turn ends, so the NEXT one resumes
|
|
771
701
|
// this exact thread rather than whatever the directory saw
|
|
@@ -777,11 +707,9 @@ export function createWorkSessionTurns({
|
|
|
777
707
|
cwd: dir.wt,
|
|
778
708
|
mcpArgs: mcp.args,
|
|
779
709
|
mcpEnv: mcp.env,
|
|
780
|
-
// A
|
|
781
|
-
//
|
|
782
|
-
|
|
783
|
-
// talking to it may be on a phone (noWindowEnv.mjs).
|
|
784
|
-
display: !captureTab,
|
|
710
|
+
// A Terminal tab keeps its environment's display; the capture
|
|
711
|
+
// chat does not (workTurnShape.mjs).
|
|
712
|
+
display: shape.display,
|
|
785
713
|
runtime: rt.id,
|
|
786
714
|
label: c.cyan('[tab]'),
|
|
787
715
|
// Only codex announces one (thread.started); held here so the id
|
|
@@ -888,9 +816,9 @@ export function createWorkSessionTurns({
|
|
|
888
816
|
if (
|
|
889
817
|
rt.id === 'claude' &&
|
|
890
818
|
seenClaudeSession &&
|
|
891
|
-
|
|
819
|
+
CLAUDE_SESSION_RE.test(seenClaudeSession)
|
|
892
820
|
) {
|
|
893
|
-
const convMarker = sessionMetaPath(dir.wt,
|
|
821
|
+
const convMarker = sessionMetaPath(dir.wt, RESUME_MARKERS.claude, job.sessionId);
|
|
894
822
|
if (convMarker) {
|
|
895
823
|
try {
|
|
896
824
|
writeFileSync(convMarker, seenClaudeSession);
|
|
@@ -900,7 +828,7 @@ export function createWorkSessionTurns({
|
|
|
900
828
|
}
|
|
901
829
|
}
|
|
902
830
|
if (rt.id === 'codex' && seenThreadId && CODEX_THREAD_RE.test(seenThreadId)) {
|
|
903
|
-
const threadMarker = sessionMetaPath(dir.wt,
|
|
831
|
+
const threadMarker = sessionMetaPath(dir.wt, RESUME_MARKERS.codex, job.sessionId);
|
|
904
832
|
if (threadMarker) {
|
|
905
833
|
try {
|
|
906
834
|
writeFileSync(threadMarker, seenThreadId);
|
|
@@ -923,11 +851,9 @@ export function createWorkSessionTurns({
|
|
|
923
851
|
ok: false,
|
|
924
852
|
answer: "Couldn't resume the terminal session — it may have been removed.",
|
|
925
853
|
// Whatever it DID before coming back empty is exactly the
|
|
926
|
-
// question a failed turn's log answers
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
...(usage ? { usage } : {}),
|
|
930
|
-
...(model ? { model } : {}),
|
|
854
|
+
// question a failed turn's log answers — and an empty fork
|
|
855
|
+
// still spent what it spent.
|
|
856
|
+
...spent(),
|
|
931
857
|
});
|
|
932
858
|
warn('adopt turn produced no output — settled as failed');
|
|
933
859
|
return;
|
|
@@ -938,7 +864,7 @@ export function createWorkSessionTurns({
|
|
|
938
864
|
// cwd registry. From here on the marker is the tab's identity and
|
|
939
865
|
// the registry is never trusted again.
|
|
940
866
|
if (rt.id === 'antigravity' && answer.length > 0) {
|
|
941
|
-
const convMarker = sessionMetaPath(dir.wt,
|
|
867
|
+
const convMarker = sessionMetaPath(dir.wt, RESUME_MARKERS.antigravity, job.sessionId);
|
|
942
868
|
if (convMarker && !existsSync(convMarker)) {
|
|
943
869
|
const learned = adopting ? job.adopt.id : agyRegistryLookup(dir.wt);
|
|
944
870
|
if (learned && AGY_CONV_RE.test(learned)) {
|
|
@@ -980,7 +906,7 @@ export function createWorkSessionTurns({
|
|
|
980
906
|
? refusedLine
|
|
981
907
|
? withRelay(refusedLine, answer)
|
|
982
908
|
: // Scrub: a reply can quote config or env-adjacent code.
|
|
983
|
-
envScrub(answer).slice(0,
|
|
909
|
+
envScrub(answer).slice(0, SESSION_ANSWER_MAX)
|
|
984
910
|
: (refusedLine ?? noWords),
|
|
985
911
|
// Only a turn that actually SPOKE proves a conversation lives
|
|
986
912
|
// here. Recording the path unconditionally is how a crashed first
|
|
@@ -989,9 +915,7 @@ export function createWorkSessionTurns({
|
|
|
989
915
|
// The turn's tool log, in final form — the durable copy that lands
|
|
990
916
|
// on the settled message (the live copy on the record is cleared
|
|
991
917
|
// at settle). Already scrubbed at collection.
|
|
992
|
-
...(
|
|
993
|
-
...(usage ? { usage } : {}),
|
|
994
|
-
...(model ? { model } : {}),
|
|
918
|
+
...spent(),
|
|
995
919
|
});
|
|
996
920
|
if (spoke) ok(`${c.cyan('tab')} ${c.dim('— replied in the session')}`);
|
|
997
921
|
else if (loginFailure) warn('codex answered 401 for this session turn — settled as failed, in words');
|
|
@@ -1004,11 +928,9 @@ export function createWorkSessionTurns({
|
|
|
1004
928
|
// synced secret.
|
|
1005
929
|
answer: envScrub(String(e?.message ?? 'the session turn failed')).slice(0, 2000),
|
|
1006
930
|
// "What did it do before it failed" is exactly the question a
|
|
1007
|
-
// crashed turn's log answers
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
...(usage ? { usage } : {}),
|
|
1011
|
-
...(model ? { model } : {}),
|
|
931
|
+
// crashed turn's log answers, and a turn that threw after its CLI
|
|
932
|
+
// ran still spent what it spent — the main settle's own spread.
|
|
933
|
+
...spent(),
|
|
1012
934
|
});
|
|
1013
935
|
warn(`session turn failed: ${e?.message ?? e}`);
|
|
1014
936
|
} finally {
|