flowviant 0.125.6 → 0.125.9
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 +84 -6
- package/bin/cli.mjs +7 -1
- package/bin/lib/activityFeed.mjs +69 -0
- package/bin/lib/agentMergeReports.mjs +46 -0
- package/bin/lib/agentMergeResolution.mjs +44 -0
- package/bin/lib/agentPlan.mjs +6 -2
- package/bin/lib/claudeInput.mjs +61 -1
- package/bin/lib/claudePosture.mjs +21 -0
- package/bin/lib/claudeStream.mjs +54 -4
- package/bin/lib/codexContract.mjs +52 -0
- package/bin/lib/codexResearch.mjs +33 -5
- package/bin/lib/commandSpecs.mjs +8 -5
- package/bin/lib/config.mjs +22 -1
- package/bin/lib/daemonJournal.mjs +102 -0
- package/bin/lib/daemonLogging.mjs +22 -18
- package/bin/lib/fleet.mjs +16 -6
- package/bin/lib/fleetReports.mjs +12 -5
- package/bin/lib/hooks/orchestrator.mjs +3 -1
- package/bin/lib/logReplay.mjs +133 -0
- package/bin/lib/mcpInventory.mjs +2 -1
- package/bin/lib/mcpReasons.mjs +6 -0
- package/bin/lib/nativeAgentTools.mjs +20 -3
- package/bin/lib/observedLines.mjs +63 -0
- package/bin/lib/orchestrator.mjs +49 -7
- package/bin/lib/orchestratorNative.mjs +133 -3
- package/bin/lib/orchestratorPrompt.mjs +50 -4
- package/bin/lib/postureTools.mjs +14 -6
- package/bin/lib/printable.mjs +1 -1
- package/bin/lib/processGroup.mjs +41 -0
- package/bin/lib/projectTools.mjs +3 -2
- package/bin/lib/prompts.mjs +47 -18
- package/bin/lib/reportJournal.mjs +61 -0
- package/bin/lib/repositoryMerge.mjs +1 -1
- package/bin/lib/researchHandoff.mjs +144 -0
- package/bin/lib/researchHandoffText.mjs +3 -0
- package/bin/lib/researchKnowledge.mjs +60 -0
- package/bin/lib/researchRestoration.mjs +38 -0
- package/bin/lib/resumeLost.mjs +21 -1
- package/bin/lib/runTurn.mjs +98 -44
- package/bin/lib/runtimeCapabilities.mjs +7 -2
- package/bin/lib/runtimeClaude.mjs +30 -8
- package/bin/lib/runtimeCodex.mjs +54 -22
- package/bin/lib/runtimeEvents.mjs +17 -7
- package/bin/lib/runtimeOfferings.mjs +3 -1
- package/bin/lib/startProject.mjs +10 -0
- package/bin/lib/turnDiagnostics.mjs +2 -1
- package/bin/lib/turnOutput.mjs +63 -0
- package/bin/lib/turnRecord.mjs +11 -0
- package/bin/lib/turnSubagents.mjs +13 -3
- package/bin/lib/ui.mjs +2 -2
- package/bin/lib/update.mjs +18 -4
- package/bin/lib/uplinkScrub.mjs +49 -0
- package/bin/lib/views.mjs +49 -58
- package/bin/lib/wikiRunner.mjs +2 -2
- package/bin/lib/work.mjs +5 -5
- package/bin/lib/workActivity.mjs +65 -0
- package/bin/lib/workAgentBriefRunner.mjs +2 -2
- package/bin/lib/workAgentMerges.mjs +57 -52
- package/bin/lib/workAgentPlans.mjs +76 -27
- package/bin/lib/workAgentPrecheck.mjs +1 -1
- package/bin/lib/workAgentTurnExecution.mjs +62 -8
- package/bin/lib/workAgentTurnReports.mjs +33 -3
- package/bin/lib/workAgentTurnShape.mjs +6 -4
- package/bin/lib/workAgentTurns.mjs +12 -8
- package/bin/lib/workAttachments.mjs +27 -5
- package/bin/lib/workIntake.mjs +8 -2
- package/bin/lib/workMergeBatch.mjs +7 -1
- package/bin/lib/workNarration.mjs +32 -0
- package/bin/lib/workPlanRecovery.mjs +50 -0
- package/bin/lib/workReportQueue.mjs +18 -1
- package/bin/lib/workSessionTurns.mjs +64 -22
- package/bin/lib/worktreeDiff.mjs +29 -5
- package/package.json +1 -1
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { reportWorkOutcome } from './workActivity.mjs';
|
|
2
|
+
import { createTurnOutput } from './turnOutput.mjs';
|
|
1
3
|
import { mergeRepositoryPlace } from './repositoryMerge.mjs';
|
|
2
4
|
import { fleetRequest } from './fleetPost.mjs';
|
|
3
5
|
/** Agent merge claims, pull-request approval, and branch cleanup. */
|
|
@@ -13,7 +15,9 @@ import { gitMergeIn } from './gitMergeIdentity.mjs';
|
|
|
13
15
|
import { warn } from './ui.mjs';
|
|
14
16
|
import { scrub as envScrub } from './uplinkScrub.mjs';
|
|
15
17
|
import { fleetEndpoint } from './fleetWire.mjs';
|
|
16
|
-
import { claimFleetLease
|
|
18
|
+
import { claimFleetLease } from './fleetPost.mjs';
|
|
19
|
+
import { createMergeReports } from './agentMergeReports.mjs';
|
|
20
|
+
import { gitReportedConflict, recordMergeFailure, clearMergeFailure } from './agentMergeResolution.mjs';
|
|
17
21
|
import { worktreePath } from './workPlaces.mjs';
|
|
18
22
|
|
|
19
23
|
/** Git puts merge conflicts on stdout, while refusals usually use stderr.
|
|
@@ -46,7 +50,7 @@ export function createWorkAgentMerges({
|
|
|
46
50
|
agentPublished,
|
|
47
51
|
}) {
|
|
48
52
|
const AGENT_MERGE_CLAIM_URL = fleetEndpoint('agent-merge-claim', FLEET_URL);
|
|
49
|
-
|
|
53
|
+
|
|
50
54
|
|
|
51
55
|
// ── THE MERGE ──────────────────────────────────────────────────────────────
|
|
52
56
|
//
|
|
@@ -63,9 +67,28 @@ export function createWorkAgentMerges({
|
|
|
63
67
|
// The peer may hold it; doing nothing is the safe answer.
|
|
64
68
|
const claimAgentMerge = (agentId) => claimFleetLease(AGENT_MERGE_CLAIM_URL, { agentId });
|
|
65
69
|
|
|
66
|
-
|
|
67
|
-
//
|
|
68
|
-
|
|
70
|
+
const retirements = new Map();
|
|
71
|
+
// Remote cleanup is tail work, after acknowledgement and outside the place
|
|
72
|
+
// lock, for both immediate delivery and journal replay.
|
|
73
|
+
const retireAgentMerge = async (id) => {
|
|
74
|
+
const held = retirements.get(id);
|
|
75
|
+
if (!held) return;
|
|
76
|
+
retirements.delete(id);
|
|
77
|
+
const { body, place, publishedRef } = held;
|
|
78
|
+
if (!isSafePathSegment(place)) return;
|
|
79
|
+
clearMergeFailure(worktreePath(baseDir, place));
|
|
80
|
+
agentPublished.delete(place);
|
|
81
|
+
agentRemoteAt.delete(place);
|
|
82
|
+
const args = publishDeleteArgs(publishedRef);
|
|
83
|
+
if (args) {
|
|
84
|
+
try { gitNet(args, 60_000); }
|
|
85
|
+
catch (e) { warn(`agent ${body.agentId}: the published branch could not be deleted — ${publishErrorText(envScrub(e?.stderr?.toString?.() || e?.message || ''))}`); }
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
const reports = createMergeReports({ baseDir, delivered: async (held, settled) => {
|
|
89
|
+
if (held.body.ok && settled) retirements.set(String(held.body.agentId), held);
|
|
90
|
+
} });
|
|
91
|
+
const postAgentMerge = (body, meta) => reports.report(body, meta);
|
|
69
92
|
|
|
70
93
|
// A merge COMMIT needs an identity the machine may not have; the probe and
|
|
71
94
|
// the daemon's fallback live in gitMergeIdentity.mjs.
|
|
@@ -81,6 +104,8 @@ export function createWorkAgentMerges({
|
|
|
81
104
|
return;
|
|
82
105
|
}
|
|
83
106
|
if (!(await claimAgentMerge(agentId))) return;
|
|
107
|
+
const activity = createTurnOutput({ work: { kind: 'merge', id: agentId, name: job.agentName || agentId } });
|
|
108
|
+
activity.event('Merge started', 'start');
|
|
84
109
|
|
|
85
110
|
/**
|
|
86
111
|
* NO EXIT PATH MAY LEAVE A CLAIMED MERGE UNREPORTED — the same belt the
|
|
@@ -111,11 +136,17 @@ export function createWorkAgentMerges({
|
|
|
111
136
|
* landed is the case it exists for. It is recorded in `report` rather than
|
|
112
137
|
* at the three success sites so a fourth one cannot forget it.
|
|
113
138
|
*/
|
|
114
|
-
let landedRef = null;
|
|
115
139
|
const report = async (body) => {
|
|
140
|
+
const wt = worktreePath(baseDir, place);
|
|
141
|
+
if (!job.repositoryMerge && !body.ok) {
|
|
142
|
+
body.conflict ??= gitReportedConflict(null, wt);
|
|
143
|
+
recordMergeFailure(wt, baseRef(), body.conflict);
|
|
144
|
+
}
|
|
145
|
+
// From here the journal owns retries, including a dropped 200 or a
|
|
146
|
+
// restart. The catch must not overwrite this measured outcome.
|
|
116
147
|
reported = true;
|
|
117
|
-
|
|
118
|
-
await postAgentMerge({ ...body, ...(body.detail ? { detail: relayedText(String(body.detail)).slice(0, 2000) } : {}) });
|
|
148
|
+
reportWorkOutcome({ ...activity.identity, id: activity.identity.workId }, { outcome: body.ok ? 'merged' : 'nothing', answer: body.detail });
|
|
149
|
+
await postAgentMerge({ ...body, ...(body.detail ? { detail: relayedText(String(body.detail)).slice(0, 2000) } : {}) }, { place, publishedRef: job.publishedRef ?? null });
|
|
119
150
|
};
|
|
120
151
|
|
|
121
152
|
// A WRITER on the place, exactly as a ship is. It folds base in and pushes
|
|
@@ -160,6 +191,7 @@ export function createWorkAgentMerges({
|
|
|
160
191
|
agentId,
|
|
161
192
|
ok: false,
|
|
162
193
|
detail: agentMergeFailureDetail(e),
|
|
194
|
+
conflict: gitReportedConflict(e, wt),
|
|
163
195
|
});
|
|
164
196
|
return;
|
|
165
197
|
}
|
|
@@ -168,7 +200,11 @@ export function createWorkAgentMerges({
|
|
|
168
200
|
// anything merges: a failed merge sends the agent BACK to review, which
|
|
169
201
|
// is a review-entry beat like any other, and a stale reading standing
|
|
170
202
|
// over a rebased branch is exactly what `precheckSha` exists to void.
|
|
171
|
-
await runReviewEntry(agentId, wt, job.agentName);
|
|
203
|
+
const check = await runReviewEntry(agentId, wt, job.agentName);
|
|
204
|
+
if (check?.status === 'failed') {
|
|
205
|
+
await report({ agentId, ok: false, detail: check.text || check.output || 'The repository check failed after folding base.' });
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
172
208
|
}
|
|
173
209
|
// `git()` THROWS on a non-zero exit, and `symbolic-ref` exits non-zero on
|
|
174
210
|
// a detached HEAD — so the guard below was unreachable and the throw
|
|
@@ -367,11 +403,11 @@ export function createWorkAgentMerges({
|
|
|
367
403
|
agentId,
|
|
368
404
|
ok: false,
|
|
369
405
|
// WHOSE FIX IT IS (0.108.0), when the merge said: a refusal about
|
|
370
|
-
//
|
|
371
|
-
//
|
|
372
|
-
// agent resolves it in its worktree as before.
|
|
406
|
+
// person's side must become a question during diagnosis; it is
|
|
407
|
+
// never permission to modify that checkout or its credentials.
|
|
373
408
|
...(e?.fix === 'person' ? { fix: 'person' } : {}),
|
|
374
409
|
detail: agentMergeFailureDetail(e),
|
|
410
|
+
conflict: gitReportedConflict(e, wt),
|
|
375
411
|
});
|
|
376
412
|
return;
|
|
377
413
|
}
|
|
@@ -388,62 +424,31 @@ export function createWorkAgentMerges({
|
|
|
388
424
|
} finally {
|
|
389
425
|
if (!reported) {
|
|
390
426
|
// Belt over braces. A merge this daemon claimed and cannot account for
|
|
391
|
-
// is a FAILURE, said out loud, so the agent
|
|
427
|
+
// is a FAILURE, said out loud, so the agent resumes diagnosis with a
|
|
392
428
|
// reason instead of waiting out an expiry that blames nobody.
|
|
393
429
|
await postAgentMerge({
|
|
394
430
|
agentId,
|
|
395
431
|
ok: false,
|
|
396
432
|
detail: 'the merge did not complete — check the daemon log',
|
|
397
|
-
}).catch(() => {});
|
|
398
|
-
}
|
|
399
|
-
/**
|
|
400
|
-
* …AND THE RETIREMENT IS TAIL WORK, OUTSIDE THE PLACE LOCK — the same
|
|
401
|
-
* placement the turn's publish argues for, for the same reason.
|
|
402
|
-
*
|
|
403
|
-
* `gitNet` is `execFileSync`: it blocks the whole event loop for up to its
|
|
404
|
-
* timeout, and inside `inPlace(place, true, …)` it would hold this
|
|
405
|
-
* agent's WRITER lock through a remote's bad day, with the merge already
|
|
406
|
-
* settled and nothing left that the delay serves. This block is PAST the
|
|
407
|
-
* lock: `inPlace` has returned (or thrown) before a `finally` runs.
|
|
408
|
-
*
|
|
409
|
-
* IN THE `finally` rather than after it, so a throw between the ok settle
|
|
410
|
-
* and the end of the locked block cannot strand the ref. `landedRef` is
|
|
411
|
-
* set only by a reported success, so there is nothing here to run on any
|
|
412
|
-
* other path — and a remote that refuses the delete only warns: the merge
|
|
413
|
-
* is the thing that matters, and a landed branch must not become a failed
|
|
414
|
-
* approval.
|
|
415
|
-
*
|
|
416
|
-
* The record goes with the ref. `agentPublished` is what the SWEEP
|
|
417
|
-
* republishes from, so leaving the entry behind would let the next sweep
|
|
418
|
-
* push the ref straight back — an orphan no later merge job can ever
|
|
419
|
-
* carry, and therefore one nothing can delete.
|
|
420
|
-
*/
|
|
421
|
-
if (landedRef) {
|
|
422
|
-
agentPublished.delete(place);
|
|
423
|
-
agentRemoteAt.delete(place);
|
|
424
|
-
const args = publishDeleteArgs(landedRef);
|
|
425
|
-
if (args) {
|
|
426
|
-
try {
|
|
427
|
-
gitNet(args, 60_000);
|
|
428
|
-
} catch (e) {
|
|
429
|
-
warn(
|
|
430
|
-
`agent ${agentId}: the published branch ${landedRef} could not be deleted — ${publishErrorText(envScrub(e?.stderr?.toString?.() || e?.message || ''))}`
|
|
431
|
-
);
|
|
432
|
-
}
|
|
433
|
-
}
|
|
433
|
+
}, { place, publishedRef: job.publishedRef ?? null }).catch(() => {});
|
|
434
434
|
}
|
|
435
|
+
await retireAgentMerge(agentId);
|
|
435
436
|
}
|
|
436
437
|
};
|
|
437
438
|
|
|
438
439
|
const processAgentMergeJobs = (jobs) => {
|
|
440
|
+
void (async () => {
|
|
441
|
+
await reports.flush();
|
|
442
|
+
for (const id of [...retirements.keys()]) if (!agentMerges.has(id)) await retireAgentMerge(id);
|
|
443
|
+
})().catch(() => {});
|
|
439
444
|
if (!Array.isArray(jobs) || jobs.length === 0) return;
|
|
440
445
|
for (const job of jobs.slice(0, 2)) {
|
|
441
446
|
const id = String(job?.batch?.spec?.id || job?.agentId || '');
|
|
442
|
-
if (!id || agentMerges.has(id)) continue;
|
|
447
|
+
if (!id || agentMerges.has(id) || reports.pending.has(id)) continue;
|
|
443
448
|
agentMerges.add(id);
|
|
444
449
|
void (job.batch ? runBatch(job) : runAgentMerge(job)).finally(() => agentMerges.delete(id));
|
|
445
450
|
}
|
|
446
451
|
};
|
|
447
452
|
|
|
448
|
-
return { processAgentMergeJobs, agentMerges };
|
|
453
|
+
return { processAgentMergeJobs, agentMerges, pendingMergeReports: reports.pending };
|
|
449
454
|
}
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
import { planWork, reportPlanOutcome } from './workActivity.mjs';
|
|
1
2
|
/** Agent plan claims, read-only planning turns, and proposal reports. */
|
|
3
|
+
import { PLAN_RECOVERY_SYSTEM, parsePlanDiagnosis, watchPlanAttempt } from './workPlanRecovery.mjs';
|
|
4
|
+
import { readClaudeAuthStatus } from './runtimeAuth.mjs';
|
|
2
5
|
import { discoveryGuide } from './discoveryContract.mjs';
|
|
3
6
|
import { compareRepositoryBaselines, measureRepositoryBaseline, repositoryBaselineOf } from './repositoryBaseline.mjs';
|
|
4
7
|
import { knowledgeDirFor } from './knowledgeFiles.mjs';
|
|
@@ -33,10 +36,12 @@ export function createWorkAgentPlans({
|
|
|
33
36
|
runTurn = defaultRunTurn,
|
|
34
37
|
pickRuntimeFor = defaultPickRuntimeFor,
|
|
35
38
|
detectRuntimes = defaultDetectRuntimes,
|
|
39
|
+
authStatus = readClaudeAuthStatus,
|
|
36
40
|
}) {
|
|
37
41
|
const AGENT_PLAN_DONE_URL = fleetEndpoint('agent-plan-done', FLEET_URL);
|
|
38
42
|
const AGENT_PLAN_ACTIVITY_URL = fleetEndpoint('agent-plan-activity', FLEET_URL);
|
|
39
43
|
const AGENT_PLAN_CLAIM_URL = fleetEndpoint('agent-plan-claim', FLEET_URL);
|
|
44
|
+
const AGENT_PLAN_STATE_URL = fleetEndpoint('agent-plan-state', FLEET_URL);
|
|
40
45
|
|
|
41
46
|
// ── AGENT PLAN JOBS: the Deploy press ──────────────────────────────────────
|
|
42
47
|
//
|
|
@@ -60,13 +65,13 @@ export function createWorkAgentPlans({
|
|
|
60
65
|
// tabs. It writes nothing, so a writer lock would only starve real work.
|
|
61
66
|
const planning = new Set(); // press ids in flight on this tick
|
|
62
67
|
|
|
63
|
-
const
|
|
68
|
+
const sendAgentPlan = async (body) => {
|
|
64
69
|
// Bounded retry on 408/429/5xx, an other-4xx counted as delivered — see
|
|
65
70
|
// postBestEffort's own docblock. Previously this swallowed every
|
|
66
71
|
// response including ordinary 5xx and rate limits, so a transient
|
|
67
72
|
// failure looked identical to a hostile refusal: nothing retried, and
|
|
68
73
|
// the proposal sat unsettled until the server's own expiry.
|
|
69
|
-
|
|
74
|
+
return postBestEffort(AGENT_PLAN_DONE_URL, { ...body, instance: DAEMON_INSTANCE });
|
|
70
75
|
};
|
|
71
76
|
|
|
72
77
|
/**
|
|
@@ -90,7 +95,7 @@ export function createWorkAgentPlans({
|
|
|
90
95
|
* turn it describes. A daemon that never calls this is a machine that has
|
|
91
96
|
* not narrated, which is the only thing an absent line can mean.
|
|
92
97
|
*/
|
|
93
|
-
const postAgentPlanActivity = async (planId, line) => {
|
|
98
|
+
const postAgentPlanActivity = async (planId, line, attempt, phase, diagnosis) => {
|
|
94
99
|
const text = envScrub(String(line ?? '')).slice(0, 400);
|
|
95
100
|
if (!text) return;
|
|
96
101
|
try {
|
|
@@ -98,7 +103,7 @@ export function createWorkAgentPlans({
|
|
|
98
103
|
method: 'POST',
|
|
99
104
|
headers: fleetHeaders(),
|
|
100
105
|
signal: AbortSignal.timeout(15_000),
|
|
101
|
-
body: JSON.stringify({ planId, line: text }),
|
|
106
|
+
body: JSON.stringify({ planId, line: text, instance: DAEMON_INSTANCE, ...(attempt !== undefined ? { attempt } : {}), ...(phase ? { phase, diagnosis } : {}) }),
|
|
102
107
|
});
|
|
103
108
|
} catch {
|
|
104
109
|
/* a readout — losing a line costs nothing */
|
|
@@ -134,7 +139,7 @@ export function createWorkAgentPlans({
|
|
|
134
139
|
const PLAN_TURN_TIMEOUT_MS = 15 * 60_000;
|
|
135
140
|
|
|
136
141
|
// The peer may hold it; doing nothing is the safe answer.
|
|
137
|
-
const claimAgentPlan = (id) => claimFleetLease(AGENT_PLAN_CLAIM_URL, { id });
|
|
142
|
+
const claimAgentPlan = (id, attempt) => claimFleetLease(AGENT_PLAN_CLAIM_URL, { id, ...(attempt !== undefined ? { attempt } : {}) });
|
|
138
143
|
|
|
139
144
|
/** See `runAgentTurn` — the admission reservation, released the moment the
|
|
140
145
|
* planner's CLI exists. */
|
|
@@ -149,11 +154,27 @@ export function createWorkAgentPlans({
|
|
|
149
154
|
// kill keeps (refuse cheaply, then claim) and it is because these two
|
|
150
155
|
// lanes settle differently: a kill's settle does not check a lease for the
|
|
151
156
|
// not-found case, and a plan's always does.
|
|
152
|
-
if (!(await claimAgentPlan(id))) return;
|
|
157
|
+
if (!(await claimAgentPlan(id, job.attempt))) return;
|
|
158
|
+
const control = job.recoveryEnabled ? await watchPlanAttempt(AGENT_PLAN_STATE_URL, job) : null;
|
|
159
|
+
const report = async (body) => {
|
|
160
|
+
const result = { ...body, ...(job.attempt !== undefined ? { attempt: job.attempt } : {}) };
|
|
161
|
+
reportPlanOutcome(job, result);
|
|
162
|
+
// Keep the measured result while its acknowledgement is unknown. A
|
|
163
|
+
// network retry delivers this report, never another paid planning turn.
|
|
164
|
+
while (!control?.signal.aborted) {
|
|
165
|
+
const sent = await sendAgentPlan(result);
|
|
166
|
+
if (sent !== false || !job.recoveryEnabled) return;
|
|
167
|
+
await postAgentPlanActivity(id, 'The Plan result has not been acknowledged. Retrying delivery.', job.attempt);
|
|
168
|
+
await new Promise(resolve => setTimeout(resolve, 3000));
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
const unavailable = (reason) => report({ id, ...(job.recoveryEnabled ? { question: { question: `${reason}. Can you make that CLI available on the machine and tell me when it is ready?`, context: reason } } : { error: reason }) });
|
|
172
|
+
try {
|
|
173
|
+
if (control?.signal.aborted) return;
|
|
153
174
|
if (tasks.length === 0) {
|
|
154
175
|
// The press named cards the doc no longer has. A real answer, and one the
|
|
155
176
|
// board can explain, rather than a turn that plans nothing.
|
|
156
|
-
await
|
|
177
|
+
await report({ id, error: 'those cards are no longer on the board' });
|
|
157
178
|
return;
|
|
158
179
|
}
|
|
159
180
|
|
|
@@ -172,23 +193,27 @@ export function createWorkAgentPlans({
|
|
|
172
193
|
rt = pickedRuntime;
|
|
173
194
|
const runtime = runtimeById(rt);
|
|
174
195
|
if (!runtime) {
|
|
175
|
-
await
|
|
196
|
+
await unavailable(`${rt} is not a CLI this machine knows`);
|
|
176
197
|
return;
|
|
177
198
|
}
|
|
178
199
|
const label = runtime.label;
|
|
179
200
|
if (!detectRuntimes().find((r) => r.id === rt)?.installed) {
|
|
180
|
-
await
|
|
201
|
+
await unavailable(`${label} is not installed on this machine`);
|
|
181
202
|
return;
|
|
182
203
|
}
|
|
183
204
|
if (!canRun(runtime, 'consult')) {
|
|
184
|
-
await
|
|
205
|
+
await unavailable(`${label} can't run a read-only turn on this machine`);
|
|
185
206
|
return;
|
|
186
207
|
}
|
|
187
208
|
} else {
|
|
188
209
|
rt = pickRuntimeFor('consult');
|
|
189
210
|
}
|
|
190
211
|
if (!rt) {
|
|
191
|
-
await
|
|
212
|
+
await unavailable('No CLI on this machine can run a read-only turn');
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
if (job.recoveryEnabled && job.recovery && rt === 'claude' && authStatus({ refresh: true }).signedIn === false) {
|
|
216
|
+
await report({ id, question: { question: 'Claude Code reports that it is signed out. Can you sign in on this machine and tell me when it is ready?', context: 'The CLI’s native auth status reported signed out.' } });
|
|
192
217
|
return;
|
|
193
218
|
}
|
|
194
219
|
|
|
@@ -204,7 +229,7 @@ export function createWorkAgentPlans({
|
|
|
204
229
|
const now = Date.now();
|
|
205
230
|
if (now - lastPlanBeat < 2_000) return;
|
|
206
231
|
lastPlanBeat = now;
|
|
207
|
-
void postAgentPlanActivity(id, line);
|
|
232
|
+
void postAgentPlanActivity(id, line, job.attempt);
|
|
208
233
|
};
|
|
209
234
|
/**
|
|
210
235
|
* THE MACHINE'S OWN VOICE, for the moments only this side can see — the
|
|
@@ -219,10 +244,11 @@ export function createWorkAgentPlans({
|
|
|
219
244
|
*/
|
|
220
245
|
const say = (line) => {
|
|
221
246
|
lastPlanBeat = Date.now();
|
|
222
|
-
void postAgentPlanActivity(id, line);
|
|
247
|
+
void postAgentPlanActivity(id, line, job.attempt);
|
|
223
248
|
};
|
|
224
249
|
|
|
225
250
|
let out = '';
|
|
251
|
+
let diagnosis = null;
|
|
226
252
|
/** The cap fired: the CLI was still running when this machine stopped it. */
|
|
227
253
|
let wedged = false;
|
|
228
254
|
/**
|
|
@@ -234,7 +260,7 @@ export function createWorkAgentPlans({
|
|
|
234
260
|
*/
|
|
235
261
|
let usage = null;
|
|
236
262
|
const discovery = createToolLog(repoRoot);
|
|
237
|
-
const spent = () => ({ ...(usage ? { usage } : {}), tools: discovery.toolLog });
|
|
263
|
+
const spent = () => ({ ...(usage ? { usage } : {}), tools: discovery.toolLog, ...(diagnosis ? { diagnosis: diagnosis.summary } : {}) });
|
|
238
264
|
// Said BEFORE the lock is asked for, because a reader only waits when a
|
|
239
265
|
// writer holds the checkout or is queued ahead of it — which is exactly
|
|
240
266
|
// this condition, read one line before we join the queue.
|
|
@@ -261,20 +287,17 @@ export function createWorkAgentPlans({
|
|
|
261
287
|
discovery.toolLog.baseline = measureRepositoryBaseline({ cwd: repoRoot, baseRef, knowledgeDir });
|
|
262
288
|
discovery.toolLog.captureBaseline = repositoryBaselineOf(job.captureBaseline);
|
|
263
289
|
discovery.toolLog.reconciliation = compareRepositoryBaselines(discovery.toolLog.baseline, discovery.toolLog.captureBaseline);
|
|
264
|
-
(
|
|
290
|
+
const consult = async (prompt, system) => await runCappedTurn({
|
|
265
291
|
runTurn,
|
|
292
|
+
signal: control?.signal,
|
|
266
293
|
capMs: PLAN_TURN_TIMEOUT_MS,
|
|
267
294
|
workChildren,
|
|
268
295
|
releaseSlot,
|
|
269
296
|
transcriptCwd: repoRoot,
|
|
270
297
|
turnArgs: {
|
|
271
298
|
diagnosticPlanId: id,
|
|
272
|
-
prompt
|
|
273
|
-
|
|
274
|
-
liveAgents,
|
|
275
|
-
agentCap: Number.isInteger(job.agentCap) && job.agentCap > 0 ? job.agentCap : 3,
|
|
276
|
-
}),
|
|
277
|
-
system: withProjectContext(SYSTEM_PLAN, { knowledgeDir }) + '\n\n' + discoveryGuide(rt, 'planning') + '\n\n' + REPOSITORY_BASELINE_PARAGRAPH(discovery.toolLog.baseline, discovery.toolLog.captureBaseline, discovery.toolLog.reconciliation),
|
|
299
|
+
prompt,
|
|
300
|
+
system,
|
|
278
301
|
knowledgeDir,
|
|
279
302
|
// READ-ONLY, and no MCP: `mcpArgs` is omitted entirely rather than
|
|
280
303
|
// passed empty, so there is no control plane on this turn at all.
|
|
@@ -283,7 +306,7 @@ export function createWorkAgentPlans({
|
|
|
283
306
|
runtime: rt,
|
|
284
307
|
...brainFor(job, rt),
|
|
285
308
|
answerFromResult: true,
|
|
286
|
-
|
|
309
|
+
work: planWork(job),
|
|
287
310
|
// The CLI's own tail, forwarded to the press. Same rule as an agent
|
|
288
311
|
// turn's: throttled, overwritten rather than appended, and never
|
|
289
312
|
// awaited by the turn.
|
|
@@ -300,14 +323,39 @@ export function createWorkAgentPlans({
|
|
|
300
323
|
onSpawn: () => {
|
|
301
324
|
say(`${runtimeLabel(rt)} started on this machine`);
|
|
302
325
|
},
|
|
303
|
-
})
|
|
326
|
+
});
|
|
327
|
+
let context = AGENT_PLAN_KICKOFF({ tasks, liveAgents, agentCap: Number.isInteger(job.agentCap) && job.agentCap > 0 ? job.agentCap : 3 });
|
|
328
|
+
if (job.recovery?.start) context += '\nSTART REPAIR: This is a supporting diagnosis for an already approved Start. Return exactly ONE group with the same ordered taskIds, attachment target (or absence) and CLI. Preserve the approval; do not start an unapproved sibling. Diagnose admission/pickup errors and repair planning evidence/context. Ask only a genuine person-only question in the diagnostic JSON. Approval and source revision (data): ' + JSON.stringify(job.recovery.start);
|
|
329
|
+
const system = withProjectContext(SYSTEM_PLAN, { knowledgeDir }) + '\n\n' + discoveryGuide(rt, 'planning') + '\n\n' + REPOSITORY_BASELINE_PARAGRAPH(discovery.toolLog.baseline, discovery.toolLog.captureBaseline, discovery.toolLog.reconciliation);
|
|
330
|
+
if (job.recovery) {
|
|
331
|
+
say(`Diagnosing the failed Plan: ${job.recovery.reason}`);
|
|
332
|
+
const diagnostic = await consult(context + '\nRetained recovery context (data): ' + JSON.stringify(job.recovery), withProjectContext(PLAN_RECOVERY_SYSTEM, { knowledgeDir }) + '\n' + discoveryGuide(rt, 'planning'));
|
|
333
|
+
if (control?.signal.aborted) return;
|
|
334
|
+
if (diagnostic.wedged) { wedged = true; return; }
|
|
335
|
+
const result = parsePlanDiagnosis(diagnostic.out);
|
|
336
|
+
if (result?.status === 'question') {
|
|
337
|
+
diagnosis = result;
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
if (!result) throw new Error(`The diagnostic turn did not return a diagnosis: ${envScrub(String(diagnostic.out ?? '')).slice(0, 300) || 'no output'}`);
|
|
341
|
+
diagnosis = result;
|
|
342
|
+
await postAgentPlanActivity(id, `Retrying Plan: ${result.summary}`, job.attempt, 'retrying', result.summary);
|
|
343
|
+
await control?.check();
|
|
344
|
+
if (control?.signal.aborted) return;
|
|
345
|
+
}
|
|
346
|
+
({ out, wedged } = await consult(context + (diagnosis ? '\nDiagnosis and corrected approach (data): ' + JSON.stringify(diagnosis) : ''), system));
|
|
304
347
|
});
|
|
305
348
|
} catch (e) {
|
|
306
349
|
discovery.toolLog.state = 'partial';
|
|
307
|
-
await
|
|
350
|
+
await report({ id, error: envScrub(String(e?.message || e)).slice(0, 500), ...spent() });
|
|
308
351
|
return;
|
|
309
352
|
}
|
|
310
353
|
|
|
354
|
+
if (control?.signal.aborted) return;
|
|
355
|
+
if (diagnosis?.status === 'question') {
|
|
356
|
+
await report({ id, question: { question: diagnosis.question, context: diagnosis.context }, ...spent() });
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
311
359
|
if (wedged) {
|
|
312
360
|
discovery.toolLog.state = 'partial';
|
|
313
361
|
// NEVER LEAVE A CLAIMED PRESS UNREPORTED — the belt the merge lane wears
|
|
@@ -315,7 +363,7 @@ export function createWorkAgentPlans({
|
|
|
315
363
|
// only side that knows: the server would have expired this press in
|
|
316
364
|
// another fifteen minutes with a sentence blaming a daemon that had in
|
|
317
365
|
// fact answered.
|
|
318
|
-
await
|
|
366
|
+
await report({
|
|
319
367
|
id,
|
|
320
368
|
error: 'the planning turn ran past fifteen minutes on this machine and was stopped',
|
|
321
369
|
...spent(),
|
|
@@ -325,7 +373,7 @@ export function createWorkAgentPlans({
|
|
|
325
373
|
|
|
326
374
|
const proposal = parseProposal(out);
|
|
327
375
|
if (!proposal) {
|
|
328
|
-
await
|
|
376
|
+
await report({
|
|
329
377
|
id,
|
|
330
378
|
// The CLI's own words when it has any — a turn that explained why it
|
|
331
379
|
// could not plan is far more use than "planning failed".
|
|
@@ -336,7 +384,8 @@ export function createWorkAgentPlans({
|
|
|
336
384
|
});
|
|
337
385
|
return;
|
|
338
386
|
}
|
|
339
|
-
await
|
|
387
|
+
await report({ id, proposal, sessionRef: repoRoot, ...spent() });
|
|
388
|
+
} finally { control?.stop(); }
|
|
340
389
|
};
|
|
341
390
|
|
|
342
391
|
const processAgentPlanJobs = (jobs) => {
|
|
@@ -239,7 +239,7 @@ export function createWorkAgentPrecheck({ baseRef, admit, sessionMetaPath, workC
|
|
|
239
239
|
// agent grading its own homework out of its own context; this is a
|
|
240
240
|
// stranger reading a diff.
|
|
241
241
|
answerFromResult: true,
|
|
242
|
-
|
|
242
|
+
work: { kind: 'pre-review', id: agentId, name: agentName },
|
|
243
243
|
// The CLI's own count for this reading. Set, never accumulated: one
|
|
244
244
|
// turn is one `result` event, and the adding-up is the server's.
|
|
245
245
|
// Tagged with the CLI, so a Claude pre-review of a Codex agent is
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { agentWork } from './workActivity.mjs';
|
|
2
|
+
import { relayedText } from './prWorkflow.mjs';
|
|
3
|
+
import { measuredMergeResolution } from './agentMergeResolution.mjs';
|
|
1
4
|
import { finishRepositoryDelivery, allResolutionsCommitted } from './repositoryDelivery.mjs';
|
|
2
5
|
/**
|
|
3
6
|
* ONE AGENT TURN, RUN: the worktree, the CLI, and what it left behind.
|
|
@@ -18,7 +21,7 @@ import { finishRepositoryDelivery, allResolutionsCommitted } from './repositoryD
|
|
|
18
21
|
* answered. `measured` is the same bargain the other way: the branch facts and
|
|
19
22
|
* the spend, written here as they are learned, read by that catch.
|
|
20
23
|
*/
|
|
21
|
-
import { PROJECT_ID } from './config.mjs';
|
|
24
|
+
import { MODEL, PROJECT_ID } from './config.mjs';
|
|
22
25
|
import { setRuntimeLimit } from './daemonState.mjs';
|
|
23
26
|
import { gitOrNull } from './git.mjs';
|
|
24
27
|
import { turnCommits } from './turnReceipts.mjs';
|
|
@@ -49,6 +52,8 @@ import { createDeliveryReporter } from './workDelivery.mjs';
|
|
|
49
52
|
import { createLiveMessages } from './workLiveMessages.mjs';
|
|
50
53
|
import { createOrchestrator } from './orchestrator.mjs';
|
|
51
54
|
import { claudeOrchestrationSafe } from './orchestratorHooks.mjs';
|
|
55
|
+
import { ORCHESTRATED_POSTURES, orchestratesPosture } from './orchestratorPrompt.mjs';
|
|
56
|
+
import { knownCodexModels } from './runtimeOfferings.mjs';
|
|
52
57
|
import { codexSubagentsSafe } from './runtimeCodex.mjs';
|
|
53
58
|
import { measuredRuntimeVersion } from './runtimeDetection.mjs';
|
|
54
59
|
|
|
@@ -72,6 +77,7 @@ export function createAgentTurnExecution({
|
|
|
72
77
|
agentChildren,
|
|
73
78
|
postAgentTrace,
|
|
74
79
|
postAgentActivity,
|
|
80
|
+
postAgentContext = () => {},
|
|
75
81
|
postAgentParked,
|
|
76
82
|
fetchAgentFiles,
|
|
77
83
|
makeLiveMessages = createLiveMessages,
|
|
@@ -303,10 +309,12 @@ export function createAgentTurnExecution({
|
|
|
303
309
|
}
|
|
304
310
|
|
|
305
311
|
// ORCHESTRATED ONLY WHERE THE CLI CAN BE GATED (2026-10-17). v2 reaches every
|
|
306
|
-
// job at
|
|
307
|
-
// included) has no controller, so
|
|
312
|
+
// job at its kind's floor; an artifact card (design, model, image, deck) or
|
|
313
|
+
// a CLI below its measured floor (unknown included) has no controller, so
|
|
314
|
+
// no marker: its notes fold on receipt. Research is orchestrated too since
|
|
315
|
+
// 2026-10-19, with read-only children (orchestratorPrompt.mjs).
|
|
308
316
|
const codexVersion = rt === 'codex' ? measuredRuntimeVersion('codex')?.version : undefined;
|
|
309
|
-
const orchestrating = job.orchestrationVersion === 2 && posture
|
|
317
|
+
const orchestrating = job.orchestrationVersion === 2 && orchestratesPosture(posture) &&
|
|
310
318
|
(rt === 'claude' ? claudeOrchestrationSafe(measuredRuntimeVersion('claude')?.version) : rt === 'codex' && codexSubagentsSafe(codexVersion));
|
|
311
319
|
/**
|
|
312
320
|
* THE WHOLE STREAM, not just its latest line — see trace.mjs.
|
|
@@ -350,6 +358,10 @@ export function createAgentTurnExecution({
|
|
|
350
358
|
* neither key.
|
|
351
359
|
*/
|
|
352
360
|
const brain = brainFor(job, rt);
|
|
361
|
+
// The card's model and effort as the parent runs them, for an orchestrated
|
|
362
|
+
// run's children and contract: Claude always names --model (the daemon's
|
|
363
|
+
// pin when the card has none); Codex's unpinned default is its own.
|
|
364
|
+
const cardBrain = { model: brain.model ?? (rt === 'claude' ? MODEL : null), effort: brain.effort ?? null };
|
|
353
365
|
|
|
354
366
|
let out = '';
|
|
355
367
|
let child = null;
|
|
@@ -383,7 +395,7 @@ export function createAgentTurnExecution({
|
|
|
383
395
|
/** THE MODEL THE TURN RAN ON, as its CLI named it (turnModel.mjs) — rides
|
|
384
396
|
* every settle `usage` rides, for the same reason; null sends no key. */
|
|
385
397
|
let model = null;
|
|
386
|
-
const brief = makeBrief?.({ agentId, turnId, runtime: rt, model: () => model ?? job.model });
|
|
398
|
+
const brief = makeBrief?.({ agentId, turnId, workName: job.task?.title || job.agentName, runtime: rt, model: () => model ?? job.model });
|
|
387
399
|
brief?.append('prompt', job.task ? { title: job.task.title, message: job.body, discussion: job.cardThread, brief: job.task.brief, criteria: job.task.criteria } : job.body);
|
|
388
400
|
/**
|
|
389
401
|
* THE AGENT'S LAST MESSAGE, ALONE — where the final JSON object lives.
|
|
@@ -403,6 +415,9 @@ export function createAgentTurnExecution({
|
|
|
403
415
|
try {
|
|
404
416
|
if (orchestrating) {
|
|
405
417
|
orchestrator = await createOrchestrator({ turnId, runtime: rt, trace, admit,
|
|
418
|
+
// Codex's spawn takes only a catalog model (knownCodexModels, the
|
|
419
|
+
// measured models_cache.json rows the roster already reports).
|
|
420
|
+
children: { access: ORCHESTRATED_POSTURES[posture], ...cardBrain, ...(rt === 'codex' ? { catalog: knownCodexModels() } : {}) },
|
|
406
421
|
// Whether the words reached the CLI; Codex reads its inbox from hooks.
|
|
407
422
|
notify: message => liveInput ? run.live?.inject(message) === true : false,
|
|
408
423
|
taken: id => run.live?.onTaken(id) });
|
|
@@ -423,8 +438,10 @@ export function createAgentTurnExecution({
|
|
|
423
438
|
})) } : {}),
|
|
424
439
|
// The kickoff and the system prompt, composed by the agent lane's
|
|
425
440
|
// shape (workAgentTurnShape.mjs) from what this run measured.
|
|
441
|
+
researchHandoff: job.researchHandoff,
|
|
442
|
+
researchIdentity: { projectId: PROJECT_ID, agentId, turnId, taskId: job.task?.id ?? job.repositoryTaskId },
|
|
426
443
|
prompt: agentKickoff(job, { cardFiles: files.card, messageFiles: files.message }),
|
|
427
|
-
system: agentTurnSystem({ taskKind, runtimeLabel: runtimeById(rt)?.label, prerequisitesAccepted: job.prerequisitesAccepted === true, knowledgeDir: knowledgeDirFor(repoRoot), artifactsAccepted: getArtifactsAccepted(), toolsNote, artifactInstructions, toolInstructions: projectTools.instructions, repositoryRun,
|
|
444
|
+
system: agentTurnSystem({ taskKind, runtimeLabel: runtimeById(rt)?.label, prerequisitesAccepted: job.prerequisitesAccepted === true, knowledgeDir: knowledgeDirFor(repoRoot), artifactsAccepted: getArtifactsAccepted(), toolsNote, artifactInstructions, toolInstructions: projectTools.instructions, repositoryRun, orchestration: orchestrator ? { posture, runtime: rt, ...cardBrain } : null }),
|
|
428
445
|
agentTools: projectTools,
|
|
429
446
|
writableDirs: repositoryRun?.writableDirs,
|
|
430
447
|
...((tools.envNeeded || repositoryRun?.tool || orchestrator) ? { mcpEnv: { ...tools.env, ...(repositoryRun?.tool?.env ?? {}), ...(orchestrator?.env ?? {}) } } : {}),
|
|
@@ -464,7 +481,26 @@ export function createAgentTurnExecution({
|
|
|
464
481
|
// Present only when the container named one — see brainFor.
|
|
465
482
|
...brain,
|
|
466
483
|
answerFromResult: true,
|
|
467
|
-
|
|
484
|
+
work: agentWork(job),
|
|
485
|
+
// The CLI's own context measure while the turn runs (claudeInput.mjs),
|
|
486
|
+
// the latest one at a time; the server clears it at turn end.
|
|
487
|
+
onContext: (() => {
|
|
488
|
+
let next = null;
|
|
489
|
+
let busy = false;
|
|
490
|
+
return (ctx) => {
|
|
491
|
+
next = ctx;
|
|
492
|
+
if (busy) return;
|
|
493
|
+
busy = true;
|
|
494
|
+
void (async () => {
|
|
495
|
+
while (next) {
|
|
496
|
+
const measured = next;
|
|
497
|
+
next = null;
|
|
498
|
+
await postAgentContext(agentId, turnId, measured);
|
|
499
|
+
}
|
|
500
|
+
busy = false;
|
|
501
|
+
})();
|
|
502
|
+
};
|
|
503
|
+
})(),
|
|
468
504
|
// The CLI's own tail, relayed. Throttled by the same rule the tab's
|
|
469
505
|
// narrator keeps: overwritten, never appended, and ignored by the
|
|
470
506
|
// board past ~90 seconds.
|
|
@@ -732,7 +768,25 @@ export function createAgentTurnExecution({
|
|
|
732
768
|
// reported as `nothing` — it did not deliver and it did not ask.
|
|
733
769
|
await postAgentParked(limit, rt);
|
|
734
770
|
}
|
|
735
|
-
if (job.kind === 'merge_resolve'
|
|
771
|
+
if (job.kind === 'merge_resolve') {
|
|
772
|
+
const resolution = () => repositoryRun
|
|
773
|
+
? allResolutionsCommitted(repositoryRun.sourcesNow())
|
|
774
|
+
: measuredMergeResolution(wt, before, baseRef());
|
|
775
|
+
body.mergeResolved = resolution();
|
|
776
|
+
if (body.outcome === 'delivered' && body.mergeResolved) {
|
|
777
|
+
const rows = repositoryRun?.sourcesNow() ?? [{ wt }];
|
|
778
|
+
for (const r of rows) {
|
|
779
|
+
const head = gitOrNull(['rev-parse', 'HEAD'], r.wt);
|
|
780
|
+
const check = await runReviewEntry(agentId, r.wt, job.agentName, repositoryRun ? r : undefined);
|
|
781
|
+
if (check?.status === 'failed') {
|
|
782
|
+
body.mergeResolved = false;
|
|
783
|
+
body.mergeDetail = relayedText(check.text || check.output || 'The repository check failed during merge recovery.').slice(0, 2000);
|
|
784
|
+
}
|
|
785
|
+
if (!head || head !== gitOrNull(['rev-parse', 'HEAD'], r.wt)) body.mergeResolved = false;
|
|
786
|
+
}
|
|
787
|
+
body.mergeResolved = body.mergeResolved && resolution();
|
|
788
|
+
}
|
|
789
|
+
}
|
|
736
790
|
const reply = await postAgentTurn(body);
|
|
737
791
|
// The queue just emptied. Run the project's own check and the AI
|
|
738
792
|
// pre-review HERE, in the worktree we are already standing in and still
|