multi-agents-cli 1.0.52 → 1.0.53
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/core/templates/.agents/backend/API.md +259 -0
- package/core/templates/.agents/backend/AUTH.md +246 -0
- package/core/templates/.agents/backend/DB.md +257 -0
- package/core/templates/.agents/backend/EVENTS.md +253 -0
- package/core/templates/.agents/backend/INIT.md +239 -0
- package/core/templates/.agents/backend/JOBS.md +256 -0
- package/core/templates/.agents/backend/LOGIC.md +291 -0
- package/core/templates/.agents/backend/TESTING.md +266 -0
- package/core/templates/.agents/client/ACCESSIBILITY.md +266 -0
- package/core/templates/.agents/client/FORMS.md +234 -0
- package/core/templates/.agents/client/LOGIC.md +277 -0
- package/core/templates/.agents/client/ROUTING.md +235 -0
- package/core/templates/.agents/client/TESTING.md +241 -0
- package/core/templates/.agents/client/UI.md +226 -0
- package/core/templates/.agents/shared/CLOUD.md +229 -0
- package/core/templates/.agents/shared/CLOUD_TEARDOWN.md +158 -0
- package/core/templates/.agents/shared/SECURITY.md +286 -0
- package/core/templates/.frameworks/backend/django.md +55 -0
- package/core/templates/.frameworks/backend/express.md +74 -0
- package/core/templates/.frameworks/backend/fastapi.md +107 -0
- package/core/templates/.frameworks/backend/fastify.md +74 -0
- package/core/templates/.frameworks/backend/nestjs.md +75 -0
- package/core/templates/.frameworks/client/angular.md +80 -0
- package/core/templates/.frameworks/client/nextjs.md +47 -0
- package/core/templates/.frameworks/client/nuxt.md +45 -0
- package/core/templates/.frameworks/client/remix.md +44 -0
- package/core/templates/.frameworks/client/sveltekit.md +44 -0
- package/core/templates/.frameworks/client/vite-react.md +45 -0
- package/core/templates/CLAUDE.md +531 -0
- package/core/templates/CLOUD_STATE.md +55 -0
- package/core/templates/CONTRACTS.md +16 -0
- package/core/templates/TASKS_HISTORY.md +6 -0
- package/core/templates/backend/CLAUDE.md +207 -0
- package/core/templates/client/CLAUDE.md +213 -0
- package/core/templates/shared/.gitkeep +0 -0
- package/core/templates/shared/wiring.config.json +14 -0
- package/core/workflow/agent.js +1404 -0
- package/core/workflow/complete.js +354 -0
- package/core/workflow/guards.js +643 -0
- package/core/workflow/reset.js +246 -0
- package/core/workflow/restart.js +243 -0
- package/core/workflow/tasks_history.js +120 -0
- package/init.js +3 -15
- package/package.json +2 -1
|
@@ -0,0 +1,643 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Multi-Agent Monorepo Template - Guards
|
|
5
|
+
* Required by agent.js and complete.js
|
|
6
|
+
*
|
|
7
|
+
* Handles:
|
|
8
|
+
* - Config field validation
|
|
9
|
+
* - .tracking.json load / write / clear
|
|
10
|
+
* - Stale worktree reconciliation
|
|
11
|
+
* - MISSING detection + decision gate
|
|
12
|
+
* - Coexistence check (git analysis)
|
|
13
|
+
* - Recovery guidance generation
|
|
14
|
+
* - Agent active check
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const fs = require('fs');
|
|
18
|
+
const path = require('path');
|
|
19
|
+
const { execSync } = require('child_process');
|
|
20
|
+
|
|
21
|
+
// ── Colors ────────────────────────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
const c = {
|
|
24
|
+
reset: '\x1b[0m',
|
|
25
|
+
bold: '\x1b[1m',
|
|
26
|
+
dim: '\x1b[2m',
|
|
27
|
+
green: '\x1b[32m',
|
|
28
|
+
yellow: '\x1b[33m',
|
|
29
|
+
cyan: '\x1b[36m',
|
|
30
|
+
red: '\x1b[31m',
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const bold = (s) => `${c.bold}${s}${c.reset}`;
|
|
34
|
+
const green = (s) => `${c.green}${s}${c.reset}`;
|
|
35
|
+
const yellow = (s) => `${c.yellow}${s}${c.reset}`;
|
|
36
|
+
const dim = (s) => `${c.dim}${s}${c.reset}`;
|
|
37
|
+
const cyan = (s) => `${c.cyan}${s}${c.reset}`;
|
|
38
|
+
const red = (s) => `${c.red}${s}${c.reset}`;
|
|
39
|
+
|
|
40
|
+
const separator = () => console.log(`\n${dim('─'.repeat(60))}`);
|
|
41
|
+
|
|
42
|
+
// ── Slot template ─────────────────────────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
const emptySlot = () => ({
|
|
45
|
+
branch: null,
|
|
46
|
+
timestamp: null,
|
|
47
|
+
launchedAt: null,
|
|
48
|
+
status: null,
|
|
49
|
+
missingCount: 0,
|
|
50
|
+
worktreePath: null,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// ── Generate tracking structure ───────────────────────────────────────────────
|
|
54
|
+
|
|
55
|
+
const generateTrackingStructure = (config) => {
|
|
56
|
+
const bt = config.backend?.type;
|
|
57
|
+
|
|
58
|
+
const structure = {
|
|
59
|
+
client: {
|
|
60
|
+
UI: emptySlot(),
|
|
61
|
+
LOGIC: emptySlot(),
|
|
62
|
+
FORMS: emptySlot(),
|
|
63
|
+
ROUTING: emptySlot(),
|
|
64
|
+
TESTING: emptySlot(),
|
|
65
|
+
ACCESSIBILITY: emptySlot(),
|
|
66
|
+
},
|
|
67
|
+
shared: {
|
|
68
|
+
SECURITY: emptySlot(),
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
if (bt === 'separate') {
|
|
73
|
+
structure.backend = {
|
|
74
|
+
INIT: emptySlot(),
|
|
75
|
+
API: emptySlot(),
|
|
76
|
+
LOGIC: emptySlot(),
|
|
77
|
+
AUTH: emptySlot(),
|
|
78
|
+
DB: emptySlot(),
|
|
79
|
+
EVENTS: emptySlot(),
|
|
80
|
+
JOBS: emptySlot(),
|
|
81
|
+
TESTING: emptySlot(),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return structure;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
// ── Load tracking ─────────────────────────────────────────────────────────────
|
|
89
|
+
|
|
90
|
+
const loadTracking = (ROOT, config) => {
|
|
91
|
+
const trackingPath = path.join(ROOT, '.scaffold', '.tracking.json');
|
|
92
|
+
|
|
93
|
+
if (!fs.existsSync(trackingPath)) {
|
|
94
|
+
console.log(dim(' ℹ Tracking file not found - regenerating from config.'));
|
|
95
|
+
const fresh = generateTrackingStructure(config);
|
|
96
|
+
fs.writeFileSync(trackingPath, JSON.stringify(fresh, null, 2), 'utf8');
|
|
97
|
+
return fresh;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
return JSON.parse(fs.readFileSync(trackingPath, 'utf8'));
|
|
102
|
+
} catch {
|
|
103
|
+
console.log(yellow(' ⚠ .tracking.json is corrupt - regenerating.'));
|
|
104
|
+
const fresh = generateTrackingStructure(config);
|
|
105
|
+
fs.writeFileSync(trackingPath, JSON.stringify(fresh, null, 2), 'utf8');
|
|
106
|
+
return fresh;
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
// ── Write tracking slot ───────────────────────────────────────────────────────
|
|
111
|
+
|
|
112
|
+
const updateTrackingSlot = (tracking, scope, agent, data, ROOT) => {
|
|
113
|
+
if (!tracking[scope]) tracking[scope] = {};
|
|
114
|
+
if (!tracking[scope][agent]) tracking[scope][agent] = emptySlot();
|
|
115
|
+
|
|
116
|
+
tracking[scope][agent] = { ...tracking[scope][agent], ...data };
|
|
117
|
+
|
|
118
|
+
const trackingPath = path.join(ROOT, '.scaffold', '.tracking.json');
|
|
119
|
+
fs.writeFileSync(trackingPath, JSON.stringify(tracking, null, 2), 'utf8');
|
|
120
|
+
return tracking;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
// ── Clear tracking slot ───────────────────────────────────────────────────────
|
|
124
|
+
|
|
125
|
+
const clearTrackingSlot = (tracking, scope, agent, ROOT) => {
|
|
126
|
+
if (!tracking[scope] || !tracking[scope][agent]) return tracking;
|
|
127
|
+
|
|
128
|
+
// Preserve missingCount across completes - reset to 0 only on successful complete
|
|
129
|
+
tracking[scope][agent] = emptySlot();
|
|
130
|
+
|
|
131
|
+
const trackingPath = path.join(ROOT, '.scaffold', '.tracking.json');
|
|
132
|
+
fs.writeFileSync(trackingPath, JSON.stringify(tracking, null, 2), 'utf8');
|
|
133
|
+
return tracking;
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
// ── Validate config fields ────────────────────────────────────────────────────
|
|
137
|
+
|
|
138
|
+
const REQUIRED_CRITICAL = ['projectName', 'ide', 'client'];
|
|
139
|
+
const REQUIRED_BACKFILL = {
|
|
140
|
+
'ide.openArgs': [],
|
|
141
|
+
'ide.winPaths': [],
|
|
142
|
+
'ide.linuxPaths': [],
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const validateConfig = (config, ROOT) => {
|
|
146
|
+
const errors = [];
|
|
147
|
+
const backfilled = [];
|
|
148
|
+
|
|
149
|
+
// Critical fields - hard stop if missing
|
|
150
|
+
for (const field of REQUIRED_CRITICAL) {
|
|
151
|
+
if (!config[field]) errors.push(field);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (errors.length > 0) {
|
|
155
|
+
console.log(`\n${red(' Config is missing critical fields:')} ${errors.join(', ')}`);
|
|
156
|
+
console.log(dim(' Run npm run init to regenerate.\n'));
|
|
157
|
+
process.exit(1);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Non-critical fields - backfill with defaults
|
|
161
|
+
for (const [fieldPath, defaultVal] of Object.entries(REQUIRED_BACKFILL)) {
|
|
162
|
+
const parts = fieldPath.split('.');
|
|
163
|
+
let target = config;
|
|
164
|
+
let parent = null;
|
|
165
|
+
let key = null;
|
|
166
|
+
|
|
167
|
+
for (const part of parts) {
|
|
168
|
+
parent = target;
|
|
169
|
+
key = part;
|
|
170
|
+
target = target?.[part];
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (target === undefined && parent && key) {
|
|
174
|
+
parent[key] = defaultVal;
|
|
175
|
+
backfilled.push(fieldPath);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (backfilled.length > 0) {
|
|
180
|
+
console.log(dim(` ℹ Config backfilled: ${backfilled.join(', ')}`));
|
|
181
|
+
// Write updated config back
|
|
182
|
+
const configPath = path.join(ROOT, '.scaffold', '.config.json');
|
|
183
|
+
try {
|
|
184
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf8');
|
|
185
|
+
} catch { /* best-effort */ }
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return config;
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
// ── Check agent active ────────────────────────────────────────────────────────
|
|
192
|
+
|
|
193
|
+
const checkAgentActive = (tracking, scope, agent) => {
|
|
194
|
+
const slot = tracking?.[scope]?.[agent];
|
|
195
|
+
if (!slot || slot.status !== 'ACTIVE') return { active: false, slot: null };
|
|
196
|
+
return { active: true, slot };
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
// ── Active worktree branches (path-verified) ──────────────────────────────────
|
|
200
|
+
|
|
201
|
+
const getActiveWorktrees = (ROOT) => {
|
|
202
|
+
try {
|
|
203
|
+
const output = execSync('git worktree list --porcelain', { cwd: ROOT, stdio: 'pipe' }).toString();
|
|
204
|
+
const worktrees = [];
|
|
205
|
+
const blocks = output.trim().split('\n\n');
|
|
206
|
+
for (const block of blocks) {
|
|
207
|
+
const lines = block.split('\n');
|
|
208
|
+
const pathLine = lines.find(l => l.startsWith('worktree '));
|
|
209
|
+
const branchLine = lines.find(l => l.startsWith('branch '));
|
|
210
|
+
if (pathLine && branchLine) {
|
|
211
|
+
const wtPath = pathLine.replace('worktree ', '').trim();
|
|
212
|
+
const wtBranch = branchLine.replace('branch refs/heads/', '').trim();
|
|
213
|
+
if (fs.existsSync(wtPath)) worktrees.push({ path: wtPath, branch: wtBranch });
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return worktrees;
|
|
217
|
+
} catch { return []; }
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
// ── Coexistence check ─────────────────────────────────────────────────────────
|
|
221
|
+
|
|
222
|
+
const coexistenceCheck = (branch, ROOT) => {
|
|
223
|
+
const result = {
|
|
224
|
+
remoteExists: false,
|
|
225
|
+
unmergedCommits: 0,
|
|
226
|
+
divergenceCount: 0,
|
|
227
|
+
conflictFiles: [],
|
|
228
|
+
prerequisites: {},
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
// Check remote branch exists
|
|
232
|
+
try {
|
|
233
|
+
const remote = execSync(
|
|
234
|
+
`git ls-remote --heads origin ${branch}`,
|
|
235
|
+
{ cwd: ROOT, stdio: 'pipe', encoding: 'utf8' }
|
|
236
|
+
).trim();
|
|
237
|
+
result.remoteExists = remote.length > 0;
|
|
238
|
+
} catch { return result; }
|
|
239
|
+
|
|
240
|
+
if (!result.remoteExists) return result;
|
|
241
|
+
|
|
242
|
+
// Fetch remote branch
|
|
243
|
+
try {
|
|
244
|
+
execSync(`git fetch origin ${branch}`, { cwd: ROOT, stdio: 'pipe' });
|
|
245
|
+
} catch { return result; }
|
|
246
|
+
|
|
247
|
+
// Unmerged commits on branch
|
|
248
|
+
try {
|
|
249
|
+
const commits = execSync(
|
|
250
|
+
`git log main..origin/${branch} --oneline`,
|
|
251
|
+
{ cwd: ROOT, stdio: 'pipe', encoding: 'utf8' }
|
|
252
|
+
).trim();
|
|
253
|
+
result.unmergedCommits = commits ? commits.split('\n').length : 0;
|
|
254
|
+
} catch {}
|
|
255
|
+
|
|
256
|
+
// How many commits is main ahead of branch
|
|
257
|
+
try {
|
|
258
|
+
const ahead = execSync(
|
|
259
|
+
`git log origin/${branch}..main --oneline`,
|
|
260
|
+
{ cwd: ROOT, stdio: 'pipe', encoding: 'utf8' }
|
|
261
|
+
).trim();
|
|
262
|
+
result.divergenceCount = ahead ? ahead.split('\n').length : 0;
|
|
263
|
+
} catch {}
|
|
264
|
+
|
|
265
|
+
// File overlap - files changed in both branch and main since divergence
|
|
266
|
+
if (result.divergenceCount > 0) {
|
|
267
|
+
try {
|
|
268
|
+
const branchFiles = execSync(
|
|
269
|
+
`git diff main...origin/${branch} --name-only`,
|
|
270
|
+
{ cwd: ROOT, stdio: 'pipe', encoding: 'utf8' }
|
|
271
|
+
).trim().split('\n').filter(Boolean);
|
|
272
|
+
|
|
273
|
+
const mainFiles = execSync(
|
|
274
|
+
`git log origin/${branch}..main --name-only --format=`,
|
|
275
|
+
{ cwd: ROOT, stdio: 'pipe', encoding: 'utf8' }
|
|
276
|
+
).trim().split('\n').filter(Boolean);
|
|
277
|
+
|
|
278
|
+
const mainSet = new Set(mainFiles);
|
|
279
|
+
result.conflictFiles = branchFiles.filter(f => mainSet.has(f));
|
|
280
|
+
} catch {}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return result;
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
// ── Recovery guidance generator ───────────────────────────────────────────────
|
|
287
|
+
|
|
288
|
+
const generateRecoveryGuidance = (branch, coexistence) => {
|
|
289
|
+
const lines = [];
|
|
290
|
+
|
|
291
|
+
lines.push('## Recovery Notes');
|
|
292
|
+
lines.push('');
|
|
293
|
+
lines.push('This workspace was recovered from a missing worktree.');
|
|
294
|
+
lines.push('Complete the following steps before implementing anything:');
|
|
295
|
+
lines.push('');
|
|
296
|
+
lines.push('### Step 1 - Pull main into this branch');
|
|
297
|
+
lines.push('```');
|
|
298
|
+
lines.push('git pull origin main');
|
|
299
|
+
lines.push('```');
|
|
300
|
+
|
|
301
|
+
if (coexistence.conflictFiles.length > 0) {
|
|
302
|
+
lines.push('');
|
|
303
|
+
lines.push('### Step 2 - Resolve conflicts in this order');
|
|
304
|
+
coexistence.conflictFiles.forEach(file => {
|
|
305
|
+
if (file.includes('CONTRACTS.md')) {
|
|
306
|
+
lines.push(`- **${file}**: Keep all types added since this branch was created.`);
|
|
307
|
+
lines.push(' Merge your proposed types alongside them - do not overwrite.');
|
|
308
|
+
} else if (file.includes('Providers')) {
|
|
309
|
+
lines.push(`- **${file}**: Rewired since this branch was created.`);
|
|
310
|
+
lines.push(' Adapt your hooks to consume the new provider shape.');
|
|
311
|
+
} else {
|
|
312
|
+
lines.push(`- **${file}**: Modified since this branch was created.`);
|
|
313
|
+
lines.push(' Preserve changes from main - adapt your work accordingly.');
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
lines.push('');
|
|
319
|
+
lines.push('### Do NOT');
|
|
320
|
+
lines.push('- Force push this branch');
|
|
321
|
+
lines.push('- Start new implementation before git status is clean');
|
|
322
|
+
lines.push('- Overwrite types in CONTRACTS.md added by other agents');
|
|
323
|
+
lines.push('');
|
|
324
|
+
lines.push('---');
|
|
325
|
+
|
|
326
|
+
return lines.join('\n');
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
// ── MISSING gate ──────────────────────────────────────────────────────────────
|
|
330
|
+
|
|
331
|
+
const runMissingGate = async (params) => {
|
|
332
|
+
const { scope, agent, slot, tracking, config, ROOT, ask } = params;
|
|
333
|
+
const { branch, timestamp, missingCount } = slot;
|
|
334
|
+
|
|
335
|
+
separator();
|
|
336
|
+
console.log(`\n${yellow(` ⚠ ${scope.toUpperCase()} / ${agent} workspace is missing`)}\n`);
|
|
337
|
+
console.log(` ${dim('Branch')} : ${branch}`);
|
|
338
|
+
console.log(` ${dim('Created')} : ${slot.launchedAt ? new Date(slot.launchedAt).toLocaleString() : 'unknown'}`);
|
|
339
|
+
|
|
340
|
+
// Run coexistence check
|
|
341
|
+
console.log(dim('\n Checking remote and coexistence...\n'));
|
|
342
|
+
const coexistence = coexistenceCheck(branch, ROOT);
|
|
343
|
+
|
|
344
|
+
// Display coexistence results
|
|
345
|
+
if (!coexistence.remoteExists) {
|
|
346
|
+
console.log(` ${dim('Remote')} : ${red('not found - branch was deleted remotely')}`);
|
|
347
|
+
} else {
|
|
348
|
+
console.log(` ${dim('Remote')} : ${green('exists')} - ${coexistence.unmergedCommits} unmerged commit(s)`);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (coexistence.divergenceCount > 0) {
|
|
352
|
+
console.log(` ${dim('Divergence')}: ${yellow(`main is ${coexistence.divergenceCount} commit(s) ahead`)}`);
|
|
353
|
+
} else {
|
|
354
|
+
console.log(` ${dim('Divergence')}: ${green('none - branch is up to date with main')}`);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if (coexistence.conflictFiles.length > 0) {
|
|
358
|
+
console.log(` ${dim('Conflicts')} : ${red(coexistence.conflictFiles.length + ' file(s) overlap with main changes:')}`);
|
|
359
|
+
coexistence.conflictFiles.forEach(f => console.log(` ${dim('→')} ${f}`));
|
|
360
|
+
} else if (coexistence.remoteExists) {
|
|
361
|
+
console.log(` ${dim('Conflicts')} : ${green('none detected')}`);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
if (missingCount > 0) {
|
|
365
|
+
console.log(`\n ${yellow(`ℹ This agent has been missing ${missingCount} time(s) before.`)}`);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Present options
|
|
369
|
+
separator();
|
|
370
|
+
console.log(`\n ${bold('What would you like to do?')}\n`);
|
|
371
|
+
|
|
372
|
+
if (coexistence.remoteExists) {
|
|
373
|
+
console.log(` ${dim('1.')} ${bold('Recover')} - restore workspace from remote branch`);
|
|
374
|
+
console.log(` ${dim('→')} Your ${coexistence.unmergedCommits} commit(s) will be available to continue`);
|
|
375
|
+
if (coexistence.divergenceCount > 0) {
|
|
376
|
+
console.log(` ${yellow(`⚠ ${coexistence.divergenceCount} commit(s) to reconcile - conflicts likely in ${coexistence.conflictFiles.length} file(s)`)}`);
|
|
377
|
+
}
|
|
378
|
+
console.log('');
|
|
379
|
+
console.log(` ${dim('2.')} ${bold('Reset')} - delete branch locally and remotely, start fresh`);
|
|
380
|
+
console.log(` ${red('⚠ All ' + coexistence.unmergedCommits + ' commit(s) will be permanently lost')}`);
|
|
381
|
+
console.log('');
|
|
382
|
+
console.log(` ${dim('3.')} ${bold('New task')} - create a new ${agent} branch, leave remote unresolved`);
|
|
383
|
+
console.log(` ${yellow('⚠ 2 unmerged ' + agent + ' branches will exist')}`);
|
|
384
|
+
console.log(` ${dim('Only recommended for a genuinely separate concern')}`);
|
|
385
|
+
} else {
|
|
386
|
+
// Remote doesn't exist - only reset or new task
|
|
387
|
+
console.log(` ${dim('1.')} ${bold('Reset')} - clear this tracking entry, start fresh`);
|
|
388
|
+
console.log(` ${dim('→')} Remote branch is already gone - no data loss`);
|
|
389
|
+
console.log('');
|
|
390
|
+
console.log(` ${dim('2.')} ${bold('New task')} - create a new ${agent} branch`);
|
|
391
|
+
console.log(` ${dim('→')} Tracking entry will be replaced`);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const maxOption = coexistence.remoteExists ? 3 : 2;
|
|
395
|
+
let choice;
|
|
396
|
+
while (!choice) {
|
|
397
|
+
const input = await ask(`\n ${bold(`Select (1-${maxOption})`)}: `);
|
|
398
|
+
const n = parseInt(input);
|
|
399
|
+
if (!isNaN(n) && n >= 1 && n <= maxOption) choice = n;
|
|
400
|
+
else console.log(yellow(` Please enter a number between 1 and ${maxOption}.`));
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// ── Handle: Recover ─────────────────────────────────────────────────────────
|
|
404
|
+
|
|
405
|
+
if (coexistence.remoteExists && choice === 1) {
|
|
406
|
+
separator();
|
|
407
|
+
console.log(`\n${bold('Recovering workspace...')}\n`);
|
|
408
|
+
|
|
409
|
+
const sanitizedName = config.projectName.toLowerCase().replace(/\s+/g, '-');
|
|
410
|
+
const worktreeName = `${scope}-${sanitizedName}-${agent.toLowerCase()}-${timestamp}`;
|
|
411
|
+
const worktreePath = path.join(ROOT, 'worktrees', worktreeName);
|
|
412
|
+
|
|
413
|
+
try {
|
|
414
|
+
// Check if local branch exists
|
|
415
|
+
try {
|
|
416
|
+
execSync(`git show-ref --verify --quiet refs/heads/${branch}`, { cwd: ROOT, stdio: 'pipe' });
|
|
417
|
+
execSync(`git worktree add "${worktreePath}" ${branch}`, { cwd: ROOT, stdio: 'pipe' });
|
|
418
|
+
} catch {
|
|
419
|
+
// Local branch gone - create from remote
|
|
420
|
+
execSync(`git worktree add "${worktreePath}" -b ${branch} origin/${branch}`, { cwd: ROOT, stdio: 'pipe' });
|
|
421
|
+
}
|
|
422
|
+
console.log(` ${green('✓')} Workspace restored at: worktrees/${worktreeName}`);
|
|
423
|
+
} catch (err) {
|
|
424
|
+
console.log(` ${red('✗')} Could not restore workspace: ${err.message}`);
|
|
425
|
+
console.log(dim(' Try manually: git worktree add worktrees/' + worktreeName + ' ' + branch));
|
|
426
|
+
return { action: 'failed' };
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// Append recovery guidance to TASK.md
|
|
430
|
+
const taskMdPath = path.join(worktreePath, 'TASK.md');
|
|
431
|
+
if (fs.existsSync(taskMdPath)) {
|
|
432
|
+
const guidance = generateRecoveryGuidance(branch, coexistence);
|
|
433
|
+
fs.appendFileSync(taskMdPath, `\n${guidance}\n`, 'utf8');
|
|
434
|
+
console.log(` ${green('✓')} Recovery guidance appended to TASK.md`);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Show best-practice summary
|
|
438
|
+
console.log(`\n${bold('Before implementing - complete these steps:')}\n`);
|
|
439
|
+
console.log(` ${bold('1.')} Pull main into this branch:`);
|
|
440
|
+
console.log(` ${cyan('git pull origin main')}\n`);
|
|
441
|
+
if (coexistence.conflictFiles.length > 0) {
|
|
442
|
+
console.log(` ${bold('2.')} Resolve conflicts in: ${coexistence.conflictFiles.join(', ')}`);
|
|
443
|
+
console.log(dim(' See Recovery Notes in TASK.md for file-specific guidance\n'));
|
|
444
|
+
}
|
|
445
|
+
console.log(` ${yellow('⚠ Do NOT start new implementation until git status is clean.')}\n`);
|
|
446
|
+
|
|
447
|
+
// Update tracking slot - back to ACTIVE
|
|
448
|
+
updateTrackingSlot(tracking, scope, agent, {
|
|
449
|
+
status: 'ACTIVE',
|
|
450
|
+
worktreePath,
|
|
451
|
+
}, ROOT);
|
|
452
|
+
|
|
453
|
+
return { action: 'recovered', worktreePath };
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// ── Handle: Reset ──────────────────────────────────────────────────────────
|
|
457
|
+
|
|
458
|
+
const isReset = coexistence.remoteExists ? choice === 2 : choice === 1;
|
|
459
|
+
|
|
460
|
+
if (isReset) {
|
|
461
|
+
separator();
|
|
462
|
+
console.log(`\n${bold('Resetting...')}\n`);
|
|
463
|
+
|
|
464
|
+
// Delete local branch
|
|
465
|
+
try {
|
|
466
|
+
execSync(`git branch -D ${branch}`, { cwd: ROOT, stdio: 'pipe' });
|
|
467
|
+
console.log(` ${green('✓')} Local branch deleted`);
|
|
468
|
+
} catch {
|
|
469
|
+
console.log(` ${dim('!')} Local branch not found - skipping`);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// Delete remote branch
|
|
473
|
+
if (coexistence.remoteExists) {
|
|
474
|
+
try {
|
|
475
|
+
execSync(`git push origin --delete ${branch}`, { cwd: ROOT, stdio: 'pipe' });
|
|
476
|
+
console.log(` ${green('✓')} Remote branch deleted`);
|
|
477
|
+
} catch {
|
|
478
|
+
console.log(` ${yellow('!')} Could not delete remote branch - delete manually: git push origin --delete ${branch}`);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// Clear tracking slot
|
|
483
|
+
tracking[scope][agent] = emptySlot();
|
|
484
|
+
const trackingPath = path.join(ROOT, '.scaffold', '.tracking.json');
|
|
485
|
+
fs.writeFileSync(trackingPath, JSON.stringify(tracking, null, 2), 'utf8');
|
|
486
|
+
console.log(` ${green('✓')} Tracking entry cleared`);
|
|
487
|
+
|
|
488
|
+
// Update BUILD_STATE.md
|
|
489
|
+
const buildStatePath = path.join(ROOT, 'BUILD_STATE.md');
|
|
490
|
+
if (fs.existsSync(buildStatePath)) {
|
|
491
|
+
let content = fs.readFileSync(buildStatePath, 'utf8');
|
|
492
|
+
content = content.replace(
|
|
493
|
+
`| IN PROGRESS | ${branch} |`,
|
|
494
|
+
`| RESET | ${branch} |`
|
|
495
|
+
);
|
|
496
|
+
content = content.replace(
|
|
497
|
+
`| MISSING | ${branch} |`,
|
|
498
|
+
`| RESET | ${branch} |`
|
|
499
|
+
);
|
|
500
|
+
fs.writeFileSync(buildStatePath, content, 'utf8');
|
|
501
|
+
try {
|
|
502
|
+
execSync('git add BUILD_STATE.md .scaffold/.tracking.json', { cwd: ROOT, stdio: 'pipe' });
|
|
503
|
+
execSync(`git commit -m "build: reset ${scope}/${agent} [${branch}]"`, { cwd: ROOT, stdio: 'pipe' });
|
|
504
|
+
} catch { /* best-effort */ }
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
console.log(`\n ${green('✓')} Reset complete - proceeding to launch new ${agent} task.\n`);
|
|
508
|
+
return { action: 'reset' };
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// ── Handle: New task ───────────────────────────────────────────────────────
|
|
512
|
+
|
|
513
|
+
console.log(dim('\n Proceeding with new branch. Remote branch left unresolved.\n'));
|
|
514
|
+
return { action: 'new' };
|
|
515
|
+
};
|
|
516
|
+
|
|
517
|
+
// ── Reconcile stale worktrees ─────────────────────────────────────────────────
|
|
518
|
+
|
|
519
|
+
const reconcileStaleWorktrees = (entries, tracking, ROOT) => {
|
|
520
|
+
const activeWorktrees = getActiveWorktrees(ROOT);
|
|
521
|
+
const activeBranches = new Set(activeWorktrees.map(w => w.branch));
|
|
522
|
+
|
|
523
|
+
const stale = entries.filter(e =>
|
|
524
|
+
e.status === 'IN PROGRESS' &&
|
|
525
|
+
e.branch &&
|
|
526
|
+
!activeBranches.has(e.branch)
|
|
527
|
+
);
|
|
528
|
+
|
|
529
|
+
if (stale.length === 0) return entries;
|
|
530
|
+
|
|
531
|
+
const buildStatePath = path.join(ROOT, 'BUILD_STATE.md');
|
|
532
|
+
let content = fs.existsSync(buildStatePath)
|
|
533
|
+
? fs.readFileSync(buildStatePath, 'utf8')
|
|
534
|
+
: '';
|
|
535
|
+
|
|
536
|
+
stale.forEach(e => {
|
|
537
|
+
content = content.replace(
|
|
538
|
+
`| IN PROGRESS | ${e.branch} |`,
|
|
539
|
+
`| MISSING | ${e.branch} |`
|
|
540
|
+
);
|
|
541
|
+
|
|
542
|
+
// Update tracking slot missingCount
|
|
543
|
+
const scope = e.scope;
|
|
544
|
+
const agent = e.agent;
|
|
545
|
+
if (tracking?.[scope]?.[agent]) {
|
|
546
|
+
tracking[scope][agent].status = 'MISSING';
|
|
547
|
+
tracking[scope][agent].missingCount = (tracking[scope][agent].missingCount || 0) + 1;
|
|
548
|
+
}
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
if (stale.length > 0 && fs.existsSync(buildStatePath)) {
|
|
552
|
+
fs.writeFileSync(buildStatePath, content, 'utf8');
|
|
553
|
+
const trackingPath = path.join(ROOT, '.scaffold', '.tracking.json');
|
|
554
|
+
fs.writeFileSync(trackingPath, JSON.stringify(tracking, null, 2), 'utf8');
|
|
555
|
+
|
|
556
|
+
try {
|
|
557
|
+
execSync('git add BUILD_STATE.md .scaffold/.tracking.json', { cwd: ROOT, stdio: 'pipe' });
|
|
558
|
+
execSync('git commit -m "build: mark missing worktrees"', { cwd: ROOT, stdio: 'pipe' });
|
|
559
|
+
} catch { /* best-effort */ }
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
return entries.map(e =>
|
|
563
|
+
stale.find(s => s.branch === e.branch) ? { ...e, status: 'MISSING' } : e
|
|
564
|
+
);
|
|
565
|
+
};
|
|
566
|
+
|
|
567
|
+
// ── Browser opener ───────────────────────────────────────────────────────────
|
|
568
|
+
|
|
569
|
+
const openBrowser = (url) => {
|
|
570
|
+
const platform = process.platform;
|
|
571
|
+
try {
|
|
572
|
+
if (platform === 'darwin') {
|
|
573
|
+
try {
|
|
574
|
+
execSync(`open "${url}"`, { stdio: 'pipe' });
|
|
575
|
+
return true;
|
|
576
|
+
} catch {}
|
|
577
|
+
const browsers = [
|
|
578
|
+
'Google Chrome', 'Safari', 'Firefox',
|
|
579
|
+
'Microsoft Edge', 'Arc', 'Brave Browser',
|
|
580
|
+
];
|
|
581
|
+
for (const browser of browsers) {
|
|
582
|
+
if (fs.existsSync(`/Applications/${browser}.app`)) {
|
|
583
|
+
execSync(`open -a "${browser}" "${url}"`, { stdio: 'pipe' });
|
|
584
|
+
return true;
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
} else if (platform === 'win32') {
|
|
588
|
+
execSync(`start "" "${url}"`, { stdio: 'pipe' });
|
|
589
|
+
return true;
|
|
590
|
+
} else {
|
|
591
|
+
execSync(`xdg-open "${url}"`, { stdio: 'pipe' });
|
|
592
|
+
return true;
|
|
593
|
+
}
|
|
594
|
+
} catch {}
|
|
595
|
+
return false;
|
|
596
|
+
};
|
|
597
|
+
|
|
598
|
+
// ── Silent auth detection ─────────────────────────────────────────────────────
|
|
599
|
+
|
|
600
|
+
const detectAuthMethod = (ROOT) => {
|
|
601
|
+
// 1. SSH
|
|
602
|
+
try {
|
|
603
|
+
const out = execSync('ssh -T git@github.com 2>&1', { stdio: 'pipe', encoding: 'utf8' });
|
|
604
|
+
if (out.includes('successfully authenticated')) return 'ssh';
|
|
605
|
+
} catch (e) {
|
|
606
|
+
// exit code 1 = authenticated (GitHub returns 1 for no shell access)
|
|
607
|
+
if (e.stdout?.includes('successfully authenticated') ||
|
|
608
|
+
e.stderr?.includes('successfully authenticated')) return 'ssh';
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// 2. gh CLI
|
|
612
|
+
try {
|
|
613
|
+
execSync('gh auth status', { stdio: 'pipe' });
|
|
614
|
+
return 'gh';
|
|
615
|
+
} catch {}
|
|
616
|
+
|
|
617
|
+
// 3. HTTPS stored credentials
|
|
618
|
+
try {
|
|
619
|
+
execSync('git ls-remote https://github.com 2>/dev/null', {
|
|
620
|
+
cwd: ROOT, stdio: 'pipe', timeout: 5000,
|
|
621
|
+
});
|
|
622
|
+
return 'https';
|
|
623
|
+
} catch {}
|
|
624
|
+
|
|
625
|
+
return null;
|
|
626
|
+
};
|
|
627
|
+
|
|
628
|
+
// ── Exports ───────────────────────────────────────────────────────────────────
|
|
629
|
+
|
|
630
|
+
module.exports = {
|
|
631
|
+
generateTrackingStructure,
|
|
632
|
+
loadTracking,
|
|
633
|
+
updateTrackingSlot,
|
|
634
|
+
clearTrackingSlot,
|
|
635
|
+
validateConfig,
|
|
636
|
+
checkAgentActive,
|
|
637
|
+
coexistenceCheck,
|
|
638
|
+
runMissingGate,
|
|
639
|
+
reconcileStaleWorktrees,
|
|
640
|
+
getActiveWorktrees,
|
|
641
|
+
openBrowser,
|
|
642
|
+
detectAuthMethod,
|
|
643
|
+
};
|