@covibes/zeroshot 1.0.1 → 1.1.3
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 +46 -0
- package/README.md +2 -0
- package/cli/index.js +151 -208
- package/cli/message-formatter-utils.js +75 -0
- package/cli/message-formatters-normal.js +214 -0
- package/cli/message-formatters-watch.js +181 -0
- package/cluster-templates/base-templates/full-workflow.json +10 -5
- package/docker/zeroshot-cluster/Dockerfile +6 -0
- package/package.json +5 -2
- package/src/agent/agent-task-executor.js +237 -112
- package/src/isolation-manager.js +94 -51
- package/src/orchestrator.js +45 -10
- package/src/preflight.js +383 -0
- package/src/process-metrics.js +546 -0
- package/src/status-footer.js +543 -0
- package/task-lib/attachable-watcher.js +202 -0
- package/task-lib/commands/clean.js +50 -0
- package/task-lib/commands/get-log-path.js +23 -0
- package/task-lib/commands/kill.js +32 -0
- package/task-lib/commands/list.js +105 -0
- package/task-lib/commands/logs.js +411 -0
- package/task-lib/commands/resume.js +41 -0
- package/task-lib/commands/run.js +48 -0
- package/task-lib/commands/schedule.js +105 -0
- package/task-lib/commands/scheduler-cmd.js +96 -0
- package/task-lib/commands/schedules.js +98 -0
- package/task-lib/commands/status.js +44 -0
- package/task-lib/commands/unschedule.js +16 -0
- package/task-lib/completion.js +9 -0
- package/task-lib/config.js +10 -0
- package/task-lib/name-generator.js +230 -0
- package/task-lib/package.json +3 -0
- package/task-lib/runner.js +123 -0
- package/task-lib/scheduler.js +252 -0
- package/task-lib/store.js +217 -0
- package/task-lib/tui/formatters.js +166 -0
- package/task-lib/tui/index.js +197 -0
- package/task-lib/tui/layout.js +111 -0
- package/task-lib/tui/renderer.js +119 -0
- package/task-lib/tui.js +384 -0
- package/task-lib/watcher.js +162 -0
- package/cluster-templates/conductor-junior-bootstrap.json +0 -69
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,49 @@
|
|
|
1
|
+
## [1.1.3](https://github.com/covibes/zeroshot/compare/v1.1.2...v1.1.3) (2025-12-28)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* **publish:** remove tests from prepublishOnly to prevent double execution ([3e11e71](https://github.com/covibes/zeroshot/commit/3e11e71cb722f835634d21f80fee79ea3c29b031))
|
|
7
|
+
|
|
8
|
+
## [1.1.2](https://github.com/covibes/zeroshot/compare/v1.1.1...v1.1.2) (2025-12-28)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* **ci:** resolve ESLint violations and status-footer test failures ([0d794f9](https://github.com/covibes/zeroshot/commit/0d794f98aa10d2492d8ab0af516bb1e5abee0566))
|
|
14
|
+
* **isolation:** handle missing/directory .gitconfig in CI environments ([3d754e4](https://github.com/covibes/zeroshot/commit/3d754e4a02d40e2fd902d97d17a6532ba247f780))
|
|
15
|
+
* **workflow:** extract tarball filename correctly from npm pack output ([3cf48a3](https://github.com/covibes/zeroshot/commit/3cf48a3ddf4f1938916c7ed5a2be1796003a988f))
|
|
16
|
+
|
|
17
|
+
## [1.1.1](https://github.com/covibes/zeroshot/compare/v1.1.0...v1.1.1) (2025-12-28)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
### Bug Fixes
|
|
21
|
+
|
|
22
|
+
* **lint:** resolve require-await and unused-imports errors ([852c8a0](https://github.com/covibes/zeroshot/commit/852c8a0e9076eb5403105c6f319e66e53c27fd6d))
|
|
23
|
+
|
|
24
|
+
# [1.1.0](https://github.com/covibes/zeroshot/compare/v1.0.2...v1.1.0) (2025-12-28)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
### Bug Fixes
|
|
28
|
+
|
|
29
|
+
* **docker:** use repo root as build context for Dockerfile ([c1d6719](https://github.com/covibes/zeroshot/commit/c1d6719eb43787ba62e5f69663eb4e5bd1aeb492))
|
|
30
|
+
* **lint:** remove unused import and fix undefined variable in test ([41c9965](https://github.com/covibes/zeroshot/commit/41c9965eb84d2b8c22eaaf8e1d65a5f41c7b1e44))
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
### Features
|
|
34
|
+
|
|
35
|
+
* **isolation:** use zeroshot task infrastructure inside containers ([922f30d](https://github.com/covibes/zeroshot/commit/922f30d5ddd8c4d87cac375fd97025f402e7c43e))
|
|
36
|
+
* **monitoring:** add live status footer with CPU/memory metrics ([2df3de0](https://github.com/covibes/zeroshot/commit/2df3de0a1fe9573961b596da9e78a159f3c33086))
|
|
37
|
+
* **validators:** add zero-tolerance rejection rules for incomplete code ([308aef8](https://github.com/covibes/zeroshot/commit/308aef8b5ee2e3ff05e336ee810b842492183b2e))
|
|
38
|
+
* **validators:** strengthen with senior engineering principles ([d83f666](https://github.com/covibes/zeroshot/commit/d83f6668a145e36bd7d807d9821e8631a3a1cc18))
|
|
39
|
+
|
|
40
|
+
## [1.0.2](https://github.com/covibes/zeroshot/compare/v1.0.1...v1.0.2) (2025-12-27)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
### Bug Fixes
|
|
44
|
+
|
|
45
|
+
* include task-lib in npm package ([37602fb](https://github.com/covibes/zeroshot/commit/37602fb3f1f6cd735d8db232be5829dc342b815d))
|
|
46
|
+
|
|
1
47
|
## [1.0.1](https://github.com/covibes/zeroshot/compare/v1.0.0...v1.0.1) (2025-12-27)
|
|
2
48
|
|
|
3
49
|
|
package/README.md
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
[](https://nodejs.org/)
|
|
5
5
|
[]()
|
|
6
6
|
|
|
7
|
+
> **2024** was the year of LLMs. **2025** was the year of agents. **2026** is the year of agent clusters.
|
|
8
|
+
|
|
7
9
|
**Multi-agent coding CLI built on Claude Code.**
|
|
8
10
|
|
|
9
11
|
You know the problem. Your AI agent:
|
package/cli/index.js
CHANGED
|
@@ -22,6 +22,23 @@ const chalk = require('chalk');
|
|
|
22
22
|
const Orchestrator = require('../src/orchestrator');
|
|
23
23
|
const { setupCompletion } = require('../lib/completion');
|
|
24
24
|
const { parseChunk } = require('../lib/stream-json-parser');
|
|
25
|
+
const { formatWatchMode } = require('./message-formatters-watch');
|
|
26
|
+
const {
|
|
27
|
+
formatAgentLifecycle,
|
|
28
|
+
formatAgentError: formatAgentErrorNormal,
|
|
29
|
+
formatIssueOpened: formatIssueOpenedNormal,
|
|
30
|
+
formatImplementationReady: formatImplementationReadyNormal,
|
|
31
|
+
formatValidationResult: formatValidationResultNormal,
|
|
32
|
+
formatPrCreated,
|
|
33
|
+
formatClusterComplete,
|
|
34
|
+
formatClusterFailed,
|
|
35
|
+
formatGenericMessage,
|
|
36
|
+
} = require('./message-formatters-normal');
|
|
37
|
+
const {
|
|
38
|
+
getColorForSender,
|
|
39
|
+
buildMessagePrefix,
|
|
40
|
+
buildClusterPrefix,
|
|
41
|
+
} = require('./message-formatter-utils');
|
|
25
42
|
const {
|
|
26
43
|
loadSettings,
|
|
27
44
|
saveSettings,
|
|
@@ -29,6 +46,8 @@ const {
|
|
|
29
46
|
coerceValue,
|
|
30
47
|
DEFAULT_SETTINGS,
|
|
31
48
|
} = require('../lib/settings');
|
|
49
|
+
const { requirePreflight } = require('../src/preflight');
|
|
50
|
+
const { StatusFooter } = require('../src/status-footer');
|
|
32
51
|
|
|
33
52
|
const program = new Command();
|
|
34
53
|
|
|
@@ -317,11 +336,7 @@ if (shouldShowBanner) {
|
|
|
317
336
|
showBanner();
|
|
318
337
|
}
|
|
319
338
|
|
|
320
|
-
//
|
|
321
|
-
const COLORS = [chalk.cyan, chalk.yellow, chalk.magenta, chalk.blue, chalk.white, chalk.gray];
|
|
322
|
-
|
|
323
|
-
// Map agent IDs to colors
|
|
324
|
-
const agentColors = new Map();
|
|
339
|
+
// NOTE: Agent color handling moved to message-formatter-utils.js
|
|
325
340
|
|
|
326
341
|
program
|
|
327
342
|
.name('zeroshot')
|
|
@@ -423,6 +438,16 @@ Input formats:
|
|
|
423
438
|
input.text = inputArg;
|
|
424
439
|
}
|
|
425
440
|
|
|
441
|
+
// === PREFLIGHT CHECKS ===
|
|
442
|
+
// Validate all dependencies BEFORE starting anything
|
|
443
|
+
// This gives users clear, actionable error messages upfront
|
|
444
|
+
const preflightOptions = {
|
|
445
|
+
requireGh: !!input.issue, // gh CLI required when fetching GitHub issues
|
|
446
|
+
requireDocker: options.isolation || options.full, // Docker required for isolation mode
|
|
447
|
+
quiet: process.env.CREW_DAEMON === '1', // Suppress success in daemon mode
|
|
448
|
+
};
|
|
449
|
+
requirePreflight(preflightOptions);
|
|
450
|
+
|
|
426
451
|
// === CLUSTER MODE ===
|
|
427
452
|
// Validate --pr requires --isolation
|
|
428
453
|
if (options.pr && !options.isolation) {
|
|
@@ -580,6 +605,60 @@ Input formats:
|
|
|
580
605
|
// Track messages we've already processed (to avoid duplicates between history and subscription)
|
|
581
606
|
const processedMessageIds = new Set();
|
|
582
607
|
|
|
608
|
+
// === STATUS FOOTER: Live agent monitoring ===
|
|
609
|
+
// Shows CPU, memory, network metrics for all agents at bottom of terminal
|
|
610
|
+
const statusFooter = new StatusFooter({
|
|
611
|
+
refreshInterval: 1000,
|
|
612
|
+
enabled: process.stdout.isTTY,
|
|
613
|
+
});
|
|
614
|
+
statusFooter.setCluster(clusterId);
|
|
615
|
+
statusFooter.setClusterState('running');
|
|
616
|
+
|
|
617
|
+
// Subscribe to AGENT_LIFECYCLE to track agent states and PIDs
|
|
618
|
+
const lifecycleUnsubscribe = cluster.messageBus.subscribeTopic('AGENT_LIFECYCLE', (msg) => {
|
|
619
|
+
const data = msg.content?.data || {};
|
|
620
|
+
const event = data.event;
|
|
621
|
+
const agentId = data.agent || msg.sender;
|
|
622
|
+
|
|
623
|
+
// Update agent state based on lifecycle event
|
|
624
|
+
if (event === 'STARTED') {
|
|
625
|
+
statusFooter.updateAgent({
|
|
626
|
+
id: agentId,
|
|
627
|
+
state: 'idle',
|
|
628
|
+
pid: null,
|
|
629
|
+
iteration: data.iteration || 0,
|
|
630
|
+
});
|
|
631
|
+
} else if (event === 'TASK_STARTED') {
|
|
632
|
+
statusFooter.updateAgent({
|
|
633
|
+
id: agentId,
|
|
634
|
+
state: 'executing',
|
|
635
|
+
pid: statusFooter.agents.get(agentId)?.pid || null,
|
|
636
|
+
iteration: data.iteration || 0,
|
|
637
|
+
});
|
|
638
|
+
} else if (event === 'PROCESS_SPAWNED') {
|
|
639
|
+
// Got the PID - update the agent with it
|
|
640
|
+
const current = statusFooter.agents.get(agentId) || { state: 'executing', iteration: 0 };
|
|
641
|
+
statusFooter.updateAgent({
|
|
642
|
+
id: agentId,
|
|
643
|
+
state: current.state,
|
|
644
|
+
pid: data.pid,
|
|
645
|
+
iteration: current.iteration,
|
|
646
|
+
});
|
|
647
|
+
} else if (event === 'TASK_COMPLETED' || event === 'TASK_FAILED') {
|
|
648
|
+
statusFooter.updateAgent({
|
|
649
|
+
id: agentId,
|
|
650
|
+
state: 'idle',
|
|
651
|
+
pid: null,
|
|
652
|
+
iteration: data.iteration || 0,
|
|
653
|
+
});
|
|
654
|
+
} else if (event === 'STOPPED') {
|
|
655
|
+
statusFooter.removeAgent(agentId);
|
|
656
|
+
}
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
// Start the status footer
|
|
660
|
+
statusFooter.start();
|
|
661
|
+
|
|
583
662
|
// Message handler - processes messages, deduplicates by ID
|
|
584
663
|
const handleMessage = (msg) => {
|
|
585
664
|
if (msg.cluster_id !== clusterId) return;
|
|
@@ -618,6 +697,9 @@ Input formats:
|
|
|
618
697
|
if (status.state !== 'running') {
|
|
619
698
|
clearInterval(checkInterval);
|
|
620
699
|
clearInterval(flushInterval);
|
|
700
|
+
// Stop status footer
|
|
701
|
+
statusFooter.stop();
|
|
702
|
+
lifecycleUnsubscribe();
|
|
621
703
|
// Final flush
|
|
622
704
|
for (const sender of sendersWithOutput) {
|
|
623
705
|
const prefix = getColorForSender(sender)(`${sender.padEnd(15)} |`);
|
|
@@ -630,6 +712,8 @@ Input formats:
|
|
|
630
712
|
// Cluster may have been removed
|
|
631
713
|
clearInterval(checkInterval);
|
|
632
714
|
clearInterval(flushInterval);
|
|
715
|
+
statusFooter.stop();
|
|
716
|
+
lifecycleUnsubscribe();
|
|
633
717
|
unsubscribe();
|
|
634
718
|
resolve();
|
|
635
719
|
}
|
|
@@ -639,6 +723,10 @@ Input formats:
|
|
|
639
723
|
// CRITICAL: In foreground mode, the cluster runs IN this process.
|
|
640
724
|
// If we exit without stopping, the cluster becomes a zombie (state=running but no process).
|
|
641
725
|
process.on('SIGINT', async () => {
|
|
726
|
+
// Stop status footer first to restore terminal
|
|
727
|
+
statusFooter.stop();
|
|
728
|
+
lifecycleUnsubscribe();
|
|
729
|
+
|
|
642
730
|
console.log(chalk.dim('\n\n--- Interrupted ---'));
|
|
643
731
|
clearInterval(checkInterval);
|
|
644
732
|
clearInterval(flushInterval);
|
|
@@ -752,6 +840,14 @@ taskCmd
|
|
|
752
840
|
.option('--silent-json-output', 'Log ONLY final structured output')
|
|
753
841
|
.action(async (prompt, options) => {
|
|
754
842
|
try {
|
|
843
|
+
// === PREFLIGHT CHECKS ===
|
|
844
|
+
// Claude CLI must be installed and authenticated for task execution
|
|
845
|
+
requirePreflight({
|
|
846
|
+
requireGh: false, // gh not needed for plain tasks
|
|
847
|
+
requireDocker: false, // Docker not needed for plain tasks
|
|
848
|
+
quiet: false,
|
|
849
|
+
});
|
|
850
|
+
|
|
755
851
|
// Dynamically import task command (ESM module)
|
|
756
852
|
const { runTask } = await import('../task-lib/commands/run.js');
|
|
757
853
|
await runTask(prompt, options);
|
|
@@ -1886,6 +1982,16 @@ program
|
|
|
1886
1982
|
// Check if cluster exists
|
|
1887
1983
|
const cluster = orchestrator.getCluster(id);
|
|
1888
1984
|
|
|
1985
|
+
// === PREFLIGHT CHECKS ===
|
|
1986
|
+
// Claude CLI must be installed and authenticated
|
|
1987
|
+
// Check if cluster uses isolation (needs Docker)
|
|
1988
|
+
const requiresDocker = cluster?.isolation?.enabled || false;
|
|
1989
|
+
requirePreflight({
|
|
1990
|
+
requireGh: false, // Resume doesn't fetch new issues
|
|
1991
|
+
requireDocker: requiresDocker,
|
|
1992
|
+
quiet: false,
|
|
1993
|
+
});
|
|
1994
|
+
|
|
1889
1995
|
if (cluster) {
|
|
1890
1996
|
// Resume cluster
|
|
1891
1997
|
console.log(chalk.cyan(`Resuming cluster ${id}...`));
|
|
@@ -2928,18 +3034,6 @@ function formatToolResult(content, isError, toolName, toolInput) {
|
|
|
2928
3034
|
|
|
2929
3035
|
// Helper function to get deterministic color for an agent/sender based on name hash
|
|
2930
3036
|
// Uses djb2 hash algorithm for good distribution across color palette
|
|
2931
|
-
function getColorForSender(sender) {
|
|
2932
|
-
if (!agentColors.has(sender)) {
|
|
2933
|
-
let hash = 5381;
|
|
2934
|
-
for (let i = 0; i < sender.length; i++) {
|
|
2935
|
-
hash = (hash << 5) + hash + sender.charCodeAt(i);
|
|
2936
|
-
}
|
|
2937
|
-
const colorIndex = Math.abs(hash) % COLORS.length;
|
|
2938
|
-
agentColors.set(sender, COLORS[colorIndex]);
|
|
2939
|
-
}
|
|
2940
|
-
return agentColors.get(sender);
|
|
2941
|
-
}
|
|
2942
|
-
|
|
2943
3037
|
// Track recently seen content to avoid duplicates
|
|
2944
3038
|
const recentContentHashes = new Set();
|
|
2945
3039
|
const MAX_RECENT_HASHES = 100;
|
|
@@ -3546,166 +3640,62 @@ const FILTERED_PATTERNS = [
|
|
|
3546
3640
|
|
|
3547
3641
|
// Helper function to print a message (docker-compose style with colors)
|
|
3548
3642
|
function printMessage(msg, showClusterId = false, watchMode = false, isActive = true) {
|
|
3643
|
+
// Build prefix using utility function
|
|
3644
|
+
const prefix = buildMessagePrefix(msg, showClusterId, isActive);
|
|
3645
|
+
|
|
3549
3646
|
const timestamp = new Date(msg.timestamp).toLocaleTimeString('en-US', {
|
|
3550
3647
|
hour12: false,
|
|
3551
3648
|
});
|
|
3552
|
-
// Use dim colors for inactive clusters
|
|
3553
|
-
const color = isActive ? getColorForSender(msg.sender) : chalk.dim;
|
|
3554
3649
|
|
|
3555
|
-
//
|
|
3556
|
-
let senderLabel = msg.sender;
|
|
3557
|
-
if (showClusterId && msg.cluster_id) {
|
|
3558
|
-
senderLabel = `${msg.cluster_id}/${msg.sender}`;
|
|
3559
|
-
}
|
|
3560
|
-
const modelSuffix = msg.sender_model ? chalk.dim(` [${msg.sender_model}]`) : '';
|
|
3561
|
-
const prefix = color(`${senderLabel.padEnd(showClusterId ? 25 : 15)} |`) + modelSuffix;
|
|
3562
|
-
|
|
3563
|
-
// Watch mode: ONLY show high-level business events in human-readable format
|
|
3650
|
+
// Watch mode: delegate to watch mode formatter
|
|
3564
3651
|
if (watchMode) {
|
|
3565
|
-
|
|
3566
|
-
|
|
3567
|
-
|
|
3568
|
-
|
|
3569
|
-
|
|
3570
|
-
// Clear status line, print message, will be redrawn by status interval
|
|
3571
|
-
process.stdout.write('\r' + ' '.repeat(120) + '\r');
|
|
3572
|
-
|
|
3573
|
-
// Simplified prefix for watch mode: just cluster ID (white = alive, grey = dead)
|
|
3574
|
-
const clusterPrefix = isActive
|
|
3575
|
-
? chalk.white(`${msg.cluster_id.padEnd(20)} |`)
|
|
3576
|
-
: chalk.dim(`${msg.cluster_id.padEnd(20)} |`);
|
|
3577
|
-
|
|
3578
|
-
// AGENT_ERROR: Show errors prominently
|
|
3579
|
-
if (msg.topic === 'AGENT_ERROR') {
|
|
3580
|
-
const errorMsg = `${msg.sender} ${chalk.bold.red('ERROR')}`;
|
|
3581
|
-
console.log(`${clusterPrefix} ${errorMsg}`);
|
|
3582
|
-
if (msg.content?.text) {
|
|
3583
|
-
console.log(`${clusterPrefix} ${chalk.red(msg.content.text)}`);
|
|
3584
|
-
}
|
|
3585
|
-
return;
|
|
3586
|
-
}
|
|
3587
|
-
|
|
3588
|
-
// Human-readable event descriptions (with consistent agent colors)
|
|
3589
|
-
const agentColor = getColorForSender(msg.sender);
|
|
3590
|
-
const agentName = agentColor(msg.sender);
|
|
3591
|
-
let eventText = '';
|
|
3592
|
-
|
|
3593
|
-
switch (msg.topic) {
|
|
3594
|
-
case 'ISSUE_OPENED':
|
|
3595
|
-
const issueNum = msg.content?.data?.issue_number || '';
|
|
3596
|
-
const title = msg.content?.data?.title || '';
|
|
3597
|
-
const prompt = msg.content?.data?.prompt || msg.content?.text || '';
|
|
3598
|
-
|
|
3599
|
-
// If it's manual input, show the prompt instead of "Manual Input"
|
|
3600
|
-
const taskDesc = title === 'Manual Input' && prompt ? prompt : title;
|
|
3601
|
-
const truncatedDesc =
|
|
3602
|
-
taskDesc && taskDesc.length > 60 ? taskDesc.substring(0, 60) + '...' : taskDesc;
|
|
3603
|
-
|
|
3604
|
-
eventText = `Started ${issueNum ? `#${issueNum}` : 'task'}${truncatedDesc ? chalk.dim(` - ${truncatedDesc}`) : ''}`;
|
|
3605
|
-
break;
|
|
3606
|
-
|
|
3607
|
-
case 'IMPLEMENTATION_READY':
|
|
3608
|
-
eventText = `${agentName} completed implementation`;
|
|
3609
|
-
break;
|
|
3610
|
-
|
|
3611
|
-
case 'VALIDATION_RESULT':
|
|
3612
|
-
const data = msg.content?.data;
|
|
3613
|
-
const approved = data?.approved === 'true' || data?.approved === true;
|
|
3614
|
-
const status = approved ? chalk.green('APPROVED') : chalk.red('REJECTED');
|
|
3615
|
-
eventText = `${agentName} ${status}`;
|
|
3616
|
-
if (data?.summary && !approved) {
|
|
3617
|
-
eventText += chalk.dim(` - ${data.summary}`);
|
|
3618
|
-
}
|
|
3619
|
-
console.log(`${clusterPrefix} ${eventText}`);
|
|
3620
|
-
|
|
3621
|
-
// Show rejection details (character counts only)
|
|
3622
|
-
if (!approved) {
|
|
3623
|
-
let errors = data.errors;
|
|
3624
|
-
let issues = data.issues;
|
|
3625
|
-
|
|
3626
|
-
if (typeof errors === 'string') {
|
|
3627
|
-
try {
|
|
3628
|
-
errors = JSON.parse(errors);
|
|
3629
|
-
} catch {
|
|
3630
|
-
errors = [];
|
|
3631
|
-
}
|
|
3632
|
-
}
|
|
3633
|
-
if (typeof issues === 'string') {
|
|
3634
|
-
try {
|
|
3635
|
-
issues = JSON.parse(issues);
|
|
3636
|
-
} catch {
|
|
3637
|
-
issues = [];
|
|
3638
|
-
}
|
|
3639
|
-
}
|
|
3652
|
+
const clusterPrefix = buildClusterPrefix(msg, isActive);
|
|
3653
|
+
formatWatchMode(msg, clusterPrefix);
|
|
3654
|
+
return;
|
|
3655
|
+
}
|
|
3640
3656
|
|
|
3641
|
-
|
|
3642
|
-
|
|
3643
|
-
|
|
3657
|
+
// Normal mode: delegate to appropriate formatter based on topic
|
|
3658
|
+
if (msg.topic === 'AGENT_LIFECYCLE') {
|
|
3659
|
+
formatAgentLifecycle(msg, prefix);
|
|
3660
|
+
return;
|
|
3661
|
+
}
|
|
3644
3662
|
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
|
|
3649
|
-
);
|
|
3650
|
-
}
|
|
3663
|
+
if (msg.topic === 'AGENT_ERROR') {
|
|
3664
|
+
formatAgentErrorNormal(msg, prefix, timestamp);
|
|
3665
|
+
return;
|
|
3666
|
+
}
|
|
3651
3667
|
|
|
3652
|
-
|
|
3653
|
-
|
|
3654
|
-
|
|
3655
|
-
|
|
3656
|
-
);
|
|
3657
|
-
}
|
|
3658
|
-
}
|
|
3659
|
-
return;
|
|
3668
|
+
if (msg.topic === 'ISSUE_OPENED') {
|
|
3669
|
+
formatIssueOpenedNormal(msg, prefix, timestamp);
|
|
3670
|
+
return;
|
|
3671
|
+
}
|
|
3660
3672
|
|
|
3661
|
-
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
|
|
3673
|
+
if (msg.topic === 'IMPLEMENTATION_READY') {
|
|
3674
|
+
formatImplementationReadyNormal(msg, prefix, timestamp);
|
|
3675
|
+
return;
|
|
3676
|
+
}
|
|
3665
3677
|
|
|
3666
|
-
|
|
3667
|
-
|
|
3668
|
-
|
|
3678
|
+
if (msg.topic === 'VALIDATION_RESULT') {
|
|
3679
|
+
formatValidationResultNormal(msg, prefix, timestamp);
|
|
3680
|
+
return;
|
|
3681
|
+
}
|
|
3669
3682
|
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
|
|
3673
|
-
|
|
3683
|
+
if (msg.topic === 'PR_CREATED') {
|
|
3684
|
+
formatPrCreated(msg, prefix, timestamp);
|
|
3685
|
+
return;
|
|
3686
|
+
}
|
|
3674
3687
|
|
|
3675
|
-
|
|
3688
|
+
if (msg.topic === 'CLUSTER_COMPLETE') {
|
|
3689
|
+
formatClusterComplete(msg, prefix, timestamp);
|
|
3676
3690
|
return;
|
|
3677
3691
|
}
|
|
3678
3692
|
|
|
3679
|
-
|
|
3680
|
-
|
|
3681
|
-
const data = msg.content?.data;
|
|
3682
|
-
const event = data?.event;
|
|
3683
|
-
|
|
3684
|
-
let icon, eventText;
|
|
3685
|
-
switch (event) {
|
|
3686
|
-
case 'STARTED':
|
|
3687
|
-
icon = chalk.green('▶');
|
|
3688
|
-
const triggers = data.triggers?.join(', ') || 'none';
|
|
3689
|
-
eventText = `started (listening for: ${chalk.dim(triggers)})`;
|
|
3690
|
-
break;
|
|
3691
|
-
case 'TASK_STARTED':
|
|
3692
|
-
icon = chalk.yellow('⚡');
|
|
3693
|
-
eventText = `${chalk.cyan(data.triggeredBy)} → task #${data.iteration} (${chalk.dim(data.model)})`;
|
|
3694
|
-
break;
|
|
3695
|
-
case 'TASK_COMPLETED':
|
|
3696
|
-
icon = chalk.green('✓');
|
|
3697
|
-
eventText = `task #${data.iteration} completed`;
|
|
3698
|
-
break;
|
|
3699
|
-
default:
|
|
3700
|
-
icon = chalk.dim('•');
|
|
3701
|
-
eventText = event || 'unknown event';
|
|
3702
|
-
}
|
|
3703
|
-
|
|
3704
|
-
console.log(`${prefix} ${icon} ${eventText}`);
|
|
3693
|
+
if (msg.topic === 'CLUSTER_FAILED') {
|
|
3694
|
+
formatClusterFailed(msg, prefix, timestamp);
|
|
3705
3695
|
return;
|
|
3706
3696
|
}
|
|
3707
3697
|
|
|
3708
|
-
// AGENT_OUTPUT:
|
|
3698
|
+
// AGENT_OUTPUT: handle separately (complex streaming logic - kept in main file due to dependencies)
|
|
3709
3699
|
if (msg.topic === 'AGENT_OUTPUT') {
|
|
3710
3700
|
// Support both old 'chunk' and new 'line' formats
|
|
3711
3701
|
const content = msg.content?.data?.line || msg.content?.data?.chunk || msg.content?.text;
|
|
@@ -3917,55 +3907,8 @@ function printMessage(msg, showClusterId = false, watchMode = false, isActive =
|
|
|
3917
3907
|
return;
|
|
3918
3908
|
}
|
|
3919
3909
|
|
|
3920
|
-
//
|
|
3921
|
-
|
|
3922
|
-
console.log('');
|
|
3923
|
-
console.log(chalk.bold.green(`${'─'.repeat(60)}`));
|
|
3924
|
-
console.log(`${prefix} ${chalk.gray(timestamp)} ${chalk.bold.green('🔗 PR CREATED')}`);
|
|
3925
|
-
if (msg.content?.data?.pr_url) {
|
|
3926
|
-
console.log(`${prefix} ${chalk.cyan(msg.content.data.pr_url)}`);
|
|
3927
|
-
}
|
|
3928
|
-
if (msg.content?.data?.merged) {
|
|
3929
|
-
console.log(`${prefix} ${chalk.bold.cyan('✓ MERGED')}`);
|
|
3930
|
-
}
|
|
3931
|
-
console.log(chalk.bold.green(`${'─'.repeat(60)}`));
|
|
3932
|
-
return;
|
|
3933
|
-
}
|
|
3934
|
-
|
|
3935
|
-
// CLUSTER_COMPLETE: show completion banner
|
|
3936
|
-
if (msg.topic === 'CLUSTER_COMPLETE') {
|
|
3937
|
-
console.log('');
|
|
3938
|
-
console.log(chalk.bold.green(`${'═'.repeat(60)}`));
|
|
3939
|
-
console.log(`${prefix} ${chalk.gray(timestamp)} ${chalk.bold.green('✅ CLUSTER COMPLETED')}`);
|
|
3940
|
-
if (msg.content?.text) {
|
|
3941
|
-
console.log(`${prefix} ${chalk.white(msg.content.text)}`);
|
|
3942
|
-
}
|
|
3943
|
-
if (msg.content?.data?.reason) {
|
|
3944
|
-
console.log(`${prefix} ${chalk.dim('Reason:')} ${msg.content.data.reason}`);
|
|
3945
|
-
}
|
|
3946
|
-
console.log(chalk.bold.green(`${'═'.repeat(60)}`));
|
|
3947
|
-
console.log('');
|
|
3948
|
-
return;
|
|
3949
|
-
}
|
|
3950
|
-
|
|
3951
|
-
// Other topics: generic display
|
|
3952
|
-
const topicColor = msg.topic.startsWith('TASK_')
|
|
3953
|
-
? chalk.bold.green
|
|
3954
|
-
: msg.topic.startsWith('ERROR')
|
|
3955
|
-
? chalk.bold.red
|
|
3956
|
-
: chalk.bold;
|
|
3957
|
-
|
|
3958
|
-
console.log(`${prefix} ${chalk.gray(timestamp)} ${topicColor(msg.topic)}`);
|
|
3959
|
-
|
|
3960
|
-
// Show text content (skip template variables)
|
|
3961
|
-
if (msg.content?.text && !msg.content.text.includes('{{')) {
|
|
3962
|
-
const lines = msg.content.text.split('\n');
|
|
3963
|
-
for (const line of lines) {
|
|
3964
|
-
if (line.trim()) {
|
|
3965
|
-
console.log(`${prefix} ${line}`);
|
|
3966
|
-
}
|
|
3967
|
-
}
|
|
3968
|
-
}
|
|
3910
|
+
// Fallback: generic message display for unknown topics
|
|
3911
|
+
formatGenericMessage(msg, prefix, timestamp);
|
|
3969
3912
|
}
|
|
3970
3913
|
|
|
3971
3914
|
// Default command handling: if first arg doesn't match a known command, treat it as 'run'
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Message formatting utilities for CLI output
|
|
3
|
+
* Extracted from index.js to reduce complexity
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const chalk = require('chalk');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Get color for sender based on consistent hashing
|
|
10
|
+
* @param {string} sender - Sender name
|
|
11
|
+
* @returns {Function} Chalk color function
|
|
12
|
+
*/
|
|
13
|
+
function getColorForSender(sender) {
|
|
14
|
+
const colors = [chalk.cyan, chalk.magenta, chalk.yellow, chalk.green, chalk.blue];
|
|
15
|
+
let hash = 0;
|
|
16
|
+
for (let i = 0; i < sender.length; i++) {
|
|
17
|
+
hash = (hash << 5) - hash + sender.charCodeAt(i);
|
|
18
|
+
hash = hash & hash;
|
|
19
|
+
}
|
|
20
|
+
return colors[Math.abs(hash) % colors.length];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Build message prefix with timestamp, sender, and optional cluster ID
|
|
25
|
+
* @param {Object} msg - Message object
|
|
26
|
+
* @param {boolean} showClusterId - Whether to show cluster ID
|
|
27
|
+
* @param {boolean} isActive - Whether cluster is active
|
|
28
|
+
* @returns {string} Formatted prefix
|
|
29
|
+
*/
|
|
30
|
+
function buildMessagePrefix(msg, showClusterId, isActive) {
|
|
31
|
+
const color = isActive ? getColorForSender(msg.sender) : chalk.dim;
|
|
32
|
+
|
|
33
|
+
let senderLabel = msg.sender;
|
|
34
|
+
if (showClusterId && msg.cluster_id) {
|
|
35
|
+
senderLabel = `${msg.cluster_id}/${msg.sender}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const modelSuffix = msg.sender_model ? chalk.dim(` [${msg.sender_model}]`) : '';
|
|
39
|
+
return color(`${senderLabel.padEnd(showClusterId ? 25 : 15)} |`) + modelSuffix;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Build cluster prefix for watch mode
|
|
44
|
+
* @param {string} clusterId - Cluster ID
|
|
45
|
+
* @param {boolean} isActive - Whether cluster is active
|
|
46
|
+
* @returns {string} Formatted prefix
|
|
47
|
+
*/
|
|
48
|
+
function buildClusterPrefix(clusterId, isActive) {
|
|
49
|
+
return isActive
|
|
50
|
+
? chalk.white(`${clusterId.padEnd(20)} |`)
|
|
51
|
+
: chalk.dim(`${clusterId.padEnd(20)} |`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Parse and normalize data fields (handles string JSON)
|
|
56
|
+
* @param {string|Array} data - Data to parse
|
|
57
|
+
* @returns {Array} Parsed array
|
|
58
|
+
*/
|
|
59
|
+
function parseDataField(data) {
|
|
60
|
+
if (typeof data === 'string') {
|
|
61
|
+
try {
|
|
62
|
+
return JSON.parse(data);
|
|
63
|
+
} catch {
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return Array.isArray(data) ? data : [];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
module.exports = {
|
|
71
|
+
getColorForSender,
|
|
72
|
+
buildMessagePrefix,
|
|
73
|
+
buildClusterPrefix,
|
|
74
|
+
parseDataField,
|
|
75
|
+
};
|