@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 +23 -2
- package/package.json +1 -1
- package/src/governed-runner.js +159 -2
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
|
|
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
|
-
--
|
|
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
package/src/governed-runner.js
CHANGED
|
@@ -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:
|
|
583
|
-
|
|
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,
|