flowviant 0.115.0 → 0.117.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -12
- package/bin/cli.mjs +13 -17
- package/bin/lib/admission.mjs +7 -14
- package/bin/lib/agentInternalWait.mjs +83 -0
- package/bin/lib/agentPlan.mjs +56 -24
- package/bin/lib/agentPublish.mjs +12 -13
- package/bin/lib/agentTaskKinds.mjs +4 -7
- package/bin/lib/agySessions.mjs +3 -3
- package/bin/lib/artifactPreview.mjs +92 -18
- package/bin/lib/artifactReporter.mjs +16 -8
- package/bin/lib/artifacts.mjs +64 -60
- package/bin/lib/authproxy.mjs +2 -1
- package/bin/lib/childEnv.mjs +9 -4
- package/bin/lib/claude.mjs +10 -20
- package/bin/lib/claudePersonal.mjs +71 -6
- package/bin/lib/claudePosture.mjs +119 -42
- package/bin/lib/claudeSessions.mjs +7 -7
- package/bin/lib/claudeStream.mjs +7 -11
- package/bin/lib/cliLimit.mjs +2 -2
- package/bin/lib/codexHome.mjs +17 -0
- package/bin/lib/codexPersonal.mjs +44 -3
- package/bin/lib/codexRelay.mjs +21 -5
- package/bin/lib/codexResearch.mjs +180 -0
- package/bin/lib/config.mjs +37 -24
- package/bin/lib/conversationIds.mjs +51 -0
- package/bin/lib/credentials.mjs +2 -7
- package/bin/lib/daemonLocks.mjs +88 -0
- package/bin/lib/daemonState.mjs +5 -5
- package/bin/lib/deploy.mjs +6 -9
- package/bin/lib/deployCheckout.mjs +26 -22
- package/bin/lib/deployRunner.mjs +54 -18
- package/bin/lib/deployWire.mjs +32 -23
- package/bin/lib/desktopContract.mjs +3 -5
- package/bin/lib/env.mjs +104 -35
- package/bin/lib/fleet.mjs +7 -9
- package/bin/lib/fleetPost.mjs +172 -25
- package/bin/lib/fleetReports.mjs +130 -170
- package/bin/lib/fleetStartup.mjs +6 -6
- package/bin/lib/fleetWire.mjs +42 -0
- package/bin/lib/git.mjs +111 -61
- package/bin/lib/gitMergeIdentity.mjs +2 -8
- package/bin/lib/grant.mjs +23 -7
- package/bin/lib/holder.mjs +108 -81
- package/bin/lib/hooks/readGuard.mjs +41 -7
- package/bin/lib/install.mjs +80 -33
- package/bin/lib/instance.mjs +36 -83
- package/bin/lib/intakeDraft.mjs +4 -1
- package/bin/lib/knowledgeFileSync.mjs +31 -0
- package/bin/lib/knowledgeFiles.mjs +28 -83
- package/bin/lib/landed.mjs +22 -29
- package/bin/lib/launchCommand.mjs +91 -0
- package/bin/lib/listeners.mjs +0 -5
- package/bin/lib/localSessions.mjs +52 -39
- package/bin/lib/login.mjs +22 -12
- package/bin/lib/loginPath.mjs +22 -17
- package/bin/lib/machineListing.mjs +6 -3
- package/bin/lib/mcp-cli.mjs +9 -10
- package/bin/lib/noWindowEnv.mjs +23 -3
- package/bin/lib/ownedDirs.mjs +99 -0
- package/bin/lib/pinnedCapture.mjs +383 -0
- package/bin/lib/prWorkflow.mjs +94 -17
- package/bin/lib/preflight.mjs +8 -25
- package/bin/lib/preview.mjs +19 -25
- package/bin/lib/previewGatePolicy.mjs +34 -9
- package/bin/lib/previewRegistry.mjs +4 -11
- package/bin/lib/printable.mjs +70 -7
- package/bin/lib/procRegistry.mjs +33 -13
- package/bin/lib/procScan.mjs +2 -3
- package/bin/lib/procStat.mjs +76 -0
- package/bin/lib/processGroup.mjs +48 -0
- package/bin/lib/processes.mjs +10 -24
- package/bin/lib/projectToolRuntimes.mjs +101 -16
- package/bin/lib/projectTools.mjs +50 -32
- package/bin/lib/prompts.mjs +41 -19
- package/bin/lib/repoPath.mjs +19 -0
- package/bin/lib/repoState.mjs +17 -19
- package/bin/lib/runTurn.mjs +268 -210
- package/bin/lib/runtimeAntigravity.mjs +16 -5
- package/bin/lib/runtimeCapabilities.mjs +140 -20
- package/bin/lib/runtimeClaude.mjs +55 -5
- package/bin/lib/runtimeCodex.mjs +245 -205
- package/bin/lib/runtimeDetection.mjs +2 -2
- package/bin/lib/runtimeEvents.mjs +16 -10
- package/bin/lib/runtimeLimits.mjs +5 -6
- package/bin/lib/runtimeOfferings.mjs +24 -11
- package/bin/lib/runtimes.mjs +10 -0
- package/bin/lib/shipSweep.mjs +5 -3
- package/bin/lib/shot.mjs +91 -17
- package/bin/lib/sleep.mjs +11 -0
- package/bin/lib/startProject.mjs +5 -5
- package/bin/lib/statePaths.mjs +40 -0
- package/bin/lib/tty.mjs +25 -15
- package/bin/lib/turnLock.mjs +7 -14
- package/bin/lib/turnProfile.mjs +1 -1
- package/bin/lib/ui.mjs +0 -3
- package/bin/lib/uninstall.mjs +30 -26
- package/bin/lib/update.mjs +79 -27
- package/bin/lib/uplinkScrub.mjs +296 -31
- package/bin/lib/vault.mjs +78 -10
- package/bin/lib/vaultDiff.mjs +5 -2
- package/bin/lib/views.mjs +37 -12
- package/bin/lib/wikiRunner.mjs +40 -61
- package/bin/lib/work.mjs +17 -7
- package/bin/lib/workAdoptCarry.mjs +2 -4
- package/bin/lib/workAgentCheck.mjs +104 -57
- package/bin/lib/workAgentMerges.mjs +62 -57
- package/bin/lib/workAgentPlans.mjs +99 -145
- package/bin/lib/workAgentPrecheck.mjs +71 -138
- package/bin/lib/workAgentPublish.mjs +6 -8
- package/bin/lib/workAgentTurnBegun.mjs +15 -11
- package/bin/lib/workAgentTurnExecution.mjs +46 -53
- package/bin/lib/workAgentTurnReports.mjs +9 -37
- package/bin/lib/workAgentTurns.mjs +4 -3
- package/bin/lib/workAttachments.mjs +66 -11
- package/bin/lib/workBrain.mjs +9 -4
- package/bin/lib/workCappedTurn.mjs +109 -0
- package/bin/lib/workChildren.mjs +5 -5
- package/bin/lib/workCommandAudit.mjs +11 -2
- package/bin/lib/workDiffs.mjs +52 -6
- package/bin/lib/workIntake.mjs +35 -60
- package/bin/lib/workPlaceReports.mjs +7 -4
- package/bin/lib/workPlaces.mjs +96 -30
- package/bin/lib/workPreviews.mjs +47 -112
- package/bin/lib/workProcesses.mjs +8 -35
- package/bin/lib/workPullRequests.mjs +37 -56
- package/bin/lib/workReportQueue.mjs +100 -148
- package/bin/lib/workRetire.mjs +2 -1
- package/bin/lib/workSessionRuntime.mjs +38 -37
- package/bin/lib/workSessionTurns.mjs +111 -189
- package/bin/lib/workShip.mjs +43 -23
- package/bin/lib/workTurnShape.mjs +163 -0
- package/npm-shrinkwrap.json +59 -0
- package/package.json +3 -2
- package/bin/lib/fleetJobs.mjs +0 -120
- package/bin/lib/patchRevert.mjs +0 -60
|
@@ -8,16 +8,17 @@
|
|
|
8
8
|
* the process-group kill), the pre-review for MODEL-REPORTING ones. The review
|
|
9
9
|
* entry beat sequences the two and owns neither.
|
|
10
10
|
*/
|
|
11
|
-
import { readFileSync,
|
|
12
|
-
import { spawn } from 'node:child_process';
|
|
13
|
-
import { join } from 'node:path';
|
|
11
|
+
import { readFileSync, realpathSync, rmSync } from 'node:fs';
|
|
12
|
+
import { spawn as spawnProcess } from 'node:child_process';
|
|
13
|
+
import { dirname, join } from 'node:path';
|
|
14
14
|
import { FLEET_URL } from './config.mjs';
|
|
15
|
-
import {
|
|
15
|
+
import { gitOrNull } from './git.mjs';
|
|
16
16
|
import { warn } from './ui.mjs';
|
|
17
|
-
import { ARTIFACT_DIR, artifactTypeFor } from './artifacts.mjs';
|
|
17
|
+
import { ARTIFACT_DIR, artifactTypeFor, walkArtifactDir } from './artifacts.mjs';
|
|
18
|
+
import { flowviantParentState, realDirState } from './ownedDirs.mjs';
|
|
19
|
+
import { killGroup } from './processGroup.mjs';
|
|
18
20
|
import { scrub as envScrub } from './uplinkScrub.mjs';
|
|
19
|
-
import {
|
|
20
|
-
import { agentBrowserHome, openBrowserHome, withoutWindows } from './noWindowEnv.mjs';
|
|
21
|
+
import { agentBrowserHome, openBrowserHome, unattendedEnv } from './noWindowEnv.mjs';
|
|
21
22
|
import { fleetEndpoint } from './fleetWire.mjs';
|
|
22
23
|
import { workLanesStopped } from './standDownGate.mjs';
|
|
23
24
|
|
|
@@ -35,54 +36,75 @@ import { workLanesStopped } from './standDownGate.mjs';
|
|
|
35
36
|
* before any person looked — and the directory is git-excluded, so the file
|
|
36
37
|
* never appears in the diff a reviewer reads.
|
|
37
38
|
*
|
|
38
|
-
* What an artifact IS is already a closed list (`ARTIFACT_TYPES
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
39
|
+
* What an artifact IS is already a closed list (`ARTIFACT_TYPES`, which holds
|
|
40
|
+
* no script type), and WHERE one may stand is the scan's own walk
|
|
41
|
+
* (`walkArtifactDir`, artifacts.mjs — one walk since 2026-10-10, the audit's
|
|
42
|
+
* F067). This header used to say the scan showed only TOP-LEVEL files, and
|
|
43
|
+
* the fence removed every subdirectory whole: a 3D-model page
|
|
44
|
+
* (`<name>/index.html`) or a bundle's sidecars went the moment the queue
|
|
45
|
+
* emptied, and a send-back turn that did not rewrite the page settled
|
|
46
|
+
* "nothing". So real directories within the scan's depth stay and are walked;
|
|
47
|
+
* at ANY depth a dot-name, a symlink, anything that is not a regular file, a
|
|
48
|
+
* file off the allowlist, and a directory deeper than the scan looks is
|
|
49
|
+
* removed — nothing the product will show, so removing it costs nothing a
|
|
50
|
+
* person could see. A `.flowviant` that is not a real directory is left alone
|
|
51
|
+
* (that is the repository's own content, not something a turn wrote). Returns
|
|
52
|
+
* the relative paths removed, for the log line.
|
|
45
53
|
*/
|
|
46
54
|
export function clearNonArtifacts(wt) {
|
|
47
55
|
const removed = [];
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
56
|
+
if (flowviantParentState(wt) !== 'dir') return removed;
|
|
57
|
+
const dir = join(wt, ARTIFACT_DIR);
|
|
58
|
+
const state = realDirState(dir);
|
|
59
|
+
if (state === 'none') return removed; // the ordinary case
|
|
60
|
+
if (state === 'other') {
|
|
61
|
+
// A link or a file AT the directory's name: the link goes, never its target.
|
|
62
|
+
try {
|
|
54
63
|
rmSync(dir, { force: true, recursive: false });
|
|
55
64
|
removed.push(ARTIFACT_DIR);
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
for (const name of readdirSync(dir)) {
|
|
59
|
-
const p = join(dir, name);
|
|
60
|
-
let e;
|
|
61
|
-
try {
|
|
62
|
-
e = lstatSync(p);
|
|
63
|
-
} catch {
|
|
64
|
-
continue; // vanished between the list and the stat
|
|
65
|
-
}
|
|
66
|
-
const keep = e.isFile() && !name.startsWith('.') && artifactTypeFor(name) !== null;
|
|
67
|
-
if (keep) continue;
|
|
68
|
-
try {
|
|
69
|
-
rmSync(p, { recursive: true, force: true });
|
|
70
|
-
removed.push(name);
|
|
71
|
-
} catch {
|
|
72
|
-
/* best-effort — reported by omission */
|
|
73
|
-
}
|
|
65
|
+
} catch {
|
|
66
|
+
/* best-effort — reported by omission */
|
|
74
67
|
}
|
|
68
|
+
return removed;
|
|
69
|
+
}
|
|
70
|
+
// A REMOVAL STAYS INSIDE THE DIRECTORY. The walk descends only what `lstat`
|
|
71
|
+
// called a real directory, but this one deletes, so each entry's parent is
|
|
72
|
+
// resolved again right before it goes: a folder swapped for a link after
|
|
73
|
+
// its look is never a way to remove what the link points at.
|
|
74
|
+
let root;
|
|
75
|
+
try {
|
|
76
|
+
root = realpathSync(dir);
|
|
75
77
|
} catch {
|
|
76
|
-
|
|
78
|
+
return removed;
|
|
77
79
|
}
|
|
80
|
+
const inside = (name, path) => {
|
|
81
|
+
try {
|
|
82
|
+
return realpathSync(dirname(path)) === join(root, dirname(name));
|
|
83
|
+
} catch {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
walkArtifactDir(wt, ({ name, path, kind }) => {
|
|
88
|
+
if (kind === 'dir' || (kind === 'file' && artifactTypeFor(name) !== null)) return;
|
|
89
|
+
if (!inside(name, path)) return;
|
|
90
|
+
try {
|
|
91
|
+
// `rm -r` on a link removes the link; on a real directory, its contents
|
|
92
|
+
// (links inside it unlinked, never followed).
|
|
93
|
+
rmSync(path, { recursive: true, force: true });
|
|
94
|
+
removed.push(name);
|
|
95
|
+
} catch {
|
|
96
|
+
/* best-effort — reported by omission */
|
|
97
|
+
}
|
|
98
|
+
});
|
|
78
99
|
return removed;
|
|
79
100
|
}
|
|
80
101
|
|
|
81
102
|
/**
|
|
82
103
|
* THE ENVIRONMENT THE PROJECT'S CHECK RUNS UNDER: the daemon's own, minus the
|
|
83
104
|
* machine credential — the same rule `cliEnv` (runTurn.mjs) applies to a turn,
|
|
84
|
-
* imported from its one home (
|
|
85
|
-
* module, so the check never depends on the CLI module's spawn
|
|
105
|
+
* imported from its one home (`unattendedEnv`, noWindowEnv.mjs) rather than
|
|
106
|
+
* from the CLI module, so the check never depends on the CLI module's spawn
|
|
107
|
+
* helpers. Nothing a check runs needs the credential the daemon
|
|
86
108
|
* authenticates with; everything else is what the agent's own turn saw when it
|
|
87
109
|
* ran the same tests.
|
|
88
110
|
*/
|
|
@@ -90,10 +112,22 @@ export function checkEnv(env = process.env, browserHome = null) {
|
|
|
90
112
|
// …and, like that turn since 2026-09-29, no way to a window: a test run
|
|
91
113
|
// nobody is watching that opens a headed browser or an HTML report opens it
|
|
92
114
|
// on nobody's behalf (noWindowEnv.mjs). The agent's own browser profile.
|
|
93
|
-
return
|
|
115
|
+
return unattendedEnv(env, { browserHome });
|
|
94
116
|
}
|
|
95
117
|
|
|
96
|
-
|
|
118
|
+
/**
|
|
119
|
+
* `spawn` is node's unless a test hands one in: the release below is about a
|
|
120
|
+
* child whose 'close' never comes, which a real process only shows with a
|
|
121
|
+
* survivor outside its group holding the pipe.
|
|
122
|
+
*/
|
|
123
|
+
export function createWorkAgentCheck({
|
|
124
|
+
repoRoot,
|
|
125
|
+
postBestEffort,
|
|
126
|
+
workChildren,
|
|
127
|
+
groupKillChildren,
|
|
128
|
+
sessionMetaPath,
|
|
129
|
+
spawn = spawnProcess,
|
|
130
|
+
}) {
|
|
97
131
|
const AGENT_CHECK_DONE_URL = fleetEndpoint('agent-check-done', FLEET_URL);
|
|
98
132
|
|
|
99
133
|
// ── THE PROJECT'S OWN CHECK, and the MERGE ─────────────────────────────────
|
|
@@ -136,7 +170,12 @@ export function createWorkAgentCheck({ repoRoot, postBestEffort, workChildren, g
|
|
|
136
170
|
|
|
137
171
|
const runCheck = async (agentId, wt) => {
|
|
138
172
|
const cmd = readCheckCommand();
|
|
139
|
-
|
|
173
|
+
// OPTIONAL, like the pre-review's twin read: an unreadable head (a
|
|
174
|
+
// worktree retired mid-beat, an unborn branch) costs the answer its
|
|
175
|
+
// headSha, never the answer. This read was the one copy left unguarded,
|
|
176
|
+
// and its throw kept the row on its previous, older check label (audit
|
|
177
|
+
// 2026-10-10, F144).
|
|
178
|
+
const headSha = gitOrNull(['rev-parse', 'HEAD'], wt) ?? undefined;
|
|
140
179
|
if (!cmd) {
|
|
141
180
|
await postCheck({ agentId, status: 'none', ...(headSha ? { headSha } : {}) });
|
|
142
181
|
return;
|
|
@@ -161,6 +200,21 @@ export function createWorkAgentCheck({ repoRoot, postBestEffort, workChildren, g
|
|
|
161
200
|
// After the teardown no check starts; this promise stays pending.
|
|
162
201
|
if (workLanesStopped()) return;
|
|
163
202
|
let child;
|
|
203
|
+
/**
|
|
204
|
+
* ONE RELEASE, from every way the check ends (2026-10-10, the audit's
|
|
205
|
+
* F152). Only 'close' and 'error' used to untrack the child, and
|
|
206
|
+
* nothing ever pruned `groupKillChildren` — so a timed-out check whose
|
|
207
|
+
* pipe a survivor outside its group still held (a watcher, a dev server
|
|
208
|
+
* it daemonised) never closed, and stayed a live turn for good: the
|
|
209
|
+
* machine's admission count inflated and `workBusy` held off every
|
|
210
|
+
* self-update until a restart, while the set kept one dead child per
|
|
211
|
+
* run. Idempotent; the timer, 'error' and 'close' each call it.
|
|
212
|
+
*/
|
|
213
|
+
const release = () => {
|
|
214
|
+
if (!child) return;
|
|
215
|
+
workChildren.delete(child);
|
|
216
|
+
groupKillChildren.delete(child);
|
|
217
|
+
};
|
|
164
218
|
try {
|
|
165
219
|
// DETACHED, so the child's pid is its PROCESS GROUP. A check is almost
|
|
166
220
|
// always a shell that spawns the real runner, and signalling the shell
|
|
@@ -242,17 +296,10 @@ export function createWorkAgentCheck({ repoRoot, postBestEffort, workChildren, g
|
|
|
242
296
|
child.stdout?.on('data', keep);
|
|
243
297
|
child.stderr?.on('data', keep);
|
|
244
298
|
const timer = setTimeout(() => {
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
} catch {
|
|
250
|
-
try {
|
|
251
|
-
child.kill('SIGKILL');
|
|
252
|
-
} catch {
|
|
253
|
-
/* already gone */
|
|
254
|
-
}
|
|
255
|
-
}
|
|
299
|
+
// The GROUP, not the child: killing the shell leaves whatever it
|
|
300
|
+
// started running, which is the thing actually taking ten minutes.
|
|
301
|
+
killGroup(child, 'SIGKILL');
|
|
302
|
+
release();
|
|
256
303
|
text += '\n[flowviant] the check ran past ten minutes and was stopped';
|
|
257
304
|
// RESOLVE HERE TOO. Waiting for 'close' after a kill is the shape that
|
|
258
305
|
// hangs: if the group is already gone the event never arrives, and this
|
|
@@ -261,13 +308,13 @@ export function createWorkAgentCheck({ repoRoot, postBestEffort, workChildren, g
|
|
|
261
308
|
}, CHECK_TIMEOUT_MS);
|
|
262
309
|
child.on('error', (e) => {
|
|
263
310
|
clearTimeout(timer);
|
|
264
|
-
|
|
311
|
+
release();
|
|
265
312
|
text += String(e?.message || e);
|
|
266
313
|
finish('failed');
|
|
267
314
|
});
|
|
268
315
|
child.on('close', (code) => {
|
|
269
316
|
clearTimeout(timer);
|
|
270
|
-
|
|
317
|
+
release();
|
|
271
318
|
finish(code === 0 ? 'passed' : 'failed');
|
|
272
319
|
});
|
|
273
320
|
});
|
|
@@ -1,16 +1,33 @@
|
|
|
1
1
|
/** Agent merge claims, pull-request approval, and branch cleanup. */
|
|
2
|
-
import { ensureOpenPr,
|
|
2
|
+
import { ensureOpenPr, ghReady, mergeAndVerifyTip, PR_URL_RE, relayedFailure } from './prWorkflow.mjs';
|
|
3
3
|
import { existsSync } from 'node:fs';
|
|
4
4
|
import { execFileSync } from 'node:child_process';
|
|
5
5
|
import { join } from 'node:path';
|
|
6
|
-
import { FLEET_URL
|
|
7
|
-
import { git, gitNet as gitNetIn, baseBranchName, isSafePathSegment } from './git.mjs';
|
|
6
|
+
import { FLEET_URL } from './config.mjs';
|
|
7
|
+
import { git, gitOrNull, gitNet as gitNetIn, baseBranchName, isSafePathSegment, sessionBranch, stripUrlUserinfo } from './git.mjs';
|
|
8
8
|
import { isPublishRef, publishPushArgs, publishDeleteArgs, publishErrorText } from './agentPublish.mjs';
|
|
9
9
|
import { mergeOutward as shipMergeOutward } from './shipMerge.mjs';
|
|
10
10
|
import { gitMergeIn } from './gitMergeIdentity.mjs';
|
|
11
11
|
import { warn } from './ui.mjs';
|
|
12
12
|
import { scrub as envScrub } from './uplinkScrub.mjs';
|
|
13
13
|
import { fleetEndpoint } from './fleetWire.mjs';
|
|
14
|
+
import { claimFleetLease, settleLease } from './fleetPost.mjs';
|
|
15
|
+
import { worktreePath } from './workPlaces.mjs';
|
|
16
|
+
|
|
17
|
+
/** Git puts merge conflicts on stdout, while refusals usually use stderr.
|
|
18
|
+
* Keep both before the command wrapper, scrub before bounding, and put Git's
|
|
19
|
+
* reason first so the server's shorter mergeError cap keeps the useful part.
|
|
20
|
+
* A genuine conflict stays in the worktree for the merge_resolve turn.
|
|
21
|
+
* Userinfo is stripped too (2026-10-10, F153): the merge's own push to
|
|
22
|
+
* `origin` can print the remote URL, token and all, into a team-visible
|
|
23
|
+
* mergeError. */
|
|
24
|
+
export function agentMergeFailureDetail(error) {
|
|
25
|
+
const output = [error?.stdout, error?.stderr]
|
|
26
|
+
.map((value) => String(value ?? '').trim())
|
|
27
|
+
.filter(Boolean)
|
|
28
|
+
.join('\n');
|
|
29
|
+
return envScrub(stripUrlUserinfo(output || String(error?.message || error))).slice(0, 2000);
|
|
30
|
+
}
|
|
14
31
|
|
|
15
32
|
export function createWorkAgentMerges({
|
|
16
33
|
repoRoot,
|
|
@@ -39,42 +56,12 @@ export function createWorkAgentMerges({
|
|
|
39
56
|
// landed observer's own fetch sees it, and the cards close there.
|
|
40
57
|
const agentMerges = new Set(); // agent ids in flight on this tick
|
|
41
58
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const res = await fetch(AGENT_MERGE_CLAIM_URL, {
|
|
45
|
-
method: 'POST',
|
|
46
|
-
headers: {
|
|
47
|
-
Authorization: `Bearer ${FLEET_TOKEN}`,
|
|
48
|
-
'User-Agent': USER_AGENT,
|
|
49
|
-
'Content-Type': 'application/json',
|
|
50
|
-
},
|
|
51
|
-
signal: AbortSignal.timeout(15_000),
|
|
52
|
-
body: JSON.stringify({ agentId, instance: DAEMON_INSTANCE }),
|
|
53
|
-
});
|
|
54
|
-
const j = await res.json().catch(() => null);
|
|
55
|
-
return j?.data?.claimed === true;
|
|
56
|
-
} catch {
|
|
57
|
-
return false; // the peer may hold it; doing nothing is the safe answer
|
|
58
|
-
}
|
|
59
|
-
};
|
|
59
|
+
// The peer may hold it; doing nothing is the safe answer.
|
|
60
|
+
const claimAgentMerge = (agentId) => claimFleetLease(AGENT_MERGE_CLAIM_URL, { agentId });
|
|
60
61
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
method: 'POST',
|
|
65
|
-
headers: {
|
|
66
|
-
Authorization: `Bearer ${FLEET_TOKEN}`,
|
|
67
|
-
'User-Agent': USER_AGENT,
|
|
68
|
-
'Content-Type': 'application/json',
|
|
69
|
-
},
|
|
70
|
-
signal: AbortSignal.timeout(30_000),
|
|
71
|
-
body: JSON.stringify({ ...body, instance: DAEMON_INSTANCE }),
|
|
72
|
-
});
|
|
73
|
-
} catch {
|
|
74
|
-
/* the lease lapses and the job is re-offered — a merge is idempotent
|
|
75
|
-
against an already-merged branch, which mergeOutward detects */
|
|
76
|
-
}
|
|
77
|
-
};
|
|
62
|
+
// On a failure the lease lapses and the job is re-offered — a merge is
|
|
63
|
+
// idempotent against an already-merged branch, which mergeOutward detects.
|
|
64
|
+
const postAgentMerge = (body) => settleLease(AGENT_MERGE_DONE_URL, body, { timeoutMs: 30_000 });
|
|
78
65
|
|
|
79
66
|
// A merge COMMIT needs an identity the machine may not have; the probe and
|
|
80
67
|
// the daemon's fallback live in gitMergeIdentity.mjs.
|
|
@@ -132,7 +119,7 @@ export function createWorkAgentMerges({
|
|
|
132
119
|
// does not know exists.
|
|
133
120
|
try {
|
|
134
121
|
await inPlace(place, true, async () => {
|
|
135
|
-
const wt =
|
|
122
|
+
const wt = worktreePath(baseDir, place);
|
|
136
123
|
if (!existsSync(wt)) {
|
|
137
124
|
await report({ agentId, ok: false, detail: 'the worktree is gone' });
|
|
138
125
|
return;
|
|
@@ -154,7 +141,7 @@ export function createWorkAgentMerges({
|
|
|
154
141
|
await report({
|
|
155
142
|
agentId,
|
|
156
143
|
ok: false,
|
|
157
|
-
detail:
|
|
144
|
+
detail: agentMergeFailureDetail(e),
|
|
158
145
|
});
|
|
159
146
|
return;
|
|
160
147
|
}
|
|
@@ -168,21 +155,19 @@ export function createWorkAgentMerges({
|
|
|
168
155
|
// `git()` THROWS on a non-zero exit, and `symbolic-ref` exits non-zero on
|
|
169
156
|
// a detached HEAD — so the guard below was unreachable and the throw
|
|
170
157
|
// escaped the claimed merge, leaving it unsettled until its lease lapsed.
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
branch = (git(['symbolic-ref', '--quiet', '--short', 'HEAD'], wt) || '').trim();
|
|
174
|
-
} catch {
|
|
175
|
-
branch = '';
|
|
176
|
-
}
|
|
158
|
+
// `gitOrNull` reads that as no branch.
|
|
159
|
+
const branch = gitOrNull(['symbolic-ref', '--quiet', '--short', 'HEAD'], wt) ?? '';
|
|
177
160
|
if (!branch) {
|
|
178
161
|
// A detached HEAD names no branch, so there is nothing to merge and
|
|
179
162
|
// nothing to record. An ambiguity in git, not a rule of ours.
|
|
180
163
|
await report({ agentId, ok: false, detail: 'this worktree is on a detached HEAD' });
|
|
181
164
|
return;
|
|
182
165
|
}
|
|
183
|
-
const tip = (
|
|
184
|
-
|
|
185
|
-
|
|
166
|
+
const tip = gitOrNull(['rev-parse', 'HEAD'], wt) ?? '';
|
|
167
|
+
// The count stays a THROWING read, inside the merge's own catch: a
|
|
168
|
+
// range git cannot answer must never read as 0, which is "already on
|
|
169
|
+
// base" and reports success.
|
|
170
|
+
const count = Number(git(['rev-list', '--count', `${baseRef()}..HEAD`], wt)) || 0;
|
|
186
171
|
if (count === 0) {
|
|
187
172
|
// Already on base — an idempotent re-offer, or an agent that changed
|
|
188
173
|
// nothing. Reported as a SUCCESS: the branch's work is on base, which
|
|
@@ -249,7 +234,7 @@ export function createWorkAgentMerges({
|
|
|
249
234
|
* work on that ref that nobody approved; the plain push of the checked
|
|
250
235
|
* out branch is the honest fallback.
|
|
251
236
|
*/
|
|
252
|
-
const ownBranch = branch ===
|
|
237
|
+
const ownBranch = branch === sessionBranch(place);
|
|
253
238
|
const head =
|
|
254
239
|
ownBranch && isPublishRef(job.publishedRef) ? job.publishedRef : branch;
|
|
255
240
|
try {
|
|
@@ -268,14 +253,15 @@ export function createWorkAgentMerges({
|
|
|
268
253
|
agentRemoteAt.set(place, { ref: head, sha: tip });
|
|
269
254
|
}
|
|
270
255
|
} catch (e) {
|
|
271
|
-
// SCRUBBED
|
|
272
|
-
//
|
|
273
|
-
//
|
|
274
|
-
//
|
|
275
|
-
//
|
|
256
|
+
// SCRUBBED AND USERINFO-STRIPPED (`relayedFailure`). A push writes
|
|
257
|
+
// the REMOTE URL to stderr and a remote can carry a token in its
|
|
258
|
+
// userinfo, and this line becomes a stored, team-visible
|
|
259
|
+
// `mergeError`. The env scrub alone left `//user:token@` standing
|
|
260
|
+
// (audit 2026-10-10, F153); gh's own refusals below take the same
|
|
261
|
+
// relay inside prWorkflow.mjs.
|
|
276
262
|
// A refused push is the remote or its login (no `origin`, a
|
|
277
263
|
// credential, a protected ref) — the person's to fix, not the branch.
|
|
278
|
-
await report({ agentId, ok: false, fix: 'person', detail:
|
|
264
|
+
await report({ agentId, ok: false, fix: 'person', detail: relayedFailure(e) });
|
|
279
265
|
return;
|
|
280
266
|
}
|
|
281
267
|
const pr = ensureOpenPr(head, prBase, { cwd: repoRoot });
|
|
@@ -309,6 +295,19 @@ export function createWorkAgentMerges({
|
|
|
309
295
|
});
|
|
310
296
|
return;
|
|
311
297
|
}
|
|
298
|
+
// GitHub lists no open pull request FROM this head in this
|
|
299
|
+
// repository, so nothing was merged — never one picked by a selector
|
|
300
|
+
// that merely looks like the branch (prWorkflow.mjs).
|
|
301
|
+
if (merged.noPr) {
|
|
302
|
+
await report({
|
|
303
|
+
agentId,
|
|
304
|
+
ok: false,
|
|
305
|
+
detail:
|
|
306
|
+
`there is no open pull request from ${head} in this repository to merge` +
|
|
307
|
+
(linked ? ` — the one opened is at ${prUrl}` : ' — approve again to open one'),
|
|
308
|
+
});
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
312
311
|
if (merged.notOnBase) {
|
|
313
312
|
// gh said yes and base does not have the tip: a merge queue may
|
|
314
313
|
// still be running it, or an older PR merged. The server closes
|
|
@@ -354,7 +353,7 @@ export function createWorkAgentMerges({
|
|
|
354
353
|
// resolve turn on the server. A conflict says nothing, and the
|
|
355
354
|
// agent resolves it in its worktree as before.
|
|
356
355
|
...(e?.fix === 'person' ? { fix: 'person' } : {}),
|
|
357
|
-
detail:
|
|
356
|
+
detail: agentMergeFailureDetail(e),
|
|
358
357
|
});
|
|
359
358
|
return;
|
|
360
359
|
}
|
|
@@ -362,6 +361,12 @@ export function createWorkAgentMerges({
|
|
|
362
361
|
onRepoChanged();
|
|
363
362
|
landed.observe();
|
|
364
363
|
});
|
|
364
|
+
} catch (e) {
|
|
365
|
+
// Unexpected Git failures must retain their diagnostics too. The belt
|
|
366
|
+
// below remains for paths which exit without an error or a report.
|
|
367
|
+
if (!reported) {
|
|
368
|
+
await report({ agentId, ok: false, detail: agentMergeFailureDetail(e) });
|
|
369
|
+
}
|
|
365
370
|
} finally {
|
|
366
371
|
if (!reported) {
|
|
367
372
|
// Belt over braces. A merge this daemon claimed and cannot account for
|