@covibes/zeroshot 1.1.4 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -0
- package/cli/index.js +107 -6
- package/cluster-templates/base-templates/full-workflow.json +40 -9
- package/lib/stream-json-parser.js +9 -1
- package/package.json +1 -1
- package/src/agent/agent-lifecycle.js +23 -0
- package/src/agent/agent-task-executor.js +41 -0
- package/src/ledger.js +149 -0
- package/src/message-bus.js +42 -0
- package/src/status-footer.js +231 -74
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,23 @@
|
|
|
1
|
+
# [1.3.0](https://github.com/covibes/zeroshot/compare/v1.2.0...v1.3.0) (2025-12-28)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* **planner:** enforce explicit acceptance criteria via JSON schema ([73009d9](https://github.com/covibes/zeroshot/commit/73009d9ad33e46e546721680be6d2cab9c9e46f0)), closes [#16](https://github.com/covibes/zeroshot/issues/16)
|
|
7
|
+
|
|
8
|
+
# [1.2.0](https://github.com/covibes/zeroshot/compare/v1.1.4...v1.2.0) (2025-12-28)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* **status-footer:** robust terminal resize handling ([767a610](https://github.com/covibes/zeroshot/commit/767a610027b3e2bb238b54c31a3a7e93db635319))
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
### Features
|
|
17
|
+
|
|
18
|
+
* **agent:** publish TOKEN_USAGE events with task completion ([c79482c](https://github.com/covibes/zeroshot/commit/c79482c82582b75a692ba71005c821decdc1d769))
|
|
19
|
+
* **stream-parser:** add token usage tracking to result events ([91ad850](https://github.com/covibes/zeroshot/commit/91ad8507f42fd1a398bdc06f3b91b0a13eec8941))
|
|
20
|
+
|
|
1
21
|
## [1.1.4](https://github.com/covibes/zeroshot/compare/v1.1.3...v1.1.4) (2025-12-28)
|
|
2
22
|
|
|
3
23
|
|
package/cli/index.js
CHANGED
|
@@ -915,23 +915,44 @@ program
|
|
|
915
915
|
if (clusters.length > 0) {
|
|
916
916
|
console.log(chalk.bold('\n=== Clusters ==='));
|
|
917
917
|
console.log(
|
|
918
|
-
`${'ID'.padEnd(25)} ${'State'.padEnd(
|
|
918
|
+
`${'ID'.padEnd(25)} ${'State'.padEnd(12)} ${'Agents'.padEnd(8)} ${'Tokens'.padEnd(12)} ${'Cost'.padEnd(8)} Created`
|
|
919
919
|
);
|
|
920
920
|
console.log('-'.repeat(100));
|
|
921
921
|
|
|
922
|
+
const orchestrator = getOrchestrator();
|
|
922
923
|
for (const cluster of clusters) {
|
|
923
924
|
const created = new Date(cluster.createdAt).toLocaleString();
|
|
924
925
|
|
|
926
|
+
// Get token usage
|
|
927
|
+
let tokenDisplay = '-';
|
|
928
|
+
let costDisplay = '-';
|
|
929
|
+
try {
|
|
930
|
+
const clusterObj = orchestrator.getCluster(cluster.id);
|
|
931
|
+
if (clusterObj?.messageBus) {
|
|
932
|
+
const tokensByRole = clusterObj.messageBus.getTokensByRole(cluster.id);
|
|
933
|
+
if (tokensByRole?._total?.count > 0) {
|
|
934
|
+
const total = tokensByRole._total;
|
|
935
|
+
const totalTokens = (total.inputTokens || 0) + (total.outputTokens || 0);
|
|
936
|
+
tokenDisplay = totalTokens.toLocaleString();
|
|
937
|
+
if (total.totalCostUsd > 0) {
|
|
938
|
+
costDisplay = '$' + total.totalCostUsd.toFixed(3);
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
} catch {
|
|
943
|
+
/* Token tracking not available */
|
|
944
|
+
}
|
|
945
|
+
|
|
925
946
|
// Highlight zombie clusters in red
|
|
926
947
|
const stateDisplay =
|
|
927
948
|
cluster.state === 'zombie'
|
|
928
|
-
? chalk.red(cluster.state.padEnd(
|
|
929
|
-
: cluster.state.padEnd(
|
|
949
|
+
? chalk.red(cluster.state.padEnd(12))
|
|
950
|
+
: cluster.state.padEnd(12);
|
|
930
951
|
|
|
931
952
|
const rowColor = cluster.state === 'zombie' ? chalk.red : (s) => s;
|
|
932
953
|
|
|
933
954
|
console.log(
|
|
934
|
-
`${rowColor(cluster.id.padEnd(25))} ${stateDisplay} ${cluster.agentCount.toString().padEnd(
|
|
955
|
+
`${rowColor(cluster.id.padEnd(25))} ${stateDisplay} ${cluster.agentCount.toString().padEnd(8)} ${tokenDisplay.padEnd(12)} ${costDisplay.padEnd(8)} ${created}`
|
|
935
956
|
);
|
|
936
957
|
}
|
|
937
958
|
} else {
|
|
@@ -987,6 +1008,24 @@ program
|
|
|
987
1008
|
}
|
|
988
1009
|
console.log(`Created: ${new Date(status.createdAt).toLocaleString()}`);
|
|
989
1010
|
console.log(`Messages: ${status.messageCount}`);
|
|
1011
|
+
|
|
1012
|
+
// Show token usage if available
|
|
1013
|
+
try {
|
|
1014
|
+
const cluster = getOrchestrator().getCluster(id);
|
|
1015
|
+
if (cluster?.messageBus) {
|
|
1016
|
+
const tokensByRole = cluster.messageBus.getTokensByRole(id);
|
|
1017
|
+
const tokenLines = formatTokenUsage(tokensByRole);
|
|
1018
|
+
if (tokenLines) {
|
|
1019
|
+
console.log('');
|
|
1020
|
+
for (const line of tokenLines) {
|
|
1021
|
+
console.log(line);
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
} catch {
|
|
1026
|
+
/* Token tracking not available */
|
|
1027
|
+
}
|
|
1028
|
+
|
|
990
1029
|
console.log(`\nAgents:`);
|
|
991
1030
|
|
|
992
1031
|
for (const agent of status.agents) {
|
|
@@ -1553,16 +1592,29 @@ Key bindings:
|
|
|
1553
1592
|
for (const clusterId of clusters) {
|
|
1554
1593
|
const agents = await socketDiscovery.listAttachableAgents(clusterId);
|
|
1555
1594
|
console.log(` ${clusterId}`);
|
|
1556
|
-
// Get agent models from orchestrator (if available)
|
|
1595
|
+
// Get agent models and token usage from orchestrator (if available)
|
|
1557
1596
|
let agentModels = {};
|
|
1597
|
+
let tokenUsageLines = null;
|
|
1558
1598
|
try {
|
|
1559
1599
|
const orchestrator = OrchestratorModule.getInstance();
|
|
1560
1600
|
const status = orchestrator.getStatus(clusterId);
|
|
1561
1601
|
for (const a of status.agents) {
|
|
1562
1602
|
agentModels[a.id] = a.model;
|
|
1563
1603
|
}
|
|
1604
|
+
// Get token usage from message bus
|
|
1605
|
+
const cluster = orchestrator.getCluster(clusterId);
|
|
1606
|
+
if (cluster?.messageBus) {
|
|
1607
|
+
const tokensByRole = cluster.messageBus.getTokensByRole(clusterId);
|
|
1608
|
+
tokenUsageLines = formatTokenUsage(tokensByRole);
|
|
1609
|
+
}
|
|
1564
1610
|
} catch {
|
|
1565
|
-
/* orchestrator not running - models unavailable */
|
|
1611
|
+
/* orchestrator not running - models/tokens unavailable */
|
|
1612
|
+
}
|
|
1613
|
+
// Display token usage if available
|
|
1614
|
+
if (tokenUsageLines) {
|
|
1615
|
+
for (const line of tokenUsageLines) {
|
|
1616
|
+
console.log(` ${line}`);
|
|
1617
|
+
}
|
|
1566
1618
|
}
|
|
1567
1619
|
for (const agent of agents) {
|
|
1568
1620
|
const modelLabel = agentModels[agent] ? chalk.dim(` [${agentModels[agent]}]`) : '';
|
|
@@ -3087,6 +3139,55 @@ function formatTaskSummary(issueOpened, maxLen = 35) {
|
|
|
3087
3139
|
return firstLine.slice(0, maxLen) + (firstLine.length > maxLen ? '...' : '');
|
|
3088
3140
|
}
|
|
3089
3141
|
|
|
3142
|
+
// Format token usage for display
|
|
3143
|
+
function formatTokenUsage(tokensByRole) {
|
|
3144
|
+
if (!tokensByRole || !tokensByRole._total || tokensByRole._total.count === 0) {
|
|
3145
|
+
return null;
|
|
3146
|
+
}
|
|
3147
|
+
|
|
3148
|
+
const total = tokensByRole._total;
|
|
3149
|
+
const lines = [];
|
|
3150
|
+
|
|
3151
|
+
// Format numbers with commas
|
|
3152
|
+
const fmt = (n) => n.toLocaleString();
|
|
3153
|
+
|
|
3154
|
+
// Total line
|
|
3155
|
+
const inputTokens = total.inputTokens || 0;
|
|
3156
|
+
const outputTokens = total.outputTokens || 0;
|
|
3157
|
+
const totalTokens = inputTokens + outputTokens;
|
|
3158
|
+
const cost = total.totalCostUsd || 0;
|
|
3159
|
+
|
|
3160
|
+
lines.push(
|
|
3161
|
+
chalk.dim('Tokens: ') +
|
|
3162
|
+
chalk.cyan(fmt(totalTokens)) +
|
|
3163
|
+
chalk.dim(' (') +
|
|
3164
|
+
chalk.green(fmt(inputTokens)) +
|
|
3165
|
+
chalk.dim(' in / ') +
|
|
3166
|
+
chalk.yellow(fmt(outputTokens)) +
|
|
3167
|
+
chalk.dim(' out)')
|
|
3168
|
+
);
|
|
3169
|
+
|
|
3170
|
+
// Cost line (if available)
|
|
3171
|
+
if (cost > 0) {
|
|
3172
|
+
lines.push(chalk.dim('Cost: ') + chalk.green('$' + cost.toFixed(4)));
|
|
3173
|
+
}
|
|
3174
|
+
|
|
3175
|
+
// Per-role breakdown (compact)
|
|
3176
|
+
const roles = Object.keys(tokensByRole).filter((r) => r !== '_total');
|
|
3177
|
+
if (roles.length > 1) {
|
|
3178
|
+
const roleStats = roles
|
|
3179
|
+
.map((role) => {
|
|
3180
|
+
const r = tokensByRole[role];
|
|
3181
|
+
const roleTotal = (r.inputTokens || 0) + (r.outputTokens || 0);
|
|
3182
|
+
return `${role}: ${fmt(roleTotal)}`;
|
|
3183
|
+
})
|
|
3184
|
+
.join(chalk.dim(' | '));
|
|
3185
|
+
lines.push(chalk.dim('By role: ') + roleStats);
|
|
3186
|
+
}
|
|
3187
|
+
|
|
3188
|
+
return lines;
|
|
3189
|
+
}
|
|
3190
|
+
|
|
3090
3191
|
// Set terminal title (works in most terminals)
|
|
3091
3192
|
function setTerminalTitle(title) {
|
|
3092
3193
|
// ESC ] 0 ; <title> BEL
|
|
@@ -102,12 +102,27 @@
|
|
|
102
102
|
}
|
|
103
103
|
}
|
|
104
104
|
}
|
|
105
|
+
},
|
|
106
|
+
"acceptanceCriteria": {
|
|
107
|
+
"type": "array",
|
|
108
|
+
"description": "EXPLICIT, TESTABLE acceptance criteria. Each must be verifiable. NO VAGUE BULLSHIT.",
|
|
109
|
+
"items": {
|
|
110
|
+
"type": "object",
|
|
111
|
+
"properties": {
|
|
112
|
+
"id": { "type": "string", "description": "AC1, AC2, etc." },
|
|
113
|
+
"criterion": { "type": "string", "description": "MUST be testable - if you can't verify it, rewrite it" },
|
|
114
|
+
"verification": { "type": "string", "description": "EXACT steps to verify (command, URL, test name)" },
|
|
115
|
+
"priority": { "type": "string", "enum": ["MUST", "SHOULD", "NICE"], "description": "MUST = blocks completion" }
|
|
116
|
+
},
|
|
117
|
+
"required": ["id", "criterion", "verification", "priority"]
|
|
118
|
+
},
|
|
119
|
+
"minItems": 3
|
|
105
120
|
}
|
|
106
121
|
},
|
|
107
|
-
"required": ["plan", "summary", "filesAffected"]
|
|
122
|
+
"required": ["plan", "summary", "filesAffected", "acceptanceCriteria"]
|
|
108
123
|
},
|
|
109
124
|
"prompt": {
|
|
110
|
-
"system": "## 🚫 YOU CANNOT ASK QUESTIONS\n\nYou are running non-interactively. There is NO USER to answer.\n- NEVER use AskUserQuestion tool\n- NEVER say \"Should I...\" or \"Would you like...\"\n- When unsure: Make the SAFER choice and proceed.\n\nYou are a planning agent for a {{complexity}} {{task_type}} task.\n\n## Your Job\nCreate a comprehensive implementation plan.\n\n## Planning Process\n1. Analyze requirements thoroughly\n2. Explore codebase to understand architecture\n3. Identify ALL files that need changes\n4. Break down into concrete, actionable steps\n5. Consider cross-component dependencies\n6. Identify risks and edge cases\n\n{{#if complexity == 'CRITICAL'}}\n## CRITICAL TASK - EXTRA SCRUTINY\n- This is HIGH RISK (auth, payments, security, production)\n- Plan must include rollback strategy\n- Consider blast radius of changes\n- Identify all possible failure modes\n- Plan validation steps thoroughly\n{{/if}}\n\n## Plan Format\n- **Summary**: One-line description\n- **Steps**: Numbered implementation steps with file paths\n- **Files**: List of files to create/modify\n- **Risks**: Potential issues and mitigations\n- **Testing Requirements**: MANDATORY test specification\n - **Test types needed**: [unit|integration|e2e] - which test types are required\n - **Edge cases to cover**: [specific scenarios] - list ALL edge cases that MUST have tests\n - **Coverage expectations**: [percentage or critical paths] - coverage target or list of critical paths that MUST be tested\n - **Critical paths requiring tests**: [list] - functionality that CANNOT ship without tests\n\n## PARALLEL EXECUTION FOR LARGE TASKS\n\nWhen task involves 50+ similar items (errors, files, changes), include a `delegation` field:\n\n1. ANALYZE scope and categorize by:\n - Rule/error type (group similar fixes)\n - File/directory (group by location)\n - Dependency order (what must be fixed first)\n\n2. OUTPUT delegation structure with:\n - strategy: 'parallel' (independent), 'sequential' (ordered), 'phased' (groups)\n - tasks: List of sub-tasks with model selection:\n * haiku: Mechanical deletion, simple regex (trivial)\n * sonnet: Type fixes, moderate refactors (moderate)\n * opus: Architecture, security, complex logic (complex)\n - phases: Group tasks that can run in parallel within each phase\n\n3. MODEL SELECTION:\n - Delete unused code → haiku\n - Fix type errors → sonnet\n - Reduce complexity → opus\n - Security fixes → opus\n\n4. DEPENDENCY ORDER:\n - Fix base types before dependent files\n - Fix imports before type errors\n - Mechanical cleanup before logic changes\n\nDO NOT implement - planning only."
|
|
125
|
+
"system": "## 🚫 YOU CANNOT ASK QUESTIONS\n\nYou are running non-interactively. There is NO USER to answer.\n- NEVER use AskUserQuestion tool\n- NEVER say \"Should I...\" or \"Would you like...\"\n- When unsure: Make the SAFER choice and proceed.\n\nYou are a planning agent for a {{complexity}} {{task_type}} task.\n\n## Your Job\nCreate a comprehensive implementation plan.\n\n## Planning Process\n1. Analyze requirements thoroughly\n2. Explore codebase to understand architecture\n3. Identify ALL files that need changes\n4. Break down into concrete, actionable steps\n5. Consider cross-component dependencies\n6. Identify risks and edge cases\n\n{{#if complexity == 'CRITICAL'}}\n## CRITICAL TASK - EXTRA SCRUTINY\n- This is HIGH RISK (auth, payments, security, production)\n- Plan must include rollback strategy\n- Consider blast radius of changes\n- Identify all possible failure modes\n- Plan validation steps thoroughly\n{{/if}}\n\n## Plan Format\n- **Summary**: One-line description\n- **Steps**: Numbered implementation steps with file paths\n- **Files**: List of files to create/modify\n- **Risks**: Potential issues and mitigations\n- **Testing Requirements**: MANDATORY test specification\n - **Test types needed**: [unit|integration|e2e] - which test types are required\n - **Edge cases to cover**: [specific scenarios] - list ALL edge cases that MUST have tests\n - **Coverage expectations**: [percentage or critical paths] - coverage target or list of critical paths that MUST be tested\n - **Critical paths requiring tests**: [list] - functionality that CANNOT ship without tests\n\n## 🔴 ACCEPTANCE CRITERIA (REQUIRED - minItems: 3)\n\nYou MUST output explicit, testable acceptance criteria. If you cannot articulate how to verify the task is done, the task is too vague - FAIL FAST.\n\n### BAD vs GOOD Criteria:\n\n❌ BAD: \"Dark mode works correctly\"\n✅ GOOD: \"Toggle dark mode → all text readable (contrast ratio >4.5:1), background #1a1a1a\"\n\n❌ BAD: \"API handles errors\"\n✅ GOOD: \"POST /api/users with invalid email → returns 400 + {error: 'Invalid email format'}\"\n\n❌ BAD: \"Tests pass\"\n✅ GOOD: \"npm run test:unit shows 100% pass, coverage >80% on new files\"\n\n❌ BAD: \"Feature is implemented\"\n✅ GOOD: \"User clicks 'Export' → CSV file downloads with columns: id, name, email, created_at\"\n\n❌ BAD: \"Performance is acceptable\"\n✅ GOOD: \"API response time <200ms for 1000 concurrent users (verified via k6 load test)\"\n\n### Criteria Format:\nEach criterion MUST have:\n- **id**: AC1, AC2, AC3, etc.\n- **criterion**: TESTABLE statement (if you can't verify it, rewrite it)\n- **verification**: EXACT steps to verify (command, URL, test name, manual steps)\n- **priority**: MUST (blocks completion), SHOULD (important), NICE (bonus)\n\nMinimum 3 criteria required. At least 1 MUST be priority=MUST.\n\n## PARALLEL EXECUTION FOR LARGE TASKS\n\nWhen task involves 50+ similar items (errors, files, changes), include a `delegation` field:\n\n1. ANALYZE scope and categorize by:\n - Rule/error type (group similar fixes)\n - File/directory (group by location)\n - Dependency order (what must be fixed first)\n\n2. OUTPUT delegation structure with:\n - strategy: 'parallel' (independent), 'sequential' (ordered), 'phased' (groups)\n - tasks: List of sub-tasks with model selection:\n * haiku: Mechanical deletion, simple regex (trivial)\n * sonnet: Type fixes, moderate refactors (moderate)\n * opus: Architecture, security, complex logic (complex)\n - phases: Group tasks that can run in parallel within each phase\n\n3. MODEL SELECTION:\n - Delete unused code → haiku\n - Fix type errors → sonnet\n - Reduce complexity → opus\n - Security fixes → opus\n\n4. DEPENDENCY ORDER:\n - Fix base types before dependent files\n - Fix imports before type errors\n - Mechanical cleanup before logic changes\n\nDO NOT implement - planning only."
|
|
111
126
|
},
|
|
112
127
|
"contextStrategy": {
|
|
113
128
|
"sources": [{ "topic": "ISSUE_OPENED", "limit": 1 }],
|
|
@@ -126,7 +141,8 @@
|
|
|
126
141
|
"summary": "{{result.summary}}",
|
|
127
142
|
"filesAffected": "{{result.filesAffected}}",
|
|
128
143
|
"risks": "{{result.risks}}",
|
|
129
|
-
"delegation": "{{result.delegation}}"
|
|
144
|
+
"delegation": "{{result.delegation}}",
|
|
145
|
+
"acceptanceCriteria": "{{result.acceptanceCriteria}}"
|
|
130
146
|
}
|
|
131
147
|
}
|
|
132
148
|
}
|
|
@@ -138,8 +154,8 @@
|
|
|
138
154
|
"role": "implementation",
|
|
139
155
|
"model": "{{worker_model}}",
|
|
140
156
|
"prompt": {
|
|
141
|
-
"initial": "## 🚫 YOU CANNOT ASK QUESTIONS\n\nYou are running non-interactively. There is NO USER to answer.\n- NEVER use AskUserQuestion tool\n- NEVER say \"Should I...\" or \"Would you like...\"\n- When unsure: Make the SAFER choice and proceed.\n\nYou are an implementation agent for a {{complexity}} {{task_type}} task.\n\n## First Pass - Do It Right\nImplement a COMPLETE solution from PLAN_READY:\n- Follow the plan steps carefully\n- Handle common edge cases (empty, null, error states)\n- Include error handling for likely failures\n- Write clean code with proper types\n- Write tests for ALL new functionality (reference PLAN_READY test requirements)\n- Tests MUST have meaningful assertions (not just existence checks)\n- Tests MUST be isolated and deterministic (no shared state, no network)\n- Verify edge cases from plan are covered\n- Run tests to verify your implementation passes\n\nAim for first-try approval. Don't leave obvious gaps for validators to find.\n\n## EXECUTING DELEGATED TASKS\n\n⚠️ SUB-AGENT LIMITS (CRITICAL - prevents context explosion):\n- Maximum 3 parallel sub-agents at once\n- If phase has more tasks, batch them into groups of 3\n- Prioritize by dependency order, then complexity\n\nIf PLAN_READY contains a 'delegation' field in its data, you MUST use parallel sub-agents:\n\n1. Parse delegation.phases and delegation.tasks from the plan data\n2. For each phase in order:\n a. Find all tasks for this phase (matching taskIds)\n b. Split into batches of MAX 3 tasks each\n c. For each batch:\n - Spawn sub-agents using Task tool (run_in_background: true)\n - Use the model specified in each task (haiku/sonnet/opus)\n - Wait for batch to complete using TaskOutput with block: true\n - SUMMARIZE each result (see OUTPUT HANDLING below)\n - Only proceed to next batch after current batch completes\n3. After ALL phases complete, verify changes work together\n4. Do NOT commit until all sub-agents finish\n\nExample Task tool call for each delegated task:\n```\nTask tool with:\n subagent_type: 'general-purpose'\n model: [task.model from delegation]\n prompt: '[task.description]. Files: [task.scope]. Do NOT commit.'\n run_in_background: true\n```\n\n## SUB-AGENT OUTPUT HANDLING (CRITICAL - prevents context bloat)\n\nWhen TaskOutput returns a sub-agent result, SUMMARIZE immediately:\n- Extract ONLY: success/failure, files modified, key outcomes\n- Discard: full file contents, verbose logs, intermediate steps\n- Keep as: \"Task [id] completed: [2-3 sentence summary]\"\n\nExample: \"Task fix-auth completed: Fixed JWT validation in auth.ts, added null check. Tests pass.\"\n\nDO NOT accumulate full sub-agent output - this causes context explosion.\n\nIf NO delegation field, implement directly as normal.\n\n{{#if complexity == 'CRITICAL'}}\n## CRITICAL TASK - EXTRA CARE\n- Double-check every change\n- No shortcuts or assumptions\n- Consider security implications\n- Add comprehensive error handling\n{{/if}}",
|
|
142
|
-
"subsequent": "## 🚫 YOU CANNOT ASK QUESTIONS\n\nYou are running non-interactively. There is NO USER to answer.\n- NEVER use AskUserQuestion tool\n- NEVER say \"Should I...\" or \"Would you like...\"\n- When unsure: Make the SAFER choice and proceed.\n\nYou are an implementation agent for a {{complexity}} {{task_type}} task.\n\n## VALIDATORS REJECTED YOUR WORK\n\nThis is NOT a minor revision request. Senior engineers reviewed your code and found it UNACCEPTABLE. Read ALL VALIDATION_RESULT messages carefully.\n\n## FIX LIKE A SENIOR ARCHITECT WOULD\n\n### 1. DIAGNOSE BEFORE FIXING\n- Read EVERY rejection reason completely\n- Understand the ROOT CAUSE, not just the symptom\n- If multiple validators rejected, their issues may be related\n- Ask: 'Why did I make this mistake? Is my approach fundamentally flawed?'\n\n### 2. FIX PROPERLY - NO BAND-AIDS\n- A band-aid fix will be caught and rejected again\n- If your approach was wrong, REDESIGN it from scratch\n- Consider: 'Would a senior engineer be proud of this fix?'\n- Think about edge cases, error handling, maintainability\n- Don't just make the error go away - solve the actual problem\n\n### 3. VERIFY COMPREHENSIVELY\n- Test that your fix actually works\n- Verify you didn't break anything else\n- Run relevant tests if they exist\n- If you're unsure, investigate before committing\n\n### 4. ARCHITECTURAL THINKING\n- Consider blast radius of your changes\n- Think about how your fix affects other parts of the system\n- Is there a better abstraction or pattern?\n- Future maintainers will inherit your decisions\n\n## MINDSET\n- Validators are not being pedantic - they found REAL problems\n- Every rejection is expensive - get it right this time\n- Shortcuts and hacks will be caught immediately\n- Pride in craftsmanship: deliver code you'd want to maintain\n\n{{#if complexity == 'CRITICAL'}}\n## CRITICAL TASK - ZERO TOLERANCE FOR SHORTCUTS\n- This is HIGH RISK code (auth, payments, security, production)\n- Triple-check every change\n- Consider all failure modes\n- Security implications must be addressed\n- Comprehensive error handling is MANDATORY\n- If unsure, err on the side of caution\n{{/if}}"
|
|
157
|
+
"initial": "## 🚫 YOU CANNOT ASK QUESTIONS\n\nYou are running non-interactively. There is NO USER to answer.\n- NEVER use AskUserQuestion tool\n- NEVER say \"Should I...\" or \"Would you like...\"\n- When unsure: Make the SAFER choice and proceed.\n\nYou are an implementation agent for a {{complexity}} {{task_type}} task.\n\n## First Pass - Do It Right\nImplement a COMPLETE solution from PLAN_READY:\n- Follow the plan steps carefully\n- Handle common edge cases (empty, null, error states)\n- Include error handling for likely failures\n- Write clean code with proper types\n- Write tests for ALL new functionality (reference PLAN_READY test requirements)\n- Tests MUST have meaningful assertions (not just existence checks)\n- Tests MUST be isolated and deterministic (no shared state, no network)\n- Verify edge cases from plan are covered\n- Run tests to verify your implementation passes\n\nAim for first-try approval. Don't leave obvious gaps for validators to find.\n\n## 🔴 ACCEPTANCE CRITERIA CHECKLIST\n\nBefore publishing IMPLEMENTATION_READY, verify EVERY acceptance criterion from PLAN_READY:\n\n1. **Parse acceptanceCriteria** from PLAN_READY data\n2. **For EACH criterion with priority=MUST**:\n - Execute the verification steps\n - Confirm the criterion is satisfied\n - If NOT satisfied: FIX IT before continuing\n3. **For priority=SHOULD/NICE**: Implement if time permits, document if skipped\n\n**DO NOT publish IMPLEMENTATION_READY if ANY priority=MUST criterion fails.**\n\nValidators will check each criterion explicitly. Missing MUST criteria = instant rejection.\n\n## EXECUTING DELEGATED TASKS\n\n⚠️ SUB-AGENT LIMITS (CRITICAL - prevents context explosion):\n- Maximum 3 parallel sub-agents at once\n- If phase has more tasks, batch them into groups of 3\n- Prioritize by dependency order, then complexity\n\nIf PLAN_READY contains a 'delegation' field in its data, you MUST use parallel sub-agents:\n\n1. Parse delegation.phases and delegation.tasks from the plan data\n2. For each phase in order:\n a. Find all tasks for this phase (matching taskIds)\n b. Split into batches of MAX 3 tasks each\n c. For each batch:\n - Spawn sub-agents using Task tool (run_in_background: true)\n - Use the model specified in each task (haiku/sonnet/opus)\n - Wait for batch to complete using TaskOutput with block: true\n - SUMMARIZE each result (see OUTPUT HANDLING below)\n - Only proceed to next batch after current batch completes\n3. After ALL phases complete, verify changes work together\n4. Do NOT commit until all sub-agents finish\n\nExample Task tool call for each delegated task:\n```\nTask tool with:\n subagent_type: 'general-purpose'\n model: [task.model from delegation]\n prompt: '[task.description]. Files: [task.scope]. Do NOT commit.'\n run_in_background: true\n```\n\n## SUB-AGENT OUTPUT HANDLING (CRITICAL - prevents context bloat)\n\nWhen TaskOutput returns a sub-agent result, SUMMARIZE immediately:\n- Extract ONLY: success/failure, files modified, key outcomes\n- Discard: full file contents, verbose logs, intermediate steps\n- Keep as: \"Task [id] completed: [2-3 sentence summary]\"\n\nExample: \"Task fix-auth completed: Fixed JWT validation in auth.ts, added null check. Tests pass.\"\n\nDO NOT accumulate full sub-agent output - this causes context explosion.\n\nIf NO delegation field, implement directly as normal.\n\n{{#if complexity == 'CRITICAL'}}\n## CRITICAL TASK - EXTRA CARE\n- Double-check every change\n- No shortcuts or assumptions\n- Consider security implications\n- Add comprehensive error handling\n{{/if}}",
|
|
158
|
+
"subsequent": "## 🚫 YOU CANNOT ASK QUESTIONS\n\nYou are running non-interactively. There is NO USER to answer.\n- NEVER use AskUserQuestion tool\n- NEVER say \"Should I...\" or \"Would you like...\"\n- When unsure: Make the SAFER choice and proceed.\n\nYou are an implementation agent for a {{complexity}} {{task_type}} task.\n\n## VALIDATORS REJECTED YOUR WORK\n\nThis is NOT a minor revision request. Senior engineers reviewed your code and found it UNACCEPTABLE. Read ALL VALIDATION_RESULT messages carefully.\n\n## 🔴 CHECK ACCEPTANCE CRITERIA AGAIN\n\nValidators check against the acceptance criteria from PLAN_READY. Before resubmitting:\n1. Re-read EACH criterion (especially priority=MUST ones)\n2. Check if rejection was due to failed criteria\n3. Verify EVERY criterion passes before publishing IMPLEMENTATION_READY\n\n## FIX LIKE A SENIOR ARCHITECT WOULD\n\n### 1. DIAGNOSE BEFORE FIXING\n- Read EVERY rejection reason completely\n- Understand the ROOT CAUSE, not just the symptom\n- If multiple validators rejected, their issues may be related\n- Ask: 'Why did I make this mistake? Is my approach fundamentally flawed?'\n\n### 2. FIX PROPERLY - NO BAND-AIDS\n- A band-aid fix will be caught and rejected again\n- If your approach was wrong, REDESIGN it from scratch\n- Consider: 'Would a senior engineer be proud of this fix?'\n- Think about edge cases, error handling, maintainability\n- Don't just make the error go away - solve the actual problem\n\n### 3. VERIFY COMPREHENSIVELY\n- Test that your fix actually works\n- Verify you didn't break anything else\n- Run relevant tests if they exist\n- If you're unsure, investigate before committing\n\n### 4. ARCHITECTURAL THINKING\n- Consider blast radius of your changes\n- Think about how your fix affects other parts of the system\n- Is there a better abstraction or pattern?\n- Future maintainers will inherit your decisions\n\n## MINDSET\n- Validators are not being pedantic - they found REAL problems\n- Every rejection is expensive - get it right this time\n- Shortcuts and hacks will be caught immediately\n- Pride in craftsmanship: deliver code you'd want to maintain\n\n{{#if complexity == 'CRITICAL'}}\n## CRITICAL TASK - ZERO TOLERANCE FOR SHORTCUTS\n- This is HIGH RISK code (auth, payments, security, production)\n- Triple-check every change\n- Consider all failure modes\n- Security implications must be addressed\n- Comprehensive error handling is MANDATORY\n- If unsure, err on the side of caution\n{{/if}}"
|
|
143
159
|
},
|
|
144
160
|
"contextStrategy": {
|
|
145
161
|
"sources": [
|
|
@@ -188,12 +204,26 @@
|
|
|
188
204
|
"properties": {
|
|
189
205
|
"approved": { "type": "boolean" },
|
|
190
206
|
"summary": { "type": "string" },
|
|
191
|
-
"errors": { "type": "array", "items": { "type": "string" } }
|
|
207
|
+
"errors": { "type": "array", "items": { "type": "string" } },
|
|
208
|
+
"criteriaResults": {
|
|
209
|
+
"type": "array",
|
|
210
|
+
"description": "PASS/FAIL status for each acceptance criterion from PLAN_READY",
|
|
211
|
+
"items": {
|
|
212
|
+
"type": "object",
|
|
213
|
+
"properties": {
|
|
214
|
+
"id": { "type": "string", "description": "AC1, AC2, etc. from plan" },
|
|
215
|
+
"status": { "type": "string", "enum": ["PASS", "FAIL", "SKIPPED"] },
|
|
216
|
+
"evidence": { "type": "string", "description": "How you verified (command output, observation)" },
|
|
217
|
+
"notes": { "type": "string", "description": "Why it failed or additional context" }
|
|
218
|
+
},
|
|
219
|
+
"required": ["id", "status"]
|
|
220
|
+
}
|
|
221
|
+
}
|
|
192
222
|
},
|
|
193
|
-
"required": ["approved", "summary"]
|
|
223
|
+
"required": ["approved", "summary", "criteriaResults"]
|
|
194
224
|
},
|
|
195
225
|
"prompt": {
|
|
196
|
-
"system": "## 🚫 YOU CANNOT ASK QUESTIONS\n\nYou are running non-interactively. There is NO USER to answer.\n- NEVER use AskUserQuestion tool\n- NEVER say \"Should I...\" or \"Would you like...\"\n- When unsure: Make the SAFER choice and proceed.\n\nYou are a requirements validator for a {{complexity}} {{task_type}} task.\n\n## Your Role\nVerify implementation meets requirements. Be thorough. Hold a high bar.\n\n## Validation Checklist - ALL must pass:\n1. Does implementation address ALL requirements from ISSUE_OPENED?\n2. Are edge cases handled? (empty, null, boundaries, error states)\n3. Is error handling present for failure paths?\n4. Are types strict? (no any, no ts-ignore)\n5. Is input validation present at boundaries?\n\n## 🔴 INSTANT REJECTION (Zero tolerance - REJECT immediately):\n- TODO/FIXME/HACK/XXX comments in code = REJECT (incomplete work)\n- console.log/print/debug statements left in code = REJECT (debugging artifacts)\n- Mock/stub/fake implementations where real code expected = REJECT (lazy implementation)\n- Empty catch blocks or error swallowing = REJECT (hiding failures)\n- \"Will implement later\" or partial work = REJECT (incomplete delivery)\n- Any requirement skipped without \"OUT OF SCOPE\" in original spec = REJECT (ignoring requirements)\n- Commented-out code blocks = REJECT (dead code)\n- `any` type in TypeScript = REJECT (type escape hatch)\n\nThese are AUTOMATIC rejections. No exceptions. No \"it's mostly done\". The code is either COMPLETE or it's REJECTED.\n\n## BLOCKING Issues (must reject):\n- Missing core functionality\n- Missing error handling for common failures\n- Hardcoded values that should be configurable\n- Crashes on empty/null input\n- Types not strict\n\n## NON-BLOCKING Issues (note in summary, don't reject alone):\n- Minor style preferences\n- Could be slightly DRYer\n- Rare edge cases\n\n## Output\n- approved: true if all BLOCKING criteria pass\n- summary: Assessment with blocking and non-blocking issues noted\n- errors: List of BLOCKING issues only"
|
|
226
|
+
"system": "## 🚫 YOU CANNOT ASK QUESTIONS\n\nYou are running non-interactively. There is NO USER to answer.\n- NEVER use AskUserQuestion tool\n- NEVER say \"Should I...\" or \"Would you like...\"\n- When unsure: Make the SAFER choice and proceed.\n\nYou are a requirements validator for a {{complexity}} {{task_type}} task.\n\n## Your Role\nVerify implementation meets requirements. Be thorough. Hold a high bar.\n\n## 🔴 ACCEPTANCE CRITERIA VERIFICATION (REQUIRED)\n\n**You MUST check EVERY acceptance criterion from PLAN_READY.**\n\n### Verification Process:\n1. **Parse acceptanceCriteria** from PLAN_READY data\n2. **For EACH criterion**:\n a. Execute the verification steps specified in the criterion\n b. Record PASS or FAIL with evidence (command output, observation)\n c. If FAIL: Add to errors array if priority=MUST\n3. **Output criteriaResults** with status for each criterion\n\n### Automatic Rejection Rules:\n- ANY criterion with priority=MUST that fails → approved: false\n- SHOULD/NICE criteria can fail without rejection (note in summary)\n\n### Example criteriaResults:\n```json\n[\n {\"id\": \"AC1\", \"status\": \"PASS\", \"evidence\": \"npm test shows 15/15 passing\"},\n {\"id\": \"AC2\", \"status\": \"FAIL\", \"evidence\": \"POST /api/users returns 500\", \"notes\": \"Missing validation\"},\n {\"id\": \"AC3\", \"status\": \"PASS\", \"evidence\": \"Manual test: dark mode toggle works\"}\n]\n```\n\n## Validation Checklist - ALL must pass:\n1. Does implementation address ALL requirements from ISSUE_OPENED?\n2. Are edge cases handled? (empty, null, boundaries, error states)\n3. Is error handling present for failure paths?\n4. Are types strict? (no any, no ts-ignore)\n5. Is input validation present at boundaries?\n\n## 🔴 INSTANT REJECTION (Zero tolerance - REJECT immediately):\n- TODO/FIXME/HACK/XXX comments in code = REJECT (incomplete work)\n- console.log/print/debug statements left in code = REJECT (debugging artifacts)\n- Mock/stub/fake implementations where real code expected = REJECT (lazy implementation)\n- Empty catch blocks or error swallowing = REJECT (hiding failures)\n- \"Will implement later\" or partial work = REJECT (incomplete delivery)\n- Any requirement skipped without \"OUT OF SCOPE\" in original spec = REJECT (ignoring requirements)\n- Commented-out code blocks = REJECT (dead code)\n- `any` type in TypeScript = REJECT (type escape hatch)\n\nThese are AUTOMATIC rejections. No exceptions. No \"it's mostly done\". The code is either COMPLETE or it's REJECTED.\n\n## BLOCKING Issues (must reject):\n- Missing core functionality\n- Missing error handling for common failures\n- Hardcoded values that should be configurable\n- Crashes on empty/null input\n- Types not strict\n- **ANY priority=MUST criterion that fails**\n\n## NON-BLOCKING Issues (note in summary, don't reject alone):\n- Minor style preferences\n- Could be slightly DRYer\n- Rare edge cases\n- priority=SHOULD/NICE criteria that fail\n\n## Output\n- approved: true if all BLOCKING criteria pass AND all priority=MUST acceptance criteria pass\n- summary: Assessment with blocking and non-blocking issues noted\n- errors: List of BLOCKING issues only\n- criteriaResults: PASS/FAIL for EACH acceptance criterion"
|
|
197
227
|
},
|
|
198
228
|
"contextStrategy": {
|
|
199
229
|
"sources": [
|
|
@@ -218,7 +248,8 @@
|
|
|
218
248
|
"text": "{{result.summary}}",
|
|
219
249
|
"data": {
|
|
220
250
|
"approved": "{{result.approved}}",
|
|
221
|
-
"errors": "{{result.errors}}"
|
|
251
|
+
"errors": "{{result.errors}}",
|
|
252
|
+
"criteriaResults": "{{result.criteriaResults}}"
|
|
222
253
|
}
|
|
223
254
|
}
|
|
224
255
|
}
|
|
@@ -45,8 +45,9 @@ function parseStreamLine(line) {
|
|
|
45
45
|
return parseUserMessage(event.message);
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
// result - final task result
|
|
48
|
+
// result - final task result (includes token usage and cost)
|
|
49
49
|
if (event.type === 'result') {
|
|
50
|
+
const usage = event.usage || {};
|
|
50
51
|
return {
|
|
51
52
|
type: 'result',
|
|
52
53
|
success: event.subtype === 'success',
|
|
@@ -54,6 +55,13 @@ function parseStreamLine(line) {
|
|
|
54
55
|
error: event.is_error ? event.result : null,
|
|
55
56
|
cost: event.total_cost_usd,
|
|
56
57
|
duration: event.duration_ms,
|
|
58
|
+
// Token usage from Claude API
|
|
59
|
+
inputTokens: usage.input_tokens || 0,
|
|
60
|
+
outputTokens: usage.output_tokens || 0,
|
|
61
|
+
cacheReadInputTokens: usage.cache_read_input_tokens || 0,
|
|
62
|
+
cacheCreationInputTokens: usage.cache_creation_input_tokens || 0,
|
|
63
|
+
// Per-model breakdown (for multi-model tasks)
|
|
64
|
+
modelUsage: event.modelUsage || null,
|
|
57
65
|
};
|
|
58
66
|
}
|
|
59
67
|
|
package/package.json
CHANGED
|
@@ -278,8 +278,31 @@ async function executeTask(agent, triggeringMessage) {
|
|
|
278
278
|
iteration: agent.iteration,
|
|
279
279
|
success: true,
|
|
280
280
|
taskId: agent.currentTaskId,
|
|
281
|
+
tokenUsage: result.tokenUsage || null,
|
|
281
282
|
});
|
|
282
283
|
|
|
284
|
+
// Publish TOKEN_USAGE event for aggregation and tracking
|
|
285
|
+
// CRITICAL: Include taskId for causal linking - allows consumers to group
|
|
286
|
+
// messages by task regardless of interleaved timing from async hooks
|
|
287
|
+
if (result.tokenUsage) {
|
|
288
|
+
agent.messageBus.publish({
|
|
289
|
+
cluster_id: agent.cluster.id,
|
|
290
|
+
topic: 'TOKEN_USAGE',
|
|
291
|
+
sender: agent.id,
|
|
292
|
+
content: {
|
|
293
|
+
text: `${agent.id} used ${result.tokenUsage.inputTokens} input + ${result.tokenUsage.outputTokens} output tokens`,
|
|
294
|
+
data: {
|
|
295
|
+
agentId: agent.id,
|
|
296
|
+
role: agent.role,
|
|
297
|
+
model: agent._selectModel(),
|
|
298
|
+
iteration: agent.iteration,
|
|
299
|
+
taskId: agent.currentTaskId, // Causal linking for message ordering
|
|
300
|
+
...result.tokenUsage,
|
|
301
|
+
},
|
|
302
|
+
},
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
|
|
283
306
|
// Execute onComplete hook
|
|
284
307
|
await executeHook({
|
|
285
308
|
hook: agent.config.hooks?.onComplete,
|
|
@@ -54,6 +54,44 @@ function sanitizeErrorMessage(error) {
|
|
|
54
54
|
// Track if we've already ensured the AskUserQuestion hook is installed
|
|
55
55
|
let askUserQuestionHookInstalled = false;
|
|
56
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Extract token usage from NDJSON output.
|
|
59
|
+
* Looks for the 'result' event line which contains usage data.
|
|
60
|
+
*
|
|
61
|
+
* @param {string} output - Full NDJSON output from Claude CLI
|
|
62
|
+
* @returns {Object|null} Token usage data or null if not found
|
|
63
|
+
*/
|
|
64
|
+
function extractTokenUsage(output) {
|
|
65
|
+
if (!output) return null;
|
|
66
|
+
|
|
67
|
+
const lines = output.split('\n');
|
|
68
|
+
|
|
69
|
+
// Find the result line containing usage data
|
|
70
|
+
for (const line of lines) {
|
|
71
|
+
if (!line.trim()) continue;
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
const event = JSON.parse(line.trim());
|
|
75
|
+
if (event.type === 'result') {
|
|
76
|
+
const usage = event.usage || {};
|
|
77
|
+
return {
|
|
78
|
+
inputTokens: usage.input_tokens || 0,
|
|
79
|
+
outputTokens: usage.output_tokens || 0,
|
|
80
|
+
cacheReadInputTokens: usage.cache_read_input_tokens || 0,
|
|
81
|
+
cacheCreationInputTokens: usage.cache_creation_input_tokens || 0,
|
|
82
|
+
totalCostUsd: event.total_cost_usd || null,
|
|
83
|
+
durationMs: event.duration_ms || null,
|
|
84
|
+
modelUsage: event.modelUsage || null,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
} catch {
|
|
88
|
+
// Not valid JSON, continue
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
|
|
57
95
|
/**
|
|
58
96
|
* Ensure the AskUserQuestion blocking hook is installed in user's Claude config.
|
|
59
97
|
* This adds defense-in-depth by blocking the tool at the Claude CLI level.
|
|
@@ -569,6 +607,7 @@ function followClaudeTaskLogs(agent, taskId) {
|
|
|
569
607
|
success,
|
|
570
608
|
output,
|
|
571
609
|
error: sanitizeErrorMessage(errorContext),
|
|
610
|
+
tokenUsage: extractTokenUsage(output),
|
|
572
611
|
});
|
|
573
612
|
}, 500);
|
|
574
613
|
}
|
|
@@ -590,6 +629,7 @@ function followClaudeTaskLogs(agent, taskId) {
|
|
|
590
629
|
success: false,
|
|
591
630
|
output,
|
|
592
631
|
error: reason,
|
|
632
|
+
tokenUsage: extractTokenUsage(output),
|
|
593
633
|
});
|
|
594
634
|
},
|
|
595
635
|
};
|
|
@@ -907,6 +947,7 @@ function followClaudeTaskLogsIsolated(agent, taskId) {
|
|
|
907
947
|
output: fullOutput,
|
|
908
948
|
taskId,
|
|
909
949
|
result: parsedResult,
|
|
950
|
+
tokenUsage: extractTokenUsage(fullOutput),
|
|
910
951
|
});
|
|
911
952
|
}
|
|
912
953
|
} catch (pollErr) {
|
package/src/ledger.js
CHANGED
|
@@ -116,6 +116,84 @@ class Ledger extends EventEmitter {
|
|
|
116
116
|
}
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
+
/**
|
|
120
|
+
* Append multiple messages atomically using a transaction
|
|
121
|
+
* All messages get contiguous timestamps and are committed together.
|
|
122
|
+
* If any insert fails, the entire batch is rolled back.
|
|
123
|
+
*
|
|
124
|
+
* Use this for task completion messages to prevent interleaving:
|
|
125
|
+
* - TOKEN_USAGE, TASK_COMPLETED, and hook messages published atomically
|
|
126
|
+
* - Other agents' messages cannot appear between them
|
|
127
|
+
*
|
|
128
|
+
* @param {Array<Object>} messages - Array of message objects
|
|
129
|
+
* @returns {Array<Object>} Array of appended messages with generated IDs
|
|
130
|
+
*/
|
|
131
|
+
batchAppend(messages) {
|
|
132
|
+
if (!Array.isArray(messages) || messages.length === 0) {
|
|
133
|
+
return [];
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Create transaction function - all inserts happen atomically
|
|
137
|
+
const insertMany = this.db.transaction((msgs) => {
|
|
138
|
+
const results = [];
|
|
139
|
+
const baseTimestamp = Date.now();
|
|
140
|
+
|
|
141
|
+
for (let i = 0; i < msgs.length; i++) {
|
|
142
|
+
const message = msgs[i];
|
|
143
|
+
const id = message.id || `msg_${crypto.randomBytes(16).toString('hex')}`;
|
|
144
|
+
// Use incrementing timestamps to preserve order within batch
|
|
145
|
+
const timestamp = message.timestamp || (baseTimestamp + i);
|
|
146
|
+
|
|
147
|
+
const record = {
|
|
148
|
+
id,
|
|
149
|
+
timestamp,
|
|
150
|
+
topic: message.topic,
|
|
151
|
+
sender: message.sender,
|
|
152
|
+
receiver: message.receiver || 'broadcast',
|
|
153
|
+
content_text: message.content?.text || null,
|
|
154
|
+
content_data: message.content?.data ? JSON.stringify(message.content.data) : null,
|
|
155
|
+
metadata: message.metadata ? JSON.stringify(message.metadata) : null,
|
|
156
|
+
cluster_id: message.cluster_id,
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
this.stmts.insert.run(
|
|
160
|
+
record.id,
|
|
161
|
+
record.timestamp,
|
|
162
|
+
record.topic,
|
|
163
|
+
record.sender,
|
|
164
|
+
record.receiver,
|
|
165
|
+
record.content_text,
|
|
166
|
+
record.content_data,
|
|
167
|
+
record.metadata,
|
|
168
|
+
record.cluster_id
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
results.push(this._deserializeMessage(record));
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return results;
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
try {
|
|
178
|
+
// Execute transaction (atomic - all or nothing)
|
|
179
|
+
const appendedMessages = insertMany(messages);
|
|
180
|
+
|
|
181
|
+
// Invalidate cache
|
|
182
|
+
this.cache.clear();
|
|
183
|
+
|
|
184
|
+
// Emit events for subscriptions AFTER transaction commits
|
|
185
|
+
// This ensures listeners see consistent state
|
|
186
|
+
for (const fullMessage of appendedMessages) {
|
|
187
|
+
this.emit('message', fullMessage);
|
|
188
|
+
this.emit(`topic:${fullMessage.topic}`, fullMessage);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return appendedMessages;
|
|
192
|
+
} catch (error) {
|
|
193
|
+
throw new Error(`Failed to batch append messages: ${error.message}`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
119
197
|
/**
|
|
120
198
|
* Query messages with filters
|
|
121
199
|
* @param {Object} criteria - Query criteria
|
|
@@ -269,6 +347,77 @@ class Ledger extends EventEmitter {
|
|
|
269
347
|
return rows.map((row) => this._deserializeMessage(row));
|
|
270
348
|
}
|
|
271
349
|
|
|
350
|
+
/**
|
|
351
|
+
* Get aggregated token usage by agent role
|
|
352
|
+
* Queries TOKEN_USAGE messages and sums tokens per role
|
|
353
|
+
* @param {String} cluster_id - Cluster ID
|
|
354
|
+
* @returns {Object} Token usage aggregated by role
|
|
355
|
+
* Example: {
|
|
356
|
+
* implementation: { inputTokens: 5000, outputTokens: 2000, totalCostUsd: 0.05, count: 3 },
|
|
357
|
+
* validator: { inputTokens: 3000, outputTokens: 1500, totalCostUsd: 0.03, count: 2 },
|
|
358
|
+
* _total: { inputTokens: 8000, outputTokens: 3500, totalCostUsd: 0.08, count: 5 }
|
|
359
|
+
* }
|
|
360
|
+
*/
|
|
361
|
+
getTokensByRole(cluster_id) {
|
|
362
|
+
if (!cluster_id) {
|
|
363
|
+
throw new Error('cluster_id is required for getTokensByRole');
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Query all TOKEN_USAGE messages for this cluster
|
|
367
|
+
const sql = `SELECT * FROM messages WHERE cluster_id = ? AND topic = 'TOKEN_USAGE' ORDER BY timestamp ASC`;
|
|
368
|
+
const stmt = this.db.prepare(sql);
|
|
369
|
+
const rows = stmt.all(cluster_id);
|
|
370
|
+
|
|
371
|
+
const byRole = {};
|
|
372
|
+
const total = {
|
|
373
|
+
inputTokens: 0,
|
|
374
|
+
outputTokens: 0,
|
|
375
|
+
cacheReadInputTokens: 0,
|
|
376
|
+
cacheCreationInputTokens: 0,
|
|
377
|
+
totalCostUsd: 0,
|
|
378
|
+
count: 0,
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
for (const row of rows) {
|
|
382
|
+
const message = this._deserializeMessage(row);
|
|
383
|
+
const data = message.content?.data || {};
|
|
384
|
+
const role = data.role || 'unknown';
|
|
385
|
+
|
|
386
|
+
// Initialize role bucket if needed
|
|
387
|
+
if (!byRole[role]) {
|
|
388
|
+
byRole[role] = {
|
|
389
|
+
inputTokens: 0,
|
|
390
|
+
outputTokens: 0,
|
|
391
|
+
cacheReadInputTokens: 0,
|
|
392
|
+
cacheCreationInputTokens: 0,
|
|
393
|
+
totalCostUsd: 0,
|
|
394
|
+
count: 0,
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
// Aggregate tokens for this role
|
|
399
|
+
byRole[role].inputTokens += data.inputTokens || 0;
|
|
400
|
+
byRole[role].outputTokens += data.outputTokens || 0;
|
|
401
|
+
byRole[role].cacheReadInputTokens += data.cacheReadInputTokens || 0;
|
|
402
|
+
byRole[role].cacheCreationInputTokens += data.cacheCreationInputTokens || 0;
|
|
403
|
+
byRole[role].totalCostUsd += data.totalCostUsd || 0;
|
|
404
|
+
byRole[role].count += 1;
|
|
405
|
+
|
|
406
|
+
// Aggregate totals
|
|
407
|
+
total.inputTokens += data.inputTokens || 0;
|
|
408
|
+
total.outputTokens += data.outputTokens || 0;
|
|
409
|
+
total.cacheReadInputTokens += data.cacheReadInputTokens || 0;
|
|
410
|
+
total.cacheCreationInputTokens += data.cacheCreationInputTokens || 0;
|
|
411
|
+
total.totalCostUsd += data.totalCostUsd || 0;
|
|
412
|
+
total.count += 1;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Add total as special _total key
|
|
416
|
+
byRole._total = total;
|
|
417
|
+
|
|
418
|
+
return byRole;
|
|
419
|
+
}
|
|
420
|
+
|
|
272
421
|
/**
|
|
273
422
|
* Subscribe to new messages
|
|
274
423
|
* @param {Function} callback - Called with each new message
|
package/src/message-bus.js
CHANGED
|
@@ -126,6 +126,48 @@ class MessageBus extends EventEmitter {
|
|
|
126
126
|
return this.ledger.getAll(cluster_id);
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
+
/**
|
|
130
|
+
* Publish multiple messages atomically
|
|
131
|
+
* All messages are committed in a single transaction with contiguous timestamps.
|
|
132
|
+
* Use this for task completion to prevent message interleaving between agents.
|
|
133
|
+
*
|
|
134
|
+
* @param {Array<Object>} messages - Array of messages to publish
|
|
135
|
+
* @returns {Array<Object>} Published messages with IDs
|
|
136
|
+
*/
|
|
137
|
+
batchPublish(messages) {
|
|
138
|
+
// Validate all messages
|
|
139
|
+
for (const message of messages) {
|
|
140
|
+
if (!message.cluster_id) {
|
|
141
|
+
throw new Error('cluster_id is required for all messages');
|
|
142
|
+
}
|
|
143
|
+
if (!message.topic) {
|
|
144
|
+
throw new Error('topic is required for all messages');
|
|
145
|
+
}
|
|
146
|
+
if (!message.sender) {
|
|
147
|
+
throw new Error('sender is required for all messages');
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Delegate to ledger's atomic batchAppend
|
|
152
|
+
const published = this.ledger.batchAppend(messages);
|
|
153
|
+
|
|
154
|
+
// Emit to topic-specific listeners for each message
|
|
155
|
+
for (const msg of published) {
|
|
156
|
+
this.emit(`topic:${msg.topic}`, msg);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return published;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Get aggregated token usage by role (passthrough to ledger)
|
|
164
|
+
* @param {String} cluster_id - Cluster ID
|
|
165
|
+
* @returns {Object} Token usage aggregated by role with _total key
|
|
166
|
+
*/
|
|
167
|
+
getTokensByRole(cluster_id) {
|
|
168
|
+
return this.ledger.getTokensByRole(cluster_id);
|
|
169
|
+
}
|
|
170
|
+
|
|
129
171
|
/**
|
|
130
172
|
* Register a WebSocket client for broadcasts
|
|
131
173
|
* @param {WebSocket} ws - WebSocket connection
|
package/src/status-footer.js
CHANGED
|
@@ -8,6 +8,13 @@
|
|
|
8
8
|
*
|
|
9
9
|
* Uses ANSI escape sequences to maintain a fixed footer while
|
|
10
10
|
* allowing normal terminal output to scroll above it.
|
|
11
|
+
*
|
|
12
|
+
* ROBUST RESIZE HANDLING:
|
|
13
|
+
* - Debounced resize events (100ms) prevent rapid-fire redraws
|
|
14
|
+
* - Render lock prevents concurrent renders from corrupting state
|
|
15
|
+
* - Full footer clear before scroll region reset prevents artifacts
|
|
16
|
+
* - Dimension checkpointing skips unnecessary redraws
|
|
17
|
+
* - Graceful degradation for terminals < 8 rows
|
|
11
18
|
*/
|
|
12
19
|
|
|
13
20
|
const { getProcessMetrics } = require('./process-metrics');
|
|
@@ -37,6 +44,20 @@ const COLORS = {
|
|
|
37
44
|
bgBlack: `${CSI}40m`,
|
|
38
45
|
};
|
|
39
46
|
|
|
47
|
+
/**
|
|
48
|
+
* Debounce function - prevents rapid-fire calls during resize
|
|
49
|
+
* @param {Function} fn - Function to debounce
|
|
50
|
+
* @param {number} ms - Debounce delay in milliseconds
|
|
51
|
+
* @returns {Function} Debounced function
|
|
52
|
+
*/
|
|
53
|
+
function debounce(fn, ms) {
|
|
54
|
+
let timeout;
|
|
55
|
+
return (...args) => {
|
|
56
|
+
clearTimeout(timeout);
|
|
57
|
+
timeout = setTimeout(() => fn(...args), ms);
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
40
61
|
/**
|
|
41
62
|
* @typedef {Object} AgentState
|
|
42
63
|
* @property {string} id - Agent ID
|
|
@@ -65,6 +86,17 @@ class StatusFooter {
|
|
|
65
86
|
this.clusterId = null;
|
|
66
87
|
this.clusterState = 'initializing';
|
|
67
88
|
this.startTime = Date.now();
|
|
89
|
+
|
|
90
|
+
// Robust resize handling state
|
|
91
|
+
this.isRendering = false; // Render lock - prevents concurrent renders
|
|
92
|
+
this.pendingResize = false; // Queue resize if render in progress
|
|
93
|
+
this.lastKnownRows = 0; // Track terminal dimensions for change detection
|
|
94
|
+
this.lastKnownCols = 0;
|
|
95
|
+
this.minRows = 8; // Minimum rows for footer display (graceful degradation)
|
|
96
|
+
this.hidden = false; // True when terminal too small for footer
|
|
97
|
+
|
|
98
|
+
// Debounced resize handler (100ms) - prevents rapid-fire redraws
|
|
99
|
+
this._debouncedResize = debounce(() => this._handleResize(), 100);
|
|
68
100
|
}
|
|
69
101
|
|
|
70
102
|
/**
|
|
@@ -95,22 +127,80 @@ class StatusFooter {
|
|
|
95
127
|
process.stdout.write(`${CSI}${row};${col}H`);
|
|
96
128
|
}
|
|
97
129
|
|
|
130
|
+
/**
|
|
131
|
+
* Clear a specific line completely
|
|
132
|
+
* @param {number} row - 1-based row number
|
|
133
|
+
* @private
|
|
134
|
+
*/
|
|
135
|
+
_clearLine(row) {
|
|
136
|
+
process.stdout.write(`${CSI}${row};1H${CLEAR_LINE}`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Clear all footer lines (uses last known height for safety)
|
|
141
|
+
* @private
|
|
142
|
+
*/
|
|
143
|
+
_clearFooterArea() {
|
|
144
|
+
const { rows } = this.getTerminalSize();
|
|
145
|
+
// Use max of current and last footer height to ensure full cleanup
|
|
146
|
+
const heightToClear = Math.max(this.footerHeight, this.lastFooterHeight, 3);
|
|
147
|
+
const startRow = Math.max(1, rows - heightToClear + 1);
|
|
148
|
+
|
|
149
|
+
for (let row = startRow; row <= rows; row++) {
|
|
150
|
+
this._clearLine(row);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
98
154
|
/**
|
|
99
155
|
* Set up scroll region to reserve space for footer
|
|
156
|
+
* ROBUST: Clears footer area first, resets to full screen, then sets new region
|
|
100
157
|
*/
|
|
101
158
|
setupScrollRegion() {
|
|
102
159
|
if (!this.isTTY()) return;
|
|
103
160
|
|
|
104
|
-
const { rows } = this.getTerminalSize();
|
|
161
|
+
const { rows, cols } = this.getTerminalSize();
|
|
162
|
+
|
|
163
|
+
// Graceful degradation: hide footer if terminal too small
|
|
164
|
+
if (rows < this.minRows) {
|
|
165
|
+
if (!this.hidden) {
|
|
166
|
+
this.hidden = true;
|
|
167
|
+
// Reset to full screen scroll
|
|
168
|
+
process.stdout.write(`${CSI}1;${rows}r`);
|
|
169
|
+
this.scrollRegionSet = false;
|
|
170
|
+
}
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Restore footer if terminal grew large enough
|
|
175
|
+
if (this.hidden) {
|
|
176
|
+
this.hidden = false;
|
|
177
|
+
}
|
|
178
|
+
|
|
105
179
|
const scrollEnd = rows - this.footerHeight;
|
|
106
180
|
|
|
107
|
-
//
|
|
181
|
+
// CRITICAL: Save cursor before any manipulation
|
|
182
|
+
process.stdout.write(SAVE_CURSOR);
|
|
183
|
+
process.stdout.write(HIDE_CURSOR);
|
|
184
|
+
|
|
185
|
+
// Step 1: Reset scroll region to full screen first (prevents artifacts)
|
|
186
|
+
process.stdout.write(`${CSI}1;${rows}r`);
|
|
187
|
+
|
|
188
|
+
// Step 2: Clear footer area completely (prevents ghosting)
|
|
189
|
+
this._clearFooterArea();
|
|
190
|
+
|
|
191
|
+
// Step 3: Set new scroll region (lines 1 to scrollEnd)
|
|
108
192
|
process.stdout.write(`${CSI}1;${scrollEnd}r`);
|
|
109
193
|
|
|
110
|
-
// Move cursor to
|
|
111
|
-
process.stdout.write(`${CSI}
|
|
194
|
+
// Step 4: Move cursor to bottom of scroll region (safe position)
|
|
195
|
+
process.stdout.write(`${CSI}${scrollEnd};1H`);
|
|
196
|
+
|
|
197
|
+
// Restore cursor and show it
|
|
198
|
+
process.stdout.write(RESTORE_CURSOR);
|
|
199
|
+
process.stdout.write(SHOW_CURSOR);
|
|
112
200
|
|
|
113
201
|
this.scrollRegionSet = true;
|
|
202
|
+
this.lastKnownRows = rows;
|
|
203
|
+
this.lastKnownCols = cols;
|
|
114
204
|
}
|
|
115
205
|
|
|
116
206
|
/**
|
|
@@ -124,6 +214,35 @@ class StatusFooter {
|
|
|
124
214
|
this.scrollRegionSet = false;
|
|
125
215
|
}
|
|
126
216
|
|
|
217
|
+
/**
|
|
218
|
+
* Handle terminal resize event
|
|
219
|
+
* Called via debounced wrapper to prevent rapid-fire redraws
|
|
220
|
+
* @private
|
|
221
|
+
*/
|
|
222
|
+
_handleResize() {
|
|
223
|
+
if (!this.isTTY()) return;
|
|
224
|
+
|
|
225
|
+
const { rows, cols } = this.getTerminalSize();
|
|
226
|
+
|
|
227
|
+
// Skip if dimensions haven't actually changed (debounce may still fire)
|
|
228
|
+
if (rows === this.lastKnownRows && cols === this.lastKnownCols) {
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// If render in progress, queue resize for after
|
|
233
|
+
if (this.isRendering) {
|
|
234
|
+
this.pendingResize = true;
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Update dimensions and reconfigure
|
|
239
|
+
this.lastKnownRows = rows;
|
|
240
|
+
this.lastKnownCols = cols;
|
|
241
|
+
|
|
242
|
+
this.setupScrollRegion();
|
|
243
|
+
this.render();
|
|
244
|
+
}
|
|
245
|
+
|
|
127
246
|
/**
|
|
128
247
|
* Register cluster for monitoring
|
|
129
248
|
* @param {string} clusterId
|
|
@@ -223,72 +342,101 @@ class StatusFooter {
|
|
|
223
342
|
|
|
224
343
|
/**
|
|
225
344
|
* Render the footer
|
|
345
|
+
* ROBUST: Uses render lock to prevent concurrent renders from corrupting state
|
|
226
346
|
*/
|
|
227
347
|
async render() {
|
|
228
348
|
if (!this.enabled || !this.isTTY()) return;
|
|
229
349
|
|
|
230
|
-
|
|
350
|
+
// Graceful degradation: don't render if hidden
|
|
351
|
+
if (this.hidden) return;
|
|
231
352
|
|
|
232
|
-
//
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
try {
|
|
236
|
-
const metrics = await getProcessMetrics(agent.pid, { samplePeriodMs: 500 });
|
|
237
|
-
this.metricsCache.set(agentId, metrics);
|
|
238
|
-
} catch {
|
|
239
|
-
// Process may have exited
|
|
240
|
-
this.metricsCache.delete(agentId);
|
|
241
|
-
}
|
|
242
|
-
}
|
|
353
|
+
// Render lock: prevent concurrent renders
|
|
354
|
+
if (this.isRendering) {
|
|
355
|
+
return;
|
|
243
356
|
}
|
|
357
|
+
this.isRendering = true;
|
|
244
358
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
.filter(([, agent]) => agent.state === 'executing')
|
|
248
|
-
.slice(0, this.maxAgentRows);
|
|
359
|
+
try {
|
|
360
|
+
const { rows, cols } = this.getTerminalSize();
|
|
249
361
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
362
|
+
// Double-check terminal size (may have changed since last check)
|
|
363
|
+
if (rows < this.minRows) {
|
|
364
|
+
this.hidden = true;
|
|
365
|
+
this.resetScrollRegion();
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
254
368
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
369
|
+
// Collect metrics for all agents with PIDs
|
|
370
|
+
for (const [agentId, agent] of this.agents) {
|
|
371
|
+
if (agent.pid) {
|
|
372
|
+
try {
|
|
373
|
+
const metrics = await getProcessMetrics(agent.pid, { samplePeriodMs: 500 });
|
|
374
|
+
this.metricsCache.set(agentId, metrics);
|
|
375
|
+
} catch {
|
|
376
|
+
// Process may have exited
|
|
377
|
+
this.metricsCache.delete(agentId);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
260
381
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
382
|
+
// Get executing agents for display
|
|
383
|
+
const executingAgents = Array.from(this.agents.entries())
|
|
384
|
+
.filter(([, agent]) => agent.state === 'executing')
|
|
385
|
+
.slice(0, this.maxAgentRows);
|
|
386
|
+
|
|
387
|
+
// Calculate dynamic footer height: header + agent rows + summary
|
|
388
|
+
// Minimum 3 lines (header + "no agents" message + summary)
|
|
389
|
+
const agentRowCount = Math.max(1, executingAgents.length);
|
|
390
|
+
const newHeight = 2 + agentRowCount + 1; // header + agents + summary
|
|
391
|
+
|
|
392
|
+
// Update scroll region if height changed
|
|
393
|
+
if (newHeight !== this.footerHeight) {
|
|
394
|
+
this.lastFooterHeight = this.footerHeight;
|
|
395
|
+
this.footerHeight = newHeight;
|
|
396
|
+
this.setupScrollRegion();
|
|
397
|
+
}
|
|
265
398
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
399
|
+
// Build footer lines
|
|
400
|
+
const headerLine = this.buildHeaderLine(cols);
|
|
401
|
+
const agentRows = this.buildAgentRows(executingAgents, cols);
|
|
402
|
+
const summaryLine = this.buildSummaryLine(cols);
|
|
269
403
|
|
|
270
|
-
|
|
271
|
-
|
|
404
|
+
// Save cursor, render footer, restore cursor
|
|
405
|
+
process.stdout.write(SAVE_CURSOR);
|
|
406
|
+
process.stdout.write(HIDE_CURSOR);
|
|
272
407
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
process.stdout.write(CLEAR_LINE);
|
|
276
|
-
process.stdout.write(`${COLORS.bgBlack}${headerLine}${COLORS.reset}`);
|
|
408
|
+
// Render from top of footer area
|
|
409
|
+
let currentRow = rows - this.footerHeight + 1;
|
|
277
410
|
|
|
278
|
-
|
|
279
|
-
for (const agentRow of agentRows) {
|
|
411
|
+
// Header line
|
|
280
412
|
this.moveTo(currentRow++, 1);
|
|
281
413
|
process.stdout.write(CLEAR_LINE);
|
|
282
|
-
process.stdout.write(`${COLORS.bgBlack}${
|
|
283
|
-
|
|
414
|
+
process.stdout.write(`${COLORS.bgBlack}${headerLine}${COLORS.reset}`);
|
|
415
|
+
|
|
416
|
+
// Agent rows
|
|
417
|
+
for (const agentRow of agentRows) {
|
|
418
|
+
this.moveTo(currentRow++, 1);
|
|
419
|
+
process.stdout.write(CLEAR_LINE);
|
|
420
|
+
process.stdout.write(`${COLORS.bgBlack}${agentRow}${COLORS.reset}`);
|
|
421
|
+
}
|
|
284
422
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
423
|
+
// Summary line (with bottom border)
|
|
424
|
+
this.moveTo(currentRow, 1);
|
|
425
|
+
process.stdout.write(CLEAR_LINE);
|
|
426
|
+
process.stdout.write(`${COLORS.bgBlack}${summaryLine}${COLORS.reset}`);
|
|
289
427
|
|
|
290
|
-
|
|
291
|
-
|
|
428
|
+
process.stdout.write(RESTORE_CURSOR);
|
|
429
|
+
process.stdout.write(SHOW_CURSOR);
|
|
430
|
+
} finally {
|
|
431
|
+
this.isRendering = false;
|
|
432
|
+
|
|
433
|
+
// Process pending resize if one was queued during render
|
|
434
|
+
if (this.pendingResize) {
|
|
435
|
+
this.pendingResize = false;
|
|
436
|
+
// Use setImmediate to avoid deep recursion
|
|
437
|
+
setImmediate(() => this._handleResize());
|
|
438
|
+
}
|
|
439
|
+
}
|
|
292
440
|
}
|
|
293
441
|
|
|
294
442
|
/**
|
|
@@ -402,7 +550,7 @@ class StatusFooter {
|
|
|
402
550
|
parts.push(` ${COLORS.gray}│${COLORS.reset} ${COLORS.dim}${duration}${COLORS.reset}`);
|
|
403
551
|
|
|
404
552
|
// Agent counts
|
|
405
|
-
const executing = Array.from(this.agents.values()).filter(a => a.state === 'executing').length;
|
|
553
|
+
const executing = Array.from(this.agents.values()).filter((a) => a.state === 'executing').length;
|
|
406
554
|
const total = this.agents.size;
|
|
407
555
|
parts.push(` ${COLORS.gray}│${COLORS.reset} ${COLORS.green}${executing}/${total}${COLORS.reset} active`);
|
|
408
556
|
|
|
@@ -456,13 +604,21 @@ class StatusFooter {
|
|
|
456
604
|
return;
|
|
457
605
|
}
|
|
458
606
|
|
|
607
|
+
// Initialize dimension tracking
|
|
608
|
+
const { rows, cols } = this.getTerminalSize();
|
|
609
|
+
this.lastKnownRows = rows;
|
|
610
|
+
this.lastKnownCols = cols;
|
|
611
|
+
|
|
612
|
+
// Check for graceful degradation at startup
|
|
613
|
+
if (rows < this.minRows) {
|
|
614
|
+
this.hidden = true;
|
|
615
|
+
return; // Don't set up scroll region for tiny terminals
|
|
616
|
+
}
|
|
617
|
+
|
|
459
618
|
this.setupScrollRegion();
|
|
460
619
|
|
|
461
|
-
// Handle terminal resize
|
|
462
|
-
process.stdout.on('resize',
|
|
463
|
-
this.setupScrollRegion();
|
|
464
|
-
this.render();
|
|
465
|
-
});
|
|
620
|
+
// Handle terminal resize with debounced handler
|
|
621
|
+
process.stdout.on('resize', this._debouncedResize);
|
|
466
622
|
|
|
467
623
|
// Start refresh interval
|
|
468
624
|
this.intervalId = setInterval(() => {
|
|
@@ -482,19 +638,19 @@ class StatusFooter {
|
|
|
482
638
|
this.intervalId = null;
|
|
483
639
|
}
|
|
484
640
|
|
|
485
|
-
|
|
641
|
+
// Remove resize listener
|
|
642
|
+
process.stdout.removeListener('resize', this._debouncedResize);
|
|
643
|
+
|
|
644
|
+
if (this.isTTY() && !this.hidden) {
|
|
486
645
|
// Reset scroll region
|
|
487
646
|
this.resetScrollRegion();
|
|
488
647
|
|
|
489
|
-
// Clear all footer lines
|
|
490
|
-
|
|
491
|
-
const startRow = rows - this.footerHeight + 1;
|
|
492
|
-
for (let row = startRow; row <= rows; row++) {
|
|
493
|
-
this.moveTo(row, 1);
|
|
494
|
-
process.stdout.write(CLEAR_LINE);
|
|
495
|
-
}
|
|
648
|
+
// Clear all footer lines
|
|
649
|
+
this._clearFooterArea();
|
|
496
650
|
|
|
497
651
|
// Move cursor to safe position
|
|
652
|
+
const { rows } = this.getTerminalSize();
|
|
653
|
+
const startRow = rows - this.footerHeight + 1;
|
|
498
654
|
this.moveTo(startRow, 1);
|
|
499
655
|
process.stdout.write(SHOW_CURSOR);
|
|
500
656
|
}
|
|
@@ -507,14 +663,7 @@ class StatusFooter {
|
|
|
507
663
|
if (!this.isTTY()) return;
|
|
508
664
|
|
|
509
665
|
this.resetScrollRegion();
|
|
510
|
-
|
|
511
|
-
// Clear all footer lines (dynamic height)
|
|
512
|
-
const { rows } = this.getTerminalSize();
|
|
513
|
-
const startRow = rows - this.footerHeight + 1;
|
|
514
|
-
for (let row = startRow; row <= rows; row++) {
|
|
515
|
-
this.moveTo(row, 1);
|
|
516
|
-
process.stdout.write(CLEAR_LINE);
|
|
517
|
-
}
|
|
666
|
+
this._clearFooterArea();
|
|
518
667
|
}
|
|
519
668
|
|
|
520
669
|
/**
|
|
@@ -523,6 +672,14 @@ class StatusFooter {
|
|
|
523
672
|
show() {
|
|
524
673
|
if (!this.isTTY()) return;
|
|
525
674
|
|
|
675
|
+
// Reset hidden state and check terminal size
|
|
676
|
+
const { rows } = this.getTerminalSize();
|
|
677
|
+
if (rows < this.minRows) {
|
|
678
|
+
this.hidden = true;
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
this.hidden = false;
|
|
526
683
|
this.setupScrollRegion();
|
|
527
684
|
this.render();
|
|
528
685
|
}
|