flowviant 0.125.21 → 0.125.22
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/agentCards.mjs +24 -1
- package/bin/lib/agentPlan.mjs +3 -0
- package/bin/lib/agentQueueContext.mjs +20 -1
- package/bin/lib/artifactContracts.mjs +45 -0
- package/bin/lib/codexSubagents.mjs +6 -2
- package/bin/lib/knowledgeFetch.mjs +8 -3
- package/bin/lib/knowledgeFiles.mjs +4 -4
- package/bin/lib/orchestrator.mjs +40 -14
- package/bin/lib/orchestratorHooks.mjs +15 -2
- package/bin/lib/orchestratorNative.mjs +30 -9
- package/bin/lib/prompts.mjs +25 -4
- package/bin/lib/runtimeCodex.mjs +1 -1
- package/bin/lib/turnGaps.mjs +47 -0
- package/bin/lib/uiReviewContract.mjs +1 -1
- package/bin/lib/workAgentPrecheck.mjs +4 -1
- package/bin/lib/workAgentTurnExecution.mjs +6 -3
- package/bin/lib/workAgentTurnOutcome.mjs +13 -3
- package/package.json +1 -1
package/bin/lib/agentCards.mjs
CHANGED
|
@@ -70,6 +70,24 @@ export function stashCard(path, taskId, spec) {
|
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
/**
|
|
74
|
+
* WHAT THE AGENT SAID IT DID NOT DELIVER on a card (2026-10-25, turnGaps.mjs),
|
|
75
|
+
* kept beside the spec so the pre-review can read the request, the diff and
|
|
76
|
+
* the agent's own account of what is missing together. Same append rule; the
|
|
77
|
+
* reader keeps the last declaration per card.
|
|
78
|
+
*/
|
|
79
|
+
export function stashGaps(path, taskId, gaps) {
|
|
80
|
+
if (!path || !Array.isArray(gaps)) return false;
|
|
81
|
+
const id = String(taskId ?? '').trim();
|
|
82
|
+
if (!id) return false;
|
|
83
|
+
try {
|
|
84
|
+
appendFileSync(path, JSON.stringify({ taskId: id.slice(0, 64), gaps }) + '\n');
|
|
85
|
+
return true;
|
|
86
|
+
} catch {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
73
91
|
/**
|
|
74
92
|
* What this box holds, newest spec per card, in the order the cards were worked.
|
|
75
93
|
*
|
|
@@ -89,6 +107,7 @@ export function readStash(path) {
|
|
|
89
107
|
* order stays the order the cards were first handed out — which is the order
|
|
90
108
|
* the branch was built in, and the order a reviewer reads them in. */
|
|
91
109
|
const byId = new Map();
|
|
110
|
+
const gapsById = new Map();
|
|
92
111
|
for (const line of raw.split('\n')) {
|
|
93
112
|
if (!line.trim()) continue;
|
|
94
113
|
let v;
|
|
@@ -99,10 +118,14 @@ export function readStash(path) {
|
|
|
99
118
|
}
|
|
100
119
|
if (!v || typeof v !== 'object') continue;
|
|
101
120
|
const taskId = typeof v.taskId === 'string' ? v.taskId.trim() : '';
|
|
121
|
+
if (taskId && Array.isArray(v.gaps)) {
|
|
122
|
+
gapsById.set(taskId, v.gaps);
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
102
125
|
const prompt = typeof v.prompt === 'string' ? v.prompt : '';
|
|
103
126
|
if (!taskId || !prompt.trim()) continue;
|
|
104
127
|
byId.set(taskId, prompt);
|
|
105
128
|
}
|
|
106
|
-
const all = [...byId].map(([taskId, prompt]) => ({ taskId, prompt }));
|
|
129
|
+
const all = [...byId].map(([taskId, prompt]) => ({ taskId, prompt, ...(gapsById.has(taskId) ? { gaps: gapsById.get(taskId) } : {}) }));
|
|
107
130
|
return all;
|
|
108
131
|
}
|
package/bin/lib/agentPlan.mjs
CHANGED
|
@@ -35,6 +35,7 @@ import { AGENT_TASK_KINDS } from './agentTaskKinds.mjs';
|
|
|
35
35
|
import { blockedAnswer } from './agentAsk.mjs';
|
|
36
36
|
import { isResearchUse } from './researchUse.mjs';
|
|
37
37
|
import { candidateObjects } from './modelJson.mjs';
|
|
38
|
+
import { parseGaps } from './turnGaps.mjs';
|
|
38
39
|
|
|
39
40
|
/** The biggest a single proposal may be. Bounds on a machine, not a policy:
|
|
40
41
|
* the server caps these again at its own boundary. */
|
|
@@ -272,6 +273,8 @@ export function parseTurnResult(text, scrub = envScrub) {
|
|
|
272
273
|
...(typeof v.automation === 'string' && v.automation.trim()
|
|
273
274
|
? { automation: scrub(v.automation.trim()) } : {}),
|
|
274
275
|
...(parseTestFindings(v.testFindings, scrub) ? { testFindings: parseTestFindings(v.testFindings, scrub) } : {}),
|
|
276
|
+
// What it says it did not deliver (turnGaps.mjs): absent is unknown.
|
|
277
|
+
...(parseGaps(v.gaps, scrub) ? { gaps: parseGaps(v.gaps, scrub) } : {}),
|
|
275
278
|
...progressOf(v, scrub),
|
|
276
279
|
raised: Array.isArray(v.raised)
|
|
277
280
|
? v.raised
|
|
@@ -14,6 +14,9 @@ an order alone is not a declared dependency. requirements.status=missing is
|
|
|
14
14
|
unavailable, never an empty brief. Discussions carry their existing omission
|
|
15
15
|
counts and may be unavailable; do not treat a shortened discussion as full
|
|
16
16
|
requirements. The brief and criteria marked full are supplied without clipping.
|
|
17
|
+
sourceChats are the person's exact words in the New task chat these cards were
|
|
18
|
+
planned in, up to each card's landing; a brief is the planner's paraphrase of
|
|
19
|
+
them, and links, references and file lists there are part of the request.
|
|
17
20
|
Human messages are exact recorded words across this agent, with the preceding
|
|
18
21
|
question when present. taskId on a message is where it was recorded, not an
|
|
19
22
|
implicit scope restriction. target.kind=agent explicitly addresses the whole
|
|
@@ -32,7 +35,9 @@ relying on an excluded decision; do not recover it from older native memory.
|
|
|
32
35
|
Latest outcomes and progress are reported summaries, not complete specifications,
|
|
33
36
|
proof that checks passed, human acceptance or evidence that bytes exist locally.
|
|
34
37
|
Stored outputs name exact ids/turns/hashes, not a successful local read. Retained
|
|
35
|
-
result selections preserve their exact scope.
|
|
38
|
+
result selections preserve their exact scope. localInputs lists the retained
|
|
39
|
+
paths that exist in this worktree this turn; any other retained path named in
|
|
40
|
+
the snapshot does not. Use the active turn's measured
|
|
36
41
|
input manifest and prerequisite receipts before relying on files; never replace
|
|
37
42
|
or regenerate missing selected inputs or assume queue knowledge discovers them.
|
|
38
43
|
Only execute the active turn's assigned work. Upcoming cards are context, not
|
|
@@ -44,6 +49,20 @@ only relevant prior findings/decisions/input identities, not the entire queue.
|
|
|
44
49
|
${fence('AGENT QUEUE SNAPSHOT', context)}`;
|
|
45
50
|
}
|
|
46
51
|
|
|
52
|
+
/** THE CARD'S SOURCE CHAT, as words the pre-review reads beside its spec
|
|
53
|
+
* (2026-10-25): the stash is per card and the snapshot is gone by review
|
|
54
|
+
* entry, so the person's request is kept with the spec it shaped. Empty when
|
|
55
|
+
* the card landed from no chat (or the server sent no source chats). */
|
|
56
|
+
export function sourceChatText(context, taskId) {
|
|
57
|
+
const chats = (context?.sourceChats?.chats ?? []).filter((c) => Array.isArray(c.taskIds) && c.taskIds.includes(taskId));
|
|
58
|
+
if (!chats.length) return '';
|
|
59
|
+
const said = chats.flatMap((c) => [
|
|
60
|
+
...(c.messages ?? []).map((m) => `${m.answering ? `(answering: ${m.answering})\n` : ''}${m.body}`),
|
|
61
|
+
...(c.omitted ? [`(${c.omitted} more message${c.omitted === 1 ? '' : 's'} omitted)`] : []),
|
|
62
|
+
]);
|
|
63
|
+
return `\n\nTHE PERSON'S OWN WORDS (the New task chat this card was planned in; the brief above paraphrases them):\n${said.join('\n\n')}`;
|
|
64
|
+
}
|
|
65
|
+
|
|
47
66
|
/** The gate supplies active identity/fences, not upcoming work to a child.
|
|
48
67
|
* Relevant findings still travel with the root's focused assignment. */
|
|
49
68
|
export function agentChildScope(job) {
|
|
@@ -82,6 +82,49 @@ is missing, so you lead:
|
|
|
82
82
|
outside decision — stop and name exactly that dependency; a skip never
|
|
83
83
|
supplies it.`;
|
|
84
84
|
|
|
85
|
+
/**
|
|
86
|
+
* THE WHOLE REQUEST, NOT ONLY THE BRIEF (2026-10-25) — every agent contract
|
|
87
|
+
* closes on it, before HELP_THEM_DECIDE.
|
|
88
|
+
*
|
|
89
|
+
* Eighteen merged runs were audited and most delivered part of what was asked
|
|
90
|
+
* while every check passed: a Codex planner could not open a Claude Design
|
|
91
|
+
* file and wrote "use only the described controls as scope" into the brief;
|
|
92
|
+
* the Claude agent, which could open it, never tried; an agent recast the
|
|
93
|
+
* design states between its cards as "decisions"; another satisfied "measure
|
|
94
|
+
* the waits" with a timer it wrote. The brief is a paraphrase, the person's
|
|
95
|
+
* words now ride the queue snapshot (`sourceChats`), a reference the agent
|
|
96
|
+
* can open is the agent's to open, and the delivery says what is missing
|
|
97
|
+
* (`gaps`, turnGaps.mjs) instead of leaving the person to find it.
|
|
98
|
+
*/
|
|
99
|
+
export const WHOLE_REQUEST = `THE WHOLE REQUEST, NOT ONLY THE BRIEF:
|
|
100
|
+
- A brief is a planner's paraphrase. When the queue snapshot carries
|
|
101
|
+
"sourceChats", those are the person's exact words from the chat this card
|
|
102
|
+
was planned in: read them before you start. Links, design references, file
|
|
103
|
+
lists and pasted text there are part of the request.
|
|
104
|
+
- A REFERENCE YOU CAN OPEN, YOU OPEN. Read every design or reference that the
|
|
105
|
+
brief, the source chat, the card's discussion or the person's messages link
|
|
106
|
+
to before you build against it. A claude.ai/design link is read with a
|
|
107
|
+
DesignSync tool where your CLI offers one: load it (ToolSearch
|
|
108
|
+
"select:DesignSync"), then get_file the named file and its imports,
|
|
109
|
+
read-only, never its write methods. A brief that says a reference "could not be opened", "is
|
|
110
|
+
unavailable" or "use only X as scope" describes the planner's access, not
|
|
111
|
+
yours: try it yourself. If you still cannot read it, name it in "gaps" and
|
|
112
|
+
build nothing that depends on guessing what it contains.
|
|
113
|
+
- COVER THE REQUEST, THEN SAY WHAT IS MISSING. Before you deliver, walk the
|
|
114
|
+
request: the source chat's words, every frame, state and rule a reference
|
|
115
|
+
you read shows or states, the brief and each "done when". Add "gaps" to the
|
|
116
|
+
delivered JSON object, one entry per item this card was meant to deliver
|
|
117
|
+
that you did not build ("not built"), built in part ("partly") or did not
|
|
118
|
+
check against the real thing ("unverified"), and per part of the request
|
|
119
|
+
that NO card in this run owns ("unowned"), so the person can file it:
|
|
120
|
+
"gaps": [{ "item": "what is missing", "status": "not built | partly | unverified | unowned", "why": "one sentence" }]
|
|
121
|
+
"gaps": [] says you walked the request and nothing is missing. Never recast
|
|
122
|
+
a gap as a decision: a choice you made in place of the request is a gap
|
|
123
|
+
("partly") with its reason.
|
|
124
|
+
- A "reproduce", "measure" or "verify" step is done only against the real
|
|
125
|
+
thing. A mock, timer or fixture you wrote yourself does not satisfy it:
|
|
126
|
+
list it as "unverified".`;
|
|
127
|
+
|
|
85
128
|
/**
|
|
86
129
|
* Rule 3, rule 4 and the final JSON a file-handing contract ends with.
|
|
87
130
|
* `ask` is rule 4's body after its number (the decision only a person can
|
|
@@ -119,6 +162,8 @@ or, if you are stopping to ask:
|
|
|
119
162
|
"progress" is REQUIRED on both shapes and REPLACES the previous one whole —
|
|
120
163
|
past tense, plain sentences, no card ids.
|
|
121
164
|
|
|
165
|
+
${WHOLE_REQUEST}
|
|
166
|
+
|
|
122
167
|
${HELP_THEM_DECIDE}`;
|
|
123
168
|
|
|
124
169
|
/**
|
|
@@ -11,6 +11,7 @@ import { CODEX_CHILD_THREAD_RE } from './conversationIds.mjs';
|
|
|
11
11
|
import { followJsonl } from './jsonlTail.mjs';
|
|
12
12
|
import { codexCollabUpdates, codexSpawnResult, statusOf } from './codexSubagentEvents.mjs';
|
|
13
13
|
import { NATIVE_AGENT_TOOLS } from './nativeAgentTools.mjs';
|
|
14
|
+
import { isSealedMessage } from './orchestratorNative.mjs';
|
|
14
15
|
import { SUBAGENT_CAP } from './subagentBounds.mjs';
|
|
15
16
|
|
|
16
17
|
const CODEX = NATIVE_AGENT_TOOLS.codex;
|
|
@@ -84,7 +85,7 @@ export function createCodexSubagentReader({ since = Date.now(), env = process.en
|
|
|
84
85
|
if (!COLLAB_CALLS.includes(name)) return;
|
|
85
86
|
let args;
|
|
86
87
|
try { args = JSON.parse(p.arguments); } catch { return; }
|
|
87
|
-
calls.set(p.call_id, { name, args });
|
|
88
|
+
calls.set(p.call_id, { name, args, namespace: p.namespace });
|
|
88
89
|
if (calls.size > 128) calls.delete(calls.keys().next().value);
|
|
89
90
|
const id = targetId(args.target ?? args.id);
|
|
90
91
|
if (id && name !== 'spawn_agent') {
|
|
@@ -122,7 +123,10 @@ export function createCodexSubagentReader({ since = Date.now(), env = process.en
|
|
|
122
123
|
const title = args.task_name ?? args.description ?? output.agent_nickname ?? '';
|
|
123
124
|
if (args.task_name) rememberTarget(args.task_name, id);
|
|
124
125
|
if (typeof path === 'string') rememberTarget(path, id);
|
|
125
|
-
|
|
126
|
+
// A V2 message is ciphertext (orchestratorNative.mjs `isSealedMessage`):
|
|
127
|
+
// its words are unknown, so the child carries no brief rather than that.
|
|
128
|
+
const sealed = isSealedMessage(call.namespace ? `${call.namespace}.${call.name}` : call.name, args.message);
|
|
129
|
+
report({ id, kind: args.agent_type ?? '', title, ...(sealed ? {} : { brief: args.message ?? '' }), status: 'running', now: '' });
|
|
126
130
|
return;
|
|
127
131
|
}
|
|
128
132
|
if (owner !== root && ev.type === 'response_item' && p.type === 'custom_tool_call') {
|
|
@@ -21,17 +21,22 @@ export function knowledgeFetcher({ fleetUrl, token, userAgent, maxBytes = KNOWLE
|
|
|
21
21
|
// A kept library item (0.97.0) is its sibling, `GET /fleet/library/:id` —
|
|
22
22
|
// the same credential, the same shape, the same caps.
|
|
23
23
|
const libraryBase = fleetEndpoint('library', fleetUrl);
|
|
24
|
-
|
|
24
|
+
// THE CAP IS THE FILE'S (2026-10-25): the sync holds a kept model under
|
|
25
|
+
// models/ or designs/ to 32 MiB (`knowledgeMaxFor`) and hands that cap here;
|
|
26
|
+
// this default alone turned every 20–32 MiB GLB into the over-cap sentinel,
|
|
27
|
+
// a failed hash on every poll, so a map the Library kept never reached an
|
|
28
|
+
// agent.
|
|
29
|
+
return async (id, { library = false, preview = false, fileIndex = null, maxBytes: cap = maxBytes } = {}) => {
|
|
25
30
|
const suffix = preview ? '/preview' : Number.isSafeInteger(fileIndex) && fileIndex >= 0 ? `/file/${fileIndex}` : '';
|
|
26
31
|
const res = await fetch(`${library ? libraryBase : base}/${encodeURIComponent(id)}${suffix}`, {
|
|
27
32
|
headers: { Authorization: `Bearer ${token}`, 'User-Agent': userAgent },
|
|
28
33
|
signal: AbortSignal.timeout(60_000),
|
|
29
34
|
});
|
|
30
35
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
31
|
-
if (Number(res.headers.get('content-length') ?? '0') >
|
|
36
|
+
if (Number(res.headers.get('content-length') ?? '0') > cap) {
|
|
32
37
|
// Returned rather than thrown: over the cap is a REFUSAL, permanent for
|
|
33
38
|
// this rev, not a failure the backoff should keep retrying.
|
|
34
|
-
return Buffer.alloc(
|
|
39
|
+
return Buffer.alloc(cap + 1);
|
|
35
40
|
}
|
|
36
41
|
return Buffer.from(await res.arrayBuffer());
|
|
37
42
|
};
|
|
@@ -217,7 +217,7 @@ export async function syncKnowledge({ checkoutDir, manifest, fetchFile, maxBytes
|
|
|
217
217
|
sha256: f.sha256,
|
|
218
218
|
bytes: f.bytes,
|
|
219
219
|
maxBytes: knowledgeMaxFor(f.name, maxBytes),
|
|
220
|
-
fetch: () => fetchFile(f.id),
|
|
220
|
+
fetch: () => fetchFile(f.id, { maxBytes: knowledgeMaxFor(f.name, maxBytes) }),
|
|
221
221
|
});
|
|
222
222
|
// Stale or failed clears `ok` and is retried (a stale file is better than
|
|
223
223
|
// a missing one until the retry lands); a refusal is listed as one.
|
|
@@ -299,7 +299,7 @@ async function syncLibrary({ dir, items, fetchFile, maxBytes, keep, result }) {
|
|
|
299
299
|
sha256: it.sha256,
|
|
300
300
|
bytes: it.bytes,
|
|
301
301
|
maxBytes: itemMax,
|
|
302
|
-
fetch: () => fetchFile(it.id, { library: true }),
|
|
302
|
+
fetch: () => fetchFile(it.id, { library: true, maxBytes: itemMax }),
|
|
303
303
|
beforeWrite: () => ensureLibraryParents(dir, it.path),
|
|
304
304
|
});
|
|
305
305
|
if (recordSyncState(result, entryState, it.path) === 'refused') {
|
|
@@ -317,7 +317,7 @@ async function syncLibrary({ dir, items, fetchFile, maxBytes, keep, result }) {
|
|
|
317
317
|
sha256: it.preview.sha256,
|
|
318
318
|
maxBytes: 2 * 1024 * 1024,
|
|
319
319
|
oversizeFails: true,
|
|
320
|
-
fetch: () => fetchFile(it.id, { library: true, preview: true }),
|
|
320
|
+
fetch: () => fetchFile(it.id, { library: true, preview: true, maxBytes: 2 * 1024 * 1024 }),
|
|
321
321
|
beforeWrite: () => ensureLibraryParents(dir, it.previewPath),
|
|
322
322
|
});
|
|
323
323
|
recordSyncState(result, previewState, it.previewPath);
|
|
@@ -335,7 +335,7 @@ async function syncLibrary({ dir, items, fetchFile, maxBytes, keep, result }) {
|
|
|
335
335
|
sha256: String(f.sha256),
|
|
336
336
|
bytes: f.bytes,
|
|
337
337
|
maxBytes: knowledgeMaxFor(f.path, maxBytes),
|
|
338
|
-
fetch: () => fetchFile(it.id, { library: true, fileIndex: f.index }),
|
|
338
|
+
fetch: () => fetchFile(it.id, { library: true, fileIndex: f.index, maxBytes: knowledgeMaxFor(f.path, maxBytes) }),
|
|
339
339
|
beforeWrite: () => ensureLibraryParents(dir, f.path),
|
|
340
340
|
});
|
|
341
341
|
if (recordSyncState(result, fileState, f.path) === 'refused') wanted[sub].delete(rel);
|
package/bin/lib/orchestrator.mjs
CHANGED
|
@@ -58,7 +58,9 @@ export async function createOrchestrator({ turnId, runtime, trace, admit, reques
|
|
|
58
58
|
// wakes never evicts a note input() accepted.
|
|
59
59
|
const inbox = new Map(), wakes = new Map();
|
|
60
60
|
const pending = new Map(), steps = new Map();
|
|
61
|
-
|
|
61
|
+
// Whether this run admitted a sealed (Codex V2) spawn or follow-up, whose
|
|
62
|
+
// child reads its contract at SubagentStart or its first tool instead.
|
|
63
|
+
let closed = false, polling = false, rootSession = null, offerAt = 0, sealedRun = false;
|
|
62
64
|
const identity = readOnlyRun ? { ...readOnlyRun, instance: DAEMON_INSTANCE } : { scope: 'agent', turnId, instance: DAEMON_INSTANCE };
|
|
63
65
|
const ended = () => closed || signal?.aborted;
|
|
64
66
|
const resolve = id => aliases.get(id) ?? id;
|
|
@@ -123,6 +125,19 @@ export async function createOrchestrator({ turnId, runtime, trace, admit, reques
|
|
|
123
125
|
return held.find(a => a.brief === brief) ?? held.find(a => title && a.title === title) ?? held.find(a => kind && a.kind === kind)
|
|
124
126
|
?? held[0] ?? open.find(a => a.brief === brief) ?? open.find(a => title && a.title === title);
|
|
125
127
|
};
|
|
128
|
+
// A child's contract, naming its record by its id (the words ride its
|
|
129
|
+
// prompt) or, with no id, a sealed Codex V2 child's record by its task
|
|
130
|
+
// name: the plain `Task name: /root/<name>` header Codex puts on its task
|
|
131
|
+
// message, which resolves through the spawn's task_name alias (`named`).
|
|
132
|
+
// The start hook cannot say which spawn a child came from; its name can.
|
|
133
|
+
const childContract = (id = null) => {
|
|
134
|
+
const ref = id ?? 'your task name: the "Task name:" line of your task message, such as /root/backend',
|
|
135
|
+
arg = id ? `agentId=${id}` : 'agentId set to that task name';
|
|
136
|
+
return (readOnlyRun
|
|
137
|
+
? `\n\nRead-only investigation agent record id: ${ref}. FIRST call card_orchestrator.register_agent with ${arg} before work (tool discovery is allowed). Share this checkout and its repository/knowledge read boundary. Write no file, edit nothing, commit nothing, start no work and call no board or staging tool. Return findings, source paths and uncertainties in your final message to the coordinator. Ask questions only through card_orchestrator.agent_question with ${arg}, and read its answer before proceeding. ${readOnlyRun.scope === 'capture' ? 'Call no flowviant tool, including streaming. Only the coordinator dedupes and authors staged outcomes and asks the person through the existing single-question flowviant-ask fence.' : 'Only the coordinator reconciles the complete workload and returns the proposal.'}`
|
|
138
|
+
: `\n\nCard orchestrator agent record id: ${ref}. FIRST call card_orchestrator.register_agent with ${arg} before doing work (tool discovery is allowed). Share this worktree and preserve others’ edits. Ask questions through card_orchestrator.agent_question with ${arg}; read its answer before proceeding. Only your orchestrator escalates person-only decisions. When you are done, end with your work or findings as your final message: that is how they reach your orchestrator, which owns the card’s final commit and JSON. agent_question is only for a question you cannot settle yourself, never for handing back results.`)
|
|
139
|
+
+ (!readOnlyRun ? assignmentContext() : '') + (access === 'read' && !readOnlyRun ? READ_ONLY_CHILD : '');
|
|
140
|
+
};
|
|
126
141
|
// Admitted: the offer is spent, and notes that paused this child are being
|
|
127
142
|
// delivered, so its checkpoint lets it work until their route lands.
|
|
128
143
|
const start = (a, now) => {
|
|
@@ -146,7 +161,8 @@ export async function createOrchestrator({ turnId, runtime, trace, admit, reques
|
|
|
146
161
|
if (aliases.size > 256) aliases.delete(aliases.keys().next().value);
|
|
147
162
|
if (calls.size > 128) calls.delete(calls.keys().next().value);
|
|
148
163
|
if (!records.has(id) && records.size >= SUBAGENT_CAP) return { deny: `This run already has ${SUBAGENT_CAP} agent records. Continue the work yourself.` };
|
|
149
|
-
|
|
164
|
+
// A sealed message's words are unknown: no brief, never its ciphertext.
|
|
165
|
+
const a = emit({ id, ...(spawn.sealed ? {} : { brief }), title: spawn.title ?? '', kind: spawn.kind ?? '', status: 'waiting', now: WAIT });
|
|
150
166
|
if (!slot(id)) { a.denied = resumed ? 'resume' : 'spawn'; return { deny: `${WAIT}. Agent id: ${id}. Keep working yourself; you will be told when this spawn can be retried. Do not wait for a child that has not started.` }; }
|
|
151
167
|
// Claude's SendMessage addresses a child by its Agent `name`, Codex V2 by its task name.
|
|
152
168
|
for (const name of spawn.names) if (filled(name) && name.length <= 160) aliases.set(name, id);
|
|
@@ -155,11 +171,9 @@ export async function createOrchestrator({ turnId, runtime, trace, admit, reques
|
|
|
155
171
|
// stream, and the parent is asked to say why in its own update.
|
|
156
172
|
const choice = childChoice(event, spawn, card);
|
|
157
173
|
if (choice) trace.prose('note', `Runs on ${choice}.`, id);
|
|
158
|
-
|
|
159
|
-
? `\n\nRead-only investigation agent record id: ${id}. FIRST call card_orchestrator.register_agent with agentId=${id} before work (tool discovery is allowed). Share this checkout and its repository/knowledge read boundary. Write no file, edit nothing, commit nothing, start no work and call no board or staging tool. Return findings, source paths and uncertainties in your final message to the coordinator. Ask questions only through card_orchestrator.agent_question with agentId=${id}, and read its answer before proceeding. ${readOnlyRun.scope === 'capture' ? 'Call no flowviant tool, including streaming. Only the coordinator dedupes and authors staged outcomes and asks the person through the existing single-question flowviant-ask fence.' : 'Only the coordinator reconciles the complete workload and returns the proposal.'}`
|
|
160
|
-
: `\n\nCard orchestrator agent record id: ${id}. FIRST call card_orchestrator.register_agent with agentId=${id} before doing work (tool discovery is allowed). Share this worktree and preserve others’ edits. Ask questions through card_orchestrator.agent_question with agentId=${id}; read its answer before proceeding. Only your orchestrator escalates person-only decisions. When you are done, end with your work or findings as your final message: that is how they reach your orchestrator, which owns the card’s final commit and JSON. agent_question is only for a question you cannot settle yourself, never for handing back results.`;
|
|
174
|
+
if (spawn.sealed) sealedRun = true;
|
|
161
175
|
return { context: `Agent record id: ${id}. Use it with card_orchestrator for routing and questions.${spawn.relocated ? ' Its isolation and cwd were removed: children share this card’s worktree on this machine.' : ''}${choice ? ` This child runs on ${choice}: name that choice and why in your next update.` : ''}`,
|
|
162
|
-
updatedInput: withChildContract(event, spawn,
|
|
176
|
+
updatedInput: withChildContract(event, spawn, spawn.sealed ? '' : childContract(id), chosen.brain) };
|
|
163
177
|
}
|
|
164
178
|
// Native readings never revive a settled record or start a waiting spawn
|
|
165
179
|
// or follow-up (Codex records a refused call's start anyway); only an
|
|
@@ -205,10 +219,13 @@ export async function createOrchestrator({ turnId, runtime, trace, admit, reques
|
|
|
205
219
|
steps.delete(native);
|
|
206
220
|
if (aliases.size > 256) aliases.delete(aliases.keys().next().value);
|
|
207
221
|
};
|
|
208
|
-
// Codex V2 names a child `backend` or `/root/backend` interchangeably
|
|
222
|
+
// Codex V2 names a child `backend` or `/root/backend` interchangeably, and
|
|
223
|
+
// a sealed child registers by the canonical path its task message wears.
|
|
209
224
|
const named = native => {
|
|
225
|
+
if (typeof native !== 'string') return resolve(native);
|
|
210
226
|
if (aliases.has(native) || records.has(native)) return resolve(native);
|
|
211
|
-
const
|
|
227
|
+
const near = k => k.endsWith(`/${native}`) || native.endsWith(`/${k}`);
|
|
228
|
+
const hits = new Set([...[...aliases].filter(([k]) => near(k)).map(([, v]) => v), ...[...records.keys()].filter(near)]);
|
|
212
229
|
return hits.size === 1 ? [...hits][0] : native;
|
|
213
230
|
};
|
|
214
231
|
function postSpawn(event) {
|
|
@@ -219,7 +236,7 @@ export async function createOrchestrator({ turnId, runtime, trace, admit, reques
|
|
|
219
236
|
if (failed) { release(id); emit({ id, status: 'stopped' }); }
|
|
220
237
|
}
|
|
221
238
|
function preResume(event) {
|
|
222
|
-
const input = event.tool_input ?? {}, { claude, native } = followupTarget(event);
|
|
239
|
+
const input = event.tool_input ?? {}, { claude, native, sealed } = followupTarget(event);
|
|
223
240
|
if (native === null) return claude ? null : { deny: 'Name the native agent to resume.' };
|
|
224
241
|
const id = named(native);
|
|
225
242
|
if (!records.has(id)) {
|
|
@@ -227,7 +244,7 @@ export async function createOrchestrator({ turnId, runtime, trace, admit, reques
|
|
|
227
244
|
// SendMessage also reaches "main" and teammates: only a known child is a follow-up.
|
|
228
245
|
if (claude) return null;
|
|
229
246
|
if (records.size >= SUBAGENT_CAP) return { deny: 'Continue the work in this run’s orchestrator.' };
|
|
230
|
-
emit({ id, kind: '', title: native, brief: typeof input.message === 'string' ? input.message : '', status: 'waiting', now: WAIT, resumable: true });
|
|
247
|
+
emit({ id, kind: '', title: native, ...(sealed ? {} : { brief: typeof input.message === 'string' ? input.message : '' }), status: 'waiting', now: WAIT, resumable: true });
|
|
231
248
|
}
|
|
232
249
|
if (spawnable(records.get(id))) return { deny: `Agent ${id} has not started. Retry its native spawn instead.` };
|
|
233
250
|
// A refused follow-up retried on its offered slot reaches a finished child.
|
|
@@ -236,6 +253,9 @@ export async function createOrchestrator({ turnId, runtime, trace, admit, reques
|
|
|
236
253
|
start(records.get(id), 'Continuing with the orchestrator’s note');
|
|
237
254
|
records.get(id).queued = running;
|
|
238
255
|
calls.set(event.tool_use_id, id);
|
|
256
|
+
// A sealed follow-up rides as written; its child registers by task name
|
|
257
|
+
// if this run has not bound it yet (the refusal at its first tool says how).
|
|
258
|
+
if (sealed) { sealedRun = true; return {}; }
|
|
239
259
|
return typeof input.message === 'string' ? { updatedInput: { ...input,
|
|
240
260
|
message: input.message + `\n\nCurrent card orchestrator agent record id: ${id}. Before continuing work, call card_orchestrator.register_agent with this id. Return questions to the orchestrator.` } } : {};
|
|
241
261
|
}
|
|
@@ -259,6 +279,10 @@ export async function createOrchestrator({ turnId, runtime, trace, admit, reques
|
|
|
259
279
|
return { deny: 'Your orchestrator has a note for this checkpoint. Stop editing and wait for its follow-up; keep your work so far.' };
|
|
260
280
|
}
|
|
261
281
|
function hook(ev) {
|
|
282
|
+
// A sealed child's contract, as it starts (measured on codex-cli 0.163
|
|
283
|
+
// and 0.164: this additionalContext reached the child, and the event
|
|
284
|
+
// names the child's agent_id but not the spawn that started it).
|
|
285
|
+
if (ev.hook_event_name === 'SubagentStart') return sealedRun && childOf(ev) ? { context: childContract().trimStart() } : {};
|
|
262
286
|
// A child's next tool round carries a follow-up queued to it; after its
|
|
263
287
|
// stop, its own tool use is the CLI running it again with that follow-up.
|
|
264
288
|
const kid = records.get(resolve(childOf(ev)));
|
|
@@ -275,12 +299,14 @@ export async function createOrchestrator({ turnId, runtime, trace, admit, reques
|
|
|
275
299
|
if (String(ev.tool_name).includes(ORCHESTRATOR_SERVER) && String(ev.tool_name).endsWith('register_agent')) {
|
|
276
300
|
// Binding the root would pause the orchestrator at its children's checkpoints.
|
|
277
301
|
if (!child) return { deny: 'Only a native child registers an agent record. As the orchestrator, use the record id with describe_agent, route_message or answer_agent.' };
|
|
278
|
-
const id =
|
|
279
|
-
if (!records.has(id)) return { deny: 'Use the agent record id in your prompt.' };
|
|
302
|
+
const id = named(ev.tool_input?.agentId);
|
|
303
|
+
if (!records.has(id)) return { deny: sealedRun ? `That names no agent in this run.${childContract()}` : 'Use the agent record id in your prompt.' };
|
|
280
304
|
bind(child, id);
|
|
281
305
|
}
|
|
306
|
+
// A sealed child that missed (or never got) its start's words reads
|
|
307
|
+
// them here, whole: an earlier run's follow-up, or a CLI without the hook.
|
|
282
308
|
if (child && !records.has(resolve(child)) && !isDiscoveryTool(ev.tool_name))
|
|
283
|
-
return { deny: 'Before working, call card_orchestrator.register_agent with the agent record id in your prompt. Tool discovery is allowed.' };
|
|
309
|
+
return { deny: sealedRun ? `Register with your orchestrator before working.${childContract()}` : 'Before working, call card_orchestrator.register_agent with the agent record id in your prompt. Tool discovery is allowed.' };
|
|
284
310
|
if ((child && access === 'read' || readOnlyRun) && isChildWriteTool(ev.tool_name)) return { deny: readOnlyRun ? 'This attempt only reads: no file writes.' : READ_ONLY_REFUSAL };
|
|
285
311
|
if (rootTools && captureToolName(ev.tool_name)) {
|
|
286
312
|
if (child || !rootSession || ev.session_id !== rootSession) return { deny: 'Only the top-level capture coordinator may call capture tools. Return findings to it.' };
|
|
@@ -314,7 +340,7 @@ export async function createOrchestrator({ turnId, runtime, trace, admit, reques
|
|
|
314
340
|
return held.length ? { ...result, context: [result.context, ...held].filter(Boolean).join('\n\n') } : result;
|
|
315
341
|
}
|
|
316
342
|
if (action === 'route_message') return route(args);
|
|
317
|
-
const id =
|
|
343
|
+
const id = named(args.agentId);
|
|
318
344
|
// A person’s answer starts a new CLI turn. The parent carries the earlier
|
|
319
345
|
// question and native identity in its own conversation; relay that known
|
|
320
346
|
// context instead of inventing a new child or requiring another question.
|
|
@@ -24,9 +24,22 @@ export function orchestratorHooks() {
|
|
|
24
24
|
return { PreToolUse: [{ matcher: '.*', hooks }], PostToolUse: [{ matcher: spawns, hooks }], PostToolUseFailure: [{ matcher: spawns, hooks }], SubagentStop: [{ hooks }] };
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
/**
|
|
28
|
+
* CODEX'S SubagentStart HANDS A SEALED CHILD ITS CONTRACT (2026-10-24). A V2
|
|
29
|
+
* spawn's message is ciphertext the gate must not edit (orchestratorNative.mjs
|
|
30
|
+
* `isSealedMessage`), so the words reach the child as this hook's
|
|
31
|
+
* additionalContext. Measured on real codex-cli 0.163.0 and 0.164.0 with this
|
|
32
|
+
* exact `matcher=".*"` row: the hook ran once per child and the child obeyed
|
|
33
|
+
* its context. Both CLIs start normally with the key (and with an unknown
|
|
34
|
+
* event's, offline). An older CLI is unmeasured, so it goes without; its
|
|
35
|
+
* children meet the same words when the gate refuses their first tool.
|
|
36
|
+
*/
|
|
37
|
+
export const CODEX_SUBAGENT_START_MIN = '0.163.0';
|
|
38
|
+
export function codexOrchestratorArgs(codexVersion) {
|
|
28
39
|
// A Codex `-c` value is TOML, quoted by the one quoter (codexToml.mjs).
|
|
29
40
|
const row = `{matcher=".*",hooks=[{type="command",command=${toml(command())},timeout=${ORCHESTRATOR_HOOK_TIMEOUT_S}}]}`;
|
|
41
|
+
const start = versionAtLeast(String(codexVersion ?? '').replace(/^codex-cli\s+/, ''), CODEX_SUBAGENT_START_MIN);
|
|
30
42
|
return ['--dangerously-bypass-hook-trust', '-c', 'features.hooks=true',
|
|
31
|
-
'-c', `hooks.PreToolUse=[${row}]`, '-c', `hooks.PostToolUse=[${row}]`, '-c', `hooks.SubagentStop=[${row}]
|
|
43
|
+
'-c', `hooks.PreToolUse=[${row}]`, '-c', `hooks.PostToolUse=[${row}]`, '-c', `hooks.SubagentStop=[${row}]`,
|
|
44
|
+
...(start ? ['-c', `hooks.SubagentStart=[${row}]`] : [])];
|
|
32
45
|
}
|
|
@@ -12,8 +12,27 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import { isBackgroundSpawnTool, isClaudeFollowupTool, isSpawnTool } from './nativeAgentTools.mjs';
|
|
14
14
|
|
|
15
|
+
/**
|
|
16
|
+
* A CODEX V2 MESSAGE IS CIPHERTEXT (2026-10-24). Since codex-cli 0.138
|
|
17
|
+
* (openai/codex PR #26210) the model writes spawn_agent's and followup_task's
|
|
18
|
+
* `message` encrypted, and the child's first request hands it back to be
|
|
19
|
+
* decrypted. The gate appended its contract to it, so every V2 child of the
|
|
20
|
+
* card runs from 10-19 to 10-24 (133 of them) died on that request with
|
|
21
|
+
* `invalid_encrypted_content` before its first tool. Measured on real 0.164:
|
|
22
|
+
* no hook and an identity rewrite both ran the child; one appended line failed
|
|
23
|
+
* it. Sealed, a message is relayed untouched and never read as a brief.
|
|
24
|
+
* V2 is the `collaboration` namespace (all 209 real messages, every one a
|
|
25
|
+
* `gAAAAA…` token); the token's shape is checked too, so a sealed message under
|
|
26
|
+
* another spelling is never edited. Codex V1 and Claude write plain text.
|
|
27
|
+
*/
|
|
28
|
+
const SEALED_TOKEN = /^gAAAAA[A-Za-z0-9_-]{40,}={0,2}$/;
|
|
29
|
+
export const isSealedMessage = (toolName, message) => typeof message === 'string' &&
|
|
30
|
+
!isBackgroundSpawnTool(toolName) && !isClaudeFollowupTool(toolName) &&
|
|
31
|
+
(/^collaboration[./]?(?:spawn_agent|followup_task)$/.test(String(toolName ?? '')) || SEALED_TOKEN.test(message));
|
|
32
|
+
|
|
15
33
|
/** A spawn's prompt and the fields a waiting record is matched and named by,
|
|
16
|
-
* or null when the gate cannot hold it (no call id or no text prompt).
|
|
34
|
+
* or null when the gate cannot hold it (no call id or no text prompt). A
|
|
35
|
+
* sealed message (`isSealedMessage`) gives no brief: its words are unknown. */
|
|
17
36
|
export function readSpawn(event) {
|
|
18
37
|
// A child stays in the card's worktree on this machine, inside the pool.
|
|
19
38
|
const { isolation, cwd, ...input } = event.tool_input ?? {}, callId = event.tool_use_id;
|
|
@@ -21,18 +40,20 @@ export function readSpawn(event) {
|
|
|
21
40
|
const items = Array.isArray(input.items) ? input.items : null;
|
|
22
41
|
const key = ['prompt', 'message'].find(k => typeof input[k] === 'string') ?? (items ? 'items' : null);
|
|
23
42
|
if (!callId || !key) return null;
|
|
24
|
-
const
|
|
25
|
-
|
|
43
|
+
const sealed = key === 'message' && isSealedMessage(event.tool_name, input.message);
|
|
44
|
+
const brief = sealed ? null : key === 'items' ? items.filter(i => i?.type === 'text' && typeof i.text === 'string').map(i => i.text).join('\n') : input[key];
|
|
45
|
+
return { callId, input, items, key, brief, sealed, relocated: isolation !== undefined || cwd !== undefined,
|
|
26
46
|
resume: typeof input.resume === 'string' ? input.resume : null,
|
|
27
47
|
title: input.description ?? input.task_name, kind: input.subagent_type ?? input.agent_type,
|
|
28
48
|
names: [input.name, input.task_name] };
|
|
29
49
|
}
|
|
30
50
|
|
|
31
51
|
/** The spawn's input as the gate rewrites it: the contract appended to its
|
|
32
|
-
* prompt
|
|
33
|
-
*
|
|
52
|
+
* prompt (never to a sealed message, whose child is handed it at its start),
|
|
53
|
+
* a Claude child sent to the background, and the child's model and effort
|
|
54
|
+
* (`brain`, from `childBrain`) where the spawn named none. */
|
|
34
55
|
export const withChildContract = (event, spawn, contract, brain = {}) => ({ ...spawn.input, ...brain, ...(isBackgroundSpawnTool(event.tool_name) ? { run_in_background: true } : {}),
|
|
35
|
-
...(spawn.key === 'items' ? { items: [...spawn.items, { type: 'text', text: contract.trimStart() }] } : { [spawn.key]: spawn.input[spawn.key] + contract }) });
|
|
56
|
+
...(spawn.sealed ? {} : spawn.key === 'items' ? { items: [...spawn.items, { type: 'text', text: contract.trimStart() }] } : { [spawn.key]: spawn.input[spawn.key] + contract }) });
|
|
36
57
|
|
|
37
58
|
/** Both CLIs' effort ladder, lowest first (Codex stops at xhigh). */
|
|
38
59
|
export const EFFORT_LADDER = Object.freeze(['low', 'medium', 'high', 'xhigh', 'max']);
|
|
@@ -178,11 +199,11 @@ export function spawnReceipt(event) {
|
|
|
178
199
|
return { ids, failed: Boolean(output?.is_error || output?.isError || event.error) };
|
|
179
200
|
}
|
|
180
201
|
|
|
181
|
-
/** Which child a follow-up addresses (null when it names none in bounds),
|
|
182
|
-
* whether it is Claude's SendMessage. */
|
|
202
|
+
/** Which child a follow-up addresses (null when it names none in bounds),
|
|
203
|
+
* whether it is Claude's SendMessage, and whether its message is sealed. */
|
|
183
204
|
export function followupTarget(event) {
|
|
184
205
|
const input = event.tool_input ?? {}, claude = isClaudeFollowupTool(event.tool_name);
|
|
185
206
|
// Measured Claude 2.1.302 PreToolUse SendMessage input: {to, message, recipient, content, …}.
|
|
186
207
|
const native = claude ? input.to ?? input.recipient ?? input.target ?? input.id : input.target ?? input.id;
|
|
187
|
-
return { claude, native: typeof native === 'string' && native.length <= 160 ? native : null };
|
|
208
|
+
return { claude, native: typeof native === 'string' && native.length <= 160 ? native : null, sealed: isSealedMessage(event.tool_name, input.message) };
|
|
188
209
|
}
|
package/bin/lib/prompts.mjs
CHANGED
|
@@ -24,7 +24,7 @@ import { AGENT_PREREQUISITE_CONTRACT } from './agentPrerequisiteContract.mjs';
|
|
|
24
24
|
import { agentTaskKindOf } from './agentTaskKinds.mjs';
|
|
25
25
|
// A card's type (0.106.0) likewise: the table holds the words, this file prints them.
|
|
26
26
|
import { AGENT_TASK_TYPES, agentTaskTypeFor, agentTaskTypeOf } from './agentTaskTypes.mjs';
|
|
27
|
-
import { ARTIFACT_CONTRACT_TAIL, HELP_THEM_DECIDE, LOOK_HEADLESS, SYSTEM_AGENT_DECK, SYSTEM_AGENT_IMAGE, SYSTEM_AGENT_MODEL } from './artifactContracts.mjs';
|
|
27
|
+
import { ARTIFACT_CONTRACT_TAIL, HELP_THEM_DECIDE, LOOK_HEADLESS, SYSTEM_AGENT_DECK, SYSTEM_AGENT_IMAGE, SYSTEM_AGENT_MODEL, WHOLE_REQUEST } from './artifactContracts.mjs';
|
|
28
28
|
import { isLibraryReference } from './knowledgeLibrary.mjs';
|
|
29
29
|
import { readCardThread } from './cardThread.mjs';
|
|
30
30
|
import { AGENT_FILE_LINE_MAX, AGENT_FILES_PER_MESSAGE_MAX, CARD_FILES_PER_TURN_MAX } from './agentFiles.mjs';
|
|
@@ -581,9 +581,19 @@ with the matching name your CLI exposes.
|
|
|
581
581
|
ask: each brief names the original ask, the part this card delivers and
|
|
582
582
|
what its sibling cards own, and carries the context its agent needs — the
|
|
583
583
|
design link or kept reference, the expected look and behaviour of its
|
|
584
|
-
area, the files you found.
|
|
585
|
-
|
|
586
|
-
|
|
584
|
+
area, the files you found. Copy the person's links, design references and
|
|
585
|
+
file lists into every brief they shape, word for word. If you could not
|
|
586
|
+
open or read a reference, say so in each brief and in \`assumed\`, and cut
|
|
587
|
+
by what the person described and the current product shows; never invent
|
|
588
|
+
what it contains. AN UNREAD REFERENCE NEVER NARROWS SCOPE: write "UNREAD
|
|
589
|
+
AT PLANNING: <the link or name>. The agent working this card opens it
|
|
590
|
+
itself (a Claude Code agent reads a claude.ai/design link with its
|
|
591
|
+
DesignSync tool), builds this card's share of all of it, and names any part
|
|
592
|
+
no card owns." Never write that the agent should use only what you could
|
|
593
|
+
read as its scope, and never state that the reference is unavailable to the
|
|
594
|
+
agent: your read access is not its. When the person names a design or
|
|
595
|
+
mockup without a link, look for its link in the repository's docs, decision
|
|
596
|
+
records and the Library, and carry what you found. For example,
|
|
587
597
|
"remake the settings page from this design", where the design shows a new
|
|
588
598
|
sidebar, a redesigned profile form, a billing tab and inline save
|
|
589
599
|
confirmations, is four cards, each carrying the design link and the
|
|
@@ -1268,6 +1278,8 @@ implementation and ask for the person's decision. Unknown generator-source
|
|
|
1268
1278
|
identity is not a claim that an embedded GLB image is the original PNG. Opening
|
|
1269
1279
|
a result handoff never authorizes an automatic asset commit or merge.
|
|
1270
1280
|
|
|
1281
|
+
${WHOLE_REQUEST}
|
|
1282
|
+
|
|
1271
1283
|
${HELP_THEM_DECIDE}`;
|
|
1272
1284
|
|
|
1273
1285
|
/**
|
|
@@ -1814,6 +1826,15 @@ HOW TO READ IT:
|
|
|
1814
1826
|
asked for that the diff does not show, changes that reach further than the
|
|
1815
1827
|
card did. Not formatting, not naming preferences, not the rewrite you would
|
|
1816
1828
|
have preferred.
|
|
1829
|
+
6. THE WHOLE REQUEST, AND WHAT THE AGENT SAYS IS MISSING. A card may carry
|
|
1830
|
+
THE PERSON'S OWN WORDS (the chat it was planned in) and the agent's
|
|
1831
|
+
declared gaps. Judge the diff against the words as well as the criteria:
|
|
1832
|
+
a screen, state, rule or linked design file the person asked for that the
|
|
1833
|
+
diff does not show and the gaps do not list is a concern, named in the
|
|
1834
|
+
card's note. A declared gap is the agent being honest, not a concern by
|
|
1835
|
+
itself; put the serious ones in "overall" so the reviewer meets them first.
|
|
1836
|
+
A linked design you cannot open is named in the note as unchecked, never
|
|
1837
|
+
judged from its name.
|
|
1817
1838
|
|
|
1818
1839
|
END YOUR TURN WITH ONE JSON OBJECT AND NOTHING AFTER IT, in a \`\`\`json fence:
|
|
1819
1840
|
|
package/bin/lib/runtimeCodex.mjs
CHANGED
|
@@ -347,7 +347,7 @@ export const CODEX_RUNTIME = {
|
|
|
347
347
|
if (a.includes('--ignore-user-config')) a.push(...(personal?.args ?? []));
|
|
348
348
|
|
|
349
349
|
a.push(...mcp, ...resultSchemaArgs);
|
|
350
|
-
if (gated) a.push(...codexOrchestratorArgs());
|
|
350
|
+
if (gated) a.push(...codexOrchestratorArgs(codexVersion));
|
|
351
351
|
a.push(streamInput ? '-' : codexPrompt({ system, prompt, profile }));
|
|
352
352
|
return a;
|
|
353
353
|
},
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WHAT THE AGENT SAYS IT DID NOT DELIVER (2026-10-25).
|
|
3
|
+
*
|
|
4
|
+
* An audit of eighteen merged runs found the same shape in most of them: the
|
|
5
|
+
* delivery report said what was done and how it was checked, and nothing in
|
|
6
|
+
* the contract asked what was NOT done against the request — so a dropped
|
|
7
|
+
* design state came back as a "decision" ("Before Review they keep the old
|
|
8
|
+
* per-card plates"), a faked measurement as "measured 2500ms", and the pre-review,
|
|
9
|
+
* grading only the card's own checklist, passed both. The contract's
|
|
10
|
+
* `WHOLE_REQUEST` now asks for `gaps`; this reads them and says them where the
|
|
11
|
+
* person reviews (Verification on a code card, the summary on any other).
|
|
12
|
+
*
|
|
13
|
+
* THREE STATES, the house rule: no key is unknown and renders NOTHING (an
|
|
14
|
+
* older prompt, a resumed conversation that never saw it); `[]` is the
|
|
15
|
+
* agent's claim that it walked the request and nothing is missing; entries are
|
|
16
|
+
* listed. A malformed entry costs that entry alone.
|
|
17
|
+
*/
|
|
18
|
+
export const GAP_STATUSES = ['not built', 'partly', 'unverified', 'unowned'];
|
|
19
|
+
export const MAX_GAPS = 40;
|
|
20
|
+
export const MAX_GAP_ITEM = 300;
|
|
21
|
+
export const MAX_GAP_WHY = 800;
|
|
22
|
+
|
|
23
|
+
const cut = (s, n) => (s.length > n ? s.slice(0, n - 1) + '…' : s);
|
|
24
|
+
|
|
25
|
+
/** The declared gaps, scrubbed then bounded; undefined when none were declared. */
|
|
26
|
+
export function parseGaps(raw, scrub) {
|
|
27
|
+
if (!Array.isArray(raw)) return undefined;
|
|
28
|
+
const out = [];
|
|
29
|
+
for (const g of raw) {
|
|
30
|
+
if (out.length >= MAX_GAPS) break;
|
|
31
|
+
if (!g || typeof g !== 'object' || typeof g.item !== 'string' || !g.item.trim()) continue;
|
|
32
|
+
const status = typeof g.status === 'string' ? g.status.trim().toLowerCase() : '';
|
|
33
|
+
if (!GAP_STATUSES.includes(status)) continue;
|
|
34
|
+
const why = typeof g.why === 'string' && g.why.trim() ? cut(scrub(g.why.trim()), MAX_GAP_WHY) : null;
|
|
35
|
+
out.push({ item: cut(scrub(g.item.trim()), MAX_GAP_ITEM), status, ...(why ? { why } : {}) });
|
|
36
|
+
}
|
|
37
|
+
return out;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** The section the person reads, or null for no declaration. */
|
|
41
|
+
export function gapsMarkdown(gaps) {
|
|
42
|
+
if (!Array.isArray(gaps)) return null;
|
|
43
|
+
if (!gaps.length) return '**Gaps against the request:** none reported.';
|
|
44
|
+
return ['**Gaps against the request**', ...gaps.map((g) => `- **${g.status}** — ${oneLine(g.item)}${g.why ? `: ${oneLine(g.why)}` : ''}`)].join('\n');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const oneLine = (s) => s.replace(/\s*\n\s*/g, ' ');
|
|
@@ -46,6 +46,6 @@ export function validateReviewPlan(plan) {
|
|
|
46
46
|
|
|
47
47
|
export function uiReviewInstructions(argv) {
|
|
48
48
|
return `LOCAL UI SELF-REVIEW — before delivering UI-changing code, use this turn's owned headless runner: ${JSON.stringify([...argv, '<plan.json>'])}.
|
|
49
|
-
Read repository preview scripts and affected acceptance criteria first. Write a JSON plan with command (argv array), basis (repository file/script), cwd (relative directory, optional), port (dedicated port explicitly set in command), readyPath, fixture: { isolated: true, description }, and scenarios: [{ name, path, actions: [{ type, selector, value }] }]. Actions: click, fill, submit, assertText, assertUrl (value is a local path), waitFor. Use only disposable local fixtures; never production data, real email, payments, personal sessions or credentials. The runner starts and measures its own preview and permits only that preview's resources. External dependencies are blocked and reported; use local fixtures instead.
|
|
49
|
+
Read repository preview scripts and affected acceptance criteria first. Write a JSON plan with command (argv array), basis (repository file/script), cwd (relative directory, optional), port (dedicated port explicitly set in command), readyPath, fixture: { isolated: true, description }, and scenarios: [{ name, path, actions: [{ type, selector, value }] }]. Actions: click, fill, submit, assertText, assertUrl (value is a local path), waitFor. Use only disposable local fixtures; never production data, real email, payments, personal sessions or credentials. The runner starts and measures its own preview and permits only that preview's resources. External dependencies are blocked and reported; use local fixtures instead. Shape the fixture from the case the request describes, not from your implementation: the person's screenshot or report (its counts, names and states, such as a 23-card agent or a page with no stored render) or a committed export of real data among the repository's test fixtures when one exists. A fixture shaped only by your own code shows the code renders, not that the request is met: list that as an "unverified" gap.
|
|
50
50
|
Run desktop and mobile scenarios; read the returned report and OPEN EVERY returned screenshot using card_ui_review.inspect_screenshot when that tool is actually mounted (it returns the exact PNG as native image input), or your runtime's actual image-input tool (for example Read or view_image). Record which files you visually inspected and what you observed in delivery verification. A PNG path, capture success, tool listing or a skill is not proof of inspection. If image input is unavailable, report visual inspection skipped with its measured reason. Check console/network findings, expected and observed interaction results. Fix in-scope problems in this same worktree and rerun affected checks. At most ${UI_REVIEW_MAX_RUNS} runs per turn, each at most ${UI_REVIEW_TIMEOUT_MS / 1000}s; at the bound deliver remaining findings honestly. Later source edits invalidate earlier evidence; commit before the final run when possible. Include changed routes, ordered actions/outcomes, viewport sizes, screenshots, changes prompted by review and every unrun/unavailable check with reasons in verification. Missing browser, runnable preview or safe fixtures is unavailable, never passed; do not install tools or weaken isolation. This review never approves or merges work. For non-UI changes, state why browser review is not applicable.`;
|
|
51
51
|
}
|
|
@@ -11,6 +11,7 @@ import { FLEET_URL } from './config.mjs';
|
|
|
11
11
|
import { gitOrNull } from './git.mjs';
|
|
12
12
|
import { parsePrecheck } from './agentPlan.mjs';
|
|
13
13
|
import { readStash } from './agentCards.mjs';
|
|
14
|
+
import { gapsMarkdown } from './turnGaps.mjs';
|
|
14
15
|
import { c, note } from './ui.mjs';
|
|
15
16
|
import { runTurn as defaultRunTurn } from './runTurn.mjs';
|
|
16
17
|
import { SYSTEM_PRECHECK, AGENT_PRECHECK_KICKOFF } from './prompts.mjs';
|
|
@@ -222,7 +223,9 @@ export function createWorkAgentPrecheck({ baseRef, admit, sessionMetaPath, workC
|
|
|
222
223
|
turnArgs: {
|
|
223
224
|
prompt: AGENT_PRECHECK_KICKOFF({
|
|
224
225
|
agentName,
|
|
225
|
-
|
|
226
|
+
// Each spec with the agent's own account of what it did not
|
|
227
|
+
// deliver; no declaration is said, never read as "nothing missing".
|
|
228
|
+
cards: stash.map((s) => `${s.prompt}\n\n${gapsMarkdown(s.gaps) ?? "The agent declared no gaps list for this card."}`).join('\n---\n'),
|
|
226
229
|
missingSpecs,
|
|
227
230
|
commits: log.text,
|
|
228
231
|
diffCommand: `git diff ${repository?.baseRef ?? baseRef()}...HEAD`,
|
|
@@ -2,7 +2,7 @@ import { reportLocalCleanup } from './localCleanupReport.mjs';
|
|
|
2
2
|
import { createManagedLocalCleanup } from './managedLocalCleanup.mjs';
|
|
3
3
|
import { createCaptureRetention } from './referenceCaptures.mjs';
|
|
4
4
|
import { imageRevisionInstructions } from './artifactIteration.mjs';
|
|
5
|
-
import { agentChildScope } from './agentQueueContext.mjs';
|
|
5
|
+
import { agentChildScope, sourceChatText } from './agentQueueContext.mjs';
|
|
6
6
|
import { measureTestFindings } from './testFindings.mjs';
|
|
7
7
|
import { fetchResultInputs } from './resultInputs.mjs';
|
|
8
8
|
import { agentWork } from './workActivity.mjs';
|
|
@@ -33,7 +33,7 @@ import { setRuntimeLimit } from './daemonState.mjs';
|
|
|
33
33
|
import { gitOrNull } from './git.mjs';
|
|
34
34
|
import { turnCommits } from './turnReceipts.mjs';
|
|
35
35
|
import { addUsage } from './turnUsage.mjs';
|
|
36
|
-
import { stashCard } from './agentCards.mjs';
|
|
36
|
+
import { stashCard, stashGaps } from './agentCards.mjs';
|
|
37
37
|
import { c } from './ui.mjs';
|
|
38
38
|
import { runTurn } from './runTurn.mjs';
|
|
39
39
|
import { liveGroups, processesSupported } from './processes.mjs';
|
|
@@ -326,7 +326,7 @@ export function createAgentTurnExecution({
|
|
|
326
326
|
stashCard(
|
|
327
327
|
sessionMetaPath(wt, 'flowviant-agent-cards', agentId),
|
|
328
328
|
job.task.id,
|
|
329
|
-
AGENT_TASK_SPEC(job.task, job.cardThread, files.card)
|
|
329
|
+
AGENT_TASK_SPEC(job.task, job.cardThread, files.card) + sourceChatText(job.queueContext, job.task.id)
|
|
330
330
|
);
|
|
331
331
|
}
|
|
332
332
|
|
|
@@ -845,6 +845,9 @@ export function createAgentTurnExecution({
|
|
|
845
845
|
modelExports: intermediateDelivery?.modelExports,
|
|
846
846
|
});
|
|
847
847
|
appendUiReviewEvidence(body, uiReviewEvidence);
|
|
848
|
+
// The agent's own account of what it did not deliver, for the pre-review.
|
|
849
|
+
if (job.kind === 'task' && job.task && body.outcome === 'delivered' && res?.gaps)
|
|
850
|
+
stashGaps(sessionMetaPath(wt, 'flowviant-agent-cards', agentId), job.task.id, res.gaps);
|
|
848
851
|
if (body.outcome === 'delivered') intermediateDelivery?.commit();
|
|
849
852
|
// This place's writer lock is still held. Measure the exited children
|
|
850
853
|
// and their groups before releasing any file requirements. Dirty repository files are unrelated to managed scratch eligibility.
|
|
@@ -34,6 +34,7 @@ import { usableImageArtifact } from './imageArtifact.mjs';
|
|
|
34
34
|
import { withoutTurnDeclaration } from './turnDeclaration.mjs';
|
|
35
35
|
import { ANSWER_DELIVERED_USES, researchUseOf, withResearchHeader } from './researchUse.mjs';
|
|
36
36
|
import { measuredModelExports } from './intermediateArtifacts.mjs';
|
|
37
|
+
import { gapsMarkdown } from './turnGaps.mjs';
|
|
37
38
|
|
|
38
39
|
/** Materialization runs only for a declared delivery; settlement remains the
|
|
39
40
|
* sole parser and the sole authority deciding whether it really delivered. */
|
|
@@ -166,6 +167,11 @@ export function agentTurnSettlement({ turnId, job, kind, out, lastAnswer, usage,
|
|
|
166
167
|
: prerequisite?.taskKind === 'image' && (job.runtime ?? 'claude') !== 'codex'
|
|
167
168
|
? 'This agent runs on a CLI that cannot generate images. Image cards require Codex’s own image generation; this agent cannot run the requested texture work.'
|
|
168
169
|
: null;
|
|
170
|
+
/* WHAT IT SAYS IT DID NOT DELIVER (turnGaps.mjs), where the person reads
|
|
171
|
+
the delivery: Verification on a code card, which carries it; the summary
|
|
172
|
+
on every other kind, which has no Verification lane. */
|
|
173
|
+
const gaps = res.outcome === 'delivered' && !cannotRequest ? gapsMarkdown(res.gaps) : null;
|
|
174
|
+
const verification = [res.verification, kind.posture === 'build' ? gaps : null].filter(Boolean).join('\n\n');
|
|
169
175
|
return report(
|
|
170
176
|
{
|
|
171
177
|
turnId,
|
|
@@ -173,16 +179,20 @@ export function agentTurnSettlement({ turnId, job, kind, out, lastAnswer, usage,
|
|
|
173
179
|
outcome: res.outcome,
|
|
174
180
|
answer: cannotRequest ?? envScrub((kind.posture === 'research' && researchUse
|
|
175
181
|
? withResearchHeader(researchUse, res.answer ?? '') : res.answer ?? '') +
|
|
176
|
-
(job.task?.taskType === 'tests' && res.raised?.length ? '\n\nAdditional CLI-reported findings (not filed as fixes):\n' + res.raised.map(r => `${r.title}\n${r.brief ?? ''}`).join('\n\n') : '')
|
|
182
|
+
(job.task?.taskType === 'tests' && res.raised?.length ? '\n\nAdditional CLI-reported findings (not filed as fixes):\n' + res.raised.map(r => `${r.title}\n${r.brief ?? ''}`).join('\n\n') : '') +
|
|
183
|
+
(gaps && kind.posture !== 'build' ? `\n\n${gaps}` : '')),
|
|
177
184
|
// CLI-authored evidence travels separately from the short answer and
|
|
178
185
|
// measured commits/checks. Older turns simply send no report.
|
|
179
|
-
...(kind.posture === 'build' && res.outcome === 'delivered' &&
|
|
180
|
-
? { verification: envScrub(
|
|
186
|
+
...(kind.posture === 'build' && res.outcome === 'delivered' && verification
|
|
187
|
+
? { verification: envScrub(verification) } : {}),
|
|
181
188
|
...(kind.posture === 'build' && res.outcome === 'delivered' && res.handover
|
|
182
189
|
? { handover: envScrub(res.handover) } : {}),
|
|
183
190
|
...(kind.posture === 'build' && res.outcome === 'delivered' && res.automation
|
|
184
191
|
? { automation: envScrub(res.automation) } : {}),
|
|
185
192
|
...(commits.length ? { commits } : {}),
|
|
193
|
+
// …and the same gaps as data, so the server can propose the parts no
|
|
194
|
+
// card owned (`gapFollowUps.ts`). A report: an older server drops it.
|
|
195
|
+
...(gaps && res.gaps?.length ? { gaps: res.gaps } : {}),
|
|
186
196
|
// SCRUBBED like the answer beside it: a raised card lands in the
|
|
187
197
|
// project doc every member reads, and a brief quoting the `.env` the
|
|
188
198
|
// agent just read ("value sk_live_… is logged in pay.ts") went out
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "flowviant",
|
|
3
|
-
"version": "0.125.
|
|
3
|
+
"version": "0.125.22",
|
|
4
4
|
"description": "Run your own coding CLIs as build agents for Flowviant \u2014 Claude Code, Codex or Antigravity, on your own credentials. Holds your sessions, keeps a worktree per tab, and ships branches on your word.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|