flowviant 0.86.0 → 0.88.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/lib/agentCards.mjs +122 -0
- package/bin/lib/agentPlan.mjs +124 -31
- package/bin/lib/claude.mjs +32 -5
- package/bin/lib/fleet.mjs +8 -1
- package/bin/lib/prompts.mjs +173 -5
- package/bin/lib/runtimes.mjs +91 -10
- package/bin/lib/trace.mjs +72 -9
- package/bin/lib/work.mjs +415 -11
- package/package.json +1 -1
package/bin/lib/work.mjs
CHANGED
|
@@ -52,7 +52,8 @@ import { listenersIn, measureListeners, listenersSupported } from './listeners.m
|
|
|
52
52
|
import { measureProcesses, liveGroups, processesSupported } from './processes.mjs';
|
|
53
53
|
import { mutateRegistry, processAlive, readRegistry } from './procRegistry.mjs';
|
|
54
54
|
import { createPlaceLock } from './placeLock.mjs';
|
|
55
|
-
import { parseProposal, parseTurnResult } from './agentPlan.mjs';
|
|
55
|
+
import { parseProposal, parsePrecheck, parseTurnResult } from './agentPlan.mjs';
|
|
56
|
+
import { readStash, stashCard } from './agentCards.mjs';
|
|
56
57
|
import { sweepMergedBranch } from './shipSweep.mjs';
|
|
57
58
|
import { mergeOutward as shipMergeOutward } from './shipMerge.mjs';
|
|
58
59
|
import { openTunnel } from './preview.mjs';
|
|
@@ -68,8 +69,11 @@ import {
|
|
|
68
69
|
SYSTEM_PLAN,
|
|
69
70
|
AGENT_PLAN_KICKOFF,
|
|
70
71
|
SYSTEM_AGENT,
|
|
72
|
+
SYSTEM_PRECHECK,
|
|
71
73
|
AGENT_TASK_KICKOFF,
|
|
74
|
+
AGENT_TASK_SPEC,
|
|
72
75
|
AGENT_HUMAN_KICKOFF,
|
|
76
|
+
AGENT_PRECHECK_KICKOFF,
|
|
73
77
|
} from './prompts.mjs';
|
|
74
78
|
import {
|
|
75
79
|
materializeInto,
|
|
@@ -84,6 +88,7 @@ import {
|
|
|
84
88
|
pickRuntimeFor,
|
|
85
89
|
recordSkills,
|
|
86
90
|
toolEventOf,
|
|
91
|
+
removeProbeTranscript,
|
|
87
92
|
CLAUDE_TOOL_PROSE_KINDS,
|
|
88
93
|
RUNTIMES,
|
|
89
94
|
} from './runtimes.mjs';
|
|
@@ -222,6 +227,7 @@ export function createWorkManager({
|
|
|
222
227
|
const AGENT_TRACE_URL = FLEET_URL.replace(/\/agents\/?$/, '/agent-trace');
|
|
223
228
|
const AGENT_PARKED_URL = FLEET_URL.replace(/\/agents\/?$/, '/agent-parked');
|
|
224
229
|
const AGENT_CHECK_DONE_URL = FLEET_URL.replace(/\/agents\/?$/, '/agent-check-done');
|
|
230
|
+
const AGENT_PRECHECK_URL = FLEET_URL.replace(/\/agents\/?$/, '/agent-precheck');
|
|
225
231
|
const AGENT_MERGE_CLAIM_URL = FLEET_URL.replace(/\/agents\/?$/, '/agent-merge-claim');
|
|
226
232
|
const AGENT_MERGE_DONE_URL = FLEET_URL.replace(/\/agents\/?$/, '/agent-merge-done');
|
|
227
233
|
// What arrived on base, whichever road it took — observed after every beat
|
|
@@ -4810,6 +4816,34 @@ export function createWorkManager({
|
|
|
4810
4816
|
*/
|
|
4811
4817
|
const resume = rt === 'claude' && Boolean(ranMarker && existsSync(ranMarker));
|
|
4812
4818
|
|
|
4819
|
+
/**
|
|
4820
|
+
* WRITE THE CARD DOWN BEFORE HANDING IT OVER — the material the AI
|
|
4821
|
+
* pre-review reads at review entry (see agentCards.mjs).
|
|
4822
|
+
*
|
|
4823
|
+
* HERE rather than at review entry because here is the ONLY moment this
|
|
4824
|
+
* machine holds the card at all: the server feeds an agent one card per
|
|
4825
|
+
* prompt and keeps no copy on this disk, so by the time the queue empties
|
|
4826
|
+
* card one exists locally as nothing but its own commit messages — which
|
|
4827
|
+
* are a claim about the work, not the specification it was judged against.
|
|
4828
|
+
*
|
|
4829
|
+
* BEFORE the CLI runs rather than after, so a turn that crashes still
|
|
4830
|
+
* leaves the spec behind: the card really was given to the agent, the
|
|
4831
|
+
* commits it made are on the branch, and a reviewer is entitled to read
|
|
4832
|
+
* what was asked for either way.
|
|
4833
|
+
*
|
|
4834
|
+
* ONE SPEC BUILDER (`AGENT_TASK_SPEC`) shared with the kickoff below, so
|
|
4835
|
+
* what the reviewer reads is byte-identical to what the agent read.
|
|
4836
|
+
* Failure is swallowed inside `stashCard`: a note about a turn may never
|
|
4837
|
+
* cost the turn.
|
|
4838
|
+
*/
|
|
4839
|
+
if (job.kind === 'task' && job.task) {
|
|
4840
|
+
stashCard(
|
|
4841
|
+
sessionMetaPath(wt, 'flowviant-agent-cards', agentId),
|
|
4842
|
+
job.task.id,
|
|
4843
|
+
AGENT_TASK_SPEC(job.task)
|
|
4844
|
+
);
|
|
4845
|
+
}
|
|
4846
|
+
|
|
4813
4847
|
/**
|
|
4814
4848
|
* THE WHOLE STREAM, not just its latest line — see trace.mjs.
|
|
4815
4849
|
*
|
|
@@ -4888,7 +4922,16 @@ export function createWorkManager({
|
|
|
4888
4922
|
// seconds. Two channels, one stream, and the drop-sampler stays a
|
|
4889
4923
|
// drop-sampler — buffering the pulse would make a stale line look
|
|
4890
4924
|
// fresh, which is the one thing it exists to answer.
|
|
4891
|
-
|
|
4925
|
+
//
|
|
4926
|
+
// …AND THE TRACE TAKES THE WHOLE LINE. `label` is a 160-char
|
|
4927
|
+
// console readout; `full` is what the CLI actually said, when the
|
|
4928
|
+
// parser had more than the label could hold (see runtimes.mjs).
|
|
4929
|
+
// The fallback is not a degradation — it is what every activity
|
|
4930
|
+
// without a fuller form carries, and what an entire pre-0.87.0
|
|
4931
|
+
// daemon carried for all of them. The PULSE below is deliberately
|
|
4932
|
+
// still `line`: it is one overwritten line on a board, and a
|
|
4933
|
+
// paragraph there would be a paragraph nobody can read.
|
|
4934
|
+
if (!doubledKinds || !doubledKinds.has(a.kind)) trace.prose(a.kind, a.full ?? line);
|
|
4892
4935
|
const now = Date.now();
|
|
4893
4936
|
if (now - (lastAgentBeat.get(agentId) ?? 0) < 2_000) return;
|
|
4894
4937
|
lastAgentBeat.set(agentId, now);
|
|
@@ -5000,9 +5043,10 @@ export function createWorkManager({
|
|
|
5000
5043
|
branch,
|
|
5001
5044
|
worktree: wt,
|
|
5002
5045
|
});
|
|
5003
|
-
// The queue just emptied. Run the project's own check
|
|
5004
|
-
// worktree we are already standing in and still
|
|
5005
|
-
|
|
5046
|
+
// The queue just emptied. Run the project's own check and the AI
|
|
5047
|
+
// pre-review HERE, in the worktree we are already standing in and still
|
|
5048
|
+
// hold the lock on — see `runReviewEntry`.
|
|
5049
|
+
if (reply?.review === true) await runReviewEntry(agentId, wt, job.agentName);
|
|
5006
5050
|
});
|
|
5007
5051
|
/**
|
|
5008
5052
|
* …AND THEN PUBLISH, IF THE PROJECT PUBLISHES.
|
|
@@ -5093,8 +5137,8 @@ export function createWorkManager({
|
|
|
5093
5137
|
// body — never the CLI, which would spend the operator's quota again
|
|
5094
5138
|
// and write a second set of commits. The reply can still carry the one
|
|
5095
5139
|
// instruction a settle can (`review: true`, the queue just emptied),
|
|
5096
|
-
// so the project's check
|
|
5097
|
-
// lock the turn itself would have held.
|
|
5140
|
+
// so the project's check and the AI pre-review run from here too, under
|
|
5141
|
+
// the same writer lock the turn itself would have held.
|
|
5098
5142
|
agentTurns.add(id);
|
|
5099
5143
|
void (async () => {
|
|
5100
5144
|
const reply = await postAgentTurn(held.body);
|
|
@@ -5102,7 +5146,7 @@ export function createWorkManager({
|
|
|
5102
5146
|
const wt = typeof held.body.worktree === 'string' ? held.body.worktree : null;
|
|
5103
5147
|
if (reply?.review === true && isSafePathSegment(place) && wt && existsSync(wt)) {
|
|
5104
5148
|
await inPlace(place, place.startsWith('a-'), () =>
|
|
5105
|
-
|
|
5149
|
+
runReviewEntry(String(job.agentId || ''), wt, job.agentName)
|
|
5106
5150
|
);
|
|
5107
5151
|
}
|
|
5108
5152
|
})()
|
|
@@ -5352,6 +5396,363 @@ export function createWorkManager({
|
|
|
5352
5396
|
});
|
|
5353
5397
|
};
|
|
5354
5398
|
|
|
5399
|
+
// ── THE AI PRE-REVIEW ──────────────────────────────────────────────────────
|
|
5400
|
+
//
|
|
5401
|
+
// A FRESH Claude reads the branch before the human does. The owner asked for
|
|
5402
|
+
// it in these words: "before having the user manually check, can we have the
|
|
5403
|
+
// daemon … spawn an agent to review the work so basically we get an ai to look
|
|
5404
|
+
// at the review before a human looks at it for a double check."
|
|
5405
|
+
//
|
|
5406
|
+
// IT IS NOT THE AGENT CHECKING ITSELF. `runTurn` is called with no `resume`,
|
|
5407
|
+
// so there is no conversation to inherit: an agent that spent four turns
|
|
5408
|
+
// arguing itself into a design defends that design, and asked whether its work
|
|
5409
|
+
// meets the card it answers from the very context that produced the work. The
|
|
5410
|
+
// reviewer stands IN the agent's worktree because it needs the code and the
|
|
5411
|
+
// diff, under `readOnly` (CONSULT_PERM — Read, Grep, Glob and a few git reads)
|
|
5412
|
+
// with NO MCP passed at all, so there is no control plane on this turn even if
|
|
5413
|
+
// the repository it reads tries to steer it.
|
|
5414
|
+
//
|
|
5415
|
+
// IT LABELS AND NEVER BLOCKS — the check's own law, one function up. Approve,
|
|
5416
|
+
// the per-card verdicts and the ship quiz do not know this exists. Every exit
|
|
5417
|
+
// below POSTS NOTHING, and the server renders an absent precheck as nothing:
|
|
5418
|
+
// a failed, timed-out, skipped or unparseable read leaves the human's review
|
|
5419
|
+
// exactly as it was before this feature existed. Ignorance never withholds.
|
|
5420
|
+
//
|
|
5421
|
+
// NO VERSION FLOOR. This is a daemon→server report on a NEW endpoint, so an
|
|
5422
|
+
// older daemon simply never posts, and an older SERVER 404s — which `postPre`
|
|
5423
|
+
// treats as delivered-and-done for the agent-trace reason stated there.
|
|
5424
|
+
//
|
|
5425
|
+
// IT RIDES THE BEAT THE CHECK ALREADY OWNS (`runReviewEntry`), AFTER it: the
|
|
5426
|
+
// check is a local command and this is a model call, so the cheap answer lands
|
|
5427
|
+
// on the row first and a wedged reviewer cannot delay it.
|
|
5428
|
+
/**
|
|
5429
|
+
* FIVE MINUTES, and `runTurn` has no timer of its own.
|
|
5430
|
+
*
|
|
5431
|
+
* Half the planner's cap, because this turn is strictly smaller — it reads one
|
|
5432
|
+
* branch's diff and answers, where a planner reads a repository to decide
|
|
5433
|
+
* whether a batch of work collides. And it is HELD INSIDE THE PLACE WRITER
|
|
5434
|
+
* LOCK by the beat it rides, so every minute here is a minute the agent's next
|
|
5435
|
+
* turn (or its merge) is waiting: a generous cap on a label would be spending
|
|
5436
|
+
* the work's time on a note about the work.
|
|
5437
|
+
*/
|
|
5438
|
+
const PRECHECK_TIMEOUT_MS = 5 * 60_000;
|
|
5439
|
+
/** Commit subjects handed to the reviewer. A bound on the prompt, not on the
|
|
5440
|
+
* branch — the reviewer reads the diff itself, and the log is context. */
|
|
5441
|
+
const PRECHECK_LOG_LINES = 80;
|
|
5442
|
+
|
|
5443
|
+
/**
|
|
5444
|
+
* ONE PRE-REVIEW, POSTED.
|
|
5445
|
+
*
|
|
5446
|
+
* Resolves TRUE for a permanent refusal as well as a success, and that is
|
|
5447
|
+
* deliberate — the `postAgentTrace` rule, for the same reason: a server with
|
|
5448
|
+
* no such route 404s this body and will 404 every retry of it, so re-sending
|
|
5449
|
+
* would be a wedge wearing a retry's clothes. A NETWORK error resolves false
|
|
5450
|
+
* and is retried ONCE, because unlike a trace batch this body cost a whole
|
|
5451
|
+
* model call and losing it to a blip means the operator paid for a label
|
|
5452
|
+
* nobody ever sees.
|
|
5453
|
+
*/
|
|
5454
|
+
const postPre = async (body) => {
|
|
5455
|
+
try {
|
|
5456
|
+
const res = await fetch(AGENT_PRECHECK_URL, {
|
|
5457
|
+
method: 'POST',
|
|
5458
|
+
headers: {
|
|
5459
|
+
Authorization: `Bearer ${FLEET_TOKEN}`,
|
|
5460
|
+
'User-Agent': USER_AGENT,
|
|
5461
|
+
'Content-Type': 'application/json',
|
|
5462
|
+
},
|
|
5463
|
+
signal: AbortSignal.timeout(30_000),
|
|
5464
|
+
body: JSON.stringify(body),
|
|
5465
|
+
});
|
|
5466
|
+
return (
|
|
5467
|
+
res.ok ||
|
|
5468
|
+
(res.status >= 400 && res.status < 500 && res.status !== 408 && res.status !== 429)
|
|
5469
|
+
);
|
|
5470
|
+
} catch {
|
|
5471
|
+
return false; // a blip — worth one more attempt at a model call's answer
|
|
5472
|
+
}
|
|
5473
|
+
};
|
|
5474
|
+
|
|
5475
|
+
/**
|
|
5476
|
+
* THE BRANCH'S OWN COMMITS, subject + trailers, and the card ids they name.
|
|
5477
|
+
*
|
|
5478
|
+
* MEASURED, never asserted — the same reason `commitsBetween` exists. The
|
|
5479
|
+
* trailer ids are what let the prompt say "N earlier cards' specs are not on
|
|
5480
|
+
* this box" honestly: the difference between the cards this branch claims and
|
|
5481
|
+
* the specs this disk holds is a fact, and a box that adopted the agent
|
|
5482
|
+
* mid-run is exactly the case where it is non-zero.
|
|
5483
|
+
*
|
|
5484
|
+
* IT CANNOT THROW. `baseRef()` is free text an owner typed and may name no
|
|
5485
|
+
* ref at all; an unreadable range costs the reviewer its context, never the
|
|
5486
|
+
* review-entry beat it is standing in.
|
|
5487
|
+
*/
|
|
5488
|
+
const branchLog = (wt) => {
|
|
5489
|
+
let out;
|
|
5490
|
+
try {
|
|
5491
|
+
out = git(['log', '--format=%s%n%b%n--', `${baseRef()}..HEAD`, '--not', baseRef()], wt);
|
|
5492
|
+
} catch {
|
|
5493
|
+
return { text: '', taskIds: [] };
|
|
5494
|
+
}
|
|
5495
|
+
if (typeof out !== 'string') return { text: '', taskIds: [] };
|
|
5496
|
+
const taskIds = new Set();
|
|
5497
|
+
for (const m of out.matchAll(/^\s*Flowviant-Task:\s*(\S+)\s*$/gm)) {
|
|
5498
|
+
taskIds.add(m[1].slice(0, 64));
|
|
5499
|
+
}
|
|
5500
|
+
const text = out
|
|
5501
|
+
.split('\n')
|
|
5502
|
+
.filter((l) => l.trim())
|
|
5503
|
+
.slice(0, PRECHECK_LOG_LINES)
|
|
5504
|
+
.join('\n');
|
|
5505
|
+
return { text, taskIds: [...taskIds] };
|
|
5506
|
+
};
|
|
5507
|
+
|
|
5508
|
+
const runPrecheck = async (agentId, wt, agentName) => {
|
|
5509
|
+
/**
|
|
5510
|
+
* WHICH CLI READS. `pickRuntimeFor('consult')` — the same picker the scratch
|
|
5511
|
+
* planner uses, and for the same reason: this prompt was written against
|
|
5512
|
+
* Claude, and a machine with no read-only-capable runtime simply does not
|
|
5513
|
+
* produce a precheck. Nothing is posted and nothing is said on the row.
|
|
5514
|
+
*/
|
|
5515
|
+
const rt = pickRuntimeFor('consult');
|
|
5516
|
+
if (!rt) return;
|
|
5517
|
+
/**
|
|
5518
|
+
* THE PRESSURE GUARD, at the spawn, exactly as every other unattended lane
|
|
5519
|
+
* asks it. `churn` and never `interactive`: nobody is watching this, and a
|
|
5520
|
+
* label is the first thing that should not be started on a box that is
|
|
5521
|
+
* struggling. Deferring here does NOT queue anything — there is no job and
|
|
5522
|
+
* no re-offer — so a precheck skipped under pressure is simply a precheck
|
|
5523
|
+
* that did not happen, which is what absence already means.
|
|
5524
|
+
*/
|
|
5525
|
+
const hold = admit('churn');
|
|
5526
|
+
if (hold) {
|
|
5527
|
+
note(`${c.cyan('pre-review')} ${c.dim(`— skipped: ${hold.reason}`)}`);
|
|
5528
|
+
return;
|
|
5529
|
+
}
|
|
5530
|
+
const releaseSlot = admit.reserve();
|
|
5531
|
+
|
|
5532
|
+
// THE HEAD THE READING BELONGS TO, taken BEFORE the turn — the
|
|
5533
|
+
// `checkFingerprint` shape, so a commit landing after this voids it rather
|
|
5534
|
+
// than letting an old reading label a branch it never saw. Optional: an
|
|
5535
|
+
// unreadable head costs the staleness comparison, not the precheck.
|
|
5536
|
+
let headSha = null;
|
|
5537
|
+
try {
|
|
5538
|
+
headSha = (git(['rev-parse', 'HEAD'], wt) || '').trim() || null;
|
|
5539
|
+
} catch {
|
|
5540
|
+
headSha = null;
|
|
5541
|
+
}
|
|
5542
|
+
|
|
5543
|
+
const stash = readStash(sessionMetaPath(wt, 'flowviant-agent-cards', agentId));
|
|
5544
|
+
const log = branchLog(wt);
|
|
5545
|
+
/** Cards the BRANCH names that this box has no spec for. Measured, not
|
|
5546
|
+
* guessed — see agentCards.mjs on why a stash is per-box. */
|
|
5547
|
+
const held = new Set(stash.map((s) => s.taskId));
|
|
5548
|
+
const missingSpecs = log.taskIds.filter((id) => !held.has(id)).length;
|
|
5549
|
+
|
|
5550
|
+
let out = '';
|
|
5551
|
+
let child = null;
|
|
5552
|
+
let timer = null;
|
|
5553
|
+
/** The cap fired: the CLI was still running when this machine stopped it. */
|
|
5554
|
+
let wedged = false;
|
|
5555
|
+
/**
|
|
5556
|
+
* THE CONVERSATION THIS READING SPEAKS UNDER — held for exactly one reason:
|
|
5557
|
+
* to DELETE the transcript it leaves behind (review, 2026-09-17).
|
|
5558
|
+
*
|
|
5559
|
+
* This is the first thing in the daemon to run a second `claude -p` inside
|
|
5560
|
+
* an AGENT's worktree, and the agent's own resume is `--continue`, which is
|
|
5561
|
+
* CWD-KEYED — the invariant `runAgentTurn` states in words ("ONE AGENT IS
|
|
5562
|
+
* ONE DIRECTORY, so the CLI's own cwd-keyed resume is exactly right here").
|
|
5563
|
+
* Leaving this turn's `~/.claude/projects/<munged-cwd>/<id>.jsonl` in place
|
|
5564
|
+
* makes the read-only stranger the newest conversation in that directory,
|
|
5565
|
+
* so the agent's NEXT turn — a send-back's re-queued card, a merge-resolve,
|
|
5566
|
+
* a human's typed answer — resumes "you are a SECOND reviewer… YOU ARE
|
|
5567
|
+
* READ-ONLY" instead of its own four-turn context, under build permissions.
|
|
5568
|
+
* That is the 0.69.0 Workbench cross-resume and codex's `resume --last`,
|
|
5569
|
+
* arriving a third time by a third route.
|
|
5570
|
+
*
|
|
5571
|
+
* `removeProbeTranscript` is exported for precisely this and already serves
|
|
5572
|
+
* the skills probe and the dev-command resolver.
|
|
5573
|
+
*/
|
|
5574
|
+
let preSession = null;
|
|
5575
|
+
try {
|
|
5576
|
+
/**
|
|
5577
|
+
* THE CAP RESOLVES THE WAIT ITSELF rather than waiting for `close` after
|
|
5578
|
+
* the kill — the shape the project check and the planner both keep. A
|
|
5579
|
+
* SIGKILLed process whose stdio a grandchild still holds can be slow to
|
|
5580
|
+
* emit `close`, or never emit it, and this promise is inside the place's
|
|
5581
|
+
* writer lock.
|
|
5582
|
+
*/
|
|
5583
|
+
let stopWaiting = () => {};
|
|
5584
|
+
const capped = new Promise((r) => {
|
|
5585
|
+
stopWaiting = r;
|
|
5586
|
+
});
|
|
5587
|
+
const turn = runTurn({
|
|
5588
|
+
prompt: AGENT_PRECHECK_KICKOFF({
|
|
5589
|
+
agentName,
|
|
5590
|
+
cards: stash.map((s) => s.prompt).join('\n---\n'),
|
|
5591
|
+
missingSpecs,
|
|
5592
|
+
commits: log.text,
|
|
5593
|
+
diffCommand: `git diff ${baseRef()}...HEAD`,
|
|
5594
|
+
}),
|
|
5595
|
+
system: SYSTEM_PRECHECK,
|
|
5596
|
+
// READ-ONLY, and no MCP: `mcpArgs` is omitted entirely rather than
|
|
5597
|
+
// passed empty, so there is no control plane on this turn at all.
|
|
5598
|
+
readOnly: true,
|
|
5599
|
+
cwd: wt,
|
|
5600
|
+
runtime: rt,
|
|
5601
|
+
// NO `resume`, AND THAT IS THE FEATURE. A resumed turn would be the
|
|
5602
|
+
// agent grading its own homework out of its own context; this is a
|
|
5603
|
+
// stranger reading a diff.
|
|
5604
|
+
streamJson: true,
|
|
5605
|
+
answerFromResult: true,
|
|
5606
|
+
label: c.cyan('[pre-review]'),
|
|
5607
|
+
// The id the CLI reports at `system.init` — harvested off the stream
|
|
5608
|
+
// this turn already parses, no probe and no extra spawn. Held only so
|
|
5609
|
+
// the `finally` below can delete this turn's transcript; see
|
|
5610
|
+
// `preSession`.
|
|
5611
|
+
onInit: (i) => {
|
|
5612
|
+
if (typeof i.sessionId === 'string' && i.sessionId.trim())
|
|
5613
|
+
preSession = i.sessionId.trim();
|
|
5614
|
+
},
|
|
5615
|
+
onSpawn: (ch) => {
|
|
5616
|
+
child = ch;
|
|
5617
|
+
// No task id: a pre-review belongs to no card, and the machine
|
|
5618
|
+
// snapshot's per-task rows must not invent one. It still COUNTS
|
|
5619
|
+
// against the machine's ceiling — see liveTurnCount.
|
|
5620
|
+
workChildren.set(ch, null);
|
|
5621
|
+
releaseSlot();
|
|
5622
|
+
// ARMED AT THE SPAWN, not at entry: time spent getting here is not a
|
|
5623
|
+
// wedged CLI. The CHILD and never its group — a read-only turn starts
|
|
5624
|
+
// no server, so there is nothing behind it worth signalling and
|
|
5625
|
+
// everything to lose by signalling somebody else's.
|
|
5626
|
+
timer = setTimeout(() => {
|
|
5627
|
+
wedged = true;
|
|
5628
|
+
try {
|
|
5629
|
+
ch.kill('SIGKILL');
|
|
5630
|
+
} catch {
|
|
5631
|
+
/* already gone */
|
|
5632
|
+
}
|
|
5633
|
+
stopWaiting('');
|
|
5634
|
+
}, PRECHECK_TIMEOUT_MS);
|
|
5635
|
+
timer.unref?.();
|
|
5636
|
+
},
|
|
5637
|
+
});
|
|
5638
|
+
out = await Promise.race([turn, capped]);
|
|
5639
|
+
} catch {
|
|
5640
|
+
return; // a label may never fail the beat it rides
|
|
5641
|
+
} finally {
|
|
5642
|
+
if (timer) clearTimeout(timer);
|
|
5643
|
+
if (child) workChildren.delete(child);
|
|
5644
|
+
releaseSlot();
|
|
5645
|
+
/**
|
|
5646
|
+
* DELETE THIS READING'S TRANSCRIPT, on EVERY exit — settled, wedged and
|
|
5647
|
+
* killed, or thrown — because every one of them leaves the file behind
|
|
5648
|
+
* and the agent's `--continue` reads the newest one in the directory.
|
|
5649
|
+
*
|
|
5650
|
+
* AFTER the kill and ON A DELAY, the skills probe's own shape: the
|
|
5651
|
+
* transcript is the CHILD's file, so removing it while the child is still
|
|
5652
|
+
* dying races a recreate. Unref'd — it must not hold the process open.
|
|
5653
|
+
*/
|
|
5654
|
+
if (preSession) setTimeout(() => removeProbeTranscript(wt, preSession), 750).unref?.();
|
|
5655
|
+
}
|
|
5656
|
+
|
|
5657
|
+
// A WEDGED READING POSTS NOTHING. There is no row to settle and nobody
|
|
5658
|
+
// waiting on an answer, so the honest record is that no pre-review exists —
|
|
5659
|
+
// the same silence a machine that never ran one leaves.
|
|
5660
|
+
if (wedged) {
|
|
5661
|
+
note(`${c.cyan('pre-review')} ${c.dim('— ran past five minutes and was stopped')}`);
|
|
5662
|
+
return;
|
|
5663
|
+
}
|
|
5664
|
+
|
|
5665
|
+
/**
|
|
5666
|
+
* SCRUBBED ON THE WAY OUT, every string — AND SCRUBBED BEFORE IT IS CUT,
|
|
5667
|
+
* which is the order that matters and the reason `envScrub` rides INTO the
|
|
5668
|
+
* parser rather than being applied to what comes back out.
|
|
5669
|
+
*
|
|
5670
|
+
* This turn read the repository with `cat` and `git show` in a worktree
|
|
5671
|
+
* holding the project's materialized dev secrets, and its answer is about to
|
|
5672
|
+
* be stored and rendered to every member of the project. `scrub` replaces
|
|
5673
|
+
* EXACT full values, so a note capped first and scrubbed second hands the
|
|
5674
|
+
* scrub a credential already cut in half: it matches nothing and the
|
|
5675
|
+
* surviving prefix ships. The check's output lane learned exactly that the
|
|
5676
|
+
* expensive way, and this lane relearned it in review (2026-09-17) — see
|
|
5677
|
+
* `parsePrecheck`, which now caps only what it has already redacted.
|
|
5678
|
+
*/
|
|
5679
|
+
const result = parsePrecheck(out, envScrub);
|
|
5680
|
+
|
|
5681
|
+
/**
|
|
5682
|
+
* A QUOTA LIMIT SKIPS THE PRE-REVIEW AND PARKS NOTHING — AND A LIMIT IS
|
|
5683
|
+
* ONLY A LIMIT WHEN THE READING PRODUCED NOTHING.
|
|
5684
|
+
*
|
|
5685
|
+
* `limitLine` is a literal phrase match over the CLI's whole output, and
|
|
5686
|
+
* under `answerFromResult` that output IS the reviewer's answer — so a
|
|
5687
|
+
* pre-review OF rate-limiting code, or any triage that quotes the phrase,
|
|
5688
|
+
* read as a quota failure and threw a perfectly good reading away. The
|
|
5689
|
+
* agent-turn lane fixed this exact false positive once (`const limit = res
|
|
5690
|
+
* ? null : limitLine(out)`); gating on "nothing parsed" is what makes the
|
|
5691
|
+
* match mean what it says.
|
|
5692
|
+
*
|
|
5693
|
+
* And when it IS a limit, nothing parks. `postAgentParked` stops EVERY
|
|
5694
|
+
* agent on the project, because the CLI login is shared — the right answer
|
|
5695
|
+
* when the thing that hit the limit was somebody's actual work, the wrong
|
|
5696
|
+
* one here: parking a whole fleet because a LABEL could not be written
|
|
5697
|
+
* would let an optional readout take the product's primary lane down. The
|
|
5698
|
+
* branch is still reviewable; it just has no note on it.
|
|
5699
|
+
*/
|
|
5700
|
+
if (!result) {
|
|
5701
|
+
if (limitLine(out)) {
|
|
5702
|
+
note(`${c.cyan('pre-review')} ${c.dim('— skipped: the CLI reported a limit')}`);
|
|
5703
|
+
}
|
|
5704
|
+
// UNPARSEABLE POSTS NOTHING. A half-read triage is a plausible-looking
|
|
5705
|
+
// paragraph nobody wrote, rendered on the surface where somebody decides
|
|
5706
|
+
// whether a branch reaches main — see parsePrecheck.
|
|
5707
|
+
return;
|
|
5708
|
+
}
|
|
5709
|
+
|
|
5710
|
+
const body = {
|
|
5711
|
+
agentId,
|
|
5712
|
+
...(headSha ? { headSha } : {}),
|
|
5713
|
+
cards: result.cards.map((cd) => ({
|
|
5714
|
+
taskId: cd.taskId,
|
|
5715
|
+
verdict: cd.verdict,
|
|
5716
|
+
...(cd.note ? { note: cd.note } : {}),
|
|
5717
|
+
})),
|
|
5718
|
+
...(result.overall ? { overall: result.overall } : {}),
|
|
5719
|
+
};
|
|
5720
|
+
// ONE RETRY, and only for a network error — see `postPre`. A permanent
|
|
5721
|
+
// refusal is an older server, and asking it again changes nothing.
|
|
5722
|
+
if (!(await postPre(body))) await postPre(body);
|
|
5723
|
+
};
|
|
5724
|
+
|
|
5725
|
+
/**
|
|
5726
|
+
* REVIEW ENTRY — everything this machine does the moment an agent's queue
|
|
5727
|
+
* empties, in one place.
|
|
5728
|
+
*
|
|
5729
|
+
* It exists so the two readings cannot drift apart at the three call sites
|
|
5730
|
+
* that own this beat (the settle reply, the held body's re-POST, and the
|
|
5731
|
+
* stale-merge re-read after base is folded in). Each of those used to call
|
|
5732
|
+
* `runCheck` directly; a second thing to run at the same moment is a second
|
|
5733
|
+
* thing three call sites can forget.
|
|
5734
|
+
*
|
|
5735
|
+
* THE CHECK FIRST, ALWAYS. It is a local command whose answer the board wants
|
|
5736
|
+
* on the row immediately; the pre-review is a model call that may take
|
|
5737
|
+
* minutes. Ordering them the other way would put a label behind a label.
|
|
5738
|
+
*
|
|
5739
|
+
* NEITHER MAY THROW PAST THIS POINT. Both are optional readouts and both run
|
|
5740
|
+
* INSIDE the place's writer lock on a path whose callers settle real work —
|
|
5741
|
+
* `runAgentMerge` in particular reports a claimed merge after this returns.
|
|
5742
|
+
*/
|
|
5743
|
+
const runReviewEntry = async (agentId, wt, agentName) => {
|
|
5744
|
+
try {
|
|
5745
|
+
await runCheck(agentId, wt);
|
|
5746
|
+
} catch {
|
|
5747
|
+
/* the row keeps its previous check answer, which is null the first time */
|
|
5748
|
+
}
|
|
5749
|
+
try {
|
|
5750
|
+
await runPrecheck(agentId, wt, agentName);
|
|
5751
|
+
} catch {
|
|
5752
|
+
/* no pre-review is posted, and absence renders nothing */
|
|
5753
|
+
}
|
|
5754
|
+
};
|
|
5755
|
+
|
|
5355
5756
|
// ── THE MERGE ──────────────────────────────────────────────────────────────
|
|
5356
5757
|
//
|
|
5357
5758
|
// LEASED, because two `git merge --no-ff` and two pushes over one branch is
|
|
@@ -5505,9 +5906,12 @@ export function createWorkManager({
|
|
|
5505
5906
|
});
|
|
5506
5907
|
return;
|
|
5507
5908
|
}
|
|
5508
|
-
// The branch changed, so the previous check
|
|
5509
|
-
// tree. Re-
|
|
5510
|
-
|
|
5909
|
+
// The branch changed, so the previous check — and the previous
|
|
5910
|
+
// pre-review — answered about a different tree. Re-read it before
|
|
5911
|
+
// anything merges: a failed merge sends the agent BACK to review, which
|
|
5912
|
+
// is a review-entry beat like any other, and a stale reading standing
|
|
5913
|
+
// over a rebased branch is exactly what `precheckSha` exists to void.
|
|
5914
|
+
await runReviewEntry(agentId, wt, job.agentName);
|
|
5511
5915
|
}
|
|
5512
5916
|
// `git()` THROWS on a non-zero exit, and `symbolic-ref` exits non-zero on
|
|
5513
5917
|
// a detached HEAD — so the guard below was unreachable and the throw
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "flowviant",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.88.0",
|
|
4
4
|
"description": "Run your own coding CLIs as build agents for Flowviant \u2014 Claude Code, Codex or Antigravity, on your own credentials. Holds your sessions, keeps a worktree per tab, and ships branches on your word.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|