flowviant 0.75.0 → 0.77.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/agentPlan.mjs +187 -0
- package/bin/lib/fleet.mjs +19 -0
- package/bin/lib/prompts.mjs +251 -0
- package/bin/lib/work.mjs +1016 -2
- package/package.json +2 -2
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* READING A PLANNER'S ANSWER.
|
|
3
|
+
*
|
|
4
|
+
* The scratch agent behind a Deploy press is asked for one JSON object and
|
|
5
|
+
* nothing else. This is what turns its final message into a proposal, and it is
|
|
6
|
+
* its own module because it is the one piece of that turn that can quietly LIE:
|
|
7
|
+
* everything else either works or throws, while a half-parsed proposal renders
|
|
8
|
+
* as a plausible-looking board somebody accepts.
|
|
9
|
+
*
|
|
10
|
+
* LENIENT ON PACKAGING, STRICT ON SHAPE.
|
|
11
|
+
*
|
|
12
|
+
* A model asked for one object will sometimes fence it and sometimes not, and
|
|
13
|
+
* sometimes say "Here you go:" first. Refusing the whole plan over that spends
|
|
14
|
+
* the operator's own model quota to produce nothing, so the packaging is
|
|
15
|
+
* forgiven. What is NOT forgiven is the shape: an agent holding no cards, a
|
|
16
|
+
* `taskIds` that is not an array of strings, or no agents at all is a proposal
|
|
17
|
+
* nobody can accept, and `null` — which the caller reports in the machine's own
|
|
18
|
+
* words — beats rendering an empty board with an Accept button on it.
|
|
19
|
+
*
|
|
20
|
+
* Every string is CAPPED here rather than downstream. This text is model
|
|
21
|
+
* output about untrusted card content, and it becomes a row.
|
|
22
|
+
*
|
|
23
|
+
* Run: node --test bin/lib/agentPlan.test.mjs
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/** The biggest a single proposal may be. Bounds on a machine, not a policy:
|
|
27
|
+
* the server caps these again at its own boundary. */
|
|
28
|
+
const MAX_AGENTS = 20;
|
|
29
|
+
const MAX_TASKS_PER_AGENT = 60;
|
|
30
|
+
const MAX_NAME = 80;
|
|
31
|
+
const MAX_NOTE = 1000;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Find the object.
|
|
35
|
+
*
|
|
36
|
+
* A fenced block first, because that is what was asked for. Otherwise every `{`
|
|
37
|
+
* in the text is tried as a start, and its BALANCED end is found by counting
|
|
38
|
+
* braces while skipping string literals — the first candidate that parses into
|
|
39
|
+
* something with an `agents` array wins.
|
|
40
|
+
*
|
|
41
|
+
* The obvious cheap version — first `{` to last `}` — is wrong in a way a test
|
|
42
|
+
* caught: a planner that writes "I looked at {the auth module} first" before its
|
|
43
|
+
* JSON produces a span starting at the wrong brace, and the whole plan is lost
|
|
44
|
+
* to a sentence. Scanning candidates costs nothing at this size and cannot be
|
|
45
|
+
* defeated by prose.
|
|
46
|
+
*/
|
|
47
|
+
function extract(raw) {
|
|
48
|
+
const fenced = raw.match(/```(?:json)?\s*([\s\S]*?)```/);
|
|
49
|
+
const candidates = [];
|
|
50
|
+
if (fenced) candidates.push(fenced[1]);
|
|
51
|
+
for (let i = 0; i < raw.length; i++) {
|
|
52
|
+
if (raw[i] !== '{') continue;
|
|
53
|
+
const end = balanced(raw, i);
|
|
54
|
+
if (end > i) candidates.push(raw.slice(i, end + 1));
|
|
55
|
+
}
|
|
56
|
+
for (const body of candidates) {
|
|
57
|
+
if (!body.trim()) continue;
|
|
58
|
+
try {
|
|
59
|
+
const v = JSON.parse(body);
|
|
60
|
+
if (v && typeof v === 'object' && Array.isArray(v.agents)) return v;
|
|
61
|
+
} catch {
|
|
62
|
+
/* the next candidate may be the object */
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** The index of the `}` that closes the `{` at `from`, or -1. Skips string
|
|
69
|
+
* literals so a brace inside a card title cannot end the object early. */
|
|
70
|
+
function balanced(s, from) {
|
|
71
|
+
let depth = 0;
|
|
72
|
+
let inStr = false;
|
|
73
|
+
let esc = false;
|
|
74
|
+
for (let i = from; i < s.length; i++) {
|
|
75
|
+
const ch = s[i];
|
|
76
|
+
if (inStr) {
|
|
77
|
+
if (esc) esc = false;
|
|
78
|
+
else if (ch === '\\') esc = true;
|
|
79
|
+
else if (ch === '"') inStr = false;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (ch === '"') inStr = true;
|
|
83
|
+
else if (ch === '{') depth++;
|
|
84
|
+
else if (ch === '}' && --depth === 0) return i;
|
|
85
|
+
}
|
|
86
|
+
return -1;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function parseProposal(text) {
|
|
90
|
+
const raw = String(text ?? '');
|
|
91
|
+
const parsed = extract(raw);
|
|
92
|
+
if (!parsed) return null;
|
|
93
|
+
|
|
94
|
+
const agents = [];
|
|
95
|
+
for (const [i, g] of parsed.agents.slice(0, MAX_AGENTS).entries()) {
|
|
96
|
+
if (!g || typeof g !== 'object') continue;
|
|
97
|
+
const taskIds = Array.isArray(g.taskIds)
|
|
98
|
+
? g.taskIds.filter((t) => typeof t === 'string' && t.trim()).slice(0, MAX_TASKS_PER_AGENT)
|
|
99
|
+
: [];
|
|
100
|
+
// AN AGENT WITH NO CARDS IS NOT AN AGENT. Dropping it is right rather than
|
|
101
|
+
// merely tidy: accepting one would create a worktree and a branch for
|
|
102
|
+
// nothing, and it would sit in Working forever with an empty queue.
|
|
103
|
+
if (taskIds.length === 0) continue;
|
|
104
|
+
agents.push({
|
|
105
|
+
tempId: String(g.tempId || `a${i + 1}`).slice(0, 64),
|
|
106
|
+
name: String(g.name ?? '').slice(0, MAX_NAME),
|
|
107
|
+
taskIds,
|
|
108
|
+
...(Number.isFinite(g.pointsBudget) && g.pointsBudget > 0
|
|
109
|
+
? { pointsBudget: Math.min(Math.round(g.pointsBudget), 100_000) }
|
|
110
|
+
: {}),
|
|
111
|
+
...(Array.isArray(g.waitsOn)
|
|
112
|
+
? { waitsOn: g.waitsOn.filter((w) => typeof w === 'string' && w).slice(0, 20) }
|
|
113
|
+
: {}),
|
|
114
|
+
...(typeof g.intoAgentId === 'string' && g.intoAgentId
|
|
115
|
+
? { intoAgentId: g.intoAgentId.slice(0, 64) }
|
|
116
|
+
: {}),
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
if (agents.length === 0) return null;
|
|
120
|
+
return {
|
|
121
|
+
agents,
|
|
122
|
+
...(typeof parsed.note === 'string' && parsed.note.trim()
|
|
123
|
+
? { note: parsed.note.slice(0, MAX_NOTE) }
|
|
124
|
+
: {}),
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* READING AN AGENT'S ANSWER at the end of a turn.
|
|
130
|
+
*
|
|
131
|
+
* Same lenient-packaging / strict-shape rule as `parseProposal`, and the same
|
|
132
|
+
* reason: the turn already ran and the operator already paid for it, so
|
|
133
|
+
* refusing the whole thing over a stray "Here you go:" throws away real work.
|
|
134
|
+
*
|
|
135
|
+
* NULL IS A REAL ANSWER AND THE MOST IMPORTANT ONE. It means the turn declared
|
|
136
|
+
* NEITHER delivered nor blocked — a signed-out CLI, a crash, an exhausted
|
|
137
|
+
* quota, or a model that simply stopped — and the caller reports it as
|
|
138
|
+
* `nothing`, which sends the agent to Stuck. Optimistic status from a machine
|
|
139
|
+
* that quit is the one lie this board cannot afford, so anything ambiguous ends
|
|
140
|
+
* up here rather than being read as success.
|
|
141
|
+
*/
|
|
142
|
+
export function parseTurnResult(text) {
|
|
143
|
+
const parsedRaw = String(text ?? '');
|
|
144
|
+
const fenced = parsedRaw.match(/```(?:json)?\s*([\s\S]*?)```/);
|
|
145
|
+
const candidates = [];
|
|
146
|
+
if (fenced) candidates.push(fenced[1]);
|
|
147
|
+
for (let i = 0; i < parsedRaw.length; i++) {
|
|
148
|
+
if (parsedRaw[i] !== '{') continue;
|
|
149
|
+
const end = balanced(parsedRaw, i);
|
|
150
|
+
if (end > i) candidates.push(parsedRaw.slice(i, end + 1));
|
|
151
|
+
}
|
|
152
|
+
for (const body of candidates) {
|
|
153
|
+
let v;
|
|
154
|
+
try {
|
|
155
|
+
v = JSON.parse(body);
|
|
156
|
+
} catch {
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
if (!v || typeof v !== 'object') continue;
|
|
160
|
+
if (v.status === 'blocked') {
|
|
161
|
+
const question = typeof v.question === 'string' ? v.question.trim() : '';
|
|
162
|
+
// A "blocked" with no question is not an answer anybody can act on — it
|
|
163
|
+
// parks an agent with nothing to reply to. Treated as `nothing`, which
|
|
164
|
+
// at least says truthfully that the machine went quiet.
|
|
165
|
+
if (!question) continue;
|
|
166
|
+
return { outcome: 'question', answer: question.slice(0, 8000) };
|
|
167
|
+
}
|
|
168
|
+
if (v.status === 'delivered') {
|
|
169
|
+
return {
|
|
170
|
+
outcome: 'delivered',
|
|
171
|
+
answer: (typeof v.summary === 'string' ? v.summary : '').slice(0, 8000),
|
|
172
|
+
raised: Array.isArray(v.raised)
|
|
173
|
+
? v.raised
|
|
174
|
+
.filter((r) => r && typeof r.title === 'string' && r.title.trim())
|
|
175
|
+
.slice(0, 10)
|
|
176
|
+
.map((r) => ({
|
|
177
|
+
title: r.title.trim().slice(0, 300),
|
|
178
|
+
...(typeof r.brief === 'string' && r.brief.trim()
|
|
179
|
+
? { brief: r.brief.trim().slice(0, 2000) }
|
|
180
|
+
: {}),
|
|
181
|
+
}))
|
|
182
|
+
: [],
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return null;
|
|
187
|
+
}
|
package/bin/lib/fleet.mjs
CHANGED
|
@@ -817,6 +817,10 @@ export async function runFleetDaemon() {
|
|
|
817
817
|
processDiffJobs,
|
|
818
818
|
processKillJobs,
|
|
819
819
|
processPrJobs,
|
|
820
|
+
processAgentPlanJobs,
|
|
821
|
+
processAgentTurnJobs,
|
|
822
|
+
processAgentMergeJobs,
|
|
823
|
+
freshenManualPlaces,
|
|
820
824
|
heldSessionIds,
|
|
821
825
|
processPreviewJobs,
|
|
822
826
|
livePreviewIds,
|
|
@@ -1616,6 +1620,21 @@ export async function runFleetDaemon() {
|
|
|
1616
1620
|
// `gh` credential; a settle never closes a card (done is observed by the
|
|
1617
1621
|
// landed walk when the merge reaches base).
|
|
1618
1622
|
processPrJobs(roster.prJobs);
|
|
1623
|
+
// A Deploy press waiting for a plan. AFTER the job lanes above and before
|
|
1624
|
+
// the worktree report, for no reason other than that it reads directories
|
|
1625
|
+
// those lanes may still be writing — it measures, so a stale read is a
|
|
1626
|
+
// slightly worse hint and never a wrong action.
|
|
1627
|
+
processAgentPlanJobs(roster.agentPlanJobs);
|
|
1628
|
+
// …and an agent's next card. After the plan jobs because a press becoming
|
|
1629
|
+
// agents is the thing that produces these.
|
|
1630
|
+
processAgentTurnJobs(roster.agentTurnJobs);
|
|
1631
|
+
// …and a branch somebody approved. After the turns: a merge takes the
|
|
1632
|
+
// place's WRITER lock, and writer preference means it goes ahead of any
|
|
1633
|
+
// reader queued behind it anyway.
|
|
1634
|
+
processAgentMergeJobs(roster.agentMergeJobs);
|
|
1635
|
+
// Catch each person's manual worktree up to base while it is clean. Silent,
|
|
1636
|
+
// fast-forward only, and it never touches an agent's branch.
|
|
1637
|
+
freshenManualPlaces();
|
|
1619
1638
|
// …and what the SURVIVING ones hold: branch, ahead-of-base, diffstat.
|
|
1620
1639
|
// Throttled inside, never awaited — a `git status` the human cannot run
|
|
1621
1640
|
// themselves from a browser, relayed. After retirement so a directory that
|
package/bin/lib/prompts.mjs
CHANGED
|
@@ -501,3 +501,254 @@ export const REGROUND_KICKOFF = ({ sha, title, files, vaultDir, predictedPages =
|
|
|
501
501
|
`chapter that covers them), append the feature-history entry to log.md,\n` +
|
|
502
502
|
`then output REGROUND_DONE.`;
|
|
503
503
|
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* THE PLANNER — the scratch agent behind a Deploy press (2026-09-03).
|
|
507
|
+
*
|
|
508
|
+
* Somebody selected cards and pressed Deploy. This turn's whole job is to
|
|
509
|
+
* decide HOW THAT WORK SHOULD BE SPLIT across agents, and then stop. It writes
|
|
510
|
+
* no code, edits no files and starts nothing: a person reads what it proposes,
|
|
511
|
+
* edits it on the board, and accepting is what spawns anything.
|
|
512
|
+
*
|
|
513
|
+
* It runs READ-ONLY IN THE CHECKOUT under CONSULT_PERM — no Write, no Edit, no
|
|
514
|
+
* mkdir, no rm, and no MCP at all. The proposal comes back as its final
|
|
515
|
+
* message, not through a tool, which is what lets that permission set be this
|
|
516
|
+
* narrow. A planner authors a decision, and there is no file on this machine it
|
|
517
|
+
* has any business touching.
|
|
518
|
+
*
|
|
519
|
+
* The two facts it is asked to weigh are the only two that are actually
|
|
520
|
+
* knowable here: what the cards SAY, and what each live agent has already
|
|
521
|
+
* TOUCHED. Everything else — how long something will take, who should own it,
|
|
522
|
+
* whether it is a good idea — is not a question a planner can answer from a
|
|
523
|
+
* repository, and asking for it produces confident invention.
|
|
524
|
+
*/
|
|
525
|
+
export const SYSTEM_PLAN = `You are the human's own Claude, planning a batch of work in their repository.
|
|
526
|
+
|
|
527
|
+
You are READ-ONLY. You cannot write, edit or create files, and you have no tools
|
|
528
|
+
beyond reading the repo. Do not try. Your entire output is a plan.
|
|
529
|
+
|
|
530
|
+
WHAT YOU ARE DECIDING: a set of task cards has been selected. Split them across
|
|
531
|
+
one or more AGENTS. An agent is one CLI in one git worktree on one branch,
|
|
532
|
+
working its cards ONE AT A TIME in the order you give, and landing all of them
|
|
533
|
+
as a single reviewable branch.
|
|
534
|
+
|
|
535
|
+
THE RULES THAT MATTER:
|
|
536
|
+
|
|
537
|
+
1. SEQUENTIAL WORK BELONGS IN ONE AGENT. If B needs A's code to exist, put them
|
|
538
|
+
in the same agent, A first. Splitting a chain across agents means the second
|
|
539
|
+
one waits for the first to MERGE before it can even start.
|
|
540
|
+
|
|
541
|
+
2. SPLIT ONLY WHAT CAN GENUINELY RUN AT THE SAME TIME. Two agents editing the
|
|
542
|
+
same files land two branches that conflict, and somebody resolves it by hand.
|
|
543
|
+
Read the repo to find out whether they collide — do not guess from titles.
|
|
544
|
+
|
|
545
|
+
3. AN AGENT IS ONE REVIEWABLE BRANCH. Everything you put in one agent gets
|
|
546
|
+
approved or rejected TOGETHER. Cards a person would want to judge separately
|
|
547
|
+
belong in separate agents.
|
|
548
|
+
|
|
549
|
+
4. FEWER, LARGER AGENTS BEAT MANY SMALL ONES. Only a limited number run at once;
|
|
550
|
+
past that they queue, and a queue of tiny agents is slower than a few real
|
|
551
|
+
ones. Never propose more agents than the cap you were given.
|
|
552
|
+
|
|
553
|
+
5. NAME EACH AGENT AFTER THE WORK ("auth", "billing webhooks"), not after a
|
|
554
|
+
number. People say these names out loud.
|
|
555
|
+
|
|
556
|
+
6. A POINTS BUDGET bounds how far an agent may grow while it works — it files
|
|
557
|
+
its own follow-up cards when it finds things, and the budget is where it
|
|
558
|
+
stops and asks. Set it from the size of what you put in, leaving some room.
|
|
559
|
+
Omit it if the cards carry no sizes.
|
|
560
|
+
|
|
561
|
+
7. YOU MAY ADD CARDS TO A LIVE AGENT instead of creating a new one, when the
|
|
562
|
+
work needs what that agent has already built and has not merged yet. Use its
|
|
563
|
+
id in "intoAgentId".
|
|
564
|
+
|
|
565
|
+
ANSWER WITH ONE JSON OBJECT AND NOTHING ELSE — no prose before it, no prose
|
|
566
|
+
after it. Wrap it in a \`\`\`json fence:
|
|
567
|
+
|
|
568
|
+
\`\`\`json
|
|
569
|
+
{
|
|
570
|
+
"note": "one sentence on why you split it this way",
|
|
571
|
+
"agents": [
|
|
572
|
+
{
|
|
573
|
+
"tempId": "a1",
|
|
574
|
+
"name": "auth",
|
|
575
|
+
"taskIds": ["<card id>", "<card id>"],
|
|
576
|
+
"pointsBudget": 8,
|
|
577
|
+
"waitsOn": [],
|
|
578
|
+
"intoAgentId": null
|
|
579
|
+
}
|
|
580
|
+
]
|
|
581
|
+
}
|
|
582
|
+
\`\`\`
|
|
583
|
+
|
|
584
|
+
Every selected card id must appear EXACTLY ONCE across all agents. Use the ids
|
|
585
|
+
exactly as given. "waitsOn" holds tempIds of agents that must MERGE first.`;
|
|
586
|
+
|
|
587
|
+
/**
|
|
588
|
+
* The planner's turn.
|
|
589
|
+
*
|
|
590
|
+
* The cards and the live agents are FENCED: their titles, briefs and criteria
|
|
591
|
+
* are written by whoever files cards in this project and by agents themselves,
|
|
592
|
+
* and this turn reads a repository afterwards. The instruction that matters —
|
|
593
|
+
* "split this" — is ours and sits outside the fence.
|
|
594
|
+
*/
|
|
595
|
+
export const AGENT_PLAN_KICKOFF = ({ tasks, liveAgents, agentCap }) => {
|
|
596
|
+
const cards = tasks
|
|
597
|
+
.map(
|
|
598
|
+
(t) =>
|
|
599
|
+
`- id: ${t.id}\n title: ${t.title}\n` +
|
|
600
|
+
(t.points ? ` points: ${t.points}\n` : '') +
|
|
601
|
+
(t.anchors?.length ? ` owns: ${t.anchors.join(', ')}\n` : '') +
|
|
602
|
+
(t.brief ? ` brief: ${t.brief}\n` : '') +
|
|
603
|
+
(t.criteria?.length ? ` done when:\n${t.criteria.map((c) => ` - ${c}`).join('\n')}\n` : '')
|
|
604
|
+
)
|
|
605
|
+
.join('\n');
|
|
606
|
+
const live = liveAgents.length
|
|
607
|
+
? liveAgents
|
|
608
|
+
.map(
|
|
609
|
+
(a) =>
|
|
610
|
+
`- id: ${a.id}\n name: ${a.name || '(unnamed)'}\n status: ${a.status}\n` +
|
|
611
|
+
(a.changedFiles?.length
|
|
612
|
+
? ` has already changed:\n${a.changedFiles.map((f) => ` - ${f}`).join('\n')}\n`
|
|
613
|
+
: ' has changed nothing yet\n')
|
|
614
|
+
)
|
|
615
|
+
.join('\n')
|
|
616
|
+
: '(none)';
|
|
617
|
+
return (
|
|
618
|
+
`Split this batch of work across agents.\n\n` +
|
|
619
|
+
`At most ${agentCap} agent${agentCap === 1 ? '' : 's'} run at once on this machine; ` +
|
|
620
|
+
`propose no more than that.\n\n` +
|
|
621
|
+
`${fence('THE SELECTED CARDS', cards)}\n\n` +
|
|
622
|
+
`${fence('AGENTS ALREADY RUNNING (you may add to one)', live)}\n\n` +
|
|
623
|
+
`Read whatever you need from the repository to decide whether these collide. ` +
|
|
624
|
+
`Then answer with the JSON object and nothing else.`
|
|
625
|
+
);
|
|
626
|
+
};
|
|
627
|
+
|
|
628
|
+
/**
|
|
629
|
+
* AN AGENT'S SYSTEM PROMPT — the contract for one card in one worktree.
|
|
630
|
+
*
|
|
631
|
+
* Deliberately not SYSTEM_WORK. A Workbench tab is a CONVERSATION: it narrates
|
|
632
|
+
* through tools, holds context across many turns, and a person is watching. An
|
|
633
|
+
* agent turn is a TASK — it starts, does one card, and ends — and nobody is
|
|
634
|
+
* watching while it runs. So the whole contract is: do this card, commit it,
|
|
635
|
+
* and end with a JSON object saying what happened.
|
|
636
|
+
*
|
|
637
|
+
* IT HAS NO TOOLS BEYOND THE REPOSITORY. There is no MCP on this turn at all,
|
|
638
|
+
* which is why everything it needs to say has to fit in that final object and
|
|
639
|
+
* everything it needs to PROVE is measured from git afterwards. An agent naming
|
|
640
|
+
* its own commit shas would be a receipt pointing at whatever it liked; the
|
|
641
|
+
* daemon reads the log instead.
|
|
642
|
+
*
|
|
643
|
+
* THE ONE THING IT MUST NOT DO IS GUESS. A turn that ends with a question costs
|
|
644
|
+
* a person one reply; a turn that guesses costs them a review, a rejection and
|
|
645
|
+
* a second run — and the guess arrives wearing a confident summary.
|
|
646
|
+
*/
|
|
647
|
+
export const SYSTEM_AGENT = `You are the human's own Claude, working one task in a git worktree of their
|
|
648
|
+
repository. Nobody is watching this run. You have the repo and nothing else —
|
|
649
|
+
no project tools, no board, no chat.
|
|
650
|
+
|
|
651
|
+
WHAT TO DO:
|
|
652
|
+
|
|
653
|
+
1. Do the card you are given. Read whatever you need first. Follow the
|
|
654
|
+
repository's own conventions over anything you would do by default.
|
|
655
|
+
|
|
656
|
+
2. COMMIT YOUR WORK before you finish. Small, real commit messages. Uncommitted
|
|
657
|
+
work is work nobody can review or merge.
|
|
658
|
+
|
|
659
|
+
3. If you cannot finish because you need a DECISION only a person can make —
|
|
660
|
+
an ambiguous requirement, a choice between two designs, a missing credential
|
|
661
|
+
— STOP AND ASK. Do not guess. A question costs one reply; a guess costs a
|
|
662
|
+
review, a rejection and a second run, and it arrives looking finished.
|
|
663
|
+
|
|
664
|
+
4. If you find something broken that is NOT this card — a bug, a failing test
|
|
665
|
+
you did not cause — you may fix it, and you must SAY SO by raising it. It
|
|
666
|
+
becomes its own card so a person can see it happened rather than finding it
|
|
667
|
+
in the diff.
|
|
668
|
+
|
|
669
|
+
COMMIT TRAILER: every commit you make must end with a line reading
|
|
670
|
+
Flowviant-Task: <the card id you were given>
|
|
671
|
+
It is how the board attaches your commits to the card; a commit without it is
|
|
672
|
+
work nobody can trace back.
|
|
673
|
+
|
|
674
|
+
END YOUR TURN WITH ONE JSON OBJECT AND NOTHING AFTER IT, in a \`\`\`json fence:
|
|
675
|
+
|
|
676
|
+
\`\`\`json
|
|
677
|
+
{
|
|
678
|
+
"status": "delivered",
|
|
679
|
+
"summary": "one or two sentences on what you actually changed",
|
|
680
|
+
"raised": [{ "title": "short title", "brief": "what is wrong and what you did" }]
|
|
681
|
+
}
|
|
682
|
+
\`\`\`
|
|
683
|
+
|
|
684
|
+
or, if you are stopping to ask:
|
|
685
|
+
|
|
686
|
+
\`\`\`json
|
|
687
|
+
{
|
|
688
|
+
"status": "blocked",
|
|
689
|
+
"question": "the specific thing you need decided, in one or two sentences"
|
|
690
|
+
}
|
|
691
|
+
\`\`\`
|
|
692
|
+
|
|
693
|
+
"raised" is optional and only for work you did that was NOT this card. Do not
|
|
694
|
+
list the card itself there. Do not put commit shas in the summary — they are
|
|
695
|
+
read from git.`;
|
|
696
|
+
|
|
697
|
+
/**
|
|
698
|
+
* The turn itself.
|
|
699
|
+
*
|
|
700
|
+
* The card is FENCED: its title, brief and criteria are written by whoever
|
|
701
|
+
* files cards in this project, and this turn is about to edit code. The
|
|
702
|
+
* instruction is ours and sits outside the fence.
|
|
703
|
+
*
|
|
704
|
+
* The queue POSITION is stated because it changes behaviour: an agent that
|
|
705
|
+
* thinks it is finishing tidies up, writes summaries and stops; one that knows
|
|
706
|
+
* three more cards are coming leaves the ground ready for them.
|
|
707
|
+
*/
|
|
708
|
+
export const AGENT_TASK_KICKOFF = ({ agentName, task, position, total }) =>
|
|
709
|
+
`You are the agent "${safeName(agentName)}", working card ${position} of ${total} ` +
|
|
710
|
+
`on this branch. Everything you commit here is reviewed and merged TOGETHER with ` +
|
|
711
|
+
`the other cards in this run.\n\n` +
|
|
712
|
+
`${fence('THE CARD', taskBlock(task))}\n\n` +
|
|
713
|
+
`When you commit, put this trailer on the LAST line of each commit message so ` +
|
|
714
|
+
`the card can find its own commits:\n` +
|
|
715
|
+
`Flowviant-Task: ${task?.id ?? ''}\n\n` +
|
|
716
|
+
`Do it, commit it, and end with the JSON object.`;
|
|
717
|
+
|
|
718
|
+
/**
|
|
719
|
+
* An agent's NAME is model-authored — the planner chose it — and it is
|
|
720
|
+
* interpolated at the head of the prompt, OUTSIDE the fence, where a sentence
|
|
721
|
+
* would read as an instruction from us. Fencing the name would be absurd (it is
|
|
722
|
+
* two words in the middle of ours), so it is reduced to something that cannot
|
|
723
|
+
* be a sentence: one line, no fence delimiters, and short.
|
|
724
|
+
*/
|
|
725
|
+
const safeName = (n) =>
|
|
726
|
+
String(n ?? '')
|
|
727
|
+
.replace(/[\r\n]+/g, ' ')
|
|
728
|
+
.replace(/<<<|>>>/g, '')
|
|
729
|
+
.replace(/"/g, "'")
|
|
730
|
+
.trim()
|
|
731
|
+
.slice(0, 60) || 'agent';
|
|
732
|
+
|
|
733
|
+
const taskBlock = (task) =>
|
|
734
|
+
`id: ${task?.id ?? ''}\n` +
|
|
735
|
+
`title: ${task?.title ?? ''}\n` +
|
|
736
|
+
(task?.brief ? `\nbrief:\n${task.brief}\n` : '') +
|
|
737
|
+
(task?.criteria?.length ? `\ndone when:\n${task.criteria.map((c) => `- ${c}`).join('\n')}\n` : '') +
|
|
738
|
+
(task?.anchors?.length ? `\nthis card owns:\n${task.anchors.map((a) => `- ${a}`).join('\n')}\n` : '');
|
|
739
|
+
|
|
740
|
+
/**
|
|
741
|
+
* A PERSON SPOKE TO THE AGENT — usually the answer to its own question.
|
|
742
|
+
*
|
|
743
|
+
* It is the same shape as a task turn on purpose: the agent still ends with the
|
|
744
|
+
* JSON object, because whatever it does next either finishes the card it was on
|
|
745
|
+
* or blocks again, and those are the only two things the board can act on.
|
|
746
|
+
*/
|
|
747
|
+
export const AGENT_HUMAN_KICKOFF = ({ agentName, message, askedByName, task, position, total }) =>
|
|
748
|
+
`You are the agent "${safeName(agentName)}"` +
|
|
749
|
+
(task ? `, working card ${position} of ${total} on this branch` : '') +
|
|
750
|
+
`.\n\n` +
|
|
751
|
+
`${fence('WHO IS TALKING', askedByName || 'a member of this project')}\n\n` +
|
|
752
|
+
`${fence('WHAT THEY SAID', message)}\n\n` +
|
|
753
|
+
(task ? `${fence('THE CARD YOU ARE ON', taskBlock(task))}\n\n` : '') +
|
|
754
|
+
`Carry on, and end with the JSON object as usual.`;
|