flowviant 0.124.0 → 0.125.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 +6 -0
- package/bin/cli.mjs +7 -0
- package/bin/lib/agentInternalWait.mjs +2 -1
- package/bin/lib/agentPlan.mjs +9 -0
- package/bin/lib/agentRepositories.mjs +48 -0
- package/bin/lib/agentTaskKinds.mjs +2 -0
- package/bin/lib/checkoutIdentity.mjs +25 -0
- package/bin/lib/claudeInput.mjs +32 -0
- package/bin/lib/claudeSessions.mjs +20 -0
- package/bin/lib/credentials.mjs +52 -17
- package/bin/lib/daemonLocks.mjs +21 -3
- package/bin/lib/fleet.mjs +19 -6
- package/bin/lib/fleetRoster.mjs +1 -89
- package/bin/lib/fleetStartup.mjs +11 -14
- package/bin/lib/holder.mjs +4 -37
- package/bin/lib/instance.mjs +20 -7
- package/bin/lib/landed.mjs +2 -2
- package/bin/lib/login.mjs +39 -4
- package/bin/lib/mergeBatch.mjs +172 -0
- package/bin/lib/placeLock.mjs +9 -7
- package/bin/lib/prWorkflow.mjs +15 -0
- package/bin/lib/projectLanded.mjs +16 -0
- package/bin/lib/projectRepositories.mjs +60 -0
- package/bin/lib/projectTools.mjs +2 -1
- package/bin/lib/projectWorktreeHome.mjs +25 -0
- package/bin/lib/prompts.mjs +128 -48
- package/bin/lib/repositoryDelivery.mjs +64 -0
- package/bin/lib/repositoryMerge.mjs +116 -0
- package/bin/lib/repositoryPlaces.mjs +179 -0
- package/bin/lib/repositorySetup.mjs +58 -0
- package/bin/lib/repositoryTool.mjs +71 -0
- package/bin/lib/runTurn.mjs +23 -2
- package/bin/lib/runtimeAntigravity.mjs +2 -1
- package/bin/lib/runtimeClaude.mjs +5 -2
- package/bin/lib/runtimeCodex.mjs +2 -1
- package/bin/lib/shipMerge.mjs +5 -2
- package/bin/lib/startProject.mjs +4 -4
- package/bin/lib/trace.mjs +8 -3
- package/bin/lib/turnDeclaration.mjs +88 -0
- package/bin/lib/turnLock.mjs +48 -8
- package/bin/lib/work.mjs +23 -24
- package/bin/lib/workAgentCheck.mjs +6 -5
- package/bin/lib/workAgentMerges.mjs +24 -4
- package/bin/lib/workAgentPrecheck.mjs +5 -5
- package/bin/lib/workAgentPublish.mjs +18 -5
- package/bin/lib/workAgentRecovery.mjs +85 -0
- package/bin/lib/workAgentReview.mjs +5 -3
- package/bin/lib/workAgentTurnBegun.mjs +6 -4
- package/bin/lib/workAgentTurnExecution.mjs +79 -9
- package/bin/lib/workAgentTurnOutcome.mjs +20 -5
- package/bin/lib/workAgentTurns.mjs +33 -9
- package/bin/lib/workCappedTurn.mjs +6 -4
- package/bin/lib/workDelivery.mjs +21 -0
- package/bin/lib/workDiffs.mjs +18 -11
- package/bin/lib/workLiveMessages.mjs +129 -0
- package/bin/lib/workMergeBatch.mjs +104 -0
- package/bin/lib/workPlaceReports.mjs +5 -0
- package/bin/lib/workPlaces.mjs +6 -1
- package/bin/lib/workRetire.mjs +5 -0
- package/bin/lib/workSessionTurns.mjs +72 -40
- package/bin/lib/workShip.mjs +17 -16
- package/package.json +1 -1
- package/npm-shrinkwrap.json +0 -69
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { finishRepositoryDelivery, allResolutionsCommitted } from './repositoryDelivery.mjs';
|
|
1
2
|
/**
|
|
2
3
|
* ONE AGENT TURN, RUN: the worktree, the CLI, and what it left behind.
|
|
3
4
|
*
|
|
@@ -38,12 +39,17 @@ import { runTurnResumingOnce } from './resumeLost.mjs';
|
|
|
38
39
|
import { INTERNAL_WAIT_CONTRACT, runAgentWithInternalWaits } from './agentInternalWait.mjs';
|
|
39
40
|
import { CODEX_THREAD_RE } from './workSessionRuntime.mjs';
|
|
40
41
|
import { brainFor } from './workBrain.mjs';
|
|
42
|
+
import { refreshRestartBase } from './workAgentRecovery.mjs';
|
|
41
43
|
import { begunTurnRefusal } from './workAgentTurnBegun.mjs';
|
|
42
44
|
import { agentTurnSettlement } from './workAgentTurnOutcome.mjs';
|
|
43
45
|
import { ensureArtifactDir, ensureFenceScratch } from './artifacts.mjs';
|
|
44
46
|
import { agentBrowserHome } from './noWindowEnv.mjs';
|
|
47
|
+
import { CLAUDE_SESSION_RE, RESUME_MARKERS } from './conversationIds.mjs';
|
|
48
|
+
import { createDeliveryReporter } from './workDelivery.mjs';
|
|
49
|
+
import { createLiveMessages } from './workLiveMessages.mjs';
|
|
45
50
|
|
|
46
51
|
export function createAgentTurnExecution({
|
|
52
|
+
agentRepositories,
|
|
47
53
|
makeBrief,
|
|
48
54
|
baseRef,
|
|
49
55
|
baseDir,
|
|
@@ -63,7 +69,9 @@ export function createAgentTurnExecution({
|
|
|
63
69
|
postAgentActivity,
|
|
64
70
|
postAgentParked,
|
|
65
71
|
fetchAgentFiles,
|
|
72
|
+
makeLiveMessages = createLiveMessages,
|
|
66
73
|
}) {
|
|
74
|
+
const reportDelivery = createDeliveryReporter();
|
|
67
75
|
/** How long the final flush may hold the settle. Bounded because the settle
|
|
68
76
|
* is the turn's contract and the trace is a readout: a wedged uplink costs
|
|
69
77
|
* the tail of a trace, never the answer behind it. */
|
|
@@ -114,6 +122,11 @@ export function createAgentTurnExecution({
|
|
|
114
122
|
/** `run` is the lane's record of this turn (`interrupted`, set by
|
|
115
123
|
* `interruptAgentTurns`); absent, nothing can interrupt it. */
|
|
116
124
|
const runAgentTurnInPlace = async (job, { turnId, agentId, place, releaseSlot, postAgentTurn, measured, run = { interrupted: false } }) => {
|
|
125
|
+
let repositoryRun = null;
|
|
126
|
+
const setupController = new AbortController();
|
|
127
|
+
run.abortSetup = () => setupController.abort();
|
|
128
|
+
if (run.interrupted) setupController.abort();
|
|
129
|
+
try {
|
|
117
130
|
// WORK THAT HAS BEGUN LIVES ON EXACTLY ONE BOX — read BEFORE anything is
|
|
118
131
|
// cut, because `placeWtFor` would cut a rival branch of the same name. The
|
|
119
132
|
// rule and its three-state branch measurement: workAgentTurnBegun.mjs.
|
|
@@ -124,7 +137,10 @@ export function createAgentTurnExecution({
|
|
|
124
137
|
return;
|
|
125
138
|
}
|
|
126
139
|
}
|
|
127
|
-
|
|
140
|
+
if (!(await refreshRestartBase(job, repoRoot))) return;
|
|
141
|
+
repositoryRun = await agentRepositories?.prepare(job, place, { signal: setupController.signal });
|
|
142
|
+
measured.repositoryReceipts = repositoryRun?.receipts;
|
|
143
|
+
const dir = repositoryRun ?? placeWtFor(place);
|
|
128
144
|
if (!dir) {
|
|
129
145
|
// No worktree and none could be cut. `nothing`, WITH git's own words:
|
|
130
146
|
// without them the agent sat in Stuck with no question and no reason
|
|
@@ -294,10 +310,13 @@ export function createAgentTurnExecution({
|
|
|
294
310
|
}
|
|
295
311
|
}
|
|
296
312
|
let seenThreadId = null;
|
|
313
|
+
const claudeMarker = rt === 'claude' ? sessionMetaPath(wt, RESUME_MARKERS.claude, agentId) : null;
|
|
314
|
+
let claudeResumeId = null;
|
|
315
|
+
try { const id = readFileSync(claudeMarker, 'utf8').trim(); if (CLAUDE_SESSION_RE.test(id)) claudeResumeId = id; } catch { /* unknown */ }
|
|
297
316
|
const resume =
|
|
298
317
|
rt === 'codex'
|
|
299
318
|
? Boolean(codexResumeId)
|
|
300
|
-
: rt === 'claude' && Boolean(ranMarker && existsSync(ranMarker));
|
|
319
|
+
: rt === 'claude' && (Boolean(claudeResumeId) || Boolean(ranMarker && existsSync(ranMarker)));
|
|
301
320
|
/**
|
|
302
321
|
* …AND THE HOME THAT THREAD IS FILED UNDER, KEPT PER AGENT (2026-09-29).
|
|
303
322
|
*
|
|
@@ -454,7 +473,14 @@ export function createAgentTurnExecution({
|
|
|
454
473
|
try {
|
|
455
474
|
// A turn reads the base ref afresh. Changes the agent has made to its
|
|
456
475
|
// own worktree cannot silently change the next turn's tools.
|
|
457
|
-
|
|
476
|
+
const sources = repositoryRun?.sources ?? [];
|
|
477
|
+
const source = sources.find((r) => r.wt === wt);
|
|
478
|
+
const snapshot = readBaseTools(source?.repoRoot ?? repoRoot, source?.baseRef ?? baseRef());
|
|
479
|
+
for (const other of sources.filter((r) => r !== source)) {
|
|
480
|
+
const base = readBaseTools(other.repoRoot, other.baseRef);
|
|
481
|
+
snapshot.instructions.push(...[...base.instructions, ...base.skillFiles].map((file) => ({ ...file, path: `repositories/${other.folder}/${file.path}` })));
|
|
482
|
+
}
|
|
483
|
+
projectTools = prepareAgentTools(snapshot, rt, process.env, wt, { codexHome: codexAgentHome, turnServers: repositoryRun?.tool ? { card_repositories: repositoryRun.tool.config } : {} });
|
|
458
484
|
const mountsProjectMcp = TURN_PROFILES[posture]?.projectMcp === true && (rt === 'claude' || rt === 'codex');
|
|
459
485
|
// A fenced posture on a runner that ignores its own file there (Codex's
|
|
460
486
|
// `--ignore-user-config`) takes the servers as `fencedMount` instead.
|
|
@@ -475,6 +501,16 @@ export function createAgentTurnExecution({
|
|
|
475
501
|
unmounted: [...projectTools.mcpUnmounted.map((u) => ({ ...u, source: 'project' })), ...(fencedMount ? fencedMount.unmounted : [])],
|
|
476
502
|
});
|
|
477
503
|
const agentTurnArgs = {
|
|
504
|
+
...(rt === 'claude' ? { liveMessages: (run.live = makeLiveMessages({
|
|
505
|
+
scope: 'agent', turnId, cwd: wt, sessionMetaPath, interrupted: () => run.interrupted,
|
|
506
|
+
format: async (followup) => {
|
|
507
|
+
const attached = followup.attachments?.length ? await fetchAgentFiles(wt, followup.attachments.map((file) => ({ ...file, from: 'message' }))) : { message: [], card: [] };
|
|
508
|
+
return AGENT_HUMAN_KICKOFF({ agentName: job.agentName, message: followup.body + (followup.libraryPrompt ?? ''),
|
|
509
|
+
askedByName: followup.askedByName, task: job.task, cardThread: job.cardThread,
|
|
510
|
+
investigationContext: job.investigationContext, researchContext: job.researchContext,
|
|
511
|
+
messageFiles: attached.message, cardFiles: files.card, position: job.position ?? 1, total: job.total ?? 1 });
|
|
512
|
+
},
|
|
513
|
+
})) } : {}),
|
|
478
514
|
prompt:
|
|
479
515
|
job.kind === 'task' && job.task
|
|
480
516
|
? AGENT_TASK_KICKOFF({
|
|
@@ -485,6 +521,7 @@ export function createAgentTurnExecution({
|
|
|
485
521
|
// on it (0.112.0), after it, the same way.
|
|
486
522
|
resumeBody: job.body,
|
|
487
523
|
investigationContext: job.investigationContext,
|
|
524
|
+
researchContext: job.researchContext,
|
|
488
525
|
cardThread: job.cardThread,
|
|
489
526
|
cardFiles: files.card,
|
|
490
527
|
position: job.position ?? 1,
|
|
@@ -496,6 +533,7 @@ export function createAgentTurnExecution({
|
|
|
496
533
|
interrupted: job.interrupted === true,
|
|
497
534
|
message: job.body ?? '',
|
|
498
535
|
investigationContext: job.investigationContext,
|
|
536
|
+
researchContext: job.researchContext,
|
|
499
537
|
askedByName: job.askedByName,
|
|
500
538
|
task: job.task,
|
|
501
539
|
// The discussion on the one card this message is about, in a
|
|
@@ -518,9 +556,10 @@ export function createAgentTurnExecution({
|
|
|
518
556
|
// ARTIFACTS (0.94.0), while the server can show one — an agent's
|
|
519
557
|
// land on its page, under the facts row.
|
|
520
558
|
artifacts: getArtifactsAccepted(),
|
|
521
|
-
}) + (toolsNote ? `\n\n${toolsNote}` : '') + (artifactInstructions ? `\n\n${artifactInstructions}` : '') + `\n\n${INTERNAL_WAIT_CONTRACT}` + (projectTools.instructions ? `\n\n${projectTools.instructions}` : ''),
|
|
559
|
+
}) + (toolsNote ? `\n\n${toolsNote}` : '') + (artifactInstructions ? `\n\n${artifactInstructions}` : '') + `\n\n${INTERNAL_WAIT_CONTRACT}` + (projectTools.instructions ? `\n\n${projectTools.instructions}` : '') + (repositoryRun ? `\n\n${repositoryRun.note}\nRepository command argv: ${JSON.stringify([repositoryRun.tool?.config.command, ...(repositoryRun.tool?.config.args.slice(0, -1) ?? []), "__repository", "<name>"])}` : ''),
|
|
522
560
|
agentTools: projectTools,
|
|
523
|
-
|
|
561
|
+
writableDirs: repositoryRun?.writableDirs,
|
|
562
|
+
...((projectTools.codexHome || fencedMount || repositoryRun?.tool) ? { mcpEnv: { ...(projectTools.codexHome ? { CODEX_HOME: projectTools.codexHome } : {}), ...(fencedMount?.env ?? {}), ...(repositoryRun?.tool?.env ?? {}) } } : {}),
|
|
524
563
|
// NOBODY IS AT THIS SCREEN (2026-09-29): no display, BROWSER=none, and
|
|
525
564
|
// a browser profile of the agent's own, kept beside its Codex home —
|
|
526
565
|
// so a Chrome it starts to look at its page never reaches the
|
|
@@ -542,7 +581,11 @@ export function createAgentTurnExecution({
|
|
|
542
581
|
cwd: wt,
|
|
543
582
|
runtime: rt,
|
|
544
583
|
resume,
|
|
545
|
-
resumeThreadId: codexResumeId || undefined,
|
|
584
|
+
resumeThreadId: codexResumeId || claudeResumeId || undefined,
|
|
585
|
+
onInit: (init) => {
|
|
586
|
+
if (claudeMarker && CLAUDE_SESSION_RE.test(init.sessionId ?? ''))
|
|
587
|
+
try { writeFileSync(claudeMarker, init.sessionId); } catch { /* best effort */ }
|
|
588
|
+
},
|
|
546
589
|
onThreadId: (id) => {
|
|
547
590
|
seenThreadId = String(id ?? '').trim() || seenThreadId;
|
|
548
591
|
},
|
|
@@ -558,7 +601,7 @@ export function createAgentTurnExecution({
|
|
|
558
601
|
// board past ~90 seconds.
|
|
559
602
|
onActivity: (a) => {
|
|
560
603
|
brief?.append(a?.kind ?? 'activity', a?.full ?? a?.label);
|
|
561
|
-
|
|
604
|
+
let line = a?.label;
|
|
562
605
|
if (!line) return;
|
|
563
606
|
// THE TRACE TAKES EVERY LINE; the pulse takes one every two
|
|
564
607
|
// seconds. Two channels, one stream, and the drop-sampler stays a
|
|
@@ -573,7 +616,12 @@ export function createAgentTurnExecution({
|
|
|
573
616
|
// daemon carried for all of them. The PULSE below is deliberately
|
|
574
617
|
// still `line`: it is one overwritten line on a board, and a
|
|
575
618
|
// paragraph there would be a paragraph nobody can read.
|
|
576
|
-
if (!doubledKinds || !doubledKinds.has(a.kind))
|
|
619
|
+
if (!doubledKinds || !doubledKinds.has(a.kind)) {
|
|
620
|
+
const full = a.full ?? line;
|
|
621
|
+
const readable = trace.prose(a.kind, full);
|
|
622
|
+
if (readable === null) return;
|
|
623
|
+
if (readable !== full) line = envScrub(readable).replace(/\s+/g, ' ').trim().slice(0, 160);
|
|
624
|
+
}
|
|
577
625
|
const now = Date.now();
|
|
578
626
|
if (now - (lastAgentBeat.get(agentId) ?? 0) < 2_000) return;
|
|
579
627
|
lastAgentBeat.set(agentId, now);
|
|
@@ -612,10 +660,12 @@ export function createAgentTurnExecution({
|
|
|
612
660
|
// (runTurn.mjs, `initHarvestFor`). An agent-only box learns from the
|
|
613
661
|
// startup probe and its Terminal tabs.
|
|
614
662
|
onSpawn: (ch) => {
|
|
663
|
+
if (rt !== 'claude') reportDelivery('agent', job, 'taken');
|
|
615
664
|
brief?.running(true);
|
|
616
665
|
ch.once('close', () => { if (child === ch) brief?.running(false); });
|
|
617
666
|
child = ch;
|
|
618
667
|
spawnedChildren.add(ch);
|
|
668
|
+
measured.ranCli = true;
|
|
619
669
|
// The AGENT it serves — what the machine snapshot charges this
|
|
620
670
|
// child's memory to.
|
|
621
671
|
workChildren.set(ch, agentId);
|
|
@@ -664,12 +714,30 @@ export function createAgentTurnExecution({
|
|
|
664
714
|
void postAgentActivity(agentId, 'resuming after an internal wait');
|
|
665
715
|
} }
|
|
666
716
|
);
|
|
717
|
+
out = await finishRepositoryDelivery({
|
|
718
|
+
job, run: repositoryRun, out, answer: () => lastAnswer,
|
|
719
|
+
resume: async (prompt) => {
|
|
720
|
+
lastAnswer = null;
|
|
721
|
+
const counted = usage;
|
|
722
|
+
return runCli({
|
|
723
|
+
...agentTurnArgs, prompt, liveContinuation: true,
|
|
724
|
+
onUsage: (next) => agentTurnArgs.onUsage(Object.fromEntries(
|
|
725
|
+
['input', 'output', 'cacheCreate', 'cacheRead'].map((k) =>
|
|
726
|
+
[k, (counted?.[k] ?? 0) + (next?.[k] ?? 0)])
|
|
727
|
+
)),
|
|
728
|
+
writableDirs: repositoryRun.sourcesNow().filter((r) => r.wt !== wt).map((r) => r.wt),
|
|
729
|
+
resume: true,
|
|
730
|
+
resumeThreadId: seenThreadId ?? agentTurnArgs.resumeThreadId,
|
|
731
|
+
});
|
|
732
|
+
},
|
|
733
|
+
});
|
|
667
734
|
} finally {
|
|
668
735
|
await brief?.stop();
|
|
669
736
|
// What the CLI counted, for the lane's catch if anything below throws.
|
|
670
737
|
measured.usage = usage;
|
|
671
738
|
measured.model = model;
|
|
672
739
|
projectTools?.cleanup();
|
|
740
|
+
|
|
673
741
|
/**
|
|
674
742
|
* THE TAIL, BEFORE THE SETTLE — so the last thing the agent did is on
|
|
675
743
|
* the record by the time the board is told the turn is over.
|
|
@@ -721,7 +789,7 @@ export function createAgentTurnExecution({
|
|
|
721
789
|
}
|
|
722
790
|
|
|
723
791
|
|
|
724
|
-
const commits = commitsBetween(wt, before);
|
|
792
|
+
const commits = repositoryRun ? [...new Set(repositoryRun.receipts().map((r) => r.sha))].slice(0, 50) : commitsBetween(wt, before);
|
|
725
793
|
/**
|
|
726
794
|
* A PERSON INTERRUPTED IT (Interrupt now). Whatever the CLI printed was
|
|
727
795
|
* cut off, so there is no outcome to read: the turn reports `interrupted`
|
|
@@ -771,12 +839,14 @@ export function createAgentTurnExecution({
|
|
|
771
839
|
// reported as `nothing` — it did not deliver and it did not ask.
|
|
772
840
|
await postAgentParked(limit, rt);
|
|
773
841
|
}
|
|
842
|
+
if (job.kind === 'merge_resolve' && repositoryRun) body.mergeResolved = allResolutionsCommitted(repositoryRun.sourcesNow());
|
|
774
843
|
const reply = await postAgentTurn(body);
|
|
775
844
|
// The queue just emptied. Run the project's own check and the AI
|
|
776
845
|
// pre-review HERE, in the worktree we are already standing in and still
|
|
777
846
|
// hold the lock on — see `runReviewEntry`. Only after the settle that may
|
|
778
847
|
// say delivered: the three `nothing` settles never opened it.
|
|
779
848
|
if (final && reply?.review === true) await runReviewEntry(agentId, wt, job.agentName);
|
|
849
|
+
} finally { await repositoryRun?.tool?.close(); }
|
|
780
850
|
};
|
|
781
851
|
|
|
782
852
|
return { runAgentTurnInPlace, commitsBetween };
|
|
@@ -26,6 +26,7 @@ import { parseTurnResult } from './agentPlan.mjs';
|
|
|
26
26
|
import { changedArtifacts, scanArtifacts } from './artifacts.mjs';
|
|
27
27
|
import { scrub as envScrub } from './uplinkScrub.mjs';
|
|
28
28
|
import { usableImageArtifact } from './imageArtifact.mjs';
|
|
29
|
+
import { withoutTurnDeclaration } from './turnDeclaration.mjs';
|
|
29
30
|
|
|
30
31
|
export function agentTurnSettlement({ turnId, job, kind, out, lastAnswer, usage, model = null, commits, artifactsBefore, branch, wt }) {
|
|
31
32
|
const res = (lastAnswer && parseTurnResult(lastAnswer)) || parseTurnResult(out);
|
|
@@ -39,7 +40,9 @@ export function agentTurnSettlement({ turnId, job, kind, out, lastAnswer, usage,
|
|
|
39
40
|
* makes the match mean what it says: the CLI failed and this is the
|
|
40
41
|
* sentence it failed with.
|
|
41
42
|
*/
|
|
42
|
-
|
|
43
|
+
// An unreadable protocol object is not a native CLI diagnostic either.
|
|
44
|
+
const readableOut = res ? '' : withoutTurnDeclaration(out);
|
|
45
|
+
const limit = res ? null : limitLine(readableOut);
|
|
43
46
|
/** What the caller posts, and whether it is the one settle that may say
|
|
44
47
|
* delivered (the only one whose reply can open the review-entry beat).
|
|
45
48
|
* Every one carries the model the CLI said it ran on, when it said
|
|
@@ -60,13 +63,16 @@ export function agentTurnSettlement({ turnId, job, kind, out, lastAnswer, usage,
|
|
|
60
63
|
}
|
|
61
64
|
|
|
62
65
|
if (!res) {
|
|
66
|
+
const raw = lastAnswer || out;
|
|
67
|
+
const readable = lastAnswer ? withoutTurnDeclaration(raw) : readableOut;
|
|
63
68
|
return report({
|
|
64
69
|
turnId,
|
|
65
70
|
outcome: 'nothing',
|
|
66
71
|
// The CLI's own words when it produced any. A turn that explained why
|
|
67
72
|
// it stopped is far more use than "the agent stopped".
|
|
68
|
-
answer:
|
|
69
|
-
? envScrub(
|
|
73
|
+
answer: raw.trim()
|
|
74
|
+
? readable.trim() ? envScrub(readable).slice(-1500)
|
|
75
|
+
: 'The turn returned an unreadable outcome declaration. This work has not been delivered.'
|
|
70
76
|
: 'the turn produced no output on the machine — its CLI may be signed out',
|
|
71
77
|
...(commits.length ? { commits } : {}),
|
|
72
78
|
...(usage ? { usage } : {}),
|
|
@@ -109,7 +115,15 @@ export function agentTurnSettlement({ turnId, job, kind, out, lastAnswer, usage,
|
|
|
109
115
|
* Commits are still reported if any exist — the posture prevents them,
|
|
110
116
|
* and a report never lies by omission about what is on the branch.
|
|
111
117
|
*/
|
|
112
|
-
|
|
118
|
+
// Existing document deliveries default to the Markdown proof. Only an
|
|
119
|
+
// explicit temporary-use declaration can deliver through the stored answer.
|
|
120
|
+
const briefUse = /^Research intended use: (tasks|context|document)(?:\r?\n|$)/.exec(job.task?.brief ?? '')?.[1];
|
|
121
|
+
const researchUse = kind.posture === 'research' ? briefUse ?? res.researchUse : null;
|
|
122
|
+
const temporaryResearch = ['tasks', 'context'].includes(researchUse);
|
|
123
|
+
if (res.outcome === 'delivered' && temporaryResearch && (!res.answer?.trim() || res.answer.length > 7500)) {
|
|
124
|
+
return report({ turnId, outcome: 'nothing', answer: res.answer?.trim() ? 'Keep the research findings, sources and uncertainties within 7500 characters so the full answer can be retained.' : 'The research turn returned no findings.', branch, worktree: wt });
|
|
125
|
+
}
|
|
126
|
+
if (res.outcome === 'delivered' && kind.artifact && !temporaryResearch) {
|
|
113
127
|
const want = kind.artifact.match;
|
|
114
128
|
const has = (l) => l.some((e) => want.test(e.name)
|
|
115
129
|
&& (kind.posture !== 'image' || usableImageArtifact(e)));
|
|
@@ -139,7 +153,8 @@ export function agentTurnSettlement({ turnId, job, kind, out, lastAnswer, usage,
|
|
|
139
153
|
turnId,
|
|
140
154
|
...(prerequisite && !cannotRequest ? { prerequisite } : {}),
|
|
141
155
|
outcome: res.outcome,
|
|
142
|
-
answer: cannotRequest ?? envScrub(
|
|
156
|
+
answer: cannotRequest ?? envScrub(kind.posture === 'research' && researchUse
|
|
157
|
+
? `Research intended use: ${researchUse}\n\n${res.answer ?? ''}` : res.answer ?? '').slice(0, 8000),
|
|
143
158
|
...(commits.length ? { commits } : {}),
|
|
144
159
|
// SCRUBBED like the answer beside it: a raised card lands in the
|
|
145
160
|
// project doc every member reads, and a brief quoting the `.env` the
|
|
@@ -26,9 +26,11 @@ import { createAgentTurnReports } from './workAgentTurnReports.mjs';
|
|
|
26
26
|
import { createAgentTurnExecution } from './workAgentTurnExecution.mjs';
|
|
27
27
|
import { failedTurnSettlement } from './workAgentTurnOutcome.mjs';
|
|
28
28
|
import { killGroup } from './processGroup.mjs';
|
|
29
|
+
import { createDeliveryReporter } from './workDelivery.mjs';
|
|
29
30
|
import { createAgentBriefRunner } from './workAgentBriefRunner.mjs';
|
|
30
31
|
|
|
31
32
|
export function createWorkAgentTurns({
|
|
33
|
+
agentRepositories,
|
|
32
34
|
REJECT_RETRY_MS,
|
|
33
35
|
baseRef,
|
|
34
36
|
inPlace,
|
|
@@ -44,6 +46,7 @@ export function createWorkAgentTurns({
|
|
|
44
46
|
artifacts,
|
|
45
47
|
runReviewEntry,
|
|
46
48
|
publishAgentBranch,
|
|
49
|
+
stopAgentPublishing = () => {},
|
|
47
50
|
admit,
|
|
48
51
|
workChildren,
|
|
49
52
|
agentPublished,
|
|
@@ -52,6 +55,7 @@ export function createWorkAgentTurns({
|
|
|
52
55
|
fetchAgentFiles,
|
|
53
56
|
makeBrief = createAgentBriefRunner({ admit, workChildren }),
|
|
54
57
|
}) {
|
|
58
|
+
const reportDelivery = createDeliveryReporter();
|
|
55
59
|
const {
|
|
56
60
|
postAgentTurn,
|
|
57
61
|
postAgentTrace,
|
|
@@ -68,11 +72,9 @@ export function createWorkAgentTurns({
|
|
|
68
72
|
// A TIME. The server types the next prompt when this one lands; this side
|
|
69
73
|
// does the work and reports what happened.
|
|
70
74
|
//
|
|
71
|
-
//
|
|
72
|
-
//
|
|
73
|
-
//
|
|
74
|
-
// pointing at whatever it liked. That is also what keeps this feature from
|
|
75
|
-
// adding a token kind to a scope map that has silently shipped empty twice.
|
|
75
|
+
// Outcomes stay in the final JSON; commits are measured from git. The
|
|
76
|
+
// local card-repository tool is a turn-scoped capability, never a machine
|
|
77
|
+
// credential handed to the CLI.
|
|
76
78
|
//
|
|
77
79
|
// UNLEASED, unlike a plan or a kill. The server hands out at most one turn
|
|
78
80
|
// per agent per poll and its settle is conditional on the row still being
|
|
@@ -110,6 +112,7 @@ export function createWorkAgentTurns({
|
|
|
110
112
|
const stoppedFor = new Map(); // agentId -> when
|
|
111
113
|
|
|
112
114
|
const { runAgentTurnInPlace, commitsBetween } = createAgentTurnExecution({
|
|
115
|
+
agentRepositories,
|
|
113
116
|
makeBrief,
|
|
114
117
|
baseRef,
|
|
115
118
|
baseDir,
|
|
@@ -184,7 +187,8 @@ export function createWorkAgentTurns({
|
|
|
184
187
|
let settled = false;
|
|
185
188
|
const settleOnce = (body) => {
|
|
186
189
|
settled = true;
|
|
187
|
-
|
|
190
|
+
const liveMessages = agentRuns.get(turnId)?.live?.receipts();
|
|
191
|
+
return postAgentTurn({ ...body, ...(liveMessages?.ids.length ? { liveMessages } : {}) });
|
|
188
192
|
};
|
|
189
193
|
|
|
190
194
|
// A WRITER on the agent's own place — see the lane header. Only `a-<id>`
|
|
@@ -192,14 +196,15 @@ export function createWorkAgentTurns({
|
|
|
192
196
|
// be a tab's directory, where a turn is a reader by the product's own law.
|
|
193
197
|
const run = agentRuns.get(turnId) ?? { place, interrupted: false };
|
|
194
198
|
await inPlace(place, place.startsWith('a-'), async () => {
|
|
199
|
+
run.waitingReason = null;
|
|
195
200
|
try {
|
|
196
201
|
await runAgentTurnInPlace(job, { turnId, agentId, place, releaseSlot, postAgentTurn: settleOnce, measured, run });
|
|
197
202
|
if (run.interrupted && measured.ranCli) stoppedFor.set(agentId, Date.now());
|
|
198
203
|
} catch (error) {
|
|
199
204
|
if (settled) return;
|
|
200
|
-
const commits = measured.wt ? commitsBetween(measured.wt, measured.before) : [];
|
|
205
|
+
const commits = measured.repositoryReceipts ? [...new Set(measured.repositoryReceipts().map((r) => r.sha))].slice(0, 50) : measured.wt ? commitsBetween(measured.wt, measured.before) : [];
|
|
201
206
|
if (run.interrupted) {
|
|
202
|
-
await postAgentTurn({ turnId, outcome: 'nothing', interrupted: true, ran: true, commits, ...(measured.usage ? { usage: measured.usage } : {}) });
|
|
207
|
+
await postAgentTurn({ turnId, outcome: 'nothing', interrupted: true, ran: measured.ranCli === true, commits, ...(measured.usage ? { usage: measured.usage } : {}) });
|
|
203
208
|
return;
|
|
204
209
|
}
|
|
205
210
|
await postAgentTurn(
|
|
@@ -214,6 +219,9 @@ export function createWorkAgentTurns({
|
|
|
214
219
|
})
|
|
215
220
|
);
|
|
216
221
|
}
|
|
222
|
+
}, () => {
|
|
223
|
+
run.waitingReason = 'waiting for this turn to finish';
|
|
224
|
+
reportDelivery('agent', job, 'queued', run.waitingReason);
|
|
217
225
|
});
|
|
218
226
|
/**
|
|
219
227
|
* …AND THEN PUBLISH, IF THE PROJECT PUBLISHES.
|
|
@@ -266,7 +274,12 @@ export function createWorkAgentTurns({
|
|
|
266
274
|
let saidPressure = false;
|
|
267
275
|
for (const job of list.slice(0, 4)) {
|
|
268
276
|
const id = String(job?.id || '');
|
|
269
|
-
if (!id
|
|
277
|
+
if (!id) continue;
|
|
278
|
+
if (agentTurns.has(id)) {
|
|
279
|
+
const reason = agentRuns.get(id)?.waitingReason;
|
|
280
|
+
if (reason) reportDelivery('agent', job, 'queued', reason);
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
270
283
|
const held = agentReported.get(id);
|
|
271
284
|
if (reportsOnly && !held) continue;
|
|
272
285
|
// A body the server REFUSED waits out its backoff — still held, so the
|
|
@@ -321,8 +334,15 @@ export function createWorkAgentTurns({
|
|
|
321
334
|
* would all be admitted against the same stale count. The reserved slot
|
|
322
335
|
* is what the next iteration sees. See admission.mjs.
|
|
323
336
|
*/
|
|
337
|
+
const missing = agentRepositories?.missing(job);
|
|
338
|
+
if (missing) {
|
|
339
|
+
if (!saidPressure) { saidPressure = true; note(missing); }
|
|
340
|
+
void postAgentActivity(String(job.agentId), missing);
|
|
341
|
+
continue; // no CLI, no failed settle; login and the next poll remeasure
|
|
342
|
+
}
|
|
324
343
|
const hold = admit('churn');
|
|
325
344
|
if (hold) {
|
|
345
|
+
reportDelivery('agent', job, 'queued', hold.reason);
|
|
326
346
|
if (!saidPressure) {
|
|
327
347
|
saidPressure = true;
|
|
328
348
|
note(`${c.cyan('agent')} ${c.dim(`— holding off: ${hold.reason}`)}`);
|
|
@@ -367,6 +387,9 @@ export function createWorkAgentTurns({
|
|
|
367
387
|
* a wedged uplink must not hold the stand-down open.
|
|
368
388
|
*/
|
|
369
389
|
const settleAgentTurns = async (sentence) => {
|
|
390
|
+
// Stand-down precedes flushing held replies; a late accepted reply must
|
|
391
|
+
// never push this displaced machine's work after the holder has moved.
|
|
392
|
+
stopAgentPublishing();
|
|
370
393
|
const answer = String(sentence ?? '').slice(0, 1000);
|
|
371
394
|
const posts = [];
|
|
372
395
|
for (const turnId of agentTurns) {
|
|
@@ -393,6 +416,7 @@ export function createWorkAgentTurns({
|
|
|
393
416
|
if (run) {
|
|
394
417
|
if (!run.interrupted) note(`${c.cyan('agent')} ${c.dim('— interrupting the running turn')}`);
|
|
395
418
|
run.interrupted = true;
|
|
419
|
+
run.abortSetup?.();
|
|
396
420
|
const child = agentChildren.get(run.place);
|
|
397
421
|
if (child) killGroup(child);
|
|
398
422
|
continue;
|
|
@@ -18,9 +18,10 @@
|
|
|
18
18
|
* doing both is the normal case (admission.mjs).
|
|
19
19
|
* · THE CAP IS ARMED AT THE SPAWN, not at entry: time spent waiting for a
|
|
20
20
|
* checkout lock is not a wedged CLI. It SIGKILLs the CHILD and never its
|
|
21
|
-
* group — the rule teardown keeps: a read-only turn starts no server, so
|
|
21
|
+
* group by default — the rule teardown keeps: a read-only turn starts no server, so
|
|
22
22
|
* there is nothing behind it worth signalling and everything to lose by
|
|
23
|
-
* signalling somebody else's.
|
|
23
|
+
* signalling somebody else's. A build caller may supply its owned-process
|
|
24
|
+
* group killer because its turn can start tools.
|
|
24
25
|
* · THE CAP RESOLVES THE WAIT ITSELF rather than waiting for `close` after
|
|
25
26
|
* the kill: a SIGKILLed process whose stdio a grandchild still holds can be
|
|
26
27
|
* slow to emit `close`, or never emit it, and the caller is holding a
|
|
@@ -61,6 +62,7 @@ export async function runCappedTurn({
|
|
|
61
62
|
onInit = () => {},
|
|
62
63
|
removeTranscript = removeTranscriptSoon,
|
|
63
64
|
signal,
|
|
65
|
+
killChild,
|
|
64
66
|
}) {
|
|
65
67
|
// Wiring mistakes, refused before anything spawns: a hook inside turnArgs
|
|
66
68
|
// would be silently replaced below, and a missing cap would fire at once.
|
|
@@ -77,7 +79,7 @@ export async function runCappedTurn({
|
|
|
77
79
|
});
|
|
78
80
|
const cancel = () => {
|
|
79
81
|
wedged = true;
|
|
80
|
-
try { child?.kill('SIGKILL'); } catch { /* already gone */ }
|
|
82
|
+
try { if (killChild) killChild(child); else child?.kill('SIGKILL'); } catch { /* already gone */ }
|
|
81
83
|
stopWaiting('');
|
|
82
84
|
};
|
|
83
85
|
signal?.addEventListener('abort', cancel, { once: true });
|
|
@@ -96,7 +98,7 @@ export async function runCappedTurn({
|
|
|
96
98
|
timer = setTimeout(() => {
|
|
97
99
|
wedged = true;
|
|
98
100
|
try {
|
|
99
|
-
ch.kill('SIGKILL');
|
|
101
|
+
if (killChild) killChild(ch); else ch.kill('SIGKILL');
|
|
100
102
|
} catch {
|
|
101
103
|
/* already gone */
|
|
102
104
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** A measured wait belongs to its message. Reports decorate the queue and
|
|
2
|
+
* never gate a turn; retries come with the next measurement/poll. */
|
|
3
|
+
import { DAEMON_INSTANCE, FLEET_URL } from './config.mjs';
|
|
4
|
+
import { fleetRequest } from './fleetPost.mjs';
|
|
5
|
+
import { fleetEndpoint } from './fleetWire.mjs';
|
|
6
|
+
|
|
7
|
+
export function createDeliveryReporter(request = fleetRequest, now = Date.now) {
|
|
8
|
+
const said = new Map();
|
|
9
|
+
return (scope, job, state, reason) => {
|
|
10
|
+
const key = `${scope}:${job.id}`;
|
|
11
|
+
const value = `${state}:${reason ?? ''}`;
|
|
12
|
+
const prior = said.get(key);
|
|
13
|
+
if (prior?.value === value && now() - prior.at < 10_000) return;
|
|
14
|
+
said.set(key, { value, at: now() });
|
|
15
|
+
// Bound a day-long daemon's decoration cache.
|
|
16
|
+
if (said.size > 500) said.delete(said.keys().next().value);
|
|
17
|
+
void request(fleetEndpoint('turn-delivery', FLEET_URL), { timeoutMs: 10_000,
|
|
18
|
+
body: { scope, turnId: job.id, instance: DAEMON_INSTANCE, ...(Number.isInteger(job.attempt) ? { attempt: job.attempt } : {}), state, ...(reason ? { reason } : {}) },
|
|
19
|
+
}).then(result => { if (!result.ok && said.get(key)?.value === value) said.delete(key); }).catch(() => { said.delete(key); });
|
|
20
|
+
};
|
|
21
|
+
}
|
package/bin/lib/workDiffs.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/** Commit diff requests and their bounded reports. */
|
|
2
2
|
import { FLEET_URL, FLEET_TOKEN, USER_AGENT } from './config.mjs';
|
|
3
|
+
import { listStoredProjects } from './credentials.mjs';
|
|
3
4
|
import { git, GIT_WIDE_BUFFER, OBJECT_ID_RE } from './git.mjs';
|
|
4
5
|
import { fleetEndpoint } from './fleetWire.mjs';
|
|
5
6
|
import { scrub, scrubCut } from './uplinkScrub.mjs';
|
|
@@ -21,7 +22,9 @@ function gitBig(args, cwd) {
|
|
|
21
22
|
}
|
|
22
23
|
}
|
|
23
24
|
|
|
24
|
-
export function createWorkDiffs({ repoRoot }) {
|
|
25
|
+
export function createWorkDiffs({ repoRoot, repositories }) {
|
|
26
|
+
// The picker may have changed the active token since config was imported.
|
|
27
|
+
const roots = repositories ?? listStoredProjects().find((entry) => entry.fleetToken === FLEET_TOKEN)?.repositories ?? {};
|
|
25
28
|
const DIFF_DONE_URL = fleetEndpoint('diff-done', FLEET_URL);
|
|
26
29
|
|
|
27
30
|
/**
|
|
@@ -82,24 +85,28 @@ export function createWorkDiffs({ repoRoot }) {
|
|
|
82
85
|
// In-flight guard, not a cache: the server stops offering a sha the
|
|
83
86
|
// moment it settles, so this only stops the SAME poll's job being
|
|
84
87
|
// started twice while its `git show` is still running.
|
|
85
|
-
|
|
86
|
-
servedDiffs.
|
|
88
|
+
const jobKey = `${job.repo ?? ''}:${sha}`;
|
|
89
|
+
if (servedDiffs.has(jobKey)) continue;
|
|
90
|
+
servedDiffs.add(jobKey);
|
|
91
|
+
const cwd = job.repo ? roots[job.repositoryName] : repoRoot;
|
|
92
|
+
const report = (body) => postDiff({ ...body, ...(job.repo ? { repo: job.repo } : {}) });
|
|
87
93
|
void (async () => {
|
|
88
94
|
try {
|
|
95
|
+
if (!cwd) { await report({ sha, error: 'This repository has no checkout on this machine. Run flowviant login in that checkout.' }); return; }
|
|
89
96
|
let commit = '';
|
|
90
97
|
try {
|
|
91
|
-
commit = gitBig(['rev-parse', '--verify', '--quiet', `${sha}^{commit}`],
|
|
98
|
+
commit = gitBig(['rev-parse', '--verify', '--quiet', `${sha}^{commit}`], cwd);
|
|
92
99
|
} catch (e) {
|
|
93
100
|
// Exit 1 is `--verify --quiet`'s "not a commit here" (absent,
|
|
94
101
|
// ambiguous, or another object type); anything else is git's own
|
|
95
102
|
// failure, relayed in its words as before.
|
|
96
103
|
if (e?.status !== 1) {
|
|
97
|
-
await
|
|
104
|
+
await report({ sha, error: scrubCut(e?.message || e, 500) });
|
|
98
105
|
return;
|
|
99
106
|
}
|
|
100
107
|
}
|
|
101
108
|
if (!/^[0-9a-f]{40,64}$/.test(commit)) {
|
|
102
|
-
await
|
|
109
|
+
await report({ sha, error: 'no such commit on this machine' });
|
|
103
110
|
return;
|
|
104
111
|
}
|
|
105
112
|
let patch = '';
|
|
@@ -109,13 +116,13 @@ export function createWorkDiffs({ repoRoot }) {
|
|
|
109
116
|
// date already reached the card on the worktree sweep, and
|
|
110
117
|
// repeating them inside the patch would put a second copy above
|
|
111
118
|
// every hunk.
|
|
112
|
-
patch = scrub(gitBig(['show', '--patch', '--format=', commit],
|
|
119
|
+
patch = scrub(gitBig(['show', '--patch', '--format=', commit], cwd));
|
|
113
120
|
} catch (e) {
|
|
114
|
-
await
|
|
121
|
+
await report({ sha, error: scrubCut(e?.message || e, 500) });
|
|
115
122
|
return;
|
|
116
123
|
}
|
|
117
124
|
try {
|
|
118
|
-
const raw = gitBig(['show', '--numstat', '--format=', commit],
|
|
125
|
+
const raw = gitBig(['show', '--numstat', '--format=', commit], cwd);
|
|
119
126
|
for (const line of raw.split('\n')) {
|
|
120
127
|
if (!line.trim()) continue;
|
|
121
128
|
const [a, d, ...rest] = line.split('\t');
|
|
@@ -133,7 +140,7 @@ export function createWorkDiffs({ repoRoot }) {
|
|
|
133
140
|
/* a header without counts still beats no diff */
|
|
134
141
|
}
|
|
135
142
|
const truncated = Buffer.byteLength(patch, 'utf8') > MAX_PATCH_BYTES;
|
|
136
|
-
await
|
|
143
|
+
await report({
|
|
137
144
|
sha,
|
|
138
145
|
// Cut on a LINE boundary: half a hunk header renders as garbage,
|
|
139
146
|
// and the viewer says the patch was truncated either way.
|
|
@@ -144,7 +151,7 @@ export function createWorkDiffs({ repoRoot }) {
|
|
|
144
151
|
truncated,
|
|
145
152
|
});
|
|
146
153
|
} finally {
|
|
147
|
-
servedDiffs.delete(
|
|
154
|
+
servedDiffs.delete(jobKey);
|
|
148
155
|
}
|
|
149
156
|
})();
|
|
150
157
|
}
|