@getmarrow/install 0.1.19 → 0.1.21
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/README.md +44 -89
- package/bin/marrow-install.js +1 -1
- package/package.json +1 -3
- package/src/governed-runner.js +486 -23
- package/src/installer.js +80 -333
package/src/governed-runner.js
CHANGED
|
@@ -8,6 +8,7 @@ const readline = require('node:readline');
|
|
|
8
8
|
const DEFAULT_BASE_URL = 'https://api.getmarrow.ai';
|
|
9
9
|
const HIGH_RISK_TERMS = /\b(deploy|prod|production|publish|release|merge|migration|migrate|secret|token|key|cloudflare|wrangler|npm publish|gh pr merge|git push|terraform apply|kubectl apply|delete|destroy|drop)\b/i;
|
|
10
10
|
const GOVERN_TUI_ROW_COUNT = 7;
|
|
11
|
+
const FLEET_TUI_ROW_COUNT = 11;
|
|
11
12
|
function usage() {
|
|
12
13
|
return `Usage:
|
|
13
14
|
npx @getmarrow/install run --agent deploy-agent -- npm test
|
|
@@ -17,6 +18,7 @@ function usage() {
|
|
|
17
18
|
npx @getmarrow/install status
|
|
18
19
|
npx @getmarrow/install govern
|
|
19
20
|
npx @getmarrow/install govern --no-interactive
|
|
21
|
+
npx @getmarrow/install fleet
|
|
20
22
|
|
|
21
23
|
Commands:
|
|
22
24
|
run Run a command through Marrow pre-action gate and automatic outcome closure
|
|
@@ -24,6 +26,7 @@ Commands:
|
|
|
24
26
|
proof Commit an outcome/proof for an existing decision
|
|
25
27
|
status Read /v1/agent/status
|
|
26
28
|
govern Interactive setup TUI when run in a terminal; text panel in CI/non-TTY
|
|
29
|
+
fleet Fleet operator TUI for live agents, workflows, gates, proof debt, and exact fixes
|
|
27
30
|
|
|
28
31
|
Options:
|
|
29
32
|
--agent <id> Agent identity. Defaults to MARROW_FLEET_AGENT_ID, MARROW_AGENT_ID, or local user
|
|
@@ -40,7 +43,7 @@ Options:
|
|
|
40
43
|
--key <key> Marrow API key. Prefer MARROW_API_KEY
|
|
41
44
|
--json Print machine-readable result after completion
|
|
42
45
|
--interactive Force interactive govern TUI when possible
|
|
43
|
-
--no-interactive Print govern panel instead of opening the TUI
|
|
46
|
+
--no-interactive Print govern/fleet panel instead of opening the TUI
|
|
44
47
|
`;
|
|
45
48
|
}
|
|
46
49
|
|
|
@@ -265,7 +268,7 @@ function parseArgs(argv) {
|
|
|
265
268
|
return { command, options };
|
|
266
269
|
}
|
|
267
270
|
|
|
268
|
-
if (command === 'status' || command === 'govern') {
|
|
271
|
+
if (command === 'status' || command === 'govern' || command === 'fleet') {
|
|
269
272
|
const parsed = parseBaseOptions(argv, 1);
|
|
270
273
|
if (parsed.options.help) return { command: 'help' };
|
|
271
274
|
return { command, options: parsed.options };
|
|
@@ -568,29 +571,282 @@ async function statusOnly(parsed) {
|
|
|
568
571
|
return requestJson(parsed.options, 'GET', '/v1/agent/status');
|
|
569
572
|
}
|
|
570
573
|
|
|
571
|
-
function
|
|
572
|
-
|
|
574
|
+
async function optionalRequestJson(options, method, route, body) {
|
|
575
|
+
try {
|
|
576
|
+
return { ok: true, data: await requestJson(options, method, route, body) };
|
|
577
|
+
} catch (error) {
|
|
578
|
+
return {
|
|
579
|
+
ok: false,
|
|
580
|
+
error: error instanceof Error ? error.message : String(error),
|
|
581
|
+
status: error?.status || 0,
|
|
582
|
+
details: error?.details || null,
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
function firstDefined(...values) {
|
|
588
|
+
for (const value of values) {
|
|
589
|
+
if (value !== undefined && value !== null && value !== '') return value;
|
|
590
|
+
}
|
|
591
|
+
return undefined;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
function numberValue(value, fallback = null) {
|
|
595
|
+
if (typeof value === 'number' && Number.isFinite(value)) return value;
|
|
596
|
+
if (typeof value === 'string' && value.trim() !== '' && Number.isFinite(Number(value))) return Number(value);
|
|
597
|
+
return fallback;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
function listValue(...values) {
|
|
601
|
+
for (const value of values) {
|
|
602
|
+
if (Array.isArray(value)) return value;
|
|
603
|
+
}
|
|
604
|
+
return [];
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
function statusLabel(value, fallback = 'unknown') {
|
|
608
|
+
if (typeof value === 'string' && value.trim()) return displayText(value, 80);
|
|
609
|
+
if (typeof value === 'boolean') return value ? 'ok' : 'warn';
|
|
610
|
+
return fallback;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
function normalizeRecentDecisions(status, report, fleet) {
|
|
614
|
+
return listValue(
|
|
615
|
+
fleet.recent_decisions,
|
|
616
|
+
fleet.decisions,
|
|
617
|
+
report.recent_decisions,
|
|
618
|
+
report.decisions,
|
|
619
|
+
status.recent_decisions,
|
|
620
|
+
status.last_decisions,
|
|
621
|
+
).slice(0, 5).map((decision) => {
|
|
622
|
+
if (typeof decision === 'string') return displayText(decision, 120);
|
|
623
|
+
return displayText(
|
|
624
|
+
firstDefined(
|
|
625
|
+
decision?.summary,
|
|
626
|
+
decision?.action,
|
|
627
|
+
decision?.type,
|
|
628
|
+
decision?.id,
|
|
629
|
+
decision?.decision_id,
|
|
630
|
+
JSON.stringify(decision || {}),
|
|
631
|
+
),
|
|
632
|
+
120,
|
|
633
|
+
);
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
function normalizeAgents(report, fleet, status) {
|
|
638
|
+
return listValue(
|
|
639
|
+
fleet.agents,
|
|
640
|
+
fleet.live_agents,
|
|
641
|
+
fleet.active_agents,
|
|
642
|
+
report.agents,
|
|
643
|
+
report.active_agents,
|
|
644
|
+
status.agents,
|
|
645
|
+
).slice(0, 20).map((agent) => {
|
|
646
|
+
if (typeof agent === 'string') return { id: displayText(agent, 80), status: 'active', role: '' };
|
|
647
|
+
return {
|
|
648
|
+
id: displayText(firstDefined(agent?.id, agent?.agent_id, agent?.name, agent?.key, 'agent'), 80),
|
|
649
|
+
status: displayText(firstDefined(agent?.status, agent?.health, agent?.state, 'active'), 40),
|
|
650
|
+
role: displayText(firstDefined(agent?.role, agent?.type, ''), 60),
|
|
651
|
+
last_seen_at: displayText(firstDefined(agent?.last_seen_at, agent?.last_event_at, agent?.updated_at, ''), 80),
|
|
652
|
+
};
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
function normalizeGates(status, report) {
|
|
657
|
+
const gateSource = status.gates || report.gates || {};
|
|
658
|
+
const gateValue = (name) => {
|
|
659
|
+
const value = firstDefined(
|
|
660
|
+
gateSource[name],
|
|
661
|
+
status[`${name}_gate`],
|
|
662
|
+
report[`${name}_gate`],
|
|
663
|
+
status[`${name}_status`],
|
|
664
|
+
report[`${name}_status`],
|
|
665
|
+
);
|
|
666
|
+
if (value && typeof value === 'object') {
|
|
667
|
+
return statusLabel(firstDefined(value.status, value.decision, value.enforcement_decision, value.state), 'unknown');
|
|
668
|
+
}
|
|
669
|
+
return statusLabel(value, 'unknown');
|
|
670
|
+
};
|
|
671
|
+
return {
|
|
672
|
+
deploy: gateValue('deploy'),
|
|
673
|
+
publish: gateValue('publish'),
|
|
674
|
+
merge: gateValue('merge'),
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
function normalizeFixCommands(status, capacity) {
|
|
679
|
+
const commands = [];
|
|
680
|
+
const add = (value, commandOnly = false) => {
|
|
681
|
+
const text = displayText(value || '', 240);
|
|
682
|
+
if (commandOnly && !/^(?:npx|npm|pnpm|yarn|node|export|MARROW_|curl|bash|sh)\b/.test(text)) return;
|
|
683
|
+
if (text && !commands.includes(text)) commands.push(text);
|
|
684
|
+
};
|
|
685
|
+
add(status.exact_fix);
|
|
686
|
+
add(status.recommended_fix);
|
|
687
|
+
add(status.next_action);
|
|
688
|
+
add(status.route_contract?.exact_fix);
|
|
689
|
+
add(status.auto_outcome_closure?.exact_fix);
|
|
690
|
+
add(status.capture_coverage?.exact_fix);
|
|
691
|
+
add(capacity.exact_next_action, true);
|
|
692
|
+
add(capacity.next_action, true);
|
|
693
|
+
if (listValue(status.missed_hooks, status.degraded_hooks).length) add('npx @getmarrow/install --repair');
|
|
694
|
+
if (status.auto_outcome_closure?.status === 'degraded') add('npx @getmarrow/install --repair');
|
|
695
|
+
return commands.slice(0, 6);
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
function normalizeFleetSnapshot(raw, options) {
|
|
699
|
+
const status = raw.status?.data || {};
|
|
700
|
+
const capacity = raw.capacity?.data || {};
|
|
701
|
+
const report = raw.report?.data || {};
|
|
702
|
+
const fleet = raw.fleet?.data || {};
|
|
703
|
+
const agents = normalizeAgents(report, fleet, status);
|
|
704
|
+
const liveAgents = numberValue(firstDefined(
|
|
705
|
+
fleet.live_agent_count,
|
|
706
|
+
fleet.active_agent_count,
|
|
707
|
+
fleet.fleet?.active_agents,
|
|
708
|
+
fleet.fleet?.total_agents,
|
|
709
|
+
report.live_agent_count,
|
|
710
|
+
report.active_agent_count,
|
|
711
|
+
report.fleet?.active_agents,
|
|
712
|
+
report.fleet?.total_agents,
|
|
713
|
+
status.live_agent_count,
|
|
714
|
+
status.active_agent_count,
|
|
715
|
+
agents.length || undefined,
|
|
716
|
+
), agents.length || null);
|
|
717
|
+
const activeWorkflows = numberValue(firstDefined(
|
|
718
|
+
Array.isArray(fleet.active_workflows) ? fleet.active_workflows.length : undefined,
|
|
719
|
+
fleet.active_workflows,
|
|
720
|
+
fleet.active_workflow_count,
|
|
721
|
+
Array.isArray(report.active_workflows) ? report.active_workflows.length : undefined,
|
|
722
|
+
report.active_workflows,
|
|
723
|
+
report.active_workflow_count,
|
|
724
|
+
status.active_workflows,
|
|
725
|
+
status.active_workflow_count,
|
|
726
|
+
status.workflow_sessions?.active,
|
|
727
|
+
), null);
|
|
728
|
+
const proofWaiting = numberValue(firstDefined(
|
|
729
|
+
status.proof_packs?.waiting,
|
|
730
|
+
status.proof_packs?.incomplete,
|
|
731
|
+
status.proof_pack_hygiene?.incomplete,
|
|
732
|
+
status.proof_pack_incomplete,
|
|
733
|
+
report.proof_packs?.waiting,
|
|
734
|
+
fleet.proof_packs?.waiting,
|
|
735
|
+
), 0);
|
|
736
|
+
const failedStaleOutcomes = numberValue(firstDefined(
|
|
737
|
+
status.failed_stale_outcomes,
|
|
738
|
+
status.stale_outcomes,
|
|
739
|
+
status.auto_outcome_closure?.stale,
|
|
740
|
+
status.outcome_hygiene?.stale,
|
|
741
|
+
report.failed_outcomes,
|
|
742
|
+
fleet.failed_outcomes,
|
|
743
|
+
), 0);
|
|
744
|
+
const missedHooks = listValue(status.missed_hooks, status.degraded_hooks, status.capture_coverage?.missed_hooks)
|
|
745
|
+
.map((hook) => displayText(typeof hook === 'string' ? hook : firstDefined(hook?.name, hook?.hook, JSON.stringify(hook)), 80))
|
|
746
|
+
.slice(0, 8);
|
|
747
|
+
const backpressure = capacity.current_backpressure || capacity.backpressure || capacity.scale_slo?.backpressure || {};
|
|
748
|
+
const backpressureStatus = statusLabel(firstDefined(
|
|
749
|
+
backpressure.status,
|
|
750
|
+
backpressure.level,
|
|
751
|
+
capacity.status,
|
|
752
|
+
capacity.scale_status,
|
|
753
|
+
capacity.agent_capacity?.status,
|
|
754
|
+
), raw.capacity?.ok ? 'ok' : 'unknown');
|
|
755
|
+
const recentDecisions = normalizeRecentDecisions(status, report, fleet);
|
|
756
|
+
const gates = normalizeGates(status, report);
|
|
757
|
+
const fixCommands = normalizeFixCommands(status, capacity);
|
|
758
|
+
const errors = Object.entries(raw)
|
|
759
|
+
.filter(([, value]) => value && value.ok === false)
|
|
760
|
+
.map(([name, value]) => `${name}: ${displayText(value.error, 120)}`);
|
|
761
|
+
|
|
762
|
+
return {
|
|
763
|
+
ok: raw.status?.ok !== false,
|
|
764
|
+
generated_at: new Date().toISOString(),
|
|
765
|
+
agent_id: displayText(options.agentId, 80),
|
|
766
|
+
base_url: options.baseUrl,
|
|
767
|
+
live_agents: liveAgents,
|
|
768
|
+
active_workflows: activeWorkflows,
|
|
769
|
+
proof_waiting: proofWaiting,
|
|
770
|
+
failed_stale_outcomes: failedStaleOutcomes,
|
|
771
|
+
backpressure_status: backpressureStatus,
|
|
772
|
+
capacity_next_action: displayText(firstDefined(capacity.exact_next_action, capacity.next_action, capacity.failure_mode?.exact_next_action, ''), 180),
|
|
773
|
+
recent_decisions: recentDecisions,
|
|
774
|
+
degraded_hooks: missedHooks,
|
|
775
|
+
gates,
|
|
776
|
+
agents,
|
|
777
|
+
fix_commands: fixCommands.length ? fixCommands : ['npx @getmarrow/install doctor'],
|
|
778
|
+
source_errors: errors,
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
async function fleetSnapshot(options) {
|
|
783
|
+
if (!options.apiKey) {
|
|
784
|
+
return normalizeFleetSnapshot({
|
|
785
|
+
status: {
|
|
786
|
+
ok: false,
|
|
787
|
+
error: 'MARROW_API_KEY missing',
|
|
788
|
+
data: {
|
|
789
|
+
enabled: false,
|
|
790
|
+
missed_hooks: ['api_key'],
|
|
791
|
+
recommended_fix: 'export MARROW_API_KEY=mrw_live_... && npx @getmarrow/install fleet',
|
|
792
|
+
},
|
|
793
|
+
},
|
|
794
|
+
capacity: { ok: false, error: 'MARROW_API_KEY missing', data: {} },
|
|
795
|
+
report: { ok: false, error: 'MARROW_API_KEY missing', data: {} },
|
|
796
|
+
fleet: { ok: false, error: 'MARROW_API_KEY missing', data: {} },
|
|
797
|
+
}, options);
|
|
798
|
+
}
|
|
799
|
+
const [status, capacity, report, fleet] = await Promise.all([
|
|
800
|
+
optionalRequestJson(options, 'GET', '/v1/agent/status?fast=1'),
|
|
801
|
+
optionalRequestJson(options, 'GET', '/v1/agent/scale/capacity-contract'),
|
|
802
|
+
optionalRequestJson(options, 'GET', '/v1/agent/report'),
|
|
803
|
+
optionalRequestJson(options, 'GET', '/v1/fleet'),
|
|
804
|
+
]);
|
|
805
|
+
return normalizeFleetSnapshot({ status, capacity, report, fleet }, options);
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
function fleetPanel(snapshot) {
|
|
809
|
+
const degraded = snapshot.degraded_hooks.length ? snapshot.degraded_hooks.join(', ') : 'none';
|
|
810
|
+
const recent = snapshot.recent_decisions.length ? snapshot.recent_decisions.map((item) => ` - ${item}`).join('\n') : ' - none reported yet';
|
|
811
|
+
const agents = snapshot.agents.length
|
|
812
|
+
? snapshot.agents.slice(0, 5).map((agent) => ` - ${agent.id}${agent.role ? ` (${agent.role})` : ''}: ${agent.status}${agent.last_seen_at ? ` last_seen=${agent.last_seen_at}` : ''}`).join('\n')
|
|
813
|
+
: ' - no agent roster returned yet';
|
|
814
|
+
const fixes = snapshot.fix_commands.map((command) => ` - ${command}`).join('\n');
|
|
815
|
+
return [
|
|
816
|
+
'Marrow Fleet Operator',
|
|
817
|
+
'',
|
|
818
|
+
`Agent: ${snapshot.agent_id}`,
|
|
819
|
+
`Snapshot: ${snapshot.generated_at}`,
|
|
820
|
+
'',
|
|
821
|
+
`Live agents: ${snapshot.live_agents ?? 'unknown'}`,
|
|
822
|
+
`Active workflows: ${snapshot.active_workflows ?? 'unknown'}`,
|
|
823
|
+
`Risky actions waiting for proof: ${snapshot.proof_waiting}`,
|
|
824
|
+
`Failed/stale outcomes: ${snapshot.failed_stale_outcomes}`,
|
|
825
|
+
`Backpressure/capacity status: ${snapshot.backpressure_status}${snapshot.capacity_next_action ? ` - ${snapshot.capacity_next_action}` : ''}`,
|
|
826
|
+
`Degraded hooks: ${degraded}`,
|
|
827
|
+
`Deploy/publish/merge gates: deploy=${snapshot.gates.deploy} publish=${snapshot.gates.publish} merge=${snapshot.gates.merge}`,
|
|
828
|
+
'',
|
|
829
|
+
'Live agent roster:',
|
|
830
|
+
agents,
|
|
831
|
+
'',
|
|
832
|
+
'Recent decisions:',
|
|
833
|
+
recent,
|
|
834
|
+
'',
|
|
835
|
+
'Press Enter to inspect agent when run in an interactive terminal.',
|
|
836
|
+
'Copy exact fix command:',
|
|
837
|
+
fixes,
|
|
838
|
+
snapshot.source_errors.length ? ['', 'Partial data:', ...snapshot.source_errors.map((error) => ` - ${error}`)].join('\n') : '',
|
|
839
|
+
].filter(Boolean).join('\n');
|
|
573
840
|
}
|
|
574
841
|
|
|
575
842
|
function detectHarnesses(cwd = process.cwd()) {
|
|
576
|
-
const home = os.homedir();
|
|
577
843
|
const candidates = [
|
|
578
|
-
{ name: 'Codex', command: 'codex', detected:
|
|
579
|
-
{ name: 'Claude Code', command: 'claude -p', detected:
|
|
580
|
-
{ name: 'Cursor', command: 'cursor', detected:
|
|
581
|
-
{ name: '
|
|
582
|
-
{ name: '
|
|
583
|
-
{ name: '
|
|
584
|
-
{ name: 'Minimax CLI', command: 'minimax', detected: existsAny([path.join(cwd, 'MINIMAX.md'), path.join(cwd, '.minimax'), path.join(home, '.minimax')]) },
|
|
585
|
-
{ name: 'Kimi CLI', command: 'kimi', detected: existsAny([path.join(cwd, 'KIMI.md'), path.join(cwd, 'MOONSHOT.md'), path.join(cwd, '.kimi'), path.join(cwd, '.moonshot'), path.join(home, '.kimi'), path.join(home, '.moonshot')]) },
|
|
586
|
-
{ name: 'Hermes', command: 'hermes', detected: existsAny([path.join(cwd, 'HERMES.md'), path.join(cwd, 'hermes.json'), path.join(cwd, '.hermes'), path.join(home, '.hermes')]) },
|
|
587
|
-
{ name: 'GLM CLI', command: 'glm', detected: existsAny([path.join(cwd, 'GLM.md'), path.join(cwd, '.glm'), path.join(home, '.glm')]) },
|
|
588
|
-
{ name: 'Qwen CLI', command: 'qwen', detected: existsAny([path.join(cwd, 'QWEN.md'), path.join(cwd, '.qwen'), path.join(home, '.qwen')]) },
|
|
589
|
-
{ name: 'Cline', command: 'cline', detected: existsAny([path.join(cwd, '.cline'), path.join(cwd, 'CLINE.md'), path.join(home, '.cline')]) },
|
|
590
|
-
{ name: 'OpenCode', command: 'opencode', detected: existsAny([path.join(cwd, 'opencode.json'), path.join(cwd, '.opencode'), path.join(home, '.opencode')]) },
|
|
591
|
-
{ name: 'OpenClaw', command: 'openclaw agent', detected: existsAny([path.join(cwd, 'openclaw.json'), path.join(home, '.openclaw')]) },
|
|
592
|
-
{ name: 'MCP-compatible client', command: '<mcp-client>', detected: existsAny([path.join(cwd, '.mcp.json'), path.join(cwd, '.cursor', 'mcp.json'), path.join(cwd, '.claude', 'settings.json')]) },
|
|
593
|
-
{ name: 'CI scripts', command: 'npm test', detected: existsAny([path.join(cwd, '.github', 'workflows'), path.join(cwd, '.gitlab-ci.yml'), path.join(cwd, 'package.json')]) },
|
|
844
|
+
{ name: 'Codex', command: 'codex', detected: fs.existsSync(path.join(cwd, 'AGENTS.md')) || fs.existsSync(path.join(os.homedir(), '.codex')) },
|
|
845
|
+
{ name: 'Claude Code', command: 'claude -p', detected: fs.existsSync(path.join(cwd, 'CLAUDE.md')) || fs.existsSync(path.join(os.homedir(), '.claude.json')) },
|
|
846
|
+
{ name: 'Cursor', command: 'cursor', detected: fs.existsSync(path.join(cwd, '.cursor')) || fs.existsSync(path.join(os.homedir(), '.cursor')) },
|
|
847
|
+
{ name: 'OpenCode', command: 'opencode', detected: fs.existsSync(path.join(cwd, 'opencode.json')) || fs.existsSync(path.join(os.homedir(), '.opencode')) },
|
|
848
|
+
{ name: 'OpenClaw', command: 'openclaw agent', detected: fs.existsSync(path.join(os.homedir(), '.openclaw')) },
|
|
849
|
+
{ name: 'CI script', command: 'npm test', detected: fs.existsSync(path.join(cwd, 'package.json')) },
|
|
594
850
|
{ name: 'Custom command', command: '<your-agent-command>', detected: true },
|
|
595
851
|
];
|
|
596
852
|
return candidates;
|
|
@@ -791,6 +1047,112 @@ function renderGovernTui(state, options) {
|
|
|
791
1047
|
return lines.join('\n');
|
|
792
1048
|
}
|
|
793
1049
|
|
|
1050
|
+
function buildFleetState(snapshot) {
|
|
1051
|
+
return {
|
|
1052
|
+
cursor: 0,
|
|
1053
|
+
agentIndex: 0,
|
|
1054
|
+
snapshot,
|
|
1055
|
+
status: '',
|
|
1056
|
+
lastResult: '',
|
|
1057
|
+
};
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
function renderFleetTui(state) {
|
|
1061
|
+
const snapshot = state.snapshot;
|
|
1062
|
+
const selectedAgent = snapshot.agents[state.agentIndex] || { id: 'no agent returned', status: 'unknown', role: '' };
|
|
1063
|
+
const degraded = snapshot.degraded_hooks.length ? snapshot.degraded_hooks.join(', ') : 'none';
|
|
1064
|
+
const gateSummary = `deploy=${snapshot.gates.deploy} publish=${snapshot.gates.publish} merge=${snapshot.gates.merge}`;
|
|
1065
|
+
const fixCommand = snapshot.fix_commands[0] || 'npx @getmarrow/install doctor';
|
|
1066
|
+
const recent = snapshot.recent_decisions[0] || 'none reported yet';
|
|
1067
|
+
const rows = [
|
|
1068
|
+
{
|
|
1069
|
+
label: 'Live agents',
|
|
1070
|
+
value: `${snapshot.live_agents ?? 'unknown'} total; selected ${selectedAgent.id}`,
|
|
1071
|
+
hint: 'Left/right changes the selected agent. Enter shows agent detail.',
|
|
1072
|
+
},
|
|
1073
|
+
{
|
|
1074
|
+
label: 'Active workflows',
|
|
1075
|
+
value: String(snapshot.active_workflows ?? 'unknown'),
|
|
1076
|
+
hint: 'Current account-scoped workflow pressure from Marrow.',
|
|
1077
|
+
},
|
|
1078
|
+
{
|
|
1079
|
+
label: 'Risky actions waiting for proof',
|
|
1080
|
+
value: String(snapshot.proof_waiting),
|
|
1081
|
+
hint: 'Deploy, publish, merge, migration, or sensitive actions waiting on proof packs.',
|
|
1082
|
+
},
|
|
1083
|
+
{
|
|
1084
|
+
label: 'Failed/stale outcomes',
|
|
1085
|
+
value: String(snapshot.failed_stale_outcomes),
|
|
1086
|
+
hint: 'Outcome closure debt that can weaken fleet learning.',
|
|
1087
|
+
},
|
|
1088
|
+
{
|
|
1089
|
+
label: 'Backpressure / capacity',
|
|
1090
|
+
value: snapshot.backpressure_status,
|
|
1091
|
+
hint: snapshot.capacity_next_action || 'Capacity contract loaded when available.',
|
|
1092
|
+
},
|
|
1093
|
+
{
|
|
1094
|
+
label: 'Recent decisions',
|
|
1095
|
+
value: recent,
|
|
1096
|
+
hint: 'Latest fleet decision signal returned by Marrow.',
|
|
1097
|
+
},
|
|
1098
|
+
{
|
|
1099
|
+
label: 'Degraded hooks',
|
|
1100
|
+
value: degraded,
|
|
1101
|
+
hint: degraded === 'none' ? 'No missing hooks reported.' : 'Repair hooks before trusting passive coverage.',
|
|
1102
|
+
},
|
|
1103
|
+
{
|
|
1104
|
+
label: 'Deploy/publish/merge gates',
|
|
1105
|
+
value: gateSummary,
|
|
1106
|
+
hint: 'High-risk release surfaces should remain gated.',
|
|
1107
|
+
},
|
|
1108
|
+
{
|
|
1109
|
+
label: 'Inspect agent',
|
|
1110
|
+
value: selectedAgent.id,
|
|
1111
|
+
hint: 'Press Enter to inspect agent.',
|
|
1112
|
+
},
|
|
1113
|
+
{
|
|
1114
|
+
label: 'Copy exact fix command',
|
|
1115
|
+
value: fixCommand,
|
|
1116
|
+
hint: 'Press Enter to print this command back to the shell.',
|
|
1117
|
+
},
|
|
1118
|
+
{
|
|
1119
|
+
label: 'Exit',
|
|
1120
|
+
value: 'Return to shell',
|
|
1121
|
+
hint: 'Press Enter, q, Esc, or Ctrl+C.',
|
|
1122
|
+
},
|
|
1123
|
+
];
|
|
1124
|
+
const lines = [
|
|
1125
|
+
'\x1b[2J\x1b[H',
|
|
1126
|
+
'+------------------------------------------------------------+',
|
|
1127
|
+
'| Marrow Fleet Operator |',
|
|
1128
|
+
'| Live fleet health, proof debt, gates, and exact fixes |',
|
|
1129
|
+
'+------------------------------------------------------------+',
|
|
1130
|
+
'',
|
|
1131
|
+
`Agent: ${displayText(snapshot.agent_id, 36)} Snapshot: ${displayText(snapshot.generated_at, 36)}`,
|
|
1132
|
+
`API: ${displayText(snapshot.base_url, 60)}`,
|
|
1133
|
+
'',
|
|
1134
|
+
'Navigation: Up/Down move Left/Right select agent Enter inspect/print',
|
|
1135
|
+
'Exit: q, Esc, or Ctrl+C',
|
|
1136
|
+
'',
|
|
1137
|
+
];
|
|
1138
|
+
rows.forEach((row, index) => {
|
|
1139
|
+
lines.push(...renderOptionBox(row, index === state.cursor), '');
|
|
1140
|
+
});
|
|
1141
|
+
if (state.status) {
|
|
1142
|
+
lines.push(`Status: ${displayText(state.status, 120)}`);
|
|
1143
|
+
}
|
|
1144
|
+
if (state.lastResult) {
|
|
1145
|
+
lines.push('');
|
|
1146
|
+
lines.push(displayText(state.lastResult, 700));
|
|
1147
|
+
}
|
|
1148
|
+
if (snapshot.source_errors.length) {
|
|
1149
|
+
lines.push('');
|
|
1150
|
+
lines.push('Partial data:');
|
|
1151
|
+
for (const error of snapshot.source_errors.slice(0, 4)) lines.push(`- ${displayText(error, 120)}`);
|
|
1152
|
+
}
|
|
1153
|
+
return lines.join('\n');
|
|
1154
|
+
}
|
|
1155
|
+
|
|
794
1156
|
function canUseInteractive(options, input = process.stdin, output = process.stdout) {
|
|
795
1157
|
if (options.interactive === false) return false;
|
|
796
1158
|
if (options.interactive === true) return Boolean(input.isTTY && output.isTTY);
|
|
@@ -980,6 +1342,99 @@ async function runGovernInteractive(options, input = process.stdin, output = pro
|
|
|
980
1342
|
}
|
|
981
1343
|
}
|
|
982
1344
|
|
|
1345
|
+
async function runFleetInteractive(options, input = process.stdin, output = process.stdout) {
|
|
1346
|
+
const snapshot = await fleetSnapshot(options);
|
|
1347
|
+
if (options.json) return snapshot;
|
|
1348
|
+
if (!canUseInteractive(options, input, output)) {
|
|
1349
|
+
output.write(`${fleetPanel(snapshot)}\n`);
|
|
1350
|
+
return snapshot;
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
const state = buildFleetState(snapshot);
|
|
1354
|
+
readline.emitKeypressEvents(input);
|
|
1355
|
+
input.setRawMode(true);
|
|
1356
|
+
output.write('\x1b[?25l');
|
|
1357
|
+
|
|
1358
|
+
let cleaned = false;
|
|
1359
|
+
const cleanup = () => {
|
|
1360
|
+
if (cleaned) return;
|
|
1361
|
+
cleaned = true;
|
|
1362
|
+
if (input.setRawMode) input.setRawMode(false);
|
|
1363
|
+
output.write('\x1b[?25h');
|
|
1364
|
+
};
|
|
1365
|
+
const render = () => output.write(renderFleetTui(state));
|
|
1366
|
+
|
|
1367
|
+
render();
|
|
1368
|
+
let keyHandler;
|
|
1369
|
+
try {
|
|
1370
|
+
await new Promise((resolve) => {
|
|
1371
|
+
keyHandler = (str, key = {}) => {
|
|
1372
|
+
if (key.ctrl && key.name === 'c') {
|
|
1373
|
+
cleanup();
|
|
1374
|
+
resolve();
|
|
1375
|
+
return;
|
|
1376
|
+
}
|
|
1377
|
+
if (key.name === 'q' || key.name === 'escape' || str === 'q') {
|
|
1378
|
+
cleanup();
|
|
1379
|
+
resolve();
|
|
1380
|
+
return;
|
|
1381
|
+
}
|
|
1382
|
+
if (key.name === 'up') {
|
|
1383
|
+
state.cursor = (state.cursor + FLEET_TUI_ROW_COUNT - 1) % FLEET_TUI_ROW_COUNT;
|
|
1384
|
+
render();
|
|
1385
|
+
} else if (key.name === 'down') {
|
|
1386
|
+
state.cursor = (state.cursor + 1) % FLEET_TUI_ROW_COUNT;
|
|
1387
|
+
render();
|
|
1388
|
+
} else if ((key.name === 'left' || key.name === 'right') && state.snapshot.agents.length) {
|
|
1389
|
+
const direction = key.name === 'right' ? 1 : -1;
|
|
1390
|
+
state.agentIndex = (state.agentIndex + direction + state.snapshot.agents.length) % state.snapshot.agents.length;
|
|
1391
|
+
state.status = 'Agent selection changed.';
|
|
1392
|
+
render();
|
|
1393
|
+
} else if (key.name === 'return') {
|
|
1394
|
+
if (state.cursor === 9) {
|
|
1395
|
+
cleanup();
|
|
1396
|
+
output.write(`\n${state.snapshot.fix_commands[0] || 'npx @getmarrow/install doctor'}\n`);
|
|
1397
|
+
resolve();
|
|
1398
|
+
return;
|
|
1399
|
+
}
|
|
1400
|
+
if (state.cursor === 10) {
|
|
1401
|
+
cleanup();
|
|
1402
|
+
resolve();
|
|
1403
|
+
return;
|
|
1404
|
+
}
|
|
1405
|
+
const selectedAgent = state.snapshot.agents[state.agentIndex];
|
|
1406
|
+
if (state.cursor === 0 || state.cursor === 8) {
|
|
1407
|
+
state.lastResult = selectedAgent
|
|
1408
|
+
? `Agent ${selectedAgent.id}: status=${selectedAgent.status}${selectedAgent.role ? ` role=${selectedAgent.role}` : ''}${selectedAgent.last_seen_at ? ` last_seen=${selectedAgent.last_seen_at}` : ''}`
|
|
1409
|
+
: 'No live agent roster returned yet. Check API key scope or wait for agents to log activity.';
|
|
1410
|
+
} else if (state.cursor === 5) {
|
|
1411
|
+
state.lastResult = state.snapshot.recent_decisions.length
|
|
1412
|
+
? state.snapshot.recent_decisions.map((decision, index) => `${index + 1}. ${decision}`).join(' ')
|
|
1413
|
+
: 'No recent decisions returned yet.';
|
|
1414
|
+
} else if (state.cursor === 6) {
|
|
1415
|
+
state.lastResult = state.snapshot.degraded_hooks.length
|
|
1416
|
+
? `Degraded hooks: ${state.snapshot.degraded_hooks.join(', ')}. Fix: ${state.snapshot.fix_commands[0]}`
|
|
1417
|
+
: 'No degraded hooks reported.';
|
|
1418
|
+
} else if (state.cursor === 7) {
|
|
1419
|
+
state.lastResult = `Release gates: deploy=${state.snapshot.gates.deploy}, publish=${state.snapshot.gates.publish}, merge=${state.snapshot.gates.merge}.`;
|
|
1420
|
+
} else {
|
|
1421
|
+
state.lastResult = fleetPanel(state.snapshot).replace(/\n/g, ' ');
|
|
1422
|
+
}
|
|
1423
|
+
state.status = 'Inspection updated.';
|
|
1424
|
+
render();
|
|
1425
|
+
}
|
|
1426
|
+
};
|
|
1427
|
+
input.on('keypress', keyHandler);
|
|
1428
|
+
});
|
|
1429
|
+
} finally {
|
|
1430
|
+
if (keyHandler) input.off('keypress', keyHandler);
|
|
1431
|
+
if (input.pause) input.pause();
|
|
1432
|
+
cleanup();
|
|
1433
|
+
output.write('\n');
|
|
1434
|
+
}
|
|
1435
|
+
return snapshot;
|
|
1436
|
+
}
|
|
1437
|
+
|
|
983
1438
|
async function runCli(argv) {
|
|
984
1439
|
const parsed = parseArgs(argv);
|
|
985
1440
|
if (parsed.command === 'help') {
|
|
@@ -999,11 +1454,13 @@ async function runCli(argv) {
|
|
|
999
1454
|
else if (parsed.command === 'govern') {
|
|
1000
1455
|
await runGovernInteractive(parsed.options);
|
|
1001
1456
|
return;
|
|
1457
|
+
} else if (parsed.command === 'fleet') {
|
|
1458
|
+
result = await runFleetInteractive(parsed.options);
|
|
1002
1459
|
}
|
|
1003
1460
|
|
|
1004
1461
|
if (parsed.options?.json) process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
1005
1462
|
else if (result?.blocked) process.stderr.write(`BLOCKED: ${result.message || 'Marrow blocked this action.'}\n`);
|
|
1006
|
-
else if (parsed.command !== 'run') process.stdout.write('Marrow command completed.\n');
|
|
1463
|
+
else if (parsed.command !== 'run' && parsed.command !== 'fleet') process.stdout.write('Marrow command completed.\n');
|
|
1007
1464
|
|
|
1008
1465
|
if (parsed.command === 'run' || result?.blocked) process.exitCode = result?.exitCode ?? (result?.ok === false ? 1 : 0);
|
|
1009
1466
|
}
|
|
@@ -1016,7 +1473,6 @@ module.exports = {
|
|
|
1016
1473
|
inferSurfaces,
|
|
1017
1474
|
commandForSelection,
|
|
1018
1475
|
buildGovernState,
|
|
1019
|
-
detectHarnesses,
|
|
1020
1476
|
detectProjectSignals,
|
|
1021
1477
|
recommendGovernanceMode,
|
|
1022
1478
|
recordGovernanceModeSelection,
|
|
@@ -1032,5 +1488,12 @@ module.exports = {
|
|
|
1032
1488
|
runStatusCheck,
|
|
1033
1489
|
runGateCheck,
|
|
1034
1490
|
runGovernInteractive,
|
|
1491
|
+
optionalRequestJson,
|
|
1492
|
+
normalizeFleetSnapshot,
|
|
1493
|
+
fleetSnapshot,
|
|
1494
|
+
fleetPanel,
|
|
1495
|
+
buildFleetState,
|
|
1496
|
+
renderFleetTui,
|
|
1497
|
+
runFleetInteractive,
|
|
1035
1498
|
runCli,
|
|
1036
1499
|
};
|