@getmarrow/install 0.1.19 → 0.1.20
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 +51 -37
- package/bin/marrow-install.js +1 -1
- package/package.json +1 -1
- package/src/governed-runner.js +486 -23
- package/src/installer.js +18 -312
package/README.md
CHANGED
|
@@ -9,35 +9,32 @@ npx @getmarrow/install --dry-run
|
|
|
9
9
|
npx @getmarrow/install --yes
|
|
10
10
|
npx @getmarrow/install --repair
|
|
11
11
|
npx @getmarrow/install doctor
|
|
12
|
+
npx @getmarrow/install fleet
|
|
12
13
|
```
|
|
13
14
|
|
|
14
|
-
## What's New in v0.1.
|
|
15
|
+
## What's New in v0.1.20
|
|
15
16
|
|
|
16
|
-
v0.1.
|
|
17
|
+
v0.1.20 adds the Fleet Operator TUI for teams running multiple agents under one Marrow account.
|
|
17
18
|
|
|
18
|
-
- `npx @getmarrow/install
|
|
19
|
-
-
|
|
20
|
-
-
|
|
21
|
-
-
|
|
22
|
-
-
|
|
19
|
+
- `npx @getmarrow/install fleet` opens a terminal operator view when run in a real TTY.
|
|
20
|
+
- CI/headless agents can use `npx @getmarrow/install fleet --no-interactive` for a stable text snapshot or `--json` for machine-readable status.
|
|
21
|
+
- The view shows live agents, active workflows, risky actions waiting for proof, failed/stale outcomes, backpressure/capacity status, recent decisions, degraded hooks, and deploy/publish/merge gate posture.
|
|
22
|
+
- Operators can press Enter to inspect an agent and print the exact fix command when hooks, outcome closure, or status coverage is degraded.
|
|
23
|
+
- The command reads existing Marrow account endpoints and degrades gracefully when a route is unavailable, so it is safe to use during incidents.
|
|
23
24
|
|
|
24
|
-
|
|
25
|
+
Example:
|
|
25
26
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
- Cursor is treated as a development workflow surface. Marrow can govern Cursor-backed code review, edits, tests, deploys, and release steps through MCP setup, project rules, or the governed command wrapper.
|
|
32
|
-
- If a harness is not detected yet, use **Custom command** or `npx @getmarrow/install run -- <your-agent-command>` to govern it immediately.
|
|
33
|
-
|
|
34
|
-
Business value: teams can add Marrow to the agent stack they already use instead of migrating to a new agent host. Codex, Claude Code, Cursor, Cline, Gemini, Grok, DeepSeek, Minimax, Kimi/Moonshot, Hermes, GLM, Qwen, OpenClaw, OpenCode, MCP clients, and CI scripts can all be brought under the same governance loop.
|
|
27
|
+
```bash
|
|
28
|
+
MARROW_API_KEY=mrw_live_xxx npx @getmarrow/install fleet
|
|
29
|
+
MARROW_API_KEY=mrw_live_xxx npx @getmarrow/install fleet --no-interactive
|
|
30
|
+
MARROW_API_KEY=mrw_live_xxx npx @getmarrow/install fleet --json
|
|
31
|
+
```
|
|
35
32
|
|
|
36
33
|
## What's New in v0.1.14
|
|
37
34
|
|
|
38
35
|
v0.1.14 adds adaptive governance mode recommendations without silent auto-switching.
|
|
39
36
|
|
|
40
|
-
- `npx @getmarrow/install govern` now detects project signals such as `package.json`, deploy/publish scripts,
|
|
37
|
+
- `npx @getmarrow/install govern` now detects project signals such as `package.json`, deploy/publish scripts, `wrangler` config, GitHub workflows, migrations, Cursor/Codex/Claude files, and MCP config.
|
|
41
38
|
- When `MARROW_API_KEY` is present, the TUI asks Marrow for a recommended mode: `passive`, `pilot`, or `enforce`.
|
|
42
39
|
- The TUI shows the exact reasons, confidence, and selected command before the user applies anything.
|
|
43
40
|
- User choice is explicit. Marrow logs whether the recommendation was accepted or overridden, but it does not silently switch modes.
|
|
@@ -49,7 +46,7 @@ Example recommendation:
|
|
|
49
46
|
Recommended mode: pilot
|
|
50
47
|
Reason:
|
|
51
48
|
- Node project detected
|
|
52
|
-
-
|
|
49
|
+
- Cloudflare Worker detected
|
|
53
50
|
- GitHub workflow detected
|
|
54
51
|
- No owner approval policy configured yet
|
|
55
52
|
```
|
|
@@ -104,7 +101,7 @@ MARROW_API_KEY=mrw_live_xxx npx @getmarrow/install run --agent codex-prod --prof
|
|
|
104
101
|
Gate a production action before the agent executes it:
|
|
105
102
|
|
|
106
103
|
```bash
|
|
107
|
-
MARROW_API_KEY=mrw_live_xxx npx @getmarrow/install gate "deploy production
|
|
104
|
+
MARROW_API_KEY=mrw_live_xxx npx @getmarrow/install gate "deploy production worker after tests pass"
|
|
108
105
|
```
|
|
109
106
|
|
|
110
107
|
Wrap a real deploy, publish, merge, or migration command only after the agent has the required proof:
|
|
@@ -115,7 +112,7 @@ MARROW_API_KEY=mrw_live_xxx npx @getmarrow/install run \
|
|
|
115
112
|
--type deploy \
|
|
116
113
|
--profile production \
|
|
117
114
|
--policy enforce \
|
|
118
|
-
--
|
|
115
|
+
-- wrangler deploy
|
|
119
116
|
```
|
|
120
117
|
|
|
121
118
|
Use `--policy warn` for pilot mode and `--fail-open` only for non-production local workflows where Marrow should never block execution.
|
|
@@ -175,24 +172,49 @@ I am about to deploy to production. What should I check first?
|
|
|
175
172
|
|
|
176
173
|
Marrow should answer with `proceed`, `warn`, `block`, or `owner_approval_required`, plus required proof and matching fleet lessons/playbooks before the agent acts. This is the first product moment: not just "hooks installed", but "the agent is being warned before risky work."
|
|
177
174
|
|
|
175
|
+
## Fleet Scale Startup Guidance
|
|
176
|
+
|
|
177
|
+
For business fleets, agents can read `GET /v1/agent/scale/capacity-contract` after install or during startup. The capacity contract tells the agent whether the account is under backpressure, when low-risk runtime guidance can be reused, when high-risk actions must still call Marrow, and how to batch low-risk telemetry through `/v1/agent/ingest/batch`.
|
|
178
|
+
|
|
179
|
+
This keeps Marrow passive without making it chatty. The current contract models the common `50 businesses x 50 agents = 2,500 agents` rollout shape and gives agents an exact next action: cache low-risk runtime guidance briefly, batch command/tool telemetry, and preserve proof-gated checks for deploys, publishes, merges, migrations, secrets, billing, and destructive work.
|
|
180
|
+
|
|
181
|
+
## Fleet Operator TUI
|
|
182
|
+
|
|
183
|
+
Use the operator TUI when a human owner or orchestrator agent needs to understand fleet health before acting:
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
MARROW_API_KEY=mrw_live_xxx npx @getmarrow/install fleet
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
It shows:
|
|
190
|
+
|
|
191
|
+
- live agents and selected agent inspection
|
|
192
|
+
- active workflows
|
|
193
|
+
- risky actions waiting for proof
|
|
194
|
+
- failed or stale outcomes
|
|
195
|
+
- backpressure and capacity guidance
|
|
196
|
+
- recent decisions
|
|
197
|
+
- degraded hooks
|
|
198
|
+
- deploy, publish, and merge gate status
|
|
199
|
+
- exact fix commands for common degraded states
|
|
200
|
+
|
|
201
|
+
For automated reports or headless agents:
|
|
202
|
+
|
|
203
|
+
```bash
|
|
204
|
+
MARROW_API_KEY=mrw_live_xxx npx @getmarrow/install fleet --no-interactive
|
|
205
|
+
MARROW_API_KEY=mrw_live_xxx npx @getmarrow/install fleet --json
|
|
206
|
+
```
|
|
207
|
+
|
|
178
208
|
## What It Detects
|
|
179
209
|
|
|
180
210
|
- OpenClaw-style workspaces
|
|
181
211
|
- Codex/agent instruction files such as `AGENTS.md`
|
|
182
212
|
- Claude Code settings and hooks
|
|
183
213
|
- Cursor project folders
|
|
184
|
-
- Gemini, Grok, DeepSeek, Minimax, Kimi/Moonshot, Hermes, GLM, and Qwen project marker files when present
|
|
185
214
|
- MCP config files
|
|
186
|
-
- CI workflow/script markers
|
|
187
215
|
- Node projects
|
|
188
216
|
- Python projects
|
|
189
217
|
|
|
190
|
-
The detector is intentionally conservative. If Marrow cannot identify the harness from local files, it still supports the workflow through the custom command path:
|
|
191
|
-
|
|
192
|
-
```bash
|
|
193
|
-
MARROW_API_KEY=mrw_live_xxx npx @getmarrow/install run --agent research-agent -- <your-agent-command>
|
|
194
|
-
```
|
|
195
|
-
|
|
196
218
|
## Install Modes
|
|
197
219
|
|
|
198
220
|
```bash
|
|
@@ -233,14 +255,6 @@ Doctor check:
|
|
|
233
255
|
MARROW_API_KEY=mrw_live_xxx npx @getmarrow/install doctor
|
|
234
256
|
```
|
|
235
257
|
|
|
236
|
-
Deep doctor with harmless write/outcome verification:
|
|
237
|
-
|
|
238
|
-
```bash
|
|
239
|
-
MARROW_API_KEY=mrw_live_xxx npx @getmarrow/install doctor --self-test
|
|
240
|
-
```
|
|
241
|
-
|
|
242
|
-
Expected healthy output includes `key valid: yes`, `write test event: passed`, and `outcome closed: passed`.
|
|
243
|
-
|
|
244
258
|
Repair missing hooks/config:
|
|
245
259
|
|
|
246
260
|
```bash
|
package/bin/marrow-install.js
CHANGED
|
@@ -4,7 +4,7 @@ const installer = require('../src/installer');
|
|
|
4
4
|
const governedRunner = require('../src/governed-runner');
|
|
5
5
|
|
|
6
6
|
const argv = process.argv.slice(2);
|
|
7
|
-
const governedCommands = new Set(['run', 'gate', 'proof', 'status', 'govern']);
|
|
7
|
+
const governedCommands = new Set(['run', 'gate', 'proof', 'status', 'govern', 'fleet']);
|
|
8
8
|
const runCli = governedCommands.has(argv[0]) ? governedRunner.runCli : installer.runCli;
|
|
9
9
|
|
|
10
10
|
runCli(argv).catch((error) => {
|
package/package.json
CHANGED
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
|
};
|
package/src/installer.js
CHANGED
|
@@ -4,9 +4,6 @@ const os = require('node:os');
|
|
|
4
4
|
const crypto = require('node:crypto');
|
|
5
5
|
|
|
6
6
|
const DEFAULT_BASE_URL = 'https://api.getmarrow.ai';
|
|
7
|
-
const INSTALLER_LATEST = '0.1.19';
|
|
8
|
-
const SDK_LATEST = '3.7.35';
|
|
9
|
-
const MCP_LATEST = '3.9.35';
|
|
10
7
|
const MARROW_BLOCK_START = '<!-- marrow:passive-start -->';
|
|
11
8
|
const MARROW_BLOCK_END = '<!-- marrow:passive-end -->';
|
|
12
9
|
|
|
@@ -18,7 +15,7 @@ function parseArgs(argv) {
|
|
|
18
15
|
doctor: false,
|
|
19
16
|
repair: false,
|
|
20
17
|
mode: 'auto',
|
|
21
|
-
apiKey: process.env.MARROW_API_KEY ||
|
|
18
|
+
apiKey: process.env.MARROW_API_KEY || '',
|
|
22
19
|
baseUrl: process.env.MARROW_BASE_URL || DEFAULT_BASE_URL,
|
|
23
20
|
agentId: process.env.MARROW_FLEET_AGENT_ID || process.env.MARROW_AGENT_ID || '',
|
|
24
21
|
selfTest: true,
|
|
@@ -60,12 +57,6 @@ function parseArgs(argv) {
|
|
|
60
57
|
throw new Error('--mode must be one of auto, mcp, sdk, both, md');
|
|
61
58
|
}
|
|
62
59
|
|
|
63
|
-
const resolved = resolveMarrowKeyMaterial(options.cwd);
|
|
64
|
-
if (!options.apiKey && resolved.apiKey) options.apiKey = resolved.apiKey;
|
|
65
|
-
if (!options.agentId && resolved.agentId) options.agentId = resolved.agentId;
|
|
66
|
-
if (options.baseUrl === DEFAULT_BASE_URL && resolved.baseUrl) options.baseUrl = resolved.baseUrl;
|
|
67
|
-
if (!options.keySource && resolved.source) options.keySource = resolved.source;
|
|
68
|
-
|
|
69
60
|
return options;
|
|
70
61
|
}
|
|
71
62
|
|
|
@@ -80,14 +71,14 @@ function usage() {
|
|
|
80
71
|
|
|
81
72
|
Options:
|
|
82
73
|
--dry-run Print planned changes without writing
|
|
83
|
-
--doctor Check install health
|
|
74
|
+
--doctor Check install health without writing
|
|
84
75
|
--repair Write missing hooks/config, then run self-test and status check
|
|
85
76
|
--yes, -y Write detected config files
|
|
86
77
|
--mode <mode> auto, mcp, sdk, both, or md
|
|
87
78
|
--key <key> Marrow API key for self-test. Prefer MARROW_API_KEY because CLI args can appear in process listings.
|
|
88
79
|
--base-url <url> Marrow API base URL
|
|
89
80
|
--agent-id <id> Agent/fleet id for self-test headers
|
|
90
|
-
--no-self-test Skip API smoke/self-test
|
|
81
|
+
--no-self-test Skip API smoke/self-test
|
|
91
82
|
`;
|
|
92
83
|
}
|
|
93
84
|
|
|
@@ -166,7 +157,8 @@ function findLikelyEnvFiles(detection, env = process.env) {
|
|
|
166
157
|
path.join(detection.root, '.marrow', 'env'),
|
|
167
158
|
path.join(detection.root, '.marrow', 'env.local'),
|
|
168
159
|
path.join(home, '.marrow', 'env'),
|
|
169
|
-
path.join(home, '.
|
|
160
|
+
path.join(home, '.openclaw', 'credentials', 'marrow-mcp.env'),
|
|
161
|
+
path.join(home, '.openclaw', 'gateway.systemd.env'),
|
|
170
162
|
];
|
|
171
163
|
return candidates.filter((filePath) => {
|
|
172
164
|
if (!exists(filePath)) return false;
|
|
@@ -191,36 +183,6 @@ function readEnvVar(filePath, name) {
|
|
|
191
183
|
return match ? stripQuotes(match[1]) : '';
|
|
192
184
|
}
|
|
193
185
|
|
|
194
|
-
function resolveMarrowKeyMaterial(cwd = process.cwd(), env = process.env) {
|
|
195
|
-
if (env.MARROW_API_KEY || env.MARROW_KEY) {
|
|
196
|
-
return {
|
|
197
|
-
apiKey: env.MARROW_API_KEY || env.MARROW_KEY,
|
|
198
|
-
baseUrl: env.MARROW_BASE_URL || DEFAULT_BASE_URL,
|
|
199
|
-
agentId: env.MARROW_FLEET_AGENT_ID || env.MARROW_AGENT_ID || '',
|
|
200
|
-
source: env.MARROW_API_KEY ? 'MARROW_API_KEY' : 'MARROW_KEY',
|
|
201
|
-
};
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
const detection = detectEnvironment(cwd, env);
|
|
205
|
-
for (const filePath of findLikelyEnvFiles(detection, env)) {
|
|
206
|
-
const apiKey = readEnvVar(filePath, 'MARROW_API_KEY') || readEnvVar(filePath, 'MARROW_KEY');
|
|
207
|
-
if (!apiKey) continue;
|
|
208
|
-
return {
|
|
209
|
-
apiKey,
|
|
210
|
-
baseUrl: readEnvVar(filePath, 'MARROW_BASE_URL') || env.MARROW_BASE_URL || DEFAULT_BASE_URL,
|
|
211
|
-
agentId: readEnvVar(filePath, 'MARROW_FLEET_AGENT_ID') || readEnvVar(filePath, 'MARROW_AGENT_ID') || env.MARROW_FLEET_AGENT_ID || env.MARROW_AGENT_ID || '',
|
|
212
|
-
source: filePath,
|
|
213
|
-
};
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
return {
|
|
217
|
-
apiKey: '',
|
|
218
|
-
baseUrl: env.MARROW_BASE_URL || DEFAULT_BASE_URL,
|
|
219
|
-
agentId: env.MARROW_FLEET_AGENT_ID || env.MARROW_AGENT_ID || '',
|
|
220
|
-
source: null,
|
|
221
|
-
};
|
|
222
|
-
}
|
|
223
|
-
|
|
224
186
|
function readFirstLineSecret(filePath) {
|
|
225
187
|
if (!exists(filePath)) return '';
|
|
226
188
|
return safeRead(filePath).split(/\r?\n/).map((line) => line.trim()).find(Boolean) || '';
|
|
@@ -335,79 +297,27 @@ Required environment:
|
|
|
335
297
|
|
|
336
298
|
- \`MARROW_API_KEY\`
|
|
337
299
|
- Optional: \`MARROW_BASE_URL\`, \`MARROW_FLEET_AGENT_ID\`
|
|
338
|
-
|
|
339
|
-
Key loading:
|
|
340
|
-
|
|
341
|
-
- Prefer your shell, MCP secret store, or agent secret manager.
|
|
342
|
-
- Marrow also auto-detects \`.marrow/env\`, \`.marrow/env.local\`, \`.env\`, \`.env.local\`, and \`~/.marrow/env\`.
|
|
343
|
-
- Run \`npx @getmarrow/install doctor\` any time an agent says Marrow is missing or degraded.
|
|
344
300
|
${MARROW_BLOCK_END}`;
|
|
345
301
|
}
|
|
346
302
|
|
|
347
303
|
function passiveRuntimeSource() {
|
|
348
|
-
return `
|
|
349
|
-
import path from 'node:path';
|
|
350
|
-
import os from 'node:os';
|
|
351
|
-
|
|
352
|
-
function stripQuotes(value) {
|
|
353
|
-
const trimmed = String(value || '').trim();
|
|
354
|
-
if ((trimmed.startsWith('"') && trimmed.endsWith('"')) || (trimmed.startsWith("'") && trimmed.endsWith("'"))) return trimmed.slice(1, -1);
|
|
355
|
-
return trimmed;
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
function readEnvFile(filePath) {
|
|
359
|
-
if (!fs.existsSync(filePath)) return {};
|
|
360
|
-
const values = {};
|
|
361
|
-
const allowed = new Set(['MARROW_API_KEY', 'MARROW_KEY', 'MARROW_BASE_URL', 'MARROW_FLEET_AGENT_ID', 'MARROW_AGENT_ID', 'MARROW_SESSION_ID', 'MARROW_ENFORCEMENT_MODE', 'MARROW_PASSIVE_VALUE_REPORT', 'MARROW_VALUE_REPORT_PERIOD', 'MARROW_AGENT_RUNTIME', 'MARROW_WORKFLOW_GATE', 'MARROW_REQUIRE_OUTCOME_CLOSURE']);
|
|
362
|
-
for (const line of fs.readFileSync(filePath, 'utf8').split(/\\r?\\n/)) {
|
|
363
|
-
const match = line.match(/^\\s*(?:export\\s+)?([A-Z_][A-Z0-9_]*)\\s*=\\s*(.*?)\\s*$/);
|
|
364
|
-
if (!match) continue;
|
|
365
|
-
if (!allowed.has(match[1])) continue;
|
|
366
|
-
let value = match[2] || '';
|
|
367
|
-
const hashIndex = value.search(/\\s+#/);
|
|
368
|
-
if (hashIndex >= 0) value = value.slice(0, hashIndex);
|
|
369
|
-
values[match[1]] = stripQuotes(value);
|
|
370
|
-
}
|
|
371
|
-
return values;
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
function resolveMarrowEnv() {
|
|
375
|
-
if (process.env.MARROW_API_KEY || process.env.MARROW_KEY) return process.env;
|
|
376
|
-
const home = process.env.HOME || process.env.USERPROFILE || os.homedir();
|
|
377
|
-
const files = [];
|
|
378
|
-
let dir = process.cwd();
|
|
379
|
-
for (let depth = 0; depth < 8; depth += 1) {
|
|
380
|
-
files.push(path.join(dir, '.marrow', 'env'), path.join(dir, '.marrow', 'env.local'), path.join(dir, '.env'), path.join(dir, '.env.local'));
|
|
381
|
-
const parent = path.dirname(dir);
|
|
382
|
-
if (parent === dir) break;
|
|
383
|
-
dir = parent;
|
|
384
|
-
}
|
|
385
|
-
files.push(path.join(home, '.marrow', 'env'), path.join(home, '.marrow', 'env.local'));
|
|
386
|
-
for (const filePath of [...new Set(files)]) {
|
|
387
|
-
const values = readEnvFile(filePath);
|
|
388
|
-
if (values.MARROW_API_KEY || values.MARROW_KEY) return { ...process.env, ...values };
|
|
389
|
-
}
|
|
390
|
-
return process.env;
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
const marrowEnv = resolveMarrowEnv();
|
|
394
|
-
const apiKey = marrowEnv.MARROW_API_KEY || marrowEnv.MARROW_KEY;
|
|
304
|
+
return `const apiKey = process.env.MARROW_API_KEY;
|
|
395
305
|
if (apiKey && !globalThis.__MARROW_PASSIVE_RUNTIME__) {
|
|
396
306
|
try {
|
|
397
307
|
const { MarrowClient } = await import('@getmarrow/sdk');
|
|
398
308
|
const marrow = new MarrowClient(apiKey, {
|
|
399
|
-
baseUrl:
|
|
400
|
-
agentId:
|
|
401
|
-
sessionId:
|
|
402
|
-
mode:
|
|
309
|
+
baseUrl: process.env.MARROW_BASE_URL,
|
|
310
|
+
agentId: process.env.MARROW_FLEET_AGENT_ID || process.env.MARROW_AGENT_ID,
|
|
311
|
+
sessionId: process.env.MARROW_SESSION_ID,
|
|
312
|
+
mode: process.env.MARROW_ENFORCEMENT_MODE || 'auto',
|
|
403
313
|
});
|
|
404
314
|
|
|
405
315
|
const runtime = marrow.createPassiveRuntime({
|
|
406
|
-
includeValueReport:
|
|
407
|
-
valueReportPeriod:
|
|
408
|
-
useAgentRuntime:
|
|
409
|
-
useWorkflowGate:
|
|
410
|
-
requireOutcomeClosure:
|
|
316
|
+
includeValueReport: process.env.MARROW_PASSIVE_VALUE_REPORT !== 'false',
|
|
317
|
+
valueReportPeriod: process.env.MARROW_VALUE_REPORT_PERIOD || '7d',
|
|
318
|
+
useAgentRuntime: process.env.MARROW_AGENT_RUNTIME !== 'false',
|
|
319
|
+
useWorkflowGate: process.env.MARROW_WORKFLOW_GATE !== 'false',
|
|
320
|
+
requireOutcomeClosure: process.env.MARROW_REQUIRE_OUTCOME_CLOSURE !== 'false',
|
|
411
321
|
});
|
|
412
322
|
|
|
413
323
|
runtime.install();
|
|
@@ -415,8 +325,6 @@ if (apiKey && !globalThis.__MARROW_PASSIVE_RUNTIME__) {
|
|
|
415
325
|
} catch {
|
|
416
326
|
console.warn('[Marrow] passive runtime skipped: install @getmarrow/sdk or verify SDK initialization. Run npm install @getmarrow/sdk, then rerun npx @getmarrow/install --repair.');
|
|
417
327
|
}
|
|
418
|
-
} else if (!apiKey) {
|
|
419
|
-
console.warn('[Marrow] passive runtime skipped: MARROW_API_KEY missing. Put it in .marrow/env or run npx @getmarrow/install doctor for the exact fix.');
|
|
420
328
|
}
|
|
421
329
|
`;
|
|
422
330
|
}
|
|
@@ -536,82 +444,6 @@ function inspectSdkDependency(detection) {
|
|
|
536
444
|
};
|
|
537
445
|
}
|
|
538
446
|
|
|
539
|
-
function compareVersions(a, b) {
|
|
540
|
-
const left = String(a || '0.0.0').replace(/^[^\d]*/, '').split('.').map((part) => parseInt(part, 10) || 0);
|
|
541
|
-
const right = String(b || '0.0.0').replace(/^[^\d]*/, '').split('.').map((part) => parseInt(part, 10) || 0);
|
|
542
|
-
for (let i = 0; i < Math.max(left.length, right.length); i += 1) {
|
|
543
|
-
const x = left[i] || 0;
|
|
544
|
-
const y = right[i] || 0;
|
|
545
|
-
if (x > y) return 1;
|
|
546
|
-
if (x < y) return -1;
|
|
547
|
-
}
|
|
548
|
-
return 0;
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
function dependencyVersion(packageJson, packageName) {
|
|
552
|
-
for (const block of ['dependencies', 'devDependencies', 'optionalDependencies', 'peerDependencies']) {
|
|
553
|
-
const value = packageJson?.[block]?.[packageName];
|
|
554
|
-
if (typeof value === 'string') return value;
|
|
555
|
-
}
|
|
556
|
-
return null;
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
function installedPackageVersion(root, packageName) {
|
|
560
|
-
const packageJsonPath = path.join(root, 'node_modules', ...packageName.split('/'), 'package.json');
|
|
561
|
-
try {
|
|
562
|
-
const raw = safeRead(packageJsonPath);
|
|
563
|
-
if (!raw) return null;
|
|
564
|
-
const parsed = JSON.parse(raw);
|
|
565
|
-
return typeof parsed.version === 'string' ? parsed.version : null;
|
|
566
|
-
} catch {
|
|
567
|
-
return null;
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
function inspectPackageVersions(detection) {
|
|
572
|
-
const rootPackage = safeRead(detection.paths.packageJson);
|
|
573
|
-
let packageJson = {};
|
|
574
|
-
try {
|
|
575
|
-
packageJson = rootPackage ? JSON.parse(rootPackage) : {};
|
|
576
|
-
} catch {
|
|
577
|
-
packageJson = {};
|
|
578
|
-
}
|
|
579
|
-
const versions = [
|
|
580
|
-
{
|
|
581
|
-
name: '@getmarrow/install',
|
|
582
|
-
installed: require('../package.json').version,
|
|
583
|
-
latest: INSTALLER_LATEST,
|
|
584
|
-
source: 'current installer',
|
|
585
|
-
update_command: 'npm install -g @getmarrow/install@latest',
|
|
586
|
-
},
|
|
587
|
-
{
|
|
588
|
-
name: '@getmarrow/sdk',
|
|
589
|
-
installed: installedPackageVersion(detection.root, '@getmarrow/sdk') || dependencyVersion(packageJson, '@getmarrow/sdk'),
|
|
590
|
-
latest: SDK_LATEST,
|
|
591
|
-
source: installedPackageVersion(detection.root, '@getmarrow/sdk') ? 'node_modules' : 'package.json',
|
|
592
|
-
update_command: 'npm install @getmarrow/sdk@latest',
|
|
593
|
-
},
|
|
594
|
-
{
|
|
595
|
-
name: '@getmarrow/mcp',
|
|
596
|
-
installed: installedPackageVersion(detection.root, '@getmarrow/mcp') || dependencyVersion(packageJson, '@getmarrow/mcp'),
|
|
597
|
-
latest: MCP_LATEST,
|
|
598
|
-
source: installedPackageVersion(detection.root, '@getmarrow/mcp') ? 'node_modules' : 'package.json',
|
|
599
|
-
update_command: 'npm install @getmarrow/mcp@latest',
|
|
600
|
-
},
|
|
601
|
-
];
|
|
602
|
-
return versions.map((entry) => {
|
|
603
|
-
const normalized = entry.installed ? String(entry.installed).replace(/^[^\d]*/, '') : null;
|
|
604
|
-
const outdated = normalized ? compareVersions(normalized, entry.latest) < 0 : false;
|
|
605
|
-
return {
|
|
606
|
-
...entry,
|
|
607
|
-
installed: normalized,
|
|
608
|
-
present: Boolean(normalized),
|
|
609
|
-
outdated,
|
|
610
|
-
warning: outdated ? `${entry.name} ${normalized} is older than ${entry.latest}.` : null,
|
|
611
|
-
};
|
|
612
|
-
});
|
|
613
|
-
}
|
|
614
|
-
|
|
615
447
|
function buildPlan(detection, options) {
|
|
616
448
|
const mode = options.mode === 'auto'
|
|
617
449
|
? detection.node && (detection.claudeCode || detection.cursor || detection.codex || detection.openclaw)
|
|
@@ -716,108 +548,11 @@ async function requestJson(url, options) {
|
|
|
716
548
|
}
|
|
717
549
|
if (!res.ok) {
|
|
718
550
|
const message = json.error || json.message || `HTTP ${res.status}`;
|
|
719
|
-
|
|
720
|
-
error.status = res.status;
|
|
721
|
-
error.code = json.code || null;
|
|
722
|
-
error.details = json.details || null;
|
|
723
|
-
throw error;
|
|
551
|
+
throw new Error(String(message));
|
|
724
552
|
}
|
|
725
553
|
return json.data || json;
|
|
726
554
|
}
|
|
727
555
|
|
|
728
|
-
function classifyDoctorFailure(error) {
|
|
729
|
-
const status = error?.status || 0;
|
|
730
|
-
const text = `${error?.code || ''} ${error?.message || error}`.toLowerCase();
|
|
731
|
-
if (status === 401 || /missing_key|invalid_key|unauthorized|invalid api key/.test(text)) return 'invalid_key';
|
|
732
|
-
if (status === 403 && /agent|bound|identity/.test(text)) return 'wrong_agent_id';
|
|
733
|
-
if (status === 403) return 'invalid_key';
|
|
734
|
-
if (status === 409 || /proof/.test(text)) return 'proof_required';
|
|
735
|
-
if (status === 429 || /rate limit|too many/.test(text)) return 'network_blocked';
|
|
736
|
-
if (status >= 500 || /timeout|network|fetch failed|econnreset|enotfound|eai_again/.test(text)) return 'network_blocked';
|
|
737
|
-
return 'unknown';
|
|
738
|
-
}
|
|
739
|
-
|
|
740
|
-
async function runDoctorValidation(options, selfTest) {
|
|
741
|
-
if (!options.apiKey) {
|
|
742
|
-
return {
|
|
743
|
-
key_found: false,
|
|
744
|
-
key_valid: false,
|
|
745
|
-
account_active: false,
|
|
746
|
-
agent_identity_accepted: false,
|
|
747
|
-
write_test_event: 'skipped',
|
|
748
|
-
outcome_closed: 'skipped',
|
|
749
|
-
failure_reason: 'missing_key',
|
|
750
|
-
exact_fix: 'Create an API key at https://getmarrow.ai/account, then put MARROW_API_KEY in .marrow/env and run npx @getmarrow/install doctor --self-test.',
|
|
751
|
-
};
|
|
752
|
-
}
|
|
753
|
-
|
|
754
|
-
if (selfTest && !selfTest.skipped && selfTest.active && !selfTest.error) {
|
|
755
|
-
return {
|
|
756
|
-
key_found: true,
|
|
757
|
-
key_valid: true,
|
|
758
|
-
account_active: true,
|
|
759
|
-
agent_identity_accepted: true,
|
|
760
|
-
write_test_event: 'passed',
|
|
761
|
-
outcome_closed: 'passed',
|
|
762
|
-
failure_reason: null,
|
|
763
|
-
decision_id: selfTest.decision_id,
|
|
764
|
-
exact_fix: null,
|
|
765
|
-
};
|
|
766
|
-
}
|
|
767
|
-
|
|
768
|
-
if (selfTest && selfTest.error) {
|
|
769
|
-
const reason = classifyDoctorFailure(selfTest);
|
|
770
|
-
return {
|
|
771
|
-
key_found: true,
|
|
772
|
-
key_valid: !['missing_key', 'invalid_key'].includes(reason),
|
|
773
|
-
account_active: !['missing_key', 'invalid_key'].includes(reason),
|
|
774
|
-
agent_identity_accepted: reason !== 'wrong_agent_id',
|
|
775
|
-
write_test_event: 'failed',
|
|
776
|
-
outcome_closed: 'failed',
|
|
777
|
-
failure_reason: reason,
|
|
778
|
-
exact_fix: reason === 'wrong_agent_id'
|
|
779
|
-
? 'Set MARROW_FLEET_AGENT_ID/MARROW_AGENT_ID to the id bound to this key, then rerun doctor.'
|
|
780
|
-
: reason === 'network_blocked'
|
|
781
|
-
? 'Retry from a network path that can reach api.getmarrow.ai, or reduce status polling if rate-limited.'
|
|
782
|
-
: 'Create/copy a live API key from the Marrow dashboard and update MARROW_API_KEY.',
|
|
783
|
-
};
|
|
784
|
-
}
|
|
785
|
-
|
|
786
|
-
const headers = { authorization: `Bearer ${options.apiKey}` };
|
|
787
|
-
if (options.agentId) headers['x-marrow-agent-id'] = options.agentId;
|
|
788
|
-
try {
|
|
789
|
-
const status = await requestJson(`${options.baseUrl.replace(/\/+$/, '')}/v1/agent/status`, { headers });
|
|
790
|
-
return {
|
|
791
|
-
key_found: true,
|
|
792
|
-
key_valid: true,
|
|
793
|
-
account_active: true,
|
|
794
|
-
agent_identity_accepted: true,
|
|
795
|
-
write_test_event: 'skipped',
|
|
796
|
-
outcome_closed: 'skipped',
|
|
797
|
-
failure_reason: null,
|
|
798
|
-
status_health: status.health || 'unknown',
|
|
799
|
-
status_failure_reasons: status.failure_reasons || [],
|
|
800
|
-
exact_fix: status.recommended_fix || status.diagnostics?.exact_fix || null,
|
|
801
|
-
};
|
|
802
|
-
} catch (error) {
|
|
803
|
-
const reason = classifyDoctorFailure(error);
|
|
804
|
-
return {
|
|
805
|
-
key_found: true,
|
|
806
|
-
key_valid: !['missing_key', 'invalid_key'].includes(reason),
|
|
807
|
-
account_active: !['missing_key', 'invalid_key'].includes(reason),
|
|
808
|
-
agent_identity_accepted: reason !== 'wrong_agent_id',
|
|
809
|
-
write_test_event: 'skipped',
|
|
810
|
-
outcome_closed: 'skipped',
|
|
811
|
-
failure_reason: reason,
|
|
812
|
-
exact_fix: reason === 'wrong_agent_id'
|
|
813
|
-
? 'Set MARROW_FLEET_AGENT_ID/MARROW_AGENT_ID to the id bound to this key, then rerun doctor.'
|
|
814
|
-
: reason === 'network_blocked'
|
|
815
|
-
? 'Check network access to api.getmarrow.ai and rerun doctor.'
|
|
816
|
-
: 'Create/copy a live API key from the Marrow dashboard and update MARROW_API_KEY.',
|
|
817
|
-
};
|
|
818
|
-
}
|
|
819
|
-
}
|
|
820
|
-
|
|
821
556
|
async function runSelfTest(options) {
|
|
822
557
|
if (!options.selfTest) return { skipped: true, reason: 'disabled' };
|
|
823
558
|
if (!options.apiKey) {
|
|
@@ -1103,24 +838,6 @@ function printReport(report) {
|
|
|
1103
838
|
process.stdout.write(`- missing env: ${report.doctor.missingEnv.length ? report.doctor.missingEnv.join(', ') : 'none'}\n`);
|
|
1104
839
|
if (report.doctor.envHints.length) process.stdout.write(`- possible env files: ${report.doctor.envHints.join(', ')}\n`);
|
|
1105
840
|
process.stdout.write(`- missing hooks/config: ${report.doctor.missingHooks.length ? report.doctor.missingHooks.join('; ') : 'none'}\n`);
|
|
1106
|
-
if (report.doctor.validation) {
|
|
1107
|
-
const validation = report.doctor.validation;
|
|
1108
|
-
process.stdout.write(`- key found: ${validation.key_found ? 'yes' : 'no'}\n`);
|
|
1109
|
-
process.stdout.write(`- key valid: ${validation.key_valid ? 'yes' : 'no'}\n`);
|
|
1110
|
-
process.stdout.write(`- account active: ${validation.account_active ? 'yes' : 'no'}\n`);
|
|
1111
|
-
process.stdout.write(`- agent identity accepted: ${validation.agent_identity_accepted ? 'yes' : 'no'}\n`);
|
|
1112
|
-
process.stdout.write(`- write test event: ${validation.write_test_event}\n`);
|
|
1113
|
-
process.stdout.write(`- outcome closed: ${validation.outcome_closed}\n`);
|
|
1114
|
-
if (validation.failure_reason) process.stdout.write(`- failure reason: ${validation.failure_reason}\n`);
|
|
1115
|
-
if (validation.exact_fix) process.stdout.write(`- exact fix: ${validation.exact_fix}\n`);
|
|
1116
|
-
}
|
|
1117
|
-
if (report.packageVersions?.length) {
|
|
1118
|
-
const outdated = report.packageVersions.filter((pkg) => pkg.outdated);
|
|
1119
|
-
process.stdout.write(`- package versions: ${outdated.length ? 'updates recommended' : 'current'}\n`);
|
|
1120
|
-
for (const pkg of outdated) {
|
|
1121
|
-
process.stdout.write(` - ${pkg.warning} Fix: ${pkg.update_command}\n`);
|
|
1122
|
-
}
|
|
1123
|
-
}
|
|
1124
841
|
if (report.doctor.recommendedFix) process.stdout.write(`- recommended fix: ${report.doctor.recommendedFix}\n`);
|
|
1125
842
|
}
|
|
1126
843
|
|
|
@@ -1143,7 +860,6 @@ async function install(options) {
|
|
|
1143
860
|
const changes = applyPlan(plan, options);
|
|
1144
861
|
const configInspection = inspectNpmTokenConfig();
|
|
1145
862
|
const sdkDependency = inspectSdkDependency(detection);
|
|
1146
|
-
const packageVersions = inspectPackageVersions(detection);
|
|
1147
863
|
const configDiagnostics = configInspection.safe;
|
|
1148
864
|
const configRepairs = options.repair && !options.dryRun && !options.doctor
|
|
1149
865
|
? repairConfigDiagnostics(configDiagnostics)
|
|
@@ -1153,11 +869,7 @@ async function install(options) {
|
|
|
1153
869
|
skipped: false,
|
|
1154
870
|
active: false,
|
|
1155
871
|
error: error instanceof Error ? error.message : String(error),
|
|
1156
|
-
status: error?.status || null,
|
|
1157
|
-
code: error?.code || null,
|
|
1158
|
-
details: error?.details || null,
|
|
1159
872
|
}));
|
|
1160
|
-
const doctorValidation = await runDoctorValidation(options, selfTest);
|
|
1161
873
|
const changedConfig = changes.some((change) => change.changed) || configRepairs.some((repair) => repair.changed);
|
|
1162
874
|
const selfTestPassed = Boolean(!selfTest.skipped && selfTest.active && !selfTest.error);
|
|
1163
875
|
const remediation = options.repair
|
|
@@ -1194,19 +906,16 @@ async function install(options) {
|
|
|
1194
906
|
missingEnv: options.apiKey ? [] : ['MARROW_API_KEY'],
|
|
1195
907
|
envHints,
|
|
1196
908
|
missingHooks: changes.filter((change) => change.changed).map((change) => change.label),
|
|
1197
|
-
validation: doctorValidation,
|
|
1198
|
-
packageVersions,
|
|
1199
909
|
recommendedFix: configDiagnostics.npm_token.recommended_fix || selfTest.recommended_fix || (!options.apiKey
|
|
1200
910
|
? envHints.length
|
|
1201
|
-
? `MARROW_API_KEY was found in ${envHints[0]}
|
|
1202
|
-
: 'Set MARROW_API_KEY
|
|
911
|
+
? `MARROW_API_KEY was found in a likely env file at ${envHints[0]}. Load that key from trusted secret storage, export only MARROW_API_KEY, then run npx @getmarrow/install --repair.`
|
|
912
|
+
: 'Set MARROW_API_KEY, then run npx @getmarrow/install --repair.'
|
|
1203
913
|
: null),
|
|
1204
914
|
},
|
|
1205
915
|
remediation,
|
|
1206
916
|
configDiagnostics,
|
|
1207
917
|
configRepairs,
|
|
1208
918
|
sdkDependency,
|
|
1209
|
-
packageVersions,
|
|
1210
919
|
selfTest,
|
|
1211
920
|
warnings: options.keyFromArg
|
|
1212
921
|
? ['Avoid --key in shared shells because command-line arguments can be visible in process listings. Prefer MARROW_API_KEY in your environment or secret manager.']
|
|
@@ -1237,10 +946,7 @@ module.exports = {
|
|
|
1237
946
|
runSelfTest,
|
|
1238
947
|
runCli,
|
|
1239
948
|
passiveRuntimeSource,
|
|
1240
|
-
resolveMarrowKeyMaterial,
|
|
1241
949
|
inspectNpmTokenConfig,
|
|
1242
950
|
inspectSdkDependency,
|
|
1243
|
-
inspectPackageVersions,
|
|
1244
|
-
runDoctorValidation,
|
|
1245
951
|
buildInstallValueMoment,
|
|
1246
952
|
};
|