@getmarrow/install 0.1.18 → 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 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.18
15
+ ## What's New in v0.1.20
15
16
 
16
- v0.1.18 makes Marrow key loading more reliable for agents after install.
17
+ v0.1.20 adds the Fleet Operator TUI for teams running multiple agents under one Marrow account.
17
18
 
18
- - Installer, SDK passive runtime, and MCP hooks now all recognize the same key locations: process env, MCP/agent secret env, `.marrow/env`, `.marrow/env.local`, `.env`, `.env.local`, `~/.marrow/env`, and `~/.marrow/env.local`.
19
- - `MARROW_API_KEY` remains the canonical variable. `MARROW_KEY` is accepted as an alias for fleet runners and secret managers.
20
- - `npx @getmarrow/install doctor` now reports whether a key is loaded, where a likely key file exists, and the exact repair command without printing the key.
21
- - Generated `.marrow/passive-runtime.mjs` loads `.marrow/env` before deciding Marrow is missing, so agents keep logging even when the shell did not export the key.
22
- - Full keys are never printed in diagnostics. Keep `.marrow/env` out of git and set file permissions to owner-only when possible.
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
- ## What's New in v0.1.17
25
+ Example:
25
26
 
26
- v0.1.17 expands the Govern TUI harness addon coverage while preserving adaptive mode recommendations.
27
-
28
- - `npx @getmarrow/install govern` now shows first-class rows for Codex, Claude Code, Cursor, Cline, Gemini CLI, Grok CLI, DeepSeek CLI, Minimax CLI, Kimi CLI, Hermes, GLM CLI, Qwen CLI, OpenCode, OpenClaw, MCP-compatible clients, CI scripts, and custom commands.
29
- - Marrow remains a thin governance layer. It does not replace your model or harness; it wraps the command your agent already runs with pre-action risk gates, proof requirements, and automatic outcome closure.
30
- - Detection stays recommendation-first. Marrow uses local config, instruction, CI, and MCP markers to suggest the safest path, then the user or owner accepts, overrides, or saves a policy profile.
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, platform config files, GitHub workflows, migrations, Cursor/Codex/Claude files, and MCP config.
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
- - Edge service detected
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 service after tests pass"
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
- -- npm run deploy
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
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getmarrow/install",
3
- "version": "0.1.18",
3
+ "version": "0.1.20",
4
4
  "description": "Universal installer for Marrow passive agent setup.",
