flowviant 0.117.0 → 0.124.1
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 +41 -0
- package/bin/cli.mjs +44 -0
- package/bin/lib/agentAsk.mjs +123 -0
- package/bin/lib/agentBrief.mjs +36 -0
- package/bin/lib/agentFootprint.mjs +76 -0
- package/bin/lib/agentInternalWait.mjs +4 -3
- package/bin/lib/agentPlan.mjs +86 -7
- package/bin/lib/agentPrerequisiteContract.mjs +43 -0
- package/bin/lib/agentRepositories.mjs +48 -0
- package/bin/lib/agentTaskKinds.mjs +1 -1
- package/bin/lib/artifactContracts.mjs +91 -20
- package/bin/lib/artifactIteration.mjs +182 -0
- package/bin/lib/artifactRecovery.mjs +36 -0
- package/bin/lib/artifactReporter.mjs +183 -19
- package/bin/lib/artifacts.mjs +6 -3
- package/bin/lib/checkoutIdentity.mjs +25 -0
- package/bin/lib/claudeAuth.mjs +15 -5
- package/bin/lib/claudeInput.mjs +32 -0
- package/bin/lib/claudePosture.mjs +110 -14
- package/bin/lib/claudeSessions.mjs +20 -0
- package/bin/lib/claudeStream.mjs +20 -2
- package/bin/lib/codexProjectRead.mjs +63 -0
- package/bin/lib/codexResearch.mjs +16 -3
- package/bin/lib/commandSpecs.mjs +6 -0
- package/bin/lib/credentials.mjs +52 -17
- package/bin/lib/daemonLocks.mjs +26 -6
- package/bin/lib/desktopContract.mjs +3 -0
- package/bin/lib/discoveryContract.mjs +19 -0
- package/bin/lib/fleet.mjs +65 -10
- package/bin/lib/fleetReports.mjs +44 -3
- package/bin/lib/fleetRoster.mjs +1 -89
- package/bin/lib/fleetStartup.mjs +11 -14
- package/bin/lib/holder.mjs +4 -37
- package/bin/lib/hooks/mcpAllow.mjs +60 -0
- package/bin/lib/hooks/readGuard.mjs +13 -4
- package/bin/lib/imageArtifact.mjs +74 -0
- package/bin/lib/instance.mjs +20 -7
- package/bin/lib/landed.mjs +2 -2
- package/bin/lib/login.mjs +39 -4
- package/bin/lib/maintenance.mjs +47 -0
- package/bin/lib/maintenanceInventory.mjs +39 -0
- package/bin/lib/mcpInstall.mjs +163 -0
- package/bin/lib/mcpInventory.mjs +119 -0
- package/bin/lib/placeLock.mjs +9 -7
- package/bin/lib/planDependencies.mjs +70 -0
- package/bin/lib/postureTools.mjs +85 -0
- package/bin/lib/prWorkflow.mjs +15 -0
- package/bin/lib/projectLanded.mjs +16 -0
- package/bin/lib/projectRepositories.mjs +60 -0
- package/bin/lib/projectToolRuntimes.mjs +227 -3
- package/bin/lib/projectTools.mjs +63 -5
- package/bin/lib/projectWorktreeHome.mjs +25 -0
- package/bin/lib/prompts.mjs +170 -42
- package/bin/lib/repositoryBaseline.mjs +68 -0
- 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 +58 -18
- package/bin/lib/runtimeAntigravity.mjs +2 -1
- package/bin/lib/runtimeCapabilities.mjs +77 -3
- package/bin/lib/runtimeClaude.mjs +31 -10
- package/bin/lib/runtimeCodex.mjs +28 -59
- package/bin/lib/runtimeDetection.mjs +6 -0
- package/bin/lib/runtimeEvents.mjs +12 -1
- package/bin/lib/runtimeOfferings.mjs +75 -3
- 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/turnDiagnostics.mjs +93 -0
- package/bin/lib/turnLock.mjs +48 -8
- package/bin/lib/turnProfile.mjs +15 -3
- package/bin/lib/work.mjs +48 -29
- package/bin/lib/workAgentBrief.mjs +92 -0
- package/bin/lib/workAgentBriefRunner.mjs +59 -0
- package/bin/lib/workAgentCheck.mjs +6 -5
- package/bin/lib/workAgentMerges.mjs +19 -2
- package/bin/lib/workAgentPlans.mjs +21 -70
- package/bin/lib/workAgentPrecheck.mjs +10 -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 +167 -29
- package/bin/lib/workAgentTurnOutcome.mjs +20 -5
- package/bin/lib/workAgentTurns.mjs +92 -12
- package/bin/lib/workArtifactReconcile.mjs +79 -0
- package/bin/lib/workCappedTurn.mjs +10 -0
- package/bin/lib/workCommandAudit.mjs +8 -1
- package/bin/lib/workDelivery.mjs +21 -0
- package/bin/lib/workDiffs.mjs +18 -11
- package/bin/lib/workLiveMessages.mjs +129 -0
- package/bin/lib/workPlaceReports.mjs +5 -0
- package/bin/lib/workPlaces.mjs +24 -3
- package/bin/lib/workRetire.mjs +5 -0
- package/bin/lib/workSessionTurns.mjs +219 -52
- package/bin/lib/workShip.mjs +17 -16
- package/bin/lib/workToolLog.mjs +70 -3
- package/bin/lib/workTurnShape.mjs +6 -4
- package/package.json +7 -3
- package/npm-shrinkwrap.json +0 -59
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/** A readout alongside ONE running CLI, never another worker on its branch.
|
|
2
|
+
* No events since the last snapshot means no model call. A stopped child,
|
|
3
|
+
* pause, park or lost holder prevents a call or a late result from publishing. */
|
|
4
|
+
import { AGENT_BRIEF_SYSTEM, parseAgentBrief, briefModelFor } from './agentBrief.mjs';
|
|
5
|
+
|
|
6
|
+
export const AGENT_BRIEF_INTERVAL_MS = 3 * 60_000;
|
|
7
|
+
export const AGENT_BRIEF_FIRST_MS = 20_000;
|
|
8
|
+
export const AGENT_BRIEF_INPUT_MAX = 24_000;
|
|
9
|
+
export function makeAgentBrief({ runtime, model, context, summarize, post, scrub = (s) => s, models = () => [], now = Date.now,
|
|
10
|
+
setTimer = setTimeout, clearTimer = clearTimeout }) {
|
|
11
|
+
let running = false;
|
|
12
|
+
let stopped = false;
|
|
13
|
+
let timer = null;
|
|
14
|
+
let sequence = 0;
|
|
15
|
+
let consumed = 0;
|
|
16
|
+
let entries = [];
|
|
17
|
+
let flight = null;
|
|
18
|
+
let controller = null;
|
|
19
|
+
let held = null;
|
|
20
|
+
const append = (kind, data) => {
|
|
21
|
+
if (stopped || data == null) return;
|
|
22
|
+
// Scrub before trimming so a cut never turns a secret into an unrecognized prefix.
|
|
23
|
+
let text;
|
|
24
|
+
try {
|
|
25
|
+
text = (typeof data === 'string' ? scrub(data) : JSON.stringify(data, (_key, value) => {
|
|
26
|
+
// Redact decoded strings BEFORE JSON escapes quotes and newlines;
|
|
27
|
+
// otherwise a multiline secret no longer matches its learned value.
|
|
28
|
+
if (typeof value === 'string') return scrub(value);
|
|
29
|
+
if (value && typeof value === 'object' && !Array.isArray(value))
|
|
30
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [scrub(key), item]));
|
|
31
|
+
return value;
|
|
32
|
+
})).slice(0, 4_000);
|
|
33
|
+
} catch { return; } // an optional readout must never fail the working CLI
|
|
34
|
+
if (!text.trim()) return;
|
|
35
|
+
entries.push({ seq: ++sequence, kind, text });
|
|
36
|
+
while (entries.length > 100 || entries.reduce((sum, e) => sum + e.text.length, 0) > AGENT_BRIEF_INPUT_MAX) entries.shift();
|
|
37
|
+
};
|
|
38
|
+
const schedule = (ms = AGENT_BRIEF_INTERVAL_MS) => {
|
|
39
|
+
if (timer || stopped || !running) return;
|
|
40
|
+
timer = setTimer(() => { timer = null; void tick(); }, ms);
|
|
41
|
+
timer?.unref?.();
|
|
42
|
+
};
|
|
43
|
+
const refresh = async () => {
|
|
44
|
+
if (!running || stopped || (!held && sequence === consumed)) return;
|
|
45
|
+
// Retry a paid-for reading without paying for another. The API rechecks
|
|
46
|
+
// holder and turn in the write, and duplicate/older snapshots cannot win.
|
|
47
|
+
if (held) { if (await post(held)) held = null; return; }
|
|
48
|
+
const brain = briefModelFor(runtime, typeof model === 'function' ? model() : model, models());
|
|
49
|
+
if (!brain) return;
|
|
50
|
+
controller = new AbortController();
|
|
51
|
+
const state = await context(controller.signal);
|
|
52
|
+
if (state?.unsupported) { stopped = true; if (timer) clearTimer(timer); timer = null; return; }
|
|
53
|
+
if (stopped || !running || !state?.active || controller.signal.aborted) return;
|
|
54
|
+
// Card changes and daemon restarts do not reset the agent's three-minute
|
|
55
|
+
// cadence. Both dates came from the server, not this machine's clock.
|
|
56
|
+
if (state.brief && Date.parse(state.sourceAt) - Date.parse(state.brief.updatedAt) < AGENT_BRIEF_INTERVAL_MS) return;
|
|
57
|
+
const through = sequence;
|
|
58
|
+
const events = entries.filter((e) => e.seq > consumed);
|
|
59
|
+
if (!events.length) return;
|
|
60
|
+
const prompt = JSON.stringify({ previous: state.brief ?? null, taskId: state.taskId ?? null,
|
|
61
|
+
omittedEvents: Math.max(0, events[0].seq - consumed - 1), events: events.map(({ kind, text }) => ({ kind, text })) });
|
|
62
|
+
const out = await summarize({ runtime, ...brain, system: AGENT_BRIEF_SYSTEM, prompt, signal: controller.signal });
|
|
63
|
+
if (stopped || !running || controller.signal.aborted) return;
|
|
64
|
+
const content = parseAgentBrief(typeof out === 'string' ? out : out?.out, scrub);
|
|
65
|
+
if (!content) return;
|
|
66
|
+
consumed = through;
|
|
67
|
+
entries = entries.filter((e) => e.seq > through);
|
|
68
|
+
held = { ...content, sourceAt: state.sourceAt ?? new Date(now()).toISOString(), ...(out?.usage ? { usage: out.usage } : {}) };
|
|
69
|
+
if (await post(held)) held = null;
|
|
70
|
+
};
|
|
71
|
+
const tick = () => {
|
|
72
|
+
if (flight) return flight;
|
|
73
|
+
flight = refresh().catch(() => {}).finally(() => { flight = null; controller = null; schedule(); });
|
|
74
|
+
return flight;
|
|
75
|
+
};
|
|
76
|
+
return {
|
|
77
|
+
append, tick,
|
|
78
|
+
running(active) {
|
|
79
|
+
running = active;
|
|
80
|
+
if (active) schedule(AGENT_BRIEF_FIRST_MS);
|
|
81
|
+
else { if (timer) clearTimer(timer); timer = null; controller?.abort(); }
|
|
82
|
+
},
|
|
83
|
+
async stop() {
|
|
84
|
+
stopped = true;
|
|
85
|
+
running = false;
|
|
86
|
+
if (timer) clearTimer(timer);
|
|
87
|
+
timer = null;
|
|
88
|
+
controller?.abort();
|
|
89
|
+
await flight;
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/** A separate, short-lived CLI conversation using the same vendor's login.
|
|
2
|
+
* It stands in an empty scratch directory, never in the working agent's
|
|
3
|
+
* cwd-keyed resume. No project tools, no MCP, no repository instructions. */
|
|
4
|
+
import { mkdtempSync, rmSync } from 'node:fs';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { tmpdir } from 'node:os';
|
|
7
|
+
import { runTurn } from './runTurn.mjs';
|
|
8
|
+
import { runCappedTurn } from './workCappedTurn.mjs';
|
|
9
|
+
import { makeAgentBrief } from './workAgentBrief.mjs';
|
|
10
|
+
import { knownCodexModels } from './runtimeOfferings.mjs';
|
|
11
|
+
import { FLEET_URL, DAEMON_INSTANCE } from './config.mjs';
|
|
12
|
+
import { fleetRequest } from './fleetPost.mjs';
|
|
13
|
+
import { fleetEndpoint, isConsideredRefusal } from './fleetWire.mjs';
|
|
14
|
+
import { scrub } from './uplinkScrub.mjs';
|
|
15
|
+
|
|
16
|
+
export function createAgentBriefRunner({ admit, workChildren, run = runTurn, request = fleetRequest }) {
|
|
17
|
+
return ({ agentId, turnId, runtime, model }) => {
|
|
18
|
+
const identity = { agentId, turnId, instance: DAEMON_INSTANCE };
|
|
19
|
+
return makeAgentBrief({ runtime, model, scrub, models: knownCodexModels,
|
|
20
|
+
context: async (signal) => {
|
|
21
|
+
// Node >=20, including versions before AbortSignal.any was added.
|
|
22
|
+
const abort = new AbortController();
|
|
23
|
+
const timeout = AbortSignal.timeout(15_000);
|
|
24
|
+
const cancel = () => abort.abort();
|
|
25
|
+
signal.addEventListener('abort', cancel, { once: true });
|
|
26
|
+
timeout.addEventListener('abort', cancel, { once: true });
|
|
27
|
+
if (signal.aborted) cancel();
|
|
28
|
+
try {
|
|
29
|
+
const r = await request(fleetEndpoint('agent-brief-context', FLEET_URL), { body: identity, signal: abort.signal });
|
|
30
|
+
return r.ok ? r.json?.data : isConsideredRefusal(r.status) ? { unsupported: true } : null;
|
|
31
|
+
} finally {
|
|
32
|
+
signal.removeEventListener('abort', cancel);
|
|
33
|
+
timeout.removeEventListener('abort', cancel);
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
post: async (content) => {
|
|
37
|
+
const r = await request(fleetEndpoint('agent-brief', FLEET_URL), { body: { ...identity, ...content }, timeoutMs: 15_000 });
|
|
38
|
+
return r.ok || isConsideredRefusal(r.status);
|
|
39
|
+
},
|
|
40
|
+
summarize: async ({ signal, ...turnArgs }) => {
|
|
41
|
+
if (signal.aborted || admit('churn')) return '';
|
|
42
|
+
const releaseSlot = admit.reserve();
|
|
43
|
+
let cwd = null;
|
|
44
|
+
let usage = null;
|
|
45
|
+
try {
|
|
46
|
+
cwd = mkdtempSync(join(tmpdir(), 'flowviant-brief-'));
|
|
47
|
+
const result = await runCappedTurn({ runTurn: run, workChildren, releaseSlot, signal,
|
|
48
|
+
capMs: 60_000, transcriptCwd: cwd,
|
|
49
|
+
turnArgs: { ...turnArgs, cwd, profile: 'consult', answerFromResult: true, label: '[brief]',
|
|
50
|
+
onUsage: (u) => { usage = { ...u, runtime: turnArgs.runtime }; } } });
|
|
51
|
+
return result.wedged ? '' : { out: result.out, usage };
|
|
52
|
+
} finally {
|
|
53
|
+
releaseSlot();
|
|
54
|
+
if (cwd) rmSync(cwd, { recursive: true, force: true });
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
};
|
|
59
|
+
}
|
|
@@ -150,9 +150,9 @@ export function createWorkAgentCheck({
|
|
|
150
150
|
const CHECK_TIMEOUT_MS = 10 * 60_000;
|
|
151
151
|
const CHECK_OUTPUT_CAP = 4000;
|
|
152
152
|
|
|
153
|
-
const readCheckCommand = () => {
|
|
153
|
+
const readCheckCommand = (root = repoRoot) => {
|
|
154
154
|
try {
|
|
155
|
-
const raw = readFileSync(join(
|
|
155
|
+
const raw = readFileSync(join(root, '.flowviant', 'check.json'), 'utf8');
|
|
156
156
|
const cfg = JSON.parse(raw);
|
|
157
157
|
const cmd = typeof cfg?.command === 'string' ? cfg.command.trim() : '';
|
|
158
158
|
return cmd ? cmd.slice(0, 500) : null;
|
|
@@ -168,8 +168,8 @@ export function createWorkAgentCheck({
|
|
|
168
168
|
await postBestEffort(AGENT_CHECK_DONE_URL, body);
|
|
169
169
|
};
|
|
170
170
|
|
|
171
|
-
const runCheck = async (agentId, wt) => {
|
|
172
|
-
const cmd = readCheckCommand();
|
|
171
|
+
const runCheck = async (agentId, wt, repository) => {
|
|
172
|
+
const cmd = readCheckCommand(repository ? wt : repoRoot);
|
|
173
173
|
// OPTIONAL, like the pre-review's twin read: an unreadable head (a
|
|
174
174
|
// worktree retired mid-beat, an unborn branch) costs the answer its
|
|
175
175
|
// headSha, never the answer. This read was the one copy left unguarded,
|
|
@@ -178,7 +178,7 @@ export function createWorkAgentCheck({
|
|
|
178
178
|
const headSha = gitOrNull(['rev-parse', 'HEAD'], wt) ?? undefined;
|
|
179
179
|
if (!cmd) {
|
|
180
180
|
await postCheck({ agentId, status: 'none', ...(headSha ? { headSha } : {}) });
|
|
181
|
-
return;
|
|
181
|
+
return { status: 'none' };
|
|
182
182
|
}
|
|
183
183
|
const cleared = clearNonArtifacts(wt);
|
|
184
184
|
if (cleared.length)
|
|
@@ -325,6 +325,7 @@ export function createWorkAgentCheck({
|
|
|
325
325
|
output: envScrub(out.text).slice(-CHECK_OUTPUT_CAP),
|
|
326
326
|
...(headSha ? { headSha } : {}),
|
|
327
327
|
});
|
|
328
|
+
return out;
|
|
328
329
|
};
|
|
329
330
|
|
|
330
331
|
return { runCheck };
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
import { mergeRepositoryPlace } from './repositoryMerge.mjs';
|
|
2
|
+
import { fleetRequest } from './fleetPost.mjs';
|
|
1
3
|
/** Agent merge claims, pull-request approval, and branch cleanup. */
|
|
2
4
|
import { ensureOpenPr, ghReady, mergeAndVerifyTip, PR_URL_RE, relayedFailure } from './prWorkflow.mjs';
|
|
3
5
|
import { existsSync } from 'node:fs';
|
|
4
6
|
import { execFileSync } from 'node:child_process';
|
|
5
7
|
import { join } from 'node:path';
|
|
6
|
-
import { FLEET_URL } from './config.mjs';
|
|
8
|
+
import { FLEET_URL, DAEMON_INSTANCE } from './config.mjs';
|
|
7
9
|
import { git, gitOrNull, gitNet as gitNetIn, baseBranchName, isSafePathSegment, sessionBranch, stripUrlUserinfo } from './git.mjs';
|
|
8
10
|
import { isPublishRef, publishPushArgs, publishDeleteArgs, publishErrorText } from './agentPublish.mjs';
|
|
9
11
|
import { mergeOutward as shipMergeOutward } from './shipMerge.mjs';
|
|
@@ -30,6 +32,7 @@ export function agentMergeFailureDetail(error) {
|
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
export function createWorkAgentMerges({
|
|
35
|
+
repositoryPlaces,
|
|
33
36
|
repoRoot,
|
|
34
37
|
inPlace,
|
|
35
38
|
baseDir,
|
|
@@ -111,7 +114,7 @@ export function createWorkAgentMerges({
|
|
|
111
114
|
const report = async (body) => {
|
|
112
115
|
reported = true;
|
|
113
116
|
if (body?.ok === true) landedRef = job.publishedRef ?? null;
|
|
114
|
-
await postAgentMerge(body);
|
|
117
|
+
await postAgentMerge({ ...body, ...(body.detail ? { detail: envScrub(stripUrlUserinfo(String(body.detail))).slice(0, 2000) } : {}) });
|
|
115
118
|
};
|
|
116
119
|
|
|
117
120
|
// A WRITER on the place, exactly as a ship is. It folds base in and pushes
|
|
@@ -119,6 +122,20 @@ export function createWorkAgentMerges({
|
|
|
119
122
|
// does not know exists.
|
|
120
123
|
try {
|
|
121
124
|
await inPlace(place, true, async () => {
|
|
125
|
+
if (job.repositoryMerge) {
|
|
126
|
+
if (!repositoryPlaces) throw new Error('Project repository worktrees are unavailable.');
|
|
127
|
+
const result = await mergeRepositoryPlace({ job, entries: repositoryPlaces.entries(place), baseDir,
|
|
128
|
+
check: (r) => runReviewEntry(agentId, r.wt, job.agentName, r),
|
|
129
|
+
progress: async (states, retry) => {
|
|
130
|
+
const response = await fleetRequest(fleetEndpoint('agent-merge-progress', FLEET_URL), {
|
|
131
|
+
body: { agentId, instance: DAEMON_INSTANCE, states, retry } });
|
|
132
|
+
if (response.error || response.json?.data?.ok !== true) throw new Error('Repository merge progress was not acknowledged. The next attempt will remeasure base.');
|
|
133
|
+
} });
|
|
134
|
+
if (result.retry) reported = true; // The lease was released; approval remains.
|
|
135
|
+
else await report({ agentId, ...result });
|
|
136
|
+
onRepoChanged();
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
122
139
|
const wt = worktreePath(baseDir, place);
|
|
123
140
|
if (!existsSync(wt)) {
|
|
124
141
|
await report({ agentId, ok: false, detail: 'the worktree is gone' });
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
/** Agent plan claims, read-only planning turns, and proposal reports. */
|
|
2
|
-
import {
|
|
2
|
+
import { discoveryGuide } from './discoveryContract.mjs';
|
|
3
|
+
import { compareRepositoryBaselines, measureRepositoryBaseline, repositoryBaselineOf } from './repositoryBaseline.mjs';
|
|
4
|
+
import { knowledgeDirFor } from './knowledgeFiles.mjs';
|
|
5
|
+
import { createToolLog } from './workToolLog.mjs';
|
|
3
6
|
import { FLEET_URL, DAEMON_INSTANCE } from './config.mjs';
|
|
4
|
-
import {
|
|
7
|
+
import { measureAgentFootprint, liveAgentContext } from './agentFootprint.mjs';
|
|
5
8
|
import { parseProposal } from './agentPlan.mjs';
|
|
6
9
|
import { c, note } from './ui.mjs';
|
|
7
10
|
import { runTurn as defaultRunTurn } from './runTurn.mjs';
|
|
8
|
-
import { SYSTEM_PLAN, AGENT_PLAN_KICKOFF } from './prompts.mjs';
|
|
11
|
+
import { SYSTEM_PLAN, AGENT_PLAN_KICKOFF, withProjectContext, REPOSITORY_BASELINE_PARAGRAPH } from './prompts.mjs';
|
|
9
12
|
import { scrub as envScrub } from './uplinkScrub.mjs';
|
|
10
13
|
import { canRun, runtimeById, runtimeLabel } from './runtimes.mjs';
|
|
11
14
|
import { detectRuntimes as defaultDetectRuntimes, pickRuntimeFor as defaultPickRuntimeFor } from './runtimeDetection.mjs';
|
|
@@ -13,7 +16,7 @@ import { brainFor } from './workBrain.mjs';
|
|
|
13
16
|
import { runCappedTurn } from './workCappedTurn.mjs';
|
|
14
17
|
import { fleetEndpoint } from './fleetWire.mjs';
|
|
15
18
|
import { claimFleetLease, fleetHeaders } from './fleetPost.mjs';
|
|
16
|
-
import { REPO_PLACE
|
|
19
|
+
import { REPO_PLACE } from './workPlaces.mjs';
|
|
17
20
|
import { addUsage } from './turnUsage.mjs';
|
|
18
21
|
|
|
19
22
|
export function createWorkAgentPlans({
|
|
@@ -133,56 +136,6 @@ export function createWorkAgentPlans({
|
|
|
133
136
|
// The peer may hold it; doing nothing is the safe answer.
|
|
134
137
|
const claimAgentPlan = (id) => claimFleetLease(AGENT_PLAN_CLAIM_URL, { id });
|
|
135
138
|
|
|
136
|
-
/**
|
|
137
|
-
* What a live agent has already TOUCHED, measured here rather than sent.
|
|
138
|
-
*
|
|
139
|
-
* The server could have shipped a copy of this, and deliberately does not:
|
|
140
|
-
* these are directories on THIS machine, and a server-side copy would be a
|
|
141
|
-
* second, staler source of truth for a fact the daemon is standing on top of.
|
|
142
|
-
*
|
|
143
|
-
* Uncommitted work AND commits against base, because a planner asking "will
|
|
144
|
-
* these collide" cares about both — a file this agent has already rewritten
|
|
145
|
-
* and merged into its own branch collides exactly as hard as one it is
|
|
146
|
-
* editing now.
|
|
147
|
-
*/
|
|
148
|
-
const agentChangedFiles = (placeId) => {
|
|
149
|
-
if (!placeId || !isSafePathSegment(placeId)) return [];
|
|
150
|
-
const wt = worktreePath(baseDir, placeId);
|
|
151
|
-
if (!existsSync(wt)) return [];
|
|
152
|
-
const out = new Set();
|
|
153
|
-
for (const args of [
|
|
154
|
-
['diff', '--name-only', 'HEAD'],
|
|
155
|
-
['diff', '--name-only', `${baseRef()}...HEAD`],
|
|
156
|
-
['ls-files', '--others', '--exclude-standard'],
|
|
157
|
-
]) {
|
|
158
|
-
/**
|
|
159
|
-
* `git()` THROWS; it does not return a non-string.
|
|
160
|
-
*
|
|
161
|
-
* The guard below was `if (typeof r !== 'string') continue`, which is a
|
|
162
|
-
* value `execFileSync` can never produce — so every one of these calls
|
|
163
|
-
* was effectively unguarded. And this runs over the LIVE AGENTS a plan
|
|
164
|
-
* job carries, which include agents still in `planning`: they have no
|
|
165
|
-
* worktree yet, so `git diff` in a directory that does not exist exits
|
|
166
|
-
* non-zero and throws straight out of `runAgentPlan` — after the press
|
|
167
|
-
* has been claimed and before the try/finally below. The press then sat
|
|
168
|
-
* `planning` until its expiry, having spent nothing and explained
|
|
169
|
-
* nothing, on what would be an ordinary second Deploy.
|
|
170
|
-
*
|
|
171
|
-
* An agent whose files cannot be read contributes none — `gitOrNull`
|
|
172
|
-
* (git.mjs) reads a failure as null. That is the honest answer and it
|
|
173
|
-
* is what the planner should be told.
|
|
174
|
-
*/
|
|
175
|
-
const r = gitOrNull(args, wt);
|
|
176
|
-
if (r == null) continue;
|
|
177
|
-
for (const line of r.split('\n')) {
|
|
178
|
-
const f = line.trim();
|
|
179
|
-
if (f) out.add(f);
|
|
180
|
-
if (out.size >= 60) return [...out];
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
return [...out];
|
|
184
|
-
};
|
|
185
|
-
|
|
186
139
|
/** See `runAgentTurn` — the admission reservation, released the moment the
|
|
187
140
|
* planner's CLI exists. */
|
|
188
141
|
const runAgentPlan = async (job, releaseSlot = () => {}) => {
|
|
@@ -268,20 +221,6 @@ export function createWorkAgentPlans({
|
|
|
268
221
|
lastPlanBeat = Date.now();
|
|
269
222
|
void postAgentPlanActivity(id, line);
|
|
270
223
|
};
|
|
271
|
-
const liveAgents = (Array.isArray(job.liveAgents) ? job.liveAgents : []).map((a) => ({
|
|
272
|
-
id: String(a?.id ?? ''),
|
|
273
|
-
name: String(a?.name ?? ''),
|
|
274
|
-
status: String(a?.status ?? ''),
|
|
275
|
-
// Belt: `agentChangedFiles` is defensive internally, but it resolves a
|
|
276
|
-
// place first and this whole block sits outside the try below.
|
|
277
|
-
changedFiles: (() => {
|
|
278
|
-
try {
|
|
279
|
-
return agentChangedFiles(a?.placeId);
|
|
280
|
-
} catch {
|
|
281
|
-
return [];
|
|
282
|
-
}
|
|
283
|
-
})(),
|
|
284
|
-
}));
|
|
285
224
|
|
|
286
225
|
let out = '';
|
|
287
226
|
/** The cap fired: the CLI was still running when this machine stopped it. */
|
|
@@ -294,7 +233,8 @@ export function createWorkAgentPlans({
|
|
|
294
233
|
* a report the retry posts again adds nothing. Null until the CLI reports.
|
|
295
234
|
*/
|
|
296
235
|
let usage = null;
|
|
297
|
-
const
|
|
236
|
+
const discovery = createToolLog(repoRoot);
|
|
237
|
+
const spent = () => ({ ...(usage ? { usage } : {}), tools: discovery.toolLog });
|
|
298
238
|
// Said BEFORE the lock is asked for, because a reader only waits when a
|
|
299
239
|
// writer holds the checkout or is queued ahead of it — which is exactly
|
|
300
240
|
// this condition, read one line before we join the queue.
|
|
@@ -315,6 +255,12 @@ export function createWorkAgentPlans({
|
|
|
315
255
|
* fork a read-only planner into a build tab, and being what their own
|
|
316
256
|
* `claude --continue` resumed.
|
|
317
257
|
*/
|
|
258
|
+
const liveAgents = (Array.isArray(job.liveAgents) ? job.liveAgents : []).map(a =>
|
|
259
|
+
liveAgentContext(a, measureAgentFootprint({ placeId: a?.placeId, baseDir, baseRef })));
|
|
260
|
+
const knowledgeDir = knowledgeDirFor(repoRoot);
|
|
261
|
+
discovery.toolLog.baseline = measureRepositoryBaseline({ cwd: repoRoot, baseRef, knowledgeDir });
|
|
262
|
+
discovery.toolLog.captureBaseline = repositoryBaselineOf(job.captureBaseline);
|
|
263
|
+
discovery.toolLog.reconciliation = compareRepositoryBaselines(discovery.toolLog.baseline, discovery.toolLog.captureBaseline);
|
|
318
264
|
({ out, wedged } = await runCappedTurn({
|
|
319
265
|
runTurn,
|
|
320
266
|
capMs: PLAN_TURN_TIMEOUT_MS,
|
|
@@ -322,12 +268,14 @@ export function createWorkAgentPlans({
|
|
|
322
268
|
releaseSlot,
|
|
323
269
|
transcriptCwd: repoRoot,
|
|
324
270
|
turnArgs: {
|
|
271
|
+
diagnosticPlanId: id,
|
|
325
272
|
prompt: AGENT_PLAN_KICKOFF({
|
|
326
273
|
tasks,
|
|
327
274
|
liveAgents,
|
|
328
275
|
agentCap: Number.isInteger(job.agentCap) && job.agentCap > 0 ? job.agentCap : 3,
|
|
329
276
|
}),
|
|
330
|
-
system: SYSTEM_PLAN,
|
|
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),
|
|
278
|
+
knowledgeDir,
|
|
331
279
|
// READ-ONLY, and no MCP: `mcpArgs` is omitted entirely rather than
|
|
332
280
|
// passed empty, so there is no control plane on this turn at all.
|
|
333
281
|
profile: 'consult',
|
|
@@ -342,6 +290,7 @@ export function createWorkAgentPlans({
|
|
|
342
290
|
onActivity: (a) => {
|
|
343
291
|
if (a?.label) narrate(String(a.label));
|
|
344
292
|
},
|
|
293
|
+
onToolEvent: discovery.pushToolEvent,
|
|
345
294
|
// Tagged with the CLI that counted it; one turn, so the sum is the
|
|
346
295
|
// one report (turnUsage.mjs keeps it null-safe).
|
|
347
296
|
onUsage: (u) => {
|
|
@@ -354,11 +303,13 @@ export function createWorkAgentPlans({
|
|
|
354
303
|
}));
|
|
355
304
|
});
|
|
356
305
|
} catch (e) {
|
|
306
|
+
discovery.toolLog.state = 'partial';
|
|
357
307
|
await postAgentPlan({ id, error: envScrub(String(e?.message || e)).slice(0, 500), ...spent() });
|
|
358
308
|
return;
|
|
359
309
|
}
|
|
360
310
|
|
|
361
311
|
if (wedged) {
|
|
312
|
+
discovery.toolLog.state = 'partial';
|
|
362
313
|
// NEVER LEAVE A CLAIMED PRESS UNREPORTED — the belt the merge lane wears
|
|
363
314
|
// beneath this one. The machine's own words, because the machine is the
|
|
364
315
|
// only side that knows: the server would have expired this press in
|
|
@@ -116,8 +116,8 @@ export function createWorkAgentPrecheck({ baseRef, admit, sessionMetaPath, workC
|
|
|
116
116
|
* ref at all; an unreadable range costs the reviewer its context, never the
|
|
117
117
|
* review-entry beat it is standing in.
|
|
118
118
|
*/
|
|
119
|
-
const branchLog = (wt) => {
|
|
120
|
-
const out = gitOrNull(['log', '--format=%s%n%b%n--', `${
|
|
119
|
+
const branchLog = (wt, ref = baseRef()) => {
|
|
120
|
+
const out = gitOrNull(['log', '--format=%s%n%b%n--', `${ref}..HEAD`, '--not', ref], wt);
|
|
121
121
|
if (out == null) return { text: '', taskIds: [] };
|
|
122
122
|
// LINEAR, never backtracking (audit 2026-09-24) — this was
|
|
123
123
|
// `out.matchAll(/^\s*Flowviant-Task:\s*(\S+)\s*$/gm)`, whose `\s*$` is
|
|
@@ -135,7 +135,7 @@ export function createWorkAgentPrecheck({ baseRef, admit, sessionMetaPath, workC
|
|
|
135
135
|
return { text, taskIds: [...taskIds] };
|
|
136
136
|
};
|
|
137
137
|
|
|
138
|
-
const runPrecheck = async (agentId, wt, agentName) => {
|
|
138
|
+
const runPrecheck = async (agentId, wt, agentName, repository) => {
|
|
139
139
|
/**
|
|
140
140
|
* WHICH CLI READS. `pickRuntimeFor('consult')` — the same picker the scratch
|
|
141
141
|
* planner uses, and for the same reason: this prompt was written against
|
|
@@ -166,7 +166,7 @@ export function createWorkAgentPrecheck({ baseRef, admit, sessionMetaPath, workC
|
|
|
166
166
|
const headSha = gitOrNull(['rev-parse', 'HEAD'], wt);
|
|
167
167
|
|
|
168
168
|
const stash = readStash(sessionMetaPath(wt, 'flowviant-agent-cards', agentId));
|
|
169
|
-
const log = branchLog(wt);
|
|
169
|
+
const log = branchLog(wt, repository?.baseRef ?? baseRef());
|
|
170
170
|
/** Cards the BRANCH names that this box has no spec for. Measured, not
|
|
171
171
|
* guessed — see agentCards.mjs on why a stash is per-box. */
|
|
172
172
|
const held = new Set(stash.map((s) => s.taskId));
|
|
@@ -224,7 +224,7 @@ export function createWorkAgentPrecheck({ baseRef, admit, sessionMetaPath, workC
|
|
|
224
224
|
cards: stash.map((s) => s.prompt).join('\n---\n'),
|
|
225
225
|
missingSpecs,
|
|
226
226
|
commits: log.text,
|
|
227
|
-
diffCommand: `git diff ${baseRef()}...HEAD`,
|
|
227
|
+
diffCommand: `git diff ${repository?.baseRef ?? baseRef()}...HEAD`,
|
|
228
228
|
}),
|
|
229
229
|
system: SYSTEM_PRECHECK,
|
|
230
230
|
// READ-ONLY, and no MCP: `mcpArgs` is omitted entirely rather than
|
|
@@ -314,10 +314,15 @@ export function createWorkAgentPrecheck({ baseRef, admit, sessionMetaPath, workC
|
|
|
314
314
|
agentId,
|
|
315
315
|
...(headSha ? { headSha } : {}),
|
|
316
316
|
...(usage ? { usage } : {}),
|
|
317
|
+
// Named fields, never a spread of what the parser returned: a field it
|
|
318
|
+
// keeps must be passed on here as well. `criteria` rides beside the verdict
|
|
319
|
+
// and note (2026-10-10), and an older server strips it and draws the one
|
|
320
|
+
// row it always drew.
|
|
317
321
|
cards: result.cards.map((cd) => ({
|
|
318
322
|
taskId: cd.taskId,
|
|
319
323
|
verdict: cd.verdict,
|
|
320
324
|
...(cd.note ? { note: cd.note } : {}),
|
|
325
|
+
...(cd.criteria ? { criteria: cd.criteria } : {}),
|
|
321
326
|
})),
|
|
322
327
|
...(result.overall ? { overall: result.overall } : {}),
|
|
323
328
|
};
|
|
@@ -8,9 +8,9 @@
|
|
|
8
8
|
* agentPublish.mjs's (pure, tested there); this module owns the STATE and the
|
|
9
9
|
* network calls. The agent lane pushes after a settle and fetches at the
|
|
10
10
|
* begun-guard, the sweep keeps a published branch current, the merge lane
|
|
11
|
-
* retires the ref — all through the maps and
|
|
11
|
+
* retires the ref — all through the maps and functions returned here.
|
|
12
12
|
*/
|
|
13
|
-
import { gitOrNull, sessionBranchRef } from './git.mjs';
|
|
13
|
+
import { gitOrNull, sessionBranchRef, gitNetAsync } from './git.mjs';
|
|
14
14
|
import { publishPushArgs, publishFetchArgs, publishErrorText } from './agentPublish.mjs';
|
|
15
15
|
import { scrub as envScrub } from './uplinkScrub.mjs';
|
|
16
16
|
|
|
@@ -41,6 +41,8 @@ export function createWorkAgentPublish({ repoRoot, gitNet }) {
|
|
|
41
41
|
* something a rebase can invalidate.
|
|
42
42
|
*/
|
|
43
43
|
const agentPublished = new Map();
|
|
44
|
+
let publishingStopped = false;
|
|
45
|
+
const stopAgentPublishing = () => { publishingStopped = true; };
|
|
44
46
|
/**
|
|
45
47
|
* WHAT THIS PROCESS HAS SEEN AT EACH AGENT'S REMOTE REF — `{ ref, sha }`, and
|
|
46
48
|
* the ONLY input to the push's lease.
|
|
@@ -85,6 +87,7 @@ export function createWorkAgentPublish({ repoRoot, gitNet }) {
|
|
|
85
87
|
* Never throws.
|
|
86
88
|
*/
|
|
87
89
|
const publishAgentBranch = async (place, target) => {
|
|
90
|
+
if (publishingStopped) return false;
|
|
88
91
|
const sha = agentBranchSha(place);
|
|
89
92
|
if (!sha) return false;
|
|
90
93
|
const prev = agentPublished.get(place);
|
|
@@ -124,8 +127,8 @@ export function createWorkAgentPublish({ repoRoot, gitNet }) {
|
|
|
124
127
|
* BRING A PUBLISHED BRANCH BACK DOWN — the other half of the durability
|
|
125
128
|
* promise, and the only thing that lets an agent's work outlive its box.
|
|
126
129
|
*
|
|
127
|
-
* Reached
|
|
128
|
-
*
|
|
130
|
+
* Reached after the begun guard measures neither a worktree nor a branch,
|
|
131
|
+
* by the ordinary turn path or the holder's recovery inspection.
|
|
129
132
|
* The server only sends `publishedRef` when it heard about a real push, so
|
|
130
133
|
* this is not a guess at a remote branch — it is a fetch of one a machine
|
|
131
134
|
* reported writing.
|
|
@@ -159,6 +162,16 @@ export function createWorkAgentPublish({ repoRoot, gitNet }) {
|
|
|
159
162
|
why: publishErrorText(envScrub(e?.stderr?.toString?.() || e?.message || '')),
|
|
160
163
|
};
|
|
161
164
|
}
|
|
165
|
+
return fetchedBranch(place, ref);
|
|
166
|
+
};
|
|
167
|
+
const fetchPublishedBranchAsync = async (place, ref) => {
|
|
168
|
+
const args = publishFetchArgs(ref, place);
|
|
169
|
+
if (!args) return null;
|
|
170
|
+
try { await gitNetAsync(args, repoRoot, 120_000); }
|
|
171
|
+
catch (e) { return { ok: false, why: publishErrorText(envScrub(e?.stderr?.toString?.() || e?.message || '')) }; }
|
|
172
|
+
return fetchedBranch(place, ref);
|
|
173
|
+
};
|
|
174
|
+
const fetchedBranch = (place, ref) => {
|
|
162
175
|
const sha = agentBranchSha(place);
|
|
163
176
|
if (!sha) return { ok: false, why: 'the fetch reported nothing and left no local branch' };
|
|
164
177
|
// WHAT THIS PROCESS HAS NOW SEEN AT THAT REMOTE REF. A fetch is an
|
|
@@ -171,5 +184,5 @@ export function createWorkAgentPublish({ repoRoot, gitNet }) {
|
|
|
171
184
|
return { ok: true };
|
|
172
185
|
};
|
|
173
186
|
|
|
174
|
-
return { agentPublished, agentRemoteAt, publishAgentBranch, fetchPublishedBranch };
|
|
187
|
+
return { agentPublished, agentRemoteAt, publishAgentBranch, stopAgentPublishing, fetchPublishedBranch, fetchPublishedBranchAsync };
|
|
175
188
|
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { DAEMON_INSTANCE, FLEET_URL } from './config.mjs';
|
|
2
|
+
import { myPubB64 } from './boxIdentity.mjs';
|
|
3
|
+
import { isSafePathSegment, sessionBranchRef, gitNetAsync } from './git.mjs';
|
|
4
|
+
import { begunTurnRefusal } from './workAgentTurnBegun.mjs';
|
|
5
|
+
import { fleetEndpoint } from './fleetWire.mjs';
|
|
6
|
+
import { fleetRequest } from './fleetPost.mjs';
|
|
7
|
+
|
|
8
|
+
/** Inspect before any cut or CLI. A failed request waits for the next poll. */
|
|
9
|
+
export function createAgentRecovery({
|
|
10
|
+
baseDir, repoRoot, fetchPublishedBranch, inPlace, placeWtFor, reportSessionWorktree,
|
|
11
|
+
boxId = () => myPubB64(), fleetUrl = FLEET_URL,
|
|
12
|
+
request = async (job, id) => {
|
|
13
|
+
const result = await fleetRequest(fleetEndpoint('agent-recover', fleetUrl), { body: {
|
|
14
|
+
agentId: job.agentId, sourceKey: job.sourceKey, sourceBoxId: job.sourceBoxId,
|
|
15
|
+
boxId: id, daemonInstance: DAEMON_INSTANCE,
|
|
16
|
+
} });
|
|
17
|
+
return result.ok && result.json?.data?.recovered === true;
|
|
18
|
+
},
|
|
19
|
+
} = {}) {
|
|
20
|
+
const ready = new Map(); // source snapshot inspected for the next poll
|
|
21
|
+
let checking = null; // one repository fetch at a time; polling never waits
|
|
22
|
+
let lastInspected = null;
|
|
23
|
+
const processAgentRecoveryJobs = (jobs) => {
|
|
24
|
+
const blocked = new Set();
|
|
25
|
+
if (!Array.isArray(jobs)) return blocked;
|
|
26
|
+
const id = boxId();
|
|
27
|
+
if (!id) return blocked;
|
|
28
|
+
const offered = new Set(jobs.map((j) => j?.agentId));
|
|
29
|
+
for (const agent of ready.keys()) if (!offered.has(agent)) ready.delete(agent);
|
|
30
|
+
// A failed inspection must not keep every agent behind it waiting.
|
|
31
|
+
const next = jobs.findIndex((j) => j?.agentId === lastInspected) + 1;
|
|
32
|
+
for (const job of [...jobs.slice(next), ...jobs.slice(0, next)]) {
|
|
33
|
+
if (!job || !isSafePathSegment(job.agentId) || !isSafePathSegment(job.placeId) ||
|
|
34
|
+
sessionBranchRef(job.placeId) === null || typeof job.sourceKey !== 'string' ||
|
|
35
|
+
!job.sourceBoxId || job.sourceBoxId === id || job.placeId !== `a-${job.agentId}`) continue;
|
|
36
|
+
const key = `${job.sourceBoxId}:${job.sourceKey}`;
|
|
37
|
+
if (ready.get(job.agentId) === key) { ready.delete(job.agentId); continue; }
|
|
38
|
+
blocked.add(job.agentId);
|
|
39
|
+
if (checking) continue;
|
|
40
|
+
lastInspected = job.agentId;
|
|
41
|
+
checking = Promise.resolve().then(() => inPlace(job.placeId, true, async () => {
|
|
42
|
+
let available = false;
|
|
43
|
+
const facts = { begunOn: job.begunOn, publishedRef: job.publishedRef };
|
|
44
|
+
// The measurement stays in the begun guard. Only its network fallback
|
|
45
|
+
// is asynchronous here, so a slow origin never stops heartbeats.
|
|
46
|
+
const missing = begunTurnRefusal(facts, job.placeId, {
|
|
47
|
+
baseDir, repoRoot, fetchPublishedBranch: () => null,
|
|
48
|
+
onAvailable: () => { available = true; },
|
|
49
|
+
});
|
|
50
|
+
let refusal = null;
|
|
51
|
+
if (missing) {
|
|
52
|
+
const fetched = await fetchPublishedBranch(job.placeId, job.publishedRef);
|
|
53
|
+
refusal = begunTurnRefusal(facts, job.placeId, {
|
|
54
|
+
baseDir, repoRoot, fetchPublishedBranch: () => fetched,
|
|
55
|
+
onAvailable: () => { available = true; },
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
if (!refusal) {
|
|
59
|
+
// Unknown reads start no redo and leave the ordinary begun guard
|
|
60
|
+
// to report its own git failure. Inspect again on a later poll.
|
|
61
|
+
// Review and Stuck have no offered task turn to attach and measure
|
|
62
|
+
// the recovered branch. Report custody here for every available one.
|
|
63
|
+
if (available && placeWtFor?.(job.placeId)) await reportSessionWorktree?.(job.placeId);
|
|
64
|
+
ready.set(job.agentId, key);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
// Never run or settle the old offer while a restart is being decided.
|
|
68
|
+
await request(job, id);
|
|
69
|
+
})).catch(() => {}).finally(() => { checking = null; });
|
|
70
|
+
}
|
|
71
|
+
return blocked;
|
|
72
|
+
};
|
|
73
|
+
// A test or a graceful owner may wait for the current inspection. The fleet
|
|
74
|
+
// loop deliberately does not: the next poll observes its result.
|
|
75
|
+
processAgentRecoveryJobs.settled = () => checking ?? Promise.resolve();
|
|
76
|
+
return processAgentRecoveryJobs;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
/** Refresh before cutting a redo. A failed fetch keeps the prompt pending. */
|
|
81
|
+
export async function refreshRestartBase(job, repoRoot) {
|
|
82
|
+
if (!job.freshAfterSwap) return true;
|
|
83
|
+
try { await gitNetAsync(['fetch', '--prune', 'origin'], repoRoot); return true; }
|
|
84
|
+
catch { return false; }
|
|
85
|
+
}
|
|
@@ -27,17 +27,19 @@ export function createWorkAgentReview({ runCheck, runPrecheck }) {
|
|
|
27
27
|
* INSIDE the place's writer lock on a path whose callers settle real work —
|
|
28
28
|
* `runAgentMerge` in particular reports a claimed merge after this returns.
|
|
29
29
|
*/
|
|
30
|
-
const runReviewEntry = async (agentId, wt, agentName) => {
|
|
30
|
+
const runReviewEntry = async (agentId, wt, agentName, repository) => {
|
|
31
|
+
let result = { status: 'unknown' };
|
|
31
32
|
try {
|
|
32
|
-
await runCheck(agentId, wt);
|
|
33
|
+
result = await runCheck(agentId, wt, repository) ?? result;
|
|
33
34
|
} catch {
|
|
34
35
|
/* the row keeps its previous check answer, which is null the first time */
|
|
35
36
|
}
|
|
36
37
|
try {
|
|
37
|
-
await runPrecheck(agentId, wt, agentName);
|
|
38
|
+
await runPrecheck(agentId, wt, agentName, repository);
|
|
38
39
|
} catch {
|
|
39
40
|
/* no pre-review is posted, and absence renders nothing */
|
|
40
41
|
}
|
|
42
|
+
return result;
|
|
41
43
|
};
|
|
42
44
|
|
|
43
45
|
return { runReviewEntry };
|
|
@@ -40,11 +40,11 @@ import { worktreePath } from './workPlaces.mjs';
|
|
|
40
40
|
* cleaned up on THIS box is same-box recovery of real committed work and
|
|
41
41
|
* behaves exactly as it did before this guard existed.
|
|
42
42
|
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
43
|
+
* This remains the same-box and older-server refusal. The recovery coordinator
|
|
44
|
+
* now inspects foreign work before this ordinary turn path: after a person's
|
|
45
|
+
* machine swap it can replace missing work with an explicit start from briefs.
|
|
46
46
|
*/
|
|
47
|
-
export function begunTurnRefusal(job, place, { baseDir, repoRoot, fetchPublishedBranch }) {
|
|
47
|
+
export function begunTurnRefusal(job, place, { baseDir, repoRoot, fetchPublishedBranch, onAvailable = () => {} }) {
|
|
48
48
|
// The caller validated `place` (workAgentTurns); an unsafe one would give a
|
|
49
49
|
// null directory AND a null ref below, and the null ref stands the guard
|
|
50
50
|
// down — the same fail-open direction as an unreadable repo.
|
|
@@ -105,9 +105,11 @@ export function begunTurnRefusal(job, place, { baseDir, repoRoot, fetchPublished
|
|
|
105
105
|
(fetched ? ` Its published branch could not be fetched (${fetched.why}).` : '')
|
|
106
106
|
);
|
|
107
107
|
}
|
|
108
|
+
onAvailable();
|
|
108
109
|
// The branch is here and measured. `placeWtFor`'s attach fallback
|
|
109
110
|
// opens a worktree on it below — the same path a directory somebody
|
|
110
111
|
// cleaned up already takes, on commits this box now genuinely holds.
|
|
111
112
|
}
|
|
113
|
+
if (hasBranch || (wtDir && existsSync(wtDir))) onAvailable();
|
|
112
114
|
return null;
|
|
113
115
|
}
|