flowviant 0.103.0 → 0.104.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/bin/cli.mjs +87 -474
- package/bin/lib/admission.mjs +19 -3
- package/bin/lib/agentPlan.mjs +2 -2
- package/bin/lib/agentTaskKinds.mjs +93 -0
- package/bin/lib/agySessions.mjs +158 -0
- package/bin/lib/artifactPolicy.mjs +44 -0
- package/bin/lib/artifactPreview.mjs +52 -0
- package/bin/lib/artifactReporter.mjs +163 -0
- package/bin/lib/artifacts.mjs +47 -227
- package/bin/lib/authReject.mjs +16 -4
- package/bin/lib/authproxy.mjs +47 -304
- package/bin/lib/boxIdentity.mjs +245 -0
- package/bin/lib/childEnv.mjs +11 -9
- package/bin/lib/claude.mjs +14 -851
- package/bin/lib/claudeAuth.mjs +3 -3
- package/bin/lib/claudePosture.mjs +367 -0
- package/bin/lib/claudeSessions.mjs +411 -0
- package/bin/lib/claudeStream.mjs +207 -0
- package/bin/lib/cliLimit.mjs +34 -0
- package/bin/lib/commandSpecs.mjs +173 -0
- package/bin/lib/config.mjs +11 -42
- package/bin/lib/credentialDate.mjs +22 -0
- package/bin/lib/credentials.mjs +21 -26
- package/bin/lib/daemonLogging.mjs +55 -0
- package/bin/lib/daemonState.mjs +64 -0
- package/bin/lib/deploy.mjs +148 -647
- package/bin/lib/deployCheckout.mjs +265 -0
- package/bin/lib/deployConfig.mjs +142 -0
- package/bin/lib/deployRunner.mjs +311 -0
- package/bin/lib/deployWire.mjs +41 -0
- package/bin/lib/desktopContract.mjs +56 -105
- package/bin/lib/env.mjs +49 -484
- package/bin/lib/fleet.mjs +183 -2383
- package/bin/lib/fleetJobs.mjs +120 -0
- package/bin/lib/fleetPost.mjs +40 -0
- package/bin/lib/fleetReports.mjs +392 -0
- package/bin/lib/fleetRoster.mjs +470 -0
- package/bin/lib/fleetStartup.mjs +196 -0
- package/bin/lib/fleetWake.mjs +45 -0
- package/bin/lib/fleetWire.mjs +46 -0
- package/bin/lib/git.mjs +2 -2
- package/bin/lib/gitMergeIdentity.mjs +51 -0
- package/bin/lib/grant.mjs +5 -3
- package/bin/lib/help.mjs +3 -125
- package/bin/lib/holder.mjs +482 -0
- package/bin/lib/instance.mjs +124 -46
- package/bin/lib/knowledge.mjs +16 -799
- package/bin/lib/knowledgeFetch.mjs +38 -0
- package/bin/lib/knowledgeFileSync.mjs +112 -0
- package/bin/lib/knowledgeFiles.mjs +487 -0
- package/bin/lib/knowledgeLibrary.mjs +249 -0
- package/bin/lib/landed.mjs +2 -1
- package/bin/lib/listeners.mjs +99 -141
- package/bin/lib/localSessions.mjs +30 -555
- package/bin/lib/login.mjs +3 -2
- package/bin/lib/loginPath.mjs +3 -5
- package/bin/lib/machineDisconnect.mjs +203 -0
- package/bin/lib/machineEnv.mjs +22 -0
- package/bin/lib/machineLimits.mjs +61 -0
- package/bin/lib/machineListing.mjs +338 -0
- package/bin/lib/machines.mjs +78 -455
- package/bin/lib/machinesCommand.mjs +214 -0
- package/bin/lib/mcp-cli.mjs +2 -1
- package/bin/lib/patchRevert.mjs +60 -0
- package/bin/lib/prWorkflow.mjs +157 -0
- package/bin/lib/preflight.mjs +2 -1
- package/bin/lib/pressurePolicy.mjs +104 -0
- package/bin/lib/preview.mjs +2 -272
- package/bin/lib/previewFramePolicy.mjs +112 -0
- package/bin/lib/previewGatePolicy.mjs +236 -0
- package/bin/lib/previewRegistry.mjs +235 -0
- package/bin/lib/printable.mjs +2 -2
- package/bin/lib/procRegistry.mjs +28 -18
- package/bin/lib/procScan.mjs +146 -0
- package/bin/lib/processes.mjs +29 -37
- package/bin/lib/projectToolRuntimes.mjs +138 -0
- package/bin/lib/projectTools.mjs +13 -88
- package/bin/lib/prompts.mjs +28 -42
- package/bin/lib/repoPath.mjs +26 -0
- package/bin/lib/resources.mjs +7 -69
- package/bin/lib/resumeLost.mjs +107 -0
- package/bin/lib/runTurn.mjs +324 -0
- package/bin/lib/runtimeAntigravity.mjs +230 -0
- package/bin/lib/runtimeCapabilities.mjs +334 -0
- package/bin/lib/runtimeClaude.mjs +126 -0
- package/bin/lib/runtimeCodex.mjs +217 -0
- package/bin/lib/runtimeDetection.mjs +121 -0
- package/bin/lib/runtimeEvents.mjs +482 -0
- package/bin/lib/runtimeOfferings.mjs +4 -1
- package/bin/lib/runtimes.mjs +42 -1402
- package/bin/lib/safeFileName.mjs +74 -0
- package/bin/lib/standDownExit.mjs +181 -0
- package/bin/lib/standDownGate.mjs +48 -0
- package/bin/lib/startProject.mjs +279 -0
- package/bin/lib/trace.mjs +3 -3
- package/bin/lib/turnLock.mjs +68 -0
- package/bin/lib/turnProfile.mjs +77 -0
- package/bin/lib/uninstall.mjs +55 -45
- package/bin/lib/uninstallCopies.mjs +112 -0
- package/bin/lib/update.mjs +30 -17
- package/bin/lib/uplinkScrub.mjs +265 -0
- package/bin/lib/vault.mjs +22 -151
- package/bin/lib/vaultDiff.mjs +183 -0
- package/bin/lib/versionOrder.mjs +42 -0
- package/bin/lib/views.mjs +17 -17
- package/bin/lib/wikiRunner.mjs +630 -0
- package/bin/lib/work.mjs +188 -3034
- package/bin/lib/workAdoptCarry.mjs +94 -0
- package/bin/lib/workAgentCheck.mjs +278 -0
- package/bin/lib/workAgentMerges.mjs +54 -142
- package/bin/lib/workAgentPlans.mjs +14 -12
- package/bin/lib/workAgentPrecheck.mjs +393 -0
- package/bin/lib/workAgentPublish.mjs +177 -0
- package/bin/lib/workAgentReview.mjs +8 -666
- package/bin/lib/workAgentTurnBegun.mjs +109 -0
- package/bin/lib/workAgentTurnExecution.mjs +604 -0
- package/bin/lib/workAgentTurnOutcome.mjs +207 -0
- package/bin/lib/workAgentTurnReports.mjs +189 -0
- package/bin/lib/workAgentTurns.mjs +91 -898
- package/bin/lib/workAttachments.mjs +93 -0
- package/bin/lib/workBrain.mjs +53 -0
- package/bin/lib/workChildren.mjs +110 -0
- package/bin/lib/workCommandAudit.mjs +62 -0
- package/bin/lib/workDiffs.mjs +2 -1
- package/bin/lib/workNarration.mjs +152 -0
- package/bin/lib/workPlaceReports.mjs +426 -0
- package/bin/lib/workPlaces.mjs +228 -0
- package/bin/lib/workPreviews.mjs +31 -9
- package/bin/lib/workProcesses.mjs +4 -5
- package/bin/lib/workPullRequests.mjs +52 -126
- package/bin/lib/workReportQueue.mjs +254 -0
- package/bin/lib/workRetire.mjs +185 -0
- package/bin/lib/workSessionGroups.mjs +166 -0
- package/bin/lib/workSessionRuntime.mjs +164 -0
- package/bin/lib/workSessionTokens.mjs +68 -0
- package/bin/lib/workSessionTurns.mjs +892 -0
- package/bin/lib/workShip.mjs +9 -29
- package/bin/lib/workToolLog.mjs +76 -0
- package/package.json +3 -2
- package/bin/lib/patch.mjs +0 -363
|
@@ -5,26 +5,28 @@ import { FLEET_URL, FLEET_TOKEN, USER_AGENT, DAEMON_INSTANCE } from './config.mj
|
|
|
5
5
|
import { git, isSafePathSegment } from './git.mjs';
|
|
6
6
|
import { parseProposal } from './agentPlan.mjs';
|
|
7
7
|
import { c, note } from './ui.mjs';
|
|
8
|
-
import { runTurn } from './
|
|
8
|
+
import { runTurn } from './runTurn.mjs';
|
|
9
9
|
import { SYSTEM_PLAN, AGENT_PLAN_KICKOFF } from './prompts.mjs';
|
|
10
|
-
import { scrub as envScrub } from './
|
|
11
|
-
import {
|
|
10
|
+
import { scrub as envScrub } from './uplinkScrub.mjs';
|
|
11
|
+
import { RUNTIMES } from './runtimes.mjs';
|
|
12
|
+
import { pickRuntimeFor } from './runtimeDetection.mjs';
|
|
13
|
+
import { removeProbeTranscript } from './runtimeCapabilities.mjs';
|
|
14
|
+
import { fleetEndpoint } from './fleetWire.mjs';
|
|
15
|
+
import { REPO_PLACE } from './workPlaces.mjs';
|
|
12
16
|
|
|
13
17
|
export function createWorkAgentPlans({
|
|
14
18
|
postBestEffort,
|
|
15
19
|
baseDir,
|
|
16
20
|
baseRef,
|
|
17
|
-
REPO_PLACE,
|
|
18
21
|
inPlace,
|
|
19
22
|
repoRoot,
|
|
20
23
|
admit,
|
|
21
|
-
|
|
24
|
+
placeLocks,
|
|
25
|
+
workChildren,
|
|
22
26
|
}) {
|
|
23
|
-
const AGENT_PLAN_DONE_URL =
|
|
24
|
-
const AGENT_PLAN_ACTIVITY_URL =
|
|
25
|
-
const AGENT_PLAN_CLAIM_URL =
|
|
26
|
-
|
|
27
|
-
const { placeLocks, workChildren } = state;
|
|
27
|
+
const AGENT_PLAN_DONE_URL = fleetEndpoint('agent-plan-done', FLEET_URL);
|
|
28
|
+
const AGENT_PLAN_ACTIVITY_URL = fleetEndpoint('agent-plan-activity', FLEET_URL);
|
|
29
|
+
const AGENT_PLAN_CLAIM_URL = fleetEndpoint('agent-plan-claim', FLEET_URL);
|
|
28
30
|
|
|
29
31
|
// ── AGENT PLAN JOBS: the Deploy press ──────────────────────────────────────
|
|
30
32
|
//
|
|
@@ -33,7 +35,7 @@ export function createWorkAgentPlans({
|
|
|
33
35
|
// proposes on the board, and ACCEPTING is what spawns anything. Nothing here
|
|
34
36
|
// creates a worktree, a branch or a card.
|
|
35
37
|
//
|
|
36
|
-
// READ-ONLY IN THE CHECKOUT. `
|
|
38
|
+
// READ-ONLY IN THE CHECKOUT. `profile: 'consult'` selects `consultPermFor` (the
|
|
37
39
|
// fenced Read/Glob rules plus a few path-validated `git`/`ls`/`cat` reads —
|
|
38
40
|
// no Write, no Edit, no mkdir, no rm) and NO MCP is passed at all, so this
|
|
39
41
|
// turn has no control plane to reach even if the repository it reads tries
|
|
@@ -323,7 +325,7 @@ export function createWorkAgentPlans({
|
|
|
323
325
|
system: SYSTEM_PLAN,
|
|
324
326
|
// READ-ONLY, and no MCP: `mcpArgs` is omitted entirely rather than
|
|
325
327
|
// passed empty, so there is no control plane on this turn at all.
|
|
326
|
-
|
|
328
|
+
profile: 'consult',
|
|
327
329
|
cwd: repoRoot,
|
|
328
330
|
runtime: rt,
|
|
329
331
|
streamJson: true,
|
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE AI PRE-REVIEW — a fresh, read-only reader of the agent's branch, run on
|
|
3
|
+
* the review-entry beat after the check, whose reading is posted as a label.
|
|
4
|
+
*
|
|
5
|
+
* Split out of workAgentReview.mjs (SOLID F039): this half changes for
|
|
6
|
+
* MODEL-REPORTING reasons (which CLI reads, what it is shown, how its answer
|
|
7
|
+
* is parsed, scrubbed and re-sent), the project check for SECURITY ones. The
|
|
8
|
+
* review entry beat sequences the two and owns neither.
|
|
9
|
+
*/
|
|
10
|
+
import { FLEET_URL, FLEET_TOKEN, USER_AGENT } from './config.mjs';
|
|
11
|
+
import { git } from './git.mjs';
|
|
12
|
+
import { parsePrecheck } from './agentPlan.mjs';
|
|
13
|
+
import { readStash } from './agentCards.mjs';
|
|
14
|
+
import { c, note } from './ui.mjs';
|
|
15
|
+
import { runTurn } from './runTurn.mjs';
|
|
16
|
+
import { SYSTEM_PRECHECK, AGENT_PRECHECK_KICKOFF } from './prompts.mjs';
|
|
17
|
+
import { scrub as envScrub } from './uplinkScrub.mjs';
|
|
18
|
+
import { pickRuntimeFor } from './runtimeDetection.mjs';
|
|
19
|
+
import { removeProbeTranscript } from './runtimeCapabilities.mjs';
|
|
20
|
+
import { taskIdsFromMessage } from './worktreeDiff.mjs';
|
|
21
|
+
import { limitLine } from './cliLimit.mjs';
|
|
22
|
+
import { fleetEndpoint } from './fleetWire.mjs';
|
|
23
|
+
|
|
24
|
+
export function createWorkAgentPrecheck({ baseRef, admit, sessionMetaPath, workChildren }) {
|
|
25
|
+
const AGENT_PRECHECK_URL = fleetEndpoint('agent-precheck', FLEET_URL);
|
|
26
|
+
|
|
27
|
+
// ── THE AI PRE-REVIEW ──────────────────────────────────────────────────────
|
|
28
|
+
//
|
|
29
|
+
// A FRESH Claude reads the branch before the human does. The owner asked for
|
|
30
|
+
// it in these words: "before having the user manually check, can we have the
|
|
31
|
+
// daemon … spawn an agent to review the work so basically we get an ai to look
|
|
32
|
+
// at the review before a human looks at it for a double check."
|
|
33
|
+
//
|
|
34
|
+
// IT IS NOT THE AGENT CHECKING ITSELF. `runTurn` is called with no `resume`,
|
|
35
|
+
// so there is no conversation to inherit: an agent that spent four turns
|
|
36
|
+
// arguing itself into a design defends that design, and asked whether its work
|
|
37
|
+
// meets the card it answers from the very context that produced the work. The
|
|
38
|
+
// reviewer stands IN the agent's worktree because it needs the code and the
|
|
39
|
+
// diff, under the `consult` profile (`consultPermFor` — the fenced Read/Glob rules plus
|
|
40
|
+
// a few path-validated git reads) with NO MCP passed at all, so there is no
|
|
41
|
+
// control plane on this turn even if the repository it reads tries to steer
|
|
42
|
+
// it.
|
|
43
|
+
//
|
|
44
|
+
// IT LABELS AND NEVER BLOCKS — the check's own law (workAgentCheck.mjs). Approve,
|
|
45
|
+
// the per-card verdicts and the ship quiz do not know this exists. Every exit
|
|
46
|
+
// below POSTS NOTHING, and the server renders an absent precheck as nothing:
|
|
47
|
+
// a failed, timed-out, skipped or unparseable read leaves the human's review
|
|
48
|
+
// exactly as it was before this feature existed. Ignorance never withholds.
|
|
49
|
+
//
|
|
50
|
+
// NO VERSION FLOOR. This is a daemon→server report on a NEW endpoint, so an
|
|
51
|
+
// older daemon simply never posts, and an older SERVER 404s — which `postPre`
|
|
52
|
+
// treats as delivered-and-done for the agent-trace reason stated there.
|
|
53
|
+
//
|
|
54
|
+
// IT RIDES THE BEAT THE CHECK ALREADY OWNS (`runReviewEntry`), AFTER it: the
|
|
55
|
+
// check is a local command and this is a model call, so the cheap answer lands
|
|
56
|
+
// on the row first and a wedged reviewer cannot delay it.
|
|
57
|
+
/**
|
|
58
|
+
* FIVE MINUTES, and `runTurn` has no timer of its own.
|
|
59
|
+
*
|
|
60
|
+
* Half the planner's cap, because this turn is strictly smaller — it reads one
|
|
61
|
+
* branch's diff and answers, where a planner reads a repository to decide
|
|
62
|
+
* whether a batch of work collides. And it is HELD INSIDE THE PLACE WRITER
|
|
63
|
+
* LOCK by the beat it rides, so every minute here is a minute the agent's next
|
|
64
|
+
* turn (or its merge) is waiting: a generous cap on a label would be spending
|
|
65
|
+
* the work's time on a note about the work.
|
|
66
|
+
*/
|
|
67
|
+
const PRECHECK_TIMEOUT_MS = 5 * 60_000;
|
|
68
|
+
/** Commit subjects handed to the reviewer. A bound on the prompt, not on the
|
|
69
|
+
* branch — the reviewer reads the diff itself, and the log is context. */
|
|
70
|
+
const PRECHECK_LOG_LINES = 80;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* ONE PRE-REVIEW, POSTED.
|
|
74
|
+
*
|
|
75
|
+
* Resolves TRUE for a permanent refusal as well as a success, and that is
|
|
76
|
+
* deliberate — the `postAgentTrace` rule, for the same reason: a server with
|
|
77
|
+
* no such route 404s this body and will 404 every retry of it, so re-sending
|
|
78
|
+
* would be a wedge wearing a retry's clothes. A NETWORK error resolves false
|
|
79
|
+
* and is retried ONCE, because unlike a trace batch this body cost a whole
|
|
80
|
+
* model call and losing it to a blip means the operator paid for a label
|
|
81
|
+
* nobody ever sees.
|
|
82
|
+
*
|
|
83
|
+
* THE RETRY IS NOT A GUARANTEE THAT THE FIRST ATTEMPT FAILED, and since the
|
|
84
|
+
* body started carrying `usage` (0.90.0) that matters. The `catch` below
|
|
85
|
+
* takes the 30-second `AbortSignal` with everything else, so a server that
|
|
86
|
+
* was merely SLOW — and committed — receives this identical body twice. The
|
|
87
|
+
* store was always idempotent; the token counts riding it were not, and a
|
|
88
|
+
* timed-out-but-committed post added them twice. The SERVER closes that: its
|
|
89
|
+
* write refuses a body whose text already sits on the row, so a re-send
|
|
90
|
+
* stores the same reading and charges nothing for it. Nothing here needs to
|
|
91
|
+
* change, and nothing here may start assuming a thrown fetch means the
|
|
92
|
+
* server never saw this.
|
|
93
|
+
*/
|
|
94
|
+
const postPre = async (body) => {
|
|
95
|
+
try {
|
|
96
|
+
const res = await fetch(AGENT_PRECHECK_URL, {
|
|
97
|
+
method: 'POST',
|
|
98
|
+
headers: {
|
|
99
|
+
Authorization: `Bearer ${FLEET_TOKEN}`,
|
|
100
|
+
'User-Agent': USER_AGENT,
|
|
101
|
+
'Content-Type': 'application/json',
|
|
102
|
+
},
|
|
103
|
+
signal: AbortSignal.timeout(30_000),
|
|
104
|
+
body: JSON.stringify(body),
|
|
105
|
+
});
|
|
106
|
+
return (
|
|
107
|
+
res.ok ||
|
|
108
|
+
(res.status >= 400 && res.status < 500 && res.status !== 408 && res.status !== 429)
|
|
109
|
+
);
|
|
110
|
+
} catch {
|
|
111
|
+
return false; // a blip — worth one more attempt at a model call's answer
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* THE BRANCH'S OWN COMMITS, subject + trailers, and the card ids they name.
|
|
117
|
+
*
|
|
118
|
+
* MEASURED, never asserted — the same reason `commitsBetween` exists. The
|
|
119
|
+
* trailer ids are what let the prompt say "N earlier cards' specs are not on
|
|
120
|
+
* this box" honestly: the difference between the cards this branch claims and
|
|
121
|
+
* the specs this disk holds is a fact, and a box that adopted the agent
|
|
122
|
+
* mid-run is exactly the case where it is non-zero.
|
|
123
|
+
*
|
|
124
|
+
* IT CANNOT THROW. `baseRef()` is free text an owner typed and may name no
|
|
125
|
+
* ref at all; an unreadable range costs the reviewer its context, never the
|
|
126
|
+
* review-entry beat it is standing in.
|
|
127
|
+
*/
|
|
128
|
+
const branchLog = (wt) => {
|
|
129
|
+
let out;
|
|
130
|
+
try {
|
|
131
|
+
out = git(['log', '--format=%s%n%b%n--', `${baseRef()}..HEAD`, '--not', baseRef()], wt);
|
|
132
|
+
} catch {
|
|
133
|
+
return { text: '', taskIds: [] };
|
|
134
|
+
}
|
|
135
|
+
if (typeof out !== 'string') return { text: '', taskIds: [] };
|
|
136
|
+
// LINEAR, never backtracking (audit 2026-09-24) — this was
|
|
137
|
+
// `out.matchAll(/^\s*Flowviant-Task:\s*(\S+)\s*$/gm)`, whose `\s*$` is
|
|
138
|
+
// quadratic against a long trailing run of whitespace on one line
|
|
139
|
+
// (measured ~1s at 40KB), and a branch's own commit log is exactly the
|
|
140
|
+
// kind of text an agent's commits can grow past that. `taskIdsFromMessage`
|
|
141
|
+
// is the same line-split, length-capped reader every other trailer scan
|
|
142
|
+
// in this codebase uses.
|
|
143
|
+
const taskIds = taskIdsFromMessage(out);
|
|
144
|
+
const text = out
|
|
145
|
+
.split('\n')
|
|
146
|
+
.filter((l) => l.trim())
|
|
147
|
+
.slice(0, PRECHECK_LOG_LINES)
|
|
148
|
+
.join('\n');
|
|
149
|
+
return { text, taskIds: [...taskIds] };
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const runPrecheck = async (agentId, wt, agentName) => {
|
|
153
|
+
/**
|
|
154
|
+
* WHICH CLI READS. `pickRuntimeFor('consult')` — the same picker the scratch
|
|
155
|
+
* planner uses, and for the same reason: this prompt was written against
|
|
156
|
+
* Claude, and a machine with no read-only-capable runtime simply does not
|
|
157
|
+
* produce a precheck. Nothing is posted and nothing is said on the row.
|
|
158
|
+
*/
|
|
159
|
+
const rt = pickRuntimeFor('consult');
|
|
160
|
+
if (!rt) return;
|
|
161
|
+
/**
|
|
162
|
+
* THE PRESSURE GUARD, at the spawn, exactly as every other unattended lane
|
|
163
|
+
* asks it. `churn` and never `interactive`: nobody is watching this, and a
|
|
164
|
+
* label is the first thing that should not be started on a box that is
|
|
165
|
+
* struggling. Deferring here does NOT queue anything — there is no job and
|
|
166
|
+
* no re-offer — so a precheck skipped under pressure is simply a precheck
|
|
167
|
+
* that did not happen, which is what absence already means.
|
|
168
|
+
*/
|
|
169
|
+
const hold = admit('churn');
|
|
170
|
+
if (hold) {
|
|
171
|
+
note(`${c.cyan('pre-review')} ${c.dim(`— skipped: ${hold.reason}`)}`);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
const releaseSlot = admit.reserve();
|
|
175
|
+
|
|
176
|
+
// THE HEAD THE READING BELONGS TO, taken BEFORE the turn — the
|
|
177
|
+
// `checkFingerprint` shape, so a commit landing after this voids it rather
|
|
178
|
+
// than letting an old reading label a branch it never saw. Optional: an
|
|
179
|
+
// unreadable head costs the staleness comparison, not the precheck.
|
|
180
|
+
let headSha = null;
|
|
181
|
+
try {
|
|
182
|
+
headSha = (git(['rev-parse', 'HEAD'], wt) || '').trim() || null;
|
|
183
|
+
} catch {
|
|
184
|
+
headSha = null;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const stash = readStash(sessionMetaPath(wt, 'flowviant-agent-cards', agentId));
|
|
188
|
+
const log = branchLog(wt);
|
|
189
|
+
/** Cards the BRANCH names that this box has no spec for. Measured, not
|
|
190
|
+
* guessed — see agentCards.mjs on why a stash is per-box. */
|
|
191
|
+
const held = new Set(stash.map((s) => s.taskId));
|
|
192
|
+
const missingSpecs = log.taskIds.filter((id) => !held.has(id)).length;
|
|
193
|
+
|
|
194
|
+
let out = '';
|
|
195
|
+
let child = null;
|
|
196
|
+
let timer = null;
|
|
197
|
+
/** The cap fired: the CLI was still running when this machine stopped it. */
|
|
198
|
+
let wedged = false;
|
|
199
|
+
/**
|
|
200
|
+
* WHAT THE READING SPENT — the agent-turn lane's own relay, on the one
|
|
201
|
+
* other lane that charges an AGENT (2026-09-19).
|
|
202
|
+
*
|
|
203
|
+
* It rides the agent and never a card link: a pre-review belongs to no card
|
|
204
|
+
* (the same reason `workChildren` gets a null task id below), so the four
|
|
205
|
+
* counters it adds to are the container's alone. That is also why an
|
|
206
|
+
* agent's links can never sum to its total, and the schema says so.
|
|
207
|
+
*
|
|
208
|
+
* A WEDGED OR UNPARSEABLE READING POSTS NOTHING AT ALL, so it charges
|
|
209
|
+
* nothing either — which under-reports a turn that really did spend. That
|
|
210
|
+
* is the honest direction: there is no row to put it on, and inventing a
|
|
211
|
+
* settle to carry a number would be a post whose only content is a bill.
|
|
212
|
+
*/
|
|
213
|
+
let usage = null;
|
|
214
|
+
/**
|
|
215
|
+
* THE CONVERSATION THIS READING SPEAKS UNDER — held for exactly one reason:
|
|
216
|
+
* to DELETE the transcript it leaves behind (review, 2026-09-17).
|
|
217
|
+
*
|
|
218
|
+
* This is the first thing in the daemon to run a second `claude -p` inside
|
|
219
|
+
* an AGENT's worktree, and the agent's own resume is `--continue`, which is
|
|
220
|
+
* CWD-KEYED — the invariant `runAgentTurn` states in words ("ONE AGENT IS
|
|
221
|
+
* ONE DIRECTORY, so the CLI's own cwd-keyed resume is exactly right here").
|
|
222
|
+
* Leaving this turn's `~/.claude/projects/<munged-cwd>/<id>.jsonl` in place
|
|
223
|
+
* makes the read-only stranger the newest conversation in that directory,
|
|
224
|
+
* so the agent's NEXT turn — a send-back's re-queued card, a merge-resolve,
|
|
225
|
+
* a human's typed answer — resumes "you are a SECOND reviewer… YOU ARE
|
|
226
|
+
* READ-ONLY" instead of its own four-turn context, under build permissions.
|
|
227
|
+
* That is the 0.69.0 Workbench cross-resume and codex's `resume --last`,
|
|
228
|
+
* arriving a third time by a third route.
|
|
229
|
+
*
|
|
230
|
+
* `removeProbeTranscript` is exported for precisely this and already serves
|
|
231
|
+
* the skills probe and the dev-command resolver.
|
|
232
|
+
*/
|
|
233
|
+
let preSession = null;
|
|
234
|
+
try {
|
|
235
|
+
/**
|
|
236
|
+
* THE CAP RESOLVES THE WAIT ITSELF rather than waiting for `close` after
|
|
237
|
+
* the kill — the shape the project check and the planner both keep. A
|
|
238
|
+
* SIGKILLed process whose stdio a grandchild still holds can be slow to
|
|
239
|
+
* emit `close`, or never emit it, and this promise is inside the place's
|
|
240
|
+
* writer lock.
|
|
241
|
+
*/
|
|
242
|
+
let stopWaiting = () => {};
|
|
243
|
+
const capped = new Promise((r) => {
|
|
244
|
+
stopWaiting = r;
|
|
245
|
+
});
|
|
246
|
+
const turn = runTurn({
|
|
247
|
+
prompt: AGENT_PRECHECK_KICKOFF({
|
|
248
|
+
agentName,
|
|
249
|
+
cards: stash.map((s) => s.prompt).join('\n---\n'),
|
|
250
|
+
missingSpecs,
|
|
251
|
+
commits: log.text,
|
|
252
|
+
diffCommand: `git diff ${baseRef()}...HEAD`,
|
|
253
|
+
}),
|
|
254
|
+
system: SYSTEM_PRECHECK,
|
|
255
|
+
// READ-ONLY, and no MCP: `mcpArgs` is omitted entirely rather than
|
|
256
|
+
// passed empty, so there is no control plane on this turn at all.
|
|
257
|
+
profile: 'consult',
|
|
258
|
+
cwd: wt,
|
|
259
|
+
runtime: rt,
|
|
260
|
+
// NO `resume`, AND THAT IS THE FEATURE. A resumed turn would be the
|
|
261
|
+
// agent grading its own homework out of its own context; this is a
|
|
262
|
+
// stranger reading a diff.
|
|
263
|
+
streamJson: true,
|
|
264
|
+
answerFromResult: true,
|
|
265
|
+
label: c.cyan('[pre-review]'),
|
|
266
|
+
// The id the CLI reports at `system.init` — harvested off the stream
|
|
267
|
+
// this turn already parses, no probe and no extra spawn. Held only so
|
|
268
|
+
// the `finally` below can delete this turn's transcript; see
|
|
269
|
+
// `preSession`.
|
|
270
|
+
onInit: (i) => {
|
|
271
|
+
if (typeof i.sessionId === 'string' && i.sessionId.trim())
|
|
272
|
+
preSession = i.sessionId.trim();
|
|
273
|
+
},
|
|
274
|
+
// The CLI's own count for this reading. Set, never accumulated: one
|
|
275
|
+
// turn is one `result` event, and the adding-up is the server's.
|
|
276
|
+
// Tagged with the CLI, so a Claude pre-review of a Codex agent is
|
|
277
|
+
// charged as Claude's and not folded into the agent's own figure.
|
|
278
|
+
onUsage: (u) => {
|
|
279
|
+
usage = { ...u, runtime: rt };
|
|
280
|
+
},
|
|
281
|
+
onSpawn: (ch) => {
|
|
282
|
+
child = ch;
|
|
283
|
+
// No task id: a pre-review belongs to no card, and the machine
|
|
284
|
+
// snapshot's per-task rows must not invent one. It still COUNTS
|
|
285
|
+
// against the machine's ceiling — see liveTurnCount.
|
|
286
|
+
workChildren.set(ch, null);
|
|
287
|
+
releaseSlot();
|
|
288
|
+
// ARMED AT THE SPAWN, not at entry: time spent getting here is not a
|
|
289
|
+
// wedged CLI. The CHILD and never its group — a read-only turn starts
|
|
290
|
+
// no server, so there is nothing behind it worth signalling and
|
|
291
|
+
// everything to lose by signalling somebody else's.
|
|
292
|
+
timer = setTimeout(() => {
|
|
293
|
+
wedged = true;
|
|
294
|
+
try {
|
|
295
|
+
ch.kill('SIGKILL');
|
|
296
|
+
} catch {
|
|
297
|
+
/* already gone */
|
|
298
|
+
}
|
|
299
|
+
stopWaiting('');
|
|
300
|
+
}, PRECHECK_TIMEOUT_MS);
|
|
301
|
+
timer.unref?.();
|
|
302
|
+
},
|
|
303
|
+
});
|
|
304
|
+
out = await Promise.race([turn, capped]);
|
|
305
|
+
} catch {
|
|
306
|
+
return; // a label may never fail the beat it rides
|
|
307
|
+
} finally {
|
|
308
|
+
if (timer) clearTimeout(timer);
|
|
309
|
+
if (child) workChildren.delete(child);
|
|
310
|
+
releaseSlot();
|
|
311
|
+
/**
|
|
312
|
+
* DELETE THIS READING'S TRANSCRIPT, on EVERY exit — settled, wedged and
|
|
313
|
+
* killed, or thrown — because every one of them leaves the file behind
|
|
314
|
+
* and the agent's `--continue` reads the newest one in the directory.
|
|
315
|
+
*
|
|
316
|
+
* AFTER the kill and ON A DELAY, the skills probe's own shape: the
|
|
317
|
+
* transcript is the CHILD's file, so removing it while the child is still
|
|
318
|
+
* dying races a recreate. Unref'd — it must not hold the process open.
|
|
319
|
+
*/
|
|
320
|
+
if (preSession) setTimeout(() => removeProbeTranscript(wt, preSession), 750).unref?.();
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// A WEDGED READING POSTS NOTHING. There is no row to settle and nobody
|
|
324
|
+
// waiting on an answer, so the honest record is that no pre-review exists —
|
|
325
|
+
// the same silence a machine that never ran one leaves.
|
|
326
|
+
if (wedged) {
|
|
327
|
+
note(`${c.cyan('pre-review')} ${c.dim('— ran past five minutes and was stopped')}`);
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* SCRUBBED ON THE WAY OUT, every string — AND SCRUBBED BEFORE IT IS CUT,
|
|
333
|
+
* which is the order that matters and the reason `envScrub` rides INTO the
|
|
334
|
+
* parser rather than being applied to what comes back out.
|
|
335
|
+
*
|
|
336
|
+
* This turn read the repository with `cat` and `git show` in a worktree
|
|
337
|
+
* holding the project's materialized dev secrets, and its answer is about to
|
|
338
|
+
* be stored and rendered to every member of the project. `scrub` replaces
|
|
339
|
+
* EXACT full values, so a note capped first and scrubbed second hands the
|
|
340
|
+
* scrub a credential already cut in half: it matches nothing and the
|
|
341
|
+
* surviving prefix ships. The check's output lane learned exactly that the
|
|
342
|
+
* expensive way, and this lane relearned it in review (2026-09-17) — see
|
|
343
|
+
* `parsePrecheck`, which now caps only what it has already redacted.
|
|
344
|
+
*/
|
|
345
|
+
const result = parsePrecheck(out, envScrub);
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* A QUOTA LIMIT SKIPS THE PRE-REVIEW AND PARKS NOTHING — AND A LIMIT IS
|
|
349
|
+
* ONLY A LIMIT WHEN THE READING PRODUCED NOTHING.
|
|
350
|
+
*
|
|
351
|
+
* `limitLine` is a literal phrase match over the CLI's whole output, and
|
|
352
|
+
* under `answerFromResult` that output IS the reviewer's answer — so a
|
|
353
|
+
* pre-review OF rate-limiting code, or any triage that quotes the phrase,
|
|
354
|
+
* read as a quota failure and threw a perfectly good reading away. The
|
|
355
|
+
* agent-turn lane fixed this exact false positive once (`const limit = res
|
|
356
|
+
* ? null : limitLine(out)`); gating on "nothing parsed" is what makes the
|
|
357
|
+
* match mean what it says.
|
|
358
|
+
*
|
|
359
|
+
* And when it IS a limit, nothing parks. `postAgentParked` stops EVERY
|
|
360
|
+
* agent on the project, because the CLI login is shared — the right answer
|
|
361
|
+
* when the thing that hit the limit was somebody's actual work, the wrong
|
|
362
|
+
* one here: parking a whole fleet because a LABEL could not be written
|
|
363
|
+
* would let an optional readout take the product's primary lane down. The
|
|
364
|
+
* branch is still reviewable; it just has no note on it.
|
|
365
|
+
*/
|
|
366
|
+
if (!result) {
|
|
367
|
+
if (limitLine(out)) {
|
|
368
|
+
note(`${c.cyan('pre-review')} ${c.dim('— skipped: the CLI reported a limit')}`);
|
|
369
|
+
}
|
|
370
|
+
// UNPARSEABLE POSTS NOTHING. A half-read triage is a plausible-looking
|
|
371
|
+
// paragraph nobody wrote, rendered on the surface where somebody decides
|
|
372
|
+
// whether a branch reaches main — see parsePrecheck.
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const body = {
|
|
377
|
+
agentId,
|
|
378
|
+
...(headSha ? { headSha } : {}),
|
|
379
|
+
...(usage ? { usage } : {}),
|
|
380
|
+
cards: result.cards.map((cd) => ({
|
|
381
|
+
taskId: cd.taskId,
|
|
382
|
+
verdict: cd.verdict,
|
|
383
|
+
...(cd.note ? { note: cd.note } : {}),
|
|
384
|
+
})),
|
|
385
|
+
...(result.overall ? { overall: result.overall } : {}),
|
|
386
|
+
};
|
|
387
|
+
// ONE RETRY, and only for a network error — see `postPre`. A permanent
|
|
388
|
+
// refusal is an older server, and asking it again changes nothing.
|
|
389
|
+
if (!(await postPre(body))) await postPre(body);
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
return { runPrecheck };
|
|
393
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PUBLISHING AN AGENT'S BRANCH — the push to the name the server gave it, the
|
|
3
|
+
* fetch that brings a published branch back to a box that lost it, and the
|
|
4
|
+
* two process-local records (what this machine pushed; what it has seen at
|
|
5
|
+
* the remote ref) that the push's lease and the worktree report read.
|
|
6
|
+
*
|
|
7
|
+
* Split out of work.mjs (2026-09-26, SOLID F037). The argv shapes are
|
|
8
|
+
* agentPublish.mjs's (pure, tested there); this module owns the STATE and the
|
|
9
|
+
* network calls. The agent lane pushes after a settle and fetches at the
|
|
10
|
+
* begun-guard, the sweep keeps a published branch current, the merge lane
|
|
11
|
+
* retires the ref — all through the maps and two functions returned here.
|
|
12
|
+
*/
|
|
13
|
+
import { git } from './git.mjs';
|
|
14
|
+
import { publishPushArgs, publishFetchArgs, publishErrorText } from './agentPublish.mjs';
|
|
15
|
+
import { scrub as envScrub } from './uplinkScrub.mjs';
|
|
16
|
+
|
|
17
|
+
export function createWorkAgentPublish({ repoRoot, gitNet }) {
|
|
18
|
+
// ── PUBLISHING AN AGENT'S BRANCH ───────────────────────────────────────────
|
|
19
|
+
//
|
|
20
|
+
// The server names the target (`agentTurnJobs[].publishTo`) and this machine
|
|
21
|
+
// pushes to it. Two laws hold the whole lane up:
|
|
22
|
+
//
|
|
23
|
+
// A PUSH NEVER BLOCKS OR FAILS A TURN. It is tail work after the settle, it
|
|
24
|
+
// is try/catch'd like every sweep, and a failure is REPORTED in git's own
|
|
25
|
+
// words rather than thrown. An agent whose remote push fails has still done
|
|
26
|
+
// its work, still committed it, and still settled.
|
|
27
|
+
//
|
|
28
|
+
// THE SERVER LEARNS OF A PUSH ONLY BY BEING TOLD. Nothing here composes a
|
|
29
|
+
// name: an absent `publishTo` is a project that has publishing off, or a
|
|
30
|
+
// server older than this daemon, and in both the honest behaviour is to push
|
|
31
|
+
// nothing and report nothing. Absence keeps one meaning.
|
|
32
|
+
/**
|
|
33
|
+
* WHAT THIS MACHINE LAST DID WITH EACH AGENT'S BRANCH, by place —
|
|
34
|
+
* `{ ref, sha }` for a push that landed, `{ ref, error, at, tried }` for one
|
|
35
|
+
* that did not. Mutually exclusive by construction, which is what makes the
|
|
36
|
+
* report's two keys mutually exclusive without a second rule.
|
|
37
|
+
*
|
|
38
|
+
* Process-local on purpose: it is a record of what THIS daemon pushed, so a
|
|
39
|
+
* restart re-pushes once and re-reports — a push of the same sha to the same
|
|
40
|
+
* ref is a no-op at the remote, and re-learning beats trusting a file about
|
|
41
|
+
* something a rebase can invalidate.
|
|
42
|
+
*/
|
|
43
|
+
const agentPublished = new Map();
|
|
44
|
+
/**
|
|
45
|
+
* WHAT THIS PROCESS HAS SEEN AT EACH AGENT'S REMOTE REF — `{ ref, sha }`, and
|
|
46
|
+
* the ONLY input to the push's lease.
|
|
47
|
+
*
|
|
48
|
+
* It is deliberately NOT `agentPublished`: that map is the REPORT record (what
|
|
49
|
+
* this machine pushed, and what the server may be told), while this one is an
|
|
50
|
+
* observation of the REMOTE's own position, which a box also gets by FETCHING
|
|
51
|
+
* a ref it never pushed. A box that fetch-continues an agent has seen the ref
|
|
52
|
+
* and must be able to lease against it; a box that has seen nothing pushes
|
|
53
|
+
* with no force flag at all.
|
|
54
|
+
*
|
|
55
|
+
* Process-local for the same reason the record beside it is: a restart has
|
|
56
|
+
* observed nothing, and an unforced push is the honest thing to do about that
|
|
57
|
+
* — it lands, or it refuses and says so.
|
|
58
|
+
*/
|
|
59
|
+
const agentRemoteAt = new Map();
|
|
60
|
+
/** A failed push retries on the next sweep, but not FOREVER at sweep cadence:
|
|
61
|
+
* a remote that refuses (no credentials on this box, a protected prefix)
|
|
62
|
+
* would otherwise cost a blocking network call per agent per minute for the
|
|
63
|
+
* life of the daemon. A moved branch always retries immediately — the
|
|
64
|
+
* throttle is on repeating the SAME attempt, never on new work. */
|
|
65
|
+
const PUBLISH_RETRY_MS = 5 * 60_000;
|
|
66
|
+
/** The tip of an agent's local branch, or null when this box does not hold it
|
|
67
|
+
* — which is a perfectly ordinary state (the begun-guard's whole subject) and
|
|
68
|
+
* means there is nothing to publish, never that a push failed. */
|
|
69
|
+
const agentBranchSha = (place) => {
|
|
70
|
+
try {
|
|
71
|
+
return (
|
|
72
|
+
git(['rev-parse', '--verify', '--quiet', `refs/heads/session/${place}`], repoRoot) || null
|
|
73
|
+
);
|
|
74
|
+
} catch {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
/**
|
|
79
|
+
* PUSH ONE AGENT'S BRANCH TO THE NAME THE SERVER GAVE IT.
|
|
80
|
+
*
|
|
81
|
+
* Returns TRUE when the recorded state CHANGED, because the caller's next act
|
|
82
|
+
* is a worktree report and a report that says what the last one said is a
|
|
83
|
+
* write per minute restating a fact. The rule this serves is the one every
|
|
84
|
+
* settle keeps: an action that changes what the machine would measure must
|
|
85
|
+
* cause a new measurement — and only then.
|
|
86
|
+
*
|
|
87
|
+
* Never throws.
|
|
88
|
+
*/
|
|
89
|
+
const publishAgentBranch = async (place, target) => {
|
|
90
|
+
const sha = agentBranchSha(place);
|
|
91
|
+
if (!sha) return false;
|
|
92
|
+
const prev = agentPublished.get(place);
|
|
93
|
+
if (prev?.ref === target && prev.sha === sha) return false; // already there
|
|
94
|
+
if (
|
|
95
|
+
prev?.ref === target &&
|
|
96
|
+
prev.error &&
|
|
97
|
+
prev.tried === sha &&
|
|
98
|
+
Date.now() - (prev.at ?? 0) < PUBLISH_RETRY_MS
|
|
99
|
+
)
|
|
100
|
+
return false; // the same attempt failed moments ago
|
|
101
|
+
// THE LEASE IS THIS PROCESS'S OWN LAST SIGHTING of that ref, and nothing
|
|
102
|
+
// else — never git's remote-tracking ref, which this daemon's own sweep
|
|
103
|
+
// fetch refreshes (the argument is in `publishPushArgs`). An expectation
|
|
104
|
+
// recorded against a DIFFERENT ref is no expectation for this one.
|
|
105
|
+
const seen = agentRemoteAt.get(place);
|
|
106
|
+
// A ref that is not the server's shape never reaches argv. Silent, because
|
|
107
|
+
// a refusal here is a feature not happening on this turn, not a failure of
|
|
108
|
+
// it — and a `publishError` about a value we declined to use would be this
|
|
109
|
+
// machine reporting on a push it never attempted.
|
|
110
|
+
const args = publishPushArgs(place, target, seen?.ref === target ? seen.sha : null);
|
|
111
|
+
if (!args) return false;
|
|
112
|
+
try {
|
|
113
|
+
gitNet(args, 60_000);
|
|
114
|
+
agentPublished.set(place, { ref: target, sha });
|
|
115
|
+
agentRemoteAt.set(place, { ref: target, sha });
|
|
116
|
+
return true;
|
|
117
|
+
} catch (e) {
|
|
118
|
+
const error = publishErrorText(envScrub(e?.stderr?.toString?.() || e?.message || ''));
|
|
119
|
+
agentPublished.set(place, { ref: target, error, at: Date.now(), tried: sha });
|
|
120
|
+
// A repeat of a failure already reported is not news; the server's stored
|
|
121
|
+
// sentence is already this one.
|
|
122
|
+
return !(prev?.error === error && prev.ref === target);
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
/**
|
|
126
|
+
* BRING A PUBLISHED BRANCH BACK DOWN — the other half of the durability
|
|
127
|
+
* promise, and the only thing that lets an agent's work outlive its box.
|
|
128
|
+
*
|
|
129
|
+
* Reached from ONE place: the begun-guard's refusal arm, where this machine
|
|
130
|
+
* has just MEASURED that it holds neither the agent's worktree nor its branch.
|
|
131
|
+
* The server only sends `publishedRef` when it heard about a real push, so
|
|
132
|
+
* this is not a guess at a remote branch — it is a fetch of one a machine
|
|
133
|
+
* reported writing.
|
|
134
|
+
*
|
|
135
|
+
* ── WHAT IT RECOVERS, AND WHAT IT CANNOT ──
|
|
136
|
+
*
|
|
137
|
+
* COMMITS COME BACK. The CONVERSATION DOES NOT: the CLI's held context lives
|
|
138
|
+
* in the box that ran it and nothing here transports it. The turn kickoff
|
|
139
|
+
* re-prompts from the card, which is the honest continuation — an agent that
|
|
140
|
+
* picks up its own commits and re-reads its own card, never one that remembers
|
|
141
|
+
* the argument. Nothing this returns may be phrased as if it did.
|
|
142
|
+
*
|
|
143
|
+
* THREE ANSWERS, because two would lie. `null` is "there was nothing to try"
|
|
144
|
+
* (no ref, or one whose shape this machine will not put in argv), and it must
|
|
145
|
+
* leave the existing refusal EXACTLY as it was — a sentence about a fetch
|
|
146
|
+
* nobody attempted is worse than the plain refusal. `{ ok: false, why }` is a
|
|
147
|
+
* measured failure, relayed in git's own words. `{ ok: true }` is only ever
|
|
148
|
+
* returned after re-reading the ref: a fetch that exits 0 having created
|
|
149
|
+
* nothing would otherwise walk straight into `placeWtFor` cutting a fresh
|
|
150
|
+
* branch off base — the context-free redo the guard above exists to prevent,
|
|
151
|
+
* wearing this feature's name.
|
|
152
|
+
*/
|
|
153
|
+
const fetchPublishedBranch = (place, ref) => {
|
|
154
|
+
const args = publishFetchArgs(ref, place);
|
|
155
|
+
if (!args) return null;
|
|
156
|
+
try {
|
|
157
|
+
gitNet(args, 120_000);
|
|
158
|
+
} catch (e) {
|
|
159
|
+
return {
|
|
160
|
+
ok: false,
|
|
161
|
+
why: publishErrorText(envScrub(e?.stderr?.toString?.() || e?.message || '')),
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
const sha = agentBranchSha(place);
|
|
165
|
+
if (!sha) return { ok: false, why: 'the fetch reported nothing and left no local branch' };
|
|
166
|
+
// WHAT THIS PROCESS HAS NOW SEEN AT THAT REMOTE REF. A fetch is an
|
|
167
|
+
// observation of the remote's own position, and for a box that continues an
|
|
168
|
+
// agent it never started it is the ONLY one it will ever have — without it
|
|
169
|
+
// this box's first push would carry no lease and would have to go unforced.
|
|
170
|
+
// It is not a `publishAgentBranch` record: this machine pushed nothing, so
|
|
171
|
+
// the server is told nothing.
|
|
172
|
+
agentRemoteAt.set(place, { ref, sha });
|
|
173
|
+
return { ok: true };
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
return { agentPublished, agentRemoteAt, publishAgentBranch, fetchPublishedBranch };
|
|
177
|
+
}
|