vibecoding-devtools 1.0.1 → 1.1.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/README.md +6 -5
- package/hooks/maestri-protocol.mjs +4 -3
- package/installer/cli.mjs +8 -0
- package/installer/codex-maestri.rules +13 -0
- package/installer/env-list.mjs +70 -0
- package/installer/execute.mjs +409 -386
- package/installer/json-merge.mjs +4 -4
- package/installer/plan.mjs +31 -1
- package/installer/team-checks.mjs +86 -0
- package/installer/team.mjs +260 -0
- package/npm-shrinkwrap.json +2 -2
- package/package.json +4 -2
- package/rules/maestri-usage.md +2 -0
- package/skills/maestri-agent-cli/SKILL.md +4 -2
- package/skills/maestri-agent-cli/references/antigravity.md +24 -0
- package/skills/maestri-agent-cli/references/codex.md +16 -0
- package/skills/maestri-protocol/SKILL.md +12 -7
- package/team/README.md +139 -0
- package/team/notes/project-context/acceptance-criteria.md +6 -0
- package/team/notes/project-context/code-standards.md +8 -0
- package/team/notes/project-context/contracts-integrations.md +10 -0
- package/team/notes/project-context/decisions.md +7 -0
- package/team/notes/project-context/design-system.md +8 -0
- package/team/notes/project-context/ops-runbook.md +10 -0
- package/team/notes/project-context/requirements.md +23 -0
- package/team/notes/project-context/risks-backlog.md +6 -0
- package/team/notes/team-roster.md +67 -0
- package/team/partitura/vibecoding-devtools.maestripartitura +4908 -0
- package/team/roles/_team-protocol.md +12 -0
- package/team/roles/appsec-auditor.md +45 -0
- package/team/roles/backend-architect.md +44 -0
- package/team/roles/backend-engineer.md +44 -0
- package/team/roles/code-reviewer.md +43 -0
- package/team/roles/devops-engineer.md +41 -0
- package/team/roles/frontend-engineer.md +45 -0
- package/team/roles/qa-engineer.md +41 -0
- package/team/roles/research-analyst.md +34 -0
- package/team/roles/task-runner.md +26 -0
- package/team/roles/team-maestro.md +80 -0
- package/team/roles/ux-ui-designer.md +49 -0
- package/team/roles/visual-asset-designer.md +45 -0
- package/team/roles/visual-qa-inspector.md +36 -0
- package/team/settings/task-runner-limited.json +55 -0
- package/team/team.json +59 -0
package/installer/json-merge.mjs
CHANGED
|
@@ -3,7 +3,7 @@ import path from 'node:path';
|
|
|
3
3
|
import { hashContent } from './state.mjs';
|
|
4
4
|
|
|
5
5
|
// Keep source ranges so edits never reserialize unrelated settings or hooks.
|
|
6
|
-
function parse(text) {
|
|
6
|
+
export function parse(text) {
|
|
7
7
|
JSON.parse(text.replace(/^\uFEFF/, ''));
|
|
8
8
|
let offset = text.charCodeAt(0) === 0xFEFF ? 1 : 0;
|
|
9
9
|
const whitespace = () => { while (/\s/.test(text[offset] ?? '') && offset < text.length) offset++; };
|
|
@@ -53,7 +53,7 @@ function parse(text) {
|
|
|
53
53
|
return root;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
const property = (node, key) => node.children.find((child) => child.key === key);
|
|
56
|
+
export const property = (node, key) => node.children.find((child) => child.key === key);
|
|
57
57
|
const normalizePath = (file) => file.replace(/\\/g, '/');
|
|
58
58
|
function ownsHook(hook, action) {
|
|
59
59
|
if (!hook || typeof hook !== 'object') return false;
|
|
@@ -115,7 +115,7 @@ export function jsonMergeSnapshot(current, action) {
|
|
|
115
115
|
return { hash: hashContent(JSON.stringify(entries.map((entry) => entry.fingerprint))), count: entries.length };
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
-
function removeChild(text, parent, entry) {
|
|
118
|
+
export function removeChild(text, parent, entry) {
|
|
119
119
|
const index = parent.children.indexOf(entry);
|
|
120
120
|
const next = parent.children[index + 1];
|
|
121
121
|
const previous = parent.children[index - 1];
|
|
@@ -150,7 +150,7 @@ function removeEmptyContainers(text, paths) {
|
|
|
150
150
|
return text;
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
-
function append(text, parent, key, value) {
|
|
153
|
+
export function append(text, parent, key, value) {
|
|
154
154
|
const eol = text.includes('\r\n') ? '\r\n' : '\n';
|
|
155
155
|
const unit = text.match(/\n([ \t]+)"/)?.[1] ?? ' ';
|
|
156
156
|
const lineStart = text.lastIndexOf('\n', parent.start) + 1;
|
package/installer/plan.mjs
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
// Read-only planning performs no writes, network calls, execution or real-home access.
|
|
2
2
|
import fs from "node:fs";
|
|
3
|
+
import os from "node:os";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
import { isUtf8 } from "node:buffer";
|
|
5
6
|
import { readState, hashContent } from "./state.mjs";
|
|
6
7
|
import { convertAntigravityRule, readFrontmatter, buildCodexBlock } from "./convert.mjs";
|
|
7
8
|
import { hookActions } from './hooks.mjs';
|
|
8
9
|
import { jsonMergeSituation } from './json-merge.mjs';
|
|
10
|
+
import { envListSituation } from './env-list.mjs';
|
|
9
11
|
|
|
10
12
|
const CLIS = ["claude", "codex", "agy"];
|
|
11
13
|
const uniqueList = (items = []) => [...new Set(items)].sort();
|
|
@@ -84,6 +86,7 @@ function situationOf(action, items) {
|
|
|
84
86
|
let previous = items.find((item) => item.type === action.type && samePath(item.destination, action.destination));
|
|
85
87
|
if (previous?.pending && previous.hashBefore === hashContent(current)) previous = previous.previous;
|
|
86
88
|
if (action.type === 'json-merge') return jsonMergeSituation(current, action, previous);
|
|
89
|
+
if (action.type === 'env-list') return envListSituation(current, action, previous);
|
|
87
90
|
if (action.type === "block") {
|
|
88
91
|
if (!isUtf8(current)) return "conflict";
|
|
89
92
|
const block = extractBlock(current.toString("utf8"));
|
|
@@ -135,11 +138,25 @@ export function reconcileUpdate(plan, { home }) {
|
|
|
135
138
|
item.type === 'block' ? item.marker ?? START : null]);
|
|
136
139
|
const wanted = new Set(actions.filter((a) => a.destination).map(key));
|
|
137
140
|
const removals = state.items.filter((item) => item.owner === 'kit'
|
|
138
|
-
&& ['copy', 'write', 'block', 'json-merge'].includes(item.type) && !wanted.has(key(item)))
|
|
141
|
+
&& ['copy', 'write', 'block', 'json-merge', 'env-list'].includes(item.type) && !wanted.has(key(item)))
|
|
139
142
|
.map((item) => ({ ...item, operation: 'remove', situation: 'remove' }));
|
|
140
143
|
return Object.assign([...removals, ...actions], { warnings: plan.warnings, preparation: plan.preparation });
|
|
141
144
|
}
|
|
142
145
|
|
|
146
|
+
// ECC's GateGuard asks for facts before the first edit of each file. These trees have no import value and agents
|
|
147
|
+
// write to them all the time: Claude Code's temp directory (scratchpad), its auto memory and the Maestri role folders.
|
|
148
|
+
// The temp directory follows CLAUDE_CODE_TMPDIR as Claude Code documents it: <dir>/claude-<uid> on Unix,
|
|
149
|
+
// <dir>/claude on Windows, with /tmp or the system temp directory as <dir>.
|
|
150
|
+
export function gateguardExemptions({ home, maestri, env = process.env, platform = process.platform,
|
|
151
|
+
uid = process.getuid?.() }) {
|
|
152
|
+
const slash = (file) => file.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
153
|
+
const windows = platform === "win32";
|
|
154
|
+
const base = slash(env.CLAUDE_CODE_TMPDIR || (windows ? os.tmpdir() : "/tmp"));
|
|
155
|
+
const temp = windows ? `${base}/claude` : uid == null ? null : `${base}/claude-${uid}`;
|
|
156
|
+
return [...(temp ? [`${temp}/**`] : []), `${slash(home)}/.claude/projects/*/memory/**`,
|
|
157
|
+
...(maestri ? ["**/.maestri/**"] : [])];
|
|
158
|
+
}
|
|
159
|
+
|
|
143
160
|
export function buildPlan(choices, detection, paths) {
|
|
144
161
|
const { home, kit, ecc, project } = paths;
|
|
145
162
|
if (!home || !kit) throw new Error("paths.home and paths.kit are required");
|
|
@@ -206,6 +223,12 @@ export function buildPlan(choices, detection, paths) {
|
|
|
206
223
|
add("copy", "kit", cli, path.join(kit, "skills", skill.name),
|
|
207
224
|
path.join(destinationSkills, skill.name), null, `Kit skill: ${skill.name}`);
|
|
208
225
|
}
|
|
226
|
+
// Agents message each other through the maestri CLI, which runs outside Codex's sandbox. The rules go to the
|
|
227
|
+
// user layer in either scope so every workspace's agents get them.
|
|
228
|
+
if (cli === "codex" && choices.maestri) {
|
|
229
|
+
add("copy", "kit", cli, path.join(kit, "installer", "codex-maestri.rules"),
|
|
230
|
+
path.join(home, ".codex", "rules", "vibecoding-devtools-maestri.rules"), null, "Maestri command rules for Codex");
|
|
231
|
+
}
|
|
209
232
|
}
|
|
210
233
|
if (eccClis.includes(cli)) {
|
|
211
234
|
const packages = uniqueList(["common", ...(cli === "claude" ? ["web"] : []),
|
|
@@ -249,6 +272,13 @@ export function buildPlan(choices, detection, paths) {
|
|
|
249
272
|
}
|
|
250
273
|
}
|
|
251
274
|
actions.push(...hookActions(choices, { home, kit, project }));
|
|
275
|
+
// Any ECC on Claude: the ECC plugin brings GateGuard even with the kit's ECC hooks off. The paths are this machine's,
|
|
276
|
+
// so they go to the user settings in either scope, never to a project's file.
|
|
277
|
+
if (eccClis.includes("claude")) {
|
|
278
|
+
actions.push({ type: "env-list", owner: "kit", cli: "claude", destination: path.join(home, ".claude", "settings.json"),
|
|
279
|
+
key: "GATEGUARD_EXEMPT_GLOBS", values: gateguardExemptions({ home, maestri: choices.maestri }),
|
|
280
|
+
description: "GateGuard exemptions for scratch, memory and Maestri files" });
|
|
281
|
+
}
|
|
252
282
|
const plan = actions.map((action) => ({ ...action, situation: situationOf(action, state.items) }));
|
|
253
283
|
if (warnings.length) plan.warnings = warnings;
|
|
254
284
|
return plan;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// Read-only prerequisites of the team install: the CLIs, the models the launch commands name, and ECC in Claude Code.
|
|
2
|
+
// Everything that touches the machine goes through `tools`, so tests inject fakes instead of running real CLIs.
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import { execFileSync } from 'node:child_process';
|
|
5
|
+
import { findIn, pathDirectories } from './detect.mjs';
|
|
6
|
+
|
|
7
|
+
const binaryOf = (command) => command.trim().split(/\s+/)[0];
|
|
8
|
+
const modelOf = (command) => command.match(/(?:^|\s)(?:--model|-m)[ =](\S+)/)?.[1];
|
|
9
|
+
|
|
10
|
+
// The commands the install launches: presets, and agents that carry their own command instead of a preset.
|
|
11
|
+
const launchCommands = (manifest) => [
|
|
12
|
+
...manifest.presets.map((p) => ({ label: `preset "${p.name}"`, command: p.command })),
|
|
13
|
+
...manifest.agents.filter((a) => a.command).map((a) => ({ label: `agent "${a.name}"`, command: a.command })),
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
// Model ids a CLI lists: `codex debug models` prints JSON with `slug`; `agy models` prints `<id>\t<name>` lines.
|
|
17
|
+
const MODEL_LISTS = {
|
|
18
|
+
codex: { source: 'codex debug models', read: (tools) => JSON.parse(tools.codexModels()).models.map((m) => m.slug) },
|
|
19
|
+
agy: { source: 'agy models', read: (tools) => tools.agyModels().split('\n').filter((l) => l.includes('\t'))
|
|
20
|
+
.map((l) => l.split(/\s+/)[0]) },
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const attempt = (what, fn) => {
|
|
24
|
+
try {
|
|
25
|
+
return { value: fn() };
|
|
26
|
+
} catch (e) {
|
|
27
|
+
return { problem: `could not run ${what}: ${(e.stderr?.toString().trim() || e.message).split('\n')[0]}` };
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// Missing items, all together; an empty list means the run may start. `rolesOnly` creates roles and launches nothing,
|
|
32
|
+
// so it needs only the maestri binary.
|
|
33
|
+
export function missingPrerequisites(manifest, tools, { maestri = 'maestri', rolesOnly = false } = {}) {
|
|
34
|
+
const launches = rolesOnly ? [] : launchCommands(manifest);
|
|
35
|
+
const bins = [...new Set([maestri, ...launches.map((l) => binaryOf(l.command))])];
|
|
36
|
+
const absent = new Set(bins.filter((bin) => !tools.onPath(bin)));
|
|
37
|
+
const problems = [...absent].map((bin) => `${bin} is not on PATH`);
|
|
38
|
+
|
|
39
|
+
const catalogs = new Map();
|
|
40
|
+
for (const { label, command } of launches) {
|
|
41
|
+
const bin = binaryOf(command);
|
|
42
|
+
const model = modelOf(command);
|
|
43
|
+
if (absent.has(bin) || !model || !MODEL_LISTS[bin]) continue;
|
|
44
|
+
if (!catalogs.has(bin)) {
|
|
45
|
+
const { source, read } = MODEL_LISTS[bin];
|
|
46
|
+
catalogs.set(bin, attempt(source, () => new Set(read(tools))));
|
|
47
|
+
}
|
|
48
|
+
const { value, problem } = catalogs.get(bin);
|
|
49
|
+
if (problem) {
|
|
50
|
+
if (!problems.includes(problem)) problems.push(problem);
|
|
51
|
+
} else if (!value.has(model)) {
|
|
52
|
+
problems.push(`${label}: model "${model}" is not in ${MODEL_LISTS[bin].source}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (bins.includes('claude') && !absent.has('claude')) {
|
|
57
|
+
const { value, problem } = attempt('claude plugin list', () => tools.claudePlugins());
|
|
58
|
+
if (problem) problems.push(problem);
|
|
59
|
+
else if (!/\becc@/.test(value)) {
|
|
60
|
+
problems.push('ECC is not installed in Claude Code (claude plugin list shows no ecc@): '
|
|
61
|
+
+ 'the Team Maestro role uses /ecc:plan');
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return problems;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// A path as given, or a name found on PATH (PATHEXT on Windows).
|
|
68
|
+
function onPath(bin, env, platform) {
|
|
69
|
+
if (/[\\/]/.test(bin)) return fs.existsSync(bin);
|
|
70
|
+
try {
|
|
71
|
+
return Boolean(findIn(bin, pathDirectories(env), { env, platform }));
|
|
72
|
+
} catch {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const BIG_OUTPUT = 64 * 1024 * 1024;
|
|
78
|
+
const runCli = (bin, args) => execFileSync(bin, args,
|
|
79
|
+
{ encoding: 'utf8', maxBuffer: BIG_OUTPUT, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
80
|
+
|
|
81
|
+
export const realTools = ({ env = process.env, platform = process.platform } = {}) => ({
|
|
82
|
+
onPath: (bin) => onPath(bin, env, platform),
|
|
83
|
+
codexModels: () => runCli('codex', ['debug', 'models']),
|
|
84
|
+
agyModels: () => runCli('agy', ['models']),
|
|
85
|
+
claudePlugins: () => runCli('claude', ['plugin', 'list']),
|
|
86
|
+
});
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
// Install the kit's Maestri team from team/team.json through the maestri CLI: roles, presets, agents, ropes, notes,
|
|
2
|
+
// portal and partitura. Maestri may run on another computer; this runs where the agent CLIs and `maestri` run.
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { execFileSync } from 'node:child_process';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
import { missingPrerequisites, realTools } from './team-checks.mjs';
|
|
9
|
+
|
|
10
|
+
export const TEAM_HELP = `Usage: vibecoding-devtools team [--roles-only] [--update-roles] [--dry-run]
|
|
11
|
+
Run inside a Maestri terminal in Maestro mode, on the machine where the agent CLIs run.
|
|
12
|
+
--roles-only Create the team's missing global roles only
|
|
13
|
+
--update-roles Also rewrite what differs from the kit: installed roles (restarts the agents that use them), the Maestro
|
|
14
|
+
preset command and the Task Runner settings file
|
|
15
|
+
--dry-run Print the full maestri commands without running them
|
|
16
|
+
Without --roles-only it also writes the Task Runner settings file to ~/.vibecoding-devtools/team/, creates the Maestro
|
|
17
|
+
preset, recruits the agents, notes and portal you do not reach yet, wires them, and saves the global partitura unless
|
|
18
|
+
one with its name exists.
|
|
19
|
+
Before any change, also with --dry-run, it checks read-only that the CLIs, the models in team.json and ECC in Claude
|
|
20
|
+
Code exist; missing items or conflicts stop the run with exit 1. If a run fails midway, run the same command again: it
|
|
21
|
+
skips what exists and finishes the rest.`;
|
|
22
|
+
|
|
23
|
+
const FLAGS = ['--roles-only', '--update-roles', '--dry-run'];
|
|
24
|
+
|
|
25
|
+
// `- "Name"` entries of `maestri role|preset|partitura list`.
|
|
26
|
+
export const listedNames = (output) => [...output.matchAll(/^\s*- "([^"]+)"/gm)].map((m) => m[1]);
|
|
27
|
+
|
|
28
|
+
// Top-level sections of `maestri list` (what the caller reaches); nested `⎿` lines belong to other agents' trees.
|
|
29
|
+
export function canvasEntries(output) {
|
|
30
|
+
const found = { agents: new Map(), notes: new Set(), portals: new Set(), ficharios: new Set() };
|
|
31
|
+
let section = null;
|
|
32
|
+
for (const line of output.split('\n')) {
|
|
33
|
+
if (/^\S/.test(line)) section = /^Connected (agents|notes|portals)/.exec(line)?.[1] ?? null;
|
|
34
|
+
else if (section && !line.trimStart().startsWith('⎿')) {
|
|
35
|
+
const fichario = /^\s*fichário "([^"]+)":/.exec(line);
|
|
36
|
+
const entry = /^\s*- name: "([^"]+)"(?:, role: "([^"]+)")?/.exec(line);
|
|
37
|
+
if (fichario) found.ficharios.add(fichario[1]);
|
|
38
|
+
else if (entry && section === 'agents') found.agents.set(entry[1], entry[2] ?? null);
|
|
39
|
+
else if (entry) found[section].add(entry[1]);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return found;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export const roleText = (role, protocol) => `${role.trimEnd()}\n\n${protocol.trimEnd()}`;
|
|
46
|
+
|
|
47
|
+
// `maestri preset show` prints `Command: ...` and `Maestro: on|off`.
|
|
48
|
+
export const presetShown = (output) => ({
|
|
49
|
+
command: output.match(/^Command: (.*)$/m)?.[1]?.trim(),
|
|
50
|
+
maestro: /^Maestro: on$/m.test(output),
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// maestri decodes \n and \\ in note content; keep backslashes literal.
|
|
54
|
+
const noteText = (text) => text.trimEnd().replaceAll('\\', '\\\\');
|
|
55
|
+
|
|
56
|
+
// Session settings live in a stable file under the home folder, not in the kit's (npx cache) folder.
|
|
57
|
+
export const settingsPath = (home, file) => path.join(home, '.vibecoding-devtools', 'team', path.basename(file));
|
|
58
|
+
|
|
59
|
+
// One shell argument that POSIX shells and PowerShell read the same way.
|
|
60
|
+
function shellArgument(value) {
|
|
61
|
+
if (/["`$]/.test(value)) throw new Error(`The path ${value} has a character the agent command cannot quote.`);
|
|
62
|
+
return /^[\w./\\:@-]+$/.test(value) ? value : `"${value}"`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function agentCommand(agent, home) {
|
|
66
|
+
if (!agent.settingsFile) return agent.command;
|
|
67
|
+
return agent.command.replace('{settings}', () => shellArgument(settingsPath(home, agent.settingsFile)));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Files the team needs on the machine that runs the agents: [{ path, content }].
|
|
71
|
+
export const teamFiles = (manifest, read, home) => manifest.agents.filter((a) => a.settingsFile).map((a) => ({
|
|
72
|
+
path: settingsPath(home, a.settingsFile),
|
|
73
|
+
content: `${JSON.stringify(JSON.parse(read(a.settingsFile)), null, 2)}\n`,
|
|
74
|
+
}));
|
|
75
|
+
|
|
76
|
+
const presetDiffers = (found, preset) => found.command !== preset.command || found.maestro !== Boolean(preset.maestro);
|
|
77
|
+
|
|
78
|
+
const kitRoles = (manifest, read) => {
|
|
79
|
+
const protocol = read(manifest.protocol);
|
|
80
|
+
return manifest.agents.map(({ role, roleFile }) => ({ role, text: roleText(read(roleFile), protocol) }));
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
// Everything that would make the run touch something that is not the kit's: checked before any change.
|
|
84
|
+
export function teamConflicts(manifest, read, existing, options = {}) {
|
|
85
|
+
const { rolesOnly = false, updateRoles = false, home = os.homedir() } = options;
|
|
86
|
+
const conflicts = [];
|
|
87
|
+
for (const { role, text } of kitRoles(manifest, read)) {
|
|
88
|
+
if (existing.roles.has(role) && existing.roles.get(role) !== text && !updateRoles) {
|
|
89
|
+
conflicts.push(`role "${role}" exists with other text; rerun with --update-roles to replace it`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (rolesOnly) return conflicts;
|
|
93
|
+
for (const preset of manifest.presets) {
|
|
94
|
+
const found = existing.presets.get(preset.name);
|
|
95
|
+
if (found && presetDiffers(found, preset) && !updateRoles) {
|
|
96
|
+
conflicts.push(`preset "${preset.name}" exists with another command or Maestro setting; `
|
|
97
|
+
+ 'rerun with --update-roles to replace it');
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
for (const { path: file, content } of teamFiles(manifest, read, home)) {
|
|
101
|
+
if (existing.files.has(file) && existing.files.get(file) !== content && !updateRoles) {
|
|
102
|
+
conflicts.push(`settings file ${file} exists with other content; rerun with --update-roles to replace it`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
const { agents, notes, portals, ficharios } = existing.canvas;
|
|
106
|
+
const kinds = { agent: agents, note: notes, portal: portals, fichário: ficharios };
|
|
107
|
+
const misused = (name, kind) => Object.entries(kinds).find(([k, set]) => k !== kind && set.has(name))?.[0];
|
|
108
|
+
for (const agent of manifest.agents) {
|
|
109
|
+
if (agents.has(agent.name) && agents.get(agent.name) !== agent.role) {
|
|
110
|
+
conflicts.push(`agent "${agent.name}" exists with role "${agents.get(agent.name)}", not "${agent.role}"`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
const named = [...manifest.agents.map((a) => [a.name, 'agent']), ...manifest.notes.map((n) => [n.name, 'note']),
|
|
114
|
+
...manifest.fichario.pages.map((n) => [n.name, 'note']), [manifest.fichario.name, 'fichário'],
|
|
115
|
+
...manifest.portals.map((x) => [x.name, 'portal'])];
|
|
116
|
+
for (const [name, kind] of named) {
|
|
117
|
+
const other = misused(name, kind);
|
|
118
|
+
if (other) conflicts.push(`${other} "${name}" you reach has a name the kit needs for its ${kind}`);
|
|
119
|
+
}
|
|
120
|
+
return conflicts;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Pure plan: maestri argument lists, skipping what already exists.
|
|
124
|
+
export function teamCommands(manifest, read, existing, options = {}) {
|
|
125
|
+
const { rolesOnly = false, updateRoles = false, home = os.homedir() } = options;
|
|
126
|
+
const commands = [];
|
|
127
|
+
for (const { role, text } of kitRoles(manifest, read)) {
|
|
128
|
+
if (!existing.roles.has(role)) commands.push(['role', 'create', role, text, '--scope', 'global']);
|
|
129
|
+
else if (updateRoles && existing.roles.get(role) !== text) commands.push(['role', 'write', role, text]);
|
|
130
|
+
}
|
|
131
|
+
if (rolesOnly) return commands;
|
|
132
|
+
|
|
133
|
+
for (const preset of manifest.presets) {
|
|
134
|
+
const found = existing.presets.get(preset.name);
|
|
135
|
+
const maestro = preset.maestro ? 'on' : 'off';
|
|
136
|
+
const settings = ['--command', preset.command, '--maestro', maestro];
|
|
137
|
+
if (!found) commands.push(['preset', 'create', preset.name, ...settings, '--quick-start', 'off']);
|
|
138
|
+
else if (updateRoles && presetDiffers(found, preset)) commands.push(['preset', 'set', preset.name, ...settings]);
|
|
139
|
+
}
|
|
140
|
+
for (const agent of manifest.agents.filter((a) => !existing.canvas.agents.has(a.name))) {
|
|
141
|
+
const launch = agent.preset ? ['--preset', agent.preset] : ['--command', agentCommand(agent, home)];
|
|
142
|
+
commands.push(['recruit', agent.name, '--role', agent.role, '--color', agent.color, '--icon', agent.icon, ...launch]);
|
|
143
|
+
}
|
|
144
|
+
const others = manifest.agents.map((a) => a.name).filter((n) => n !== manifest.maestro);
|
|
145
|
+
for (const [a, b] of [...others.map((n) => [manifest.maestro, n]), ...manifest.peers]) commands.push(['connect', a, b]);
|
|
146
|
+
for (const note of manifest.notes) {
|
|
147
|
+
if (!existing.canvas.notes.has(note.name)) commands.push(['note', 'create', noteText(read(note.file)), '--name', note.name, '--color', note.color]);
|
|
148
|
+
for (const agent of note.connect) commands.push(['connect', note.name, agent]);
|
|
149
|
+
}
|
|
150
|
+
for (const page of manifest.fichario.pages.filter((p) => !existing.canvas.notes.has(p.name))) {
|
|
151
|
+
commands.push(['note', 'create', noteText(read(page.file)), '--name', page.name, '--stack', manifest.fichario.name]);
|
|
152
|
+
}
|
|
153
|
+
for (const agent of manifest.fichario.connect) commands.push(['connect', manifest.fichario.name, agent]);
|
|
154
|
+
for (const portal of manifest.portals) {
|
|
155
|
+
if (!existing.canvas.portals.has(portal.name)) commands.push(['portal', 'create', portal.url, portal.name]);
|
|
156
|
+
for (const agent of portal.connect) commands.push(['connect', agent, portal.name]);
|
|
157
|
+
}
|
|
158
|
+
const { partitura } = manifest;
|
|
159
|
+
if (!existing.partituras.has(partitura.name)) {
|
|
160
|
+
commands.push(['partitura', 'create', partitura.name, ...manifest.agents.map((a) => a.name), '--without-me',
|
|
161
|
+
'--scope', 'global', '--commands', 'on', '--note-text', 'on', '--chat-history', 'off',
|
|
162
|
+
'--description', partitura.description, '--icon', partitura.icon, '--color', partitura.color]);
|
|
163
|
+
}
|
|
164
|
+
return commands;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Settings files to write: the ones missing, and with --update-roles the ones that differ. Never with --roles-only.
|
|
168
|
+
export function teamFileWrites(manifest, read, existing, options = {}) {
|
|
169
|
+
const { rolesOnly = false, updateRoles = false, home = os.homedir() } = options;
|
|
170
|
+
if (rolesOnly) return [];
|
|
171
|
+
return teamFiles(manifest, read, home).filter(({ path: file, content }) => !existing.files.has(file)
|
|
172
|
+
|| (updateRoles && existing.files.get(file) !== content));
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const quoted = (a) => (/^[\w./#@:-]+$/.test(a) ? a : JSON.stringify(a));
|
|
176
|
+
const shown = (args, full) => `maestri ${args.map((a) => quoted(full || a.length <= 60 ? a : `${a.slice(0, 57)}...`)).join(' ')}`;
|
|
177
|
+
|
|
178
|
+
const readIfExists = (file) => {
|
|
179
|
+
try {
|
|
180
|
+
return fs.readFileSync(file, 'utf8').replace(/\r\n/g, '\n');
|
|
181
|
+
} catch (e) {
|
|
182
|
+
if (e.code === 'ENOENT') return undefined;
|
|
183
|
+
throw e;
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const writeTeamFile = ({ path: file, content }) => {
|
|
188
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
189
|
+
fs.writeFileSync(file, content);
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
// Everything read-only: prerequisites, then what Maestri and the home folder hold now. Throws before any change.
|
|
193
|
+
function planInstall({ manifest, read, run, bin, home, tools, options }) {
|
|
194
|
+
const missing = missingPrerequisites(manifest, tools, { maestri: bin, rolesOnly: options.rolesOnly });
|
|
195
|
+
if (missing.length) throw new Error(`Nothing changed. Missing prerequisites:\n- ${missing.join('\n- ')}`);
|
|
196
|
+
const list = run(bin, ['list']);
|
|
197
|
+
if (!/maestro: true/.test(list.split('\n\n')[0])) throw new Error('Run this from a Maestri terminal in Maestro mode.');
|
|
198
|
+
const roleNames = new Set(listedNames(run(bin, ['role', 'list'])));
|
|
199
|
+
const presetNames = new Set(listedNames(run(bin, ['preset', 'list'])));
|
|
200
|
+
const files = teamFiles(manifest, read, home).map((f) => [f.path, readIfExists(f.path)])
|
|
201
|
+
.filter(([, text]) => text !== undefined);
|
|
202
|
+
const existing = {
|
|
203
|
+
roles: new Map(manifest.agents.filter((a) => roleNames.has(a.role))
|
|
204
|
+
.map((a) => [a.role, run(bin, ['role', 'show', a.role]).replace(/^Role: [^\n]*\n\n/, '').trimEnd()])),
|
|
205
|
+
presets: new Map(manifest.presets.filter((p) => presetNames.has(p.name))
|
|
206
|
+
.map((p) => [p.name, presetShown(run(bin, ['preset', 'show', p.name]))])),
|
|
207
|
+
files: new Map(files),
|
|
208
|
+
canvas: canvasEntries(list),
|
|
209
|
+
partituras: new Set(listedNames(run(bin, ['partitura', 'list']))),
|
|
210
|
+
};
|
|
211
|
+
const conflicts = teamConflicts(manifest, read, existing, options);
|
|
212
|
+
if (conflicts.length) throw new Error(`Nothing changed. Conflicts:\n- ${conflicts.join('\n- ')}`);
|
|
213
|
+
return [
|
|
214
|
+
...teamFileWrites(manifest, read, existing, options).map((file) => {
|
|
215
|
+
const text = `${existing.files.has(file.path) ? 'replace' : 'write'} ${file.path}`;
|
|
216
|
+
return { full: text, brief: text, go: () => writeTeamFile(file) };
|
|
217
|
+
}),
|
|
218
|
+
...teamCommands(manifest, read, existing, options)
|
|
219
|
+
.map((args) => ({ full: shown(args, true), brief: shown(args, false), go: () => run(bin, args) })),
|
|
220
|
+
];
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export async function teamMain(argv, { kit = fileURLToPath(new URL('../', import.meta.url)), out = console.log,
|
|
224
|
+
err = console.error, env = process.env, home = os.homedir(), tools = realTools({ env }),
|
|
225
|
+
run = (bin, args) => execFileSync(bin, args, { encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 }) } = {}) {
|
|
226
|
+
if (argv.includes('--help')) { out(TEAM_HELP); return 0; }
|
|
227
|
+
const unknown = argv.filter((a) => !FLAGS.includes(a));
|
|
228
|
+
if (unknown.length) { err(`Unknown argument: ${unknown[0]}\n${TEAM_HELP}`); return 2; }
|
|
229
|
+
const options = { rolesOnly: argv.includes('--roles-only'), updateRoles: argv.includes('--update-roles'), home };
|
|
230
|
+
const dryRun = argv.includes('--dry-run');
|
|
231
|
+
const dir = path.join(kit, 'team');
|
|
232
|
+
const read = (file) => fs.readFileSync(path.join(dir, file), 'utf8');
|
|
233
|
+
const bin = env.MAESTRI_CLI || 'maestri';
|
|
234
|
+
let steps;
|
|
235
|
+
try {
|
|
236
|
+
steps = planInstall({ manifest: JSON.parse(read('team.json')), read, run, bin, home, tools, options });
|
|
237
|
+
} catch (e) {
|
|
238
|
+
err(e.stderr?.toString().trim() || e.message);
|
|
239
|
+
return 1;
|
|
240
|
+
}
|
|
241
|
+
out('Prerequisites found.');
|
|
242
|
+
if (!steps.length) out('The team is already installed as the kit defines it.');
|
|
243
|
+
for (const [index, step] of steps.entries()) {
|
|
244
|
+
out(dryRun ? step.full : step.brief);
|
|
245
|
+
if (dryRun) continue;
|
|
246
|
+
try {
|
|
247
|
+
step.go();
|
|
248
|
+
} catch (e) {
|
|
249
|
+
err(e.stderr?.toString().trim() || e.message);
|
|
250
|
+
err(`Stopped at step ${index + 1} of ${steps.length} (${index} done). Run the same command again to resume: `
|
|
251
|
+
+ 'it skips what exists and finishes the rest.');
|
|
252
|
+
return 1;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
if (!options.rolesOnly && !dryRun) {
|
|
256
|
+
out('Next: answer each new agent\'s startup prompts as the maestri-agent-cli skill describes, sign in to Gemini '
|
|
257
|
+
+ 'in the portal, and run the check in team/README.md (Check section).');
|
|
258
|
+
}
|
|
259
|
+
return 0;
|
|
260
|
+
}
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vibecoding-devtools",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "vibecoding-devtools",
|
|
9
|
-
"version": "1.0
|
|
9
|
+
"version": "1.1.0",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@clack/prompts": "1.8.1"
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vibecoding-devtools",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Rules, skills, hooks and templates for AI development with Claude Code, Codex and Antigravity.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
8
|
-
"url": "https://github.com/Or4cu1o/vibecoding-devtools"
|
|
8
|
+
"url": "git+https://github.com/Or4cu1o/vibecoding-devtools.git"
|
|
9
9
|
},
|
|
10
10
|
"homepage": "https://github.com/Or4cu1o/vibecoding-devtools#readme",
|
|
11
11
|
"bugs": {
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
"installer/*.mjs",
|
|
17
17
|
"!installer/*.test.mjs",
|
|
18
18
|
"installer/third-party.json",
|
|
19
|
+
"installer/codex-maestri.rules",
|
|
19
20
|
"hooks/*.mjs",
|
|
20
21
|
"!hooks/*.test.mjs",
|
|
21
22
|
"!hooks/README.md",
|
|
@@ -24,6 +25,7 @@
|
|
|
24
25
|
"rules/",
|
|
25
26
|
"skills/",
|
|
26
27
|
"templates/",
|
|
28
|
+
"team/",
|
|
27
29
|
"README.md",
|
|
28
30
|
"LICENSE",
|
|
29
31
|
"npm-shrinkwrap.json"
|
package/rules/maestri-usage.md
CHANGED
|
@@ -10,3 +10,5 @@ Applies only when you are the Maestro in Maestri coordinating other agents. Deta
|
|
|
10
10
|
managed, and only between tasks.
|
|
11
11
|
- Before an unrelated task, clear the agent's context (command in its CLI reference). Never compact or clear an agent
|
|
12
12
|
in the middle of a task.
|
|
13
|
+
- Do not ask the user a question while a delegated task can still report back: the agent's ask back lands in your
|
|
14
|
+
input and can overwrite the user's answer. Collect pending replies first, or warn the user that one may arrive.
|
|
@@ -137,8 +137,10 @@ Answering another agent's permission prompt approves an action on the user's beh
|
|
|
137
137
|
## 5. Configure, safest first
|
|
138
138
|
|
|
139
139
|
1. **Launch flags**: model, effort and permission flags in the preset command, applied with
|
|
140
|
-
`maestri recruit "Name" --command "..."`
|
|
141
|
-
|
|
140
|
+
`maestri recruit "Name" --command "..."` or a preset. On a running agent, `maestri recruit --replace "Name"
|
|
141
|
+
--command "..."` keeps its role, connections and position; it works although `maestri recruit --help` lists only
|
|
142
|
+
`--preset` and `--account` with `--replace` (verified 2026-11-06). Each reference's **Model and effort** says where
|
|
143
|
+
the CLI lists its exact model ids and how to pass them: never guess an id.
|
|
142
144
|
2. **The CLI's settings file**, when the reference names it.
|
|
143
145
|
3. **The TUI menu** (`/model`, `/config`...) through `--raw`, step by step.
|
|
144
146
|
|
|
@@ -71,6 +71,30 @@ See the [CLI permission reference](https://antigravity.google/docs/permissions#c
|
|
|
71
71
|
|
|
72
72
|
`--dangerously-skip-permissions` auto-approves every tool request. Use only at the user's explicit request.
|
|
73
73
|
|
|
74
|
+
### Verified limited agent
|
|
75
|
+
|
|
76
|
+
Verified on Antigravity CLI 1.5.2, Linux, Google AI Pro (2026-11-06), with the global `toolPermission` set to
|
|
77
|
+
`always-proceed`:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
agy --model <id> --effort <level> --mode accept-edits --add-dir ../../.. --settings <file>
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
`<file>` is a JSON file with `"toolPermission": "request-review"` and the `permissions` lists; session settings
|
|
84
|
+
override the global value. Prefer a file over inline JSON: inline JSON needs different quoting in POSIX shells and
|
|
85
|
+
PowerShell. Keep the file at a stable path (the kit's `team` command writes it to
|
|
86
|
+
`~/.vibecoding-devtools/team/task-runner-limited.json`), not in a cache that can disappear. Verified with a file in a
|
|
87
|
+
throwaway folder: an allowed command (`ls`) ran, a denied one (`rm -f a.txt`) was refused with `Matches user-configured
|
|
88
|
+
deny rule` and the file stayed, and an unlisted one (`uname -s`) showed the command prompt (headless `-p` auto-denies
|
|
89
|
+
it). A complete allowlist for a helper agent is in `team/settings/task-runner-limited.json`.
|
|
90
|
+
|
|
91
|
+
### Recruited with a role
|
|
92
|
+
|
|
93
|
+
- The first start in each role folder (`<workspace>/.maestri/roles/<id>/`) asks `Do you trust the contents of this
|
|
94
|
+
project?`; Enter trusts it and saves it in `trustedWorkspaces`. A folder Maestri just created holds only the role
|
|
95
|
+
files.
|
|
96
|
+
- Add `--add-dir ../../..` so the workspace root is part of the agent's workspace.
|
|
97
|
+
|
|
74
98
|
## Permission prompts
|
|
75
99
|
|
|
76
100
|
File edit or creation:
|
|
@@ -48,6 +48,19 @@ Send the text, then `\n` in a separate `--raw` call.
|
|
|
48
48
|
- Changing the permission mode lowers or raises what the agent may do on the user's behalf: ask the user first.
|
|
49
49
|
- `/approve` approves one retry of a recent auto-review denial.
|
|
50
50
|
|
|
51
|
+
## Recruited with a role
|
|
52
|
+
|
|
53
|
+
Verified on Codex 0.167.0, Linux (2026-11-06).
|
|
54
|
+
|
|
55
|
+
- A recruit with a role starts in `<workspace>/.maestri/roles/<id>/`, and workspace-write covers only that folder. Add
|
|
56
|
+
`--add-dir ../../..` to the command: `/status` then shows `Workspace [<workspace root>]`. A folder every Codex agent
|
|
57
|
+
may write goes in `~/.codex/config.toml` as `[sandbox_workspace_write]` `writable_roots = ["<path>"]`.
|
|
58
|
+
- `AGENTS.md` from the role folder and from the repository both load (`/status` → `Agents.md`).
|
|
59
|
+
- Startup can stop on **Hooks need review** (`1 hook is new or changed`). With the ECC plugin, its `SessionStart` hook
|
|
60
|
+
asked again at every start: Codex re-syncs the plugin cache on launch and the stored content hash no longer matches.
|
|
61
|
+
Esc skips it (that hook does not run; trusted hooks keep running); `t` in the review panel trusts it until the next
|
|
62
|
+
start. Trusting is a persistent choice: ask the user once, then repeat it at each start.
|
|
63
|
+
|
|
51
64
|
## Autonomous launch and command rules
|
|
52
65
|
|
|
53
66
|
Verified on Codex 0.166.0, Windows (2026-11-06): local help, isolated rule checks and a live recruited agent.
|
|
@@ -185,6 +198,9 @@ Codex Document Control and Safety Settings. Maestri's skills in the list come fr
|
|
|
185
198
|
- With `--no-daemon` the startup warning is:
|
|
186
199
|
`Running without the shared background server: Bedrock sign-in requires embedded mode.`
|
|
187
200
|
|
|
201
|
+
- Startup warning `Codex could not find bubblewrap on PATH` on Linux: Codex falls back to its bundled bubblewrap.
|
|
202
|
+
Installing the OS package removes the warning.
|
|
203
|
+
|
|
188
204
|
## Not yet observed
|
|
189
205
|
|
|
190
206
|
Limit-reached screen, logged-out screen, API or network errors.
|
|
@@ -34,8 +34,8 @@ were given, or a question that is not a task) starts with one header line:
|
|
|
34
34
|
- Summary: one line, no secrets.
|
|
35
35
|
|
|
36
36
|
Details, evidence and lists go on the following lines; in PowerShell, use real line breaks, not `\n`. Keep the whole
|
|
37
|
-
message inside quotes and avoid double quotes, backticks and `$` in it, which break the shell command.
|
|
38
|
-
`ok` or `done`. One report per task; post progress with `maestri floor status`.
|
|
37
|
+
message inside double quotes and avoid unescaped double quotes, backticks and `$` in it, which break the shell command.
|
|
38
|
+
Never send a bare `ok` or `done`. One report per task; post progress with `maestri floor status`.
|
|
39
39
|
|
|
40
40
|
```text
|
|
41
41
|
maestri ask "Maestro" "~ Hawk | DONE | T-12 | login screen wired, tests green, PR #412 open"
|
|
@@ -46,18 +46,23 @@ maestri ask "Maestro" "~ Kite | ACK | design-spec | reply format adopted"
|
|
|
46
46
|
## Delegating a task
|
|
47
47
|
|
|
48
48
|
Give each task a ref (a task id like `T-12`, or the PR, branch or decision it concerns) and end the prompt with this
|
|
49
|
-
footer, filled in with your name
|
|
49
|
+
footer, filled in with your name (the address it reports to), the agent's name (who writes the report) and the ref.
|
|
50
|
+
Do this in `--batch` prompts too, and never ask for a bare `ok` reply.
|
|
50
51
|
|
|
51
52
|
```text
|
|
52
53
|
ref: T-12
|
|
53
54
|
When done, load the maestri-protocol skill and report back with:
|
|
54
|
-
maestri ask "Your Name" "~
|
|
55
|
+
maestri ask "Your Name" "~ Agent Name | STATUS | T-12 | one-line summary"
|
|
55
56
|
STATUS is one of DONE, PENDING, BLOCKED, ERROR, ACK. Details go on the following lines.
|
|
56
57
|
```
|
|
57
58
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
59
|
+
When the prompt itself is wrapped in double quotes, escape the footer's quotes (`\"`) or write that one line with
|
|
60
|
+
single quotes; the protocol hook accepts both. Your name is under `You:` in `maestri list`; when the agent is in another
|
|
61
|
+
workspace, use the `Name @ Workspace` address shown right under `You:` instead. The footer carries the whole format, so
|
|
62
|
+
the agent can follow it even if it does not load this skill.
|
|
63
|
+
|
|
64
|
+
Pass each message as a literal argument. The protocol hook cannot read a message built at run time, such as
|
|
65
|
+
`"$(cat task.md)"` or a variable, so such a message skips the check without warning.
|
|
61
66
|
|
|
62
67
|
## Receiving a message
|
|
63
68
|
|