flowviant 0.18.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.
@@ -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
@@ -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.18.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/bin/lib/live.mjs CHANGED
@@ -125,7 +125,16 @@ answered…" or teammate line as a new instruction and adapt. There is NO termin
125
125
  and NO interactive prompt — your only channel to a human is the flowviant MCP
126
126
  tools. When you hit a decision only a human can make, call report_blocker (with
127
127
  options when you can) and then STOP your turn — do not spin or guess; you will be
128
- resumed with the answer. When the work is done: open ONE draft PR (git push +
128
+ resumed with the answer. As you satisfy each "done when" criterion, call
129
+ attach_evidence for it — proof the reviewer can SEE without running anything.
130
+ Match the evidence to what you built: backend/API work → a request/response
131
+ capture or a data sample showing the write; a single screen → a screenshot.
132
+ CRITICAL for a multi-step FLOW (login, signup, checkout): a screenshot of one
133
+ page does NOT prove the flow works — you MUST prove the whole path end to end.
134
+ Best: write an e2e/integration test that DRIVES the flow (fill form → submit →
135
+ assert the post-login/success state) and attach its test_output; if you have a
136
+ browser tool (e.g. Playwright), also attach a screen recording of it running.
137
+ Never let a static screenshot stand in for a flow. When the work is done: open ONE draft PR (git push +
129
138
  gh pr create --draft), call attach_pr, then call complete with a plain-language
130
139
  summary of what you built AND a criteria self-report (index into the brief's
131
140
  "done when" list + met true/false + a short note per item). That summary +
@@ -150,7 +159,7 @@ function seedPrompt(runId, brief, transcript, resumedInPlace) {
150
159
  ? [``, `Conversation so far (you may be resuming — pick up where this left off):`, transcript]
151
160
  : []),
152
161
  ``,
153
- `${transcript ? 'Continue' : 'Begin'}. Post a short plan first as a Markdown list (one numbered line per step), then: report_progress as you go; report_blocker + stop if you hit a human decision; open a draft PR, attach_pr, then complete (summary + criteria self-report — your delivery card) when done.`,
162
+ `${transcript ? 'Continue' : 'Begin'}. Post a short plan first as a Markdown list (one numbered line per step), then: report_progress as you go; attach_evidence for each "done when" criterion as you satisfy it (test output, a request/response, a data sample, or a screenshot — so it's reviewable without running anything); report_blocker + stop if you hit a human decision; open a draft PR, attach_pr, then complete (summary + criteria self-report — your delivery card) when done.`,
154
163
  ].join('\n');
155
164
  }
156
165
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flowviant",
3
- "version": "0.18.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": {