@maestrofrontier/frontier 1.4.5 → 1.5.0
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/.agents/plugins/marketplace.json +21 -21
- package/.codex-plugin/plugin.json +29 -29
- package/.cursorrules +197 -194
- package/AGENTS.md +3 -3
- package/README.md +368 -368
- package/bin/maestro.cjs +75 -75
- package/commands/compress.md +36 -36
- package/commands/frontier.md +124 -124
- package/commands/terse.md +23 -23
- package/docs/codex.md +167 -167
- package/docs/orchestration.md +168 -168
- package/frontier/cli.cjs +279 -252
- package/frontier/config.cjs +468 -468
- package/frontier/dispatch.cjs +267 -255
- package/frontier/judge.cjs +92 -92
- package/frontier/run.cjs +201 -180
- package/frontier/schema.cjs +112 -112
- package/frontier/semaphore.cjs +49 -49
- package/frontier/synthesize.cjs +79 -79
- package/hooks/frontier-autorun.cjs +127 -120
- package/hooks/hooks.json +103 -103
- package/hooks/maestro-doctrine-guard.cjs +81 -81
- package/hooks/maestro-gate-reminder.cjs +22 -7
- package/hooks/maestro-gate-telemetry.cjs +79 -77
- package/hooks/maestro-phase-scope.cjs +118 -118
- package/hooks/maestro-statusline-sync.cjs +152 -152
- package/hooks/maestro-subagent-guard.cjs +148 -148
- package/hooks/maestro-terse-mode.cjs +189 -189
- package/hooks/maestro-toolbudget-advisory.cjs +127 -127
- package/integrations/README.md +111 -111
- package/integrations/cline/skills/frontier/SKILL.md +75 -75
- package/integrations/codex/prompts/frontier.md +70 -70
- package/integrations/codex/prompts/update.md +39 -39
- package/integrations/codex/skills/maestro-frontier/SKILL.md +122 -122
- package/integrations/codex/skills/maestro-settings/SKILL.md +55 -55
- package/integrations/codex/skills/maestro-terse/SKILL.md +58 -58
- package/integrations/codex/skills/maestro-update/SKILL.md +31 -31
- package/integrations/cursor/commands/frontier.md +63 -63
- package/integrations/cursor/commands/update.md +34 -34
- package/integrations/gemini/commands/frontier.toml +76 -76
- package/integrations/windsurf/workflows/frontier.md +70 -70
- package/package.json +58 -58
- package/scripts/install.cjs +1014 -1014
- package/settings/cli.cjs +140 -140
- package/settings/config.cjs +309 -309
- package/skills/maestro-frontier/SKILL.md +122 -122
- package/skills/maestro-settings/SKILL.md +55 -55
- package/skills/maestro-terse/SKILL.md +58 -58
- package/skills/maestro-update/SKILL.md +31 -31
- package/skills/terse/SKILL.md +74 -74
package/scripts/install.cjs
CHANGED
|
@@ -1,1014 +1,1014 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// Maestro installer — writes doctrine + engine + tool wrapper into a target
|
|
3
|
-
// project. Append-only for AGENTS.md, no-clobber for wrapper files, safe to
|
|
4
|
-
// re-run. Zero dependencies (Node stdlib only). CommonJS (.cjs).
|
|
5
|
-
//
|
|
6
|
-
// Usage (as module): const { run } = require('./install.cjs'); run(argv);
|
|
7
|
-
// Usage (as script): node scripts/install.cjs [flags]
|
|
8
|
-
|
|
9
|
-
'use strict';
|
|
10
|
-
|
|
11
|
-
const fs = require('fs');
|
|
12
|
-
const os = require('os');
|
|
13
|
-
const path = require('path');
|
|
14
|
-
const crypto = require('crypto');
|
|
15
|
-
|
|
16
|
-
// ---- constants ----
|
|
17
|
-
|
|
18
|
-
const PKG_ROOT = path.join(__dirname, '..');
|
|
19
|
-
const SENTINEL = '<!-- maestro:begin -->';
|
|
20
|
-
const SENTINEL_END = '<!-- maestro:end -->';
|
|
21
|
-
|
|
22
|
-
// Map target -> { templateSrc, projectDest, userDest }
|
|
23
|
-
// templateSrc is relative to PKG_ROOT.
|
|
24
|
-
// userDest null means no global install path for this target.
|
|
25
|
-
const WRAPPER_MAP = {
|
|
26
|
-
codex: {
|
|
27
|
-
src: 'integrations/codex/prompts/frontier.md',
|
|
28
|
-
proj: '.codex/prompts/frontier.md',
|
|
29
|
-
user: () => path.join(homeDir(), '.codex', 'prompts', 'frontier.md'),
|
|
30
|
-
},
|
|
31
|
-
cursor: {
|
|
32
|
-
src: 'integrations/cursor/commands/frontier.md',
|
|
33
|
-
proj: '.cursor/commands/frontier.md',
|
|
34
|
-
user: null, // no global path for cursor
|
|
35
|
-
},
|
|
36
|
-
gemini: {
|
|
37
|
-
src: 'integrations/gemini/commands/frontier.toml',
|
|
38
|
-
proj: '.gemini/commands/frontier.toml',
|
|
39
|
-
user: () => path.join(homeDir(), '.gemini', 'commands', 'frontier.toml'),
|
|
40
|
-
},
|
|
41
|
-
cline: {
|
|
42
|
-
src: 'integrations/cline/skills/frontier/SKILL.md',
|
|
43
|
-
proj: '.cline/skills/frontier/SKILL.md',
|
|
44
|
-
user: () => path.join(homeDir(), '.cline', 'skills', 'frontier', 'SKILL.md'),
|
|
45
|
-
},
|
|
46
|
-
windsurf: {
|
|
47
|
-
src: 'integrations/windsurf/workflows/frontier.md',
|
|
48
|
-
proj: '.windsurf/workflows/frontier.md',
|
|
49
|
-
user: () => path.join(homeDir(), '.codeium', 'windsurf', 'global_workflows', 'frontier.md'),
|
|
50
|
-
},
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
// Codex skill templates installed alongside the deprecated codex wrapper.
|
|
54
|
-
// Codex loads skills from <project>/.agents/skills/<name>/SKILL.md (project)
|
|
55
|
-
// or ~/.agents/skills/<name>/SKILL.md (global). Maestro-owned skills are
|
|
56
|
-
// refreshed when still managed; user-edited copies are preserved.
|
|
57
|
-
const CODEX_SKILLS = [
|
|
58
|
-
{ name: 'maestro-frontier', legacy: 'frontier' },
|
|
59
|
-
{ name: 'maestro-terse', legacy: 'terse' },
|
|
60
|
-
{ name: 'maestro-settings', legacy: 'settings' },
|
|
61
|
-
{ name: 'maestro-update', legacy: 'update' },
|
|
62
|
-
];
|
|
63
|
-
|
|
64
|
-
const LEGACY_CODEX_SKILL_TEMPLATES = {
|
|
65
|
-
frontier: `---
|
|
66
|
-
name: frontier
|
|
67
|
-
description: Maestro Frontier local multi-CLI fusion engine — switch mode, or run a prompt through the panel
|
|
68
|
-
---
|
|
69
|
-
|
|
70
|
-
Drive the **Maestro Frontier** engine — a zero-dependency local multi-CLI fusion
|
|
71
|
-
engine (a parallel panel of local CLIs → a judge model's analysis → a grounded
|
|
72
|
-
synthesis). It is the same engine the Claude Code plugin ships; here it runs
|
|
73
|
-
through the \`maestro\` CLI with \`--scope codex\`.
|
|
74
|
-
|
|
75
|
-
**This is a typing shortcut, not a prompt hook.** Codex has no automatic
|
|
76
|
-
prompt hook, so arming a mode does **not** auto-run the engine on later prompts —
|
|
77
|
-
it only persists the mode. To actually fuse a prompt, invoke \`run\` explicitly
|
|
78
|
-
(step 3).
|
|
79
|
-
|
|
80
|
-
Map the user's request to one engine CLI call and run it from the repo root.
|
|
81
|
-
Do not edit the engine's state file by hand.
|
|
82
|
-
|
|
83
|
-
## 1. Switch mode
|
|
84
|
-
|
|
85
|
-
Persists to \`~/.config/maestro/frontier-state.codex.json\`; default \`off\`.
|
|
86
|
-
\`--scope codex\` keeps Codex's armed mode independent from Claude Code, Cline,
|
|
87
|
-
Cursor, and Gemini on the same machine:
|
|
88
|
-
|
|
89
|
-
\`\`\`bash
|
|
90
|
-
maestro frontier mode off --scope codex
|
|
91
|
-
maestro frontier mode single --model <model> --scope codex
|
|
92
|
-
maestro frontier mode fusion --preset <preset> --scope codex
|
|
93
|
-
maestro frontier mode fusion --preset custom --models <a,b,c> --scope codex
|
|
94
|
-
maestro frontier mode fusion --preset <preset> --judge <model> --synth <model> --scope codex
|
|
95
|
-
\`\`\`
|
|
96
|
-
|
|
97
|
-
Models: \`opus\` (Claude Opus 4.8, needs \`claude\`), \`gpt-5.5\` (needs \`codex\`),
|
|
98
|
-
\`gemini\` (needs \`gemini\`). Presets: \`opus-duo\`, \`opus-gpt\`, \`gpt-duo\`,
|
|
99
|
-
\`frontier-trio\`, \`custom\`. Judge + synth default to Opus; \`--judge\`/\`--synth\`
|
|
100
|
-
override for any preset (e.g. \`--judge opus --synth gpt-5.5\`). \`gpt-duo\` runs
|
|
101
|
-
judge + synth on GPT-5.5 — a Codex-only fusion that needs no \`claude\`.
|
|
102
|
-
|
|
103
|
-
## 2. Show current mode/preset
|
|
104
|
-
|
|
105
|
-
\`\`\`bash
|
|
106
|
-
maestro frontier status --scope codex
|
|
107
|
-
\`\`\`
|
|
108
|
-
|
|
109
|
-
## 3. Run a prompt through the current mode
|
|
110
|
-
|
|
111
|
-
This is the action that actually fuses, since nothing auto-runs here. Set a
|
|
112
|
-
mode first (step 1), then:
|
|
113
|
-
|
|
114
|
-
\`\`\`bash
|
|
115
|
-
maestro frontier run "<prompt>" --scope codex
|
|
116
|
-
\`\`\`
|
|
117
|
-
|
|
118
|
-
- \`off\`: prints a notice, spawns nothing.
|
|
119
|
-
- \`single\`: dispatches the one selected CLI, prints its answer.
|
|
120
|
-
- \`fusion\`: runs the panel in parallel → judge → synthesizer; prints the final
|
|
121
|
-
answer (a one-line run meta goes to stderr). Report stdout verbatim.
|
|
122
|
-
|
|
123
|
-
On error the engine prints \`ERROR [<reason>]: <detail>\` to stderr and exits
|
|
124
|
-
non-zero — relay the reason.
|
|
125
|
-
|
|
126
|
-
## Indicator instruction
|
|
127
|
-
|
|
128
|
-
<!-- INDICATOR INSTRUCTION — apply every turn while this skill is active -->
|
|
129
|
-
|
|
130
|
-
Before composing your first reply each turn, run:
|
|
131
|
-
|
|
132
|
-
\`\`\`bash
|
|
133
|
-
maestro frontier status --scope codex
|
|
134
|
-
\`\`\`
|
|
135
|
-
|
|
136
|
-
Parse the returned JSON \`mode\` field:
|
|
137
|
-
|
|
138
|
-
- If \`mode\` is not \`off\`, make the **first line** of your reply exactly:
|
|
139
|
-
\`Maestro Frontier ON (<label>)\`
|
|
140
|
-
where \`<label>\` is formed as follows:
|
|
141
|
-
- single mode → \`single · <model>\` (e.g. \`single · opus\`)
|
|
142
|
-
- fusion mode → \`fusion · <preset>\` (e.g. \`fusion · frontier-trio\`);
|
|
143
|
-
for a custom preset use \`fusion · custom (<model1>, <model2>, ...)\`
|
|
144
|
-
- If \`mode\` is \`off\`, output no indicator line.
|
|
145
|
-
|
|
146
|
-
<!-- END INDICATOR INSTRUCTION -->
|
|
147
|
-
|
|
148
|
-
## Notes
|
|
149
|
-
|
|
150
|
-
- Real \`single\`/\`fusion\` runs spawn local CLIs and cost tokens; use small prompts.
|
|
151
|
-
\`off\` is free.
|
|
152
|
-
- Each model's CLI must be on \`PATH\`, or point at a specific build with
|
|
153
|
-
\`MAESTRO_CLAUDE_BIN\` / \`MAESTRO_CODEX_BIN\` / \`MAESTRO_GEMINI_BIN\`.
|
|
154
|
-
- Requires \`maestro\` on \`PATH\` (installed during Maestro setup). If it is
|
|
155
|
-
missing, install Maestro first.
|
|
156
|
-
`,
|
|
157
|
-
terse: `---
|
|
158
|
-
name: terse
|
|
159
|
-
description: Toggle Maestro terse output level (lite, full, ultra, off) via the settings CLI
|
|
160
|
-
---
|
|
161
|
-
|
|
162
|
-
Toggle the **Maestro terse** output level for this environment. Terse mode
|
|
163
|
-
condenses agent replies; levels range from \`off\` (default verbosity) through
|
|
164
|
-
\`lite\`, \`full\`, and \`ultra\` (most compressed).
|
|
165
|
-
|
|
166
|
-
When the user invokes this skill, run the settings CLI to read or change the
|
|
167
|
-
terse level. Do not edit settings files by hand.
|
|
168
|
-
|
|
169
|
-
## Check current terse level
|
|
170
|
-
|
|
171
|
-
\`\`\`bash
|
|
172
|
-
node settings/cli.cjs --help
|
|
173
|
-
\`\`\`
|
|
174
|
-
|
|
175
|
-
Consult the help output for the exact read subcommand, then run it. If
|
|
176
|
-
\`settings/cli.cjs\` is not present, run \`maestro --help\` to discover the
|
|
177
|
-
correct path.
|
|
178
|
-
|
|
179
|
-
## Set terse level
|
|
180
|
-
|
|
181
|
-
\`\`\`bash
|
|
182
|
-
node settings/cli.cjs terse <level>
|
|
183
|
-
\`\`\`
|
|
184
|
-
|
|
185
|
-
Valid levels: \`off\` | \`lite\` | \`full\` | \`ultra\`
|
|
186
|
-
|
|
187
|
-
Examples:
|
|
188
|
-
|
|
189
|
-
\`\`\`bash
|
|
190
|
-
node settings/cli.cjs terse off
|
|
191
|
-
node settings/cli.cjs terse lite
|
|
192
|
-
node settings/cli.cjs terse full
|
|
193
|
-
node settings/cli.cjs terse ultra
|
|
194
|
-
\`\`\`
|
|
195
|
-
|
|
196
|
-
If the CLI rejects an argument or the subcommand name differs, run
|
|
197
|
-
\`node settings/cli.cjs --help\` first and follow the printed usage.
|
|
198
|
-
|
|
199
|
-
## Notes
|
|
200
|
-
|
|
201
|
-
- The change persists in Maestro's settings store; it applies to subsequent
|
|
202
|
-
agent turns in this project.
|
|
203
|
-
- Requires \`node\` on \`PATH\` and Maestro installed in the project root. If
|
|
204
|
-
\`settings/cli.cjs\` is missing, re-run the Maestro installer:
|
|
205
|
-
\`npx github:mbanderas/maestro install --target codex\`
|
|
206
|
-
`,
|
|
207
|
-
settings: `---
|
|
208
|
-
name: settings
|
|
209
|
-
description: View and change Maestro toggles (terse, frontier, context-bar) via the settings CLI
|
|
210
|
-
---
|
|
211
|
-
|
|
212
|
-
View or change **Maestro settings** for this project. The settings CLI manages
|
|
213
|
-
the three primary toggles: \`terse\`, \`frontier\`, and \`context-bar\`.
|
|
214
|
-
|
|
215
|
-
When the user invokes this skill, run the settings CLI from the repo root.
|
|
216
|
-
Do not edit settings files by hand.
|
|
217
|
-
|
|
218
|
-
## Discover available commands
|
|
219
|
-
|
|
220
|
-
\`\`\`bash
|
|
221
|
-
node settings/cli.cjs --help
|
|
222
|
-
\`\`\`
|
|
223
|
-
|
|
224
|
-
If \`settings/cli.cjs\` is not present, run \`maestro --help\` to locate the
|
|
225
|
-
correct entry point.
|
|
226
|
-
|
|
227
|
-
## Common operations
|
|
228
|
-
|
|
229
|
-
List current settings:
|
|
230
|
-
|
|
231
|
-
\`\`\`bash
|
|
232
|
-
node settings/cli.cjs
|
|
233
|
-
\`\`\`
|
|
234
|
-
|
|
235
|
-
Set a toggle:
|
|
236
|
-
|
|
237
|
-
\`\`\`bash
|
|
238
|
-
node settings/cli.cjs terse <off|lite|full|ultra>
|
|
239
|
-
node settings/cli.cjs frontier <off|single|fusion>
|
|
240
|
-
node settings/cli.cjs context-bar <on|off>
|
|
241
|
-
\`\`\`
|
|
242
|
-
|
|
243
|
-
If a subcommand name or argument differs from the above, follow the usage
|
|
244
|
-
printed by \`--help\` — do not guess flags.
|
|
245
|
-
|
|
246
|
-
## Notes
|
|
247
|
-
|
|
248
|
-
- Changes persist in Maestro's settings store and apply to subsequent agent
|
|
249
|
-
turns in this project.
|
|
250
|
-
- Requires \`node\` on \`PATH\` and Maestro installed in the project root. If
|
|
251
|
-
\`settings/cli.cjs\` is missing, re-run the installer:
|
|
252
|
-
\`npx github:mbanderas/maestro install --target codex\`
|
|
253
|
-
`,
|
|
254
|
-
update: `---
|
|
255
|
-
name: update
|
|
256
|
-
description: Update Maestro to the latest version by re-running the installer for Codex
|
|
257
|
-
---
|
|
258
|
-
|
|
259
|
-
Update **Maestro** to the latest marketplace code. This re-runs the installer,
|
|
260
|
-
which pulls the current release and overwrites the local Maestro files in place.
|
|
261
|
-
|
|
262
|
-
When the user invokes this skill, run the installer from the repo root:
|
|
263
|
-
|
|
264
|
-
\`\`\`bash
|
|
265
|
-
npx github:mbanderas/maestro install --target codex
|
|
266
|
-
\`\`\`
|
|
267
|
-
|
|
268
|
-
The installer is idempotent — it is safe to re-run against an existing
|
|
269
|
-
installation. It will:
|
|
270
|
-
|
|
271
|
-
- Pull the latest Maestro source from the repository.
|
|
272
|
-
- Overwrite skills, hooks, and settings scaffolding with the new versions.
|
|
273
|
-
- Leave project-local configuration (state files, secrets) untouched.
|
|
274
|
-
|
|
275
|
-
## Notes
|
|
276
|
-
|
|
277
|
-
- Requires \`node\` and \`npx\` on \`PATH\`.
|
|
278
|
-
- Run from the project root so the installer targets the correct directory.
|
|
279
|
-
- After the installer completes, restart the Codex session (or reload the
|
|
280
|
-
project) so updated skills and hooks take effect.
|
|
281
|
-
- If \`npx\` is unavailable, clone \`https://github.com/mbanderas/maestro\`
|
|
282
|
-
manually and follow the repository's install instructions.
|
|
283
|
-
`,
|
|
284
|
-
};
|
|
285
|
-
|
|
286
|
-
// Runtime adapter per target. The adapter imports @AGENTS.md (Cursor has no
|
|
287
|
-
// imports, so .cursorrules embeds the kernel). codex/cline/windsurf read
|
|
288
|
-
// AGENTS.md directly and need no adapter.
|
|
289
|
-
const ADAPTER_MAP = {
|
|
290
|
-
claude: 'CLAUDE.md',
|
|
291
|
-
gemini: 'GEMINI.md',
|
|
292
|
-
cursor: '.cursorrules',
|
|
293
|
-
};
|
|
294
|
-
|
|
295
|
-
// Marker dirs used for auto-detection (scanned inside project root)
|
|
296
|
-
const AUTO_MARKERS = [
|
|
297
|
-
{ dir: '.cursor', target: 'cursor' },
|
|
298
|
-
{ dir: '.gemini', target: 'gemini' },
|
|
299
|
-
{ dir: '.codex', target: 'codex' },
|
|
300
|
-
{ dir: '.cline', target: 'cline' },
|
|
301
|
-
{ dir: '.windsurf',target: 'windsurf' },
|
|
302
|
-
{ dir: '.claude', target: 'claude' },
|
|
303
|
-
];
|
|
304
|
-
|
|
305
|
-
// ---- safety helpers ----
|
|
306
|
-
|
|
307
|
-
/**
|
|
308
|
-
* Returns true if p is a symlink (lstat-based). Never throws.
|
|
309
|
-
* @param {string} p
|
|
310
|
-
* @returns {boolean}
|
|
311
|
-
*/
|
|
312
|
-
function isSymlink(p) {
|
|
313
|
-
try {
|
|
314
|
-
return fs.lstatSync(p).isSymbolicLink();
|
|
315
|
-
} catch {
|
|
316
|
-
return false;
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
/**
|
|
321
|
-
* Create directories for destPath. Refuses to create through a symlinked
|
|
322
|
-
* ancestor directory. Returns true on success, false on refusal.
|
|
323
|
-
* @param {string} destPath
|
|
324
|
-
* @returns {boolean}
|
|
325
|
-
*/
|
|
326
|
-
function safeMkdirp(destPath) {
|
|
327
|
-
const dir = path.dirname(destPath);
|
|
328
|
-
// Walk ancestors from PKG_ROOT outward — only check the leaf dir because
|
|
329
|
-
// we cannot reliably validate every ancestor on all OSes; the write will
|
|
330
|
-
// fail safely if anything is wrong.
|
|
331
|
-
try {
|
|
332
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
333
|
-
return true;
|
|
334
|
-
} catch {
|
|
335
|
-
return false;
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
/**
|
|
340
|
-
* Write buf to dest. Refuses if dest (or its parent dir) is a symlink.
|
|
341
|
-
* Returns { ok: true } or { ok: false, reason: string }.
|
|
342
|
-
* @param {string} dest
|
|
343
|
-
* @param {string|Buffer} content
|
|
344
|
-
* @returns {{ ok: boolean, reason?: string }}
|
|
345
|
-
*/
|
|
346
|
-
function safeWrite(dest, content) {
|
|
347
|
-
// Check parent dir
|
|
348
|
-
const dir = path.dirname(dest);
|
|
349
|
-
if (isSymlink(dir)) {
|
|
350
|
-
return { ok: false, reason: `parent dir is a symlink: ${dir}` };
|
|
351
|
-
}
|
|
352
|
-
// Check destination itself
|
|
353
|
-
if (isSymlink(dest)) {
|
|
354
|
-
return { ok: false, reason: `destination is a symlink: ${dest}` };
|
|
355
|
-
}
|
|
356
|
-
try {
|
|
357
|
-
fs.writeFileSync(dest, content, 'utf8');
|
|
358
|
-
return { ok: true };
|
|
359
|
-
} catch (err) {
|
|
360
|
-
return { ok: false, reason: String(err.message || err) };
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
function homeDir() {
|
|
365
|
-
return process.platform === 'win32'
|
|
366
|
-
? (process.env.USERPROFILE || os.homedir())
|
|
367
|
-
: (process.env.HOME || os.homedir());
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
function sha256(content) {
|
|
371
|
-
return crypto.createHash('sha256').update(content, 'utf8').digest('hex');
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
function codexSkillManagedContent(name, srcContent) {
|
|
375
|
-
const body = srcContent.trimEnd() + '\n';
|
|
376
|
-
return `${body}\n<!-- maestro-managed:codex-skill name=${name} sha256=${sha256(body)} -->\n`;
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
function splitCodexSkillMarker(content) {
|
|
380
|
-
const marker = /\n?<!-- maestro-managed:codex-skill name=([^\s]+) sha256=([a-f0-9]+|0000) -->\s*$/i.exec(content);
|
|
381
|
-
if (!marker) return null;
|
|
382
|
-
return {
|
|
383
|
-
name: marker[1],
|
|
384
|
-
hash: marker[2].toLowerCase(),
|
|
385
|
-
body: content.slice(0, marker.index).trimEnd() + '\n',
|
|
386
|
-
};
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
function isManagedCodexSkillContent(content, expectedName, managedBodies) {
|
|
390
|
-
const marker = splitCodexSkillMarker(content);
|
|
391
|
-
if (marker && marker.name === expectedName) {
|
|
392
|
-
return marker.hash === '0000' || marker.hash === sha256(marker.body);
|
|
393
|
-
}
|
|
394
|
-
if (content.includes(`maestro-managed:codex-skill name=${expectedName} sha256=0000`)) {
|
|
395
|
-
return true;
|
|
396
|
-
}
|
|
397
|
-
return managedBodies.some((body) => content.trimEnd() === body.trimEnd());
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
function legacyCodexSkillContent(legacyName, namespacedName) {
|
|
401
|
-
const body = `---\nname: ${legacyName}\ndescription: Legacy Maestro compatibility skill for ${namespacedName}\n---\n\nThis legacy Maestro skill has moved to \`${namespacedName}\`.\n\nUse the \`${namespacedName}\` skill for current Maestro behavior. This compatibility skill is kept only for existing Codex installs that still reference \`${legacyName}\`.\n`;
|
|
402
|
-
return codexSkillManagedContent(legacyName, body);
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
function legacyGenericCodexTemplate(srcContent, legacyName, namespacedName) {
|
|
406
|
-
return srcContent.replace(
|
|
407
|
-
new RegExp(`(^---\\r?\\nname: )${namespacedName}(\\r?\\n)`, 'm'),
|
|
408
|
-
`$1${legacyName}$2`
|
|
409
|
-
);
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
// ---- parse argv ----
|
|
413
|
-
|
|
414
|
-
/**
|
|
415
|
-
* @param {string[]} argv
|
|
416
|
-
* @returns {{ target: string, project: string, user: boolean, dryRun: boolean, noHooks: boolean }}
|
|
417
|
-
*/
|
|
418
|
-
function parseArgs(argv) {
|
|
419
|
-
const opts = {
|
|
420
|
-
target: 'auto',
|
|
421
|
-
project: process.cwd(),
|
|
422
|
-
user: false,
|
|
423
|
-
dryRun: false,
|
|
424
|
-
noHooks: false,
|
|
425
|
-
};
|
|
426
|
-
|
|
427
|
-
let i = 0;
|
|
428
|
-
while (i < argv.length) {
|
|
429
|
-
const a = argv[i];
|
|
430
|
-
if (a === '--target' && i + 1 < argv.length) {
|
|
431
|
-
opts.target = argv[++i];
|
|
432
|
-
} else if (a === '--project' && i + 1 < argv.length) {
|
|
433
|
-
opts.project = argv[++i];
|
|
434
|
-
} else if (a === '--user') {
|
|
435
|
-
opts.user = true;
|
|
436
|
-
} else if (a === '--dry-run') {
|
|
437
|
-
opts.dryRun = true;
|
|
438
|
-
} else if (a === '--no-hooks') {
|
|
439
|
-
opts.noHooks = true;
|
|
440
|
-
}
|
|
441
|
-
i++;
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
opts.project = path.resolve(opts.project);
|
|
445
|
-
return opts;
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
// ---- auto-detect ----
|
|
449
|
-
|
|
450
|
-
/**
|
|
451
|
-
* Detect which tool is in use by looking for marker dirs.
|
|
452
|
-
* @param {string} projectRoot
|
|
453
|
-
* @returns {string} detected target or 'none'
|
|
454
|
-
*/
|
|
455
|
-
function detectTarget(projectRoot) {
|
|
456
|
-
for (const { dir, target } of AUTO_MARKERS) {
|
|
457
|
-
try {
|
|
458
|
-
const p = path.join(projectRoot, dir);
|
|
459
|
-
const st = fs.lstatSync(p);
|
|
460
|
-
if (st.isDirectory()) return target;
|
|
461
|
-
} catch {
|
|
462
|
-
// not found
|
|
463
|
-
}
|
|
464
|
-
}
|
|
465
|
-
return 'none';
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
// ---- install actions ----
|
|
469
|
-
|
|
470
|
-
/**
|
|
471
|
-
* Read a file from the package root. Returns string, or null (logs) on error.
|
|
472
|
-
* @param {string} rel
|
|
473
|
-
* @param {(msg: string) => void} log
|
|
474
|
-
* @returns {string|null}
|
|
475
|
-
*/
|
|
476
|
-
function readPkgFile(rel, log) {
|
|
477
|
-
try {
|
|
478
|
-
return fs.readFileSync(path.join(PKG_ROOT, rel), 'utf8');
|
|
479
|
-
} catch (err) {
|
|
480
|
-
log(`ERROR: cannot read package ${rel}: ${err.message}`);
|
|
481
|
-
return null;
|
|
482
|
-
}
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
/**
|
|
486
|
-
* Install a doctrine/adapter markdown file. Append-only, idempotent, never
|
|
487
|
-
* clobbers user content above the maestro block; refuses symlinks.
|
|
488
|
-
* @param {string} dest absolute destination path
|
|
489
|
-
* @param {string} srcContent content to install
|
|
490
|
-
* @param {string} label short name for logs (e.g. "AGENTS.md")
|
|
491
|
-
* @param {boolean} dryRun
|
|
492
|
-
* @param {(msg: string) => void} log
|
|
493
|
-
* @returns {boolean} true = success (or no-op), false = error
|
|
494
|
-
*/
|
|
495
|
-
function appendOnlyDoctrine(dest, srcContent, label, dryRun, log) {
|
|
496
|
-
const block = `\n${SENTINEL}\n${srcContent}\n${SENTINEL_END}\n`;
|
|
497
|
-
|
|
498
|
-
let existsStat;
|
|
499
|
-
try { existsStat = fs.lstatSync(dest); } catch { existsStat = null; }
|
|
500
|
-
|
|
501
|
-
if (existsStat) {
|
|
502
|
-
if (existsStat.isSymbolicLink()) {
|
|
503
|
-
log(`ERROR: ${label} is a symlink — refusing to write through it: ${dest}`);
|
|
504
|
-
return false;
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
let existing;
|
|
508
|
-
try { existing = fs.readFileSync(dest, 'utf8'); } catch (err) {
|
|
509
|
-
log(`ERROR: cannot read existing ${label}: ${err.message}`);
|
|
510
|
-
return false;
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
if (existing.includes(SENTINEL)) {
|
|
514
|
-
log(`[doctrine] ${label} already contains sentinel — skipping`);
|
|
515
|
-
return true;
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
if (dryRun) {
|
|
519
|
-
log(`[dry-run] would append maestro doctrine to existing ${dest}`);
|
|
520
|
-
return true;
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
const res = safeWrite(dest, existing + block);
|
|
524
|
-
if (!res.ok) {
|
|
525
|
-
log(`ERROR: failed to append to ${label}: ${res.reason}`);
|
|
526
|
-
return false;
|
|
527
|
-
}
|
|
528
|
-
log(`[doctrine] appended maestro block to existing ${label}`);
|
|
529
|
-
return true;
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
// Absent — write fresh, wrapped in the sentinel so re-runs detect it.
|
|
533
|
-
if (dryRun) {
|
|
534
|
-
log(`[dry-run] would create ${dest}`);
|
|
535
|
-
return true;
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
if (!safeMkdirp(dest)) {
|
|
539
|
-
log(`ERROR: could not create parent dir for ${dest}`);
|
|
540
|
-
return false;
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
const freshContent = SENTINEL + '\n' + srcContent + '\n' + SENTINEL_END + '\n';
|
|
544
|
-
const res = safeWrite(dest, freshContent);
|
|
545
|
-
if (!res.ok) {
|
|
546
|
-
log(`ERROR: failed to write ${label}: ${res.reason}`);
|
|
547
|
-
return false;
|
|
548
|
-
}
|
|
549
|
-
log(`[doctrine] wrote ${label}`);
|
|
550
|
-
return true;
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
/**
|
|
554
|
-
* Install the portable doctrine core (AGENTS.md) into the project root.
|
|
555
|
-
* @param {string} projectRoot
|
|
556
|
-
* @param {boolean} dryRun
|
|
557
|
-
* @param {(msg: string) => void} log
|
|
558
|
-
* @returns {boolean}
|
|
559
|
-
*/
|
|
560
|
-
function installDoctrine(projectRoot, dryRun, log) {
|
|
561
|
-
const src = readPkgFile('AGENTS.md', log);
|
|
562
|
-
if (src === null) return false;
|
|
563
|
-
return appendOnlyDoctrine(path.join(projectRoot, 'AGENTS.md'), src, 'AGENTS.md', dryRun, log);
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
/**
|
|
567
|
-
* Install the runtime adapter for a target (CLAUDE.md / GEMINI.md /
|
|
568
|
-
* .cursorrules). codex/cline/windsurf read AGENTS.md directly -> no-op.
|
|
569
|
-
* @param {string} target
|
|
570
|
-
* @param {string} projectRoot
|
|
571
|
-
* @param {boolean} dryRun
|
|
572
|
-
* @param {(msg: string) => void} log
|
|
573
|
-
* @returns {boolean}
|
|
574
|
-
*/
|
|
575
|
-
function installAdapter(target, projectRoot, dryRun, log) {
|
|
576
|
-
const rel = ADAPTER_MAP[target];
|
|
577
|
-
if (!rel) return true; // no adapter for this target
|
|
578
|
-
const src = readPkgFile(rel, log);
|
|
579
|
-
if (src === null) return false;
|
|
580
|
-
return appendOnlyDoctrine(path.join(projectRoot, rel), src, rel, dryRun, log);
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
/**
|
|
584
|
-
* Recursively copy srcDir -> destDir, skipping *.test.cjs files.
|
|
585
|
-
* @param {string} srcDir
|
|
586
|
-
* @param {string} destDir
|
|
587
|
-
* @param {boolean} dryRun
|
|
588
|
-
* @param {(msg: string) => void} log
|
|
589
|
-
* @returns {boolean}
|
|
590
|
-
*/
|
|
591
|
-
function copyDirRecursive(srcDir, destDir, dryRun, log) {
|
|
592
|
-
let entries;
|
|
593
|
-
try {
|
|
594
|
-
entries = fs.readdirSync(srcDir, { withFileTypes: true });
|
|
595
|
-
} catch (err) {
|
|
596
|
-
log(`ERROR: cannot read dir ${srcDir}: ${err.message}`);
|
|
597
|
-
return false;
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
let ok = true;
|
|
601
|
-
for (const entry of entries) {
|
|
602
|
-
if (entry.isFile() && entry.name.endsWith('.test.cjs')) continue;
|
|
603
|
-
|
|
604
|
-
const src = path.join(srcDir, entry.name);
|
|
605
|
-
const dest = path.join(destDir, entry.name);
|
|
606
|
-
|
|
607
|
-
if (entry.isDirectory()) {
|
|
608
|
-
if (!copyDirRecursive(src, dest, dryRun, log)) ok = false;
|
|
609
|
-
} else if (entry.isFile()) {
|
|
610
|
-
if (dryRun) {
|
|
611
|
-
log(`[dry-run] would write ${dest}`);
|
|
612
|
-
continue;
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
if (isSymlink(dest)) {
|
|
616
|
-
log(`ERROR: destination is a symlink — refusing: ${dest}`);
|
|
617
|
-
ok = false;
|
|
618
|
-
continue;
|
|
619
|
-
}
|
|
620
|
-
if (isSymlink(path.dirname(dest))) {
|
|
621
|
-
log(`ERROR: destination parent is a symlink — refusing: ${dest}`);
|
|
622
|
-
ok = false;
|
|
623
|
-
continue;
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
try {
|
|
627
|
-
fs.mkdirSync(destDir, { recursive: true });
|
|
628
|
-
const content = fs.readFileSync(src);
|
|
629
|
-
fs.writeFileSync(dest, content);
|
|
630
|
-
log(`[engine] copied ${dest}`);
|
|
631
|
-
} catch (err) {
|
|
632
|
-
log(`ERROR: failed to copy ${src} -> ${dest}: ${err.message}`);
|
|
633
|
-
ok = false;
|
|
634
|
-
}
|
|
635
|
-
}
|
|
636
|
-
}
|
|
637
|
-
return ok;
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
/**
|
|
641
|
-
* Install engine files (frontier/ dir + settings/ dir + bin/maestro.cjs).
|
|
642
|
-
* @param {string} projectRoot
|
|
643
|
-
* @param {boolean} dryRun
|
|
644
|
-
* @param {(msg: string) => void} log
|
|
645
|
-
* @returns {boolean}
|
|
646
|
-
*/
|
|
647
|
-
function installEngine(projectRoot, dryRun, log) {
|
|
648
|
-
const srcFrontier = path.join(PKG_ROOT, 'frontier');
|
|
649
|
-
const destFrontier = path.join(projectRoot, 'frontier');
|
|
650
|
-
const srcSettings = path.join(PKG_ROOT, 'settings');
|
|
651
|
-
const destSettings = path.join(projectRoot, 'settings');
|
|
652
|
-
const srcBin = path.join(PKG_ROOT, 'bin', 'maestro.cjs');
|
|
653
|
-
const destBin = path.join(projectRoot, 'bin', 'maestro.cjs');
|
|
654
|
-
|
|
655
|
-
let ok = copyDirRecursive(srcFrontier, destFrontier, dryRun, log);
|
|
656
|
-
if (!copyDirRecursive(srcSettings, destSettings, dryRun, log)) ok = false;
|
|
657
|
-
|
|
658
|
-
// bin/maestro.cjs
|
|
659
|
-
if (dryRun) {
|
|
660
|
-
log(`[dry-run] would write ${destBin}`);
|
|
661
|
-
} else {
|
|
662
|
-
if (isSymlink(destBin)) {
|
|
663
|
-
log(`ERROR: bin/maestro.cjs is a symlink — refusing: ${destBin}`);
|
|
664
|
-
ok = false;
|
|
665
|
-
} else {
|
|
666
|
-
try {
|
|
667
|
-
fs.mkdirSync(path.dirname(destBin), { recursive: true });
|
|
668
|
-
fs.writeFileSync(destBin, fs.readFileSync(srcBin));
|
|
669
|
-
log(`[engine] copied ${destBin}`);
|
|
670
|
-
} catch (err) {
|
|
671
|
-
log(`ERROR: failed to copy bin/maestro.cjs: ${err.message}`);
|
|
672
|
-
ok = false;
|
|
673
|
-
}
|
|
674
|
-
}
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
// docs/orchestration.md — the on-demand S2-S6 multi-agent protocol the
|
|
678
|
-
// kernel references. Maestro-owned reference file; copy (refuse symlinks).
|
|
679
|
-
const srcDocs = path.join(PKG_ROOT, 'docs', 'orchestration.md');
|
|
680
|
-
const destDocs = path.join(projectRoot, 'docs', 'orchestration.md');
|
|
681
|
-
if (dryRun) {
|
|
682
|
-
log(`[dry-run] would write ${destDocs}`);
|
|
683
|
-
} else if (isSymlink(destDocs)) {
|
|
684
|
-
log(`ERROR: docs/orchestration.md is a symlink — refusing: ${destDocs}`);
|
|
685
|
-
ok = false;
|
|
686
|
-
} else {
|
|
687
|
-
try {
|
|
688
|
-
fs.mkdirSync(path.dirname(destDocs), { recursive: true });
|
|
689
|
-
fs.writeFileSync(destDocs, fs.readFileSync(srcDocs));
|
|
690
|
-
log(`[doctrine] copied ${destDocs}`);
|
|
691
|
-
} catch (err) {
|
|
692
|
-
log(`ERROR: failed to copy docs/orchestration.md: ${err.message}`);
|
|
693
|
-
ok = false;
|
|
694
|
-
}
|
|
695
|
-
}
|
|
696
|
-
|
|
697
|
-
return ok;
|
|
698
|
-
}
|
|
699
|
-
|
|
700
|
-
/**
|
|
701
|
-
* Copy a single package template file to dest, no-clobber. Skips when dest
|
|
702
|
-
* already exists, refuses symlinks, honors dry-run. Reuses safeMkdirp +
|
|
703
|
-
* safeWrite. Shared by wrapper and Codex-skill installs.
|
|
704
|
-
* @param {string} src absolute source path (under PKG_ROOT)
|
|
705
|
-
* @param {string} dest absolute destination path
|
|
706
|
-
* @param {string} label short tag for logs (e.g. "wrapper", "codex-skill")
|
|
707
|
-
* @param {boolean} dryRun
|
|
708
|
-
* @param {(msg: string) => void} log
|
|
709
|
-
* @returns {boolean} true = success (wrote, skipped, or planned), false = error
|
|
710
|
-
*/
|
|
711
|
-
function installNoClobberFile(src, dest, label, dryRun, log) {
|
|
712
|
-
// Check if dest exists already (no-clobber)
|
|
713
|
-
let destStat;
|
|
714
|
-
try { destStat = fs.lstatSync(dest); } catch { destStat = null; }
|
|
715
|
-
|
|
716
|
-
if (destStat) {
|
|
717
|
-
if (destStat.isSymbolicLink()) {
|
|
718
|
-
log(`ERROR: ${label} dest is a symlink — refusing: ${dest}`);
|
|
719
|
-
return false;
|
|
720
|
-
}
|
|
721
|
-
log(`[${label}] skipped (exists, not clobbered): ${dest}`);
|
|
722
|
-
return true;
|
|
723
|
-
}
|
|
724
|
-
|
|
725
|
-
let srcContent;
|
|
726
|
-
try {
|
|
727
|
-
srcContent = fs.readFileSync(src, 'utf8');
|
|
728
|
-
} catch (err) {
|
|
729
|
-
log(`ERROR: cannot read template ${src}: ${err.message}`);
|
|
730
|
-
return false;
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
if (dryRun) {
|
|
734
|
-
log(`[dry-run] would create ${dest}`);
|
|
735
|
-
return true;
|
|
736
|
-
}
|
|
737
|
-
|
|
738
|
-
if (!safeMkdirp(dest)) {
|
|
739
|
-
log(`ERROR: could not create parent dir for ${dest}`);
|
|
740
|
-
return false;
|
|
741
|
-
}
|
|
742
|
-
|
|
743
|
-
const res = safeWrite(dest, srcContent);
|
|
744
|
-
if (!res.ok) {
|
|
745
|
-
log(`ERROR: failed to write ${label} ${dest}: ${res.reason}`);
|
|
746
|
-
return false;
|
|
747
|
-
}
|
|
748
|
-
log(`[${label}] wrote ${dest}`);
|
|
749
|
-
return true;
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
/**
|
|
753
|
-
* Install wrapper file (no-clobber).
|
|
754
|
-
* @param {string} target
|
|
755
|
-
* @param {string} projectRoot
|
|
756
|
-
* @param {boolean} userGlobal
|
|
757
|
-
* @param {boolean} dryRun
|
|
758
|
-
* @param {(msg: string) => void} log
|
|
759
|
-
* @returns {boolean}
|
|
760
|
-
*/
|
|
761
|
-
function installWrapper(target, projectRoot, userGlobal, dryRun, log) {
|
|
762
|
-
if (target === 'claude') {
|
|
763
|
-
log('[claude] No wrapper file — plugin delivers the command.');
|
|
764
|
-
log('[claude] To install the plugin: /plugin marketplace add mbanderas/maestro');
|
|
765
|
-
log('[claude] Then: /plugin install maestro@maestro');
|
|
766
|
-
return true;
|
|
767
|
-
}
|
|
768
|
-
|
|
769
|
-
const mapping = WRAPPER_MAP[target];
|
|
770
|
-
if (!mapping) {
|
|
771
|
-
log(`ERROR: unknown target: ${target}`);
|
|
772
|
-
return false;
|
|
773
|
-
}
|
|
774
|
-
|
|
775
|
-
const src = path.join(PKG_ROOT, mapping.src);
|
|
776
|
-
|
|
777
|
-
let dest;
|
|
778
|
-
if (userGlobal) {
|
|
779
|
-
if (!mapping.user) {
|
|
780
|
-
log(`[wrapper] --user not supported for target ${target} — writing to project instead`);
|
|
781
|
-
dest = path.join(projectRoot, mapping.proj);
|
|
782
|
-
} else {
|
|
783
|
-
dest = mapping.user();
|
|
784
|
-
}
|
|
785
|
-
} else {
|
|
786
|
-
dest = path.join(projectRoot, mapping.proj);
|
|
787
|
-
}
|
|
788
|
-
|
|
789
|
-
return installNoClobberFile(src, dest, 'wrapper', dryRun, log);
|
|
790
|
-
}
|
|
791
|
-
|
|
792
|
-
/**
|
|
793
|
-
* Install the Codex skill templates alongside the codex wrapper. Maestro-owned
|
|
794
|
-
* skill files refresh in place; user-edited copies are preserved.
|
|
795
|
-
* Project mode -> <project>/.agents/skills/<name>/SKILL.md; --user/global mode
|
|
796
|
-
* -> ~/.agents/skills/<name>/SKILL.md (mirrors installWrapper's dest logic).
|
|
797
|
-
* @param {string} projectRoot
|
|
798
|
-
* @param {boolean} userGlobal
|
|
799
|
-
* @param {boolean} dryRun
|
|
800
|
-
* @param {(msg: string) => void} log
|
|
801
|
-
* @returns {boolean}
|
|
802
|
-
*/
|
|
803
|
-
function installManagedCodexSkill(src, dest, name, legacyName, dryRun, log) {
|
|
804
|
-
let srcContent;
|
|
805
|
-
try {
|
|
806
|
-
srcContent = fs.readFileSync(src, 'utf8');
|
|
807
|
-
} catch (err) {
|
|
808
|
-
log(`ERROR: cannot read template ${src}: ${err.message}`);
|
|
809
|
-
return false;
|
|
810
|
-
}
|
|
811
|
-
|
|
812
|
-
const managedContent = codexSkillManagedContent(name, srcContent);
|
|
813
|
-
const managedBodies = [srcContent, managedContent];
|
|
814
|
-
|
|
815
|
-
let destStat;
|
|
816
|
-
try { destStat = fs.lstatSync(dest); } catch { destStat = null; }
|
|
817
|
-
|
|
818
|
-
if (destStat) {
|
|
819
|
-
if (destStat.isSymbolicLink()) {
|
|
820
|
-
log(`ERROR: codex-skill dest is a symlink — refusing: ${dest}`);
|
|
821
|
-
return false;
|
|
822
|
-
}
|
|
823
|
-
|
|
824
|
-
let existing;
|
|
825
|
-
try { existing = fs.readFileSync(dest, 'utf8'); } catch (err) {
|
|
826
|
-
log(`ERROR: cannot read existing Codex skill ${dest}: ${err.message}`);
|
|
827
|
-
return false;
|
|
828
|
-
}
|
|
829
|
-
|
|
830
|
-
if (!isManagedCodexSkillContent(existing, name, managedBodies)) {
|
|
831
|
-
log(`[codex-skill] preserved user-edited Codex skill: ${dest}`);
|
|
832
|
-
log(`[codex-skill] next step: compare with integrations/codex/skills/${name}/SKILL.md and manually merge if desired`);
|
|
833
|
-
return true;
|
|
834
|
-
}
|
|
835
|
-
|
|
836
|
-
if (existing === managedContent) {
|
|
837
|
-
log(`[codex-skill] up to date: ${dest}`);
|
|
838
|
-
return true;
|
|
839
|
-
}
|
|
840
|
-
|
|
841
|
-
if (dryRun) {
|
|
842
|
-
log(`[dry-run] would refresh managed Codex skill ${dest}`);
|
|
843
|
-
return true;
|
|
844
|
-
}
|
|
845
|
-
|
|
846
|
-
const res = safeWrite(dest, managedContent);
|
|
847
|
-
if (!res.ok) {
|
|
848
|
-
log(`ERROR: failed to refresh codex-skill ${dest}: ${res.reason}`);
|
|
849
|
-
return false;
|
|
850
|
-
}
|
|
851
|
-
log(`[codex-skill] refreshed managed Codex skill: ${dest}`);
|
|
852
|
-
return true;
|
|
853
|
-
}
|
|
854
|
-
|
|
855
|
-
if (dryRun) {
|
|
856
|
-
log(`[dry-run] would create ${dest}`);
|
|
857
|
-
return true;
|
|
858
|
-
}
|
|
859
|
-
|
|
860
|
-
if (!safeMkdirp(dest)) {
|
|
861
|
-
log(`ERROR: could not create parent dir for ${dest}`);
|
|
862
|
-
return false;
|
|
863
|
-
}
|
|
864
|
-
|
|
865
|
-
const res = safeWrite(dest, managedContent);
|
|
866
|
-
if (!res.ok) {
|
|
867
|
-
log(`ERROR: failed to write codex-skill ${dest}: ${res.reason}`);
|
|
868
|
-
return false;
|
|
869
|
-
}
|
|
870
|
-
log(`[codex-skill] wrote ${dest}`);
|
|
871
|
-
return true;
|
|
872
|
-
}
|
|
873
|
-
|
|
874
|
-
function migrateLegacyCodexSkill(dest, legacyName, namespacedName, knownTemplate, dryRun, log) {
|
|
875
|
-
let destStat;
|
|
876
|
-
try { destStat = fs.lstatSync(dest); } catch { return true; }
|
|
877
|
-
|
|
878
|
-
if (destStat.isSymbolicLink()) {
|
|
879
|
-
log(`ERROR: legacy codex-skill dest is a symlink — refusing: ${dest}`);
|
|
880
|
-
return false;
|
|
881
|
-
}
|
|
882
|
-
|
|
883
|
-
let existing;
|
|
884
|
-
try { existing = fs.readFileSync(dest, 'utf8'); } catch (err) {
|
|
885
|
-
log(`ERROR: cannot read legacy Codex skill ${dest}: ${err.message}`);
|
|
886
|
-
return false;
|
|
887
|
-
}
|
|
888
|
-
|
|
889
|
-
const shim = legacyCodexSkillContent(legacyName, namespacedName);
|
|
890
|
-
const managedBodies = [
|
|
891
|
-
knownTemplate,
|
|
892
|
-
legacyGenericCodexTemplate(knownTemplate, legacyName, namespacedName),
|
|
893
|
-
LEGACY_CODEX_SKILL_TEMPLATES[legacyName],
|
|
894
|
-
shim,
|
|
895
|
-
].filter(Boolean);
|
|
896
|
-
if (!isManagedCodexSkillContent(existing, legacyName, managedBodies)) {
|
|
897
|
-
log(`[codex-skill] preserved user-edited legacy Codex skill: ${dest}`);
|
|
898
|
-
log(`[codex-skill] next step: rename or merge it into .agents/skills/${namespacedName}/SKILL.md if you still need custom behavior`);
|
|
899
|
-
return true;
|
|
900
|
-
}
|
|
901
|
-
|
|
902
|
-
if (existing === shim) {
|
|
903
|
-
log(`[codex-skill] legacy compatibility up to date: ${dest}`);
|
|
904
|
-
return true;
|
|
905
|
-
}
|
|
906
|
-
|
|
907
|
-
if (dryRun) {
|
|
908
|
-
log(`[dry-run] would migrate legacy Codex skill ${dest}`);
|
|
909
|
-
return true;
|
|
910
|
-
}
|
|
911
|
-
|
|
912
|
-
const res = safeWrite(dest, shim);
|
|
913
|
-
if (!res.ok) {
|
|
914
|
-
log(`ERROR: failed to migrate legacy codex-skill ${dest}: ${res.reason}`);
|
|
915
|
-
return false;
|
|
916
|
-
}
|
|
917
|
-
log(`[codex-skill] migrated legacy Codex skill to compatibility shim: ${dest}`);
|
|
918
|
-
return true;
|
|
919
|
-
}
|
|
920
|
-
|
|
921
|
-
function installCodexSkills(projectRoot, userGlobal, dryRun, log) {
|
|
922
|
-
const skillsRoot = userGlobal
|
|
923
|
-
? path.join(homeDir(), '.agents', 'skills')
|
|
924
|
-
: path.join(projectRoot, '.agents', 'skills');
|
|
925
|
-
|
|
926
|
-
let ok = true;
|
|
927
|
-
for (const skill of CODEX_SKILLS) {
|
|
928
|
-
const src = path.join(PKG_ROOT, 'integrations', 'codex', 'skills', skill.name, 'SKILL.md');
|
|
929
|
-
const dest = path.join(skillsRoot, skill.name, 'SKILL.md');
|
|
930
|
-
if (!installManagedCodexSkill(src, dest, skill.name, skill.legacy, dryRun, log)) ok = false;
|
|
931
|
-
|
|
932
|
-
let legacyTemplate = '';
|
|
933
|
-
try { legacyTemplate = fs.readFileSync(src, 'utf8'); } catch {}
|
|
934
|
-
const legacyDest = path.join(skillsRoot, skill.legacy, 'SKILL.md');
|
|
935
|
-
if (!migrateLegacyCodexSkill(legacyDest, skill.legacy, skill.name, legacyTemplate, dryRun, log)) ok = false;
|
|
936
|
-
}
|
|
937
|
-
return ok;
|
|
938
|
-
}
|
|
939
|
-
|
|
940
|
-
// ---- main entry ----
|
|
941
|
-
|
|
942
|
-
/**
|
|
943
|
-
* Run the installer. Returns a numeric exit code (0 = success).
|
|
944
|
-
* @param {string[]} argv
|
|
945
|
-
* @returns {number}
|
|
946
|
-
*/
|
|
947
|
-
function run(argv) {
|
|
948
|
-
const opts = parseArgs(argv || []);
|
|
949
|
-
const { target: rawTarget, project, user: userGlobal, dryRun } = opts;
|
|
950
|
-
|
|
951
|
-
const lines = [];
|
|
952
|
-
const log = (msg) => { lines.push(msg); process.stdout.write(msg + '\n'); };
|
|
953
|
-
|
|
954
|
-
if (dryRun) log('[dry-run] planning only — no files will be written');
|
|
955
|
-
|
|
956
|
-
// Resolve target
|
|
957
|
-
let target = rawTarget;
|
|
958
|
-
if (target === 'auto') {
|
|
959
|
-
target = detectTarget(project);
|
|
960
|
-
if (target === 'none') {
|
|
961
|
-
log('[auto] no tool marker dir found — installing doctrine + engine only');
|
|
962
|
-
log('[auto] pass --target <tool> to install a command wrapper');
|
|
963
|
-
} else {
|
|
964
|
-
log(`[auto] detected target: ${target}`);
|
|
965
|
-
}
|
|
966
|
-
}
|
|
967
|
-
|
|
968
|
-
const VALID_TARGETS = ['auto', 'claude', 'codex', 'cursor', 'gemini', 'cline', 'windsurf'];
|
|
969
|
-
if (!VALID_TARGETS.includes(rawTarget)) {
|
|
970
|
-
log(`ERROR: unknown --target value: ${rawTarget}`);
|
|
971
|
-
return 1;
|
|
972
|
-
}
|
|
973
|
-
|
|
974
|
-
let anyError = false;
|
|
975
|
-
|
|
976
|
-
// 1. Doctrine — portable AGENTS.md kernel + this target's runtime adapter.
|
|
977
|
-
if (!installDoctrine(project, dryRun, log)) anyError = true;
|
|
978
|
-
if (!installAdapter(target, project, dryRun, log)) anyError = true;
|
|
979
|
-
|
|
980
|
-
// 2. Engine — frontier/ + bin/maestro.cjs + docs/orchestration.md.
|
|
981
|
-
if (!installEngine(project, dryRun, log)) anyError = true;
|
|
982
|
-
|
|
983
|
-
// 3. Wrapper — this target's /frontier command (skip if no target detected).
|
|
984
|
-
if (target !== 'none') {
|
|
985
|
-
if (!installWrapper(target, project, userGlobal, dryRun, log)) anyError = true;
|
|
986
|
-
}
|
|
987
|
-
|
|
988
|
-
// 3b. Codex skills — the .agents/skills/<name>/SKILL.md set ships alongside
|
|
989
|
-
// the deprecated codex prompt wrapper.
|
|
990
|
-
if (target === 'codex') {
|
|
991
|
-
if (!installCodexSkills(project, userGlobal, dryRun, log)) anyError = true;
|
|
992
|
-
}
|
|
993
|
-
|
|
994
|
-
if (anyError) {
|
|
995
|
-
log('install completed with errors (see above)');
|
|
996
|
-
return 1;
|
|
997
|
-
}
|
|
998
|
-
log('install complete');
|
|
999
|
-
return 0;
|
|
1000
|
-
}
|
|
1001
|
-
|
|
1002
|
-
// ---- CLI entry ----
|
|
1003
|
-
|
|
1004
|
-
if (require.main === module) {
|
|
1005
|
-
const code = run(process.argv.slice(2));
|
|
1006
|
-
process.exit(code);
|
|
1007
|
-
}
|
|
1008
|
-
|
|
1009
|
-
module.exports = {
|
|
1010
|
-
run,
|
|
1011
|
-
_test: {
|
|
1012
|
-
LEGACY_CODEX_SKILL_TEMPLATES,
|
|
1013
|
-
},
|
|
1014
|
-
};
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Maestro installer — writes doctrine + engine + tool wrapper into a target
|
|
3
|
+
// project. Append-only for AGENTS.md, no-clobber for wrapper files, safe to
|
|
4
|
+
// re-run. Zero dependencies (Node stdlib only). CommonJS (.cjs).
|
|
5
|
+
//
|
|
6
|
+
// Usage (as module): const { run } = require('./install.cjs'); run(argv);
|
|
7
|
+
// Usage (as script): node scripts/install.cjs [flags]
|
|
8
|
+
|
|
9
|
+
'use strict';
|
|
10
|
+
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const os = require('os');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
const crypto = require('crypto');
|
|
15
|
+
|
|
16
|
+
// ---- constants ----
|
|
17
|
+
|
|
18
|
+
const PKG_ROOT = path.join(__dirname, '..');
|
|
19
|
+
const SENTINEL = '<!-- maestro:begin -->';
|
|
20
|
+
const SENTINEL_END = '<!-- maestro:end -->';
|
|
21
|
+
|
|
22
|
+
// Map target -> { templateSrc, projectDest, userDest }
|
|
23
|
+
// templateSrc is relative to PKG_ROOT.
|
|
24
|
+
// userDest null means no global install path for this target.
|
|
25
|
+
const WRAPPER_MAP = {
|
|
26
|
+
codex: {
|
|
27
|
+
src: 'integrations/codex/prompts/frontier.md',
|
|
28
|
+
proj: '.codex/prompts/frontier.md',
|
|
29
|
+
user: () => path.join(homeDir(), '.codex', 'prompts', 'frontier.md'),
|
|
30
|
+
},
|
|
31
|
+
cursor: {
|
|
32
|
+
src: 'integrations/cursor/commands/frontier.md',
|
|
33
|
+
proj: '.cursor/commands/frontier.md',
|
|
34
|
+
user: null, // no global path for cursor
|
|
35
|
+
},
|
|
36
|
+
gemini: {
|
|
37
|
+
src: 'integrations/gemini/commands/frontier.toml',
|
|
38
|
+
proj: '.gemini/commands/frontier.toml',
|
|
39
|
+
user: () => path.join(homeDir(), '.gemini', 'commands', 'frontier.toml'),
|
|
40
|
+
},
|
|
41
|
+
cline: {
|
|
42
|
+
src: 'integrations/cline/skills/frontier/SKILL.md',
|
|
43
|
+
proj: '.cline/skills/frontier/SKILL.md',
|
|
44
|
+
user: () => path.join(homeDir(), '.cline', 'skills', 'frontier', 'SKILL.md'),
|
|
45
|
+
},
|
|
46
|
+
windsurf: {
|
|
47
|
+
src: 'integrations/windsurf/workflows/frontier.md',
|
|
48
|
+
proj: '.windsurf/workflows/frontier.md',
|
|
49
|
+
user: () => path.join(homeDir(), '.codeium', 'windsurf', 'global_workflows', 'frontier.md'),
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// Codex skill templates installed alongside the deprecated codex wrapper.
|
|
54
|
+
// Codex loads skills from <project>/.agents/skills/<name>/SKILL.md (project)
|
|
55
|
+
// or ~/.agents/skills/<name>/SKILL.md (global). Maestro-owned skills are
|
|
56
|
+
// refreshed when still managed; user-edited copies are preserved.
|
|
57
|
+
const CODEX_SKILLS = [
|
|
58
|
+
{ name: 'maestro-frontier', legacy: 'frontier' },
|
|
59
|
+
{ name: 'maestro-terse', legacy: 'terse' },
|
|
60
|
+
{ name: 'maestro-settings', legacy: 'settings' },
|
|
61
|
+
{ name: 'maestro-update', legacy: 'update' },
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
const LEGACY_CODEX_SKILL_TEMPLATES = {
|
|
65
|
+
frontier: `---
|
|
66
|
+
name: frontier
|
|
67
|
+
description: Maestro Frontier local multi-CLI fusion engine — switch mode, or run a prompt through the panel
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
Drive the **Maestro Frontier** engine — a zero-dependency local multi-CLI fusion
|
|
71
|
+
engine (a parallel panel of local CLIs → a judge model's analysis → a grounded
|
|
72
|
+
synthesis). It is the same engine the Claude Code plugin ships; here it runs
|
|
73
|
+
through the \`maestro\` CLI with \`--scope codex\`.
|
|
74
|
+
|
|
75
|
+
**This is a typing shortcut, not a prompt hook.** Codex has no automatic
|
|
76
|
+
prompt hook, so arming a mode does **not** auto-run the engine on later prompts —
|
|
77
|
+
it only persists the mode. To actually fuse a prompt, invoke \`run\` explicitly
|
|
78
|
+
(step 3).
|
|
79
|
+
|
|
80
|
+
Map the user's request to one engine CLI call and run it from the repo root.
|
|
81
|
+
Do not edit the engine's state file by hand.
|
|
82
|
+
|
|
83
|
+
## 1. Switch mode
|
|
84
|
+
|
|
85
|
+
Persists to \`~/.config/maestro/frontier-state.codex.json\`; default \`off\`.
|
|
86
|
+
\`--scope codex\` keeps Codex's armed mode independent from Claude Code, Cline,
|
|
87
|
+
Cursor, and Gemini on the same machine:
|
|
88
|
+
|
|
89
|
+
\`\`\`bash
|
|
90
|
+
maestro frontier mode off --scope codex
|
|
91
|
+
maestro frontier mode single --model <model> --scope codex
|
|
92
|
+
maestro frontier mode fusion --preset <preset> --scope codex
|
|
93
|
+
maestro frontier mode fusion --preset custom --models <a,b,c> --scope codex
|
|
94
|
+
maestro frontier mode fusion --preset <preset> --judge <model> --synth <model> --scope codex
|
|
95
|
+
\`\`\`
|
|
96
|
+
|
|
97
|
+
Models: \`opus\` (Claude Opus 4.8, needs \`claude\`), \`gpt-5.5\` (needs \`codex\`),
|
|
98
|
+
\`gemini\` (needs \`gemini\`). Presets: \`opus-duo\`, \`opus-gpt\`, \`gpt-duo\`,
|
|
99
|
+
\`frontier-trio\`, \`custom\`. Judge + synth default to Opus; \`--judge\`/\`--synth\`
|
|
100
|
+
override for any preset (e.g. \`--judge opus --synth gpt-5.5\`). \`gpt-duo\` runs
|
|
101
|
+
judge + synth on GPT-5.5 — a Codex-only fusion that needs no \`claude\`.
|
|
102
|
+
|
|
103
|
+
## 2. Show current mode/preset
|
|
104
|
+
|
|
105
|
+
\`\`\`bash
|
|
106
|
+
maestro frontier status --scope codex
|
|
107
|
+
\`\`\`
|
|
108
|
+
|
|
109
|
+
## 3. Run a prompt through the current mode
|
|
110
|
+
|
|
111
|
+
This is the action that actually fuses, since nothing auto-runs here. Set a
|
|
112
|
+
mode first (step 1), then:
|
|
113
|
+
|
|
114
|
+
\`\`\`bash
|
|
115
|
+
maestro frontier run "<prompt>" --scope codex
|
|
116
|
+
\`\`\`
|
|
117
|
+
|
|
118
|
+
- \`off\`: prints a notice, spawns nothing.
|
|
119
|
+
- \`single\`: dispatches the one selected CLI, prints its answer.
|
|
120
|
+
- \`fusion\`: runs the panel in parallel → judge → synthesizer; prints the final
|
|
121
|
+
answer (a one-line run meta goes to stderr). Report stdout verbatim.
|
|
122
|
+
|
|
123
|
+
On error the engine prints \`ERROR [<reason>]: <detail>\` to stderr and exits
|
|
124
|
+
non-zero — relay the reason.
|
|
125
|
+
|
|
126
|
+
## Indicator instruction
|
|
127
|
+
|
|
128
|
+
<!-- INDICATOR INSTRUCTION — apply every turn while this skill is active -->
|
|
129
|
+
|
|
130
|
+
Before composing your first reply each turn, run:
|
|
131
|
+
|
|
132
|
+
\`\`\`bash
|
|
133
|
+
maestro frontier status --scope codex
|
|
134
|
+
\`\`\`
|
|
135
|
+
|
|
136
|
+
Parse the returned JSON \`mode\` field:
|
|
137
|
+
|
|
138
|
+
- If \`mode\` is not \`off\`, make the **first line** of your reply exactly:
|
|
139
|
+
\`Maestro Frontier ON (<label>)\`
|
|
140
|
+
where \`<label>\` is formed as follows:
|
|
141
|
+
- single mode → \`single · <model>\` (e.g. \`single · opus\`)
|
|
142
|
+
- fusion mode → \`fusion · <preset>\` (e.g. \`fusion · frontier-trio\`);
|
|
143
|
+
for a custom preset use \`fusion · custom (<model1>, <model2>, ...)\`
|
|
144
|
+
- If \`mode\` is \`off\`, output no indicator line.
|
|
145
|
+
|
|
146
|
+
<!-- END INDICATOR INSTRUCTION -->
|
|
147
|
+
|
|
148
|
+
## Notes
|
|
149
|
+
|
|
150
|
+
- Real \`single\`/\`fusion\` runs spawn local CLIs and cost tokens; use small prompts.
|
|
151
|
+
\`off\` is free.
|
|
152
|
+
- Each model's CLI must be on \`PATH\`, or point at a specific build with
|
|
153
|
+
\`MAESTRO_CLAUDE_BIN\` / \`MAESTRO_CODEX_BIN\` / \`MAESTRO_GEMINI_BIN\`.
|
|
154
|
+
- Requires \`maestro\` on \`PATH\` (installed during Maestro setup). If it is
|
|
155
|
+
missing, install Maestro first.
|
|
156
|
+
`,
|
|
157
|
+
terse: `---
|
|
158
|
+
name: terse
|
|
159
|
+
description: Toggle Maestro terse output level (lite, full, ultra, off) via the settings CLI
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
Toggle the **Maestro terse** output level for this environment. Terse mode
|
|
163
|
+
condenses agent replies; levels range from \`off\` (default verbosity) through
|
|
164
|
+
\`lite\`, \`full\`, and \`ultra\` (most compressed).
|
|
165
|
+
|
|
166
|
+
When the user invokes this skill, run the settings CLI to read or change the
|
|
167
|
+
terse level. Do not edit settings files by hand.
|
|
168
|
+
|
|
169
|
+
## Check current terse level
|
|
170
|
+
|
|
171
|
+
\`\`\`bash
|
|
172
|
+
node settings/cli.cjs --help
|
|
173
|
+
\`\`\`
|
|
174
|
+
|
|
175
|
+
Consult the help output for the exact read subcommand, then run it. If
|
|
176
|
+
\`settings/cli.cjs\` is not present, run \`maestro --help\` to discover the
|
|
177
|
+
correct path.
|
|
178
|
+
|
|
179
|
+
## Set terse level
|
|
180
|
+
|
|
181
|
+
\`\`\`bash
|
|
182
|
+
node settings/cli.cjs terse <level>
|
|
183
|
+
\`\`\`
|
|
184
|
+
|
|
185
|
+
Valid levels: \`off\` | \`lite\` | \`full\` | \`ultra\`
|
|
186
|
+
|
|
187
|
+
Examples:
|
|
188
|
+
|
|
189
|
+
\`\`\`bash
|
|
190
|
+
node settings/cli.cjs terse off
|
|
191
|
+
node settings/cli.cjs terse lite
|
|
192
|
+
node settings/cli.cjs terse full
|
|
193
|
+
node settings/cli.cjs terse ultra
|
|
194
|
+
\`\`\`
|
|
195
|
+
|
|
196
|
+
If the CLI rejects an argument or the subcommand name differs, run
|
|
197
|
+
\`node settings/cli.cjs --help\` first and follow the printed usage.
|
|
198
|
+
|
|
199
|
+
## Notes
|
|
200
|
+
|
|
201
|
+
- The change persists in Maestro's settings store; it applies to subsequent
|
|
202
|
+
agent turns in this project.
|
|
203
|
+
- Requires \`node\` on \`PATH\` and Maestro installed in the project root. If
|
|
204
|
+
\`settings/cli.cjs\` is missing, re-run the Maestro installer:
|
|
205
|
+
\`npx github:mbanderas/maestro install --target codex\`
|
|
206
|
+
`,
|
|
207
|
+
settings: `---
|
|
208
|
+
name: settings
|
|
209
|
+
description: View and change Maestro toggles (terse, frontier, context-bar) via the settings CLI
|
|
210
|
+
---
|
|
211
|
+
|
|
212
|
+
View or change **Maestro settings** for this project. The settings CLI manages
|
|
213
|
+
the three primary toggles: \`terse\`, \`frontier\`, and \`context-bar\`.
|
|
214
|
+
|
|
215
|
+
When the user invokes this skill, run the settings CLI from the repo root.
|
|
216
|
+
Do not edit settings files by hand.
|
|
217
|
+
|
|
218
|
+
## Discover available commands
|
|
219
|
+
|
|
220
|
+
\`\`\`bash
|
|
221
|
+
node settings/cli.cjs --help
|
|
222
|
+
\`\`\`
|
|
223
|
+
|
|
224
|
+
If \`settings/cli.cjs\` is not present, run \`maestro --help\` to locate the
|
|
225
|
+
correct entry point.
|
|
226
|
+
|
|
227
|
+
## Common operations
|
|
228
|
+
|
|
229
|
+
List current settings:
|
|
230
|
+
|
|
231
|
+
\`\`\`bash
|
|
232
|
+
node settings/cli.cjs
|
|
233
|
+
\`\`\`
|
|
234
|
+
|
|
235
|
+
Set a toggle:
|
|
236
|
+
|
|
237
|
+
\`\`\`bash
|
|
238
|
+
node settings/cli.cjs terse <off|lite|full|ultra>
|
|
239
|
+
node settings/cli.cjs frontier <off|single|fusion>
|
|
240
|
+
node settings/cli.cjs context-bar <on|off>
|
|
241
|
+
\`\`\`
|
|
242
|
+
|
|
243
|
+
If a subcommand name or argument differs from the above, follow the usage
|
|
244
|
+
printed by \`--help\` — do not guess flags.
|
|
245
|
+
|
|
246
|
+
## Notes
|
|
247
|
+
|
|
248
|
+
- Changes persist in Maestro's settings store and apply to subsequent agent
|
|
249
|
+
turns in this project.
|
|
250
|
+
- Requires \`node\` on \`PATH\` and Maestro installed in the project root. If
|
|
251
|
+
\`settings/cli.cjs\` is missing, re-run the installer:
|
|
252
|
+
\`npx github:mbanderas/maestro install --target codex\`
|
|
253
|
+
`,
|
|
254
|
+
update: `---
|
|
255
|
+
name: update
|
|
256
|
+
description: Update Maestro to the latest version by re-running the installer for Codex
|
|
257
|
+
---
|
|
258
|
+
|
|
259
|
+
Update **Maestro** to the latest marketplace code. This re-runs the installer,
|
|
260
|
+
which pulls the current release and overwrites the local Maestro files in place.
|
|
261
|
+
|
|
262
|
+
When the user invokes this skill, run the installer from the repo root:
|
|
263
|
+
|
|
264
|
+
\`\`\`bash
|
|
265
|
+
npx github:mbanderas/maestro install --target codex
|
|
266
|
+
\`\`\`
|
|
267
|
+
|
|
268
|
+
The installer is idempotent — it is safe to re-run against an existing
|
|
269
|
+
installation. It will:
|
|
270
|
+
|
|
271
|
+
- Pull the latest Maestro source from the repository.
|
|
272
|
+
- Overwrite skills, hooks, and settings scaffolding with the new versions.
|
|
273
|
+
- Leave project-local configuration (state files, secrets) untouched.
|
|
274
|
+
|
|
275
|
+
## Notes
|
|
276
|
+
|
|
277
|
+
- Requires \`node\` and \`npx\` on \`PATH\`.
|
|
278
|
+
- Run from the project root so the installer targets the correct directory.
|
|
279
|
+
- After the installer completes, restart the Codex session (or reload the
|
|
280
|
+
project) so updated skills and hooks take effect.
|
|
281
|
+
- If \`npx\` is unavailable, clone \`https://github.com/mbanderas/maestro\`
|
|
282
|
+
manually and follow the repository's install instructions.
|
|
283
|
+
`,
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
// Runtime adapter per target. The adapter imports @AGENTS.md (Cursor has no
|
|
287
|
+
// imports, so .cursorrules embeds the kernel). codex/cline/windsurf read
|
|
288
|
+
// AGENTS.md directly and need no adapter.
|
|
289
|
+
const ADAPTER_MAP = {
|
|
290
|
+
claude: 'CLAUDE.md',
|
|
291
|
+
gemini: 'GEMINI.md',
|
|
292
|
+
cursor: '.cursorrules',
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
// Marker dirs used for auto-detection (scanned inside project root)
|
|
296
|
+
const AUTO_MARKERS = [
|
|
297
|
+
{ dir: '.cursor', target: 'cursor' },
|
|
298
|
+
{ dir: '.gemini', target: 'gemini' },
|
|
299
|
+
{ dir: '.codex', target: 'codex' },
|
|
300
|
+
{ dir: '.cline', target: 'cline' },
|
|
301
|
+
{ dir: '.windsurf',target: 'windsurf' },
|
|
302
|
+
{ dir: '.claude', target: 'claude' },
|
|
303
|
+
];
|
|
304
|
+
|
|
305
|
+
// ---- safety helpers ----
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Returns true if p is a symlink (lstat-based). Never throws.
|
|
309
|
+
* @param {string} p
|
|
310
|
+
* @returns {boolean}
|
|
311
|
+
*/
|
|
312
|
+
function isSymlink(p) {
|
|
313
|
+
try {
|
|
314
|
+
return fs.lstatSync(p).isSymbolicLink();
|
|
315
|
+
} catch {
|
|
316
|
+
return false;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Create directories for destPath. Refuses to create through a symlinked
|
|
322
|
+
* ancestor directory. Returns true on success, false on refusal.
|
|
323
|
+
* @param {string} destPath
|
|
324
|
+
* @returns {boolean}
|
|
325
|
+
*/
|
|
326
|
+
function safeMkdirp(destPath) {
|
|
327
|
+
const dir = path.dirname(destPath);
|
|
328
|
+
// Walk ancestors from PKG_ROOT outward — only check the leaf dir because
|
|
329
|
+
// we cannot reliably validate every ancestor on all OSes; the write will
|
|
330
|
+
// fail safely if anything is wrong.
|
|
331
|
+
try {
|
|
332
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
333
|
+
return true;
|
|
334
|
+
} catch {
|
|
335
|
+
return false;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Write buf to dest. Refuses if dest (or its parent dir) is a symlink.
|
|
341
|
+
* Returns { ok: true } or { ok: false, reason: string }.
|
|
342
|
+
* @param {string} dest
|
|
343
|
+
* @param {string|Buffer} content
|
|
344
|
+
* @returns {{ ok: boolean, reason?: string }}
|
|
345
|
+
*/
|
|
346
|
+
function safeWrite(dest, content) {
|
|
347
|
+
// Check parent dir
|
|
348
|
+
const dir = path.dirname(dest);
|
|
349
|
+
if (isSymlink(dir)) {
|
|
350
|
+
return { ok: false, reason: `parent dir is a symlink: ${dir}` };
|
|
351
|
+
}
|
|
352
|
+
// Check destination itself
|
|
353
|
+
if (isSymlink(dest)) {
|
|
354
|
+
return { ok: false, reason: `destination is a symlink: ${dest}` };
|
|
355
|
+
}
|
|
356
|
+
try {
|
|
357
|
+
fs.writeFileSync(dest, content, 'utf8');
|
|
358
|
+
return { ok: true };
|
|
359
|
+
} catch (err) {
|
|
360
|
+
return { ok: false, reason: String(err.message || err) };
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function homeDir() {
|
|
365
|
+
return process.platform === 'win32'
|
|
366
|
+
? (process.env.USERPROFILE || os.homedir())
|
|
367
|
+
: (process.env.HOME || os.homedir());
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function sha256(content) {
|
|
371
|
+
return crypto.createHash('sha256').update(content, 'utf8').digest('hex');
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function codexSkillManagedContent(name, srcContent) {
|
|
375
|
+
const body = srcContent.trimEnd() + '\n';
|
|
376
|
+
return `${body}\n<!-- maestro-managed:codex-skill name=${name} sha256=${sha256(body)} -->\n`;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function splitCodexSkillMarker(content) {
|
|
380
|
+
const marker = /\n?<!-- maestro-managed:codex-skill name=([^\s]+) sha256=([a-f0-9]+|0000) -->\s*$/i.exec(content);
|
|
381
|
+
if (!marker) return null;
|
|
382
|
+
return {
|
|
383
|
+
name: marker[1],
|
|
384
|
+
hash: marker[2].toLowerCase(),
|
|
385
|
+
body: content.slice(0, marker.index).trimEnd() + '\n',
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function isManagedCodexSkillContent(content, expectedName, managedBodies) {
|
|
390
|
+
const marker = splitCodexSkillMarker(content);
|
|
391
|
+
if (marker && marker.name === expectedName) {
|
|
392
|
+
return marker.hash === '0000' || marker.hash === sha256(marker.body);
|
|
393
|
+
}
|
|
394
|
+
if (content.includes(`maestro-managed:codex-skill name=${expectedName} sha256=0000`)) {
|
|
395
|
+
return true;
|
|
396
|
+
}
|
|
397
|
+
return managedBodies.some((body) => content.trimEnd() === body.trimEnd());
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function legacyCodexSkillContent(legacyName, namespacedName) {
|
|
401
|
+
const body = `---\nname: ${legacyName}\ndescription: Legacy Maestro compatibility skill for ${namespacedName}\n---\n\nThis legacy Maestro skill has moved to \`${namespacedName}\`.\n\nUse the \`${namespacedName}\` skill for current Maestro behavior. This compatibility skill is kept only for existing Codex installs that still reference \`${legacyName}\`.\n`;
|
|
402
|
+
return codexSkillManagedContent(legacyName, body);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function legacyGenericCodexTemplate(srcContent, legacyName, namespacedName) {
|
|
406
|
+
return srcContent.replace(
|
|
407
|
+
new RegExp(`(^---\\r?\\nname: )${namespacedName}(\\r?\\n)`, 'm'),
|
|
408
|
+
`$1${legacyName}$2`
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// ---- parse argv ----
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* @param {string[]} argv
|
|
416
|
+
* @returns {{ target: string, project: string, user: boolean, dryRun: boolean, noHooks: boolean }}
|
|
417
|
+
*/
|
|
418
|
+
function parseArgs(argv) {
|
|
419
|
+
const opts = {
|
|
420
|
+
target: 'auto',
|
|
421
|
+
project: process.cwd(),
|
|
422
|
+
user: false,
|
|
423
|
+
dryRun: false,
|
|
424
|
+
noHooks: false,
|
|
425
|
+
};
|
|
426
|
+
|
|
427
|
+
let i = 0;
|
|
428
|
+
while (i < argv.length) {
|
|
429
|
+
const a = argv[i];
|
|
430
|
+
if (a === '--target' && i + 1 < argv.length) {
|
|
431
|
+
opts.target = argv[++i];
|
|
432
|
+
} else if (a === '--project' && i + 1 < argv.length) {
|
|
433
|
+
opts.project = argv[++i];
|
|
434
|
+
} else if (a === '--user') {
|
|
435
|
+
opts.user = true;
|
|
436
|
+
} else if (a === '--dry-run') {
|
|
437
|
+
opts.dryRun = true;
|
|
438
|
+
} else if (a === '--no-hooks') {
|
|
439
|
+
opts.noHooks = true;
|
|
440
|
+
}
|
|
441
|
+
i++;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
opts.project = path.resolve(opts.project);
|
|
445
|
+
return opts;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// ---- auto-detect ----
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Detect which tool is in use by looking for marker dirs.
|
|
452
|
+
* @param {string} projectRoot
|
|
453
|
+
* @returns {string} detected target or 'none'
|
|
454
|
+
*/
|
|
455
|
+
function detectTarget(projectRoot) {
|
|
456
|
+
for (const { dir, target } of AUTO_MARKERS) {
|
|
457
|
+
try {
|
|
458
|
+
const p = path.join(projectRoot, dir);
|
|
459
|
+
const st = fs.lstatSync(p);
|
|
460
|
+
if (st.isDirectory()) return target;
|
|
461
|
+
} catch {
|
|
462
|
+
// not found
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
return 'none';
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// ---- install actions ----
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Read a file from the package root. Returns string, or null (logs) on error.
|
|
472
|
+
* @param {string} rel
|
|
473
|
+
* @param {(msg: string) => void} log
|
|
474
|
+
* @returns {string|null}
|
|
475
|
+
*/
|
|
476
|
+
function readPkgFile(rel, log) {
|
|
477
|
+
try {
|
|
478
|
+
return fs.readFileSync(path.join(PKG_ROOT, rel), 'utf8');
|
|
479
|
+
} catch (err) {
|
|
480
|
+
log(`ERROR: cannot read package ${rel}: ${err.message}`);
|
|
481
|
+
return null;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Install a doctrine/adapter markdown file. Append-only, idempotent, never
|
|
487
|
+
* clobbers user content above the maestro block; refuses symlinks.
|
|
488
|
+
* @param {string} dest absolute destination path
|
|
489
|
+
* @param {string} srcContent content to install
|
|
490
|
+
* @param {string} label short name for logs (e.g. "AGENTS.md")
|
|
491
|
+
* @param {boolean} dryRun
|
|
492
|
+
* @param {(msg: string) => void} log
|
|
493
|
+
* @returns {boolean} true = success (or no-op), false = error
|
|
494
|
+
*/
|
|
495
|
+
function appendOnlyDoctrine(dest, srcContent, label, dryRun, log) {
|
|
496
|
+
const block = `\n${SENTINEL}\n${srcContent}\n${SENTINEL_END}\n`;
|
|
497
|
+
|
|
498
|
+
let existsStat;
|
|
499
|
+
try { existsStat = fs.lstatSync(dest); } catch { existsStat = null; }
|
|
500
|
+
|
|
501
|
+
if (existsStat) {
|
|
502
|
+
if (existsStat.isSymbolicLink()) {
|
|
503
|
+
log(`ERROR: ${label} is a symlink — refusing to write through it: ${dest}`);
|
|
504
|
+
return false;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
let existing;
|
|
508
|
+
try { existing = fs.readFileSync(dest, 'utf8'); } catch (err) {
|
|
509
|
+
log(`ERROR: cannot read existing ${label}: ${err.message}`);
|
|
510
|
+
return false;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
if (existing.includes(SENTINEL)) {
|
|
514
|
+
log(`[doctrine] ${label} already contains sentinel — skipping`);
|
|
515
|
+
return true;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
if (dryRun) {
|
|
519
|
+
log(`[dry-run] would append maestro doctrine to existing ${dest}`);
|
|
520
|
+
return true;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
const res = safeWrite(dest, existing + block);
|
|
524
|
+
if (!res.ok) {
|
|
525
|
+
log(`ERROR: failed to append to ${label}: ${res.reason}`);
|
|
526
|
+
return false;
|
|
527
|
+
}
|
|
528
|
+
log(`[doctrine] appended maestro block to existing ${label}`);
|
|
529
|
+
return true;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// Absent — write fresh, wrapped in the sentinel so re-runs detect it.
|
|
533
|
+
if (dryRun) {
|
|
534
|
+
log(`[dry-run] would create ${dest}`);
|
|
535
|
+
return true;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
if (!safeMkdirp(dest)) {
|
|
539
|
+
log(`ERROR: could not create parent dir for ${dest}`);
|
|
540
|
+
return false;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
const freshContent = SENTINEL + '\n' + srcContent + '\n' + SENTINEL_END + '\n';
|
|
544
|
+
const res = safeWrite(dest, freshContent);
|
|
545
|
+
if (!res.ok) {
|
|
546
|
+
log(`ERROR: failed to write ${label}: ${res.reason}`);
|
|
547
|
+
return false;
|
|
548
|
+
}
|
|
549
|
+
log(`[doctrine] wrote ${label}`);
|
|
550
|
+
return true;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* Install the portable doctrine core (AGENTS.md) into the project root.
|
|
555
|
+
* @param {string} projectRoot
|
|
556
|
+
* @param {boolean} dryRun
|
|
557
|
+
* @param {(msg: string) => void} log
|
|
558
|
+
* @returns {boolean}
|
|
559
|
+
*/
|
|
560
|
+
function installDoctrine(projectRoot, dryRun, log) {
|
|
561
|
+
const src = readPkgFile('AGENTS.md', log);
|
|
562
|
+
if (src === null) return false;
|
|
563
|
+
return appendOnlyDoctrine(path.join(projectRoot, 'AGENTS.md'), src, 'AGENTS.md', dryRun, log);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* Install the runtime adapter for a target (CLAUDE.md / GEMINI.md /
|
|
568
|
+
* .cursorrules). codex/cline/windsurf read AGENTS.md directly -> no-op.
|
|
569
|
+
* @param {string} target
|
|
570
|
+
* @param {string} projectRoot
|
|
571
|
+
* @param {boolean} dryRun
|
|
572
|
+
* @param {(msg: string) => void} log
|
|
573
|
+
* @returns {boolean}
|
|
574
|
+
*/
|
|
575
|
+
function installAdapter(target, projectRoot, dryRun, log) {
|
|
576
|
+
const rel = ADAPTER_MAP[target];
|
|
577
|
+
if (!rel) return true; // no adapter for this target
|
|
578
|
+
const src = readPkgFile(rel, log);
|
|
579
|
+
if (src === null) return false;
|
|
580
|
+
return appendOnlyDoctrine(path.join(projectRoot, rel), src, rel, dryRun, log);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* Recursively copy srcDir -> destDir, skipping *.test.cjs files.
|
|
585
|
+
* @param {string} srcDir
|
|
586
|
+
* @param {string} destDir
|
|
587
|
+
* @param {boolean} dryRun
|
|
588
|
+
* @param {(msg: string) => void} log
|
|
589
|
+
* @returns {boolean}
|
|
590
|
+
*/
|
|
591
|
+
function copyDirRecursive(srcDir, destDir, dryRun, log) {
|
|
592
|
+
let entries;
|
|
593
|
+
try {
|
|
594
|
+
entries = fs.readdirSync(srcDir, { withFileTypes: true });
|
|
595
|
+
} catch (err) {
|
|
596
|
+
log(`ERROR: cannot read dir ${srcDir}: ${err.message}`);
|
|
597
|
+
return false;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
let ok = true;
|
|
601
|
+
for (const entry of entries) {
|
|
602
|
+
if (entry.isFile() && entry.name.endsWith('.test.cjs')) continue;
|
|
603
|
+
|
|
604
|
+
const src = path.join(srcDir, entry.name);
|
|
605
|
+
const dest = path.join(destDir, entry.name);
|
|
606
|
+
|
|
607
|
+
if (entry.isDirectory()) {
|
|
608
|
+
if (!copyDirRecursive(src, dest, dryRun, log)) ok = false;
|
|
609
|
+
} else if (entry.isFile()) {
|
|
610
|
+
if (dryRun) {
|
|
611
|
+
log(`[dry-run] would write ${dest}`);
|
|
612
|
+
continue;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
if (isSymlink(dest)) {
|
|
616
|
+
log(`ERROR: destination is a symlink — refusing: ${dest}`);
|
|
617
|
+
ok = false;
|
|
618
|
+
continue;
|
|
619
|
+
}
|
|
620
|
+
if (isSymlink(path.dirname(dest))) {
|
|
621
|
+
log(`ERROR: destination parent is a symlink — refusing: ${dest}`);
|
|
622
|
+
ok = false;
|
|
623
|
+
continue;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
try {
|
|
627
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
628
|
+
const content = fs.readFileSync(src);
|
|
629
|
+
fs.writeFileSync(dest, content);
|
|
630
|
+
log(`[engine] copied ${dest}`);
|
|
631
|
+
} catch (err) {
|
|
632
|
+
log(`ERROR: failed to copy ${src} -> ${dest}: ${err.message}`);
|
|
633
|
+
ok = false;
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
return ok;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
/**
|
|
641
|
+
* Install engine files (frontier/ dir + settings/ dir + bin/maestro.cjs).
|
|
642
|
+
* @param {string} projectRoot
|
|
643
|
+
* @param {boolean} dryRun
|
|
644
|
+
* @param {(msg: string) => void} log
|
|
645
|
+
* @returns {boolean}
|
|
646
|
+
*/
|
|
647
|
+
function installEngine(projectRoot, dryRun, log) {
|
|
648
|
+
const srcFrontier = path.join(PKG_ROOT, 'frontier');
|
|
649
|
+
const destFrontier = path.join(projectRoot, 'frontier');
|
|
650
|
+
const srcSettings = path.join(PKG_ROOT, 'settings');
|
|
651
|
+
const destSettings = path.join(projectRoot, 'settings');
|
|
652
|
+
const srcBin = path.join(PKG_ROOT, 'bin', 'maestro.cjs');
|
|
653
|
+
const destBin = path.join(projectRoot, 'bin', 'maestro.cjs');
|
|
654
|
+
|
|
655
|
+
let ok = copyDirRecursive(srcFrontier, destFrontier, dryRun, log);
|
|
656
|
+
if (!copyDirRecursive(srcSettings, destSettings, dryRun, log)) ok = false;
|
|
657
|
+
|
|
658
|
+
// bin/maestro.cjs
|
|
659
|
+
if (dryRun) {
|
|
660
|
+
log(`[dry-run] would write ${destBin}`);
|
|
661
|
+
} else {
|
|
662
|
+
if (isSymlink(destBin)) {
|
|
663
|
+
log(`ERROR: bin/maestro.cjs is a symlink — refusing: ${destBin}`);
|
|
664
|
+
ok = false;
|
|
665
|
+
} else {
|
|
666
|
+
try {
|
|
667
|
+
fs.mkdirSync(path.dirname(destBin), { recursive: true });
|
|
668
|
+
fs.writeFileSync(destBin, fs.readFileSync(srcBin));
|
|
669
|
+
log(`[engine] copied ${destBin}`);
|
|
670
|
+
} catch (err) {
|
|
671
|
+
log(`ERROR: failed to copy bin/maestro.cjs: ${err.message}`);
|
|
672
|
+
ok = false;
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
// docs/orchestration.md — the on-demand S2-S6 multi-agent protocol the
|
|
678
|
+
// kernel references. Maestro-owned reference file; copy (refuse symlinks).
|
|
679
|
+
const srcDocs = path.join(PKG_ROOT, 'docs', 'orchestration.md');
|
|
680
|
+
const destDocs = path.join(projectRoot, 'docs', 'orchestration.md');
|
|
681
|
+
if (dryRun) {
|
|
682
|
+
log(`[dry-run] would write ${destDocs}`);
|
|
683
|
+
} else if (isSymlink(destDocs)) {
|
|
684
|
+
log(`ERROR: docs/orchestration.md is a symlink — refusing: ${destDocs}`);
|
|
685
|
+
ok = false;
|
|
686
|
+
} else {
|
|
687
|
+
try {
|
|
688
|
+
fs.mkdirSync(path.dirname(destDocs), { recursive: true });
|
|
689
|
+
fs.writeFileSync(destDocs, fs.readFileSync(srcDocs));
|
|
690
|
+
log(`[doctrine] copied ${destDocs}`);
|
|
691
|
+
} catch (err) {
|
|
692
|
+
log(`ERROR: failed to copy docs/orchestration.md: ${err.message}`);
|
|
693
|
+
ok = false;
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
return ok;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
/**
|
|
701
|
+
* Copy a single package template file to dest, no-clobber. Skips when dest
|
|
702
|
+
* already exists, refuses symlinks, honors dry-run. Reuses safeMkdirp +
|
|
703
|
+
* safeWrite. Shared by wrapper and Codex-skill installs.
|
|
704
|
+
* @param {string} src absolute source path (under PKG_ROOT)
|
|
705
|
+
* @param {string} dest absolute destination path
|
|
706
|
+
* @param {string} label short tag for logs (e.g. "wrapper", "codex-skill")
|
|
707
|
+
* @param {boolean} dryRun
|
|
708
|
+
* @param {(msg: string) => void} log
|
|
709
|
+
* @returns {boolean} true = success (wrote, skipped, or planned), false = error
|
|
710
|
+
*/
|
|
711
|
+
function installNoClobberFile(src, dest, label, dryRun, log) {
|
|
712
|
+
// Check if dest exists already (no-clobber)
|
|
713
|
+
let destStat;
|
|
714
|
+
try { destStat = fs.lstatSync(dest); } catch { destStat = null; }
|
|
715
|
+
|
|
716
|
+
if (destStat) {
|
|
717
|
+
if (destStat.isSymbolicLink()) {
|
|
718
|
+
log(`ERROR: ${label} dest is a symlink — refusing: ${dest}`);
|
|
719
|
+
return false;
|
|
720
|
+
}
|
|
721
|
+
log(`[${label}] skipped (exists, not clobbered): ${dest}`);
|
|
722
|
+
return true;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
let srcContent;
|
|
726
|
+
try {
|
|
727
|
+
srcContent = fs.readFileSync(src, 'utf8');
|
|
728
|
+
} catch (err) {
|
|
729
|
+
log(`ERROR: cannot read template ${src}: ${err.message}`);
|
|
730
|
+
return false;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
if (dryRun) {
|
|
734
|
+
log(`[dry-run] would create ${dest}`);
|
|
735
|
+
return true;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
if (!safeMkdirp(dest)) {
|
|
739
|
+
log(`ERROR: could not create parent dir for ${dest}`);
|
|
740
|
+
return false;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
const res = safeWrite(dest, srcContent);
|
|
744
|
+
if (!res.ok) {
|
|
745
|
+
log(`ERROR: failed to write ${label} ${dest}: ${res.reason}`);
|
|
746
|
+
return false;
|
|
747
|
+
}
|
|
748
|
+
log(`[${label}] wrote ${dest}`);
|
|
749
|
+
return true;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
/**
|
|
753
|
+
* Install wrapper file (no-clobber).
|
|
754
|
+
* @param {string} target
|
|
755
|
+
* @param {string} projectRoot
|
|
756
|
+
* @param {boolean} userGlobal
|
|
757
|
+
* @param {boolean} dryRun
|
|
758
|
+
* @param {(msg: string) => void} log
|
|
759
|
+
* @returns {boolean}
|
|
760
|
+
*/
|
|
761
|
+
function installWrapper(target, projectRoot, userGlobal, dryRun, log) {
|
|
762
|
+
if (target === 'claude') {
|
|
763
|
+
log('[claude] No wrapper file — plugin delivers the command.');
|
|
764
|
+
log('[claude] To install the plugin: /plugin marketplace add mbanderas/maestro');
|
|
765
|
+
log('[claude] Then: /plugin install maestro@maestro');
|
|
766
|
+
return true;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
const mapping = WRAPPER_MAP[target];
|
|
770
|
+
if (!mapping) {
|
|
771
|
+
log(`ERROR: unknown target: ${target}`);
|
|
772
|
+
return false;
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
const src = path.join(PKG_ROOT, mapping.src);
|
|
776
|
+
|
|
777
|
+
let dest;
|
|
778
|
+
if (userGlobal) {
|
|
779
|
+
if (!mapping.user) {
|
|
780
|
+
log(`[wrapper] --user not supported for target ${target} — writing to project instead`);
|
|
781
|
+
dest = path.join(projectRoot, mapping.proj);
|
|
782
|
+
} else {
|
|
783
|
+
dest = mapping.user();
|
|
784
|
+
}
|
|
785
|
+
} else {
|
|
786
|
+
dest = path.join(projectRoot, mapping.proj);
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
return installNoClobberFile(src, dest, 'wrapper', dryRun, log);
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
/**
|
|
793
|
+
* Install the Codex skill templates alongside the codex wrapper. Maestro-owned
|
|
794
|
+
* skill files refresh in place; user-edited copies are preserved.
|
|
795
|
+
* Project mode -> <project>/.agents/skills/<name>/SKILL.md; --user/global mode
|
|
796
|
+
* -> ~/.agents/skills/<name>/SKILL.md (mirrors installWrapper's dest logic).
|
|
797
|
+
* @param {string} projectRoot
|
|
798
|
+
* @param {boolean} userGlobal
|
|
799
|
+
* @param {boolean} dryRun
|
|
800
|
+
* @param {(msg: string) => void} log
|
|
801
|
+
* @returns {boolean}
|
|
802
|
+
*/
|
|
803
|
+
function installManagedCodexSkill(src, dest, name, legacyName, dryRun, log) {
|
|
804
|
+
let srcContent;
|
|
805
|
+
try {
|
|
806
|
+
srcContent = fs.readFileSync(src, 'utf8');
|
|
807
|
+
} catch (err) {
|
|
808
|
+
log(`ERROR: cannot read template ${src}: ${err.message}`);
|
|
809
|
+
return false;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
const managedContent = codexSkillManagedContent(name, srcContent);
|
|
813
|
+
const managedBodies = [srcContent, managedContent];
|
|
814
|
+
|
|
815
|
+
let destStat;
|
|
816
|
+
try { destStat = fs.lstatSync(dest); } catch { destStat = null; }
|
|
817
|
+
|
|
818
|
+
if (destStat) {
|
|
819
|
+
if (destStat.isSymbolicLink()) {
|
|
820
|
+
log(`ERROR: codex-skill dest is a symlink — refusing: ${dest}`);
|
|
821
|
+
return false;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
let existing;
|
|
825
|
+
try { existing = fs.readFileSync(dest, 'utf8'); } catch (err) {
|
|
826
|
+
log(`ERROR: cannot read existing Codex skill ${dest}: ${err.message}`);
|
|
827
|
+
return false;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
if (!isManagedCodexSkillContent(existing, name, managedBodies)) {
|
|
831
|
+
log(`[codex-skill] preserved user-edited Codex skill: ${dest}`);
|
|
832
|
+
log(`[codex-skill] next step: compare with integrations/codex/skills/${name}/SKILL.md and manually merge if desired`);
|
|
833
|
+
return true;
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
if (existing === managedContent) {
|
|
837
|
+
log(`[codex-skill] up to date: ${dest}`);
|
|
838
|
+
return true;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
if (dryRun) {
|
|
842
|
+
log(`[dry-run] would refresh managed Codex skill ${dest}`);
|
|
843
|
+
return true;
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
const res = safeWrite(dest, managedContent);
|
|
847
|
+
if (!res.ok) {
|
|
848
|
+
log(`ERROR: failed to refresh codex-skill ${dest}: ${res.reason}`);
|
|
849
|
+
return false;
|
|
850
|
+
}
|
|
851
|
+
log(`[codex-skill] refreshed managed Codex skill: ${dest}`);
|
|
852
|
+
return true;
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
if (dryRun) {
|
|
856
|
+
log(`[dry-run] would create ${dest}`);
|
|
857
|
+
return true;
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
if (!safeMkdirp(dest)) {
|
|
861
|
+
log(`ERROR: could not create parent dir for ${dest}`);
|
|
862
|
+
return false;
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
const res = safeWrite(dest, managedContent);
|
|
866
|
+
if (!res.ok) {
|
|
867
|
+
log(`ERROR: failed to write codex-skill ${dest}: ${res.reason}`);
|
|
868
|
+
return false;
|
|
869
|
+
}
|
|
870
|
+
log(`[codex-skill] wrote ${dest}`);
|
|
871
|
+
return true;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
function migrateLegacyCodexSkill(dest, legacyName, namespacedName, knownTemplate, dryRun, log) {
|
|
875
|
+
let destStat;
|
|
876
|
+
try { destStat = fs.lstatSync(dest); } catch { return true; }
|
|
877
|
+
|
|
878
|
+
if (destStat.isSymbolicLink()) {
|
|
879
|
+
log(`ERROR: legacy codex-skill dest is a symlink — refusing: ${dest}`);
|
|
880
|
+
return false;
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
let existing;
|
|
884
|
+
try { existing = fs.readFileSync(dest, 'utf8'); } catch (err) {
|
|
885
|
+
log(`ERROR: cannot read legacy Codex skill ${dest}: ${err.message}`);
|
|
886
|
+
return false;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
const shim = legacyCodexSkillContent(legacyName, namespacedName);
|
|
890
|
+
const managedBodies = [
|
|
891
|
+
knownTemplate,
|
|
892
|
+
legacyGenericCodexTemplate(knownTemplate, legacyName, namespacedName),
|
|
893
|
+
LEGACY_CODEX_SKILL_TEMPLATES[legacyName],
|
|
894
|
+
shim,
|
|
895
|
+
].filter(Boolean);
|
|
896
|
+
if (!isManagedCodexSkillContent(existing, legacyName, managedBodies)) {
|
|
897
|
+
log(`[codex-skill] preserved user-edited legacy Codex skill: ${dest}`);
|
|
898
|
+
log(`[codex-skill] next step: rename or merge it into .agents/skills/${namespacedName}/SKILL.md if you still need custom behavior`);
|
|
899
|
+
return true;
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
if (existing === shim) {
|
|
903
|
+
log(`[codex-skill] legacy compatibility up to date: ${dest}`);
|
|
904
|
+
return true;
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
if (dryRun) {
|
|
908
|
+
log(`[dry-run] would migrate legacy Codex skill ${dest}`);
|
|
909
|
+
return true;
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
const res = safeWrite(dest, shim);
|
|
913
|
+
if (!res.ok) {
|
|
914
|
+
log(`ERROR: failed to migrate legacy codex-skill ${dest}: ${res.reason}`);
|
|
915
|
+
return false;
|
|
916
|
+
}
|
|
917
|
+
log(`[codex-skill] migrated legacy Codex skill to compatibility shim: ${dest}`);
|
|
918
|
+
return true;
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
function installCodexSkills(projectRoot, userGlobal, dryRun, log) {
|
|
922
|
+
const skillsRoot = userGlobal
|
|
923
|
+
? path.join(homeDir(), '.agents', 'skills')
|
|
924
|
+
: path.join(projectRoot, '.agents', 'skills');
|
|
925
|
+
|
|
926
|
+
let ok = true;
|
|
927
|
+
for (const skill of CODEX_SKILLS) {
|
|
928
|
+
const src = path.join(PKG_ROOT, 'integrations', 'codex', 'skills', skill.name, 'SKILL.md');
|
|
929
|
+
const dest = path.join(skillsRoot, skill.name, 'SKILL.md');
|
|
930
|
+
if (!installManagedCodexSkill(src, dest, skill.name, skill.legacy, dryRun, log)) ok = false;
|
|
931
|
+
|
|
932
|
+
let legacyTemplate = '';
|
|
933
|
+
try { legacyTemplate = fs.readFileSync(src, 'utf8'); } catch {}
|
|
934
|
+
const legacyDest = path.join(skillsRoot, skill.legacy, 'SKILL.md');
|
|
935
|
+
if (!migrateLegacyCodexSkill(legacyDest, skill.legacy, skill.name, legacyTemplate, dryRun, log)) ok = false;
|
|
936
|
+
}
|
|
937
|
+
return ok;
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
// ---- main entry ----
|
|
941
|
+
|
|
942
|
+
/**
|
|
943
|
+
* Run the installer. Returns a numeric exit code (0 = success).
|
|
944
|
+
* @param {string[]} argv
|
|
945
|
+
* @returns {number}
|
|
946
|
+
*/
|
|
947
|
+
function run(argv) {
|
|
948
|
+
const opts = parseArgs(argv || []);
|
|
949
|
+
const { target: rawTarget, project, user: userGlobal, dryRun } = opts;
|
|
950
|
+
|
|
951
|
+
const lines = [];
|
|
952
|
+
const log = (msg) => { lines.push(msg); process.stdout.write(msg + '\n'); };
|
|
953
|
+
|
|
954
|
+
if (dryRun) log('[dry-run] planning only — no files will be written');
|
|
955
|
+
|
|
956
|
+
// Resolve target
|
|
957
|
+
let target = rawTarget;
|
|
958
|
+
if (target === 'auto') {
|
|
959
|
+
target = detectTarget(project);
|
|
960
|
+
if (target === 'none') {
|
|
961
|
+
log('[auto] no tool marker dir found — installing doctrine + engine only');
|
|
962
|
+
log('[auto] pass --target <tool> to install a command wrapper');
|
|
963
|
+
} else {
|
|
964
|
+
log(`[auto] detected target: ${target}`);
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
const VALID_TARGETS = ['auto', 'claude', 'codex', 'cursor', 'gemini', 'cline', 'windsurf'];
|
|
969
|
+
if (!VALID_TARGETS.includes(rawTarget)) {
|
|
970
|
+
log(`ERROR: unknown --target value: ${rawTarget}`);
|
|
971
|
+
return 1;
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
let anyError = false;
|
|
975
|
+
|
|
976
|
+
// 1. Doctrine — portable AGENTS.md kernel + this target's runtime adapter.
|
|
977
|
+
if (!installDoctrine(project, dryRun, log)) anyError = true;
|
|
978
|
+
if (!installAdapter(target, project, dryRun, log)) anyError = true;
|
|
979
|
+
|
|
980
|
+
// 2. Engine — frontier/ + bin/maestro.cjs + docs/orchestration.md.
|
|
981
|
+
if (!installEngine(project, dryRun, log)) anyError = true;
|
|
982
|
+
|
|
983
|
+
// 3. Wrapper — this target's /frontier command (skip if no target detected).
|
|
984
|
+
if (target !== 'none') {
|
|
985
|
+
if (!installWrapper(target, project, userGlobal, dryRun, log)) anyError = true;
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
// 3b. Codex skills — the .agents/skills/<name>/SKILL.md set ships alongside
|
|
989
|
+
// the deprecated codex prompt wrapper.
|
|
990
|
+
if (target === 'codex') {
|
|
991
|
+
if (!installCodexSkills(project, userGlobal, dryRun, log)) anyError = true;
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
if (anyError) {
|
|
995
|
+
log('install completed with errors (see above)');
|
|
996
|
+
return 1;
|
|
997
|
+
}
|
|
998
|
+
log('install complete');
|
|
999
|
+
return 0;
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
// ---- CLI entry ----
|
|
1003
|
+
|
|
1004
|
+
if (require.main === module) {
|
|
1005
|
+
const code = run(process.argv.slice(2));
|
|
1006
|
+
process.exit(code);
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
module.exports = {
|
|
1010
|
+
run,
|
|
1011
|
+
_test: {
|
|
1012
|
+
LEGACY_CODEX_SKILL_TEMPLATES,
|
|
1013
|
+
},
|
|
1014
|
+
};
|