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
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE CARD SPECS AN AGENT WAS ACTUALLY GIVEN, kept on the box that gave them
|
|
3
|
+
* (2026-09-16).
|
|
4
|
+
*
|
|
5
|
+
* The AI pre-review reads a branch at review entry and has to answer "does this
|
|
6
|
+
* diff do what each card asked for" — which needs the cards. Nothing on the
|
|
7
|
+
* machine holds them: an agent turn is fed ONE card at a time, the server types
|
|
8
|
+
* the next when the previous lands, and the daemon composes the prompt and
|
|
9
|
+
* forgets it. By the time the queue empties, the only trace of card one on this
|
|
10
|
+
* disk is whatever its commits happen to say about themselves, and a commit
|
|
11
|
+
* message is a CLAIM about the work rather than the work's specification.
|
|
12
|
+
*
|
|
13
|
+
* So each turn appends the spec it typed. It is the daemon's own text — the same
|
|
14
|
+
* `AGENT_TASK_SPEC` block the agent read, so the reviewer reads what the agent
|
|
15
|
+
* read rather than a second rendering of the card that can drift from it.
|
|
16
|
+
*
|
|
17
|
+
* ── WHERE IT LIVES, AND WHY THAT IS THE WHOLE LIFECYCLE ──
|
|
18
|
+
*
|
|
19
|
+
* The worktree's PRIVATE git dir (`sessionMetaPath`), scoped by agent id. Three
|
|
20
|
+
* properties come free with that choice and none of them needs code:
|
|
21
|
+
*
|
|
22
|
+
* · it is INVISIBLE to `git status`, so a stash can never make a worktree
|
|
23
|
+
* dirty — which would refuse a ship, the exact trap a marker file in the
|
|
24
|
+
* working tree fell into;
|
|
25
|
+
* · it DIES with `git worktree remove`, so retiring an agent's worktree
|
|
26
|
+
* retires its stash. There is no sweep to write and none to forget;
|
|
27
|
+
* · it is per-BOX by construction, which is the honest answer to a machine
|
|
28
|
+
* handover: a box that adopted an agent mid-run holds only the prompts IT
|
|
29
|
+
* typed. The reviewer prompt SAYS how many it is missing rather than
|
|
30
|
+
* inventing the specs it does not have.
|
|
31
|
+
*
|
|
32
|
+
* ── IT IS A STASH, NOT A LEDGER ──
|
|
33
|
+
*
|
|
34
|
+
* Nothing reads it but the precheck, nothing is decided by it, and losing it
|
|
35
|
+
* costs one label nobody was promised. Every failure here is swallowed for that
|
|
36
|
+
* reason: a turn must never fail because a note about it could not be written.
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
import { appendFileSync, readFileSync } from 'node:fs';
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* How many card specs one agent may accumulate.
|
|
43
|
+
*
|
|
44
|
+
* A bound on a MACHINE — an agent's queue is a handful of cards, and this exists
|
|
45
|
+
* so a pathological agent (a card re-delivered fifty times, an agent grown past
|
|
46
|
+
* its budget) cannot turn a prompt into a file read. The NEWEST are kept, which
|
|
47
|
+
* is the same tail-is-what-matters rule the trace keeps.
|
|
48
|
+
*/
|
|
49
|
+
export const MAX_STASHED_CARDS = 40;
|
|
50
|
+
/** The most one spec may contribute. A brief is written by whoever filed the
|
|
51
|
+
* card and the server caps it, but this file is composed into a prompt and a
|
|
52
|
+
* bound it owns is a bound that cannot be argued away upstream. */
|
|
53
|
+
export const MAX_SPEC_CHARS = 8_000;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Write down the spec this turn is about to hand the agent.
|
|
57
|
+
*
|
|
58
|
+
* ONE JSON OBJECT PER LINE, APPENDED. Append rather than rewrite because two
|
|
59
|
+
* turns of one agent never run at once (an agent's place is taken as a WRITER)
|
|
60
|
+
* but a crash between read and write of a whole-file rewrite would lose every
|
|
61
|
+
* earlier card — and because an append is atomic enough at this size that a
|
|
62
|
+
* half-written line is the only damage a kill can do, which the reader drops.
|
|
63
|
+
*
|
|
64
|
+
* A re-delivered card appends a SECOND line for the same id; the reader keeps
|
|
65
|
+
* the last, because that is the spec the agent most recently worked from.
|
|
66
|
+
*/
|
|
67
|
+
export function stashCard(path, taskId, spec) {
|
|
68
|
+
if (!path) return false;
|
|
69
|
+
const id = String(taskId ?? '').trim();
|
|
70
|
+
const text = String(spec ?? '');
|
|
71
|
+
if (!id || !text.trim()) return false;
|
|
72
|
+
try {
|
|
73
|
+
appendFileSync(
|
|
74
|
+
path,
|
|
75
|
+
JSON.stringify({ taskId: id.slice(0, 64), prompt: text.slice(0, MAX_SPEC_CHARS) }) + '\n'
|
|
76
|
+
);
|
|
77
|
+
return true;
|
|
78
|
+
} catch {
|
|
79
|
+
// A stash that could not be written costs the precheck one card's spec,
|
|
80
|
+
// which it will say it could not check rather than guess at.
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* What this box holds, newest spec per card, in the order the cards were worked.
|
|
87
|
+
*
|
|
88
|
+
* MALFORMED LINES ARE DROPPED ALONE — the boundary rule this repo states for
|
|
89
|
+
* every relayed list: one truncated line (a daemon killed mid-append) must not
|
|
90
|
+
* throw away the thirty-nine good specs beside it.
|
|
91
|
+
*/
|
|
92
|
+
export function readStash(path) {
|
|
93
|
+
if (!path) return [];
|
|
94
|
+
let raw;
|
|
95
|
+
try {
|
|
96
|
+
raw = readFileSync(path, 'utf8');
|
|
97
|
+
} catch {
|
|
98
|
+
return []; // no file: this agent has run no card turn on this box
|
|
99
|
+
}
|
|
100
|
+
/** taskId → spec. A Map, so the LAST write per card wins while the insertion
|
|
101
|
+
* order stays the order the cards were first handed out — which is the order
|
|
102
|
+
* the branch was built in, and the order a reviewer reads them in. */
|
|
103
|
+
const byId = new Map();
|
|
104
|
+
for (const line of raw.split('\n')) {
|
|
105
|
+
if (!line.trim()) continue;
|
|
106
|
+
let v;
|
|
107
|
+
try {
|
|
108
|
+
v = JSON.parse(line);
|
|
109
|
+
} catch {
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
if (!v || typeof v !== 'object') continue;
|
|
113
|
+
const taskId = typeof v.taskId === 'string' ? v.taskId.trim() : '';
|
|
114
|
+
const prompt = typeof v.prompt === 'string' ? v.prompt : '';
|
|
115
|
+
if (!taskId || !prompt.trim()) continue;
|
|
116
|
+
byId.set(taskId, prompt.slice(0, MAX_SPEC_CHARS));
|
|
117
|
+
}
|
|
118
|
+
const all = [...byId].map(([taskId, prompt]) => ({ taskId, prompt }));
|
|
119
|
+
// The NEWEST cards when there are too many — a reviewer reading a grown
|
|
120
|
+
// agent's branch is reading the work at its end.
|
|
121
|
+
return all.slice(-MAX_STASHED_CARDS);
|
|
122
|
+
}
|
package/bin/lib/agentPlan.mjs
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* READING A
|
|
2
|
+
* READING A MODEL'S ANSWER, in the agent lane's three shapes: a planner's
|
|
3
|
+
* PROPOSAL, an agent's own TURN RESULT, and (2026-09-16) the AI pre-review's
|
|
4
|
+
* TRIAGE.
|
|
3
5
|
*
|
|
4
6
|
* The scratch agent behind a Deploy press is asked for one JSON object and
|
|
5
7
|
* nothing else. This is what turns its final message into a proposal, and it is
|
|
@@ -31,38 +33,50 @@ const MAX_NAME = 80;
|
|
|
31
33
|
const MAX_NOTE = 1000;
|
|
32
34
|
|
|
33
35
|
/**
|
|
34
|
-
* Find the
|
|
36
|
+
* Find the objects.
|
|
35
37
|
*
|
|
36
38
|
* A fenced block first, because that is what was asked for. Otherwise every `{`
|
|
37
39
|
* in the text is tried as a start, and its BALANCED end is found by counting
|
|
38
|
-
* braces while skipping string literals —
|
|
39
|
-
*
|
|
40
|
+
* braces while skipping string literals — each caller then takes the first
|
|
41
|
+
* candidate whose SHAPE is the one it asked for.
|
|
40
42
|
*
|
|
41
43
|
* The obvious cheap version — first `{` to last `}` — is wrong in a way a test
|
|
42
44
|
* caught: a planner that writes "I looked at {the auth module} first" before its
|
|
43
45
|
* JSON produces a span starting at the wrong brace, and the whole plan is lost
|
|
44
46
|
* to a sentence. Scanning candidates costs nothing at this size and cannot be
|
|
45
47
|
* defeated by prose.
|
|
48
|
+
*
|
|
49
|
+
* ONE SCANNER FOR ALL THREE READERS in this file (2026-09-16). It was written
|
|
50
|
+
* twice — once here and once inline in `parseTurnResult` — and a third copy was
|
|
51
|
+
* about to be written for the precheck. A brace scanner that skips string
|
|
52
|
+
* literals is exactly the kind of thing where two copies quietly stop agreeing
|
|
53
|
+
* about escapes and nobody notices, because the disagreement only shows up on a
|
|
54
|
+
* card title with a quote in it.
|
|
46
55
|
*/
|
|
47
|
-
function
|
|
56
|
+
function candidateObjects(raw) {
|
|
48
57
|
const fenced = raw.match(/```(?:json)?\s*([\s\S]*?)```/);
|
|
49
|
-
const
|
|
50
|
-
if (fenced)
|
|
58
|
+
const bodies = [];
|
|
59
|
+
if (fenced) bodies.push(fenced[1]);
|
|
51
60
|
for (let i = 0; i < raw.length; i++) {
|
|
52
61
|
if (raw[i] !== '{') continue;
|
|
53
62
|
const end = balanced(raw, i);
|
|
54
|
-
if (end > i)
|
|
63
|
+
if (end > i) bodies.push(raw.slice(i, end + 1));
|
|
55
64
|
}
|
|
56
|
-
|
|
65
|
+
const out = [];
|
|
66
|
+
for (const body of bodies) {
|
|
57
67
|
if (!body.trim()) continue;
|
|
58
68
|
try {
|
|
59
69
|
const v = JSON.parse(body);
|
|
60
|
-
if (v && typeof v === 'object'
|
|
70
|
+
if (v && typeof v === 'object') out.push(v);
|
|
61
71
|
} catch {
|
|
62
72
|
/* the next candidate may be the object */
|
|
63
73
|
}
|
|
64
74
|
}
|
|
65
|
-
return
|
|
75
|
+
return out;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function extract(raw) {
|
|
79
|
+
return candidateObjects(raw).find((v) => Array.isArray(v.agents)) ?? null;
|
|
66
80
|
}
|
|
67
81
|
|
|
68
82
|
/** The index of the `}` that closes the `{` at `from`, or -1. Skips string
|
|
@@ -108,9 +122,27 @@ export function parseProposal(text) {
|
|
|
108
122
|
...(Number.isFinite(g.pointsBudget) && g.pointsBudget > 0
|
|
109
123
|
? { pointsBudget: Math.min(Math.round(g.pointsBudget), 100_000) }
|
|
110
124
|
: {}),
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
125
|
+
// NO `waitsOn`, AND ITS ABSENCE IS THE FEATURE (2026-09-16).
|
|
126
|
+
//
|
|
127
|
+
// The planner's schema used to carry it — tempIds of agents that had to
|
|
128
|
+
// MERGE first — and the owner deleted the idea: "whats the point of
|
|
129
|
+
// dividing up the agents if one of the agents rely on waiting for one to
|
|
130
|
+
// finish? if thats the case have it be in the same agent." An agent
|
|
131
|
+
// already works its cards in order, so a chain split across two agents
|
|
132
|
+
// buys a second worktree, a second branch and a second review and then
|
|
133
|
+
// idles one of them; the only thing a split buys is SIMULTANEOUS work.
|
|
134
|
+
// SYSTEM_PLAN no longer mentions the key at all.
|
|
135
|
+
//
|
|
136
|
+
// So a model that emits it anyway is answering a schema it was not given
|
|
137
|
+
// — an older prompt cached in a resumed conversation, or invention — and
|
|
138
|
+
// reading it would create a silently-waiting agent through the exact door
|
|
139
|
+
// the prompt just closed. DROPPED, not REFUSED: this file's own law is
|
|
140
|
+
// lenient packaging, strict shape, and a stray key is packaging. The
|
|
141
|
+
// proposal is otherwise good work the operator already paid for.
|
|
142
|
+
//
|
|
143
|
+
// The SERVER still accepts and honours `waitsOn` on the wire — 0.86.0
|
|
144
|
+
// daemons are still proposing it and existing agents still carry it. This
|
|
145
|
+
// is the end of PROPOSING one, not the end of reading one.
|
|
114
146
|
...(typeof g.intoAgentId === 'string' && g.intoAgentId
|
|
115
147
|
? { intoAgentId: g.intoAgentId.slice(0, 64) }
|
|
116
148
|
: {}),
|
|
@@ -140,23 +172,7 @@ export function parseProposal(text) {
|
|
|
140
172
|
* up here rather than being read as success.
|
|
141
173
|
*/
|
|
142
174
|
export function parseTurnResult(text) {
|
|
143
|
-
const
|
|
144
|
-
const fenced = parsedRaw.match(/```(?:json)?\s*([\s\S]*?)```/);
|
|
145
|
-
const candidates = [];
|
|
146
|
-
if (fenced) candidates.push(fenced[1]);
|
|
147
|
-
for (let i = 0; i < parsedRaw.length; i++) {
|
|
148
|
-
if (parsedRaw[i] !== '{') continue;
|
|
149
|
-
const end = balanced(parsedRaw, i);
|
|
150
|
-
if (end > i) candidates.push(parsedRaw.slice(i, end + 1));
|
|
151
|
-
}
|
|
152
|
-
for (const body of candidates) {
|
|
153
|
-
let v;
|
|
154
|
-
try {
|
|
155
|
-
v = JSON.parse(body);
|
|
156
|
-
} catch {
|
|
157
|
-
continue;
|
|
158
|
-
}
|
|
159
|
-
if (!v || typeof v !== 'object') continue;
|
|
175
|
+
for (const v of candidateObjects(String(text ?? ''))) {
|
|
160
176
|
if (v.status === 'blocked') {
|
|
161
177
|
const question = typeof v.question === 'string' ? v.question.trim() : '';
|
|
162
178
|
// A "blocked" with no question is not an answer anybody can act on — it
|
|
@@ -185,3 +201,80 @@ export function parseTurnResult(text) {
|
|
|
185
201
|
}
|
|
186
202
|
return null;
|
|
187
203
|
}
|
|
204
|
+
|
|
205
|
+
/** The precheck's own bounds — mirrored at the server boundary, which caps
|
|
206
|
+
* again. A note is one sentence of triage and `overall` is one paragraph; the
|
|
207
|
+
* numbers are the ones SYSTEM_PRECHECK asks for, stated here so a model that
|
|
208
|
+
* ignores them cannot make the row bigger than the surface can render. */
|
|
209
|
+
const MAX_PRECHECK_CARDS = 60;
|
|
210
|
+
const MAX_PRECHECK_NOTE = 400;
|
|
211
|
+
const MAX_PRECHECK_OVERALL = 1200;
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* READING THE AI PRE-REVIEW's ANSWER (2026-09-16).
|
|
215
|
+
*
|
|
216
|
+
* Same law as its two neighbours — LENIENT ON PACKAGING, STRICT ON SHAPE — and
|
|
217
|
+
* here the strict half has a sharper consequence than usual: this text is about
|
|
218
|
+
* to be rendered on the surface where somebody decides whether a branch reaches
|
|
219
|
+
* main. A half-read answer is a plausible-looking triage nobody wrote.
|
|
220
|
+
*
|
|
221
|
+
* NULL IS THE SAFE ANSWER AND THE COMMON ONE. A precheck that came back
|
|
222
|
+
* unparseable posts NOTHING, and the absence renders nothing: the human's review
|
|
223
|
+
* is exactly what it was before this feature existed. That is the whole reason
|
|
224
|
+
* this may be strict where `parseProposal` cannot be — a lost proposal wastes a
|
|
225
|
+
* press somebody made, a lost precheck costs a label nobody was promised.
|
|
226
|
+
*
|
|
227
|
+
* ONE ENTRY PER CARD, and the FIRST one wins: a model that judges a card twice
|
|
228
|
+
* has contradicted itself, and rendering two notes on one card face would ask
|
|
229
|
+
* the reviewer to arbitrate between them. An unknown verdict word is dropped
|
|
230
|
+
* rather than coerced — `ok` is a claim ("I looked and found nothing"), and
|
|
231
|
+
* guessing it from a word nobody listed would be the parser making that claim.
|
|
232
|
+
*
|
|
233
|
+
* `scrub` RIDES IN, AND IT RUNS BEFORE EVERY CUT (review, 2026-09-17).
|
|
234
|
+
*
|
|
235
|
+
* The caller used to scrub afterwards — `envScrub(cd.note).slice(0, 400)` over
|
|
236
|
+
* a note this function had ALREADY cut to 400. `scrub` replaces EXACT full
|
|
237
|
+
* values, so a credential straddling the cut arrived here pre-severed, matched
|
|
238
|
+
* nothing, and its surviving prefix was stored and rendered to every member of
|
|
239
|
+
* the project. That is byte-for-byte the bug `runCheck`'s output lane records
|
|
240
|
+
* learning the expensive way, and the reviewer this parses reads a worktree
|
|
241
|
+
* holding the project's materialized dev secrets — a note quoting a `.env`
|
|
242
|
+
* line is the ordinary way to reach it. The fix is `toolEventOf`'s: the scrub
|
|
243
|
+
* rides INTO the builder and runs over the whole field, before the cap.
|
|
244
|
+
*
|
|
245
|
+
* DEFAULTED TO IDENTITY so the parser stays testable on its own, and so a
|
|
246
|
+
* caller that forgets loses redaction rather than the whole reading — but the
|
|
247
|
+
* ONE production caller passes `envScrub`, and `work.test.mjs` pins the order.
|
|
248
|
+
*/
|
|
249
|
+
export function parsePrecheck(text, scrub = (s) => s) {
|
|
250
|
+
const parsed = candidateObjects(String(text ?? '')).find((v) => Array.isArray(v.cards));
|
|
251
|
+
if (!parsed) return null;
|
|
252
|
+
|
|
253
|
+
const cards = [];
|
|
254
|
+
const seen = new Set();
|
|
255
|
+
for (const c of parsed.cards.slice(0, MAX_PRECHECK_CARDS)) {
|
|
256
|
+
if (!c || typeof c !== 'object') continue;
|
|
257
|
+
const taskId = typeof c.taskId === 'string' ? c.taskId.trim().slice(0, 64) : '';
|
|
258
|
+
if (!taskId || seen.has(taskId)) continue;
|
|
259
|
+
if (c.verdict !== 'ok' && c.verdict !== 'concerns') continue;
|
|
260
|
+
seen.add(taskId);
|
|
261
|
+
// SCRUB, THEN CUT — see the docblock. The whole field is in hand here, so an
|
|
262
|
+
// exact-value match still finds a secret that spans the cap.
|
|
263
|
+
const note =
|
|
264
|
+
typeof c.note === 'string' ? scrub(c.note.trim()).slice(0, MAX_PRECHECK_NOTE) : '';
|
|
265
|
+
cards.push({ taskId, verdict: c.verdict, ...(note ? { note } : {}) });
|
|
266
|
+
}
|
|
267
|
+
const overall =
|
|
268
|
+
typeof parsed.overall === 'string'
|
|
269
|
+
? scrub(parsed.overall.trim()).slice(0, MAX_PRECHECK_OVERALL)
|
|
270
|
+
: '';
|
|
271
|
+
/**
|
|
272
|
+
* AN ANSWER THAT SAYS NOTHING IS NOT AN ANSWER. No readable card verdict and
|
|
273
|
+
* no overall means the model produced the right punctuation and no content —
|
|
274
|
+
* posting that would put an empty "Claude's pre-review" heading on the deck,
|
|
275
|
+
* which reads as a feature that ran and found the branch unremarkable. It did
|
|
276
|
+
* not run.
|
|
277
|
+
*/
|
|
278
|
+
if (cards.length === 0 && !overall) return null;
|
|
279
|
+
return { cards, ...(overall ? { overall } : {}) };
|
|
280
|
+
}
|
package/bin/lib/claude.mjs
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
import { spawn } from 'node:child_process';
|
|
15
15
|
import { SAFE } from './config.mjs';
|
|
16
|
-
import { runtimeById, humanizeClaudeTool } from './runtimes.mjs';
|
|
16
|
+
import { runtimeById, humanizeClaudeTool, THINK_MARKER } from './runtimes.mjs';
|
|
17
17
|
|
|
18
18
|
// Every prompt/kickoff constant lives in prompts.mjs and is re-exported here:
|
|
19
19
|
// a dozen call sites import them from claude.mjs, and none of them care where
|
|
@@ -197,12 +197,39 @@ function handleStreamLine(line, { cwd, emit, onActivity, onToolEvent, appendText
|
|
|
197
197
|
if (ev.type === 'assistant' && Array.isArray(ev.message?.content)) {
|
|
198
198
|
for (const b of ev.message.content) {
|
|
199
199
|
if (b.type === 'thinking' || b.type === 'redacted_thinking') {
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
200
|
+
/**
|
|
201
|
+
* NO THINKING TEXT ARRIVES TODAY, AND THAT IS MEASURED (2026-09-16).
|
|
202
|
+
*
|
|
203
|
+
* This used to say the text is "usually redacted", which was a guess
|
|
204
|
+
* doing the work of a fact. The fact: across three real transcripts, 93
|
|
205
|
+
* thinking blocks, EVERY ONE of them carried an empty `thinking` — and
|
|
206
|
+
* a live probe with MAX_THINKING_TOKENS set and
|
|
207
|
+
* `--include-partial-messages` on returned an empty `thinking_delta`
|
|
208
|
+
* and a complete block of length zero (signature only). So the CLI
|
|
209
|
+
* emits the FACT that it reasoned and not a word of the reasoning, and
|
|
210
|
+
* "show the full thinking" cannot be conjured from this stream.
|
|
211
|
+
*
|
|
212
|
+
* THE BRANCH BELOW EXISTS ANYWAY, and deliberately. The shape is the
|
|
213
|
+
* whole point: when text is present it rides `full` UNCLIPPED, so the
|
|
214
|
+
* day a CLI release starts emitting it, the trace carries the thought
|
|
215
|
+
* whole with no daemon change and no version floor — the report's own
|
|
216
|
+
* presence is the capability. Until then every block takes the marker
|
|
217
|
+
* arm, and `trace.mjs` collapses a run of identical markers so the
|
|
218
|
+
* absence reads as one quiet step rather than forty.
|
|
219
|
+
*/
|
|
220
|
+
push(
|
|
221
|
+
b.thinking
|
|
222
|
+
? { kind: 'think', label: `thinking: ${oneLine(b.thinking)}`, full: b.thinking }
|
|
223
|
+
: { kind: 'think', label: THINK_MARKER }
|
|
224
|
+
);
|
|
203
225
|
} else if (b.type === 'text' && b.text?.trim()) {
|
|
204
226
|
if (!answerFromResult) appendText(b.text + '\n');
|
|
205
|
-
|
|
227
|
+
// `label` for the console and the pulse — one collapsed 160-char line,
|
|
228
|
+
// byte-identical to what it has always printed. `full` for the trace:
|
|
229
|
+
// a `say` is the agent NARRATING, several sentences at a time, and the
|
|
230
|
+
// clip at 160 was landing mid-sentence on the one thing a person opens
|
|
231
|
+
// the page to read.
|
|
232
|
+
push({ kind: 'say', label: oneLine(b.text), full: b.text });
|
|
206
233
|
} else if (b.type === 'tool_use') {
|
|
207
234
|
push(humanizeToolUse(b.name, b.input || {}, cwd));
|
|
208
235
|
// The STRUCTURED form of the same event, for the transcript's tool
|
package/bin/lib/fleet.mjs
CHANGED
|
@@ -82,6 +82,7 @@ import {
|
|
|
82
82
|
probeSkillsOnce,
|
|
83
83
|
recordSkills,
|
|
84
84
|
RUNTIMES,
|
|
85
|
+
THINK_MARKER,
|
|
85
86
|
} from './runtimes.mjs';
|
|
86
87
|
import { createWorkManager } from './work.mjs';
|
|
87
88
|
import { scanLocalSessions, ourConversationIds } from './localSessions.mjs';
|
|
@@ -1598,7 +1599,13 @@ export async function runFleetDaemon() {
|
|
|
1598
1599
|
pagesSeen.add(a.path || a.label);
|
|
1599
1600
|
}
|
|
1600
1601
|
// Collapse runs of bare "thinking…" so the feed doesn't fill with it.
|
|
1601
|
-
|
|
1602
|
+
// Keyed on the SHARED constant (runtimes.mjs), not on the literal: the
|
|
1603
|
+
// labels being compared here are the ones claude.mjs now builds from
|
|
1604
|
+
// that constant, so a reworded marker would leave this comparison
|
|
1605
|
+
// matching nothing and the 48-slot feed filling with the repeat — the
|
|
1606
|
+
// exact noise this line exists to stop, and silent, because a collapse
|
|
1607
|
+
// that stops collapsing fails no test.
|
|
1608
|
+
if (!(a.label === THINK_MARKER && feed[feed.length - 1] === THINK_MARKER)) {
|
|
1602
1609
|
feed.push(a.label);
|
|
1603
1610
|
if (feed.length > 48) feed.shift();
|
|
1604
1611
|
}
|
package/bin/lib/prompts.mjs
CHANGED
|
@@ -588,6 +588,35 @@ export const REGROUND_KICKOFF = ({ sha, title, files, vaultDir, predictedPages =
|
|
|
588
588
|
* TOUCHED. Everything else — how long something will take, who should own it,
|
|
589
589
|
* whether it is a good idea — is not a question a planner can answer from a
|
|
590
590
|
* repository, and asking for it produces confident invention.
|
|
591
|
+
*
|
|
592
|
+
* ── THERE IS NO WAY TO SAY "THIS ONE WAITS" (2026-09-16) ──
|
|
593
|
+
*
|
|
594
|
+
* The answer schema used to carry `waitsOn`, a list of tempIds an agent had to
|
|
595
|
+
* see MERGE before it could start, and the owner deleted the idea outright:
|
|
596
|
+
*
|
|
597
|
+
* "for the planning agent, whats the point of dividing up the agents if one
|
|
598
|
+
* of the agents rely on waiting for one to finish? if thats the case have it
|
|
599
|
+
* be in the same agent. the point of having it divide into various agent is
|
|
600
|
+
* so that its capable of parallel and simultaneous work."
|
|
601
|
+
*
|
|
602
|
+
* That is not a preference about a field, it is what an agent IS. An agent is
|
|
603
|
+
* one CLI in one worktree working its cards IN ORDER — ordering is the thing it
|
|
604
|
+
* already does, for free, with no branch to merge in between. So a planner that
|
|
605
|
+
* splits a chain across two agents has bought nothing and paid twice: a second
|
|
606
|
+
* worktree, a second branch, a second review, and a second agent sitting idle
|
|
607
|
+
* until the first one lands. The ONLY thing a split buys is two CLIs typing at
|
|
608
|
+
* the same time, and work that waits cannot do that by definition.
|
|
609
|
+
*
|
|
610
|
+
* So the vocabulary is gone rather than discouraged. A rule the model can still
|
|
611
|
+
* express a violation of is a rule it will sometimes express a violation of;
|
|
612
|
+
* removing the key removes the move. Rule 1 below carries the reasoning in the
|
|
613
|
+
* planner's own terms, and `agentPlan.mjs` drops the key if a model on an older
|
|
614
|
+
* prompt sends it anyway.
|
|
615
|
+
*
|
|
616
|
+
* WHAT DID NOT CHANGE, on purpose: the SERVER still accepts `waitsOn` on the
|
|
617
|
+
* wire and still honours it on stored proposals, because 0.86.0 daemons are
|
|
618
|
+
* still running and agents created under the old prompt still exist. This is a
|
|
619
|
+
* change to what is PROPOSED, not to what can be read.
|
|
591
620
|
*/
|
|
592
621
|
export const SYSTEM_PLAN = `You are the human's own Claude, planning a batch of work in their repository.
|
|
593
622
|
|
|
@@ -602,8 +631,13 @@ as a single reviewable branch.
|
|
|
602
631
|
THE RULES THAT MATTER:
|
|
603
632
|
|
|
604
633
|
1. SEQUENTIAL WORK BELONGS IN ONE AGENT. If B needs A's code to exist, put them
|
|
605
|
-
in the same agent, A first
|
|
606
|
-
|
|
634
|
+
in the same agent, A first — an agent works its cards in the order you give,
|
|
635
|
+
so ordering is free and costs no merge in between. THE ONLY REASON TO SPLIT
|
|
636
|
+
IS WORK THAT CAN RUN AT THE SAME TIME. There is no way to say that one agent
|
|
637
|
+
waits for another, and that is deliberate: an agent that has to wait is a
|
|
638
|
+
split done wrong. Splitting a chain buys a second worktree, a second branch
|
|
639
|
+
and a second review, and the second agent sits idle until the first one
|
|
640
|
+
merges — slower than one agent doing both in order.
|
|
607
641
|
|
|
608
642
|
2. SPLIT ONLY WHAT CAN GENUINELY RUN AT THE SAME TIME. Two agents editing the
|
|
609
643
|
same files land two branches that conflict, and somebody resolves it by hand.
|
|
@@ -641,7 +675,6 @@ after it. Wrap it in a \`\`\`json fence:
|
|
|
641
675
|
"name": "auth",
|
|
642
676
|
"taskIds": ["<card id>", "<card id>"],
|
|
643
677
|
"pointsBudget": 8,
|
|
644
|
-
"waitsOn": [],
|
|
645
678
|
"intoAgentId": null
|
|
646
679
|
}
|
|
647
680
|
]
|
|
@@ -649,7 +682,7 @@ after it. Wrap it in a \`\`\`json fence:
|
|
|
649
682
|
\`\`\`
|
|
650
683
|
|
|
651
684
|
Every selected card id must appear EXACTLY ONCE across all agents. Use the ids
|
|
652
|
-
exactly as given
|
|
685
|
+
exactly as given.`;
|
|
653
686
|
|
|
654
687
|
/**
|
|
655
688
|
* The planner's turn.
|
|
@@ -797,13 +830,25 @@ const safeName = (n) =>
|
|
|
797
830
|
.trim()
|
|
798
831
|
.slice(0, 60) || 'agent';
|
|
799
832
|
|
|
800
|
-
|
|
833
|
+
/**
|
|
834
|
+
* A CARD'S SPEC, WRITTEN DOWN ONCE.
|
|
835
|
+
*
|
|
836
|
+
* EXPORTED (2026-09-16) because a second reader now needs the identical text:
|
|
837
|
+
* the AI pre-review is composed from the card specs the daemon STASHED as it
|
|
838
|
+
* typed each turn's prompt, and the whole claim of that surface is that the
|
|
839
|
+
* reviewer read what the agent read. Two builders for one thing is two
|
|
840
|
+
* renderings of a card that can drift — and the drift would be invisible,
|
|
841
|
+
* because nobody reads both prompts side by side.
|
|
842
|
+
*/
|
|
843
|
+
export const AGENT_TASK_SPEC = (task) =>
|
|
801
844
|
`id: ${task?.id ?? ''}\n` +
|
|
802
845
|
`title: ${task?.title ?? ''}\n` +
|
|
803
846
|
(task?.brief ? `\nbrief:\n${task.brief}\n` : '') +
|
|
804
847
|
(task?.criteria?.length ? `\ndone when:\n${task.criteria.map((c) => `- ${c}`).join('\n')}\n` : '') +
|
|
805
848
|
(task?.anchors?.length ? `\nthis card owns:\n${task.anchors.map((a) => `- ${a}`).join('\n')}\n` : '');
|
|
806
849
|
|
|
850
|
+
const taskBlock = AGENT_TASK_SPEC;
|
|
851
|
+
|
|
807
852
|
/**
|
|
808
853
|
* A PERSON SPOKE TO THE AGENT — usually the answer to its own question.
|
|
809
854
|
*
|
|
@@ -819,3 +864,126 @@ export const AGENT_HUMAN_KICKOFF = ({ agentName, message, askedByName, task, pos
|
|
|
819
864
|
`${fence('WHAT THEY SAID', message)}\n\n` +
|
|
820
865
|
(task ? `${fence('THE CARD YOU ARE ON', taskBlock(task))}\n\n` : '') +
|
|
821
866
|
`Carry on, and end with the JSON object as usual.`;
|
|
867
|
+
|
|
868
|
+
/**
|
|
869
|
+
* THE AI PRE-REVIEW — a FRESH Claude reads the branch before the human does
|
|
870
|
+
* (2026-09-16).
|
|
871
|
+
*
|
|
872
|
+
* The owner asked for it in these words: "before having the user manually check,
|
|
873
|
+
* can we have the daemon … spawn an agent to review the work so basically we get
|
|
874
|
+
* an ai to look at the review before a human looks at it for a double check."
|
|
875
|
+
*
|
|
876
|
+
* ── FRESH EYES, AND THAT IS THE ENTIRE DESIGN ──
|
|
877
|
+
*
|
|
878
|
+
* This is NOT the agent's own conversation asked to check itself. An agent that
|
|
879
|
+
* has spent four turns arguing itself into a design defends that design; asked
|
|
880
|
+
* whether its work meets the card, it answers from the same context that
|
|
881
|
+
* produced the work and finds it good. So the precheck is a NEW `claude -p` with
|
|
882
|
+
* no resumed conversation, standing in the agent's worktree because it needs the
|
|
883
|
+
* code and the diff, under the READ-ONLY profile the scratch planner and the
|
|
884
|
+
* capture chat already run behind, with no MCP at all. It reads; it cannot
|
|
885
|
+
* write; it has no control plane to reach even if the repository it is reading
|
|
886
|
+
* tries to steer it.
|
|
887
|
+
*
|
|
888
|
+
* ── IT LABELS AND NEVER BLOCKS ──
|
|
889
|
+
*
|
|
890
|
+
* The project's own check states this law and this obeys it identically:
|
|
891
|
+
* Approve, the verdicts and the ship quiz do not know this exists. A precheck
|
|
892
|
+
* that failed, timed out, or was never run posts NOTHING, and the absence
|
|
893
|
+
* renders nothing — ignorance never withholds a human's review, the same
|
|
894
|
+
* three-state rule every readout in this product keeps.
|
|
895
|
+
*
|
|
896
|
+
* ── AND IT IS ASKED FOR A TRIAGE, NOT A VERDICT ──
|
|
897
|
+
*
|
|
898
|
+
* The one thing a second reader can do that the first cannot is say WHERE TO
|
|
899
|
+
* LOOK FIRST. Asked to approve or reject, a model produces a confident judgment
|
|
900
|
+
* nobody asked it for and somebody will eventually treat as one. Asked what a
|
|
901
|
+
* reviewer should check first, it produces a list of places — which is useful
|
|
902
|
+
* whether it is right or wrong, because the human is about to look anyway.
|
|
903
|
+
*/
|
|
904
|
+
export const SYSTEM_PRECHECK = `You are a SECOND reviewer with fresh eyes, reading a branch an agent has just
|
|
905
|
+
finished. You did not write this code and you were not in the conversation that
|
|
906
|
+
produced it. That is the whole point of you.
|
|
907
|
+
|
|
908
|
+
A PERSON REVIEWS THIS NEXT, and your job is to tell them what to look at first.
|
|
909
|
+
You are not approving or rejecting anything: nothing you say gates the merge,
|
|
910
|
+
nothing you say is shown to the agent, and nobody is waiting on a decision from
|
|
911
|
+
you.
|
|
912
|
+
|
|
913
|
+
YOU ARE READ-ONLY. You cannot write, edit or create files, and you have no tools
|
|
914
|
+
beyond reading this repository. Do not try.
|
|
915
|
+
|
|
916
|
+
HOW TO READ IT:
|
|
917
|
+
|
|
918
|
+
1. READ THE DIFF. A commit message is a CLAIM about the work; the diff is the
|
|
919
|
+
work. Run the diff command you are given and read what actually changed
|
|
920
|
+
before you say anything about it.
|
|
921
|
+
2. VERIFY EACH CARD AGAINST ITS OWN ACCEPTANCE CRITERIA. For every card you are
|
|
922
|
+
given, decide FROM THE DIFF whether what was asked for is actually there.
|
|
923
|
+
"ok" means you looked and found nothing a reviewer needs warning about.
|
|
924
|
+
"concerns" means there is something specific you would want them to check
|
|
925
|
+
first.
|
|
926
|
+
3. BE SPECIFIC OR SAY NOTHING. "Looks reasonable" helps nobody. A concern names
|
|
927
|
+
a file, a function or a behaviour and says what about it worries you. If you
|
|
928
|
+
cannot point at something, the verdict is "ok".
|
|
929
|
+
4. NEVER INVENT. If a card's spec was not given to you, judge it from the diff
|
|
930
|
+
and the commits and SAY in your note what you could not check it against.
|
|
931
|
+
Never assume a file exists, a test passes, or a criterion was met because a
|
|
932
|
+
commit message says so.
|
|
933
|
+
5. YOU ARE NOT A STYLE GUIDE. Correctness, missing pieces, things the criteria
|
|
934
|
+
asked for that the diff does not show, changes that reach further than the
|
|
935
|
+
card did. Not formatting, not naming preferences, not the rewrite you would
|
|
936
|
+
have preferred.
|
|
937
|
+
|
|
938
|
+
END YOUR TURN WITH ONE JSON OBJECT AND NOTHING AFTER IT, in a \`\`\`json fence:
|
|
939
|
+
|
|
940
|
+
\`\`\`json
|
|
941
|
+
{
|
|
942
|
+
"cards": [
|
|
943
|
+
{ "taskId": "<card id, exactly as given>", "verdict": "ok", "note": "" },
|
|
944
|
+
{ "taskId": "<card id, exactly as given>", "verdict": "concerns", "note": "what a reviewer should look at first, and why" }
|
|
945
|
+
],
|
|
946
|
+
"overall": "what you would tell the reviewer before they start reading"
|
|
947
|
+
}
|
|
948
|
+
\`\`\`
|
|
949
|
+
|
|
950
|
+
One entry per card, AT MOST ONE note each, and use the card ids exactly as
|
|
951
|
+
given. A note is at most 400 characters and "overall" at most 1200: you are
|
|
952
|
+
writing the first paragraph of somebody's review, not the review.`;
|
|
953
|
+
|
|
954
|
+
/**
|
|
955
|
+
* The precheck's turn.
|
|
956
|
+
*
|
|
957
|
+
* THE CARDS AND THE COMMITS ARE FENCED. A card's title, brief and criteria are
|
|
958
|
+
* written by whoever files cards in this project; a commit subject is written by
|
|
959
|
+
* a model that has just been editing files. Both are untrusted content this turn
|
|
960
|
+
* reads, and the instruction that matters — "read the diff and triage it" — is
|
|
961
|
+
* ours and sits outside the fence.
|
|
962
|
+
*
|
|
963
|
+
* `missingSpecs` IS MEASURED, NOT GUESSED. A box that adopted this agent
|
|
964
|
+
* mid-run (the machine moved, or an older turn ran elsewhere) holds only the
|
|
965
|
+
* prompts IT typed, so the stash can be short of what the branch carries. The
|
|
966
|
+
* count is the difference between the `Flowviant-Task:` trailers on the branch
|
|
967
|
+
* and the specs on this disk — so the reviewer is told what it could not read
|
|
968
|
+
* rather than being handed a silent gap, and never told a card exists that
|
|
969
|
+
* nothing measured.
|
|
970
|
+
*/
|
|
971
|
+
export const AGENT_PRECHECK_KICKOFF = ({
|
|
972
|
+
agentName,
|
|
973
|
+
cards,
|
|
974
|
+
missingSpecs = 0,
|
|
975
|
+
commits,
|
|
976
|
+
diffCommand,
|
|
977
|
+
}) =>
|
|
978
|
+
`The agent "${safeName(agentName)}" has finished its queue. A person is about to ` +
|
|
979
|
+
`review this branch; you are reading it first.\n\n` +
|
|
980
|
+
`${fence('THE CARDS IT WAS GIVEN', cards || '(none of this branch’s card specs are on this machine)')}\n\n` +
|
|
981
|
+
(missingSpecs > 0
|
|
982
|
+
? `${missingSpecs} earlier card${missingSpecs === 1 ? "'s spec is" : "s' specs are"} ` +
|
|
983
|
+
`not on this box — review ${missingSpecs === 1 ? 'it' : 'them'} from the diff and the ` +
|
|
984
|
+
`commits below, and say in your note what you could not check them against.\n\n`
|
|
985
|
+
: '') +
|
|
986
|
+
`${fence('THE COMMITS ON THIS BRANCH', commits || '(none)')}\n\n` +
|
|
987
|
+
`Read the diff yourself before you judge any of it:\n\n` +
|
|
988
|
+
` ${diffCommand}\n\n` +
|
|
989
|
+
`Then answer with the JSON object and nothing else.`;
|