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.
Files changed (44) hide show
  1. package/core/templates/.agents/backend/API.md +259 -0
  2. package/core/templates/.agents/backend/AUTH.md +246 -0
  3. package/core/templates/.agents/backend/DB.md +257 -0
  4. package/core/templates/.agents/backend/EVENTS.md +253 -0
  5. package/core/templates/.agents/backend/INIT.md +239 -0
  6. package/core/templates/.agents/backend/JOBS.md +256 -0
  7. package/core/templates/.agents/backend/LOGIC.md +291 -0
  8. package/core/templates/.agents/backend/TESTING.md +266 -0
  9. package/core/templates/.agents/client/ACCESSIBILITY.md +266 -0
  10. package/core/templates/.agents/client/FORMS.md +234 -0
  11. package/core/templates/.agents/client/LOGIC.md +277 -0
  12. package/core/templates/.agents/client/ROUTING.md +235 -0
  13. package/core/templates/.agents/client/TESTING.md +241 -0
  14. package/core/templates/.agents/client/UI.md +226 -0
  15. package/core/templates/.agents/shared/CLOUD.md +229 -0
  16. package/core/templates/.agents/shared/CLOUD_TEARDOWN.md +158 -0
  17. package/core/templates/.agents/shared/SECURITY.md +286 -0
  18. package/core/templates/.frameworks/backend/django.md +55 -0
  19. package/core/templates/.frameworks/backend/express.md +74 -0
  20. package/core/templates/.frameworks/backend/fastapi.md +107 -0
  21. package/core/templates/.frameworks/backend/fastify.md +74 -0
  22. package/core/templates/.frameworks/backend/nestjs.md +75 -0
  23. package/core/templates/.frameworks/client/angular.md +80 -0
  24. package/core/templates/.frameworks/client/nextjs.md +47 -0
  25. package/core/templates/.frameworks/client/nuxt.md +45 -0
  26. package/core/templates/.frameworks/client/remix.md +44 -0
  27. package/core/templates/.frameworks/client/sveltekit.md +44 -0
  28. package/core/templates/.frameworks/client/vite-react.md +45 -0
  29. package/core/templates/CLAUDE.md +531 -0
  30. package/core/templates/CLOUD_STATE.md +55 -0
  31. package/core/templates/CONTRACTS.md +16 -0
  32. package/core/templates/TASKS_HISTORY.md +6 -0
  33. package/core/templates/backend/CLAUDE.md +207 -0
  34. package/core/templates/client/CLAUDE.md +213 -0
  35. package/core/templates/shared/.gitkeep +0 -0
  36. package/core/templates/shared/wiring.config.json +14 -0
  37. package/core/workflow/agent.js +1413 -0
  38. package/core/workflow/complete.js +403 -0
  39. package/core/workflow/guards.js +643 -0
  40. package/core/workflow/reset.js +246 -0
  41. package/core/workflow/restart.js +243 -0
  42. package/core/workflow/tasks_history.js +120 -0
  43. package/init.js +32 -15
  44. 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
+ };