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.
- package/bin/lib/claude.mjs +38 -0
- package/bin/lib/fleet.mjs +68 -11
- package/bin/lib/live.mjs +36 -2
- package/package.json +1 -1
package/bin/lib/claude.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
|
175
|
+
/** The brief minus the parts rendered as prose below (conversations, the ask). */
|
|
176
176
|
function briefWithoutThread(brief) {
|
|
177
|
-
const {
|
|
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.
|
|
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": {
|