flowviant 0.19.0 → 0.20.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 +78 -0
- package/bin/lib/config.mjs +1 -1
- package/bin/lib/fleet.mjs +156 -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
|
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.20.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,143 @@ 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 wikiQueue = [];
|
|
404
|
+
let wikiBusy = false;
|
|
405
|
+
let lastSweepAt = null; // dedup: run each Regenerate request once
|
|
406
|
+
const groundedIntents = new Set(); // dedup: re-ground each delivery once
|
|
407
|
+
|
|
408
|
+
const mintWikiToken = async () => {
|
|
409
|
+
try {
|
|
410
|
+
const res = await fetch(WIKI_TOKEN_URL, {
|
|
411
|
+
method: 'POST',
|
|
412
|
+
headers: { Authorization: `Bearer ${FLEET_TOKEN}`, 'User-Agent': USER_AGENT },
|
|
413
|
+
signal: AbortSignal.timeout(30_000),
|
|
414
|
+
});
|
|
415
|
+
if (!res.ok) return null;
|
|
416
|
+
return (await res.json())?.data?.token ?? null;
|
|
417
|
+
} catch {
|
|
418
|
+
return null;
|
|
419
|
+
}
|
|
420
|
+
};
|
|
421
|
+
|
|
422
|
+
const enqueueSweep = (job) => {
|
|
423
|
+
if (!job || job.requestedAt === lastSweepAt) return;
|
|
424
|
+
lastSweepAt = job.requestedAt;
|
|
425
|
+
wikiQueue.push({ type: 'sweep' });
|
|
426
|
+
void drainWiki();
|
|
427
|
+
};
|
|
428
|
+
const enqueueReground = (intentId, prUrl, title) => {
|
|
429
|
+
if (!intentId || groundedIntents.has(intentId)) return;
|
|
430
|
+
groundedIntents.add(intentId);
|
|
431
|
+
wikiQueue.push({ type: 'reground', intentId, prUrl, title: title || 'a delivered task' });
|
|
432
|
+
void drainWiki();
|
|
433
|
+
};
|
|
434
|
+
|
|
435
|
+
// Changed files of a (merged) PR, for the re-ground prompt. Capped so a huge
|
|
436
|
+
// PR can't blow up the prompt. prUrl was already validated before the merge.
|
|
437
|
+
const changedFilesForPr = (prUrl) => {
|
|
438
|
+
try {
|
|
439
|
+
const out = execFileSync('gh', ['pr', 'view', prUrl, '--json', 'files'], {
|
|
440
|
+
cwd: repoRoot,
|
|
441
|
+
encoding: 'utf8',
|
|
442
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
443
|
+
});
|
|
444
|
+
return (JSON.parse(out).files ?? []).map((f) => f.path).filter(Boolean).slice(0, 60);
|
|
445
|
+
} catch {
|
|
446
|
+
return [];
|
|
447
|
+
}
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
async function drainWiki() {
|
|
451
|
+
if (wikiBusy || wikiQueue.length === 0) return;
|
|
452
|
+
wikiBusy = true;
|
|
453
|
+
try {
|
|
454
|
+
while (wikiQueue.length) {
|
|
455
|
+
// Fresh wiki-scoped credential per task — dedicated, so no roster
|
|
456
|
+
// re-mint can rotate it out from under a long sweep.
|
|
457
|
+
const token = await mintWikiToken();
|
|
458
|
+
if (!token) {
|
|
459
|
+
warn('wiki: could not mint the cartographer token — retrying on a later poll');
|
|
460
|
+
break; // queue intact — the reconcile loop re-drains
|
|
461
|
+
}
|
|
462
|
+
const task = wikiQueue.shift();
|
|
463
|
+
const { dir, path: mcpConfig } = mcpConfigFor(token, mcpUrl);
|
|
464
|
+
try {
|
|
465
|
+
if (!existsSync(wikiWt)) {
|
|
466
|
+
try {
|
|
467
|
+
git(['worktree', 'add', '--detach', wikiWt, baseRef], repoRoot);
|
|
468
|
+
} catch {
|
|
469
|
+
git(['worktree', 'prune'], repoRoot);
|
|
470
|
+
git(['worktree', 'add', '--detach', wikiWt, baseRef], repoRoot);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
resetWorktree(wikiWt, baseRef);
|
|
474
|
+
let sha = '';
|
|
475
|
+
try {
|
|
476
|
+
sha = git(['rev-parse', 'HEAD'], wikiWt);
|
|
477
|
+
} catch {
|
|
478
|
+
/* detached/no HEAD — still writes the map, just ungrounded */
|
|
479
|
+
}
|
|
480
|
+
if (task.type === 'sweep') {
|
|
481
|
+
note(`${c.cyan('wiki')} ${c.dim('— regenerating: your Claude is reading the repo…')}`);
|
|
482
|
+
const out = await runTurn({
|
|
483
|
+
prompt: WIKI_KICKOFF(sha),
|
|
484
|
+
resume: false,
|
|
485
|
+
system: SYSTEM_WIKI,
|
|
486
|
+
cwd: wikiWt,
|
|
487
|
+
mcpConfig,
|
|
488
|
+
label: c.cyan('[wiki]'),
|
|
489
|
+
});
|
|
490
|
+
if (sawSentinel(out, 'WIKI_DONE'))
|
|
491
|
+
ok(`${c.cyan('wiki')} ${c.dim('— regenerated from your code.')}`);
|
|
492
|
+
else
|
|
493
|
+
warn('code-wiki regeneration ended without WIKI_DONE — retry from the app if incomplete.');
|
|
494
|
+
} else {
|
|
495
|
+
const files = changedFilesForPr(task.prUrl);
|
|
496
|
+
if (files.length === 0) {
|
|
497
|
+
note(`${c.cyan('wiki')} ${c.dim(`— "${task.title}": no changed files to re-ground`)}`);
|
|
498
|
+
} else {
|
|
499
|
+
note(`${c.cyan('wiki')} ${c.dim(`— re-grounding after "${task.title}"…`)}`);
|
|
500
|
+
const out = await runTurn({
|
|
501
|
+
prompt: REGROUND_KICKOFF({ sha, title: task.title, files }),
|
|
502
|
+
resume: false,
|
|
503
|
+
system: SYSTEM_REGROUND,
|
|
504
|
+
cwd: wikiWt,
|
|
505
|
+
mcpConfig,
|
|
506
|
+
label: c.cyan('[wiki]'),
|
|
507
|
+
});
|
|
508
|
+
if (sawSentinel(out, 'REGROUND_DONE'))
|
|
509
|
+
ok(`${c.cyan('wiki')} ${c.dim(`— wiki updated for "${task.title}".`)}`);
|
|
510
|
+
else warn(`wiki re-ground for "${task.title}" ended without REGROUND_DONE.`);
|
|
511
|
+
}
|
|
512
|
+
// Consume the durable job: attempted = done (success or not — emits
|
|
513
|
+
// are idempotent and a failed turn heals on the next full sweep), so
|
|
514
|
+
// a failing re-ground can't loop-burn quota. Only a crash BEFORE
|
|
515
|
+
// this line leaves the job listed for a retry after restart.
|
|
516
|
+
await reportMergeOutcome(REGROUND_DONE_URL, { intentId: task.intentId });
|
|
517
|
+
}
|
|
518
|
+
} catch (e) {
|
|
519
|
+
warn(`wiki ${task.type} failed: ${e.message}`);
|
|
520
|
+
} finally {
|
|
521
|
+
rmSync(dir, { recursive: true, force: true });
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
} finally {
|
|
525
|
+
wikiBusy = false;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
382
529
|
let connected = false; // log the first successful poll once
|
|
383
530
|
let rosterSig = null; // last roster membership, to log changes only
|
|
384
531
|
let idleBeatAt = 0; // throttle the "still alive" idle heartbeat
|
|
@@ -511,6 +658,15 @@ export async function runFleetDaemon() {
|
|
|
511
658
|
}
|
|
512
659
|
}
|
|
513
660
|
|
|
661
|
+
// Living-wiki work (runs under its own minted wiki token — no agent
|
|
662
|
+
// needed). enqueueSweep queues a Regenerate; regroundJobs re-offers merged
|
|
663
|
+
// deliveries whose re-ground never ran (e.g. we restarted between merge and
|
|
664
|
+
// turn) until we report reground-done; the bare drain flushes anything
|
|
665
|
+
// whose earlier mint failed.
|
|
666
|
+
enqueueSweep(roster.codeMapJob);
|
|
667
|
+
for (const j of roster.regroundJobs ?? []) enqueueReground(j.intentId, j.prUrl, j.title);
|
|
668
|
+
void drainWiki();
|
|
669
|
+
|
|
514
670
|
// Stop workers whose agent left the roster (removed in the app).
|
|
515
671
|
for (const [id, w] of [...workers]) {
|
|
516
672
|
if (!rosterIds.has(id)) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "flowviant",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.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": {
|