multi-agents-cli 1.0.52 → 1.0.53
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/core/templates/.agents/backend/API.md +259 -0
- package/core/templates/.agents/backend/AUTH.md +246 -0
- package/core/templates/.agents/backend/DB.md +257 -0
- package/core/templates/.agents/backend/EVENTS.md +253 -0
- package/core/templates/.agents/backend/INIT.md +239 -0
- package/core/templates/.agents/backend/JOBS.md +256 -0
- package/core/templates/.agents/backend/LOGIC.md +291 -0
- package/core/templates/.agents/backend/TESTING.md +266 -0
- package/core/templates/.agents/client/ACCESSIBILITY.md +266 -0
- package/core/templates/.agents/client/FORMS.md +234 -0
- package/core/templates/.agents/client/LOGIC.md +277 -0
- package/core/templates/.agents/client/ROUTING.md +235 -0
- package/core/templates/.agents/client/TESTING.md +241 -0
- package/core/templates/.agents/client/UI.md +226 -0
- package/core/templates/.agents/shared/CLOUD.md +229 -0
- package/core/templates/.agents/shared/CLOUD_TEARDOWN.md +158 -0
- package/core/templates/.agents/shared/SECURITY.md +286 -0
- package/core/templates/.frameworks/backend/django.md +55 -0
- package/core/templates/.frameworks/backend/express.md +74 -0
- package/core/templates/.frameworks/backend/fastapi.md +107 -0
- package/core/templates/.frameworks/backend/fastify.md +74 -0
- package/core/templates/.frameworks/backend/nestjs.md +75 -0
- package/core/templates/.frameworks/client/angular.md +80 -0
- package/core/templates/.frameworks/client/nextjs.md +47 -0
- package/core/templates/.frameworks/client/nuxt.md +45 -0
- package/core/templates/.frameworks/client/remix.md +44 -0
- package/core/templates/.frameworks/client/sveltekit.md +44 -0
- package/core/templates/.frameworks/client/vite-react.md +45 -0
- package/core/templates/CLAUDE.md +531 -0
- package/core/templates/CLOUD_STATE.md +55 -0
- package/core/templates/CONTRACTS.md +16 -0
- package/core/templates/TASKS_HISTORY.md +6 -0
- package/core/templates/backend/CLAUDE.md +207 -0
- package/core/templates/client/CLAUDE.md +213 -0
- package/core/templates/shared/.gitkeep +0 -0
- package/core/templates/shared/wiring.config.json +14 -0
- package/core/workflow/agent.js +1404 -0
- package/core/workflow/complete.js +354 -0
- package/core/workflow/guards.js +643 -0
- package/core/workflow/reset.js +246 -0
- package/core/workflow/restart.js +243 -0
- package/core/workflow/tasks_history.js +120 -0
- package/init.js +3 -15
- package/package.json +2 -1
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Multi-Agent Monorepo Template - Task Completion
|
|
5
|
+
* Run with: node .workflow/complete.js
|
|
6
|
+
*
|
|
7
|
+
* Merges the current task branch into main,
|
|
8
|
+
* updates BUILD_STATE.md, and pushes to origin.
|
|
9
|
+
* Run from the repo root after a task is complete.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const readline = require('readline');
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
const { execSync, spawn } = require('child_process');
|
|
16
|
+
const guards = require('./guards');
|
|
17
|
+
const tasksHistory = require('./tasks_history');
|
|
18
|
+
|
|
19
|
+
// ── Colors ────────────────────────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
const c = {
|
|
22
|
+
reset: '\x1b[0m',
|
|
23
|
+
bold: '\x1b[1m',
|
|
24
|
+
dim: '\x1b[2m',
|
|
25
|
+
green: '\x1b[32m',
|
|
26
|
+
yellow: '\x1b[33m',
|
|
27
|
+
cyan: '\x1b[36m',
|
|
28
|
+
red: '\x1b[31m',
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const bold = (s) => `${c.bold}${s}${c.reset}`;
|
|
32
|
+
const green = (s) => `${c.green}${s}${c.reset}`;
|
|
33
|
+
const yellow = (s) => `${c.yellow}${s}${c.reset}`;
|
|
34
|
+
const dim = (s) => `${c.dim}${s}${c.reset}`;
|
|
35
|
+
const cyan = (s) => `${c.cyan}${s}${c.reset}`;
|
|
36
|
+
const red = (s) => `${c.red}${s}${c.reset}`;
|
|
37
|
+
|
|
38
|
+
// ── Paths ─────────────────────────────────────────────────────────────────────
|
|
39
|
+
|
|
40
|
+
const ROOT = path.join(__dirname, '..');
|
|
41
|
+
const CONFIG_PATH = path.join(ROOT, '.scaffold', '.config.json');
|
|
42
|
+
const LOCK_PATH = path.join(ROOT, '.scaffold', '.initialized');
|
|
43
|
+
|
|
44
|
+
// ── Guards ────────────────────────────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
if (!fs.existsSync(LOCK_PATH)) {
|
|
47
|
+
console.log(`\n${red(' Project not initialized.')}`);
|
|
48
|
+
console.log(dim(' Run node .scaffold/init.js first.\n'));
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const config = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'));
|
|
53
|
+
|
|
54
|
+
// ── Readline ──────────────────────────────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
const rl = readline.createInterface({
|
|
57
|
+
input: process.stdin,
|
|
58
|
+
output: process.stdout,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const ask = (question) =>
|
|
62
|
+
new Promise((resolve) => rl.question(question, (a) => resolve(a.trim())));
|
|
63
|
+
|
|
64
|
+
const separator = () => console.log(`\n${dim('─'.repeat(60))}`);
|
|
65
|
+
|
|
66
|
+
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
67
|
+
|
|
68
|
+
const getCurrentBranch = () => {
|
|
69
|
+
try {
|
|
70
|
+
return execSync('git branch --show-current', { cwd: ROOT, stdio: 'pipe' })
|
|
71
|
+
.toString().trim();
|
|
72
|
+
} catch {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const getWorktrees = () => {
|
|
78
|
+
try {
|
|
79
|
+
const output = execSync('git worktree list --porcelain', { cwd: ROOT, stdio: 'pipe' })
|
|
80
|
+
.toString().trim();
|
|
81
|
+
const worktrees = [];
|
|
82
|
+
const blocks = output.split('\n\n');
|
|
83
|
+
for (const block of blocks) {
|
|
84
|
+
const lines = block.split('\n');
|
|
85
|
+
const pathLine = lines.find(l => l.startsWith('worktree '));
|
|
86
|
+
const branchLine = lines.find(l => l.startsWith('branch '));
|
|
87
|
+
if (pathLine && branchLine) {
|
|
88
|
+
const wtPath = pathLine.replace('worktree ', '').trim();
|
|
89
|
+
const wtBranch = branchLine.replace('branch refs/heads/', '').trim();
|
|
90
|
+
if (wtBranch !== 'main' && wtBranch !== 'master') {
|
|
91
|
+
worktrees.push({ path: wtPath, branch: wtBranch });
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return worktrees;
|
|
96
|
+
} catch {
|
|
97
|
+
return [];
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const updateBuildState = (branch, status, notes = '') => {
|
|
102
|
+
const buildStatePath = path.join(ROOT, 'BUILD_STATE.md');
|
|
103
|
+
if (!fs.existsSync(buildStatePath)) return;
|
|
104
|
+
|
|
105
|
+
let content = fs.readFileSync(buildStatePath, 'utf8');
|
|
106
|
+
const date = new Date().toISOString().split('T')[0];
|
|
107
|
+
|
|
108
|
+
// Update IN PROGRESS → COMPLETED in agent log
|
|
109
|
+
content = content.replace(
|
|
110
|
+
new RegExp(`(\\| [^|]+ \\| [^|]+ \\| [^|]+ \\| [^|]+ \\|) IN PROGRESS (\\| ${branch.replace(/\//g, '\\/')} \\|)`),
|
|
111
|
+
`$1 ${status} $2`
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
fs.writeFileSync(buildStatePath, content, 'utf8');
|
|
115
|
+
|
|
116
|
+
// Update TASKS_HISTORY.md
|
|
117
|
+
tasksHistory.updateSessionStatus(ROOT, {
|
|
118
|
+
branch,
|
|
119
|
+
status,
|
|
120
|
+
completedAt: new Date().toISOString(),
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
execSync('git add BUILD_STATE.md', { cwd: ROOT, stdio: 'pipe' });
|
|
125
|
+
execSync(`git commit -m "build: task ${status.toLowerCase()} [${branch}]"`, { cwd: ROOT, stdio: 'pipe' });
|
|
126
|
+
} catch {
|
|
127
|
+
// Silent - BUILD_STATE.md update is best-effort
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
// ── Main ──────────────────────────────────────────────────────────────────────
|
|
132
|
+
|
|
133
|
+
const main = async () => {
|
|
134
|
+
console.log('\n');
|
|
135
|
+
console.log(bold(cyan(' Multi-Agent Monorepo Template')));
|
|
136
|
+
console.log(dim(` Task Completion - ${config.projectName}\n`));
|
|
137
|
+
separator();
|
|
138
|
+
|
|
139
|
+
// ── Detect active worktrees ───────────────────────────────────────────────────
|
|
140
|
+
|
|
141
|
+
const worktrees = getWorktrees();
|
|
142
|
+
|
|
143
|
+
if (worktrees.length === 0) {
|
|
144
|
+
console.log(`\n${yellow(' No active task worktrees found.')}`);
|
|
145
|
+
console.log(dim(' Run npm run agent to start a task.\n'));
|
|
146
|
+
rl.close();
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ── Select worktree to complete ───────────────────────────────────────────────
|
|
151
|
+
|
|
152
|
+
const isNonTTY = !process.stdin.isTTY;
|
|
153
|
+
|
|
154
|
+
console.log(`\n${bold('Active tasks:')}\n`);
|
|
155
|
+
worktrees.forEach((wt, i) => {
|
|
156
|
+
console.log(` ${dim(`${i + 1}.`)} ${wt.branch}`);
|
|
157
|
+
console.log(` ${dim(wt.path)}`);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
let selectedWorktree;
|
|
161
|
+
|
|
162
|
+
if (isNonTTY || worktrees.length === 1) {
|
|
163
|
+
selectedWorktree = worktrees[0];
|
|
164
|
+
console.log(dim(`\n Auto-selected: ${selectedWorktree.branch}\n`));
|
|
165
|
+
} else {
|
|
166
|
+
while (!selectedWorktree) {
|
|
167
|
+
const input = await ask(`\n ${bold('Select task to complete')} ${dim(`(1-${worktrees.length})`)}: `);
|
|
168
|
+
const index = parseInt(input) - 1;
|
|
169
|
+
if (!isNaN(index) && index >= 0 && index < worktrees.length) {
|
|
170
|
+
selectedWorktree = worktrees[index];
|
|
171
|
+
} else {
|
|
172
|
+
console.log(yellow(` Please enter a number between 1 and ${worktrees.length}.`));
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const { path: worktreePath, branch: branchName } = selectedWorktree;
|
|
178
|
+
|
|
179
|
+
// ── Check TASK.md status ──────────────────────────────────────────────────────
|
|
180
|
+
|
|
181
|
+
const taskMdPath = path.join(worktreePath, 'TASK.md');
|
|
182
|
+
if (fs.existsSync(taskMdPath)) {
|
|
183
|
+
const taskContent = fs.readFileSync(taskMdPath, 'utf8');
|
|
184
|
+
const isCompleted = taskContent.includes('[x] COMPLETED');
|
|
185
|
+
|
|
186
|
+
if (!isCompleted && !isNonTTY) {
|
|
187
|
+
console.log(`\n${yellow(' TASK.md is not marked as COMPLETED.')}`);
|
|
188
|
+
console.log(dim(' The agent may still be working on this task.\n'));
|
|
189
|
+
const proceed = await ask(` ${bold('Proceed with merge anyway?')} ${dim('(y/n)')}: `);
|
|
190
|
+
if (proceed.toLowerCase() !== 'y') {
|
|
191
|
+
console.log(yellow('\n Aborted. Complete the task first.\n'));
|
|
192
|
+
rl.close();
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
} else if (!isCompleted && isNonTTY) {
|
|
196
|
+
console.log(dim(' ℹ TASK.md not marked complete - proceeding in non-TTY mode.'));
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
separator();
|
|
201
|
+
|
|
202
|
+
// ── Confirm ───────────────────────────────────────────────────────────────────
|
|
203
|
+
|
|
204
|
+
console.log(`\n${bold('About to merge:')}\n`);
|
|
205
|
+
console.log(` ${dim('Branch')} : ${green(branchName)}`);
|
|
206
|
+
console.log(` ${dim('Into')} : ${green('main')}`);
|
|
207
|
+
console.log(` ${dim('Worktree')} : ${dim(worktreePath)}\n`);
|
|
208
|
+
|
|
209
|
+
const confirm = isNonTTY ? 'y' : await ask(`${bold('Confirm merge into main?')} ${dim('(y/n)')}: `);
|
|
210
|
+
if (confirm.toLowerCase() !== 'y') {
|
|
211
|
+
console.log(yellow('\n Aborted.\n'));
|
|
212
|
+
rl.close();
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
separator();
|
|
217
|
+
console.log(`\n${bold('Completing task...')}\n`);
|
|
218
|
+
|
|
219
|
+
// ── Auto-commit uncommitted changes in worktree ──────────────────────────────
|
|
220
|
+
|
|
221
|
+
try {
|
|
222
|
+
const status = execSync('git status --porcelain', { cwd: worktreePath, stdio: 'pipe' }).toString().trim();
|
|
223
|
+
if (status) {
|
|
224
|
+
console.log(` ${yellow('!')} Uncommitted changes detected - committing before merge...`);
|
|
225
|
+
execSync('git add .', { cwd: worktreePath, stdio: 'pipe' });
|
|
226
|
+
execSync(`git commit -m "feat: task completion commit [${branchName}]"`, { cwd: worktreePath, stdio: 'pipe' });
|
|
227
|
+
console.log(` ${green('✓')} Changes committed`);
|
|
228
|
+
} else {
|
|
229
|
+
console.log(` ${green('✓')} Worktree is clean`);
|
|
230
|
+
}
|
|
231
|
+
} catch (err) {
|
|
232
|
+
console.log(` ${yellow('!')} Could not auto-commit - commit manually if needed`);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// ── Pull latest main ──────────────────────────────────────────────────────────
|
|
236
|
+
|
|
237
|
+
try {
|
|
238
|
+
execSync('git checkout main', { cwd: ROOT, stdio: 'pipe' });
|
|
239
|
+
execSync('git pull origin main', { cwd: ROOT, stdio: 'pipe' });
|
|
240
|
+
console.log(` ${green('✓')} Pulled latest main`);
|
|
241
|
+
} catch (err) {
|
|
242
|
+
console.log(` ${yellow('!')} Could not pull latest main.`);
|
|
243
|
+
if (!isNonTTY) {
|
|
244
|
+
const proceedAnyway = await ask(` ${bold('Proceed with merge anyway?')} ${dim('(y/n)')}: `);
|
|
245
|
+
if (proceedAnyway.toLowerCase() !== 'y') {
|
|
246
|
+
console.log(yellow('\n Aborted.\n'));
|
|
247
|
+
rl.close();
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
} else {
|
|
251
|
+
console.log(dim(' Proceeding in non-TTY mode.'));
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// ── Merge branch into main ────────────────────────────────────────────────────
|
|
256
|
+
|
|
257
|
+
try {
|
|
258
|
+
execSync(`git merge ${branchName} --no-ff -m "merge: ${branchName} into main"`, {
|
|
259
|
+
cwd: ROOT,
|
|
260
|
+
stdio: 'pipe',
|
|
261
|
+
});
|
|
262
|
+
console.log(` ${green('✓')} Merged ${branchName} into main`);
|
|
263
|
+
} catch (mergeErr) {
|
|
264
|
+
// Check if BUILD_STATE.md is the only conflict - auto-resolve
|
|
265
|
+
try {
|
|
266
|
+
const conflicts = execSync('git diff --name-only --diff-filter=U', { cwd: ROOT, encoding: 'utf8' })
|
|
267
|
+
.trim().split('\n').filter(Boolean);
|
|
268
|
+
|
|
269
|
+
if (conflicts.length === 1 && conflicts[0] === 'BUILD_STATE.md') {
|
|
270
|
+
execSync('git checkout --ours BUILD_STATE.md', { cwd: ROOT, stdio: 'pipe' });
|
|
271
|
+
execSync('git add BUILD_STATE.md', { cwd: ROOT, stdio: 'pipe' });
|
|
272
|
+
execSync(`git commit -m "merge: ${branchName} into main"`, { cwd: ROOT, stdio: 'pipe' });
|
|
273
|
+
console.log(` ${green('✓')} Merged ${branchName} into main`);
|
|
274
|
+
console.log(dim(' ℹ BUILD_STATE.md conflict auto-resolved'));
|
|
275
|
+
} else {
|
|
276
|
+
console.log(`\n${red(' ✗ Merge conflict in:')} ${conflicts.join(', ')}`);
|
|
277
|
+
console.log(dim(' Resolve conflicts manually, then run: git commit\n'));
|
|
278
|
+
rl.close();
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
} catch {
|
|
282
|
+
console.log(`\n${red(' ✗ Merge failed.')} Resolve manually.\n`);
|
|
283
|
+
rl.close();
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// ── Update BUILD_STATE.md ─────────────────────────────────────────────────────
|
|
289
|
+
|
|
290
|
+
updateBuildState(branchName, 'COMPLETED');
|
|
291
|
+
console.log(` ${green('✓')} BUILD_STATE.md updated`);
|
|
292
|
+
|
|
293
|
+
// ── Clear tracking slot ───────────────────────────────────────────────────────
|
|
294
|
+
|
|
295
|
+
try {
|
|
296
|
+
const tracking = guards.loadTracking(ROOT, config);
|
|
297
|
+
// Derive scope and agent from branch name: agent/{scope}/{agent}/{timestamp}
|
|
298
|
+
const parts = branchName.split('/');
|
|
299
|
+
if (parts.length >= 4) {
|
|
300
|
+
const scope = parts[1];
|
|
301
|
+
const agent = parts[2].toUpperCase();
|
|
302
|
+
guards.clearTrackingSlot(tracking, scope, agent, ROOT);
|
|
303
|
+
console.log(` ${green('✓')} Tracking slot cleared`);
|
|
304
|
+
}
|
|
305
|
+
} catch { /* best-effort */ }
|
|
306
|
+
|
|
307
|
+
// ── Push to origin ────────────────────────────────────────────────────────────
|
|
308
|
+
|
|
309
|
+
try {
|
|
310
|
+
execSync('git push origin main', { cwd: ROOT, stdio: 'pipe' });
|
|
311
|
+
console.log(` ${green('✓')} Pushed to origin/main`);
|
|
312
|
+
} catch (err) {
|
|
313
|
+
console.log(` ${yellow('!')} Could not push to origin.`);
|
|
314
|
+
console.log(dim(' This is normal if you have not set up a remote repo yet.'));
|
|
315
|
+
console.log(dim(' When ready: git push origin main'));
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// ── Worktree retained ────────────────────────────────────────────────────────
|
|
319
|
+
// Worktree is kept for reference. Clean up manually when no longer needed:
|
|
320
|
+
// git worktree remove "${worktreePath}" --force
|
|
321
|
+
|
|
322
|
+
// ── Done ──────────────────────────────────────────────────────────────────────
|
|
323
|
+
|
|
324
|
+
separator();
|
|
325
|
+
// Derive agent and scope for summary
|
|
326
|
+
const _parts = branchName.split('/');
|
|
327
|
+
const _scope = _parts.length >= 4 ? _parts[1] : '';
|
|
328
|
+
const _agent = _parts.length >= 4 ? _parts[2].toUpperCase() : '';
|
|
329
|
+
|
|
330
|
+
// Read task from TASK.md if available
|
|
331
|
+
let _task = '';
|
|
332
|
+
try {
|
|
333
|
+
const taskMd = fs.readFileSync(path.join(worktreePath, 'TASK.md'), 'utf8');
|
|
334
|
+
const taskMatch = taskMd.match(/^## Task\n.*?Use\.agents\/.*?\. Task: (.+)$/m);
|
|
335
|
+
if (taskMatch) _task = taskMatch[1].trim();
|
|
336
|
+
} catch {}
|
|
337
|
+
|
|
338
|
+
console.log(`\n${bold(green(' Task completed successfully!'))}\n`);
|
|
339
|
+
if (_agent) console.log(` ${dim('Agent')} : ${green(_agent)} ${dim('(' + _scope + ')')}`);
|
|
340
|
+
if (_task) console.log(` ${dim('Task')} : ${dim(_task)}`);
|
|
341
|
+
console.log(` ${dim('Branch')} : ${green(branchName)} merged into ${green('main')}\n`);
|
|
342
|
+
console.log(` ${bold('What to do next:')}\n`);
|
|
343
|
+
console.log(` Start a new task:`);
|
|
344
|
+
console.log(` ${cyan('npm run agent')}\n`);
|
|
345
|
+
separator();
|
|
346
|
+
console.log('');
|
|
347
|
+
|
|
348
|
+
rl.close();
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
main().catch((err) => {
|
|
352
|
+
console.error('\n Error:', err.message);
|
|
353
|
+
process.exit(1);
|
|
354
|
+
});
|