flowviant 0.125.30 → 0.125.31
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 +8 -0
- package/bin/lib/agentQueueContext.mjs +6 -0
- package/bin/lib/agentSplit.mjs +90 -0
- package/bin/lib/architectureContract.mjs +57 -0
- package/bin/lib/architectureReport.mjs +38 -0
- package/bin/lib/artifactContracts.mjs +21 -2
- package/bin/lib/claudeInput.mjs +1 -1
- package/bin/lib/claudeSubagents.mjs +46 -6
- package/bin/lib/claudeWorkflows.mjs +94 -0
- package/bin/lib/cliLimit.mjs +5 -0
- package/bin/lib/codexInput.mjs +191 -0
- package/bin/lib/codexSubagentEvents.mjs +3 -1
- package/bin/lib/codexSubagents.mjs +34 -9
- package/bin/lib/conversationOutput.mjs +14 -0
- package/bin/lib/crossCliChild.mjs +327 -0
- package/bin/lib/crossCliLaunch.mjs +92 -0
- package/bin/lib/crossCliPosture.mjs +55 -0
- package/bin/lib/crossCliResearch.mjs +19 -0
- package/bin/lib/fixtures/workflow-ultracode.jsonl +22 -0
- package/bin/lib/nativeAgentTools.mjs +12 -5
- package/bin/lib/orchestrator.mjs +24 -7
- package/bin/lib/orchestratorClient.mjs +8 -2
- package/bin/lib/orchestratorPrompt.mjs +13 -5
- package/bin/lib/orchestratorProtocol.mjs +18 -0
- package/bin/lib/outsideCli.mjs +124 -0
- package/bin/lib/prompts.mjs +7 -5
- package/bin/lib/runTurn.mjs +30 -8
- package/bin/lib/runtimeClaude.mjs +7 -3
- package/bin/lib/runtimeCodex.mjs +12 -6
- package/bin/lib/runtimeLimits.mjs +59 -1
- package/bin/lib/splitGate.mjs +115 -0
- package/bin/lib/subagentBounds.mjs +26 -0
- package/bin/lib/subagentLedger.mjs +110 -0
- package/bin/lib/trace.mjs +134 -6
- package/bin/lib/turnDeclaration.mjs +4 -4
- package/bin/lib/turnGaps.mjs +6 -2
- package/bin/lib/turnSubagents.mjs +23 -9
- package/bin/lib/wikiDraft.mjs +9 -0
- package/bin/lib/workAgentResume.mjs +11 -2
- package/bin/lib/workAgentTurnExecution.mjs +31 -7
- package/bin/lib/workAgentTurnOutcome.mjs +11 -0
- package/bin/lib/workAgentTurnReports.mjs +5 -2
- package/package.json +1 -1
package/bin/lib/agentPlan.mjs
CHANGED
|
@@ -33,6 +33,7 @@ import { scrub as envScrub, scrubCut } from './uplinkScrub.mjs';
|
|
|
33
33
|
import { groupDependencies } from './planDependencies.mjs';
|
|
34
34
|
import { AGENT_TASK_KINDS } from './agentTaskKinds.mjs';
|
|
35
35
|
import { blockedAnswer } from './agentAsk.mjs';
|
|
36
|
+
import { splitAnswer } from './agentSplit.mjs';
|
|
36
37
|
import { isResearchUse } from './researchUse.mjs';
|
|
37
38
|
import { candidateObjects } from './modelJson.mjs';
|
|
38
39
|
import { parseGaps } from './turnGaps.mjs';
|
|
@@ -258,6 +259,13 @@ export function parseTurnResult(text, scrub = envScrub) {
|
|
|
258
259
|
// as plain text.
|
|
259
260
|
return { outcome: 'question', answer: blockedAnswer(scrub(question), v, scrub), ...progressOf(v, scrub) };
|
|
260
261
|
}
|
|
262
|
+
if (v.status === 'split') {
|
|
263
|
+
// A split proposal (2026-11-06, agentSplit.mjs) is a question: the agent
|
|
264
|
+
// freezes until the person answers Use N subagents or Do it yourself.
|
|
265
|
+
const answer = splitAnswer(v, scrub);
|
|
266
|
+
if (!answer) continue;
|
|
267
|
+
return { outcome: 'question', answer, ...progressOf(v, scrub) };
|
|
268
|
+
}
|
|
261
269
|
if (v.status === 'delivered') {
|
|
262
270
|
return {
|
|
263
271
|
outcome: 'delivered',
|
|
@@ -17,6 +17,12 @@ requirements. The brief and criteria marked full are supplied without clipping.
|
|
|
17
17
|
sourceChats are the person's exact words in the New task chat these cards were
|
|
18
18
|
planned in, up to each card's landing; a brief is the planner's paraphrase of
|
|
19
19
|
them, and links, references and file lists there are part of the request.
|
|
20
|
+
${job.gapContinuationAccepted === true ? 'This server supports disposition=required: unfinished required work continues in this card before Review.' : 'This server has not advertised automatic gap continuation. Finish required work within this turn; if it remains unfinished, ask through a blocked reply instead of declaring delivery.'}
|
|
21
|
+
relatedWork names sibling cards from the same request across agents, including
|
|
22
|
+
completed cards. Outside this queue does not mean unowned. Link the actual
|
|
23
|
+
ownerTaskId instead of proposing or implementing a sibling's work again.
|
|
24
|
+
Only a card's observed completion is recorded; verify a reported recurrence
|
|
25
|
+
instead of assuming an earlier Done card solved the current failure.
|
|
20
26
|
Human messages are exact recorded words across this agent, with the preceding
|
|
21
27
|
question when present. taskId on a message is where it was recorded, not an
|
|
22
28
|
implicit scope restriction. target.kind=agent explicitly addresses the whole
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLAUDE SUGGESTS SUBAGENTS (2026-11-06, D28–D31, epic E5): the split a card
|
|
3
|
+
* agent proposes instead of starting children that edit files, read off its
|
|
4
|
+
* final JSON and handed on as a QUESTION.
|
|
5
|
+
*
|
|
6
|
+
* {"status": "split", "split": {"reason": "…", "parts": [
|
|
7
|
+
* {"title": "Port gizmo shaders", "touches": ["src/gizmo.ts"], "edits": true},
|
|
8
|
+
* {"title": "Audit textures", "touches": ["assets/**"], "edits": false}]},
|
|
9
|
+
* "progress": "…"}
|
|
10
|
+
*
|
|
11
|
+
* The agent freezes on it as a native CLI freezes on its ask tool (D31): the
|
|
12
|
+
* turn settles `question`, the board says Needs you, and the person answers
|
|
13
|
+
* through the existing answer lane. So the answer is built as every blocked
|
|
14
|
+
* question's is (agentAsk.mjs): prose, then ONE ```flowviant-ask fence — here
|
|
15
|
+
* with the two options the server and the page key on, `Use N subagents` and
|
|
16
|
+
* `Do it yourself` (the shared `splitUseLabel` / `SPLIT_SOLO_LABEL`), and the
|
|
17
|
+
* parts under an extra `split` key the ask reader ignores. A screen that
|
|
18
|
+
* knows nothing of splits still draws an answerable question.
|
|
19
|
+
*
|
|
20
|
+
* BOUNDED, AND NEVER LOST. `SPLIT_BOUNDS` is the shared one, key for key. A
|
|
21
|
+
* proposal past any bound, or with a hostile character, loses its fence whole
|
|
22
|
+
* and goes on as a plain-text question listing what could be read, exactly as
|
|
23
|
+
* malformed options do: the person can still answer it in words.
|
|
24
|
+
*/
|
|
25
|
+
import { ASK_HOSTILE } from './agentAsk.mjs';
|
|
26
|
+
|
|
27
|
+
/** The shared `SPLIT_BOUNDS` (packages/shared/src/schemas/subagentSplit.ts). */
|
|
28
|
+
export const SPLIT_BOUNDS = Object.freeze({ parts: 12, title: 120, touches: 8, touch: 200, reason: 600 });
|
|
29
|
+
export const SPLIT_SOLO_LABEL = 'Do it yourself';
|
|
30
|
+
export const splitUseLabel = n => `Use ${n} subagent${n === 1 ? '' : 's'}`;
|
|
31
|
+
export const SPLIT_QUESTION = 'Use subagents?';
|
|
32
|
+
|
|
33
|
+
const one = (v, max, scrub) => {
|
|
34
|
+
if (typeof v !== 'string') return null;
|
|
35
|
+
const t = scrub(v).trim();
|
|
36
|
+
return t && t.length <= max && !ASK_HOSTILE.test(t) ? t : null;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/** The declared split → `{ parts, reason }`, or null when any part breaks a
|
|
40
|
+
* bound. `scrub` runs before each field is measured. */
|
|
41
|
+
export function declaredSplit(raw, scrub = s => s) {
|
|
42
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return null;
|
|
43
|
+
if (!Array.isArray(raw.parts) || !raw.parts.length || raw.parts.length > SPLIT_BOUNDS.parts) return null;
|
|
44
|
+
const parts = [];
|
|
45
|
+
for (const p of raw.parts) {
|
|
46
|
+
if (!p || typeof p !== 'object' || Array.isArray(p) || typeof p.edits !== 'boolean') return null;
|
|
47
|
+
const title = one(p.title, SPLIT_BOUNDS.title, scrub);
|
|
48
|
+
const touches = p.touches === undefined ? [] : p.touches;
|
|
49
|
+
if (!title || !Array.isArray(touches) || touches.length > SPLIT_BOUNDS.touches) return null;
|
|
50
|
+
const kept = [];
|
|
51
|
+
for (const t of touches) { const k = one(t, SPLIT_BOUNDS.touch, scrub); if (!k) return null; kept.push(k); }
|
|
52
|
+
parts.push({ title, touches: kept, edits: p.edits });
|
|
53
|
+
}
|
|
54
|
+
let reason = null;
|
|
55
|
+
if (raw.reason !== undefined && raw.reason !== null) {
|
|
56
|
+
if (typeof raw.reason !== 'string') return null;
|
|
57
|
+
const r = scrub(raw.reason).trim();
|
|
58
|
+
if (r.length > SPLIT_BOUNDS.reason || ASK_HOSTILE.test(r.replace(/\n/g, ' '))) return null;
|
|
59
|
+
reason = r || null;
|
|
60
|
+
}
|
|
61
|
+
return { parts, reason };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const partLine = (p, i) => `${i + 1}. ${p.title}${p.touches.length ? ` — ${p.touches.join(', ')}` : ''} · ${p.edits ? 'edits' : 'read-only'}`;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* THE ANSWER A SPLIT TURN HANDS ON, or null when it declared no parts at all
|
|
68
|
+
* (then it is no answer, as a blocked turn with no question is not). The
|
|
69
|
+
* prose names the parts, so a surface that draws no fence still shows them.
|
|
70
|
+
*/
|
|
71
|
+
export function splitAnswer(v, scrub = s => s) {
|
|
72
|
+
const split = declaredSplit(v?.split, scrub);
|
|
73
|
+
if (!split) {
|
|
74
|
+
// Readable parts, past a bound: a plain question, answerable in words.
|
|
75
|
+
const titles = Array.isArray(v?.split?.parts) ? v.split.parts.map(p => typeof p?.title === 'string' ? scrub(p.title).trim().slice(0, SPLIT_BOUNDS.title) : '').filter(Boolean) : [];
|
|
76
|
+
if (!titles.length) return null;
|
|
77
|
+
return `${SPLIT_QUESTION} I would split this card into ${titles.length} parts:\n\n${titles.map((t, i) => `${i + 1}. ${t}`).join('\n')}`;
|
|
78
|
+
}
|
|
79
|
+
const n = split.parts.length;
|
|
80
|
+
const prose = `${split.reason ? `${split.reason}\n\n` : ''}${split.parts.map(partLine).join('\n')}\n\n${SPLIT_QUESTION}`;
|
|
81
|
+
const ask = {
|
|
82
|
+
question: SPLIT_QUESTION, header: 'Subagents',
|
|
83
|
+
options: [
|
|
84
|
+
{ label: splitUseLabel(n), description: n === 1 ? 'Run this part as a subagent.' : 'Run the parts side by side.' },
|
|
85
|
+
{ label: SPLIT_SOLO_LABEL, description: 'Claude does every part itself, one after another.' },
|
|
86
|
+
],
|
|
87
|
+
split,
|
|
88
|
+
};
|
|
89
|
+
return `${prose}\n\n\`\`\`flowviant-ask\n${JSON.stringify(ask)}\n\`\`\``;
|
|
90
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/** Generated by the existing vault-scoped reader; no new infrastructure access. */
|
|
2
|
+
export const ARCHITECTURE_EXAMPLE = {
|
|
3
|
+
version: 1,
|
|
4
|
+
summary: 'A browser application calls an API. Production deployment is unknown.',
|
|
5
|
+
components: [
|
|
6
|
+
{ id: 'web', name: 'Web app', kind: 'application', description: 'The browser interface.', evidence: 'confirmed', sources: [{ path: 'web/package.json', reason: 'Declares the browser application.' }] },
|
|
7
|
+
{ id: 'api', name: 'API', kind: 'service', description: 'Handles application requests.', evidence: 'confirmed', sources: [{ path: 'api/server.ts', line: 1, reason: 'Defines the HTTP entry point.' }] },
|
|
8
|
+
],
|
|
9
|
+
connections: [{ from: 'web', to: 'api', label: 'HTTP requests', evidence: 'confirmed', sources: [{ path: 'web/client.ts', reason: 'Calls the API base URL.' }] }],
|
|
10
|
+
unknowns: ['The repository does not establish which services are deployed.'],
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export const SYSTEM_ARCHITECTURE_CONTRACT = `
|
|
14
|
+
UNDERSTAND MY SYSTEM — docs/01-architecture.md also supplies the Architecture
|
|
15
|
+
view. Full sweeps generate the report. Incremental re-grounds maintain it only
|
|
16
|
+
when architecture-affecting changes are in scope; never expand an unrelated
|
|
17
|
+
incremental pass into a full scan just to add this report. If no report exists
|
|
18
|
+
and this pass cannot inspect the system, leave it absent. Inspect real entry
|
|
19
|
+
points, workspace/package manifests, API clients,
|
|
20
|
+
database/storage/queue adapters and deployment configuration before writing it.
|
|
21
|
+
Map major applications, services, data stores, queues and external systems;
|
|
22
|
+
do not turn every file, dependency or function into a node.
|
|
23
|
+
|
|
24
|
+
Include exactly ONE fenced block tagged flowviant-architecture containing JSON
|
|
25
|
+
in this version-1 format (this example is FORMAT ONLY, never project facts):
|
|
26
|
+
\n\`\`\`flowviant-architecture
|
|
27
|
+
${JSON.stringify(ARCHITECTURE_EXAMPLE, null, 2)}
|
|
28
|
+
\`\`\`
|
|
29
|
+
|
|
30
|
+
Kinds: application, service, database, queue, storage, external. Component ids
|
|
31
|
+
are unique ASCII letters/digits/underscore/hyphen, starting with a letter or
|
|
32
|
+
digit, at most 80 characters. Connections reference those ids. Every component
|
|
33
|
+
and connection needs at least one source: a real repository-relative path you
|
|
34
|
+
opened, an optional positive line number and a reason explaining the evidence.
|
|
35
|
+
Never emit absolute paths, traversal or URLs as source paths. Confirmed means
|
|
36
|
+
source explicitly establishes the claim; inferred means an interpretation, and
|
|
37
|
+
the source reason must explain it. Neither means running, healthy or deployed.
|
|
38
|
+
|
|
39
|
+
State scope in the summary: only this checked-out repository is inspected;
|
|
40
|
+
other repositories, deployment state and external settings remain unknown unless
|
|
41
|
+
actually inspected. Never imply project-wide or production completeness. Put
|
|
42
|
+
unanswered questions and uninspected areas in unknowns. An empty/new repository
|
|
43
|
+
has components: [], connections: [] and an explanation in unknowns; do not invent
|
|
44
|
+
a proposed architecture. Never expose credentials, environment values, private
|
|
45
|
+
hosts or secret material. Do not contact infrastructure, provision anything,
|
|
46
|
+
install dependencies or run the application to construct this map.
|
|
47
|
+
|
|
48
|
+
At most 80 components, 200 connections, 40 sources per finding and 80 unknowns.
|
|
49
|
+
Names at most 160 characters, connection labels 200, source paths 1000 and
|
|
50
|
+
summary/descriptions/reasons/questions 4000. Keep the chapter's readable prose,
|
|
51
|
+
Mermaid overview, files: frontmatter and page-local provenance beside the report.
|
|
52
|
+
This is source analysis by the coding CLI, not independent verification by
|
|
53
|
+
Flowviant. Re-ground it when changed files affect system boundaries, config,
|
|
54
|
+
entry points or connections, including additions/removals outside its old files:
|
|
55
|
+
list. Re-read affected sources; retain unrelated findings without asserting a
|
|
56
|
+
new verified commit for unchecked claims. Respect human/reference ownership.
|
|
57
|
+
`;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/** Standalone daemon counterpart of shared/systemArchitecture.ts. The release
|
|
2
|
+
* parity gate checks the same accepted/rejected reports on both readers. */
|
|
3
|
+
export const SYSTEM_ARCHITECTURE_PATH = 'docs/01-architecture.md';
|
|
4
|
+
export const SYSTEM_ARCHITECTURE_FENCE = 'flowviant-architecture';
|
|
5
|
+
export const ARCHITECTURE_REPORT_DAEMON_VERSION = '0.125.31';
|
|
6
|
+
|
|
7
|
+
const text = (value, max = 4000) => typeof value === 'string' && value.trim().length > 0 && value.trim().length <= max;
|
|
8
|
+
const id = (value) => typeof value === 'string' && /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,79}$/.test(value);
|
|
9
|
+
const source = (value) => value && typeof value.path === 'string' && value.path.length > 0 && value.path.length <= 1000 &&
|
|
10
|
+
!/^[\/\\]|[\\\x00-\x1f\x7f:#?]/.test(value.path) && value.path.split('/').every((part) => part && part !== '.' && part !== '..') &&
|
|
11
|
+
(value.line === undefined || (Number.isInteger(value.line) && value.line > 0)) && text(value.reason);
|
|
12
|
+
const finding = (value) => value && ['confirmed', 'inferred'].includes(value.evidence) &&
|
|
13
|
+
Array.isArray(value.sources) && value.sources.length > 0 && value.sources.length <= 40 && value.sources.every(source);
|
|
14
|
+
|
|
15
|
+
export function validArchitectureReport(value) {
|
|
16
|
+
if (!value || value.version !== 1 || !text(value.summary) || !Array.isArray(value.components) || value.components.length > 80 ||
|
|
17
|
+
!Array.isArray(value.connections) || value.connections.length > 200 || !Array.isArray(value.unknowns) || value.unknowns.length > 80 || !value.unknowns.every((question) => text(question))) return false;
|
|
18
|
+
const ids = new Set();
|
|
19
|
+
for (const component of value.components) {
|
|
20
|
+
if (!component || !id(component.id) || ids.has(component.id) || !text(component.name, 160) || !text(component.description) ||
|
|
21
|
+
!['application', 'service', 'database', 'queue', 'storage', 'external'].includes(component.kind) || !finding(component)) return false;
|
|
22
|
+
ids.add(component.id);
|
|
23
|
+
}
|
|
24
|
+
return (ids.size > 0 || value.unknowns.length > 0) && value.connections.every((connection) =>
|
|
25
|
+
connection && ids.has(connection.from) && ids.has(connection.to) && text(connection.label, 200) && finding(connection));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function readSystemArchitecture(markdown) {
|
|
29
|
+
const blocks = [...markdown.matchAll(/^```flowviant-architecture[^\S\r\n]*\r?\n([\s\S]*?)^```[^\S\r\n]*\r?$/gm)];
|
|
30
|
+
if (blocks.length === 0) return { status: markdown.includes('```flowviant-architecture') ? 'invalid' : 'missing' };
|
|
31
|
+
if (blocks.length !== 1) return { status: 'invalid' };
|
|
32
|
+
try {
|
|
33
|
+
const report = JSON.parse(blocks[0][1]);
|
|
34
|
+
return validArchitectureReport(report) ? { status: 'available', report } : { status: 'invalid' };
|
|
35
|
+
} catch {
|
|
36
|
+
return { status: 'invalid' };
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -116,8 +116,27 @@ export const WHOLE_REQUEST = `THE WHOLE REQUEST, NOT ONLY THE BRIEF:
|
|
|
116
116
|
delivered JSON object, one entry per item this card was meant to deliver
|
|
117
117
|
that you did not build ("not built"), built in part ("partly") or did not
|
|
118
118
|
check against the real thing ("unverified"), and per part of the request
|
|
119
|
-
that NO card in this run owns ("unowned")
|
|
120
|
-
|
|
119
|
+
that NO card in this run owns ("unowned"). Outside this run does not mean
|
|
120
|
+
unowned in the project: check relatedWork and the current queue first.
|
|
121
|
+
"gaps": [{ "item": "what is missing", "status": "not built | partly | unverified | unowned", "why": "one sentence", "disposition": "required | owned | optional | decision", "ownerTaskId": "existing card id only when owned" }]
|
|
122
|
+
ROUTE FINDINGS BEFORE DELIVERY:
|
|
123
|
+
- Already assigned, queued, running or completed on another card: use
|
|
124
|
+
disposition "owned" and its actual ownerTaskId. Link it; do not repeat it
|
|
125
|
+
or stage another task. An older Done receipt is not proof a recurrence is fixed.
|
|
126
|
+
- Necessary to meet this card's approved outcome: finish it now in this same
|
|
127
|
+
worktree and verify the combined result. Record completed incidental repairs
|
|
128
|
+
through raised. If another work step is needed, report disposition "required"
|
|
129
|
+
with why it is necessary; the server queues the same card's next step before
|
|
130
|
+
Review. This does not authorize another card's work, a product-kind change,
|
|
131
|
+
optional improvements, merging or deployment. Keep commit attribution.
|
|
132
|
+
- Optional improvement: use disposition "optional". Describe it as an offer in
|
|
133
|
+
the agent's summary; the person may reply here to include it. It creates no
|
|
134
|
+
New task tab and does not block delivery of the approved scope.
|
|
135
|
+
- A requirement, ownership or scope decision only the person can settle:
|
|
136
|
+
use the existing blocked reply with a specific question and options. Stay in
|
|
137
|
+
this agent conversation. Never claim delivery while that decision is required.
|
|
138
|
+
- After a person says "Finish the current scope", retain declined additions as
|
|
139
|
+
optional and finish the approved request; do not ask the same scope question again.
|
|
121
140
|
"gaps": [] says you walked the request and nothing is missing. Never recast
|
|
122
141
|
a gap as a decision: a choice you made in place of the request is a gap
|
|
123
142
|
("partly") with its reason.
|
package/bin/lib/claudeInput.mjs
CHANGED
|
@@ -95,7 +95,7 @@ export function createClaudeInput(child, { onTaken = () => {}, onContext = null
|
|
|
95
95
|
}
|
|
96
96
|
if (event.type === 'system' && event.subtype === 'compact_boundary') { compacting = false; askContext(true); }
|
|
97
97
|
if (event.type === 'system' && event.subtype === 'background_tasks_changed' && Array.isArray(event.tasks)) {
|
|
98
|
-
agents = event.tasks.filter(task => task?.task_type
|
|
98
|
+
agents = event.tasks.filter(task => ['local_agent', 'local_workflow'].includes(task?.task_type)).length;
|
|
99
99
|
if (agents > 0) draining = false; // a drained message's turn started agents
|
|
100
100
|
}
|
|
101
101
|
if (event.type === 'result') {
|
|
@@ -1,11 +1,34 @@
|
|
|
1
1
|
import { NATIVE_AGENT_TOOLS } from './nativeAgentTools.mjs';
|
|
2
|
+
import { createClaudeWorkflows } from './claudeWorkflows.mjs';
|
|
3
|
+
import { turnModelOf } from './turnModel.mjs';
|
|
2
4
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
+
const textOf = content => typeof content === 'string' ? content
|
|
6
|
+
: Array.isArray(content) ? content.filter(b => b?.type === 'text' && typeof b.text === 'string').map(b => b.text).join('\n') : '';
|
|
7
|
+
const bounded = (map, n) => { if (map.size > n) map.delete(map.keys().next().value); };
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Native tool-use ids are stable for the lifetime of an Agent/Task call.
|
|
11
|
+
*
|
|
12
|
+
* WHAT A CHILD SAYS OF ITSELF (2026-11-06, S4.1): its model off its own
|
|
13
|
+
* `message.model` (never the spawn's `model` input: what was asked is not
|
|
14
|
+
* what ran), its final words off its own sidechain `result`, else the
|
|
15
|
+
* foreground call's tool_result, else its last message once it is done; its
|
|
16
|
+
* parent is the stream the spawn rode (the lead, or a child that spawned
|
|
17
|
+
* one). Claude states no per-child effort, so none is reported.
|
|
18
|
+
*/
|
|
19
|
+
export function createClaudeSubagents(report = () => {}, onWorkflow = () => {}) {
|
|
20
|
+
const workflows = createClaudeWorkflows(report, onWorkflow);
|
|
5
21
|
const calls = new Map();
|
|
6
22
|
const aliases = new Map();
|
|
7
23
|
const questions = new Map();
|
|
8
24
|
const seen = new Set();
|
|
25
|
+
const models = new Map(), said = new Map(), resulted = new Set();
|
|
26
|
+
const finish = (id, result) => {
|
|
27
|
+
const text = typeof result === 'string' && result.trim() ? result : said.get(id);
|
|
28
|
+
if (!text || resulted.has(id)) return {};
|
|
29
|
+
resulted.add(id); bounded(resulted, 128);
|
|
30
|
+
return { result: text };
|
|
31
|
+
};
|
|
9
32
|
const identify = ev => ev.parent_tool_use_id != null
|
|
10
33
|
? aliases.get(ev.parent_tool_use_id) ?? ev.parent_tool_use_id
|
|
11
34
|
: ev.isSidechain ? aliases.get(ev.agentId ?? ev.agent_id ?? ev.sessionId) : undefined;
|
|
@@ -28,7 +51,18 @@ export function createClaudeSubagents(report = () => {}) {
|
|
|
28
51
|
return true;
|
|
29
52
|
},
|
|
30
53
|
observe(ev) {
|
|
54
|
+
workflows.observe(ev);
|
|
31
55
|
const agentId = identify(ev);
|
|
56
|
+
if (ev.type === 'assistant' && agentId) {
|
|
57
|
+
const model = turnModelOf(ev.message?.model);
|
|
58
|
+
if (model && models.get(agentId) !== model) { models.set(agentId, model); bounded(models, 128); report({ id: agentId, model }); }
|
|
59
|
+
const words = textOf(ev.message?.content);
|
|
60
|
+
if (words.trim()) { said.set(agentId, words); bounded(said, 128); }
|
|
61
|
+
}
|
|
62
|
+
if (ev.type === 'result' && agentId && !ev.is_error && typeof ev.result === 'string' && ev.result.trim()) {
|
|
63
|
+
resulted.delete(agentId);
|
|
64
|
+
report({ id: agentId, ...finish(agentId, ev.result) });
|
|
65
|
+
}
|
|
32
66
|
if (ev.type === 'assistant') {
|
|
33
67
|
for (const b of ev.message?.content ?? []) {
|
|
34
68
|
if (b.type === 'tool_use' && NATIVE_AGENT_TOOLS.claude.ask.includes(b.name) && agentId) {
|
|
@@ -43,8 +77,11 @@ export function createClaudeSubagents(report = () => {}) {
|
|
|
43
77
|
aliases.set(b.id, id);
|
|
44
78
|
if (calls.size > 128) calls.delete(calls.keys().next().value);
|
|
45
79
|
if (aliases.size > 256) aliases.delete(aliases.keys().next().value);
|
|
80
|
+
// A resume names the child's native id: the identity it keeps across turns.
|
|
81
|
+
const resumed = typeof input.resume === 'string' && input.resume ? { nativeId: input.resume } : {};
|
|
82
|
+
resulted.delete(id);
|
|
46
83
|
report({ id, kind: input.subagent_type ?? '', title: input.description ?? input.name ?? '',
|
|
47
|
-
brief: input.prompt ?? '', status: 'running', now: input.description ?? '' });
|
|
84
|
+
brief: input.prompt ?? '', status: 'running', now: input.description ?? '', parent: agentId ?? 'lead', ...resumed });
|
|
48
85
|
}
|
|
49
86
|
}
|
|
50
87
|
if (ev.type === 'user') {
|
|
@@ -60,8 +97,10 @@ export function createClaudeSubagents(report = () => {}) {
|
|
|
60
97
|
if (nativeId) aliases.set(nativeId, id);
|
|
61
98
|
// Background launch is not completion; its notification closes it.
|
|
62
99
|
const background = result.status === 'async_launched' || result.isAsync === true || calls.get(b.tool_use_id).input.run_in_background === true;
|
|
63
|
-
|
|
64
|
-
|
|
100
|
+
const status = b.is_error ? 'stopped' : background ? 'running' : 'done';
|
|
101
|
+
report({ id, ...(nativeId ? { nativeId } : {}), status,
|
|
102
|
+
...(typeof result.description === 'string' ? { now: result.description } : {}),
|
|
103
|
+
...(status === 'done' ? finish(id, textOf(b.content)) : {}) });
|
|
65
104
|
}
|
|
66
105
|
}
|
|
67
106
|
if (ev.type === 'system' && ['task_started', 'task_progress', 'task_notification'].includes(ev.subtype)) {
|
|
@@ -70,7 +109,8 @@ export function createClaudeSubagents(report = () => {}) {
|
|
|
70
109
|
if (id) {
|
|
71
110
|
if (ev.task_id) aliases.set(ev.task_id, id);
|
|
72
111
|
const status = ev.status === 'completed' ? 'done' : ['failed', 'stopped'].includes(ev.status) ? 'stopped' : 'running';
|
|
73
|
-
report({ id, status, ...(ev.description || ev.summary ? { now: ev.description ?? ev.summary } : {})
|
|
112
|
+
report({ id, status, ...(ev.description || ev.summary ? { now: ev.description ?? ev.summary } : {}),
|
|
113
|
+
...(status === 'done' ? finish(id) : {}) });
|
|
74
114
|
}
|
|
75
115
|
}
|
|
76
116
|
return agentId;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/** Workflow children have no stdout of their own: task_progress carries snapshots. */
|
|
2
|
+
import { turnModelOf } from './turnModel.mjs';
|
|
3
|
+
import { WORKFLOW_AGENT_CAP, WORKFLOW_CAP } from './subagentBounds.mjs';
|
|
4
|
+
|
|
5
|
+
const text = (s, n) => typeof s === 'string' ? s.slice(0, n) : '';
|
|
6
|
+
const idOf = s => typeof s === 'string' && s.length > 0 && s.length <= 96 ? s : null;
|
|
7
|
+
const instant = v => {
|
|
8
|
+
const n = typeof v === 'number' ? v : Date.parse(v);
|
|
9
|
+
return Number.isFinite(n) && Math.abs(n) < 8.64e15 ? new Date(n).toISOString() : undefined;
|
|
10
|
+
};
|
|
11
|
+
const count = v => Number.isSafeInteger(v) && v >= 0 ? v : undefined;
|
|
12
|
+
const statusOf = s => s === 'done' || s === 'completed' ? 'done'
|
|
13
|
+
: s === 'failed' || s === 'stopped' || s === 'cancelled' ? 'stopped' : 'running';
|
|
14
|
+
|
|
15
|
+
export function createClaudeWorkflows(reportAgent = () => {}, report = () => {}) {
|
|
16
|
+
const runs = new Map(), aliases = new Map(), launches = new Map();
|
|
17
|
+
return {
|
|
18
|
+
observe(ev) {
|
|
19
|
+
if (ev.type === 'assistant' && !ev.parent_tool_use_id && !ev.isSidechain) {
|
|
20
|
+
for (const b of ev.message?.content ?? []) if (b?.type === 'tool_use' && b.name === 'Workflow' && idOf(b.id)) {
|
|
21
|
+
launches.set(b.id, text(b.input?.description, 600));
|
|
22
|
+
if (launches.size > WORKFLOW_CAP) launches.delete(launches.keys().next().value);
|
|
23
|
+
report({ launcherId: b.id });
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
if (ev.type !== 'system' || !['task_started', 'task_progress', 'task_updated', 'task_notification'].includes(ev.subtype)) return;
|
|
27
|
+
const id = idOf(ev.run_id) ?? aliases.get(ev.task_id);
|
|
28
|
+
if (!id) return;
|
|
29
|
+
if (ev.subtype === 'task_started' && ev.task_type === 'local_workflow' && !runs.has(id)) {
|
|
30
|
+
if (runs.size >= WORKFLOW_CAP) return;
|
|
31
|
+
runs.set(id, { row: { id, key: `claude-workflow:${id}`, name: text(ev.workflow_name, 200),
|
|
32
|
+
description: text(ev.description ?? launches.get(ev.tool_use_id), 600), status: 'running',
|
|
33
|
+
startedAt: instant(ev.startedAt) ?? new Date().toISOString(), phases: [], notRecorded: 0 },
|
|
34
|
+
agents: new Map(), seen: new Set(), tool: ev.tool_use_id });
|
|
35
|
+
if (idOf(ev.task_id)) aliases.set(ev.task_id, id);
|
|
36
|
+
}
|
|
37
|
+
const run = runs.get(id);
|
|
38
|
+
if (!run) return;
|
|
39
|
+
const { row, agents, seen } = run;
|
|
40
|
+
const before = JSON.stringify(row);
|
|
41
|
+
const progress = Array.isArray(ev.workflow_progress) ? ev.workflow_progress.slice(0, 2000) : [];
|
|
42
|
+
for (const a of progress) {
|
|
43
|
+
if (a?.type !== 'workflow_agent' || !idOf(a.agentId)) continue;
|
|
44
|
+
seen.add(a.agentId);
|
|
45
|
+
if (seen.size > 4096) seen.delete(seen.values().next().value);
|
|
46
|
+
const previous = agents.get(a.agentId);
|
|
47
|
+
const status = statusOf(a.state);
|
|
48
|
+
const startedAt = instant(a.startedAt) ?? previous?.startedAt;
|
|
49
|
+
const endedAt = status !== 'running' ? instant(a.endedAt) ?? instant(
|
|
50
|
+
Number.isFinite(a.startedAt) && Number.isFinite(a.durationMs) ? a.startedAt + a.durationMs : a.lastProgressAt
|
|
51
|
+
) ?? previous?.endedAt ?? new Date().toISOString() : undefined;
|
|
52
|
+
const next = { ...previous, id: a.agentId, nativeId: a.agentId, key: `claude:${a.agentId}`, cli: 'claude',
|
|
53
|
+
kind: 'workflow', parentKey: row.key, title: text(a.label, 200), status,
|
|
54
|
+
...(typeof a.phaseTitle === 'string' ? { phase: text(a.phaseTitle, 200) } : {}),
|
|
55
|
+
...(startedAt ? { startedAt } : {}), ...(endedAt ? { endedAt } : {}),
|
|
56
|
+
...(turnModelOf(a.model) ? { model: turnModelOf(a.model) } : {}),
|
|
57
|
+
...(a.lastToolName || a.lastToolSummary ? { now: [text(a.lastToolName, 80), text(a.lastToolSummary, 400)].filter(Boolean).join(' ').slice(0, 400) } : {}),
|
|
58
|
+
...(typeof a.promptPreview === 'string' ? { brief: text(a.promptPreview, 24000), briefStatus: 'truncated' } : {}),
|
|
59
|
+
...(typeof a.resultPreview === 'string' ? { result: text(a.resultPreview, 8000), resultStatus: 'truncated' } : {}),
|
|
60
|
+
...(count(a.tokens) !== undefined ? { tokens: a.tokens } : {}),
|
|
61
|
+
...(count(a.toolCalls) !== undefined ? { toolCalls: a.toolCalls } : {}) };
|
|
62
|
+
if (status === 'running') delete next.endedAt;
|
|
63
|
+
agents.set(a.agentId, next);
|
|
64
|
+
}
|
|
65
|
+
const newest = [...agents.values()].sort((a, b) => (b.startedAt ?? '').localeCompare(a.startedAt ?? ''));
|
|
66
|
+
for (const a of newest.slice(WORKFLOW_AGENT_CAP)) agents.delete(a.id);
|
|
67
|
+
row.notRecorded = Math.max(row.notRecorded, seen.size - agents.size);
|
|
68
|
+
if (progress.length) row.phases = progress.filter(p => p?.type === 'workflow_phase').slice(0, 64).map(p => {
|
|
69
|
+
const children = progress.filter(a => a?.type === 'workflow_agent' && a.phaseTitle === p.title);
|
|
70
|
+
return { title: text(p.title, 200), status: p.state ? statusOf(p.state)
|
|
71
|
+
: children.length && children.every(a => statusOf(a.state) === 'done') ? 'done' : 'running' };
|
|
72
|
+
});
|
|
73
|
+
if (count(ev.usage?.total_tokens) !== undefined) row.totalTokens = ev.usage.total_tokens;
|
|
74
|
+
if (count(ev.usage?.tool_uses) !== undefined) row.toolCalls = ev.usage.tool_uses;
|
|
75
|
+
if (ev.subtype === 'task_updated' && instant(ev.patch?.end_time)) run.endedAt = instant(ev.patch.end_time);
|
|
76
|
+
if (ev.subtype === 'task_notification') {
|
|
77
|
+
row.status = statusOf(ev.status);
|
|
78
|
+
row.endedAt = instant(ev.endedAt) ?? run.endedAt ?? new Date().toISOString();
|
|
79
|
+
row.phases = row.phases.map(p => ({ ...p, status: p.status === 'done' ? 'done' : row.status }));
|
|
80
|
+
for (const a of agents.values()) if (a.status === 'running') Object.assign(a, { status: row.status, endedAt: row.endedAt });
|
|
81
|
+
}
|
|
82
|
+
// Report only changed children, even though the CLI resends the whole snapshot.
|
|
83
|
+
for (const a of agents.values()) {
|
|
84
|
+
const signature = JSON.stringify(a);
|
|
85
|
+
if (run.sent?.get(a.id) !== signature) reportAgent(a);
|
|
86
|
+
}
|
|
87
|
+
// Signatures stay outside the record that travels on the wire.
|
|
88
|
+
run.sent ??= new Map();
|
|
89
|
+
for (const key of run.sent.keys()) if (!agents.has(key)) run.sent.delete(key);
|
|
90
|
+
for (const a of agents.values()) run.sent.set(a.id, JSON.stringify(a));
|
|
91
|
+
if (before !== JSON.stringify(row) || ev.subtype === 'task_started') report({ ...row, launcherId: run.tool });
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
}
|
package/bin/lib/cliLimit.mjs
CHANGED
|
@@ -19,6 +19,11 @@ import { scrubCut } from './uplinkScrub.mjs';
|
|
|
19
19
|
* attached, and is a perfectly survivable second-best.
|
|
20
20
|
*/
|
|
21
21
|
const LIMIT_PHRASES = [
|
|
22
|
+
// Installed codex-cli 0.166.0 native binary, strings (2026-11-06):
|
|
23
|
+
// ~/.codex/packages/standalone/releases/0.166.0-x86_64-unknown-linux-musl/bin/codex
|
|
24
|
+
// Core error display: plain, model-specific ("for ..."), Plus, credits,
|
|
25
|
+
// and admin-request variants all share this exact literal prefix.
|
|
26
|
+
/you've hit your usage limit/i,
|
|
22
27
|
/usage limit reached/i,
|
|
23
28
|
/rate limit/i,
|
|
24
29
|
/you've reached your .* limit/i,
|