@covibes/zeroshot 1.1.3 ā 1.2.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 +21 -0
- package/cli/index.js +2 -2
- package/cluster-templates/base-templates/debug-workflow.json +0 -1
- package/cluster-templates/base-templates/full-workflow.json +0 -1
- package/cluster-templates/base-templates/single-worker.json +0 -1
- package/cluster-templates/base-templates/worker-validator.json +0 -1
- package/lib/settings.js +1 -1
- package/lib/stream-json-parser.js +9 -1
- package/package.json +1 -1
- package/src/agent/agent-lifecycle.js +20 -0
- package/src/agent/agent-task-executor.js +41 -0
- package/src/process-metrics.js +10 -2
- package/src/status-footer.js +231 -74
- package/src/tui/formatters.js +6 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,24 @@
|
|
|
1
|
+
# [1.2.0](https://github.com/covibes/zeroshot/compare/v1.1.4...v1.2.0) (2025-12-28)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* **status-footer:** robust terminal resize handling ([767a610](https://github.com/covibes/zeroshot/commit/767a610027b3e2bb238b54c31a3a7e93db635319))
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Features
|
|
10
|
+
|
|
11
|
+
* **agent:** publish TOKEN_USAGE events with task completion ([c79482c](https://github.com/covibes/zeroshot/commit/c79482c82582b75a692ba71005c821decdc1d769))
|
|
12
|
+
* **stream-parser:** add token usage tracking to result events ([91ad850](https://github.com/covibes/zeroshot/commit/91ad8507f42fd1a398bdc06f3b91b0a13eec8941))
|
|
13
|
+
|
|
14
|
+
## [1.1.4](https://github.com/covibes/zeroshot/compare/v1.1.3...v1.1.4) (2025-12-28)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
### Bug Fixes
|
|
18
|
+
|
|
19
|
+
* **cli:** read version from package.json instead of hardcoded value ([a6e0e57](https://github.com/covibes/zeroshot/commit/a6e0e570feeaffa64dbc46d494eeef000f32b708))
|
|
20
|
+
* **cli:** resolve streaming mode crash and refactor message formatters ([efb9264](https://github.com/covibes/zeroshot/commit/efb9264ce0d3ede0eb7d502d4625694c2c525230))
|
|
21
|
+
|
|
1
22
|
## [1.1.3](https://github.com/covibes/zeroshot/compare/v1.1.2...v1.1.3) (2025-12-28)
|
|
2
23
|
|
|
3
24
|
|
package/cli/index.js
CHANGED
|
@@ -341,7 +341,7 @@ if (shouldShowBanner) {
|
|
|
341
341
|
program
|
|
342
342
|
.name('zeroshot')
|
|
343
343
|
.description('Multi-agent orchestration and task management for Claude')
|
|
344
|
-
.version('
|
|
344
|
+
.version(require('../package.json').version)
|
|
345
345
|
.addHelpText(
|
|
346
346
|
'after',
|
|
347
347
|
`
|
|
@@ -3666,7 +3666,7 @@ function printMessage(msg, showClusterId = false, watchMode = false, isActive =
|
|
|
3666
3666
|
}
|
|
3667
3667
|
|
|
3668
3668
|
if (msg.topic === 'ISSUE_OPENED') {
|
|
3669
|
-
formatIssueOpenedNormal(msg, prefix, timestamp);
|
|
3669
|
+
formatIssueOpenedNormal(msg, prefix, timestamp, shownNewTaskForCluster);
|
|
3670
3670
|
return;
|
|
3671
3671
|
}
|
|
3672
3672
|
|
|
@@ -81,7 +81,6 @@
|
|
|
81
81
|
"id": "fixer",
|
|
82
82
|
"role": "implementation",
|
|
83
83
|
"model": "{{fixer_model}}",
|
|
84
|
-
"outputFormat": "stream-json",
|
|
85
84
|
"prompt": {
|
|
86
85
|
"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 bug fixer. Apply the fix from the investigator.\n\n## Your Job\nFix the root cause identified in INVESTIGATION_COMPLETE.\n\n## Fix Guidelines\n- Fix the ROOT CAUSE, not just the symptom\n- Make minimal changes (don't refactor unrelated code)\n- Add comments explaining WHY if fix is non-obvious\n- Consider if same bug exists elsewhere\n\n## After Fixing\n- Run the failing tests to verify fix works\n- Run related tests for regressions\n- Add test case that would catch this bug if it recurs\n\n## š LARGE TASKS - USE SUB-AGENTS\n\nIf task affects >10 files OR >50 errors, DO NOT fix manually. Use the Task tool to spawn parallel sub-agents:\n\n1. **Analyze scope first** - Count files/errors, group by directory or error type\n2. **Spawn sub-agents** - One per group, run in parallel\n3. **Choose model wisely:**\n - **haiku**: Mechanical fixes (unused vars, missing imports, simple type annotations)\n - **sonnet**: Complex fixes (refactoring, logic changes, architectural decisions)\n4. **Aggregate results** - Wait for all sub-agents, verify combined fix\n\nExample Task tool usage:\n```\nTask(prompt=\"Fix all @typescript-eslint/no-unused-vars errors in client/src/components/features/agents/. Prefix intentionally unused params with underscore, remove genuinely unused variables.\", model=\"haiku\")\n```\n\nDO NOT waste iterations doing manual work that sub-agents can parallelize.\n\n## š“ FORBIDDEN - DO NOT FUCKING DO THESE\n\nThese are SHORTCUTS that HIDE problems instead of FIXING them:\n\n- ā NEVER disable or suppress errors/warnings (config changes, disable comments, ignore directives)\n- ā NEVER change test expectations to match broken behavior\n- ā NEVER use unsafe type casts or `any` to silence type errors\n- ā NEVER add TODO/FIXME instead of actually fixing\n- ā NEVER work around the problem - FIX THE ACTUAL CODE\n\nIF THE PROBLEM STILL EXISTS BUT IS HIDDEN, YOU HAVE NOT FIXED IT.\n\n## On Rejection - READ THE FUCKING FEEDBACK\n\nWhen tester rejects:\n1. STOP. READ what they wrote. UNDERSTAND the issue.\n2. If same problem persists ā your fix is WRONG, try DIFFERENT approach\n3. If new problems appeared ā your fix BROKE something, REVERT and rethink\n4. Do NOT blindly retry the same approach\n5. If you are STUCK, say so. Do not waste iterations doing nothing.\n\nRepeating failed approaches = wasted time and money. LEARN from rejection."
|
|
87
86
|
},
|
|
@@ -137,7 +137,6 @@
|
|
|
137
137
|
"id": "worker",
|
|
138
138
|
"role": "implementation",
|
|
139
139
|
"model": "{{worker_model}}",
|
|
140
|
-
"outputFormat": "stream-json",
|
|
141
140
|
"prompt": {
|
|
142
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}}",
|
|
143
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}}"
|
|
@@ -19,7 +19,6 @@
|
|
|
19
19
|
"id": "worker",
|
|
20
20
|
"role": "implementation",
|
|
21
21
|
"model": "{{worker_model}}",
|
|
22
|
-
"outputFormat": "stream-json",
|
|
23
22
|
"prompt": {
|
|
24
23
|
"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 an agent handling a {{task_type}} task.\n\n## TASK TYPE: {{task_type}}\n\n{{#if task_type == 'INQUIRY'}}\nThis is an INQUIRY - exploration and understanding only.\n- Answer questions about the codebase\n- Explore files and explain how things work\n- DO NOT make any changes\n- Provide clear, accurate information\n{{/if}}\n\n{{#if task_type == 'TASK'}}\nThis is a TRIVIAL TASK - quick execution.\n- Straightforward, well-defined action\n- Quick to complete (< 15 minutes)\n- Low risk of breaking existing functionality\n- Execute efficiently, verify it works, done\n{{/if}}\n\n{{#if task_type == 'DEBUG'}}\nThis is a TRIVIAL DEBUG - simple fix.\n- Obvious issue with clear solution\n- Fix the root cause, not symptoms\n- Verify the fix works\n{{/if}}"
|
|
25
24
|
},
|
|
@@ -25,7 +25,6 @@
|
|
|
25
25
|
"id": "worker",
|
|
26
26
|
"role": "implementation",
|
|
27
27
|
"model": "{{worker_model}}",
|
|
28
|
-
"outputFormat": "stream-json",
|
|
29
28
|
"prompt": {
|
|
30
29
|
"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 an implementation agent for a SIMPLE {{task_type}} task.\n\n## FIRST ITERATION\n\n{{#if task_type == 'TASK'}}\nImplement the requested feature/change:\n- Well-defined scope (one feature, one fix)\n- Standard patterns apply\n- Complete the implementation fully\n{{/if}}\n\n{{#if task_type == 'DEBUG'}}\nInvestigate and fix the issue:\n- Reproduce the problem\n- Find the root cause (not just symptoms)\n- Apply the fix\n- Verify it works\n{{/if}}\n\n{{#if task_type == 'INQUIRY'}}\nResearch and provide detailed answers:\n- Explore relevant code and documentation\n- Explain how things work\n- Provide accurate, complete information\n{{/if}}\n\n## SUBSEQUENT ITERATIONS (after rejection)\n\nYou are being called back because validators REJECTED your implementation. This is NOT a minor issue.\n\n### FIX LIKE A SENIOR ENGINEER\n\n1. **STOP AND UNDERSTAND FIRST**\n - Read ALL VALIDATION_RESULT messages completely\n - Understand WHY each issue exists, not just WHAT it is\n - Trace the root cause - don't patch symptoms\n\n2. **FIX PROPERLY - NO SHORTCUTS**\n - Fix the ACTUAL problem, not the error message\n - If your approach was wrong, redesign it - don't add band-aids\n - Consider architectural implications of your fix\n - A senior dev would be embarrassed to submit a half-fix\n\n3. **VERIFY YOUR FIX**\n - Test your changes actually work\n - Check you didn't break anything else\n - If unsure, investigate before committing\n\n### MINDSET\n- Validators are senior engineers reviewing your code\n- They found REAL problems - take them seriously\n- Shortcuts will be caught and rejected again"
|
|
31
30
|
},
|
package/lib/settings.js
CHANGED
|
@@ -15,7 +15,7 @@ const DEFAULT_SETTINGS = {
|
|
|
15
15
|
defaultModel: 'sonnet',
|
|
16
16
|
defaultConfig: 'conductor-bootstrap',
|
|
17
17
|
defaultIsolation: false,
|
|
18
|
-
strictSchema:
|
|
18
|
+
strictSchema: true, // true = reliable json output (default), false = live streaming (may crash - see bold-meadow-11)
|
|
19
19
|
logLevel: 'normal',
|
|
20
20
|
};
|
|
21
21
|
|
|
@@ -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,28 @@ 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
|
+
if (result.tokenUsage) {
|
|
286
|
+
agent.messageBus.publish({
|
|
287
|
+
cluster_id: agent.cluster.id,
|
|
288
|
+
topic: 'TOKEN_USAGE',
|
|
289
|
+
sender: agent.id,
|
|
290
|
+
content: {
|
|
291
|
+
text: `${agent.id} used ${result.tokenUsage.inputTokens} input + ${result.tokenUsage.outputTokens} output tokens`,
|
|
292
|
+
data: {
|
|
293
|
+
agentId: agent.id,
|
|
294
|
+
role: agent.role,
|
|
295
|
+
model: agent._selectModel(),
|
|
296
|
+
iteration: agent.iteration,
|
|
297
|
+
...result.tokenUsage,
|
|
298
|
+
},
|
|
299
|
+
},
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
|
|
283
303
|
// Execute onComplete hook
|
|
284
304
|
await executeHook({
|
|
285
305
|
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/process-metrics.js
CHANGED
|
@@ -366,7 +366,11 @@ async function getProcessMetricsLinuxAggregated(pid, samplePeriodMs) {
|
|
|
366
366
|
const clockTicks = 100; // Usually 100 on Linux
|
|
367
367
|
const cpuSeconds = totalCpuTicksDelta / clockTicks;
|
|
368
368
|
const sampleSeconds = samplePeriodMs / 1000;
|
|
369
|
-
const
|
|
369
|
+
const rawCpuPercent = (cpuSeconds / sampleSeconds) * 100;
|
|
370
|
+
|
|
371
|
+
// Normalize to per-core average (0-100% range)
|
|
372
|
+
const cpuCores = require('os').cpus().length;
|
|
373
|
+
const cpuPercent = Math.min(100, rawCpuPercent / cpuCores);
|
|
370
374
|
|
|
371
375
|
// Get network state for all processes
|
|
372
376
|
let network = { established: 0, hasActivity: false, sendQueueBytes: 0, recvQueueBytes: 0, bytesSent: 0, bytesReceived: 0 };
|
|
@@ -444,10 +448,14 @@ function getProcessMetricsDarwinAggregated(pid) {
|
|
|
444
448
|
if (netState.hasActivity) network.hasActivity = true;
|
|
445
449
|
}
|
|
446
450
|
|
|
451
|
+
// Normalize to per-core average (0-100% range)
|
|
452
|
+
const cpuCores = require('os').cpus().length;
|
|
453
|
+
const normalizedCpu = Math.min(100, totalCpuPercent / cpuCores);
|
|
454
|
+
|
|
447
455
|
return {
|
|
448
456
|
pid,
|
|
449
457
|
exists: true,
|
|
450
|
-
cpuPercent: parseFloat(
|
|
458
|
+
cpuPercent: parseFloat(normalizedCpu.toFixed(1)),
|
|
451
459
|
memoryMB: parseFloat((totalMemoryKB / 1024).toFixed(1)),
|
|
452
460
|
state: rootState,
|
|
453
461
|
threads: totalThreads,
|
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
|
}
|
package/src/tui/formatters.js
CHANGED
|
@@ -62,7 +62,12 @@ const formatBytes = (bytes) => {
|
|
|
62
62
|
*/
|
|
63
63
|
const formatCPU = (percent) => {
|
|
64
64
|
if (typeof percent !== 'number' || percent < 0) return '0.0%';
|
|
65
|
-
|
|
65
|
+
|
|
66
|
+
// Assert normalized range (should never exceed 100% after per-core normalization)
|
|
67
|
+
if (percent > 100) {
|
|
68
|
+
console.warn(`[formatCPU] CPU percent ${percent}% exceeds 100% - normalization bug?`);
|
|
69
|
+
percent = 100;
|
|
70
|
+
}
|
|
66
71
|
|
|
67
72
|
return `${percent.toFixed(1)}%`;
|
|
68
73
|
};
|