flowviant 0.48.5 → 0.50.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/fleet.mjs +5 -0
- package/bin/lib/prompts.mjs +29 -10
- package/bin/lib/work.mjs +100 -0
- package/bin/lib/worktreeDiff.mjs +113 -1
- package/package.json +1 -1
package/bin/lib/fleet.mjs
CHANGED
|
@@ -550,6 +550,7 @@ export async function runFleetDaemon() {
|
|
|
550
550
|
flushWorkReports,
|
|
551
551
|
processWorkTurns,
|
|
552
552
|
processShipJobs,
|
|
553
|
+
processDiffJobs,
|
|
553
554
|
retireWorkSessions,
|
|
554
555
|
reportWorktrees,
|
|
555
556
|
shutdownWork,
|
|
@@ -1173,6 +1174,10 @@ export async function runFleetDaemon() {
|
|
|
1173
1174
|
// sessions are LIVE, and the guards above (chains, shipping) are populated
|
|
1174
1175
|
// by the intake this same tick.
|
|
1175
1176
|
retireWorkSessions(roster.activeWorkSessions);
|
|
1177
|
+
// Diffs somebody has open and is waiting on. Project-scoped rather than
|
|
1178
|
+
// per-session: `git show` runs from the repo ROOT, which can see a closed
|
|
1179
|
+
// tab's branch and a shipped commit on main alike.
|
|
1180
|
+
processDiffJobs(roster.diffJobs);
|
|
1176
1181
|
// …and what the SURVIVING ones hold: branch, ahead-of-base, diffstat.
|
|
1177
1182
|
// Throttled inside, never awaited — a `git status` the human cannot run
|
|
1178
1183
|
// themselves from a browser, relayed. After retirement so a directory that
|
package/bin/lib/prompts.mjs
CHANGED
|
@@ -254,14 +254,30 @@ THE LEDGER. This session's work is logged as CARDS as it happens, by you,
|
|
|
254
254
|
through tools — so a four-hour churn doesn't evaporate into scrollback. The
|
|
255
255
|
rules:
|
|
256
256
|
|
|
257
|
-
6.
|
|
258
|
-
list_cards, then
|
|
259
|
-
|
|
260
|
-
|
|
257
|
+
6. SAY WHAT YOU ARE DOING, on the card. When they say "take the auth card" or
|
|
258
|
+
"next", call list_cards, then log_work on the one they mean — that puts it on
|
|
259
|
+
their Working pile and their name on it. Call it AGAIN at real milestones: a
|
|
260
|
+
decision made, a hard part landed, a blocker found. One short line, past
|
|
261
|
+
tense, what a person scanning the card in a week needs. NOT every turn, and
|
|
262
|
+
NOT your reasoning — this tab's transcript is where prose goes; the card is
|
|
263
|
+
the record.
|
|
264
|
+
NOTHING REFUSES YOU. Several sessions may work one card and you may work
|
|
265
|
+
several cards; a teammate already on it is worth SAYING and is never a reason
|
|
266
|
+
to stop. drop_card when they change course.
|
|
267
|
+
7. PUT THE CARD IN YOUR COMMITS. Every commit for a card ends its message with a
|
|
268
|
+
trailer on its own line:
|
|
269
|
+
|
|
270
|
+
Flowviant-Task: <the card id>
|
|
271
|
+
|
|
272
|
+
That is how the card's Changes list, and its history, learn which commits
|
|
273
|
+
built it — from the machine, not from your memory. One trailer per card the
|
|
274
|
+
commit serves. A commit that belongs to no card needs none; shipping
|
|
275
|
+
reconciles those anyway.
|
|
276
|
+
8. LOG DRIFT, don't ask permission for it. "Also fix that redirect" mid-flow:
|
|
261
277
|
do the work, and file_card it — check list_cards FIRST; if a planned card
|
|
262
|
-
already covers it,
|
|
263
|
-
shippable unit. Never card-ify chatter, questions, or exploration.
|
|
264
|
-
|
|
278
|
+
already covers it, log_work against that one instead of filing a twin. One
|
|
279
|
+
card per shippable unit. Never card-ify chatter, questions, or exploration.
|
|
280
|
+
9. PLANNING HAPPENS HERE. When they arrive with something big — "build the
|
|
265
281
|
invite flow", "scaffold the admin area" — reading the code and breaking it
|
|
266
282
|
into cards is YOUR job, in this tab. There is no planning surface anywhere
|
|
267
283
|
else. Work it out with them in prose first; when the shape is settled, write
|
|
@@ -274,13 +290,13 @@ rules:
|
|
|
274
290
|
empty and nothing breaks — the forecast quietly falls back to a flat default
|
|
275
291
|
and the review has less to ask about. A card you have just designed is the
|
|
276
292
|
only moment anyone knows those answers.
|
|
277
|
-
|
|
293
|
+
10. DELIVER WITH RECEIPTS. When a card's work is committed, deliver_card with a
|
|
278
294
|
one-paragraph summary and the commit shas. Delivered is ASSERTED; done is
|
|
279
295
|
OBSERVED (the merge, on their word). Never claim done, and never deliver
|
|
280
296
|
work that isn't committed.
|
|
281
|
-
|
|
297
|
+
11. RAISE WHAT YOU SPOT. A design flaw, a follow-up they named for later —
|
|
282
298
|
raise_card, queued, unheld. You do not start raised work.
|
|
283
|
-
|
|
299
|
+
12. BE PROPORTIONAL. A one-line typo fix inside the card you are already on is
|
|
284
300
|
that card's work, not a new card. When in doubt, fewer cards. A plan is
|
|
285
301
|
slices somebody could pick up one at a time, not a work-breakdown
|
|
286
302
|
structure — if a card cannot be shipped on its own, it is not a card.
|
|
@@ -325,6 +341,9 @@ MECHANICS OF THIS TAB:
|
|
|
325
341
|
3. YOU HAVE NO FLOWVIANT TOOLS in this session — no cards, no ledger calls.
|
|
326
342
|
Don't mention or simulate them. Your commits ARE your record: when this
|
|
327
343
|
tab's branch ships, every commit is reconciled onto the project ledger.
|
|
344
|
+
If the human names a card id, put it in the commit message as a trailer on
|
|
345
|
+
its own line — \`Flowviant-Task: <id>\` — and that commit will show up on the
|
|
346
|
+
card without any tool call. It is the one ledger gesture available here.
|
|
328
347
|
4. NEVER merge to main, deploy, or force-push unless the human explicitly says
|
|
329
348
|
so in this conversation. Branch pushes are fine when asked. Shipping is
|
|
330
349
|
their word to say, not yours to infer.
|
package/bin/lib/work.mjs
CHANGED
|
@@ -91,6 +91,7 @@ export function createWorkManager({ repoRoot, baseDir, baseRef, getMcpUrl, getLe
|
|
|
91
91
|
const SHIP_DONE_URL = FLEET_URL.replace(/\/agents\/?$/, '/ship-done');
|
|
92
92
|
const ACTIVITY_URL = FLEET_URL.replace(/\/agents\/?$/, '/session-activity');
|
|
93
93
|
const WORKTREES_URL = FLEET_URL.replace(/\/agents\/?$/, '/session-worktrees');
|
|
94
|
+
const DIFF_DONE_URL = FLEET_URL.replace(/\/agents\/?$/, '/diff-done');
|
|
94
95
|
const ATTACHMENT_URL = FLEET_URL.replace(/\/agents\/?$/, '/attachment');
|
|
95
96
|
const workAnswering = new Set(); // turn ids currently queued/running here
|
|
96
97
|
const workAttempts = new Map(); // turn id -> completed runTurn attempts
|
|
@@ -366,6 +367,104 @@ export function createWorkManager({ repoRoot, baseDir, baseRef, getMcpUrl, getLe
|
|
|
366
367
|
})();
|
|
367
368
|
};
|
|
368
369
|
|
|
370
|
+
/**
|
|
371
|
+
* COMMIT DIFFS, on request — the one PULL-shaped thing this daemon does.
|
|
372
|
+
*
|
|
373
|
+
* Everything else here is a push: the machine knows something and says it.
|
|
374
|
+
* A patch cannot work that way, because most are never opened and pushing
|
|
375
|
+
* every one would be storage and bandwidth for nothing. So the server leaves
|
|
376
|
+
* a job on the roster and this drains it.
|
|
377
|
+
*
|
|
378
|
+
* RUN FROM THE REPO ROOT, never a session worktree. A tab can be closed and
|
|
379
|
+
* its directory gone, but `session/<id>` outlives the tab and after a ship
|
|
380
|
+
* the commit is on main — the root checkout can see all of it, and a
|
|
381
|
+
* worktree can see only its own branch.
|
|
382
|
+
*
|
|
383
|
+
* Every failure is REPORTED rather than swallowed: "no such commit on this
|
|
384
|
+
* machine" is a real answer, and a viewer spinning forever is the worst thing
|
|
385
|
+
* this can do. The one thing never sent is a guess — an empty patch would
|
|
386
|
+
* tell a reader the commit changed nothing.
|
|
387
|
+
*/
|
|
388
|
+
const MAX_PATCH_BYTES = 256 * 1024;
|
|
389
|
+
const servedDiffs = new Set();
|
|
390
|
+
const postDiff = async (body) => {
|
|
391
|
+
try {
|
|
392
|
+
await fetch(DIFF_DONE_URL, {
|
|
393
|
+
method: 'POST',
|
|
394
|
+
headers: {
|
|
395
|
+
Authorization: `Bearer ${FLEET_TOKEN}`,
|
|
396
|
+
'User-Agent': USER_AGENT,
|
|
397
|
+
'Content-Type': 'application/json',
|
|
398
|
+
},
|
|
399
|
+
signal: AbortSignal.timeout(30_000),
|
|
400
|
+
body: JSON.stringify(body),
|
|
401
|
+
});
|
|
402
|
+
} catch {
|
|
403
|
+
/* the row stays pending and expires; the next click re-requests */
|
|
404
|
+
}
|
|
405
|
+
};
|
|
406
|
+
const processDiffJobs = (jobs) => {
|
|
407
|
+
if (!Array.isArray(jobs) || jobs.length === 0) return;
|
|
408
|
+
for (const job of jobs.slice(0, 5)) {
|
|
409
|
+
const sha = String(job?.sha || '').toLowerCase();
|
|
410
|
+
// A sha becomes a `git` argument. The server validates too; one place
|
|
411
|
+
// doing this check is one deploy away from being zero places.
|
|
412
|
+
if (!/^[0-9a-f]{7,64}$/.test(sha)) continue;
|
|
413
|
+
// In-flight guard, not a cache: the server stops offering a sha the
|
|
414
|
+
// moment it settles, so this only stops the SAME poll's job being
|
|
415
|
+
// started twice while its `git show` is still running.
|
|
416
|
+
if (servedDiffs.has(sha)) continue;
|
|
417
|
+
servedDiffs.add(sha);
|
|
418
|
+
void (async () => {
|
|
419
|
+
try {
|
|
420
|
+
let patch = '';
|
|
421
|
+
const files = [];
|
|
422
|
+
try {
|
|
423
|
+
// `--format=` so the body is pure diff: the subject, author and
|
|
424
|
+
// date already reached the card on the worktree sweep, and
|
|
425
|
+
// repeating them inside the patch would put a second copy above
|
|
426
|
+
// every hunk.
|
|
427
|
+
patch = git(['show', '--patch', '--format=', sha], repoRoot);
|
|
428
|
+
} catch (e) {
|
|
429
|
+
await postDiff({ sha, error: String(e?.message || e).slice(0, 500) });
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
try {
|
|
433
|
+
const raw = git(['show', '--numstat', '--format=', sha], repoRoot);
|
|
434
|
+
for (const line of raw.split('\n')) {
|
|
435
|
+
if (!line.trim()) continue;
|
|
436
|
+
const [a, d, ...rest] = line.split('\t');
|
|
437
|
+
const path = rest.join('\t');
|
|
438
|
+
if (!path) continue;
|
|
439
|
+
const binary = a === '-' || d === '-';
|
|
440
|
+
files.push({
|
|
441
|
+
path: path.slice(0, 300),
|
|
442
|
+
added: binary ? 0 : Number(a) || 0,
|
|
443
|
+
deleted: binary ? 0 : Number(d) || 0,
|
|
444
|
+
...(binary ? { binary: true } : {}),
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
} catch {
|
|
448
|
+
/* a header without counts still beats no diff */
|
|
449
|
+
}
|
|
450
|
+
const truncated = Buffer.byteLength(patch, 'utf8') > MAX_PATCH_BYTES;
|
|
451
|
+
await postDiff({
|
|
452
|
+
sha,
|
|
453
|
+
// Cut on a LINE boundary: half a hunk header renders as garbage,
|
|
454
|
+
// and the viewer says the patch was truncated either way.
|
|
455
|
+
patch: truncated
|
|
456
|
+
? patch.slice(0, MAX_PATCH_BYTES).replace(/\n[^\n]*$/, '\n')
|
|
457
|
+
: patch,
|
|
458
|
+
files: files.slice(0, 200),
|
|
459
|
+
truncated,
|
|
460
|
+
});
|
|
461
|
+
} finally {
|
|
462
|
+
servedDiffs.delete(sha);
|
|
463
|
+
}
|
|
464
|
+
})();
|
|
465
|
+
}
|
|
466
|
+
};
|
|
467
|
+
|
|
369
468
|
/**
|
|
370
469
|
* FILES THE HUMAN ATTACHED, brought to where a CLI can read them.
|
|
371
470
|
*
|
|
@@ -1754,6 +1853,7 @@ export function createWorkManager({ repoRoot, baseDir, baseRef, getMcpUrl, getLe
|
|
|
1754
1853
|
flushWorkReports,
|
|
1755
1854
|
processWorkTurns,
|
|
1756
1855
|
processShipJobs,
|
|
1856
|
+
processDiffJobs,
|
|
1757
1857
|
retireWorkSessions,
|
|
1758
1858
|
reportWorktrees,
|
|
1759
1859
|
shutdownWork,
|
package/bin/lib/worktreeDiff.mjs
CHANGED
|
@@ -13,6 +13,12 @@
|
|
|
13
13
|
* uncommitted work in the same total. Untracked files count too: git calls them
|
|
14
14
|
* nothing until they are added, and a human calls them new work.
|
|
15
15
|
*
|
|
16
|
+
* It also carries THIS BRANCH'S OWN COMMITS and the cards they name — see
|
|
17
|
+
* `branchCommits` below. That rides here rather than on a route of its own
|
|
18
|
+
* because this sweep is already standing in the right directory on the right
|
|
19
|
+
* beat, and a second endpoint would be a second poll for a fact this one is
|
|
20
|
+
* next to.
|
|
21
|
+
*
|
|
16
22
|
* Everything here is best-effort and read-only. A worktree mid-rebase, a
|
|
17
23
|
* deleted directory, a file that vanished between listing and reading — each
|
|
18
24
|
* degrades to a smaller answer, never to a thrown error. Nothing about a
|
|
@@ -30,6 +36,109 @@ const MAX_FILES = 20;
|
|
|
30
36
|
const MAX_UNTRACKED_SCAN = 200;
|
|
31
37
|
/** Past this we call a file binary rather than counting its lines. */
|
|
32
38
|
const MAX_COUNT_BYTES = 512 * 1024;
|
|
39
|
+
/** Commits reported per sweep. The server caps each CARD at 50; this is the
|
|
40
|
+
* branch-wide bound, and a session branch past it is carrying an afternoon
|
|
41
|
+
* nobody is going to read commit-by-commit. */
|
|
42
|
+
const MAX_COMMITS = 50;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* WHICH CARDS A COMMIT NAMES.
|
|
46
|
+
*
|
|
47
|
+
* The convention is a git TRAILER — `Flowviant-Task: <id>` on its own line at
|
|
48
|
+
* the foot of the message, the same shape as `Co-authored-by:` and `Signed-off-
|
|
49
|
+
* by:`. Chosen over another MCP call for three reasons: it costs no write
|
|
50
|
+
* budget, it works on agy tabs which cannot mount MCP at all, and the commit IS
|
|
51
|
+
* the evidence rather than an assertion about it.
|
|
52
|
+
*
|
|
53
|
+
* Case-insensitive on the key and tolerant of several ids on one line, because
|
|
54
|
+
* a person or an agent writing this by hand will do both. Anything that is not
|
|
55
|
+
* a plausible id is dropped here rather than shipped: the server drops unknown
|
|
56
|
+
* ids too, but a readout should not spend a request on obvious noise.
|
|
57
|
+
*/
|
|
58
|
+
function taskIdsFromMessage(body) {
|
|
59
|
+
const ids = [];
|
|
60
|
+
for (const line of String(body || '').split('\n')) {
|
|
61
|
+
const m = line.match(/^\s*Flowviant-Task\s*:\s*(.+?)\s*$/i);
|
|
62
|
+
if (!m) continue;
|
|
63
|
+
for (const raw of m[1].split(/[\s,]+/)) {
|
|
64
|
+
const id = raw.replace(/^[#<]+|[>,.]+$/g, '');
|
|
65
|
+
if (id && id.length <= 64 && /^[A-Za-z0-9_-]+$/.test(id) && !ids.includes(id)) {
|
|
66
|
+
ids.push(id);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return ids;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* The commits this branch has that base does not, with the cards they name.
|
|
75
|
+
*
|
|
76
|
+
* Only commits carrying a trailer are RETURNED: an untrailered commit belongs
|
|
77
|
+
* to no card, and ship-time reconciliation already turns those into a bundle so
|
|
78
|
+
* nothing shipped is invisible. Sending them anyway would be a payload the
|
|
79
|
+
* server drops on every sweep.
|
|
80
|
+
*
|
|
81
|
+
* `--no-merges`, because a merge commit describes a range rather than doing
|
|
82
|
+
* work, and its trailer (if it has one) would double-book the range's own
|
|
83
|
+
* commits.
|
|
84
|
+
*/
|
|
85
|
+
function branchCommits(wt, base) {
|
|
86
|
+
if (!base) return [];
|
|
87
|
+
const out = [];
|
|
88
|
+
try {
|
|
89
|
+
// %x1e between records, %x1f between fields — a subject and a body can
|
|
90
|
+
// contain anything a person can type, so the delimiters must be bytes they
|
|
91
|
+
// cannot.
|
|
92
|
+
const raw = git(
|
|
93
|
+
[
|
|
94
|
+
'log',
|
|
95
|
+
'--no-merges',
|
|
96
|
+
'-n',
|
|
97
|
+
String(MAX_COMMITS),
|
|
98
|
+
'--format=%H%x1f%s%x1f%an%x1f%aI%x1f%B%x1e',
|
|
99
|
+
`${base}..HEAD`,
|
|
100
|
+
],
|
|
101
|
+
wt
|
|
102
|
+
);
|
|
103
|
+
for (const rec of raw.split('\x1e')) {
|
|
104
|
+
const line = rec.replace(/^\n+/, '');
|
|
105
|
+
if (!line.trim()) continue;
|
|
106
|
+
const [sha, subject, author, at, body] = line.split('\x1f');
|
|
107
|
+
if (!sha) continue;
|
|
108
|
+
const taskIds = taskIdsFromMessage(body);
|
|
109
|
+
if (taskIds.length === 0) continue;
|
|
110
|
+
let additions = 0;
|
|
111
|
+
let deletions = 0;
|
|
112
|
+
try {
|
|
113
|
+
const stat = git(['show', '--numstat', '--format=', sha], wt);
|
|
114
|
+
for (const l of stat.split('\n')) {
|
|
115
|
+
if (!l.trim()) continue;
|
|
116
|
+
const [a, d] = l.split('\t');
|
|
117
|
+
if (a === '-' || d === '-') continue; // binary
|
|
118
|
+
additions += Number(a) || 0;
|
|
119
|
+
deletions += Number(d) || 0;
|
|
120
|
+
}
|
|
121
|
+
} catch {
|
|
122
|
+
/* a commit we cannot stat still names its cards — send it anyway */
|
|
123
|
+
}
|
|
124
|
+
out.push({
|
|
125
|
+
// Clamped to the server's zod caps, same rule as everything else in
|
|
126
|
+
// this file: one over-cap string 400s the whole batch.
|
|
127
|
+
sha: sha.slice(0, 64),
|
|
128
|
+
subject: (subject ?? '').slice(0, 200),
|
|
129
|
+
author: (author ?? '').slice(0, 80),
|
|
130
|
+
at: (at ?? '').slice(0, 40),
|
|
131
|
+
additions,
|
|
132
|
+
deletions,
|
|
133
|
+
taskIds: taskIds.slice(0, 8),
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
} catch {
|
|
137
|
+
/* no base, unborn branch, or a repo mid-rebase — report no commits */
|
|
138
|
+
}
|
|
139
|
+
// Oldest first, so a card's list reads in the order the work happened.
|
|
140
|
+
return out.reverse();
|
|
141
|
+
}
|
|
33
142
|
|
|
34
143
|
/** Lines in a buffer, the way a diff counts them: a trailing newline does not
|
|
35
144
|
* add a line, and a NUL byte anywhere means we are not looking at text. */
|
|
@@ -48,7 +157,9 @@ function countLines(buf) {
|
|
|
48
157
|
* baseLabel:string, baseCommits:{sha:string, subject:string, author:string}[],
|
|
49
158
|
* dirty:boolean, additions:number, deletions:number, fileCount:number,
|
|
50
159
|
* truncated:number,
|
|
51
|
-
* files:{path:string, added:number, deleted:number, binary?:boolean}[]
|
|
160
|
+
* files:{path:string, added:number, deleted:number, binary?:boolean}[],
|
|
161
|
+
* commits:{sha:string, subject:string, author:string, at:string,
|
|
162
|
+
* additions:number, deletions:number, taskIds:string[]}[]}}
|
|
52
163
|
*/
|
|
53
164
|
export function worktreeDiff(wt, baseRef) {
|
|
54
165
|
if (!wt || !existsSync(wt)) return null;
|
|
@@ -189,5 +300,6 @@ export function worktreeDiff(wt, baseRef) {
|
|
|
189
300
|
fileCount: files.length,
|
|
190
301
|
truncated: Math.max(0, files.length - MAX_FILES),
|
|
191
302
|
files: files.slice(0, MAX_FILES),
|
|
303
|
+
commits: branchCommits(wt, base),
|
|
192
304
|
};
|
|
193
305
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "flowviant",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.50.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": {
|