flowviant 0.30.0 → 0.31.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.
@@ -271,6 +271,44 @@ Steps:
271
271
  Ground every claim in files you actually read. Be efficient — look only at the
272
272
  changed area, not the whole repo; spend little quota.`;
273
273
 
274
+ /**
275
+ * CONSULT — someone is planning and asked a question only the repo can answer.
276
+ *
277
+ * Strictly read-only, and strictly an ANSWER: no edits, no commits, no branch,
278
+ * no MCP tools. A consult is not a dispatch, and the prompt says so out loud
279
+ * because the model is otherwise very willing to start building the thing it was
280
+ * asked about.
281
+ */
282
+ export const SYSTEM_CONSULT = `You are a Flowviant build agent, but you are NOT building anything right now.
283
+ Someone is PLANNING a feature and has asked you a question, because you are the
284
+ one with the actual repository in front of you. The planner they are talking to
285
+ sees only a module manifest and wiki summaries — you see the code.
286
+
287
+ Your entire job is to ANSWER, from files you actually read.
288
+
289
+ RULES:
290
+ - READ ONLY. Do not edit, create or delete any file. No git writes, no commits,
291
+ no branches, no PRs. Nothing you do here leaves a trace in the repo.
292
+ - Do NOT start implementing what they are planning, and do not offer to. If the
293
+ answer is "this needs building", say that and stop — they will dispatch it in
294
+ its own task thread when they are ready.
295
+ - Ground every claim in something you opened. Cite concrete paths
296
+ (\`apps/api/src/middleware/auth.ts\`) so the answer can be checked.
297
+ - If it already EXISTS, say so plainly and point at it — that is the single most
298
+ valuable thing you can tell someone mid-plan, and it is the answer they are
299
+ least expecting.
300
+ - If the repo genuinely does not settle the question, say THAT rather than
301
+ guessing. "I can't tell from the code" is a real answer and a useful one.
302
+ - Be brief: a few sentences, or a short list. This lands in a chat thread that a
303
+ human is reading while they think, not in a document.
304
+
305
+ Write plain Markdown for a person. No preamble, no restating the question.`;
306
+
307
+ export const CONSULT_KICKOFF = ({ planTitle, question, askedByName }) =>
308
+ `${askedByName || 'A teammate'} is planning ${planTitle ? `"${planTitle}"` : 'a feature'} and asked you:\n\n` +
309
+ `${question}\n\n` +
310
+ `Read the repo you are running in and answer. Do not change anything.`;
311
+
274
312
  export const REGROUND_KICKOFF = ({ sha, title, files, vaultDir, predictedPages = [] }) =>
275
313
  `A feature just merged. Re-ground the knowledge vault (${vaultDir}) for it.\n\n` +
276
314
  `Feature: ${title}\n` +
package/bin/lib/fleet.mjs CHANGED
@@ -54,6 +54,8 @@ import {
54
54
  SYSTEM_PLAN_CHECK,
55
55
  PLAN_CHECK_KICKOFF,
56
56
  REGROUND_KICKOFF,
57
+ SYSTEM_CONSULT,
58
+ CONSULT_KICKOFF,
57
59
  } from './claude.mjs';
58
60
  import { runLiveWorker } from './live.mjs';
59
61
  import { reapOrphanPreviews } from './preview.mjs';
@@ -402,6 +404,19 @@ export async function runFleetDaemon() {
402
404
  return null;
403
405
  };
404
406
 
407
+ /** A clean detached checkout at base — what "the real code" has to mean for a
408
+ * question about the repo, rather than whatever half-finished state an agent
409
+ * worktree happens to be in. Shared by the plan check and consults. */
410
+ const ensureWikiWorktree = () => {
411
+ if (existsSync(wikiWt)) return;
412
+ try {
413
+ git(['worktree', 'add', '--detach', wikiWt, baseRef], repoRoot);
414
+ } catch {
415
+ git(['worktree', 'prune'], repoRoot);
416
+ git(['worktree', 'add', '--detach', wikiWt, baseRef], repoRoot);
417
+ }
418
+ };
419
+
405
420
  const PLAN_CHECK_DONE_URL = FLEET_URL.replace(/\/agents\/?$/, '/plan-check-done');
406
421
  const checkingPlans = new Set();
407
422
  const processPlanCheckJobs = (jobs) => {
@@ -413,17 +428,7 @@ export async function runFleetDaemon() {
413
428
  (async () => {
414
429
  try {
415
430
  note(`${c.cyan('plan')} ${c.dim(`— checking "${job.title}" against your code…`)}`);
416
- // Reuse the wiki worktree: a clean detached checkout at base, which is
417
- // what "the real code" should mean here — not whatever half-finished
418
- // state an agent worktree happens to be in.
419
- if (!existsSync(wikiWt)) {
420
- try {
421
- git(['worktree', 'add', '--detach', wikiWt, baseRef], repoRoot);
422
- } catch {
423
- git(['worktree', 'prune'], repoRoot);
424
- git(['worktree', 'add', '--detach', wikiWt, baseRef], repoRoot);
425
- }
426
- }
431
+ ensureWikiWorktree();
427
432
  const out = await runTurn({
428
433
  prompt: PLAN_CHECK_KICKOFF({ title: job.title, intents: job.intents }),
429
434
  resume: false,
@@ -456,6 +461,57 @@ export async function runFleetDaemon() {
456
461
  }
457
462
  };
458
463
 
464
+ // Consults — a planning question aimed at THIS machine, answered by reading
465
+ // the repo. Deliberately the lightest job on the roster: same read-only
466
+ // detached checkout the plan check uses, no MCP, no writes, no run recorded.
467
+ const CONSULT_DONE_URL = FLEET_URL.replace(/\/agents\/?$/, '/consult-done');
468
+ const answering = new Set();
469
+ const processConsultJobs = (jobs) => {
470
+ for (const job of jobs ?? []) {
471
+ if (!job || typeof job.id !== 'string' || !job.question) continue;
472
+ if (answering.has(job.id)) continue;
473
+ answering.add(job.id);
474
+ (async () => {
475
+ try {
476
+ note(`${c.cyan('ask')} ${c.dim(`— ${job.askedByName || 'someone'} asked about "${job.planTitle || 'a plan'}"`)}`);
477
+ ensureWikiWorktree();
478
+ const out = await runTurn({
479
+ prompt: CONSULT_KICKOFF({
480
+ planTitle: job.planTitle,
481
+ question: job.question,
482
+ askedByName: job.askedByName,
483
+ }),
484
+ resume: false,
485
+ system: SYSTEM_CONSULT,
486
+ cwd: wikiWt,
487
+ wikiPerm: true, // read-only file perms — no MCP, no shell writes
488
+ label: c.cyan('[ask]'),
489
+ });
490
+ const answer = (out || '').trim();
491
+ await reportMergeOutcome(CONSULT_DONE_URL, {
492
+ consultId: job.id,
493
+ ok: answer.length > 0,
494
+ // Scrub: an answer can quote config or env-adjacent code.
495
+ answer: envScrub(answer).slice(0, 8000),
496
+ });
497
+ ok(`${c.cyan('ask')} ${c.dim('— answered in the plan thread')}`);
498
+ } catch (e) {
499
+ // Settle it either way. A question that cannot be answered must not
500
+ // re-burn a Claude turn on every poll, and silence would leave the
501
+ // human waiting on a machine that already gave up.
502
+ await reportMergeOutcome(CONSULT_DONE_URL, {
503
+ consultId: job.id,
504
+ ok: false,
505
+ answer: e?.message ?? 'the read failed',
506
+ });
507
+ warn(`consult failed: ${e?.message ?? e}`);
508
+ } finally {
509
+ answering.delete(job.id);
510
+ }
511
+ })();
512
+ }
513
+ };
514
+
459
515
  const processMergeJobs = (jobs) => {
460
516
  for (const job of jobs ?? []) {
461
517
  if (!job || typeof job.id !== 'string') continue; // a null element would wedge the loop
@@ -1022,6 +1078,7 @@ export async function runFleetDaemon() {
1022
1078
  processMergeJobs(roster.mergeJobs);
1023
1079
  processPatchRevertJobs(roster.patchRevertJobs);
1024
1080
  processPlanCheckJobs(roster.planCheckJobs);
1081
+ processConsultJobs(roster.consultJobs);
1025
1082
  processCleanupJobs(roster.cleanupJobs);
1026
1083
  const rosterIds = new Set(roster.agents.map((a) => a.agentId));
1027
1084
 
package/bin/lib/live.mjs CHANGED
@@ -172,12 +172,35 @@ questions, delivery summaries, commits, or PRs — reference keys by NAME only
172
172
  (e.g. "set STRIPE_KEY"). Never screenshot a terminal or page that displays a
173
173
  credential, and never commit an env file.`;
174
174
 
175
- /** The brief minus the conversation — that is rendered as prose, not JSON. */
175
+ /** The brief minus the parts rendered as prose below (conversations, the ask). */
176
176
  function briefWithoutThread(brief) {
177
- const { thread: _thread, lastMessageId: _lastMessageId, ...rest } = brief ?? {};
177
+ const {
178
+ thread: _thread,
179
+ lastMessageId: _lastMessageId,
180
+ plan: _plan,
181
+ asked: _asked,
182
+ ...rest
183
+ } = brief ?? {};
178
184
  return rest;
179
185
  }
180
186
 
187
+ /** The plan this task was carved out of, when there was one. A slice cannot
188
+ * reconstruct WHY it was cut this way from its own spec. */
189
+ function planContext(brief) {
190
+ const plan = brief?.plan;
191
+ if (!plan) return [];
192
+ const turns = (plan.recentTurns ?? [])
193
+ .map((m) => `${m.authorName || m.role}: ${m.content}`)
194
+ .join('\n');
195
+ return [
196
+ ``,
197
+ `This task is ONE SLICE of a larger plan: "${plan.title || 'untitled plan'}".`,
198
+ plan.description ? `The plan:\n${plan.description}` : '',
199
+ turns ? `How the team was talking about it, most recent last:\n${turns}` : '',
200
+ `Build only YOUR slice — the rest is there so its shape makes sense.`,
201
+ ].filter(Boolean);
202
+ }
203
+
181
204
  function seedPrompt(runId, brief, transcript, resumedInPlace) {
182
205
  return [
183
206
  `Your run id is ${runId}. Use it for every flowviant MCP tool call.`,
@@ -193,6 +216,17 @@ function seedPrompt(runId, brief, transcript, resumedInPlace) {
193
216
  // The conversation is rendered below as readable turns, not dumped twice as
194
217
  // JSON — it is the longest thing in the brief and the least useful as data.
195
218
  JSON.stringify(briefWithoutThread(brief), null, 2),
219
+ ...(brief?.asked
220
+ ? [
221
+ ``,
222
+ `What the human originally asked for, in their words:`,
223
+ ` "${brief.asked}"`,
224
+ `The specification above is someone's reading of that sentence, written`,
225
+ `without access to the repo. Where the two disagree, this is the one with`,
226
+ `a person behind it — say so rather than quietly picking one.`,
227
+ ]
228
+ : []),
229
+ ...planContext(brief),
196
230
  ...(transcript
197
231
  ? [
198
232
  ``,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flowviant",
3
- "version": "0.30.0",
3
+ "version": "0.31.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": {