multi-agents-cli 1.1.9 → 1.1.11
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 +18 -2
- package/core/templates/.agents/backend/API.md +270 -0
- package/core/templates/.agents/backend/AUTH.md +257 -0
- package/core/templates/.agents/backend/DB.md +268 -0
- package/core/templates/.agents/backend/EVENTS.md +264 -0
- package/core/templates/.agents/backend/INIT.md +250 -0
- package/core/templates/.agents/backend/JOBS.md +267 -0
- package/core/templates/.agents/backend/LOGIC.md +302 -0
- package/core/templates/.agents/backend/TESTING.md +277 -0
- package/core/templates/.agents/client/ACCESSIBILITY.md +277 -0
- package/core/templates/.agents/client/FORMS.md +245 -0
- package/core/templates/.agents/client/LOGIC.md +288 -0
- package/core/templates/.agents/client/ROUTING.md +246 -0
- package/core/templates/.agents/client/TESTING.md +252 -0
- package/core/templates/.agents/client/UI.md +237 -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 +297 -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/laravel.md +79 -0
- package/core/templates/.frameworks/backend/nestjs.md +75 -0
- package/core/templates/.frameworks/backend/rails.md +84 -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 +562 -0
- package/core/templates/CONTRACTS.md +16 -0
- package/core/templates/backend/CLAUDE.md +207 -0
- package/core/templates/client/CLAUDE.md +213 -0
- package/core/workflow/agent.js +285 -6
- package/core/workflow/complete.js +155 -36
- package/core/workflow/reset.js +28 -21
- package/core/workflow/run.js +3 -0
- package/init.js +2 -2
- package/lib/questions-flow.js +13 -2
- package/lib/steps.js +13 -1
- package/lib/ui.js +8 -0
- package/package.json +1 -1
package/core/workflow/agent.js
CHANGED
|
@@ -836,6 +836,52 @@ const main = async () => {
|
|
|
836
836
|
|
|
837
837
|
// Soft gate - backend selected but missing prerequisites
|
|
838
838
|
if (project === 'backend') {
|
|
839
|
+
// Architectural change warning — no backend/ folder + integrated backend config
|
|
840
|
+
const backendFolderExists = fs.existsSync(path.join(ROOT, 'backend'));
|
|
841
|
+
const isIntegrated = config.backend?.type === 'integrated';
|
|
842
|
+
|
|
843
|
+
if (!backendFolderExists) {
|
|
844
|
+
separator();
|
|
845
|
+
console.log(`\n ${yellow('⚠ No backend/ folder detected')}
|
|
846
|
+
`);
|
|
847
|
+
if (isIntegrated) {
|
|
848
|
+
console.log(` This project currently uses ${bold('integrated backend')} (${dim(config.frontend?.type || 'client framework')}).`);
|
|
849
|
+
console.log(` Adding a separate backend will change your project architecture:
|
|
850
|
+
`);
|
|
851
|
+
} else {
|
|
852
|
+
console.log(` A ${bold('backend/')} directory does not exist yet.`);
|
|
853
|
+
console.log(` Proceeding will create it and establish a separate backend:
|
|
854
|
+
`);
|
|
855
|
+
}
|
|
856
|
+
console.log(` ${dim('→')} A new ${bold('backend/')} directory will be created`);
|
|
857
|
+
console.log(` ${dim('→')} Client API calls will need redirecting to the new backend`);
|
|
858
|
+
console.log(` ${dim('→')} CONTRACTS.md will need updating`);
|
|
859
|
+
if (isIntegrated) {
|
|
860
|
+
console.log(` ${dim('→')} Project config will update to ${bold('separate')} backend type
|
|
861
|
+
`);
|
|
862
|
+
}
|
|
863
|
+
console.log('');
|
|
864
|
+
|
|
865
|
+
const archIdx = await arrowSelect('How would you like to proceed?', [
|
|
866
|
+
{ label: `${green('→')} Proceed — I want to add a separate backend` },
|
|
867
|
+
{ label: `${yellow('←')} Go back — keep current setup ${dim('← recommended')}` },
|
|
868
|
+
], rl);
|
|
869
|
+
|
|
870
|
+
if (archIdx === 1) continue flowLoop;
|
|
871
|
+
|
|
872
|
+
// Update config to separate if was integrated
|
|
873
|
+
if (isIntegrated) {
|
|
874
|
+
try {
|
|
875
|
+
const cfgPath = path.join(ROOT, '.scaffold', '.config.json');
|
|
876
|
+
const cfg = JSON.parse(fs.readFileSync(cfgPath, 'utf8'));
|
|
877
|
+
cfg.backend.type = 'separate';
|
|
878
|
+
fs.writeFileSync(cfgPath, JSON.stringify(cfg, null, 2), 'utf8');
|
|
879
|
+
console.log(` ${green('✓')} Project config updated to separate backend
|
|
880
|
+
`);
|
|
881
|
+
} catch {}
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
|
|
839
885
|
const clientCompleted = buildEntries.filter(e => e.scope === 'client' && e.status === 'COMPLETED');
|
|
840
886
|
const clientLogicDone = clientCompleted.some(e => e.agent === 'LOGIC');
|
|
841
887
|
const missing = [];
|
|
@@ -863,6 +909,42 @@ const main = async () => {
|
|
|
863
909
|
}
|
|
864
910
|
}
|
|
865
911
|
|
|
912
|
+
|
|
913
|
+
// Soft gate - shared selected but missing prerequisites
|
|
914
|
+
if (project === 'shared') {
|
|
915
|
+
const clientCompleted = buildEntries.filter(e => e.scope === 'client' && e.status === 'COMPLETED');
|
|
916
|
+
const backendCompleted = buildEntries.filter(e => e.scope === 'backend' && e.status === 'COMPLETED');
|
|
917
|
+
const clientUIДone = clientCompleted.some(e => e.agent === 'UI');
|
|
918
|
+
const backendINITDone = backendCompleted.some(e => e.agent === 'INIT');
|
|
919
|
+
const missing = [];
|
|
920
|
+
|
|
921
|
+
if (!clientUIДone) {
|
|
922
|
+
missing.push({ item: 'Client scaffold', detail: 'no completed client work found — SECURITY needs to know what auth patterns the client uses' });
|
|
923
|
+
}
|
|
924
|
+
if (!backendINITDone) {
|
|
925
|
+
missing.push({ item: 'Backend INIT', detail: 'backend not scaffolded yet — SECURITY needs to know the backend auth strategy and DB schema' });
|
|
926
|
+
}
|
|
927
|
+
if (!contracts.hasContent) {
|
|
928
|
+
missing.push({ item: 'CONTRACTS.md', detail: 'no shared types defined — auth utilities should align with established contracts' });
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
if (missing.length > 0) {
|
|
932
|
+
console.log(`\n${yellow(' ⚠ Shared prerequisites not fully met:')}
|
|
933
|
+
`);
|
|
934
|
+
missing.forEach(m => {
|
|
935
|
+
console.log(` ${dim('→')} ${bold(m.item)}`);
|
|
936
|
+
console.log(` ${m.detail}\n`);
|
|
937
|
+
});
|
|
938
|
+
console.log(` ${dim('The agent will proceed autonomously and make assumptions about auth patterns.')}`);
|
|
939
|
+
console.log(` ${dim('These may need revision once client and backend are fully established.\n')}`);
|
|
940
|
+
const proceedIdx = await arrowSelect('Shared prerequisites not met:', [
|
|
941
|
+
{ label: `${green('→')} Proceed anyway` },
|
|
942
|
+
{ label: `${yellow('←')} Go back — pick a different scope ${dim('← recommended')}` },
|
|
943
|
+
], rl);
|
|
944
|
+
if (proceedIdx === 1) continue flowLoop;
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
|
|
866
948
|
// ── Select agent (with re-selection loop + back to scope) ────────────────────
|
|
867
949
|
|
|
868
950
|
const agentOptions = AGENTS[project] || [];
|
|
@@ -896,12 +978,14 @@ const main = async () => {
|
|
|
896
978
|
|
|
897
979
|
const agentChoices = [
|
|
898
980
|
...agentOptions.map(a => {
|
|
899
|
-
const desc
|
|
900
|
-
const
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
const
|
|
904
|
-
|
|
981
|
+
const desc = dim(` ${AGENT_DESCRIPTIONS[project]?.[a] || ''}`);
|
|
982
|
+
const completed = completedAgents.includes(a);
|
|
983
|
+
const isActive = tracking?.[project]?.[a]?.status === 'ACTIVE';
|
|
984
|
+
const isNext = a === recommendedAgent && !isActive;
|
|
985
|
+
const indicator = completed ? green('[✓]') : isActive ? yellow('[▶]') : isNext ? cyan(' → ') : dim('[ ]');
|
|
986
|
+
const label = completed ? dim(a) : isActive ? yellow(bold(a)) : isNext ? bold(a) : a;
|
|
987
|
+
const tag = isActive ? yellow(' ← in progress') : isNext ? cyan(completedAgents.length === 0 ? ' ← start here' : ' ← next step') : '';
|
|
988
|
+
return { label: `${indicator} ${label}${tag}${desc}` };
|
|
905
989
|
}),
|
|
906
990
|
{ label: dim('← back to scope selection') },
|
|
907
991
|
];
|
|
@@ -911,6 +995,34 @@ const main = async () => {
|
|
|
911
995
|
agent = agentOptions[agentIdx];
|
|
912
996
|
contractsNote = '';
|
|
913
997
|
|
|
998
|
+
// ── Check for OTHER active agents in same scope ───────────────────────────
|
|
999
|
+
const otherActiveInScope = Object.entries(tracking?.[project] || {})
|
|
1000
|
+
.filter(([a, data]) => a !== agent && data?.status === 'ACTIVE');
|
|
1001
|
+
|
|
1002
|
+
if (otherActiveInScope.length > 0) {
|
|
1003
|
+
separator();
|
|
1004
|
+
console.log(`\n ${yellow('⚠ Active agent detected in ' + project + ' scope')}
|
|
1005
|
+
`);
|
|
1006
|
+
for (const [a, data] of otherActiveInScope) {
|
|
1007
|
+
console.log(` ${dim('→')} ${bold(a)} is currently ${yellow('ACTIVE')}`);
|
|
1008
|
+
console.log(` Branch : ${dim(data.branch)}`);
|
|
1009
|
+
console.log(` Launched: ${dim(data.launchedAt ? new Date(data.launchedAt).toLocaleString() : 'unknown')}
|
|
1010
|
+
`);
|
|
1011
|
+
}
|
|
1012
|
+
console.log(` ${yellow('Launching')} ${bold(agent)} ${yellow('alongside an active agent in the same scope means:')}`);
|
|
1013
|
+
console.log(` ${dim('→')} Both agents may write to overlapping files`);
|
|
1014
|
+
console.log(` ${dim('→')} Scope violations may occur at merge time`);
|
|
1015
|
+
console.log(` ${dim('→')} CONTRACTS.md may receive conflicting entries
|
|
1016
|
+
`);
|
|
1017
|
+
|
|
1018
|
+
const warningIdx = await arrowSelect('How would you like to proceed?', [
|
|
1019
|
+
{ label: `${yellow('→')} Proceed anyway ${dim('— I understand the risks')}` },
|
|
1020
|
+
{ label: `${green('←')} Go back — pick a different agent ${dim('← recommended')}` },
|
|
1021
|
+
], rl);
|
|
1022
|
+
|
|
1023
|
+
if (warningIdx === 1) continue agentLoop;
|
|
1024
|
+
}
|
|
1025
|
+
|
|
914
1026
|
// Agent already active - decisional block
|
|
915
1027
|
const { active, slot: activeSlot } = guards.checkAgentActive(tracking, project, agent);
|
|
916
1028
|
if (active) {
|
|
@@ -1058,6 +1170,32 @@ const main = async () => {
|
|
|
1058
1170
|
} // end else (argAgent bypass)
|
|
1059
1171
|
} // end if (project !== 'cloud')
|
|
1060
1172
|
|
|
1173
|
+
// ── First-run project context question (UI agent only, no completed agents) ──
|
|
1174
|
+
let projectContext = '';
|
|
1175
|
+
const isFirstRun = project === 'client' && agent === 'UI' && buildEntries.filter(e => e.status === 'COMPLETED').length === 0;
|
|
1176
|
+
|
|
1177
|
+
if (isFirstRun) {
|
|
1178
|
+
separator();
|
|
1179
|
+
console.log(`\n ${bold('Before we begin...')}
|
|
1180
|
+
`);
|
|
1181
|
+
|
|
1182
|
+
if (prompts && process.stdin.isTTY) {
|
|
1183
|
+
const ctxRes = await prompts({
|
|
1184
|
+
type: 'text',
|
|
1185
|
+
name: 'value',
|
|
1186
|
+
message: '* What are you building?',
|
|
1187
|
+
initial: 'e.g. A stock market prediction app with AI insights and portfolio tracking',
|
|
1188
|
+
}, { onCancel: () => process.exit(0) });
|
|
1189
|
+
const rawCtx = (ctxRes.value || '').trim();
|
|
1190
|
+
const isPlaceholderCtx = rawCtx.startsWith('e.g. ');
|
|
1191
|
+
projectContext = isPlaceholderCtx ? '' : rawCtx;
|
|
1192
|
+
} else {
|
|
1193
|
+
console.log(dim(' (optional — helps the agent make better decisions)'));
|
|
1194
|
+
const input = await ask(`\n${bold('* What are you building?')}: `);
|
|
1195
|
+
projectContext = input.trim();
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1061
1199
|
// ── Task description ──────────────────────────────────────────────────────────
|
|
1062
1200
|
|
|
1063
1201
|
separator();
|
|
@@ -1105,6 +1243,11 @@ const main = async () => {
|
|
|
1105
1243
|
let skipped = [];
|
|
1106
1244
|
contextSection = '';
|
|
1107
1245
|
|
|
1246
|
+
// Inject project context if provided on first UI run
|
|
1247
|
+
if (projectContext) {
|
|
1248
|
+
contextSection += `\n---\n\n## Project Context\n\n${projectContext}\n\nUse this as the primary directive for what you are building. All component names, page structure, and UI decisions should align with this project description.\n`;
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1108
1251
|
if (AGENT_QUESTIONS[agent] && userSeedingContracts) {
|
|
1109
1252
|
let gathering = true;
|
|
1110
1253
|
while (gathering) {
|
|
@@ -1143,6 +1286,135 @@ const main = async () => {
|
|
|
1143
1286
|
}
|
|
1144
1287
|
}
|
|
1145
1288
|
|
|
1289
|
+
|
|
1290
|
+
// ── Backend simultaneous flow (client/LOGIC only, separate backend, first run) ──
|
|
1291
|
+
let backendLaunchTiming = null;
|
|
1292
|
+
const isSimultaneousCandidate =
|
|
1293
|
+
project === 'client' &&
|
|
1294
|
+
agent === 'LOGIC' &&
|
|
1295
|
+
config.backend?.type === 'separate' &&
|
|
1296
|
+
!argAgent &&
|
|
1297
|
+
!buildEntries.some(e => e.scope === 'backend') &&
|
|
1298
|
+
!tracking?.backend?.INIT?.backendPromptShown;
|
|
1299
|
+
|
|
1300
|
+
if (isSimultaneousCandidate) {
|
|
1301
|
+
separator();
|
|
1302
|
+
console.log(`\n ${yellow('⚡ Backend not started yet')}\n`);
|
|
1303
|
+
console.log(` Your backend has not been scaffolded yet.`);
|
|
1304
|
+
console.log(` Since client/LOGIC establishes integration contracts, this is the ideal moment to plan backend scaffolding.\n`);
|
|
1305
|
+
|
|
1306
|
+
const intentIdx = await arrowSelect('Would you like to include backend/INIT?', [
|
|
1307
|
+
{ label: `${green('→')} Yes — I want to scaffold the backend` },
|
|
1308
|
+
{ label: `${dim('→')} No — continue with client/LOGIC only` },
|
|
1309
|
+
], rl);
|
|
1310
|
+
|
|
1311
|
+
if (intentIdx === 0) {
|
|
1312
|
+
console.log('');
|
|
1313
|
+
const timingIdx = await arrowSelect('When would you like to scaffold backend/INIT?', [
|
|
1314
|
+
{ label: `${green('→')} Now — open backend/INIT workspace alongside this LOGIC session\n ${dim('A second IDE window will open. Both agents run in parallel.')}` },
|
|
1315
|
+
{ label: `${dim('→')} After — launch backend/INIT when LOGIC completes\n ${dim('complete.js will prompt you immediately after merge.')}` },
|
|
1316
|
+
{ label: `${dim('→')} Skip — changed my mind, continue with client/LOGIC only` },
|
|
1317
|
+
], rl);
|
|
1318
|
+
|
|
1319
|
+
if (timingIdx === 0) backendLaunchTiming = 'now';
|
|
1320
|
+
else if (timingIdx === 1) backendLaunchTiming = 'after';
|
|
1321
|
+
else backendLaunchTiming = 'skip';
|
|
1322
|
+
} else {
|
|
1323
|
+
backendLaunchTiming = 'skip';
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
// Write flag to backend/INIT tracking slot — prevents prompt from showing again
|
|
1327
|
+
guards.updateTrackingSlot(tracking, 'backend', 'INIT', {
|
|
1328
|
+
backendPromptShown: true,
|
|
1329
|
+
backendLaunchTiming,
|
|
1330
|
+
}, ROOT);
|
|
1331
|
+
|
|
1332
|
+
// Inject backend coordination block into TASK.md contextSection
|
|
1333
|
+
if (backendLaunchTiming === 'now') {
|
|
1334
|
+
contextSection += `\n---\n\n## Backend Coordination\n\nBackend/INIT is running in parallel in a separate workspace.\nEstablish CONTRACTS.md entries early — the backend agent will consume them.\nDo not wait for backend confirmation. Work autonomously on client scope.\n`;
|
|
1335
|
+
} else if (backendLaunchTiming === 'after') {
|
|
1336
|
+
contextSection += `\n---\n\n## Backend Coordination\n\nBackend/INIT will launch after this LOGIC session completes.\nBe deliberate about API contracts — document all integration points in CONTRACTS.md.\nThe backend agent will use these as its starting reference.\n`;
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
// If Now — create backend/INIT worktree immediately
|
|
1340
|
+
if (backendLaunchTiming === 'now') {
|
|
1341
|
+
const beTimestamp = Date.now();
|
|
1342
|
+
const beBranchName = `agent/backend/init/${beTimestamp}`;
|
|
1343
|
+
const beWorktreeName = `backend-${config.projectName.toLowerCase().replace(/\s+/g, '-')}-init-${beTimestamp}`;
|
|
1344
|
+
const beWorktreePath = path.join(ROOT, 'worktrees', beWorktreeName);
|
|
1345
|
+
|
|
1346
|
+
console.log(`\n ${dim('Setting up backend/INIT workspace...')}\n`);
|
|
1347
|
+
|
|
1348
|
+
try {
|
|
1349
|
+
execSync(`git worktree add "${beWorktreePath}" -b ${beBranchName}`, { cwd: ROOT, stdio: 'pipe' });
|
|
1350
|
+
|
|
1351
|
+
// Framework files
|
|
1352
|
+
const beScope = generateClaudeScope({ project: 'backend', agent: 'INIT', branchName: beBranchName, worktreePath: beWorktreePath });
|
|
1353
|
+
fs.writeFileSync(path.join(beWorktreePath, '.claude-scope'), beScope, 'utf8');
|
|
1354
|
+
|
|
1355
|
+
const beTask = AGENT_DESCRIPTIONS.backend?.INIT || 'scaffolds backend architecture, folder structure, DB setup, wiring config and contracts';
|
|
1356
|
+
const beTaskMd = generateTaskMd({ project: 'backend', agent: 'INIT', task: beTask, branchName: beBranchName, contextSection: '', remoteSetupSection: '' });
|
|
1357
|
+
fs.writeFileSync(path.join(beWorktreePath, 'TASK.md'), beTaskMd, 'utf8');
|
|
1358
|
+
|
|
1359
|
+
const bePkg = {
|
|
1360
|
+
name: `${config.projectName.toLowerCase().replace(/\s+/g, '-')}-worktree`,
|
|
1361
|
+
version: '1.0.0', private: true,
|
|
1362
|
+
scripts: {
|
|
1363
|
+
init: 'multi-agents init',
|
|
1364
|
+
agent: `node "${ROOT}/.workflow/run.js"`,
|
|
1365
|
+
restart: `node "${ROOT}/.workflow/restart.js"`,
|
|
1366
|
+
reset: `node "${ROOT}/.workflow/reset.js"`,
|
|
1367
|
+
complete: `node "${ROOT}/.workflow/complete.js"`,
|
|
1368
|
+
},
|
|
1369
|
+
};
|
|
1370
|
+
fs.writeFileSync(path.join(beWorktreePath, 'package.json'), JSON.stringify(bePkg, null, 2), 'utf8');
|
|
1371
|
+
|
|
1372
|
+
const beGitignore = ['# Framework files — never commit these to the agent branch', 'package.json', '.claude-scope', 'scope.json', 'TASK.md', '.vscode/', '.idea/', '.zed/', 'node_modules/'].join('\n') + '\n';
|
|
1373
|
+
fs.writeFileSync(path.join(beWorktreePath, '.gitignore'), beGitignore, 'utf8');
|
|
1374
|
+
|
|
1375
|
+
// Tracking
|
|
1376
|
+
guards.updateTrackingSlot(tracking, 'backend', 'INIT', {
|
|
1377
|
+
branch: beBranchName,
|
|
1378
|
+
timestamp: beTimestamp,
|
|
1379
|
+
launchedAt: new Date().toISOString(),
|
|
1380
|
+
status: 'ACTIVE',
|
|
1381
|
+
worktreePath: beWorktreePath,
|
|
1382
|
+
backendPromptShown: true,
|
|
1383
|
+
backendLaunchTiming: 'now',
|
|
1384
|
+
}, ROOT);
|
|
1385
|
+
|
|
1386
|
+
// TASKS_HISTORY
|
|
1387
|
+
tasksHistory.ensureHistoryFile(ROOT);
|
|
1388
|
+
tasksHistory.writeSessionEntry(ROOT, { scope: 'backend', agent: 'INIT', branch: beBranchName, task: beTask, launchedAt: new Date().toISOString() });
|
|
1389
|
+
|
|
1390
|
+
console.log(` ${green('✓')} Backend/INIT worktree created: worktrees/${beWorktreeName}`);
|
|
1391
|
+
|
|
1392
|
+
// BUILD_STATE.md entry
|
|
1393
|
+
const beBuildStatePath = path.join(ROOT, 'BUILD_STATE.md');
|
|
1394
|
+
if (fs.existsSync(beBuildStatePath)) {
|
|
1395
|
+
const beDate = new Date().toISOString().split('T')[0];
|
|
1396
|
+
const beLogEntry = `| ${beDate} | INIT | backend | ${beTask} | IN PROGRESS | ${beBranchName} |\n`;
|
|
1397
|
+
fs.appendFileSync(beBuildStatePath, beLogEntry, 'utf8');
|
|
1398
|
+
try {
|
|
1399
|
+
execSync('git add BUILD_STATE.md', { cwd: ROOT, stdio: 'pipe' });
|
|
1400
|
+
execSync(`git commit --no-verify -m "build: INIT task started on backend [${beBranchName}]"`, { cwd: ROOT, stdio: 'pipe' });
|
|
1401
|
+
console.log(` ${green('✓')} BUILD_STATE.md updated`);
|
|
1402
|
+
} catch {}
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
// Open backend IDE window
|
|
1406
|
+
const beIdeOpened = openIDE(beWorktreePath);
|
|
1407
|
+
if (!beIdeOpened) console.log(` ${yellow('!')} Could not open IDE for backend — open manually at: ${dim(beWorktreePath)}`);
|
|
1408
|
+
console.log(` ${green('✓')} Backend/INIT workspace open — start a Claude Code session there when ready\n`);
|
|
1409
|
+
|
|
1410
|
+
} catch (e) {
|
|
1411
|
+
console.log(` ${yellow('⚠')} Could not create backend/INIT worktree: ${e.message}`);
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
separator();
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1146
1418
|
separator();
|
|
1147
1419
|
|
|
1148
1420
|
// ── Confirm ───────────────────────────────────────────────────────────────────
|
|
@@ -1225,6 +1497,13 @@ Mark each step complete. Only proceed to the task below when all are checked.
|
|
|
1225
1497
|
});
|
|
1226
1498
|
console.log(` ${green('✓')} Worktree created: worktrees/${worktreeName}`);
|
|
1227
1499
|
} catch (err) {
|
|
1500
|
+
const msg = err.stderr?.toString() || err.message || '';
|
|
1501
|
+
if (msg.includes('not a git repository')) {
|
|
1502
|
+
console.error(`
|
|
1503
|
+
${red('✖')} Not a git repository. Run git init first, then npm run init.
|
|
1504
|
+
`);
|
|
1505
|
+
process.exit(1);
|
|
1506
|
+
}
|
|
1228
1507
|
console.log(` ${yellow('!')} Worktree may already exist - continuing.`);
|
|
1229
1508
|
}
|
|
1230
1509
|
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* Run from the repo root after a task is complete.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
let prompts; try { prompts = require('prompts'); } catch { prompts = null; }
|
|
13
13
|
const fs = require('fs');
|
|
14
14
|
const path = require('path');
|
|
15
15
|
const { execSync, spawn } = require('child_process');
|
|
@@ -51,15 +51,23 @@ if (!fs.existsSync(LOCK_PATH)) {
|
|
|
51
51
|
|
|
52
52
|
const config = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'));
|
|
53
53
|
|
|
54
|
-
// ──
|
|
54
|
+
// ── Arrow confirm helper ──────────────────────────────────────────────────────
|
|
55
55
|
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
56
|
+
const arrowConfirm = async (message, initial = true) => {
|
|
57
|
+
if (prompts && process.stdin.isTTY) {
|
|
58
|
+
const res = await prompts({ type: 'confirm', name: 'value', message, initial }, { onCancel: () => process.exit(0) });
|
|
59
|
+
return res.value ?? initial;
|
|
60
|
+
}
|
|
61
|
+
return initial;
|
|
62
|
+
};
|
|
60
63
|
|
|
61
|
-
const
|
|
62
|
-
|
|
64
|
+
const arrowSelect = async (message, choices) => {
|
|
65
|
+
if (prompts && process.stdin.isTTY) {
|
|
66
|
+
const res = await prompts({ type: 'select', name: 'value', message, choices: choices.map((c, i) => ({ title: c, value: i })) }, { onCancel: () => process.exit(0) });
|
|
67
|
+
return res.value ?? 0;
|
|
68
|
+
}
|
|
69
|
+
return 0;
|
|
70
|
+
};
|
|
63
71
|
|
|
64
72
|
const separator = () => console.log(`\n${dim('─'.repeat(60))}`);
|
|
65
73
|
|
|
@@ -143,7 +151,6 @@ const main = async () => {
|
|
|
143
151
|
if (worktrees.length === 0) {
|
|
144
152
|
console.log(`\n${yellow(' No active task worktrees found.')}`);
|
|
145
153
|
console.log(dim(' Run npm run agent to start a task.\n'));
|
|
146
|
-
rl.close();
|
|
147
154
|
return;
|
|
148
155
|
}
|
|
149
156
|
|
|
@@ -163,15 +170,8 @@ const main = async () => {
|
|
|
163
170
|
selectedWorktree = worktrees[0];
|
|
164
171
|
console.log(dim(`\n Auto-selected: ${selectedWorktree.branch}\n`));
|
|
165
172
|
} else {
|
|
166
|
-
|
|
167
|
-
|
|
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
|
-
}
|
|
173
|
+
const idx = await arrowSelect('Select task to complete:', worktrees.map(wt => wt.branch));
|
|
174
|
+
selectedWorktree = worktrees[idx];
|
|
175
175
|
}
|
|
176
176
|
|
|
177
177
|
const { path: worktreePath, branch: branchName } = selectedWorktree;
|
|
@@ -219,7 +219,6 @@ const main = async () => {
|
|
|
219
219
|
console.log(dim(' The following files are outside the allowed scope for ' + scopeMeta.agent + ' (' + scopeMeta.scope + '):\n'));
|
|
220
220
|
violations.forEach(f => console.log(' ' + red('✗') + ' ' + f));
|
|
221
221
|
console.log('\n' + dim(' Fix the violations, then re-run npm run complete.\n'));
|
|
222
|
-
rl.close();
|
|
223
222
|
return;
|
|
224
223
|
}
|
|
225
224
|
|
|
@@ -238,10 +237,9 @@ const main = async () => {
|
|
|
238
237
|
if (!isCompleted && !isNonTTY) {
|
|
239
238
|
console.log(`\n${yellow(' TASK.md is not marked as COMPLETED.')}`);
|
|
240
239
|
console.log(dim(' The agent may still be working on this task.\n'));
|
|
241
|
-
const proceed = await
|
|
242
|
-
if (proceed
|
|
240
|
+
const proceed = await arrowConfirm('Proceed with merge anyway?', false);
|
|
241
|
+
if (!proceed) {
|
|
243
242
|
console.log(yellow('\n Aborted. Complete the task first.\n'));
|
|
244
|
-
rl.close();
|
|
245
243
|
return;
|
|
246
244
|
}
|
|
247
245
|
} else if (!isCompleted && isNonTTY) {
|
|
@@ -258,10 +256,9 @@ const main = async () => {
|
|
|
258
256
|
console.log(` ${dim('Into')} : ${green('main')}`);
|
|
259
257
|
console.log(` ${dim('Worktree')} : ${dim(worktreePath)}\n`);
|
|
260
258
|
|
|
261
|
-
const
|
|
262
|
-
if (
|
|
259
|
+
const confirmMerge = isNonTTY ? true : await arrowConfirm('Confirm merge into main?');
|
|
260
|
+
if (!confirmMerge) {
|
|
263
261
|
console.log(yellow('\n Aborted.\n'));
|
|
264
|
-
rl.close();
|
|
265
262
|
return;
|
|
266
263
|
}
|
|
267
264
|
|
|
@@ -328,10 +325,9 @@ const main = async () => {
|
|
|
328
325
|
} catch (err) {
|
|
329
326
|
console.log(` ${yellow('!')} Could not pull latest main.`);
|
|
330
327
|
if (!isNonTTY) {
|
|
331
|
-
const proceedAnyway = await
|
|
332
|
-
if (proceedAnyway
|
|
328
|
+
const proceedAnyway = await arrowConfirm('Proceed with merge anyway?');
|
|
329
|
+
if (!proceedAnyway) {
|
|
333
330
|
console.log(yellow('\n Aborted.\n'));
|
|
334
|
-
rl.close();
|
|
335
331
|
return;
|
|
336
332
|
}
|
|
337
333
|
} else {
|
|
@@ -362,12 +358,10 @@ const main = async () => {
|
|
|
362
358
|
} else {
|
|
363
359
|
console.log(`\n${red(' ✗ Merge conflict in:')} ${conflicts.join(', ')}`);
|
|
364
360
|
console.log(dim(' Resolve conflicts manually, then run: git commit\n'));
|
|
365
|
-
rl.close();
|
|
366
361
|
return;
|
|
367
362
|
}
|
|
368
363
|
} catch {
|
|
369
364
|
console.log(`\n${red(' ✗ Merge failed.')} Resolve manually.\n`);
|
|
370
|
-
rl.close();
|
|
371
365
|
return;
|
|
372
366
|
}
|
|
373
367
|
}
|
|
@@ -391,6 +385,59 @@ const main = async () => {
|
|
|
391
385
|
}
|
|
392
386
|
} catch { /* best-effort */ }
|
|
393
387
|
|
|
388
|
+
// ── Write start:client / start:backend into root package.json ───────────────
|
|
389
|
+
|
|
390
|
+
try {
|
|
391
|
+
const CLIENT_START = {
|
|
392
|
+
'Next.js': 'cd client && npm install && npm run dev',
|
|
393
|
+
'Nuxt': 'cd client && npm install && npm run dev',
|
|
394
|
+
'SvelteKit': 'cd client && npm install && npm run dev',
|
|
395
|
+
'Remix': 'cd client && npm install && npm run dev',
|
|
396
|
+
'Vite+React': 'cd client && npm install && npm run dev',
|
|
397
|
+
'Angular': 'cd client && npm install && npx ng serve',
|
|
398
|
+
};
|
|
399
|
+
const BACKEND_START = {
|
|
400
|
+
'NestJS': 'cd backend && npm install && npm run start:dev',
|
|
401
|
+
'Express': 'cd backend && npm install && npm run dev',
|
|
402
|
+
'Fastify': 'cd backend && npm install && npm run dev',
|
|
403
|
+
'Django': 'cd backend && pip install -r requirements.txt && python manage.py runserver',
|
|
404
|
+
'FastAPI': 'cd backend && pip install -r requirements.txt && uvicorn main:app --reload',
|
|
405
|
+
'Laravel': 'cd backend && composer install && php artisan serve',
|
|
406
|
+
'Rails': 'cd backend && bundle install && bin/rails server',
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
const pkgPath = path.join(ROOT, 'package.json');
|
|
410
|
+
if (fs.existsSync(pkgPath)) {
|
|
411
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
412
|
+
if (!pkg.scripts) pkg.scripts = {};
|
|
413
|
+
|
|
414
|
+
const parts = branchName.split('/');
|
|
415
|
+
const _t = guards.loadTracking(ROOT, config);
|
|
416
|
+
const uiDone = _t?.client?.UI?.status === 'COMPLETED';
|
|
417
|
+
const initDone = _t?.backend?.INIT?.status === 'COMPLETED';
|
|
418
|
+
|
|
419
|
+
if (uiDone) {
|
|
420
|
+
const fw = config.client && config.client.framework;
|
|
421
|
+
const cmd = CLIENT_START[fw];
|
|
422
|
+
if (cmd && !pkg.scripts['start:client']) {
|
|
423
|
+
pkg.scripts['start:client'] = cmd;
|
|
424
|
+
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n', 'utf8');
|
|
425
|
+
console.log(` ${green('✓')} start:client added to package.json (${cmd})`);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
if (initDone) {
|
|
430
|
+
const fw = config.backend && config.backend.framework;
|
|
431
|
+
const cmd = BACKEND_START[fw];
|
|
432
|
+
if (cmd && !pkg.scripts['start:backend']) {
|
|
433
|
+
pkg.scripts['start:backend'] = cmd;
|
|
434
|
+
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n', 'utf8');
|
|
435
|
+
console.log(` ${green('✓')} start:backend added to package.json (${cmd})`);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
} catch { /* best-effort */ }
|
|
440
|
+
|
|
394
441
|
// ── Update scaffold flags ──────────────────────────────────────────────────
|
|
395
442
|
|
|
396
443
|
try {
|
|
@@ -442,13 +489,85 @@ const main = async () => {
|
|
|
442
489
|
if (_agent) console.log(` ${dim('Agent')} : ${green(_agent)} ${dim('(' + _scope + ')')}`);
|
|
443
490
|
if (_task) console.log(` ${dim('Task')} : ${dim(_task)}`);
|
|
444
491
|
console.log(` ${dim('Branch')} : ${green(branchName)} merged into ${green('main')}\n`);
|
|
445
|
-
console.log(` ${bold('What to do next:')}\n`);
|
|
446
|
-
console.log(` Start a new task:`);
|
|
447
|
-
console.log(` ${cyan('npm run agent')}\n`);
|
|
448
|
-
separator();
|
|
449
|
-
console.log('');
|
|
450
492
|
|
|
451
|
-
|
|
493
|
+
// ── Derive next step from tracking ──────────────────────────────────────────
|
|
494
|
+
const AGENT_ORDER = {
|
|
495
|
+
client: ['UI', 'LOGIC', 'FORMS', 'ROUTING', 'TESTING', 'ACCESSIBILITY'],
|
|
496
|
+
backend: ['INIT', 'API', 'LOGIC', 'AUTH', 'DB', 'TESTING', 'EVENTS', 'JOBS'],
|
|
497
|
+
shared: ['SECURITY'],
|
|
498
|
+
};
|
|
499
|
+
const SCOPE_ORDER = ['client', 'backend', 'shared'];
|
|
500
|
+
|
|
501
|
+
try {
|
|
502
|
+
const _tracking = guards.loadTracking(ROOT, config);
|
|
503
|
+
|
|
504
|
+
const findNextInScope = (scope) => {
|
|
505
|
+
const order = AGENT_ORDER[scope] || [];
|
|
506
|
+
for (const a of order) {
|
|
507
|
+
const slot = (_tracking[scope] || {})[a];
|
|
508
|
+
if (!slot || (slot.status !== 'COMPLETED' && slot.status !== 'ACTIVE')) return a;
|
|
509
|
+
}
|
|
510
|
+
return null;
|
|
511
|
+
};
|
|
512
|
+
|
|
513
|
+
let nextScope = null;
|
|
514
|
+
let nextAgent = null;
|
|
515
|
+
|
|
516
|
+
const scopesToCheck = [_scope, ...SCOPE_ORDER.filter(s => s !== _scope)];
|
|
517
|
+
for (const s of scopesToCheck) {
|
|
518
|
+
const n = findNextInScope(s);
|
|
519
|
+
if (n) { nextScope = s; nextAgent = n; break; }
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
separator();
|
|
523
|
+
console.log(`\n ${bold('What to do next:')}`);
|
|
524
|
+
|
|
525
|
+
if (nextAgent) {
|
|
526
|
+
console.log(`\n ${green('→')} ${bold(nextAgent)} ${dim('(' + nextScope + ')')}`);
|
|
527
|
+
console.log(`\n ${cyan('npm run agent')}`);
|
|
528
|
+
console.log(` ${dim('Select: ' + nextScope + ' → ' + nextAgent)}`);
|
|
529
|
+
} else {
|
|
530
|
+
console.log(`\n ${cyan('npm run agent')}`);
|
|
531
|
+
}
|
|
532
|
+
} catch {
|
|
533
|
+
separator();
|
|
534
|
+
console.log(`\n ${bold('What to do next:')}\n`);
|
|
535
|
+
console.log(` ${cyan('npm run agent')}`);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
console.log('\n');
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
// ── Backend/INIT launch prompt (if user chose 'after' during LOGIC session) ──
|
|
542
|
+
try {
|
|
543
|
+
const _tracking = guards.loadTracking(ROOT, config);
|
|
544
|
+
const beSlot = _tracking?.backend?.INIT;
|
|
545
|
+
if (
|
|
546
|
+
_scope === 'logic' &&
|
|
547
|
+
_parts[0] === 'agent' && _parts[1] === 'client' &&
|
|
548
|
+
beSlot?.backendLaunchTiming === 'after' &&
|
|
549
|
+
beSlot?.status !== 'ACTIVE' &&
|
|
550
|
+
beSlot?.status !== 'COMPLETED'
|
|
551
|
+
) {
|
|
552
|
+
separator();
|
|
553
|
+
console.log(`\n ${yellow('⚡ Ready to scaffold your backend?')}\n`);
|
|
554
|
+
console.log(` client/LOGIC is merged. This is the ideal moment to launch backend/INIT.\n`);
|
|
555
|
+
|
|
556
|
+
const { spawn: _spawn } = require('child_process');
|
|
557
|
+
const launchIdx = await arrowSelect('Launch backend/INIT?', [
|
|
558
|
+
'Launch backend/INIT now ← recommended',
|
|
559
|
+
'Skip — I will handle it manually',
|
|
560
|
+
]);
|
|
561
|
+
|
|
562
|
+
if (launchIdx === 1) {
|
|
563
|
+
console.log(`\n ${green('✓')} Launching backend/INIT...\n`);
|
|
564
|
+
_spawn('node', [path.join(ROOT, '.workflow', 'agent.js'), '--scope=backend', '--agent=INIT'], {
|
|
565
|
+
cwd: ROOT, stdio: 'inherit',
|
|
566
|
+
});
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
} catch { /* best-effort */ }
|
|
452
571
|
};
|
|
453
572
|
|
|
454
573
|
main().catch((err) => {
|