flowviant 0.19.0 → 0.21.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/claude.mjs +193 -3
- package/bin/lib/config.mjs +1 -1
- package/bin/lib/fleet.mjs +216 -0
- package/package.json +1 -1
package/bin/lib/claude.mjs
CHANGED
|
@@ -77,6 +77,84 @@ export const SINGLE_RESUME =
|
|
|
77
77
|
'Resume your current task. Call get_blocker_resolution for the blocker you reported; ' +
|
|
78
78
|
'if resolved, apply the human’s answer and finish this one intent, then stop.';
|
|
79
79
|
|
|
80
|
+
// Wiki-gen turn: the local Claude READS the repo and writes the living code wiki
|
|
81
|
+
// via MCP. It never edits/commits code — the ONLY writes are emit_wiki_node calls.
|
|
82
|
+
export const SYSTEM_WIKI = `You are Flowviant's codebase cartographer, running FULLY AUTONOMOUSLY via the
|
|
83
|
+
"flowviant" MCP server. There is NO interactive user and NO terminal. You do NOT
|
|
84
|
+
write, edit, or commit code — you READ this repository and document it as a living
|
|
85
|
+
wiki by calling MCP tools.
|
|
86
|
+
|
|
87
|
+
Goal: map the WHOLE codebase into a graph of wiki nodes an engineer new to the
|
|
88
|
+
project could read to understand it. Explore the REAL files (Read, Grep, Glob, ls,
|
|
89
|
+
git) — never guess. Ground every claim in files you actually read.
|
|
90
|
+
|
|
91
|
+
Emit each node with emit_wiki_node. Cover, at least:
|
|
92
|
+
- ONE "overview" node (id: "overview") — what the product is, the big picture, how to run it.
|
|
93
|
+
- ONE "architecture" node (id: "architecture") — the major pieces, how they fit, the data flow.
|
|
94
|
+
- "schema" node(s) — the data model (DB tables / core types) when the repo has one.
|
|
95
|
+
- "module" nodes — ONE per significant area/package/subsystem, at the level a developer
|
|
96
|
+
thinks in (NOT one per file).
|
|
97
|
+
- "api" / "testing" / "adr" / "note" nodes where warranted (public API surface, how tests
|
|
98
|
+
run, notable decisions, cross-cutting flows).
|
|
99
|
+
|
|
100
|
+
For each node:
|
|
101
|
+
- id: a STABLE slug YOU choose ("overview", "architecture", "schema", "module:apps/web",
|
|
102
|
+
"api:rest", "note:auth-flow"). Reuse the SAME id to refine a node.
|
|
103
|
+
- title: human-readable.
|
|
104
|
+
- body: the markdown page an engineer would write after reading the code — purpose, key
|
|
105
|
+
files and what they do, important flows, gotchas. Link related nodes with [[their-id]].
|
|
106
|
+
- citations: the real repo-relative files the page draws from.
|
|
107
|
+
- filePaths: the files the node covers (module nodes especially).
|
|
108
|
+
- edges: links FROM this node to related node ids (targetId + kind ref|coupling|bridge).
|
|
109
|
+
- groundedAtSha: the commit you were told to ground to.
|
|
110
|
+
|
|
111
|
+
Judge significance YOURSELF: a big/important area gets its own node; trivial things fold
|
|
112
|
+
into a parent node's body. Do NOT emit a node per file.
|
|
113
|
+
|
|
114
|
+
When the whole codebase is mapped, call finish_wiki_generation ONCE with keepNodeIds =
|
|
115
|
+
EVERY id you emitted, then output exactly WIKI_DONE on its own line and stop.
|
|
116
|
+
|
|
117
|
+
Be efficient — this spends the user's Claude quota. Read broadly and sample enough to
|
|
118
|
+
document each area accurately; you needn't read every file. If a tool errors, retry a
|
|
119
|
+
couple of times, then move on — never stall waiting on a human.`;
|
|
120
|
+
|
|
121
|
+
export const WIKI_KICKOFF = (sha) =>
|
|
122
|
+
`Map this repository into the living code wiki now. Ground everything to commit ${sha}. ` +
|
|
123
|
+
`Read the real files, emit a node per significant area with emit_wiki_node, then call ` +
|
|
124
|
+
`finish_wiki_generation with all your node ids and output WIKI_DONE.`;
|
|
125
|
+
|
|
126
|
+
// Delivery re-ground turn: a feature just MERGED. Update only the touched wiki
|
|
127
|
+
// nodes + record a persistent feature-history node. INCREMENTAL — never a full
|
|
128
|
+
// rewrite, never finish_wiki_generation (that prunes; this only adds/updates).
|
|
129
|
+
export const SYSTEM_REGROUND = `You are Flowviant's codebase cartographer, running FULLY AUTONOMOUSLY via the
|
|
130
|
+
"flowviant" MCP server. There is NO interactive user and NO terminal. You do NOT
|
|
131
|
+
write, edit, or commit code — a feature just MERGED and you update the living code
|
|
132
|
+
wiki to reflect it, by calling MCP tools.
|
|
133
|
+
|
|
134
|
+
Steps:
|
|
135
|
+
1. Call list_wiki_nodes to see the current wiki (node ids + the files each covers).
|
|
136
|
+
2. For each existing node whose files OVERLAP the changed files, RE-READ that area's
|
|
137
|
+
real code and re-emit the node with emit_wiki_node using the SAME id (updating it
|
|
138
|
+
in place). Touch ONLY nodes the change actually affected — this is incremental.
|
|
139
|
+
If the change adds a genuinely new area with no node, emit a new one.
|
|
140
|
+
3. Emit ONE feature-history node recording what shipped: id "feature:<short-slug>",
|
|
141
|
+
kind "note", state "built", title = the feature, body = what it added and why
|
|
142
|
+
(a durable record), citations = the changed files, edges linking to the code
|
|
143
|
+
nodes it touched. state "built" makes it permanent — a future full sweep keeps it.
|
|
144
|
+
4. Do NOT call finish_wiki_generation — that is only for a full sweep and would
|
|
145
|
+
prune. Just emit, then output exactly REGROUND_DONE on its own line and stop.
|
|
146
|
+
|
|
147
|
+
Ground every claim in files you actually read. Be efficient — look only at the
|
|
148
|
+
changed area, not the whole repo; spend little quota.`;
|
|
149
|
+
|
|
150
|
+
export const REGROUND_KICKOFF = ({ sha, title, files }) =>
|
|
151
|
+
`A feature just merged. Re-ground the living wiki for it.\n\n` +
|
|
152
|
+
`Feature: ${title}\n` +
|
|
153
|
+
`Grounded commit: ${sha}\n` +
|
|
154
|
+
`Changed files:\n${files.map((f) => `- ${f}`).join('\n')}\n\n` +
|
|
155
|
+
`Follow your instructions: list_wiki_nodes, re-emit the touched nodes (same ids), ` +
|
|
156
|
+
`emit the feature-history node (state "built"), then output REGROUND_DONE.`;
|
|
157
|
+
|
|
80
158
|
// Unattended (default) skips prompts so the agent never stalls with no terminal;
|
|
81
159
|
// FLOWVIANT_SAFE=1 restricts to a curated toolset instead.
|
|
82
160
|
const PERM = SAFE
|
|
@@ -121,13 +199,88 @@ export function mcpConfigFor(token, mcpUrl) {
|
|
|
121
199
|
return { dir, path: p };
|
|
122
200
|
}
|
|
123
201
|
|
|
202
|
+
// Shorten an absolute tool path to a repo-relative one for legible output.
|
|
203
|
+
const shortPath = (p, cwd) => {
|
|
204
|
+
if (typeof p !== 'string') return '';
|
|
205
|
+
let s = p;
|
|
206
|
+
if (cwd && s.startsWith(cwd)) s = s.slice(cwd.length).replace(/^\/+/, '');
|
|
207
|
+
return s;
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
// Turn one Claude tool_use into a compact activity {kind, label}, or null for
|
|
211
|
+
// tools not worth surfacing. `kind:'read'` is what the file counter counts;
|
|
212
|
+
// an emit_wiki_node flips the phase to "writing". Used by wiki turns to stream
|
|
213
|
+
// exactly which files Claude is touching (daemon console + app cover).
|
|
214
|
+
export function humanizeToolUse(name, input = {}, cwd = '') {
|
|
215
|
+
switch (name) {
|
|
216
|
+
case 'Read':
|
|
217
|
+
return { kind: 'read', label: `read ${shortPath(input.file_path, cwd)}` };
|
|
218
|
+
case 'Grep':
|
|
219
|
+
return {
|
|
220
|
+
kind: 'search',
|
|
221
|
+
label: `grep ${JSON.stringify(input.pattern ?? '')}${input.path ? ` in ${shortPath(input.path, cwd)}` : ''}`,
|
|
222
|
+
};
|
|
223
|
+
case 'Glob':
|
|
224
|
+
return { kind: 'glob', label: `glob ${input.pattern ?? ''}` };
|
|
225
|
+
case 'LS':
|
|
226
|
+
return { kind: 'list', label: `ls ${shortPath(input.path ?? '.', cwd)}` };
|
|
227
|
+
case 'Bash':
|
|
228
|
+
return { kind: 'bash', label: `$ ${String(input.command ?? '').replace(/\s+/g, ' ').slice(0, 60)}` };
|
|
229
|
+
default:
|
|
230
|
+
if (typeof name !== 'string') return null;
|
|
231
|
+
if (name.includes('emit_wiki_node')) return { kind: 'write', label: `+ node ${input.id ?? ''}` };
|
|
232
|
+
if (name.includes('finish_wiki_generation')) return { kind: 'write', label: 'finalize wiki' };
|
|
233
|
+
if (name.includes('list_wiki_nodes')) return { kind: 'mcp', label: 'list wiki nodes' };
|
|
234
|
+
return null; // other tools: silent
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Parse ONE line of `--output-format stream-json` NDJSON. Pulls assistant text
|
|
239
|
+
// into `out` (so sentinel detection still works) and turns each tool_use into a
|
|
240
|
+
// streamed activity line. A line that isn't JSON (a stray warning) is treated as
|
|
241
|
+
// raw text so nothing is lost.
|
|
242
|
+
function handleStreamLine(line, { cwd, emit, onActivity, appendText }) {
|
|
243
|
+
let ev;
|
|
244
|
+
try {
|
|
245
|
+
ev = JSON.parse(line);
|
|
246
|
+
} catch {
|
|
247
|
+
appendText(line + '\n');
|
|
248
|
+
emit(line + '\n');
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
if (ev.type === 'assistant' && Array.isArray(ev.message?.content)) {
|
|
252
|
+
for (const b of ev.message.content) {
|
|
253
|
+
if (b.type === 'text' && b.text) appendText(b.text + '\n');
|
|
254
|
+
else if (b.type === 'tool_use') {
|
|
255
|
+
const a = humanizeToolUse(b.name, b.input || {}, cwd);
|
|
256
|
+
if (a) {
|
|
257
|
+
emit(a.label + '\n');
|
|
258
|
+
onActivity?.(a);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
} else if (ev.type === 'result' && typeof ev.result === 'string') {
|
|
263
|
+
// The final assistant text (carries WIKI_DONE / REGROUND_DONE).
|
|
264
|
+
appendText(ev.result + '\n');
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
124
268
|
// One Claude Code turn. Output is captured (for sentinel detection) and streamed
|
|
125
269
|
// through, line-prefixed with the worker label so a fleet stays legible.
|
|
126
|
-
|
|
270
|
+
//
|
|
271
|
+
// `streamJson` switches to `--output-format stream-json` and parses the event
|
|
272
|
+
// stream: only the humanized tool activity reaches the console (a legible
|
|
273
|
+
// stream of `read …`, `grep …`, `+ node …`), assistant text is folded into the
|
|
274
|
+
// returned string for sentinel detection, and each activity is handed to
|
|
275
|
+
// `onActivity` so the caller can forward progress. Build-agent turns leave it
|
|
276
|
+
// off and keep the raw text passthrough + line sentinels.
|
|
277
|
+
export function runTurn({ prompt, resume, system, cwd, mcpConfig, label, onSpawn, streamJson, onActivity }) {
|
|
127
278
|
return new Promise((resolve) => {
|
|
128
279
|
const args = [];
|
|
129
280
|
if (resume) args.push('--continue');
|
|
130
|
-
args.push('-p', prompt, '--mcp-config', mcpConfig, '--append-system-prompt', system
|
|
281
|
+
args.push('-p', prompt, '--mcp-config', mcpConfig, '--append-system-prompt', system);
|
|
282
|
+
if (streamJson) args.push('--output-format', 'stream-json', '--verbose');
|
|
283
|
+
args.push(...PERM);
|
|
131
284
|
// Force the user's Claude Code subscription — never the API. A key exported in
|
|
132
285
|
// the shell would otherwise silently bill every poll-mode turn as raw API
|
|
133
286
|
// usage (same invariant live mode enforces on its SDK session env).
|
|
@@ -138,10 +291,47 @@ export function runTurn({ prompt, resume, system, cwd, mcpConfig, label, onSpawn
|
|
|
138
291
|
onSpawn?.(child);
|
|
139
292
|
let out = '';
|
|
140
293
|
const pfx = label ? `${label} ` : '';
|
|
294
|
+
const emit = (s) => process.stdout.write(pfx ? s.replace(/\n/g, `\n${pfx}`) : s);
|
|
295
|
+
|
|
296
|
+
if (streamJson) {
|
|
297
|
+
let buf = '';
|
|
298
|
+
const appendText = (t) => {
|
|
299
|
+
out += t;
|
|
300
|
+
};
|
|
301
|
+
child.stdout.on('data', (d) => {
|
|
302
|
+
buf += d.toString();
|
|
303
|
+
let nl;
|
|
304
|
+
while ((nl = buf.indexOf('\n')) >= 0) {
|
|
305
|
+
const line = buf.slice(0, nl);
|
|
306
|
+
buf = buf.slice(nl + 1);
|
|
307
|
+
if (line.trim()) handleStreamLine(line, { cwd, emit, onActivity, appendText });
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
// stderr is not JSON (warnings/errors) — pass through and keep for sentinels.
|
|
311
|
+
child.stderr.on('data', (d) => {
|
|
312
|
+
const s = d.toString();
|
|
313
|
+
out += s;
|
|
314
|
+
emit(s);
|
|
315
|
+
});
|
|
316
|
+
child.on('error', (e) => {
|
|
317
|
+
if (e.code === 'ENOENT') {
|
|
318
|
+
console.error("\nerror: 'claude' CLI not found on PATH. Install Claude Code first.");
|
|
319
|
+
process.exit(1);
|
|
320
|
+
}
|
|
321
|
+
console.error(e);
|
|
322
|
+
resolve(out);
|
|
323
|
+
});
|
|
324
|
+
child.on('close', () => {
|
|
325
|
+
if (buf.trim()) handleStreamLine(buf, { cwd, emit, onActivity, appendText });
|
|
326
|
+
resolve(out);
|
|
327
|
+
});
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
|
|
141
331
|
const onChunk = (d) => {
|
|
142
332
|
const s = d.toString();
|
|
143
333
|
out += s;
|
|
144
|
-
|
|
334
|
+
emit(s);
|
|
145
335
|
};
|
|
146
336
|
child.stdout.on('data', onChunk);
|
|
147
337
|
child.stderr.on('data', onChunk);
|
package/bin/lib/config.mjs
CHANGED
|
@@ -4,7 +4,7 @@ import { readFileSync } from 'node:fs';
|
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
import { homedir } from 'node:os';
|
|
6
6
|
|
|
7
|
-
export const VERSION = '0.
|
|
7
|
+
export const VERSION = '0.21.0';
|
|
8
8
|
|
|
9
9
|
// Credential stored by `flowviant login` (device auth) — the no-token,
|
|
10
10
|
// no-env-var path. An explicit --fleet flag or FLOWVIANT_FLEET env still wins.
|
package/bin/lib/fleet.mjs
CHANGED
|
@@ -45,6 +45,10 @@ import {
|
|
|
45
45
|
SYSTEM_SINGLE,
|
|
46
46
|
SINGLE_KICKOFF,
|
|
47
47
|
SINGLE_RESUME,
|
|
48
|
+
SYSTEM_WIKI,
|
|
49
|
+
WIKI_KICKOFF,
|
|
50
|
+
SYSTEM_REGROUND,
|
|
51
|
+
REGROUND_KICKOFF,
|
|
48
52
|
} from './claude.mjs';
|
|
49
53
|
import { runLiveWorker } from './live.mjs';
|
|
50
54
|
import { reapOrphanPreviews } from './preview.mjs';
|
|
@@ -306,6 +310,12 @@ export async function runFleetDaemon() {
|
|
|
306
310
|
mergeAttempts.delete(job.id);
|
|
307
311
|
await reportMergeOutcome(MERGE_DONE_URL, { intentId: job.id });
|
|
308
312
|
ok(`${c.cyan('merged')} ${c.dim(`— ${job.title} → ${baseRef}`)}`);
|
|
313
|
+
// The code just landed — re-ground the living wiki for what shipped
|
|
314
|
+
// (touched nodes re-read + a persistent feature-history node).
|
|
315
|
+
// Direct enqueue = immediacy; the server's durable regroundJobs list
|
|
316
|
+
// (created by merge-done above, cleared by our reground-done report)
|
|
317
|
+
// is the restart-safe backstop — dedup'd here by groundedIntents.
|
|
318
|
+
enqueueReground(job.id, job.prUrl, job.title);
|
|
309
319
|
} else if (failedReason) {
|
|
310
320
|
// Report into the thread (server narrates + re-arms the merge
|
|
311
321
|
// button + notifies) — the job disappears from the roster.
|
|
@@ -379,6 +389,203 @@ export async function runFleetDaemon() {
|
|
|
379
389
|
}
|
|
380
390
|
};
|
|
381
391
|
|
|
392
|
+
// Living-wiki work runs ONE turn at a time in a dedicated worktree (off the
|
|
393
|
+
// agents' checkouts), via the emit_wiki_node MCP tool — never code edits. Two
|
|
394
|
+
// triggers enqueue: a Regenerate click (full SWEEP) and a successful merge
|
|
395
|
+
// (incremental RE-GROUND → feature-history). One queue + runner serializes
|
|
396
|
+
// them so they never collide on the worktree. Each turn runs under its own
|
|
397
|
+
// wiki-scoped credential (minted per task below) — the server allows ONLY the
|
|
398
|
+
// wiki tools on it, and build agents' worker tokens can't touch the map. Also
|
|
399
|
+
// means wiki work needs no agent online.
|
|
400
|
+
const wikiWt = join(baseDir, 'wiki');
|
|
401
|
+
const REGROUND_DONE_URL = FLEET_URL.replace(/\/agents\/?$/, '/reground-done');
|
|
402
|
+
const WIKI_TOKEN_URL = FLEET_URL.replace(/\/agents\/?$/, '/wiki-token');
|
|
403
|
+
const WIKI_PROGRESS_URL = FLEET_URL.replace(/\/agents\/?$/, '/wiki-progress');
|
|
404
|
+
const wikiQueue = [];
|
|
405
|
+
let wikiBusy = false;
|
|
406
|
+
let lastSweepAt = null; // dedup: run each Regenerate request once
|
|
407
|
+
const groundedIntents = new Set(); // dedup: re-ground each delivery once
|
|
408
|
+
|
|
409
|
+
const mintWikiToken = async () => {
|
|
410
|
+
try {
|
|
411
|
+
const res = await fetch(WIKI_TOKEN_URL, {
|
|
412
|
+
method: 'POST',
|
|
413
|
+
headers: { Authorization: `Bearer ${FLEET_TOKEN}`, 'User-Agent': USER_AGENT },
|
|
414
|
+
signal: AbortSignal.timeout(30_000),
|
|
415
|
+
});
|
|
416
|
+
if (!res.ok) return null;
|
|
417
|
+
return (await res.json())?.data?.token ?? null;
|
|
418
|
+
} catch {
|
|
419
|
+
return null;
|
|
420
|
+
}
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
// Stream what the wiki turn is doing to the app (the canvas renders the read
|
|
424
|
+
// phase). Throttled to ~1/s — the FIRST activity of a run and the terminal
|
|
425
|
+
// `done` frame force-send so the cover appears fast and clears cleanly.
|
|
426
|
+
let lastProgressAt = 0;
|
|
427
|
+
const postWikiProgress = async (body, force = false) => {
|
|
428
|
+
const now = Date.now();
|
|
429
|
+
if (!force && now - lastProgressAt < 1000) return;
|
|
430
|
+
lastProgressAt = now;
|
|
431
|
+
try {
|
|
432
|
+
await fetch(WIKI_PROGRESS_URL, {
|
|
433
|
+
method: 'POST',
|
|
434
|
+
headers: {
|
|
435
|
+
Authorization: `Bearer ${FLEET_TOKEN}`,
|
|
436
|
+
'User-Agent': USER_AGENT,
|
|
437
|
+
'Content-Type': 'application/json',
|
|
438
|
+
},
|
|
439
|
+
signal: AbortSignal.timeout(15_000),
|
|
440
|
+
body: JSON.stringify(body),
|
|
441
|
+
});
|
|
442
|
+
} catch {
|
|
443
|
+
/* best-effort — a dropped frame is harmless, the next one supersedes it */
|
|
444
|
+
}
|
|
445
|
+
};
|
|
446
|
+
|
|
447
|
+
const enqueueSweep = (job) => {
|
|
448
|
+
if (!job || job.requestedAt === lastSweepAt) return;
|
|
449
|
+
lastSweepAt = job.requestedAt;
|
|
450
|
+
wikiQueue.push({ type: 'sweep' });
|
|
451
|
+
void drainWiki();
|
|
452
|
+
};
|
|
453
|
+
const enqueueReground = (intentId, prUrl, title) => {
|
|
454
|
+
if (!intentId || groundedIntents.has(intentId)) return;
|
|
455
|
+
groundedIntents.add(intentId);
|
|
456
|
+
wikiQueue.push({ type: 'reground', intentId, prUrl, title: title || 'a delivered task' });
|
|
457
|
+
void drainWiki();
|
|
458
|
+
};
|
|
459
|
+
|
|
460
|
+
// Changed files of a (merged) PR, for the re-ground prompt. Capped so a huge
|
|
461
|
+
// PR can't blow up the prompt. prUrl was already validated before the merge.
|
|
462
|
+
const changedFilesForPr = (prUrl) => {
|
|
463
|
+
try {
|
|
464
|
+
const out = execFileSync('gh', ['pr', 'view', prUrl, '--json', 'files'], {
|
|
465
|
+
cwd: repoRoot,
|
|
466
|
+
encoding: 'utf8',
|
|
467
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
468
|
+
});
|
|
469
|
+
return (JSON.parse(out).files ?? []).map((f) => f.path).filter(Boolean).slice(0, 60);
|
|
470
|
+
} catch {
|
|
471
|
+
return [];
|
|
472
|
+
}
|
|
473
|
+
};
|
|
474
|
+
|
|
475
|
+
async function drainWiki() {
|
|
476
|
+
if (wikiBusy || wikiQueue.length === 0) return;
|
|
477
|
+
wikiBusy = true;
|
|
478
|
+
try {
|
|
479
|
+
while (wikiQueue.length) {
|
|
480
|
+
// Fresh wiki-scoped credential per task — dedicated, so no roster
|
|
481
|
+
// re-mint can rotate it out from under a long sweep.
|
|
482
|
+
const token = await mintWikiToken();
|
|
483
|
+
if (!token) {
|
|
484
|
+
warn('wiki: could not mint the cartographer token — retrying on a later poll');
|
|
485
|
+
break; // queue intact — the reconcile loop re-drains
|
|
486
|
+
}
|
|
487
|
+
const task = wikiQueue.shift();
|
|
488
|
+
const { dir, path: mcpConfig } = mcpConfigFor(token, mcpUrl);
|
|
489
|
+
// Live progress for this turn: count the files Claude reads, flip to the
|
|
490
|
+
// "writing" phase once it starts emitting nodes, and stream each action
|
|
491
|
+
// to the app (throttled). elapsedSec is on the daemon's own clock.
|
|
492
|
+
const mode = task.type === 'sweep' ? 'sweep' : 'reground';
|
|
493
|
+
const startedAt = Date.now();
|
|
494
|
+
let filesRead = 0;
|
|
495
|
+
let phase = 'reading';
|
|
496
|
+
const onActivity = (a) => {
|
|
497
|
+
if (a.kind === 'read') filesRead++;
|
|
498
|
+
if (a.kind === 'write') phase = 'writing';
|
|
499
|
+
void postWikiProgress({
|
|
500
|
+
mode,
|
|
501
|
+
phase,
|
|
502
|
+
activity: a.label,
|
|
503
|
+
filesRead,
|
|
504
|
+
elapsedSec: Math.round((Date.now() - startedAt) / 1000),
|
|
505
|
+
});
|
|
506
|
+
};
|
|
507
|
+
try {
|
|
508
|
+
if (!existsSync(wikiWt)) {
|
|
509
|
+
try {
|
|
510
|
+
git(['worktree', 'add', '--detach', wikiWt, baseRef], repoRoot);
|
|
511
|
+
} catch {
|
|
512
|
+
git(['worktree', 'prune'], repoRoot);
|
|
513
|
+
git(['worktree', 'add', '--detach', wikiWt, baseRef], repoRoot);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
resetWorktree(wikiWt, baseRef);
|
|
517
|
+
let sha = '';
|
|
518
|
+
try {
|
|
519
|
+
sha = git(['rev-parse', 'HEAD'], wikiWt);
|
|
520
|
+
} catch {
|
|
521
|
+
/* detached/no HEAD — still writes the map, just ungrounded */
|
|
522
|
+
}
|
|
523
|
+
if (task.type === 'sweep') {
|
|
524
|
+
note(`${c.cyan('wiki')} ${c.dim('— regenerating: your Claude is reading the repo…')}`);
|
|
525
|
+
const out = await runTurn({
|
|
526
|
+
prompt: WIKI_KICKOFF(sha),
|
|
527
|
+
resume: false,
|
|
528
|
+
system: SYSTEM_WIKI,
|
|
529
|
+
cwd: wikiWt,
|
|
530
|
+
mcpConfig,
|
|
531
|
+
label: c.cyan('[wiki]'),
|
|
532
|
+
streamJson: true,
|
|
533
|
+
onActivity,
|
|
534
|
+
});
|
|
535
|
+
if (sawSentinel(out, 'WIKI_DONE'))
|
|
536
|
+
ok(`${c.cyan('wiki')} ${c.dim('— regenerated from your code.')}`);
|
|
537
|
+
else
|
|
538
|
+
warn('code-wiki regeneration ended without WIKI_DONE — retry from the app if incomplete.');
|
|
539
|
+
} else {
|
|
540
|
+
const files = changedFilesForPr(task.prUrl);
|
|
541
|
+
if (files.length === 0) {
|
|
542
|
+
note(`${c.cyan('wiki')} ${c.dim(`— "${task.title}": no changed files to re-ground`)}`);
|
|
543
|
+
} else {
|
|
544
|
+
note(`${c.cyan('wiki')} ${c.dim(`— re-grounding after "${task.title}"…`)}`);
|
|
545
|
+
const out = await runTurn({
|
|
546
|
+
prompt: REGROUND_KICKOFF({ sha, title: task.title, files }),
|
|
547
|
+
resume: false,
|
|
548
|
+
system: SYSTEM_REGROUND,
|
|
549
|
+
cwd: wikiWt,
|
|
550
|
+
mcpConfig,
|
|
551
|
+
label: c.cyan('[wiki]'),
|
|
552
|
+
streamJson: true,
|
|
553
|
+
onActivity,
|
|
554
|
+
});
|
|
555
|
+
if (sawSentinel(out, 'REGROUND_DONE'))
|
|
556
|
+
ok(`${c.cyan('wiki')} ${c.dim(`— wiki updated for "${task.title}".`)}`);
|
|
557
|
+
else warn(`wiki re-ground for "${task.title}" ended without REGROUND_DONE.`);
|
|
558
|
+
}
|
|
559
|
+
// Consume the durable job: attempted = done (success or not — emits
|
|
560
|
+
// are idempotent and a failed turn heals on the next full sweep), so
|
|
561
|
+
// a failing re-ground can't loop-burn quota. Only a crash BEFORE
|
|
562
|
+
// this line leaves the job listed for a retry after restart.
|
|
563
|
+
await reportMergeOutcome(REGROUND_DONE_URL, { intentId: task.intentId });
|
|
564
|
+
}
|
|
565
|
+
} catch (e) {
|
|
566
|
+
warn(`wiki ${task.type} failed: ${e.message}`);
|
|
567
|
+
} finally {
|
|
568
|
+
// Terminal frame so the app cover clears promptly (don't wait for the
|
|
569
|
+
// freshness window to lapse). force-sent past the throttle.
|
|
570
|
+
await postWikiProgress(
|
|
571
|
+
{
|
|
572
|
+
mode,
|
|
573
|
+
phase,
|
|
574
|
+
activity: '',
|
|
575
|
+
filesRead,
|
|
576
|
+
elapsedSec: Math.round((Date.now() - startedAt) / 1000),
|
|
577
|
+
done: true,
|
|
578
|
+
},
|
|
579
|
+
true
|
|
580
|
+
);
|
|
581
|
+
rmSync(dir, { recursive: true, force: true });
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
} finally {
|
|
585
|
+
wikiBusy = false;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
382
589
|
let connected = false; // log the first successful poll once
|
|
383
590
|
let rosterSig = null; // last roster membership, to log changes only
|
|
384
591
|
let idleBeatAt = 0; // throttle the "still alive" idle heartbeat
|
|
@@ -511,6 +718,15 @@ export async function runFleetDaemon() {
|
|
|
511
718
|
}
|
|
512
719
|
}
|
|
513
720
|
|
|
721
|
+
// Living-wiki work (runs under its own minted wiki token — no agent
|
|
722
|
+
// needed). enqueueSweep queues a Regenerate; regroundJobs re-offers merged
|
|
723
|
+
// deliveries whose re-ground never ran (e.g. we restarted between merge and
|
|
724
|
+
// turn) until we report reground-done; the bare drain flushes anything
|
|
725
|
+
// whose earlier mint failed.
|
|
726
|
+
enqueueSweep(roster.codeMapJob);
|
|
727
|
+
for (const j of roster.regroundJobs ?? []) enqueueReground(j.intentId, j.prUrl, j.title);
|
|
728
|
+
void drainWiki();
|
|
729
|
+
|
|
514
730
|
// Stop workers whose agent left the roster (removed in the app).
|
|
515
731
|
for (const [id, w] of [...workers]) {
|
|
516
732
|
if (!rosterIds.has(id)) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "flowviant",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.0",
|
|
4
4
|
"description": "Run your own Claude Code as headless build agents for Flowviant — on your own credentials. Claims dispatched work, opens PRs, captures review evidence, and routes questions back to you.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|