@getmarrow/install 0.1.13 → 0.1.15

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
@@ -11,6 +11,27 @@ npx @getmarrow/install --repair
11
11
  npx @getmarrow/install doctor
12
12
  ```
13
13
 
14
+ ## What's New in v0.1.14
15
+
16
+ v0.1.14 adds adaptive governance mode recommendations without silent auto-switching.
17
+
18
+ - `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.
19
+ - When `MARROW_API_KEY` is present, the TUI asks Marrow for a recommended mode: `passive`, `pilot`, or `enforce`.
20
+ - The TUI shows the exact reasons, confidence, and selected command before the user applies anything.
21
+ - User choice is explicit. Marrow logs whether the recommendation was accepted or overridden, but it does not silently switch modes.
22
+ - Policy profiles are supported by the backend/SDK/MCP so businesses can define rules like local=passive, staging=pilot, production deploys=enforce.
23
+
24
+ Example recommendation:
25
+
26
+ ```text
27
+ Recommended mode: pilot
28
+ Reason:
29
+ - Node project detected
30
+ - Edge service detected
31
+ - GitHub workflow detected
32
+ - No owner approval policy configured yet
33
+ ```
34
+
14
35
  ## What's New in v0.1.13
15
36
 
16
37
  v0.1.13 turns `npx @getmarrow/install govern` into an interactive terminal setup flow when run in a real TTY.
@@ -61,7 +82,7 @@ MARROW_API_KEY=mrw_live_xxx npx @getmarrow/install run --agent codex-prod --prof
61
82
  Gate a production action before the agent executes it:
62
83
 
63
84
  ```bash
64
- MARROW_API_KEY=mrw_live_xxx npx @getmarrow/install gate "deploy production worker after tests pass"
85
+ MARROW_API_KEY=mrw_live_xxx npx @getmarrow/install gate "deploy production service after tests pass"
65
86
  ```
66
87
 
67
88
  Wrap a real deploy, publish, merge, or migration command only after the agent has the required proof:
@@ -72,7 +93,7 @@ MARROW_API_KEY=mrw_live_xxx npx @getmarrow/install run \
72
93
  --type deploy \
73
94
  --profile production \
74
95
  --policy enforce \