5
5
  "bin": {
6
6
  "marrow-install": "bin/marrow-install.js"
@@ -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 existsAny(paths) {
572
- return paths.some((candidate) => fs.existsSync(candidate));
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: existsAny([path.join(cwd, 'AGENTS.md'), path.join(cwd, '.codex'), path.join(home, '.codex')]) },
579
- { name: 'Claude Code', command: 'claude -p', detected: existsAny([path.join(cwd, 'CLAUDE.md'), path.join(cwd, '.claude'), path.join(home, '.claude.json')]) },
580
- { name: 'Cursor', command: 'cursor', detected: existsAny([path.join(cwd, '.cursor'), path.join(cwd, '.cursor', 'mcp.json'), path.join(cwd, '.cursor', 'rules'), path.join(home, '.cursor')]) },
581
- { name: 'Gemini CLI', command: 'gemini', detected: existsAny([path.join(cwd, 'GEMINI.md'), path.join(cwd, '.gemini'), path.join(home, '.gemini')]) },
582
- { name: 'Grok CLI', command: 'grok', detected: existsAny([path.join(cwd, 'GROK.md'), path.join(cwd, '.grok'), path.join(home, '.grok')]) },
583
- { name: 'DeepSeek CLI', command: 'deepseek', detected: existsAny([path.join(cwd, 'DEEPSEEK.md'), path.join(cwd, '.deepseek'), path.join(home, '.deepseek')]) },
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
@@ -15,7 +15,7 @@ function parseArgs(argv) {
15
15
  doctor: false,
16
16
  repair: false,
17
17
  mode: 'auto',
18
- apiKey: process.env.MARROW_API_KEY || process.env.MARROW_KEY || '',
18
+ apiKey: process.env.MARROW_API_KEY || '',
19
19
  baseUrl: process.env.MARROW_BASE_URL || DEFAULT_BASE_URL,
20
20
  agentId: process.env.MARROW_FLEET_AGENT_ID || process.env.MARROW_AGENT_ID || '',
21
21
  selfTest: true,
@@ -57,12 +57,6 @@ function parseArgs(argv) {
57
57
  throw new Error('--mode must be one of auto, mcp, sdk, both, md');
58
58
  }
59
59
 
60
- const resolved = resolveMarrowKeyMaterial(options.cwd);
61
- if (!options.apiKey && resolved.apiKey) options.apiKey = resolved.apiKey;
62
- if (!options.agentId && resolved.agentId) options.agentId = resolved.agentId;
63
- if (options.baseUrl === DEFAULT_BASE_URL && resolved.baseUrl) options.baseUrl = resolved.baseUrl;
64
- if (!options.keySource && resolved.source) options.keySource = resolved.source;
65
-
66
60
  return options;
67
61
  }
68
62
 
@@ -163,7 +157,8 @@ function findLikelyEnvFiles(detection, env = process.env) {
163
157
  path.join(detection.root, '.marrow', 'env'),
164
158
  path.join(detection.root, '.marrow', 'env.local'),
165
159
  path.join(home, '.marrow', 'env'),
166
- path.join(home, '.marrow', 'env.local'),
160
+ path.join(home, '.openclaw', 'credentials', 'marrow-mcp.env'),
161
+ path.join(home, '.openclaw', 'gateway.systemd.env'),
167
162
  ];
168
163
  return candidates.filter((filePath) => {
169
164
  if (!exists(filePath)) return false;
@@ -188,36 +183,6 @@ function readEnvVar(filePath, name) {
188
183
  return match ? stripQuotes(match[1]) : '';
189
184
  }
190
185
 
191
- function resolveMarrowKeyMaterial(cwd = process.cwd(), env = process.env) {
192
- if (env.MARROW_API_KEY || env.MARROW_KEY) {
193
- return {
194
- apiKey: env.MARROW_API_KEY || env.MARROW_KEY,
195
- baseUrl: env.MARROW_BASE_URL || DEFAULT_BASE_URL,
196
- agentId: env.MARROW_FLEET_AGENT_ID || env.MARROW_AGENT_ID || '',
197
- source: env.MARROW_API_KEY ? 'MARROW_API_KEY' : 'MARROW_KEY',
198
- };
199
- }
200
-
201
- const detection = detectEnvironment(cwd, env);
202
- for (const filePath of findLikelyEnvFiles(detection, env)) {
203
- const apiKey = readEnvVar(filePath, 'MARROW_API_KEY') || readEnvVar(filePath, 'MARROW_KEY');
204
- if (!apiKey) continue;
205
- return {
206
- apiKey,
207
- baseUrl: readEnvVar(filePath, 'MARROW_BASE_URL') || env.MARROW_BASE_URL || DEFAULT_BASE_URL,
208
- agentId: readEnvVar(filePath, 'MARROW_FLEET_AGENT_ID') || readEnvVar(filePath, 'MARROW_AGENT_ID') || env.MARROW_FLEET_AGENT_ID || env.MARROW_AGENT_ID || '',
209
- source: filePath,
210
- };
211
- }
212
-
213
- return {
214
- apiKey: '',
215
- baseUrl: env.MARROW_BASE_URL || DEFAULT_BASE_URL,
216
- agentId: env.MARROW_FLEET_AGENT_ID || env.MARROW_AGENT_ID || '',
217
- source: null,
218
- };
219
- }
220
-
221
186
  function readFirstLineSecret(filePath) {
222
187
  if (!exists(filePath)) return '';
223
188
  return safeRead(filePath).split(/\r?\n/).map((line) => line.trim()).find(Boolean) || '';
@@ -332,79 +297,27 @@ Required environment:
332
297
 
333
298
  - \`MARROW_API_KEY\`
334
299
  - Optional: \`MARROW_BASE_URL\`, \`MARROW_FLEET_AGENT_ID\`
335
-
336
- Key loading:
337
-
338
- - Prefer your shell, MCP secret store, or agent secret manager.
339
- - Marrow also auto-detects \`.marrow/env\`, \`.marrow/env.local\`, \`.env\`, \`.env.local\`, and \`~/.marrow/env\`.
340
- - Run \`npx @getmarrow/install doctor\` any time an agent says Marrow is missing or degraded.
341
300
  ${MARROW_BLOCK_END}`;
342
301
  }
343
302
 
344
303
  function passiveRuntimeSource() {
345
- return `import fs from 'node:fs';
346
- import path from 'node:path';
347
- import os from 'node:os';
348
-
349
- function stripQuotes(value) {
350
- const trimmed = String(value || '').trim();
351
- if ((trimmed.startsWith('"') && trimmed.endsWith('"')) || (trimmed.startsWith("'") && trimmed.endsWith("'"))) return trimmed.slice(1, -1);
352
- return trimmed;
353
- }
354
-
355
- function readEnvFile(filePath) {
356
- if (!fs.existsSync(filePath)) return {};
357
- const values = {};
358
- 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']);
359
- for (const line of fs.readFileSync(filePath, 'utf8').split(/\\r?\\n/)) {
360
- const match = line.match(/^\\s*(?:export\\s+)?([A-Z_][A-Z0-9_]*)\\s*=\\s*(.*?)\\s*$/);
361
- if (!match) continue;
362
- if (!allowed.has(match[1])) continue;
363
- let value = match[2] || '';
364
- const hashIndex = value.search(/\\s+#/);
365
- if (hashIndex >= 0) value = value.slice(0, hashIndex);
366
- values[match[1]] = stripQuotes(value);
367
- }
368
- return values;
369
- }
370
-
371
- function resolveMarrowEnv() {
372
- if (process.env.MARROW_API_KEY || process.env.MARROW_KEY) return process.env;
373
- const home = process.env.HOME || process.env.USERPROFILE || os.homedir();
374
- const files = [];
375
- let dir = process.cwd();
376
- for (let depth = 0; depth < 8; depth += 1) {
377
- files.push(path.join(dir, '.marrow', 'env'), path.join(dir, '.marrow', 'env.local'), path.join(dir, '.env'), path.join(dir, '.env.local'));
378
- const parent = path.dirname(dir);
379
- if (parent === dir) break;
380
- dir = parent;
381
- }
382
- files.push(path.join(home, '.marrow', 'env'), path.join(home, '.marrow', 'env.local'));
383
- for (const filePath of [...new Set(files)]) {
384
- const values = readEnvFile(filePath);
385
- if (values.MARROW_API_KEY || values.MARROW_KEY) return { ...process.env, ...values };
386
- }
387
- return process.env;
388
- }
389
-
390
- const marrowEnv = resolveMarrowEnv();
391
- const apiKey = marrowEnv.MARROW_API_KEY || marrowEnv.MARROW_KEY;
304
+ return `const apiKey = process.env.MARROW_API_KEY;
392
305
  if (apiKey && !globalThis.__MARROW_PASSIVE_RUNTIME__) {
393
306
  try {
394
307
  const { MarrowClient } = await import('@getmarrow/sdk');
395
308
  const marrow = new MarrowClient(apiKey, {
396
- baseUrl: marrowEnv.MARROW_BASE_URL,
397
- agentId: marrowEnv.MARROW_FLEET_AGENT_ID || marrowEnv.MARROW_AGENT_ID,
398
- sessionId: marrowEnv.MARROW_SESSION_ID,
399
- mode: marrowEnv.MARROW_ENFORCEMENT_MODE || 'auto',
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',
400
313
  });
401
314
 
402
315
  const runtime = marrow.createPassiveRuntime({
403
- includeValueReport: marrowEnv.MARROW_PASSIVE_VALUE_REPORT !== 'false',
404
- valueReportPeriod: marrowEnv.MARROW_VALUE_REPORT_PERIOD || '7d',
405
- useAgentRuntime: marrowEnv.MARROW_AGENT_RUNTIME !== 'false',
406
- useWorkflowGate: marrowEnv.MARROW_WORKFLOW_GATE !== 'false',
407
- requireOutcomeClosure: marrowEnv.MARROW_REQUIRE_OUTCOME_CLOSURE !== 'false',
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',
408
321
  });
409
322
 
410
323
  runtime.install();
@@ -412,8 +325,6 @@ if (apiKey && !globalThis.__MARROW_PASSIVE_RUNTIME__) {
412
325
  } catch {
413
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.');
414
327
  }
415
- } else if (!apiKey) {
416
- 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.');
417
328
  }
418
329
  `;
419
330
  }
@@ -997,8 +908,8 @@ async function install(options) {
997
908
  missingHooks: changes.filter((change) => change.changed).map((change) => change.label),
998
909
  recommendedFix: configDiagnostics.npm_token.recommended_fix || selfTest.recommended_fix || (!options.apiKey
999
910
  ? envHints.length
1000
- ? `MARROW_API_KEY was found in ${envHints[0]} and can now be auto-loaded by Marrow SDK/MCP runtimes. Run npx @getmarrow/install --repair to refresh hooks and self-test.`
1001
- : 'Set MARROW_API_KEY or MARROW_KEY in your shell, MCP secret store, .marrow/env, or ~/.marrow/env, then run npx @getmarrow/install --repair.'
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.'
1002
913
  : null),
1003
914
  },
1004
915
  remediation,
@@ -1035,7 +946,6 @@ module.exports = {
1035
946
  runSelfTest,
1036
947
  runCli,
1037
948
  passiveRuntimeSource,
1038
- resolveMarrowKeyMaterial,
1039
949
  inspectNpmTokenConfig,
1040
950
  inspectSdkDependency,
1041
951
  buildInstallValueMoment,