multi-agents-cli 1.0.52 → 1.0.54
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 +1413 -0
- package/core/workflow/complete.js +403 -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 +32 -15
- package/package.json +2 -1
|
@@ -0,0 +1,1413 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Multi-Agent Monorepo Template - Task Launcher
|
|
5
|
+
* Run with: npm run agent
|
|
6
|
+
*
|
|
7
|
+
* Creates a Git Worktree, generates coordination files,
|
|
8
|
+
* and opens your IDE automatically at the correct path.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const readline = require('readline');
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
const { execSync } = require('child_process');
|
|
15
|
+
const guards = require('./guards');
|
|
16
|
+
const tasksHistory = require('./tasks_history');
|
|
17
|
+
|
|
18
|
+
// ── Prompts (arrow-key navigation) ───────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
let prompts;
|
|
21
|
+
try { prompts = require('prompts'); } catch { prompts = null; }
|
|
22
|
+
|
|
23
|
+
// Arrow-key select - falls back to number input if prompts unavailable
|
|
24
|
+
const arrowSelect = async (message, choices, rl) => {
|
|
25
|
+
if (prompts && process.stdin.isTTY) {
|
|
26
|
+
const res = await prompts({
|
|
27
|
+
type: 'select',
|
|
28
|
+
name: 'value',
|
|
29
|
+
message,
|
|
30
|
+
choices: choices.map((c, i) => ({ title: c.label || c, value: i })),
|
|
31
|
+
}, { onCancel: () => process.exit(0) });
|
|
32
|
+
return res.value;
|
|
33
|
+
}
|
|
34
|
+
// Fallback: number input
|
|
35
|
+
return new Promise(resolve => {
|
|
36
|
+
choices.forEach((c, i) => console.log(` ${dim(`${i + 1}.`)} ${c.label || c}`));
|
|
37
|
+
rl.question(`\n Select (1-${choices.length}): `, ans => {
|
|
38
|
+
const n = parseInt(ans) - 1;
|
|
39
|
+
resolve(!isNaN(n) && n >= 0 && n < choices.length ? n : 0);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const arrowConfirm = async (message, rl) => {
|
|
45
|
+
if (prompts && process.stdin.isTTY) {
|
|
46
|
+
const res = await prompts({
|
|
47
|
+
type: 'confirm',
|
|
48
|
+
name: 'value',
|
|
49
|
+
message,
|
|
50
|
+
initial: true,
|
|
51
|
+
}, { onCancel: () => process.exit(0) });
|
|
52
|
+
return res.value ?? true;
|
|
53
|
+
}
|
|
54
|
+
return new Promise(resolve => {
|
|
55
|
+
rl.question(`${message} (y/n): `, ans => resolve(ans.toLowerCase() !== 'n'));
|
|
56
|
+
});
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// ── Colors ────────────────────────────────────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
const c = {
|
|
62
|
+
reset: '\x1b[0m',
|
|
63
|
+
bold: '\x1b[1m',
|
|
64
|
+
dim: '\x1b[2m',
|
|
65
|
+
green: '\x1b[32m',
|
|
66
|
+
blue: '\x1b[34m',
|
|
67
|
+
yellow: '\x1b[33m',
|
|
68
|
+
cyan: '\x1b[36m',
|
|
69
|
+
red: '\x1b[31m',
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const bold = (s) => `${c.bold}${s}${c.reset}`;
|
|
73
|
+
const green = (s) => `${c.green}${s}${c.reset}`;
|
|
74
|
+
const yellow = (s) => `${c.yellow}${s}${c.reset}`;
|
|
75
|
+
const dim = (s) => `${c.dim}${s}${c.reset}`;
|
|
76
|
+
const cyan = (s) => `${c.cyan}${s}${c.reset}`;
|
|
77
|
+
const blue = (s) => `${c.blue}${s}${c.reset}`;
|
|
78
|
+
const red = (s) => `${c.red}${s}${c.reset}`;
|
|
79
|
+
|
|
80
|
+
// ── Paths ─────────────────────────────────────────────────────────────────────
|
|
81
|
+
|
|
82
|
+
const ROOT = path.join(__dirname, '..');
|
|
83
|
+
const CONFIG_PATH = path.join(ROOT, '.scaffold', '.config.json');
|
|
84
|
+
const LOCK_PATH = path.join(ROOT, '.scaffold', '.initialized');
|
|
85
|
+
|
|
86
|
+
// ── Guards ────────────────────────────────────────────────────────────────────
|
|
87
|
+
|
|
88
|
+
if (!fs.existsSync(LOCK_PATH)) {
|
|
89
|
+
console.log(`\n${red(' Project not initialized.')}`);
|
|
90
|
+
console.log(dim(' Run `') + cyan('npm run init') + dim('` first.\n'));
|
|
91
|
+
process.exit(1);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (!fs.existsSync(CONFIG_PATH)) {
|
|
95
|
+
console.log(`\n${red(' Missing .scaffold/.config.json.')}`);
|
|
96
|
+
console.log(dim(' Run `') + cyan('npm run init') + dim('` to regenerate.\n'));
|
|
97
|
+
process.exit(1);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const config = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'));
|
|
101
|
+
|
|
102
|
+
// ── Location guard ────────────────────────────────────────────────────────────
|
|
103
|
+
|
|
104
|
+
const normalizedCwd = path.resolve(process.cwd());
|
|
105
|
+
const normalizedRoot = path.resolve(ROOT);
|
|
106
|
+
|
|
107
|
+
if (normalizedCwd !== normalizedRoot) {
|
|
108
|
+
console.log(`\n${red(' Wrong location detected.')}`);
|
|
109
|
+
console.log(dim(` You are here : ${normalizedCwd}`));
|
|
110
|
+
console.log(dim(` Should be : ${normalizedRoot}\n`));
|
|
111
|
+
console.log(bold(' Run this to fix it and launch:'));
|
|
112
|
+
console.log(cyan(`\n cd "${normalizedRoot}" && npm run agent\n`));
|
|
113
|
+
process.exit(1);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ── IDE gate ──────────────────────────────────────────────────────────────────
|
|
117
|
+
|
|
118
|
+
if (!config.ide) {
|
|
119
|
+
console.log(`\n${red(' IDE not configured.')}`);
|
|
120
|
+
console.log(dim(' Run `') + cyan('npm run init') + dim('` to configure your IDE preference.\n'));
|
|
121
|
+
process.exit(1);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ── Open IDE ──────────────────────────────────────────────────────────────────
|
|
125
|
+
|
|
126
|
+
const openIDE = (worktreePath) => {
|
|
127
|
+
const { name, strategy, cmd, app, openArgs, winPaths, linuxPaths } = config.ide;
|
|
128
|
+
|
|
129
|
+
if (strategy === 'manual' || !strategy) {
|
|
130
|
+
// Last-resort fallback for JetBrains IDEs - try open -a directly
|
|
131
|
+
if (app && process.platform === 'darwin') {
|
|
132
|
+
try {
|
|
133
|
+
execSync(`open -a "${app}" "${worktreePath}"`, { stdio: 'pipe' });
|
|
134
|
+
return name;
|
|
135
|
+
} catch { /* genuinely not installed */ }
|
|
136
|
+
}
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const args = (openArgs || []).join(' ');
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
if (strategy === 'mac-app') {
|
|
144
|
+
const argsStr = args ? `--args ${args}` : '';
|
|
145
|
+
execSync(`open -na "${app}" ${argsStr} "${worktreePath}"`.trim(), { stdio: 'pipe' });
|
|
146
|
+
|
|
147
|
+
} else if (strategy === 'win-exe') {
|
|
148
|
+
const exe = (winPaths || []).find(p => fs.existsSync(p));
|
|
149
|
+
if (!exe) return null;
|
|
150
|
+
execSync(`start "" "${exe}" ${args} "${worktreePath}"`.trim(), { stdio: 'pipe' });
|
|
151
|
+
|
|
152
|
+
} else if (strategy === 'linux-path') {
|
|
153
|
+
const bin = (linuxPaths || []).find(p => fs.existsSync(p));
|
|
154
|
+
if (!bin) return null;
|
|
155
|
+
execSync(`"${bin}" ${args} "${worktreePath}"`.trim(), { stdio: 'pipe' });
|
|
156
|
+
|
|
157
|
+
} else {
|
|
158
|
+
const platform = process.platform;
|
|
159
|
+
if (platform === 'win32') {
|
|
160
|
+
execSync(`start "" "${cmd}" ${args} "${worktreePath}"`.trim(), { stdio: 'pipe' });
|
|
161
|
+
} else {
|
|
162
|
+
execSync(`"${cmd}" ${args} "${worktreePath}"`.trim(), { stdio: 'pipe' });
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return name;
|
|
167
|
+
} catch {
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
// ── Agent map ─────────────────────────────────────────────────────────────────
|
|
173
|
+
|
|
174
|
+
const AGENTS = {
|
|
175
|
+
client: ['UI', 'LOGIC', 'FORMS', 'ROUTING', 'TESTING', 'ACCESSIBILITY'],
|
|
176
|
+
backend: ['INIT', 'API', 'LOGIC', 'AUTH', 'DB', 'TESTING', 'EVENTS', 'JOBS'],
|
|
177
|
+
shared: ['SECURITY'],
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
// Short descriptions per agent
|
|
181
|
+
const AGENT_DESCRIPTIONS = {
|
|
182
|
+
client: {
|
|
183
|
+
UI: 'scaffolds the full project structure',
|
|
184
|
+
LOGIC: 'state management, API integration, custom hooks',
|
|
185
|
+
FORMS: 'form components, validation, submission handling',
|
|
186
|
+
ROUTING: 'page routing, navigation, URL structure',
|
|
187
|
+
TESTING: 'unit and integration tests',
|
|
188
|
+
ACCESSIBILITY: 'a11y compliance, keyboard navigation',
|
|
189
|
+
},
|
|
190
|
+
backend: {
|
|
191
|
+
INIT: 'scaffolds backend architecture, folder structure, DB setup, wiring config and contracts',
|
|
192
|
+
API: 'REST/GraphQL endpoints, request/response handling',
|
|
193
|
+
LOGIC: 'business logic, services, data processing',
|
|
194
|
+
AUTH: 'authentication, authorization, session management',
|
|
195
|
+
DB: 'database schemas, migrations, queries',
|
|
196
|
+
TESTING: 'API and integration tests',
|
|
197
|
+
EVENTS: 'event queues, pub/sub, webhooks',
|
|
198
|
+
JOBS: 'background jobs, scheduled tasks, workers',
|
|
199
|
+
},
|
|
200
|
+
shared: {
|
|
201
|
+
SECURITY: 'shared auth utilities, encryption, input validation',
|
|
202
|
+
},
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
// Scope constraints appended to every task description - agent cannot bypass its own task
|
|
206
|
+
const AGENT_TASK_SUFFIX = {
|
|
207
|
+
client: {
|
|
208
|
+
UI: ' - scaffold project structure and component shells ONLY. No business logic, state management, or API calls. Use <!-- TODO: LOGIC agent --> where logic will be needed.',
|
|
209
|
+
LOGIC: ' - implement state, services, and API integration ONLY. No UI markup or styling changes. No route definitions.',
|
|
210
|
+
FORMS: ' - implement form components, validation rules, and submission handlers ONLY. No UI redesign. No state outside forms.',
|
|
211
|
+
ROUTING: ' - implement routes, guards, lazy loading, and navigation ONLY. No business logic. No UI changes.',
|
|
212
|
+
TESTING: ' - write unit and integration tests ONLY. Do not modify production code except to fix bugs directly revealed by failing tests.',
|
|
213
|
+
ACCESSIBILITY: ' - implement ARIA attributes, keyboard navigation, and semantic HTML ONLY. No visual redesign. No business logic changes.',
|
|
214
|
+
},
|
|
215
|
+
backend: {
|
|
216
|
+
INIT: ' - scaffold the backend architecture ONLY. Decide folder structure, MVC/service pattern, DB connection setup, and framework configuration. Bootstrap CONTRACTS.md with initial shared types. Write wiring.config.json backend section with all required runtime vars per environment. No endpoint implementation. No business logic.',
|
|
217
|
+
API: ' - read CONTRACTS.md and wiring.config.json first as binding contracts before implementing anything. Implement route handlers, controllers, and DTOs ONLY. No business logic services. No auth middleware. No database queries. No architectural changes.',
|
|
218
|
+
LOGIC: ' - implement services and business logic ONLY. No route definitions. No auth middleware. No database schema changes.',
|
|
219
|
+
AUTH: ' - implement authentication and authorization ONLY. No business logic. No database schema changes. No API route restructuring.',
|
|
220
|
+
DB: ' - implement database schema, migrations, and queries ONLY. No business logic. No API handlers. No auth.',
|
|
221
|
+
TESTING: ' - write unit and integration tests ONLY. Do not modify production code except to fix bugs directly revealed by failing tests.',
|
|
222
|
+
EVENTS: ' - implement event queues, pub/sub, and webhooks ONLY. No business logic. No API endpoint changes.',
|
|
223
|
+
JOBS: ' - implement background jobs and scheduled tasks ONLY. No business logic services. No API endpoints.',
|
|
224
|
+
},
|
|
225
|
+
shared: {
|
|
226
|
+
SECURITY: ' - implement shared auth utilities, encryption, and input validation ONLY. No scope-specific business logic.',
|
|
227
|
+
},
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
// Agents that require an existing scope scaffold before they can run
|
|
231
|
+
const SCAFFOLD_REQUIRED = ['LOGIC', 'FORMS', 'ROUTING', 'TESTING', 'ACCESSIBILITY', 'API', 'AUTH', 'DB', 'EVENTS', 'JOBS'];
|
|
232
|
+
|
|
233
|
+
// Agents that depend on shared contracts (CONTRACTS.md)
|
|
234
|
+
const CONTRACTS_REQUIRED = ['LOGIC', 'AUTH', 'API', 'FORMS', 'INIT'];
|
|
235
|
+
|
|
236
|
+
// ── CLOUD prereq evaluator ────────────────────────────────────────────────────
|
|
237
|
+
|
|
238
|
+
const evalCloudPrereqs = (entries) => {
|
|
239
|
+
const bt = config.backend?.type;
|
|
240
|
+
const clientUI = entries.find(e => e.scope === 'client' && e.agent === 'UI' && e.status === 'COMPLETED');
|
|
241
|
+
const clientLogic = entries.find(e => e.scope === 'client' && e.agent === 'LOGIC' && e.status === 'COMPLETED');
|
|
242
|
+
const backendINIT = entries.find(e => e.scope === 'backend' && e.agent === 'INIT' && e.status === 'COMPLETED');
|
|
243
|
+
const isClientOnly = !bt || bt === 'integrated';
|
|
244
|
+
const isBackendOnly = bt === 'separate' && !clientUI;
|
|
245
|
+
|
|
246
|
+
const prereqMet = isClientOnly
|
|
247
|
+
? !!(clientUI && clientLogic)
|
|
248
|
+
: isBackendOnly
|
|
249
|
+
? !!backendINIT
|
|
250
|
+
: !!(clientUI && clientLogic && backendINIT);
|
|
251
|
+
|
|
252
|
+
return { prereqMet, clientUI, clientLogic, backendINIT, isClientOnly, isBackendOnly };
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
// Prerequisite agents that must be COMPLETED before an agent can run
|
|
256
|
+
const AGENT_PREREQUISITES = {
|
|
257
|
+
client: {
|
|
258
|
+
LOGIC: ['UI'],
|
|
259
|
+
FORMS: ['UI'],
|
|
260
|
+
ROUTING: ['UI'],
|
|
261
|
+
TESTING: ['UI', 'LOGIC'],
|
|
262
|
+
ACCESSIBILITY: ['UI'],
|
|
263
|
+
},
|
|
264
|
+
backend: {
|
|
265
|
+
API: ['INIT'],
|
|
266
|
+
LOGIC: ['INIT', 'DB'],
|
|
267
|
+
AUTH: ['INIT', 'LOGIC'],
|
|
268
|
+
DB: ['INIT'],
|
|
269
|
+
EVENTS: ['INIT', 'API'],
|
|
270
|
+
JOBS: ['INIT', 'DB'],
|
|
271
|
+
TESTING: ['INIT', 'API', 'LOGIC'],
|
|
272
|
+
},
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
const DOD_ITEMS = {
|
|
276
|
+
UI: ['All planned components exist and render correctly', 'No business logic inside components', 'All values derive from design tokens', 'Shared types consumed from CONTRACTS.md'],
|
|
277
|
+
LOGIC: ['All planned logic units exist and function correctly', 'No API calls outside the service layer', 'All response types from CONTRACTS.md', 'State and data fetching concerns separated'],
|
|
278
|
+
FORMS: ['All fields exist with correct validation rules', 'Error messages are clear and user-facing', 'Submission payload matches CONTRACTS.md', 'Double submission is prevented'],
|
|
279
|
+
ROUTING: ['All routes resolve to correct components', 'Every protected route declares its guard', 'All routes are lazy loaded unless justified', 'Route paths are centralized'],
|
|
280
|
+
TESTING: ['All planned test cases exist and pass', 'Happy path, edge cases, and failure states covered', 'Test data shapes from CONTRACTS.md', 'No implementation changes made'],
|
|
281
|
+
ACCESSIBILITY: ['All audit findings resolved', 'Every interactive element keyboard reachable', 'Focus managed after dynamic content changes', 'Color contrast meets WCAG 2.1 AA'],
|
|
282
|
+
API: ['All endpoints exist with correct HTTP methods', 'DTOs own all input validation', 'All types in CONTRACTS.md', 'Every endpoint declares access control'],
|
|
283
|
+
AUTH: ['All strategies and guards function correctly', 'No secrets in code', 'All tokens have expiry set', 'Auth failures return consistent responses'],
|
|
284
|
+
DB: ['All entities and relationships defined', 'Migration generated and surfaced', 'Repository methods own all queries', 'No ORM auto-sync used'],
|
|
285
|
+
EVENTS: ['All emitters and handlers exist', 'Receivers acknowledge immediately', 'All handlers are idempotent', 'Failure handling defined'],
|
|
286
|
+
JOBS: ['All jobs exist with correct triggers', 'Schedule expressions from config', 'All jobs are idempotent', 'Failure strategy defined for every job'],
|
|
287
|
+
SECURITY: ['All findings documented with severity', 'Every finding has a remediation proposal', 'OWASP Top 10 coverage confirmed', 'No fixes implemented directly'],
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
// ── Agent context questions ───────────────────────────────────────────────────
|
|
291
|
+
|
|
292
|
+
const AGENT_QUESTIONS = {
|
|
293
|
+
LOGIC: [
|
|
294
|
+
{ key: 'entities', prompt: 'What entities / models are involved?', consequence: 'agent may generate incompatible types' },
|
|
295
|
+
{ key: 'endpoints', prompt: 'What API endpoints need to be integrated?', consequence: 'agent may assume incorrect contracts' },
|
|
296
|
+
{ key: 'state', prompt: 'What state needs to be managed?', consequence: 'agent may miss state requirements' },
|
|
297
|
+
{ key: 'contracts', prompt: 'Any contracts from CONTRACTS.md to reference?', consequence: 'shared types may need rework after' },
|
|
298
|
+
],
|
|
299
|
+
FORMS: [
|
|
300
|
+
{ key: 'fields', prompt: 'What form fields are required?', consequence: 'agent may miss field requirements' },
|
|
301
|
+
{ key: 'validation', prompt: 'What validation rules apply?', consequence: 'validation logic may be incomplete' },
|
|
302
|
+
{ key: 'endpoint', prompt: 'What endpoint does this form submit to?', consequence: 'submission payload may not match contracts' },
|
|
303
|
+
],
|
|
304
|
+
AUTH: [
|
|
305
|
+
{ key: 'strategy', prompt: 'What auth strategy is needed? (JWT / OAuth / etc.)', consequence: 'auth implementation may use incorrect strategy' },
|
|
306
|
+
{ key: 'guards', prompt: 'What entities or routes need auth guards?', consequence: 'access control may be incomplete' },
|
|
307
|
+
{ key: 'tokens', prompt: 'What token / session requirements apply?', consequence: 'token handling may not match contracts' },
|
|
308
|
+
],
|
|
309
|
+
API: [
|
|
310
|
+
{ key: 'endpoints', prompt: 'What endpoints need to be created?', consequence: 'endpoint coverage may be incomplete' },
|
|
311
|
+
{ key: 'dtos', prompt: 'What request / response DTOs are needed?', consequence: 'DTOs may not match client contracts' },
|
|
312
|
+
{ key: 'auth', prompt: 'Which endpoints require auth guards?', consequence: 'access control may be missing' },
|
|
313
|
+
],
|
|
314
|
+
DB: [
|
|
315
|
+
{ key: 'entities', prompt: 'What entities / tables need to be defined?', consequence: 'schema may be incomplete' },
|
|
316
|
+
{ key: 'relations', prompt: 'What relationships exist between entities?', consequence: 'relations may be missing or incorrect' },
|
|
317
|
+
{ key: 'migrations', prompt: 'Any specific migration requirements?', consequence: 'migration may not match expected schema' },
|
|
318
|
+
],
|
|
319
|
+
TESTING: [
|
|
320
|
+
{ key: 'scenarios', prompt: 'What scenarios / flows need test coverage?', consequence: 'test coverage may be insufficient' },
|
|
321
|
+
{ key: 'edge', prompt: 'What edge cases should be covered?', consequence: 'edge cases may be missed' },
|
|
322
|
+
],
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
// ── BUILD_STATE parser ────────────────────────────────────────────────────────
|
|
326
|
+
|
|
327
|
+
const parseBuildState = () => {
|
|
328
|
+
const p = path.join(ROOT, 'BUILD_STATE.md');
|
|
329
|
+
if (!fs.existsSync(p)) return [];
|
|
330
|
+
const lines = fs.readFileSync(p, 'utf8').split('\n');
|
|
331
|
+
const entries = [];
|
|
332
|
+
for (const line of lines) {
|
|
333
|
+
if (!line.startsWith('|') || line.includes('---') || line.toLowerCase().includes('| agent ')) continue;
|
|
334
|
+
const cols = line.split('|').map(c => c.trim()).filter(Boolean);
|
|
335
|
+
if (cols.length >= 5) {
|
|
336
|
+
entries.push({
|
|
337
|
+
date: cols[0],
|
|
338
|
+
agent: cols[1],
|
|
339
|
+
scope: cols[2],
|
|
340
|
+
task: cols[3],
|
|
341
|
+
status: cols[4],
|
|
342
|
+
branch: cols[5] || '',
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
return entries;
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
// ── Active worktrees / reconciliation delegated to guards ────────────────────
|
|
350
|
+
|
|
351
|
+
const checkContracts = () => {
|
|
352
|
+
const p = path.join(ROOT, 'CONTRACTS.md');
|
|
353
|
+
if (!fs.existsSync(p)) return { exists: false, hasContent: false };
|
|
354
|
+
const content = fs.readFileSync(p, 'utf8');
|
|
355
|
+
const hasContent = /interface\s+\w+|type\s+\w+\s*=|enum\s+\w+/i.test(content);
|
|
356
|
+
return { exists: true, hasContent };
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
// ── Scope options builder ─────────────────────────────────────────────────────
|
|
360
|
+
|
|
361
|
+
const buildScopeOptions = () => {
|
|
362
|
+
const bt = config.backend?.type;
|
|
363
|
+
const options = [];
|
|
364
|
+
|
|
365
|
+
options.push({ name: 'client', label: 'client' });
|
|
366
|
+
|
|
367
|
+
if (bt === 'integrated') {
|
|
368
|
+
options.push({ name: 'backend', label: 'backend' });
|
|
369
|
+
} else if (!bt) {
|
|
370
|
+
options.push({ name: 'backend', label: `backend ${yellow('⚠ not configured')}`, needsConfig: true });
|
|
371
|
+
} else {
|
|
372
|
+
options.push({ name: 'backend', label: 'backend' });
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
options.push({ name: 'shared', label: 'shared' });
|
|
376
|
+
|
|
377
|
+
// CLOUD appears only when prerequisites are met
|
|
378
|
+
const rawEntries = parseBuildState();
|
|
379
|
+
const { prereqMet } = evalCloudPrereqs(rawEntries);
|
|
380
|
+
if (prereqMet && config.cloudDeployment !== 'skipped') {
|
|
381
|
+
options.push({ name: 'cloud', label: `${cyan('☁ cloud')} ${dim('deploy to production')}` });
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
return options;
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
// ── Project status display ────────────────────────────────────────────────────
|
|
388
|
+
|
|
389
|
+
const displayProjectStatus = (entries, contracts) => {
|
|
390
|
+
const scopeEntries = (scope) => entries.filter(e => e.scope === scope);
|
|
391
|
+
|
|
392
|
+
const renderScope = (scope) => {
|
|
393
|
+
const all = scopeEntries(scope);
|
|
394
|
+
if (all.length === 0) return dim('○ not started');
|
|
395
|
+
const completed = all.filter(e => e.status === 'COMPLETED').map(e => e.agent);
|
|
396
|
+
const inProgress = all.filter(e => e.status === 'IN PROGRESS').map(e => e.agent);
|
|
397
|
+
const parts = [];
|
|
398
|
+
if (completed.length) parts.push(green(completed.join(', ') + ' ✓'));
|
|
399
|
+
if (inProgress.length) parts.push(yellow(inProgress.join(', ') + ' …'));
|
|
400
|
+
return parts.join(' ');
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
const contractsNote = contracts.hasContent
|
|
404
|
+
? green('contracts defined')
|
|
405
|
+
: yellow('no contracts defined');
|
|
406
|
+
|
|
407
|
+
const bt = config.backend?.type;
|
|
408
|
+
|
|
409
|
+
console.log(`\n${bold('Project Status')} ${dim('-')} ${bold(config.projectName)}\n`);
|
|
410
|
+
console.log(` ${bold('client')} ${renderScope('client')}`);
|
|
411
|
+
console.log(` ${bold('shared')} ${renderScope('shared')} ${dim('|')} ${contractsNote}`);
|
|
412
|
+
|
|
413
|
+
if (bt === 'integrated') {
|
|
414
|
+
console.log(` ${dim('backend')} ${dim('integrated into client')}`);
|
|
415
|
+
} else if (!bt) {
|
|
416
|
+
console.log(` ${dim('backend')} ${yellow('✗ not configured')}`);
|
|
417
|
+
} else {
|
|
418
|
+
const beStatus = renderScope('backend') || dim('○ not started');
|
|
419
|
+
const ormsNote = contracts.hasContent ? '' : ` ${dim('|')} ${yellow('no ORMs / DTOs in CONTRACTS.md')}`;
|
|
420
|
+
console.log(` ${bold('backend')} ${beStatus}${ormsNote}`);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// ── Cloud status ───────────────────────────────────────────────────────────
|
|
424
|
+
const clientUI = entries.find(e => e.scope === 'client' && e.agent === 'UI' && e.status === 'COMPLETED');
|
|
425
|
+
const clientLogic = entries.find(e => e.scope === 'client' && e.agent === 'LOGIC' && e.status === 'COMPLETED');
|
|
426
|
+
const backendINIT = entries.find(e => e.scope === 'backend' && e.agent === 'INIT' && e.status === 'COMPLETED');
|
|
427
|
+
const isClientOnly = !bt || bt === 'integrated';
|
|
428
|
+
const isBackendOnly = bt === 'separate' && !clientUI;
|
|
429
|
+
|
|
430
|
+
const cloudPrereqMet = isClientOnly
|
|
431
|
+
? (clientUI && clientLogic)
|
|
432
|
+
: isBackendOnly
|
|
433
|
+
? backendINIT
|
|
434
|
+
: (clientUI && clientLogic && backendINIT);
|
|
435
|
+
|
|
436
|
+
const cloudSkipped = config.cloudDeployment === 'skipped';
|
|
437
|
+
|
|
438
|
+
if (!cloudSkipped) {
|
|
439
|
+
if (cloudPrereqMet) {
|
|
440
|
+
console.log(` ${bold('☁ cloud')} ${green('✓ ready to deploy')} ${dim('|')} ${dim('select CLOUD to begin')}`);
|
|
441
|
+
} else {
|
|
442
|
+
console.log(` ${dim('☁ cloud')} ${dim('available after more of your project is built')}`);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
console.log('');
|
|
447
|
+
};
|
|
448
|
+
|
|
449
|
+
// ── Readline ──────────────────────────────────────────────────────────────────
|
|
450
|
+
|
|
451
|
+
const rl = readline.createInterface({
|
|
452
|
+
input: process.stdin,
|
|
453
|
+
output: process.stdout,
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
const ask = (question) =>
|
|
457
|
+
new Promise((resolve) => rl.question(question, (a) => resolve(a.trim())));
|
|
458
|
+
|
|
459
|
+
const showList = (items) => {
|
|
460
|
+
items.forEach((item, i) => {
|
|
461
|
+
const label = typeof item === 'string' ? item : item.label;
|
|
462
|
+
console.log(` ${dim(`${i + 1}.`)} ${label}`);
|
|
463
|
+
});
|
|
464
|
+
};
|
|
465
|
+
|
|
466
|
+
const showAgentList = (scope, agents, buildEntries) => {
|
|
467
|
+
const completed = buildEntries
|
|
468
|
+
.filter(e => e.scope === scope && e.status === 'COMPLETED')
|
|
469
|
+
.map(e => e.agent);
|
|
470
|
+
|
|
471
|
+
// Find recommended next agent
|
|
472
|
+
const prereqs = AGENT_PREREQUISITES[scope] || {};
|
|
473
|
+
let recommended = null;
|
|
474
|
+
|
|
475
|
+
for (const agent of agents) {
|
|
476
|
+
if (completed.includes(agent)) continue;
|
|
477
|
+
const reqs = prereqs[agent] || [];
|
|
478
|
+
const metReqs = reqs.every(r => completed.includes(r));
|
|
479
|
+
if (metReqs) { recommended = agent; break; }
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
const descriptions = AGENT_DESCRIPTIONS[scope] || {};
|
|
483
|
+
|
|
484
|
+
agents.forEach((agent, i) => {
|
|
485
|
+
const desc = descriptions[agent] ? dim(` ${descriptions[agent]}`) : '';
|
|
486
|
+
const isRecommended = agent === recommended;
|
|
487
|
+
const tag = isRecommended
|
|
488
|
+
? (completed.length === 0 ? cyan(' ← start here') : cyan(' ← next step'))
|
|
489
|
+
: '';
|
|
490
|
+
const label = isRecommended ? bold(agent) : agent;
|
|
491
|
+
console.log(` ${dim(`${i + 1}.`)} ${label}${tag}${desc}`);
|
|
492
|
+
});
|
|
493
|
+
};
|
|
494
|
+
|
|
495
|
+
const selectRequired = async (prompt, items) => {
|
|
496
|
+
const idx = await arrowSelect(prompt, items.map(i => ({ label: typeof i === 'string' ? i : (i.label || i) })), rl);
|
|
497
|
+
return items[idx];
|
|
498
|
+
};
|
|
499
|
+
|
|
500
|
+
const separator = () => console.log(`\n${dim('─'.repeat(60))}`);
|
|
501
|
+
|
|
502
|
+
// ── Agent context gathering ───────────────────────────────────────────────────
|
|
503
|
+
|
|
504
|
+
const gatherAgentContext = async (agent) => {
|
|
505
|
+
const questions = AGENT_QUESTIONS[agent];
|
|
506
|
+
if (!questions) return { answers: {}, skipped: [] };
|
|
507
|
+
|
|
508
|
+
separator();
|
|
509
|
+
console.log(`\n${bold(blue('Agent context'))} ${dim('- press Enter to skip any question')}\n`);
|
|
510
|
+
|
|
511
|
+
const answers = {};
|
|
512
|
+
const skipped = [];
|
|
513
|
+
|
|
514
|
+
for (const q of questions) {
|
|
515
|
+
if (prompts && process.stdin.isTTY) {
|
|
516
|
+
const res = await prompts({
|
|
517
|
+
type: 'text',
|
|
518
|
+
name: 'value',
|
|
519
|
+
message: q.prompt,
|
|
520
|
+
hint: 'Enter to skip',
|
|
521
|
+
}, { onCancel: () => process.exit(0) });
|
|
522
|
+
const answer = (res.value || '').trim();
|
|
523
|
+
if (!answer) skipped.push(q);
|
|
524
|
+
else answers[q.key] = answer;
|
|
525
|
+
} else {
|
|
526
|
+
const answer = await ask(` ${bold(q.prompt)}\n ${dim('→')} `);
|
|
527
|
+
if (!answer.trim()) skipped.push(q);
|
|
528
|
+
else answers[q.key] = answer.trim();
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
return { answers, skipped };
|
|
533
|
+
};
|
|
534
|
+
|
|
535
|
+
// ── Skip acknowledgment ───────────────────────────────────────────────────────
|
|
536
|
+
|
|
537
|
+
const acknowledgeSkipped = async (skipped) => {
|
|
538
|
+
separator();
|
|
539
|
+
console.log(`\n${yellow(' ⚠ Incomplete context - the following was skipped:')}\n`);
|
|
540
|
+
skipped.forEach(q => {
|
|
541
|
+
console.log(` ${dim('→')} ${bold(q.prompt)}`);
|
|
542
|
+
console.log(` ${red('Risk:')} ${q.consequence}\n`);
|
|
543
|
+
});
|
|
544
|
+
console.log(dim(' The agent will flag all assumptions based on missing context.'));
|
|
545
|
+
console.log(dim(' This may require additional review passes after completion.\n'));
|
|
546
|
+
|
|
547
|
+
const proceedIdx = await arrowSelect('Proceed with incomplete context?', [
|
|
548
|
+
{ label: `${green('✓')} Proceed - agent flags assumptions` },
|
|
549
|
+
{ label: `${yellow('←')} Go back - fill in missing context` },
|
|
550
|
+
{ label: `${red('✗')} Abort` },
|
|
551
|
+
], rl);
|
|
552
|
+
|
|
553
|
+
if (proceedIdx === 1) return null; // signal: re-gather
|
|
554
|
+
if (proceedIdx === 2) return false; // signal: abort
|
|
555
|
+
return true; // signal: confirmed
|
|
556
|
+
};
|
|
557
|
+
|
|
558
|
+
// ── File generators ───────────────────────────────────────────────────────────
|
|
559
|
+
|
|
560
|
+
const generateClaudeScope = ({ project, agent, branchName, worktreePath }) => {
|
|
561
|
+
return `# .claude-scope
|
|
562
|
+
# Auto-generated by .workflow/agent.js
|
|
563
|
+
# This file identifies the scope of this worktree.
|
|
564
|
+
# Read this file at session start and verify scope before proceeding.
|
|
565
|
+
|
|
566
|
+
project : ${project}
|
|
567
|
+
agent : ${agent}
|
|
568
|
+
branch : ${branchName}
|
|
569
|
+
worktree : ${worktreePath}
|
|
570
|
+
|
|
571
|
+
## Scope Verification Rule
|
|
572
|
+
Before doing anything else, verify:
|
|
573
|
+
1. The loaded CLAUDE.md matches the project scope above
|
|
574
|
+
2. If opened at repo root instead of this worktree - hard stop:
|
|
575
|
+
|
|
576
|
+
WRONG CONTEXT - CANNOT PROCEED
|
|
577
|
+
This worktree is scoped to: ${project}/${agent}
|
|
578
|
+
Close and reopen at: ${worktreePath}
|
|
579
|
+
|
|
580
|
+
3. Read TASK.md for the current task prompt
|
|
581
|
+
4. Reference the correct agent file: agents/${agent}.md
|
|
582
|
+
`;
|
|
583
|
+
};
|
|
584
|
+
|
|
585
|
+
const generateContextSection = (answers, skipped) => {
|
|
586
|
+
if (Object.keys(answers).length === 0 && skipped.length === 0) return '';
|
|
587
|
+
|
|
588
|
+
let section = '\n---\n\n## Agent Context\n';
|
|
589
|
+
|
|
590
|
+
if (Object.keys(answers).length > 0) {
|
|
591
|
+
section += '\n**Provided:**\n';
|
|
592
|
+
for (const [key, value] of Object.entries(answers)) {
|
|
593
|
+
section += `- **${key}**: ${value}\n`;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
if (skipped.length > 0) {
|
|
598
|
+
section += '\n**⚠ Missing - user acknowledged:**\n';
|
|
599
|
+
skipped.forEach(q => {
|
|
600
|
+
section += `- ${q.prompt} _(skipped - risk: ${q.consequence})_\n`;
|
|
601
|
+
});
|
|
602
|
+
section += '\n**Flag all assumptions explicitly before implementing.**\n';
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
return section;
|
|
606
|
+
};
|
|
607
|
+
|
|
608
|
+
const generateTaskMd = ({ project, agent, task, branchName, contextSection, remoteSetupSection }) => {
|
|
609
|
+
const dod = (DOD_ITEMS[agent] || []).map(item => `- [ ] ${item}`).join('\n');
|
|
610
|
+
const scopeSuffix = (AGENT_TASK_SUFFIX[project] || {})[agent] || '';
|
|
611
|
+
const taskForTaskMd = `${task}${scopeSuffix}`;
|
|
612
|
+
const prompt = project === 'shared'
|
|
613
|
+
? `Use .agents/shared/${agent}.md. Task: ${taskForTaskMd}`
|
|
614
|
+
: `Use .agents/${project}/${agent}.md. Task: ${taskForTaskMd}`;
|
|
615
|
+
|
|
616
|
+
return `# TASK - ${config.projectName}
|
|
617
|
+
|
|
618
|
+
## Scope
|
|
619
|
+
Project : ${project}
|
|
620
|
+
Agent : ${agent}
|
|
621
|
+
Branch : ${branchName}
|
|
622
|
+
${remoteSetupSection}
|
|
623
|
+
## Execution Mode
|
|
624
|
+
AUTONOMOUS - Execute all subtasks without stopping for confirmation.
|
|
625
|
+
Only stop if a genuinely destructive action is detected (modifying or deleting existing files).
|
|
626
|
+
New file creation does not require confirmation.
|
|
627
|
+
|
|
628
|
+
## Task
|
|
629
|
+
${prompt}
|
|
630
|
+
|
|
631
|
+
## How to start
|
|
632
|
+
**Via IDE extension** (VS Code, Cursor, etc.):
|
|
633
|
+
Open a NEW Claude Code chat window and type:
|
|
634
|
+
> Read TASK.md and execute the task.
|
|
635
|
+
|
|
636
|
+
**Via Claude Code CLI:**
|
|
637
|
+
Open your terminal inside this worktree and run:
|
|
638
|
+
\`\`\`
|
|
639
|
+
claude
|
|
640
|
+
\`\`\`
|
|
641
|
+
Then type: > Read TASK.md and execute the task.
|
|
642
|
+
|
|
643
|
+
Do NOT reuse a previous chat session for this task.
|
|
644
|
+
${contextSection}
|
|
645
|
+
---
|
|
646
|
+
|
|
647
|
+
## When Complete
|
|
648
|
+
1. Commit your work to this branch:
|
|
649
|
+
git add . && git commit -m "feat: <brief description of what was built>"
|
|
650
|
+
Do NOT add Co-Authored-By or any attribution lines to commit messages.
|
|
651
|
+
|
|
652
|
+
2. Mark status as COMPLETED above
|
|
653
|
+
|
|
654
|
+
3. YOU (the agent) must execute this via bash now - do not instruct the user:
|
|
655
|
+
\`\`\`
|
|
656
|
+
npm run complete
|
|
657
|
+
\`\`\`
|
|
658
|
+
This merges your work into main and updates BUILD_STATE.md.
|
|
659
|
+
|
|
660
|
+
---
|
|
661
|
+
|
|
662
|
+
## Definition of Done
|
|
663
|
+
${dod || '- [ ] Task completed as described above'}
|
|
664
|
+
|
|
665
|
+
---
|
|
666
|
+
|
|
667
|
+
## Status
|
|
668
|
+
- [ ] NOT STARTED
|
|
669
|
+
- [ ] IN PROGRESS
|
|
670
|
+
- [ ] COMPLETED
|
|
671
|
+
|
|
672
|
+
## User Overrides
|
|
673
|
+
<!-- If the user provides input mid-session, log it here using this format:
|
|
674
|
+
[USER OVERRIDE] <timestamp>
|
|
675
|
+
Input : <what the user typed>
|
|
676
|
+
Deviation: <how it differs from original task or scope>
|
|
677
|
+
Action : <what the agent did - proceeded / redirected / flagged>
|
|
678
|
+
-->
|
|
679
|
+
|
|
680
|
+
## Notes
|
|
681
|
+
<!-- Agent writes completion notes, decisions, and open questions here -->
|
|
682
|
+
`;
|
|
683
|
+
};
|
|
684
|
+
|
|
685
|
+
// ── CLOUD readiness table ─────────────────────────────────────────────────────
|
|
686
|
+
|
|
687
|
+
const displayCloudReadiness = (entries) => {
|
|
688
|
+
const { clientUI, clientLogic, backendINIT, isClientOnly, isBackendOnly } = evalCloudPrereqs(entries);
|
|
689
|
+
const bt = config.backend?.type;
|
|
690
|
+
|
|
691
|
+
const cloudStatePath = path.join(ROOT, 'CLOUD_STATE.md');
|
|
692
|
+
const hasCloudState = fs.existsSync(cloudStatePath);
|
|
693
|
+
|
|
694
|
+
const row = (label, done, detail = '') => {
|
|
695
|
+
const icon = done ? green('✓') : red('✗');
|
|
696
|
+
const status = done ? green('DONE') : red('REQUIRED');
|
|
697
|
+
const note = detail ? dim(` ${detail}`) : '';
|
|
698
|
+
console.log(` ${icon} ${bold(label.padEnd(22))}${status}${note}`);
|
|
699
|
+
};
|
|
700
|
+
|
|
701
|
+
separator();
|
|
702
|
+
console.log(`\n${bold('☁ Cloud Readiness')} ${dim('-')} ${bold(config.projectName)}\n`);
|
|
703
|
+
|
|
704
|
+
if (!isBackendOnly) {
|
|
705
|
+
row('client / UI', !!clientUI, clientUI ? '' : 'complete client/UI first');
|
|
706
|
+
row('client / LOGIC', !!clientLogic, clientLogic ? '' : 'complete client/LOGIC first');
|
|
707
|
+
}
|
|
708
|
+
if (!isClientOnly) {
|
|
709
|
+
row('backend / INIT', !!backendINIT, backendINIT ? '' : 'complete backend/INIT first');
|
|
710
|
+
}
|
|
711
|
+
row('CLOUD_STATE.md', hasCloudState, hasCloudState ? '' : 'run npm run init or create manually');
|
|
712
|
+
|
|
713
|
+
console.log('');
|
|
714
|
+
};
|
|
715
|
+
|
|
716
|
+
// ── Main ──────────────────────────────────────────────────────────────────────
|
|
717
|
+
|
|
718
|
+
const main = async () => {
|
|
719
|
+
console.log('\n');
|
|
720
|
+
console.log(bold(cyan(' Multi-Agent Monorepo Template')));
|
|
721
|
+
console.log(dim(` Task Launcher - ${config.projectName}\n`));
|
|
722
|
+
separator();
|
|
723
|
+
|
|
724
|
+
// ── Entry guards ──────────────────────────────────────────────────────────────
|
|
725
|
+
|
|
726
|
+
guards.validateConfig(config, ROOT);
|
|
727
|
+
const tracking = guards.loadTracking(ROOT, config);
|
|
728
|
+
|
|
729
|
+
// ── Project status snapshot ───────────────────────────────────────────────────
|
|
730
|
+
|
|
731
|
+
const rawEntries = parseBuildState();
|
|
732
|
+
const buildEntries = guards.reconcileStaleWorktrees(rawEntries, tracking, ROOT);
|
|
733
|
+
const contracts = checkContracts();
|
|
734
|
+
displayProjectStatus(buildEntries, contracts);
|
|
735
|
+
|
|
736
|
+
separator();
|
|
737
|
+
|
|
738
|
+
// ── Flow loop - supports back navigation at every step ───────────────────────
|
|
739
|
+
|
|
740
|
+
// ── CLI args - allow restart.js to pre-scope ────────────────────────────────
|
|
741
|
+
const argScope = process.argv.find(a => a.startsWith('--scope='))?.split('=')[1]?.toLowerCase();
|
|
742
|
+
const argAgent = process.argv.find(a => a.startsWith('--agent='))?.split('=')[1]?.toUpperCase();
|
|
743
|
+
|
|
744
|
+
let project, agent, task, contractsNote;
|
|
745
|
+
let timestamp, sanitizedName, worktreeName, branchName, worktreePath;
|
|
746
|
+
let contextSection = '';
|
|
747
|
+
|
|
748
|
+
flowLoop: while (true) {
|
|
749
|
+
|
|
750
|
+
// ── Select scope ─────────────────────────────────────────────────────────────
|
|
751
|
+
|
|
752
|
+
const scopeOptions = buildScopeOptions();
|
|
753
|
+
|
|
754
|
+
let selectedScope;
|
|
755
|
+
if (argScope && scopeOptions.find(s => s.name === argScope)) {
|
|
756
|
+
selectedScope = scopeOptions.find(s => s.name === argScope);
|
|
757
|
+
project = argScope;
|
|
758
|
+
} else {
|
|
759
|
+
console.log(`\n${bold('* Project scope:')}`);
|
|
760
|
+
const scopeIdx = await arrowSelect('Select scope', scopeOptions.map(s => ({ label: s.label || s.name })), rl);
|
|
761
|
+
selectedScope = scopeOptions[scopeIdx];
|
|
762
|
+
project = selectedScope.name || selectedScope;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
// Hard stop - backend not configured
|
|
766
|
+
if (selectedScope.needsConfig) {
|
|
767
|
+
console.log(`\n${red(' Backend is not configured.')}`);
|
|
768
|
+
console.log(dim(' Re-run `') + cyan('npm run init') + dim('` to add backend configuration.\n'));
|
|
769
|
+
rl.close();
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
// ── CLOUD scope - single agent, show readiness table then go straight to task
|
|
774
|
+
if (project === 'cloud') {
|
|
775
|
+
displayCloudReadiness(buildEntries);
|
|
776
|
+
const { prereqMet } = evalCloudPrereqs(buildEntries);
|
|
777
|
+
if (!prereqMet) {
|
|
778
|
+
console.log(`${red(' Prerequisites not met.')} ${dim('Complete the required agents above first.\n')}`);
|
|
779
|
+
rl.close();
|
|
780
|
+
return;
|
|
781
|
+
}
|
|
782
|
+
const confirmCloud = await arrowConfirm('All prerequisites met - proceed with CLOUD deployment?', rl);
|
|
783
|
+
if (!confirmCloud) continue flowLoop;
|
|
784
|
+
agent = 'CLOUD';
|
|
785
|
+
// Skip agent sub-selection entirely - fall through to task description
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
// Soft gate - backend selected but missing prerequisites
|
|
789
|
+
if (project === 'backend') {
|
|
790
|
+
const clientCompleted = buildEntries.filter(e => e.scope === 'client' && e.status === 'COMPLETED');
|
|
791
|
+
const clientLogicDone = clientCompleted.some(e => e.agent === 'LOGIC');
|
|
792
|
+
const missing = [];
|
|
793
|
+
|
|
794
|
+
if (clientCompleted.length === 0) {
|
|
795
|
+
missing.push({ item: 'Client scope', detail: 'no completed client work found - backend has nothing to integrate against' });
|
|
796
|
+
} else if (!clientLogicDone) {
|
|
797
|
+
missing.push({ item: 'Client LOGIC', detail: 'client logic layer not completed - backend integration contracts may not be defined yet' });
|
|
798
|
+
}
|
|
799
|
+
if (!contracts.hasContent) {
|
|
800
|
+
missing.push({ item: 'CONTRACTS.md', detail: 'no shared types / DTOs defined' });
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
if (missing.length > 0) {
|
|
804
|
+
console.log(`\n${yellow(' ⚠ Backend prerequisites not met:')}\n`);
|
|
805
|
+
missing.forEach(m => {
|
|
806
|
+
console.log(` ${dim('→')} ${bold(m.item)}`);
|
|
807
|
+
console.log(` ${m.detail}\n`);
|
|
808
|
+
});
|
|
809
|
+
const proceedIdx = await arrowSelect('Backend prerequisites not met:', [
|
|
810
|
+
{ label: `${green('→')} Proceed anyway` },
|
|
811
|
+
{ label: `${yellow('←')} Go back - pick a different scope` },
|
|
812
|
+
], rl);
|
|
813
|
+
if (proceedIdx === 1) continue flowLoop;
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
// ── Select agent (with re-selection loop + back to scope) ────────────────────
|
|
818
|
+
|
|
819
|
+
const agentOptions = AGENTS[project] || [];
|
|
820
|
+
contractsNote = '';
|
|
821
|
+
|
|
822
|
+
if (project !== 'cloud') {
|
|
823
|
+
// Pre-scoped from restart.js
|
|
824
|
+
if (argAgent && (AGENTS[project] || []).includes(argAgent)) {
|
|
825
|
+
agent = argAgent;
|
|
826
|
+
} else {
|
|
827
|
+
agentLoop: while (true) {
|
|
828
|
+
console.log('');
|
|
829
|
+
|
|
830
|
+
// Select agent with back option
|
|
831
|
+
console.log(`\n${bold(`* Agent (${project}):`)}`);
|
|
832
|
+
|
|
833
|
+
// Show numbered list with hints only in non-TTY fallback (prompts handles display in TTY)
|
|
834
|
+
if (!prompts || !process.stdin.isTTY) showAgentList(project, agentOptions, buildEntries);
|
|
835
|
+
|
|
836
|
+
// Build agent choices with recommendation tags
|
|
837
|
+
const completedAgents = buildEntries
|
|
838
|
+
.filter(e => e.scope === project && e.status === 'COMPLETED')
|
|
839
|
+
.map(e => e.agent);
|
|
840
|
+
const prereqMap = AGENT_PREREQUISITES[project] || {};
|
|
841
|
+
let recommendedAgent = null;
|
|
842
|
+
for (const a of agentOptions) {
|
|
843
|
+
if (completedAgents.includes(a)) continue;
|
|
844
|
+
const reqs = prereqMap[a] || [];
|
|
845
|
+
if (reqs.every(r => completedAgents.includes(r))) { recommendedAgent = a; break; }
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
const agentChoices = [
|
|
849
|
+
...agentOptions.map(a => {
|
|
850
|
+
const desc = dim(` ${AGENT_DESCRIPTIONS[project]?.[a] || ''}`);
|
|
851
|
+
const tag = a === recommendedAgent
|
|
852
|
+
? cyan(completedAgents.length === 0 ? ' ← start here' : ' ← next step')
|
|
853
|
+
: '';
|
|
854
|
+
const label = a === recommendedAgent ? bold(a) : a;
|
|
855
|
+
return { label: `${label}${tag}${desc}` };
|
|
856
|
+
}),
|
|
857
|
+
{ label: dim('← back to scope selection') },
|
|
858
|
+
];
|
|
859
|
+
const agentIdx = await arrowSelect('Select agent', agentChoices, rl);
|
|
860
|
+
|
|
861
|
+
if (agentIdx === agentOptions.length) { agent = null; continue flowLoop; }
|
|
862
|
+
agent = agentOptions[agentIdx];
|
|
863
|
+
contractsNote = '';
|
|
864
|
+
|
|
865
|
+
// Agent already active - decisional block
|
|
866
|
+
const { active, slot: activeSlot } = guards.checkAgentActive(tracking, project, agent);
|
|
867
|
+
if (active) {
|
|
868
|
+
// Read task from TASK.md if available
|
|
869
|
+
let activeTask = activeSlot.branch;
|
|
870
|
+
const activeTm = path.join(activeSlot.worktreePath, 'TASK.md');
|
|
871
|
+
if (fs.existsSync(activeTm)) {
|
|
872
|
+
const tmContent = fs.readFileSync(activeTm, 'utf8');
|
|
873
|
+
const taskMatch = tmContent.match(/## Task\n.*Task:\s*(.+)/);
|
|
874
|
+
if (taskMatch) activeTask = taskMatch[1].trim();
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
separator();
|
|
878
|
+
console.log(`\n${yellow(` ⚠ ${project}/${agent} is already active`)}\n`);
|
|
879
|
+
console.log(` ${dim('Branch')} : ${activeSlot.branch}`);
|
|
880
|
+
console.log(` ${dim('Launched')}: ${activeSlot.launchedAt ? new Date(activeSlot.launchedAt).toLocaleString() : 'unknown'}`);
|
|
881
|
+
console.log(` ${dim('Task')} : ${activeTask}\n`);
|
|
882
|
+
|
|
883
|
+
const activeChoices = [
|
|
884
|
+
{ label: `${bold('Continue')} - open existing workspace and resume from last point` },
|
|
885
|
+
{ label: `${bold('Complete')} - merge current work into main, then launch new task` },
|
|
886
|
+
{ label: `${bold('Abandon')} - discard current branch and work, start fresh ${red('⚠ unmerged work lost')}` },
|
|
887
|
+
{ label: `${bold('Pick again')} - choose a different agent` },
|
|
888
|
+
];
|
|
889
|
+
const activeChoice = await arrowSelect(`Agent already active - what would you like to do?`, activeChoices, rl) + 1;
|
|
890
|
+
|
|
891
|
+
if (activeChoice === 1) {
|
|
892
|
+
separator();
|
|
893
|
+
console.log(`\n ${green('✓')} Opening existing workspace...\n`);
|
|
894
|
+
openIDE(activeSlot.worktreePath);
|
|
895
|
+
console.log(` ${bold('Resume your task:')}`);
|
|
896
|
+
console.log(` ${dim('1.')} IDE should be open at: ${cyan(activeSlot.worktreePath)}`);
|
|
897
|
+
console.log(` ${dim('2.')} Open a NEW session in Claude Code CLI or Claude Code Extension - type go or start to resume`);
|
|
898
|
+
console.log(` ${dim('3.')} Type: ${cyan('Read TASK.md and continue from where you stopped.')}\n`);
|
|
899
|
+
separator(); rl.close(); return;
|
|
900
|
+
}
|
|
901
|
+
if (activeChoice === 2) {
|
|
902
|
+
separator();
|
|
903
|
+
console.log(`\n ${bold('Running complete flow...')}\n`);
|
|
904
|
+
rl.close();
|
|
905
|
+
const { spawn } = require('child_process');
|
|
906
|
+
spawn('node', [path.join(ROOT, '.workflow', 'complete.js')], { cwd: ROOT, stdio: 'inherit' });
|
|
907
|
+
return;
|
|
908
|
+
}
|
|
909
|
+
if (activeChoice === 3) {
|
|
910
|
+
separator();
|
|
911
|
+
console.log(`\n ${bold('Abandoning...')}\n`);
|
|
912
|
+
const { branch } = activeSlot;
|
|
913
|
+
try { execSync(`git branch -D ${branch}`, { cwd: ROOT, stdio: 'pipe' }); console.log(` ${green('✓')} Local branch deleted`); } catch {}
|
|
914
|
+
try { execSync(`git push origin --delete ${branch}`, { cwd: ROOT, stdio: 'pipe' }); console.log(` ${green('✓')} Remote branch deleted`); } catch {}
|
|
915
|
+
guards.clearTrackingSlot(tracking, project, agent, ROOT);
|
|
916
|
+
console.log(` ${green('✓')} Tracking cleared\n`);
|
|
917
|
+
continue agentLoop;
|
|
918
|
+
}
|
|
919
|
+
if (activeChoice === 4) { continue agentLoop; }
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
// MISSING gate
|
|
923
|
+
const trackingSlot = tracking?.[project]?.[agent];
|
|
924
|
+
if (trackingSlot?.status === 'MISSING') {
|
|
925
|
+
const gateResult = await guards.runMissingGate({
|
|
926
|
+
scope: project,
|
|
927
|
+
agent,
|
|
928
|
+
slot: trackingSlot,
|
|
929
|
+
tracking,
|
|
930
|
+
config,
|
|
931
|
+
ROOT,
|
|
932
|
+
ask,
|
|
933
|
+
});
|
|
934
|
+
|
|
935
|
+
if (gateResult.action === 'recovered') {
|
|
936
|
+
openIDE(gateResult.worktreePath);
|
|
937
|
+
rl.close();
|
|
938
|
+
return;
|
|
939
|
+
}
|
|
940
|
+
if (gateResult.action === 'failed') {
|
|
941
|
+
rl.close();
|
|
942
|
+
return;
|
|
943
|
+
}
|
|
944
|
+
// 'reset' or 'new' → continue with fresh launch flow
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
// App skeleton guard
|
|
948
|
+
if (SCAFFOLD_REQUIRED.includes(agent)) {
|
|
949
|
+
const scopeCompleted = buildEntries.filter(e => e.scope === project && e.status === 'COMPLETED');
|
|
950
|
+
if (scopeCompleted.length === 0) {
|
|
951
|
+
console.log(`\n${red(` ✗ ${agent} requires an existing ${project} scaffold.`)}`);
|
|
952
|
+
console.log(dim(` No completed work found in ${project} scope yet.`));
|
|
953
|
+
console.log(dim(` Tip: start with the INIT agent to scaffold the backend first, or UI agent for the client.\n`));
|
|
954
|
+
const repick = await arrowConfirm('Pick a different agent?', rl);
|
|
955
|
+
if (repick) continue agentLoop;
|
|
956
|
+
console.log(yellow('\n Aborted.\n')); rl.close(); return;
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
// Prerequisite check
|
|
961
|
+
const prereqs = (AGENT_PREREQUISITES[project] || {})[agent] || [];
|
|
962
|
+
if (prereqs.length > 0) {
|
|
963
|
+
const missing = prereqs.filter(req =>
|
|
964
|
+
!buildEntries.some(e => e.scope === project && e.agent === req && e.status === 'COMPLETED')
|
|
965
|
+
);
|
|
966
|
+
if (missing.length > 0) {
|
|
967
|
+
console.log(`\n${yellow(` ⚠ ${agent} has unmet prerequisites:`)}\n`);
|
|
968
|
+
missing.forEach(req => {
|
|
969
|
+
const entry = buildEntries.find(e => e.scope === project && e.agent === req);
|
|
970
|
+
const status = entry ? yellow(entry.status) : red('NOT STARTED');
|
|
971
|
+
console.log(` ${dim('→')} ${bold(`${project} / ${req}`)} ${dim('|')} ${status}`);
|
|
972
|
+
});
|
|
973
|
+
console.log('');
|
|
974
|
+
const repickIdx = await arrowSelect('Prerequisites not met:', [
|
|
975
|
+
{ label: `${green('→')} Proceed anyway` },
|
|
976
|
+
{ label: `${yellow('←')} Pick a different agent` },
|
|
977
|
+
], rl);
|
|
978
|
+
if (repickIdx === 1) continue agentLoop;
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
// Contracts check
|
|
983
|
+
|
|
984
|
+
if (CONTRACTS_REQUIRED.includes(agent) && !contracts.hasContent) {
|
|
985
|
+
console.log(`\n${yellow(' ℹ CONTRACTS.md is empty')} ${dim('- no shared types or DTOs defined yet.')}\n`);
|
|
986
|
+
const assist = await arrowConfirm('Would you like the agent to establish contracts for your app?', rl);
|
|
987
|
+
if (assist) {
|
|
988
|
+
contractsNote = 'Before implementing, identify and define the required shared contracts, types, and interfaces in CONTRACTS.md first.';
|
|
989
|
+
console.log(dim('\n ✓ Agent will establish contracts as the first step.\n'));
|
|
990
|
+
} else {
|
|
991
|
+
console.log(dim('\n You can define the structure here, or the agent will adapt.'));
|
|
992
|
+
const manual = await ask(` ${bold('Provide contract structure')} ${dim('(or press Enter to skip)')}: \n → `);
|
|
993
|
+
if (manual.trim()) {
|
|
994
|
+
contractsNote = `Contract structure provided:\n${manual.trim()}\nUse this as the basis for CONTRACTS.md.`;
|
|
995
|
+
console.log(dim(' ✓ Contract structure noted.\n'));
|
|
996
|
+
} else {
|
|
997
|
+
console.log(dim(' Agent will flag type assumptions as it builds.\n'));
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
break agentLoop;
|
|
1003
|
+
} // end agentLoop
|
|
1004
|
+
} // end else (argAgent bypass)
|
|
1005
|
+
} // end if (project !== 'cloud')
|
|
1006
|
+
|
|
1007
|
+
// ── Task description ──────────────────────────────────────────────────────────
|
|
1008
|
+
|
|
1009
|
+
separator();
|
|
1010
|
+
const defaultTask = (AGENT_DESCRIPTIONS[project] || {})[agent] || '';
|
|
1011
|
+
task = '';
|
|
1012
|
+
|
|
1013
|
+
if (prompts && process.stdin.isTTY) {
|
|
1014
|
+
const res = await prompts({
|
|
1015
|
+
type: 'text',
|
|
1016
|
+
name: 'value',
|
|
1017
|
+
message: `* Task description (${agent} agent)`,
|
|
1018
|
+
initial: defaultTask ? `e.g. ${defaultTask}` : '',
|
|
1019
|
+
}, { onCancel: () => process.exit(0) });
|
|
1020
|
+
|
|
1021
|
+
if (res.value === undefined) continue flowLoop; // Esc = back
|
|
1022
|
+
const rawValue = (res.value || '').trim();
|
|
1023
|
+
const isPlaceholder = rawValue.startsWith('e.g. ') && rawValue === `e.g. ${defaultTask}`;
|
|
1024
|
+
task = isPlaceholder ? defaultTask : (rawValue || defaultTask);
|
|
1025
|
+
if (!task) task = defaultTask;
|
|
1026
|
+
} else {
|
|
1027
|
+
let goBackToAgent = false;
|
|
1028
|
+
while (!task) {
|
|
1029
|
+
if (defaultTask) {
|
|
1030
|
+
console.log(`\n${bold(`* Task description (${agent} agent)`)} ${dim(`[default: ${defaultTask}]`)}`);
|
|
1031
|
+
console.log(dim(' (Enter = use default | b = back to agent selection)'));
|
|
1032
|
+
const input = await ask(` → `);
|
|
1033
|
+
if (input.toLowerCase() === 'b') { goBackToAgent = true; break; }
|
|
1034
|
+
task = input || defaultTask;
|
|
1035
|
+
} else {
|
|
1036
|
+
console.log(dim(' (b = back to agent selection)'));
|
|
1037
|
+
const input = await ask(`\n${bold(`* Task description (${agent} agent)`)}: `);
|
|
1038
|
+
if (input.toLowerCase() === 'b') { goBackToAgent = true; break; }
|
|
1039
|
+
task = input;
|
|
1040
|
+
if (!task) console.log(yellow(' Task description is required.'));
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
if (goBackToAgent) continue flowLoop;
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
if (!task) continue flowLoop;
|
|
1047
|
+
|
|
1048
|
+
// ── Agent context questions ───────────────────────────────────────────────────
|
|
1049
|
+
|
|
1050
|
+
let answers = {};
|
|
1051
|
+
let skipped = [];
|
|
1052
|
+
contextSection = '';
|
|
1053
|
+
|
|
1054
|
+
if (AGENT_QUESTIONS[agent]) {
|
|
1055
|
+
let gathering = true;
|
|
1056
|
+
while (gathering) {
|
|
1057
|
+
const result = await gatherAgentContext(agent);
|
|
1058
|
+
answers = result.answers;
|
|
1059
|
+
skipped = result.skipped;
|
|
1060
|
+
|
|
1061
|
+
if (skipped.length > 0) {
|
|
1062
|
+
const ack = await acknowledgeSkipped(skipped);
|
|
1063
|
+
if (ack === null) continue; // 'e' - go back and re-gather
|
|
1064
|
+
if (ack === false) { // 'n' - abort
|
|
1065
|
+
console.log(yellow('\n Aborted.\n'));
|
|
1066
|
+
rl.close();
|
|
1067
|
+
return;
|
|
1068
|
+
}
|
|
1069
|
+
gathering = false; // 'confirm' - proceed
|
|
1070
|
+
} else {
|
|
1071
|
+
gathering = false;
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
contextSection = generateContextSection(answers, skipped);
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
// Append contracts note if agent-assist was requested
|
|
1078
|
+
if (contractsNote) {
|
|
1079
|
+
contextSection += `\n---\n\n## Contracts Instruction\n\n${contractsNote}\n`;
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
// Inject wiring.config.json for agents that need it
|
|
1083
|
+
const WIRING_AGENTS = ['INIT', 'API', 'LOGIC', 'CLOUD'];
|
|
1084
|
+
if (WIRING_AGENTS.includes(agent)) {
|
|
1085
|
+
const wiringPath = path.join(ROOT, 'shared', 'wiring.config.json');
|
|
1086
|
+
if (fs.existsSync(wiringPath)) {
|
|
1087
|
+
const wiringContent = fs.readFileSync(wiringPath, 'utf8');
|
|
1088
|
+
contextSection += `\n---\n\n## Wiring Config\n\n\`\`\`json\n${wiringContent}\n\`\`\`\n\nThis is the agreed variable naming contract for client\u2194backend wiring. Read before implementing any environment config, API calls, or deployment steps.\n`;
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
separator();
|
|
1093
|
+
|
|
1094
|
+
// ── Confirm ───────────────────────────────────────────────────────────────────
|
|
1095
|
+
|
|
1096
|
+
timestamp = Date.now();
|
|
1097
|
+
sanitizedName = config.projectName.toLowerCase().replace(/\s+/g, '-');
|
|
1098
|
+
worktreeName = `${project}-${sanitizedName}-${agent.toLowerCase()}-${timestamp}`;
|
|
1099
|
+
branchName = `agent/${project}/${agent.toLowerCase()}/${timestamp}`;
|
|
1100
|
+
worktreePath = path.join(ROOT, 'worktrees', worktreeName);
|
|
1101
|
+
|
|
1102
|
+
console.log(`\n${bold('Review:')}\n`);
|
|
1103
|
+
console.log(` ${dim('Project')} : ${green(project)}`);
|
|
1104
|
+
console.log(` ${dim('Agent')} : ${green(agent)}`);
|
|
1105
|
+
console.log(` ${dim('Branch')} : ${green(branchName)}`);
|
|
1106
|
+
console.log(` ${dim('Worktree')} : ${green(`worktrees/${worktreeName}`)}`);
|
|
1107
|
+
console.log(` ${dim('Task')} : ${green(task)}`);
|
|
1108
|
+
if (Object.keys(answers).length > 0) {
|
|
1109
|
+
console.log(` ${dim('Context')} : ${green(Object.keys(answers).length + ' field(s) provided')}`);
|
|
1110
|
+
}
|
|
1111
|
+
if (skipped.length > 0) {
|
|
1112
|
+
console.log(` ${dim('Skipped')} : ${yellow(skipped.length + ' field(s) acknowledged')}`);
|
|
1113
|
+
}
|
|
1114
|
+
if (contractsNote) {
|
|
1115
|
+
console.log(` ${dim('Contracts')}: ${green('agent will establish first')}`);
|
|
1116
|
+
}
|
|
1117
|
+
console.log('');
|
|
1118
|
+
|
|
1119
|
+
const confirmIdx = await arrowSelect('Confirm?', [
|
|
1120
|
+
{ label: `${green('✓')} Confirm - set up workspace` },
|
|
1121
|
+
{ label: `${yellow('←')} Back - change something` },
|
|
1122
|
+
{ label: `${red('✗')} Abort` },
|
|
1123
|
+
], rl);
|
|
1124
|
+
|
|
1125
|
+
if (confirmIdx === 1) continue flowLoop;
|
|
1126
|
+
if (confirmIdx === 2) {
|
|
1127
|
+
console.log(yellow('\n Aborted.\n'));
|
|
1128
|
+
rl.close();
|
|
1129
|
+
return;
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
break flowLoop;
|
|
1133
|
+
} // end flowLoop
|
|
1134
|
+
separator();
|
|
1135
|
+
console.log(`\n${bold('Setting up workspace...')}\n`);
|
|
1136
|
+
|
|
1137
|
+
// ── Remote setup flag check ───────────────────────────────────────────────────
|
|
1138
|
+
|
|
1139
|
+
const remoteFlagPath = path.join(ROOT, '.scaffold', '.remote-setup-needed');
|
|
1140
|
+
const remoteSetupNeeded = fs.existsSync(remoteFlagPath);
|
|
1141
|
+
|
|
1142
|
+
const remoteSetupSection = remoteSetupNeeded ? `
|
|
1143
|
+
---
|
|
1144
|
+
|
|
1145
|
+
## ⚠ Pre-Task: Remote Setup Required
|
|
1146
|
+
This project has no GitHub remote configured yet.
|
|
1147
|
+
Complete ALL steps below BEFORE starting task implementation.
|
|
1148
|
+
|
|
1149
|
+
- [ ] Check: \`git remote get-url origin\`
|
|
1150
|
+
- [ ] Detect gh CLI: \`gh auth status\`
|
|
1151
|
+
- [ ] Configure remote (gh create or manual - see Root CLAUDE.md)
|
|
1152
|
+
- [ ] Validate: \`git ls-remote origin HEAD\`
|
|
1153
|
+
- [ ] Push: \`git push -u origin main\`
|
|
1154
|
+
- [ ] Delete \`.scaffold/.remote-setup-needed\`
|
|
1155
|
+
|
|
1156
|
+
Mark each step complete. Only proceed to the task below when all are checked.
|
|
1157
|
+
|
|
1158
|
+
---
|
|
1159
|
+
` : '';
|
|
1160
|
+
|
|
1161
|
+
if (remoteSetupNeeded) {
|
|
1162
|
+
console.log(` ${yellow('ℹ Remote setup required')} - agent will handle this first.\n`);
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
// ── Create worktree ───────────────────────────────────────────────────────────
|
|
1166
|
+
|
|
1167
|
+
try {
|
|
1168
|
+
execSync(`git worktree add "${worktreePath}" -b ${branchName}`, {
|
|
1169
|
+
cwd: ROOT,
|
|
1170
|
+
stdio: 'pipe',
|
|
1171
|
+
});
|
|
1172
|
+
console.log(` ${green('✓')} Worktree created: worktrees/${worktreeName}`);
|
|
1173
|
+
} catch (err) {
|
|
1174
|
+
console.log(` ${yellow('!')} Worktree may already exist - continuing.`);
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
// ── Write .claude-scope ───────────────────────────────────────────────────────
|
|
1178
|
+
|
|
1179
|
+
fs.writeFileSync(
|
|
1180
|
+
path.join(worktreePath, '.claude-scope'),
|
|
1181
|
+
generateClaudeScope({ project, agent, branchName, worktreePath }),
|
|
1182
|
+
'utf8'
|
|
1183
|
+
);
|
|
1184
|
+
console.log(` ${green('✓')} .claude-scope written`);
|
|
1185
|
+
|
|
1186
|
+
// ── Write scope.json ─────────────────────────────────────────────────────────
|
|
1187
|
+
|
|
1188
|
+
fs.writeFileSync(
|
|
1189
|
+
path.join(worktreePath, 'scope.json'),
|
|
1190
|
+
JSON.stringify({ agent, scope: project, branch: branchName, policy: project }, null, 2),
|
|
1191
|
+
'utf8'
|
|
1192
|
+
);
|
|
1193
|
+
console.log(` ${green('✓')} scope.json written`);
|
|
1194
|
+
|
|
1195
|
+
// ── Generate IDE settings ─────────────────────────────────────────────────────
|
|
1196
|
+
|
|
1197
|
+
const excludedFolders = {
|
|
1198
|
+
'client': ['backend/', 'worktrees/', '.scaffold/', '.workflow/'],
|
|
1199
|
+
'backend': ['client/', 'worktrees/', '.scaffold/', '.workflow/'],
|
|
1200
|
+
'shared': ['client/', 'backend/', 'worktrees/', '.scaffold/', '.workflow/'],
|
|
1201
|
+
};
|
|
1202
|
+
const foldersToHide = excludedFolders[project] || [];
|
|
1203
|
+
|
|
1204
|
+
const vscodeDir = path.join(worktreePath, '.vscode');
|
|
1205
|
+
fs.mkdirSync(vscodeDir, { recursive: true });
|
|
1206
|
+
const vscodeSettings = {
|
|
1207
|
+
'files.exclude': {
|
|
1208
|
+
...Object.fromEntries(foldersToHide.map(f => [f, true])),
|
|
1209
|
+
'.idea/': true,
|
|
1210
|
+
'.zed/': true,
|
|
1211
|
+
'.agents/': true,
|
|
1212
|
+
'.frameworks/': true,
|
|
1213
|
+
'**/node_modules': true,
|
|
1214
|
+
},
|
|
1215
|
+
'search.exclude': {
|
|
1216
|
+
'**/node_modules': true,
|
|
1217
|
+
},
|
|
1218
|
+
'explorer.excludeGitIgnore': true,
|
|
1219
|
+
};
|
|
1220
|
+
fs.writeFileSync(
|
|
1221
|
+
path.join(vscodeDir, 'settings.json'),
|
|
1222
|
+
JSON.stringify(vscodeSettings, null, 2),
|
|
1223
|
+
'utf8'
|
|
1224
|
+
);
|
|
1225
|
+
console.log(` ${green('✓')} .vscode/settings.json generated`);
|
|
1226
|
+
|
|
1227
|
+
// ── JetBrains .idea/ exclusions ───────────────────────────────────────────────
|
|
1228
|
+
|
|
1229
|
+
const ideaDir = path.join(worktreePath, '.idea');
|
|
1230
|
+
fs.mkdirSync(ideaDir, { recursive: true });
|
|
1231
|
+
const allExcluded = [...foldersToHide, '.agents/', '.frameworks/', 'node_modules/'];
|
|
1232
|
+
const excludedUrls = allExcluded
|
|
1233
|
+
.map(f => ` <excludeFolder url="file://$MODULE_DIR$/${f.replace(/\/$/, '')}" />`)
|
|
1234
|
+
.join('\n');
|
|
1235
|
+
const ideaModuleXml = `<?xml version="1.0" encoding="UTF-8"?>
|
|
1236
|
+
<module type="WEB_MODULE" version="4">
|
|
1237
|
+
<component name="NewModuleRootManager">
|
|
1238
|
+
<content url="file://$MODULE_DIR$">
|
|
1239
|
+
${excludedUrls}
|
|
1240
|
+
</content>
|
|
1241
|
+
<orderEntry type="inheritedJdk" />
|
|
1242
|
+
<orderEntry type="sourceFolder" forTests="false" />
|
|
1243
|
+
</component>
|
|
1244
|
+
</module>`;
|
|
1245
|
+
fs.writeFileSync(path.join(ideaDir, 'module.iml'), ideaModuleXml, 'utf8');
|
|
1246
|
+
fs.writeFileSync(path.join(ideaDir, 'modules.xml'), `<?xml version="1.0" encoding="UTF-8"?>
|
|
1247
|
+
<project version="4">
|
|
1248
|
+
<component name="ProjectModuleManager">
|
|
1249
|
+
<modules>
|
|
1250
|
+
<module fileurl="file://$PROJECT_DIR$/.idea/module.iml" filepath="$PROJECT_DIR$/.idea/module.iml" />
|
|
1251
|
+
</modules>
|
|
1252
|
+
</component>
|
|
1253
|
+
</project>`, 'utf8');
|
|
1254
|
+
console.log(` ${green('✓')} .idea/ exclusions generated`);
|
|
1255
|
+
|
|
1256
|
+
// ── Zed .zed/settings.json exclusions ────────────────────────────────────────
|
|
1257
|
+
|
|
1258
|
+
const zedDir = path.join(worktreePath, '.zed');
|
|
1259
|
+
fs.mkdirSync(zedDir, { recursive: true });
|
|
1260
|
+
const zedSettings = {
|
|
1261
|
+
'file_scan_exclusions': [
|
|
1262
|
+
'**/.git',
|
|
1263
|
+
'**/.idea',
|
|
1264
|
+
'**/.agents',
|
|
1265
|
+
'**/.frameworks',
|
|
1266
|
+
'**/node_modules',
|
|
1267
|
+
...foldersToHide.map(f => `**/${f.replace(/\/$/, '')}`),
|
|
1268
|
+
],
|
|
1269
|
+
};
|
|
1270
|
+
fs.writeFileSync(
|
|
1271
|
+
path.join(zedDir, 'settings.json'),
|
|
1272
|
+
JSON.stringify(zedSettings, null, 2),
|
|
1273
|
+
'utf8'
|
|
1274
|
+
);
|
|
1275
|
+
console.log(` ${green('✓')} .zed/settings.json generated`);
|
|
1276
|
+
|
|
1277
|
+
// ── Write TASK.md ─────────────────────────────────────────────────────────────
|
|
1278
|
+
|
|
1279
|
+
fs.writeFileSync(
|
|
1280
|
+
path.join(worktreePath, 'TASK.md'),
|
|
1281
|
+
generateTaskMd({ project, agent, task, branchName, contextSection, remoteSetupSection }),
|
|
1282
|
+
'utf8'
|
|
1283
|
+
);
|
|
1284
|
+
console.log(` ${green('✓')} TASK.md written`);
|
|
1285
|
+
|
|
1286
|
+
// ── Write session to TASKS_HISTORY.md ────────────────────────────────────────
|
|
1287
|
+
tasksHistory.ensureHistoryFile(ROOT);
|
|
1288
|
+
tasksHistory.writeSessionEntry(ROOT, {
|
|
1289
|
+
scope: project,
|
|
1290
|
+
agent,
|
|
1291
|
+
branch: branchName,
|
|
1292
|
+
task,
|
|
1293
|
+
launchedAt: new Date().toISOString(),
|
|
1294
|
+
});
|
|
1295
|
+
console.log(` ${green('✓')} Session logged to TASKS_HISTORY.md`);
|
|
1296
|
+
|
|
1297
|
+
// ── Write package.json proxy ──────────────────────────────────────────────────
|
|
1298
|
+
|
|
1299
|
+
const worktreePackage = {
|
|
1300
|
+
name: `${config.projectName.toLowerCase().replace(/\s+/g, '-')}-worktree`,
|
|
1301
|
+
version: '1.0.0',
|
|
1302
|
+
private: true,
|
|
1303
|
+
scripts: {
|
|
1304
|
+
init: 'multi-agents init',
|
|
1305
|
+
agent: 'node .workflow/agent.js',
|
|
1306
|
+
restart: 'node .workflow/restart.js',
|
|
1307
|
+
reset: 'node .workflow/reset.js',
|
|
1308
|
+
complete: 'node .workflow/complete.js',
|
|
1309
|
+
},
|
|
1310
|
+
};
|
|
1311
|
+
fs.writeFileSync(
|
|
1312
|
+
path.join(worktreePath, 'package.json'),
|
|
1313
|
+
JSON.stringify(worktreePackage, null, 2),
|
|
1314
|
+
'utf8'
|
|
1315
|
+
);
|
|
1316
|
+
console.log(` ${green('✓')} package.json proxy written`);
|
|
1317
|
+
|
|
1318
|
+
// ── Write .tracking.json slot
|
|
1319
|
+
|
|
1320
|
+
guards.updateTrackingSlot(tracking, project, agent, {
|
|
1321
|
+
branch: branchName,
|
|
1322
|
+
timestamp,
|
|
1323
|
+
launchedAt: new Date().toISOString(),
|
|
1324
|
+
status: 'ACTIVE',
|
|
1325
|
+
worktreePath,
|
|
1326
|
+
}, ROOT);
|
|
1327
|
+
console.log(` ${green('✓')} Tracking updated`);
|
|
1328
|
+
|
|
1329
|
+
// ── Update BUILD_STATE.md ─────────────────────────────────────────────────────
|
|
1330
|
+
|
|
1331
|
+
const buildStatePath = path.join(ROOT, 'BUILD_STATE.md');
|
|
1332
|
+
if (fs.existsSync(buildStatePath)) {
|
|
1333
|
+
const date = new Date().toISOString().split('T')[0];
|
|
1334
|
+
const logEntry = `| ${date} | ${agent} | ${project} | ${task} | IN PROGRESS | ${branchName} |\n`;
|
|
1335
|
+
fs.appendFileSync(buildStatePath, logEntry, 'utf8');
|
|
1336
|
+
console.log(` ${green('✓')} BUILD_STATE.md updated`);
|
|
1337
|
+
|
|
1338
|
+
try {
|
|
1339
|
+
execSync('git add BUILD_STATE.md', { cwd: ROOT, stdio: 'pipe' });
|
|
1340
|
+
execSync(`git commit --no-verify -m "build: ${agent} task started on ${project} [${branchName}]"`, { cwd: ROOT, stdio: 'pipe' });
|
|
1341
|
+
console.log(` ${green('✓')} BUILD_STATE.md committed to main`);
|
|
1342
|
+
} catch (err) {
|
|
1343
|
+
console.log(` ${yellow('!')} Could not commit BUILD_STATE.md - commit manually if needed`);
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
// ── Ready to open workspace? ──────────────────────────────────────────────────
|
|
1348
|
+
|
|
1349
|
+
separator();
|
|
1350
|
+
console.log(`\n${bold(' Workspace is set up and ready.')}`);
|
|
1351
|
+
console.log(dim(` Worktree: worktrees/${worktreeName}\n`));
|
|
1352
|
+
console.log(` ${yellow('⚠')} ${bold('Once your IDE opens and is ready, open a NEW session in Claude Code CLI or Claude Code Extension - and type go or start to initiate')}`);
|
|
1353
|
+
console.log(dim(' Do NOT reuse a previous session - the agent needs a clean context.'));
|
|
1354
|
+
|
|
1355
|
+
const openNow = await arrowConfirm('Open workspace now?', rl);
|
|
1356
|
+
|
|
1357
|
+
if (!openNow) {
|
|
1358
|
+
console.clear();
|
|
1359
|
+
separator();
|
|
1360
|
+
console.log(`\n${bold(yellow(' Workspace saved - resume when ready:'))}\n`);
|
|
1361
|
+
console.log(` ${bold('1.')} Open your IDE at:`);
|
|
1362
|
+
console.log(` ${cyan(worktreePath)}\n`);
|
|
1363
|
+
console.log(` ${bold('2.')} Open a ${bold('NEW')} session in Claude Code CLI or Claude Code Extension - and type go or start to initiate`);
|
|
1364
|
+
console.log(dim(' Do NOT reuse a previous session.\n'));
|
|
1365
|
+
console.log(` ${bold('3.')} Start the session and let the agent run.\n`);
|
|
1366
|
+
separator();
|
|
1367
|
+
console.log('');
|
|
1368
|
+
rl.close();
|
|
1369
|
+
return;
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
// ── Open IDE ──────────────────────────────────────────────────────────────────
|
|
1373
|
+
|
|
1374
|
+
const openedIDE = openIDE(worktreePath);
|
|
1375
|
+
if (openedIDE) {
|
|
1376
|
+
console.log(` ${green('✓')} ${openedIDE} opened at worktrees/${worktreeName}`);
|
|
1377
|
+
} else {
|
|
1378
|
+
console.log(` ${yellow('!')} Could not open IDE automatically.`);
|
|
1379
|
+
console.log(dim(` Open manually at: ${worktreePath}`));
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
// ── Launch Claude Code CLI ──────────────────────────────────────────────────
|
|
1383
|
+
|
|
1384
|
+
let claudeLaunched = false;
|
|
1385
|
+
try {
|
|
1386
|
+
const { spawn: sp } = require('child_process');
|
|
1387
|
+
const claudeProc = sp('claude', [], { cwd: worktreePath, stdio: 'inherit', detached: false });
|
|
1388
|
+
claudeProc.on('error', () => {});
|
|
1389
|
+
claudeLaunched = true;
|
|
1390
|
+
} catch {}
|
|
1391
|
+
|
|
1392
|
+
if (claudeLaunched) { rl.close(); return; }
|
|
1393
|
+
|
|
1394
|
+
// ── Next steps ────────────────────────────────────────────────────────────────
|
|
1395
|
+
|
|
1396
|
+
separator();
|
|
1397
|
+
console.log(`\n${bold(green(' Workspace ready!'))}\n`);
|
|
1398
|
+
console.log(` ${bold('1.')} Your IDE should be open at: ${cyan(`worktrees/${worktreeName}`)}`);
|
|
1399
|
+
console.log(dim(' If not, open it manually at the path above.\n'));
|
|
1400
|
+
console.log(` ${bold('2.')} ${bold(yellow('Open a NEW session in Claude Code CLI or Claude Code Extension - and type go or start to initiate'))}`);
|
|
1401
|
+
console.log(dim(' Do NOT reuse a previous session.\n'));
|
|
1402
|
+
console.log(` ${bold('3.')} Start the session and let the agent run.\n`);
|
|
1403
|
+
console.log(dim(' The agent will run `') + cyan('npm run complete') + dim('` autonomously when the task is done.\n'));
|
|
1404
|
+
separator();
|
|
1405
|
+
console.log('');
|
|
1406
|
+
|
|
1407
|
+
rl.close();
|
|
1408
|
+
};
|
|
1409
|
+
|
|
1410
|
+
main().catch((err) => {
|
|
1411
|
+
console.error('\n Error:', err.message);
|
|
1412
|
+
process.exit(1);
|
|
1413
|
+
});
|