75
- -- wrangler deploy
96
+ -- npm run deploy
76
97
  ```
77
98
 
78
99
  Use `--policy warn` for pilot mode and `--fail-open` only for non-production local workflows where Marrow should never block execution.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getmarrow/install",
3
- "version": "0.1.13",
3
+ "version": "0.1.15",
4
4
  "description": "Universal installer for Marrow passive agent setup.",
5
5
  "bin": {
6
6
  "marrow-install": "bin/marrow-install.js"
@@ -105,6 +105,71 @@ function inferSurfaces(text) {
105
105
  return [...surfaces];
106
106
  }
107
107
 
108
+ function safeJsonFile(filePath) {
109
+ try {
110
+ if (!fs.existsSync(filePath)) return null;
111
+ const parsed = JSON.parse(fs.readFileSync(filePath, 'utf8'));
112
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : null;
113
+ } catch {
114
+ return null;
115
+ }
116
+ }
117
+
118
+ function detectProjectSignals(cwd = process.cwd()) {
119
+ const packageJsonPath = path.join(cwd, 'package.json');
120
+ const packageJson = safeJsonFile(packageJsonPath);
121
+ const packageScripts = packageJson?.scripts && typeof packageJson.scripts === 'object'
122
+ ? Object.keys(packageJson.scripts)
123
+ : [];
124
+ const signals = new Set();
125
+ const frameworks = new Set();
126
+ const configFiles = [];
127
+
128
+ const addFile = (relative, signal) => {
129
+ if (fs.existsSync(path.join(cwd, relative))) {
130
+ configFiles.push(relative);
131
+ signals.add(signal);
132
+ }
133
+ };
134
+
135
+ if (packageJson) {
136
+ signals.add('package_json');
137
+ if (packageJson.dependencies?.['@cloudflare/workers-types'] || packageJson.devDependencies?.['wrangler'] || packageJson.dependencies?.['hono']) {
138
+ frameworks.add('cloudflare-workers');
139
+ }
140
+ }
141
+ addFile('wrangler.toml', 'wrangler_config');
142
+ addFile('wrangler.json', 'wrangler_config');
143
+ addFile('wrangler.jsonc', 'wrangler_config');
144
+ addFile('.github/workflows', 'github_actions');
145
+ addFile('Dockerfile', 'container');
146
+ addFile('docker-compose.yml', 'container');
147
+ addFile('terraform', 'terraform');
148
+ addFile('migrations', 'database_migrations');
149
+ addFile('prisma', 'database_migrations');
150
+ addFile('drizzle', 'database_migrations');
151
+ addFile('AGENTS.md', 'agent_instructions');
152
+ addFile('CLAUDE.md', 'agent_instructions');
153
+ addFile('.mcp.json', 'mcp_config');
154
+ addFile('.cursor', 'cursor_project');
155
+
156
+ for (const script of packageScripts) {
157
+ if (/\b(deploy|publish|release|migrate|migration|smoke|check|test)\b/i.test(script)) {
158
+ signals.add(`script:${script.toLowerCase()}`);
159
+ }
160
+ }
161
+
162
+ return {
163
+ name: packageJson?.name || path.basename(cwd),
164
+ key: packageJson?.name || path.basename(cwd),
165
+ type: packageJson ? 'node' : fs.existsSync(path.join(cwd, 'pyproject.toml')) ? 'python' : 'workspace',
166
+ frameworks: [...frameworks],
167
+ signals: [...signals],
168
+ package_scripts: packageScripts.slice(0, 30),
169
+ config_files: configFiles.slice(0, 30),
170
+ };
171
+ }
172
+
108
173
  function isRisky(text, type) {
109
174
  return HIGH_RISK_TERMS.test(`${type || ''} ${text || ''}`);
110
175
  }
@@ -280,6 +345,51 @@ async function preflightRuntime(options, action, type, commandText) {
280
345
  });
281
346
  }
282
347
 
348
+ async function recommendGovernanceMode(options, project = detectProjectSignals()) {
349
+ if (!options.apiKey) {
350
+ return {
351
+ ok: false,
352
+ skipped: true,
353
+ reason: 'MARROW_API_KEY missing',
354
+ project,
355
+ exact_fix: 'export MARROW_API_KEY=mrw_live_... && npx @getmarrow/install govern',
356
+ };
357
+ }
358
+ const action = 'configure Marrow governance mode for this project';
359
+ return requestJson(options, 'POST', '/v1/agent/mode/recommend', {
360
+ project,
361
+ workflow: {
362
+ action,
363
+ type: 'setup',
364
+ branch: process.env.GITHUB_REF_NAME || process.env.BRANCH_NAME || '',
365
+ environment: process.env.NODE_ENV || process.env.MARROW_GOVERN_PROFILE || options.profile,
366
+ },
367
+ agent: {
368
+ id: options.agentId,
369
+ role: 'setup',
370
+ },
371
+ });
372
+ }
373
+
374
+ async function recordGovernanceModeSelection(options, state) {
375
+ if (!options.apiKey || !state.recommendation?.recommended_mode) return null;
376
+ const selected = selectedGovernanceMode(state.modes[state.modeIndex]);
377
+ return requestJson(options, 'POST', '/v1/agent/mode/recommend', {
378
+ project: state.project,
379
+ workflow: {
380
+ action: 'selected Marrow governance mode for this project',
381
+ type: 'setup',
382
+ environment: process.env.NODE_ENV || process.env.MARROW_GOVERN_PROFILE || options.profile,
383
+ },
384
+ agent: {
385
+ id: options.agentId,
386
+ role: 'setup',
387
+ },
388
+ selected_mode: selected,
389
+ selection_source: selected === state.recommendation.recommended_mode ? 'accepted' : 'overridden',
390
+ });
391
+ }
392
+
283
393
  function gateDecision(runtime) {
284
394
  const gate = runtime?.risk_gate || {};
285
395
  const receipt = runtime?.gate_receipt || {};
@@ -473,6 +583,7 @@ function detectHarnesses(cwd = process.cwd()) {
473
583
 
474
584
  function governPanel(options) {
475
585
  const rows = detectHarnesses();
586
+ const project = detectProjectSignals();
476
587
  const agentId = displayText(options.agentId, 80);
477
588
  const profile = displayText(options.profile, 80);
478
589
  const policy = displayText(options.policy, 24);
@@ -488,6 +599,13 @@ function governPanel(options) {
488
599
  'Detected harnesses:',
489
600
  ...rows.map((row, index) => ` ${index + 1}. ${row.detected ? '[x]' : '[ ]'} ${row.name} ${row.command}`),
490
601
  '',
602
+ 'Detected project signals:',
603
+ ` project=${project.name} type=${project.type}`,
604
+ ` signals=${project.signals.length ? project.signals.join(', ') : 'none'}`,
605
+ options.apiKey
606
+ ? ' recommendation: run interactive TUI or use --json status for live mode recommendation.'
607
+ : ' recommendation: export MARROW_API_KEY to get an account/fleet-backed mode recommendation.',
608
+ '',
491
609
  'Recommended first commands:',
492
610
  ` npx @getmarrow/install run --agent ${shellQuoteDisplay(options.agentId)} --profile production --policy enforce -- codex`,
493
611
  ` npx @getmarrow/install run --agent deploy-agent --type deploy --policy enforce -- wrangler deploy`,
@@ -521,6 +639,13 @@ function governModes() {
521
639
  ];
522
640
  }
523
641
 
642
+ function selectedGovernanceMode(mode) {
643
+ if (!mode) return 'pilot';
644
+ if (mode.id === 'passive') return 'passive';
645
+ if (mode.id === 'enforce') return 'enforce';
646
+ return 'pilot';
647
+ }
648
+
524
649
  function commandForSelection(state, options) {
525
650
  const harness = state.harnesses[state.harnessIndex] || state.harnesses[0];
526
651
  const mode = state.modes[state.modeIndex] || state.modes[0];
@@ -535,12 +660,15 @@ function commandForSelection(state, options) {
535
660
  function buildGovernState(options, cwd = process.cwd()) {
536
661
  const harnesses = detectHarnesses(cwd);
537
662
  const firstDetected = harnesses.findIndex((harness) => harness.detected);
663
+ const project = detectProjectSignals(cwd);
538
664
  return {
539
665
  cursor: 0,
540
666
  harnesses,
541
667
  harnessIndex: firstDetected >= 0 ? firstDetected : 0,
542
668
  modes: governModes(),
543
669
  modeIndex: 0,
670
+ project,
671
+ recommendation: null,
544
672
  status: '',
545
673
  lastResult: '',
546
674
  confirmingSetup: false,
@@ -579,8 +707,12 @@ function renderGovernTui(state, options) {
579
707
  },
580
708
  {
581
709
  label: 'Mode',
582
- value: mode.label,
583
- hint: mode.description,
710
+ value: state.recommendation?.recommended_mode
711
+ ? `${mode.label} recommended: ${state.recommendation.recommended_mode}`
712
+ : mode.label,
713
+ hint: state.recommendation?.reasons?.length
714
+ ? state.recommendation.reasons.slice(0, 2).join('; ')
715
+ : mode.description,
584
716
  },
585
717
  {
586
718
  label: 'Run passive setup + self-test',
@@ -616,6 +748,7 @@ function renderGovernTui(state, options) {
616
748
  '+------------------------------------------------------------+',
617
749
  '',
618
750
  `Agent: ${displayText(options.agentId, 36)} Profile: ${displayText(options.profile, 24)} API key: ${options.apiKey ? 'present' : 'missing'}`,
751
+ `Project: ${displayText(state.project?.name || 'workspace', 36)} Signals: ${displayText((state.project?.signals || []).slice(0, 4).join(', ') || 'none', 52)}`,
619
752
  '',
620
753
  'Navigation: Up/Down move Left/Right change Enter select',
621
754
  'Exit: q, Esc, or Ctrl+C',
@@ -634,6 +767,12 @@ function renderGovernTui(state, options) {
634
767
  lines.push('');
635
768
  lines.push(displayText(state.lastResult, 500));
636
769
  }
770
+ if (state.recommendation?.recommended_mode) {
771
+ lines.push('');
772
+ lines.push(`Recommended mode: ${state.recommendation.recommended_mode} confidence=${Math.round((state.recommendation.confidence || 0) * 100)}%`);
773
+ for (const reason of (state.recommendation.reasons || []).slice(0, 5)) lines.push(`- ${displayText(reason, 110)}`);
774
+ lines.push('Apply by selecting Mode or printing the command; Marrow does not auto-switch modes silently.');
775
+ }
637
776
  return lines.join('\n');
638
777
  }
639
778
 
@@ -703,6 +842,20 @@ async function runGovernInteractive(options, input = process.stdin, output = pro
703
842
  }
704
843
 
705
844
  const state = buildGovernState(options);
845
+ try {
846
+ const recommendation = await recommendGovernanceMode(options, state.project);
847
+ if (recommendation?.recommended_mode) {
848
+ state.recommendation = recommendation;
849
+ const recommendationModeId = recommendation.recommended_mode === 'pilot' ? 'warn' : recommendation.recommended_mode;
850
+ const modeIndex = state.modes.findIndex((mode) => mode.id === recommendationModeId);
851
+ if (modeIndex >= 0) state.modeIndex = modeIndex;
852
+ state.status = 'Governance recommendation loaded. Review before applying.';
853
+ } else if (recommendation?.skipped) {
854
+ state.status = `${recommendation.reason}. ${recommendation.exact_fix}`;
855
+ }
856
+ } catch (error) {
857
+ state.status = `Recommendation unavailable: ${error instanceof Error ? error.message : String(error)}`;
858
+ }
706
859
  readline.emitKeypressEvents(input);
707
860
  input.setRawMode(true);
708
861
  output.write('\x1b[?25l');
@@ -782,6 +935,7 @@ async function runGovernInteractive(options, input = process.stdin, output = pro
782
935
  state.status = 'Gate check complete.';
783
936
  state.confirmingSetup = false;
784
937
  } else if (state.cursor === 5) {
938
+ await recordGovernanceModeSelection(options, state).catch(() => null);
785
939
  cleanup();
786
940
  output.write(`\n${commandForSelection(state, options)}\n`);
787
941
  resolve();
@@ -847,6 +1001,9 @@ module.exports = {
847
1001
  inferSurfaces,
848
1002
  commandForSelection,
849
1003
  buildGovernState,
1004
+ detectProjectSignals,
1005
+ recommendGovernanceMode,
1006
+ recordGovernanceModeSelection,
850
1007
  gateDecision,
851
1008
  shouldBlock,
852
1009
  governPanel,