@girardmedia/bootspring 2.1.0 → 2.1.2

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/cli/build.js CHANGED
@@ -165,6 +165,10 @@ async function run(args) {
165
165
  case 'all':
166
166
  return buildLoop(projectRoot, parsedArgs);
167
167
 
168
+ case 'migrate-todo':
169
+ case 'migrate':
170
+ return migrateTodoFormat(projectRoot, parsedArgs);
171
+
168
172
  case 'backfill':
169
173
  case 'recover':
170
174
  case 'hydrate':
@@ -350,7 +354,7 @@ ${c.bold}Phase:${c.reset} ${nextTask.phase || 'MVP'}
350
354
  ${c.bold}Source:${c.reset} ${nextTask.source || 'PRD.md'}
351
355
 
352
356
  ${c.cyan}${c.bold}Tell Claude Code:${c.reset}
353
- "Read planning/TASK_QUEUE.md, find ${nextTask.id}, and implement it"
357
+ "Read planning/TODO.md, find ${nextTask.id}, and implement it"
354
358
 
355
359
  ${c.dim}When done, run: bootspring build done${c.reset}
356
360
  `);
@@ -462,7 +466,7 @@ async function markTaskDone(projectRoot, _args = {}) {
462
466
  console.log(`
463
467
  ${c.cyan}${c.bold}▶ Next: ${nextTask.title}${c.reset} (${nextTask.id})
464
468
 
465
- ${c.bold}Continue building:${c.reset} Read planning/TASK_QUEUE.md, implement ${nextTask.id}
469
+ ${c.bold}Continue building:${c.reset} Read planning/TODO.md, implement ${nextTask.id}
466
470
  ${c.dim}Then run: bootspring build done${c.reset}
467
471
  `);
468
472
  } else {
@@ -545,7 +549,7 @@ ${c.bold}Progress:${c.reset} ${stats.completed}/${stats.total} tasks (${Math.rou
545
549
  ${c.bold}Git Mode:${c.reset} ${noCommit ? 'Disabled' : noPush ? 'Commit only' : 'Commit + Push'}
546
550
 
547
551
  ${c.bold}Workflow:${c.reset}
548
- 1. Read planning/TASK_QUEUE.md, find ${currentTask?.id || 'current task'}
552
+ 1. Read planning/TODO.md, find ${currentTask?.id || 'current task'}
549
553
  2. Implement the task
550
554
  3. Run: ${c.cyan}bootspring build done${c.reset}${!noCommit ? `
551
555
  (Auto-commits: "feat(${currentTask?.id}): ${currentTask?.title?.slice(0, 40) || 'task'}...")${!noPush ? `
@@ -1225,6 +1229,171 @@ ${c.bold}Next steps:${c.reset}
1225
1229
  `);
1226
1230
  }
1227
1231
 
1232
+ /**
1233
+ * Extract non-task supplementary content from TASK_QUEUE.md.
1234
+ * Captures context notes, architecture diagrams, integration guidelines,
1235
+ * and any section that isn't the task table or per-task detail blocks.
1236
+ */
1237
+ function extractSupplementaryContent(queueContent) {
1238
+ const lines = queueContent.split('\n');
1239
+ const sections = [];
1240
+ let currentSection = null;
1241
+ let inTaskTable = false;
1242
+ let inTaskDetail = false;
1243
+
1244
+ for (let i = 0; i < lines.length; i++) {
1245
+ const line = lines[i];
1246
+
1247
+ // Detect task table (starts with | Position | or | # |)
1248
+ if (/^\|\s*(Position|#)\s*\|/.test(line)) {
1249
+ inTaskTable = true;
1250
+ continue;
1251
+ }
1252
+ // Still in task table (rows starting with |)
1253
+ if (inTaskTable && /^\|/.test(line)) {
1254
+ continue;
1255
+ }
1256
+ if (inTaskTable && !/^\|/.test(line)) {
1257
+ inTaskTable = false;
1258
+ }
1259
+
1260
+ // Detect per-task detail blocks (### task-XXX: or ### bs-XXX:)
1261
+ if (/^###\s+(task-\d+|bs-\d+)\s*:/.test(line)) {
1262
+ inTaskDetail = true;
1263
+ continue;
1264
+ }
1265
+ // End task detail at next ### or ## heading
1266
+ if (inTaskDetail && /^#{2,3}\s+/.test(line) && !/^###\s+(task-\d+|bs-\d+)\s*:/.test(line)) {
1267
+ inTaskDetail = false;
1268
+ }
1269
+ if (inTaskDetail) continue;
1270
+
1271
+ // Skip the header, queue status heading, and generated boilerplate
1272
+ if (/^#\s+.*Implementation Queue/.test(line)) continue;
1273
+ if (/^>\s*(Ordered task queue|Last Updated|Current Version)/.test(line)) continue;
1274
+ if (/^## Queue Status/.test(line)) { inTaskTable = true; continue; }
1275
+ if (/^## Task Details/.test(line)) { inTaskDetail = true; continue; }
1276
+ if (/^\*Generated by/.test(line)) continue;
1277
+ if (/^---$/.test(line.trim())) continue;
1278
+
1279
+ // Detect section headings for supplementary content
1280
+ if (/^##\s+/.test(line) && !/^## (Queue Status|Task Details|Execution Guidelines)/.test(line)) {
1281
+ if (currentSection) sections.push(currentSection);
1282
+ currentSection = { heading: line, lines: [] };
1283
+ continue;
1284
+ }
1285
+
1286
+ // Execution Guidelines is useful — keep it
1287
+ if (/^## Execution Guidelines/.test(line)) {
1288
+ if (currentSection) sections.push(currentSection);
1289
+ currentSection = { heading: line, lines: [] };
1290
+ continue;
1291
+ }
1292
+
1293
+ if (currentSection) {
1294
+ currentSection.lines.push(line);
1295
+ }
1296
+ }
1297
+
1298
+ if (currentSection) sections.push(currentSection);
1299
+
1300
+ // Filter out empty sections and build output
1301
+ const output = sections
1302
+ .filter(s => s.lines.some(l => l.trim().length > 0))
1303
+ .map(s => `${s.heading}\n\n${s.lines.join('\n').trim()}`)
1304
+ .join('\n\n---\n\n');
1305
+
1306
+ return output.trim();
1307
+ }
1308
+
1309
+ /**
1310
+ * Migrate from TASK_QUEUE.md to TODO.md as single source of truth.
1311
+ */
1312
+ async function migrateTodoFormat(projectRoot, args = {}) {
1313
+ const planningDir = path.join(projectRoot, 'planning');
1314
+ const queuePath = path.join(planningDir, 'TASK_QUEUE.md');
1315
+ const todoPath = path.join(planningDir, 'TODO.md');
1316
+ const keepQueue = Boolean(args['keep-queue'] || args.keepQueue);
1317
+
1318
+ console.log(`
1319
+ ${c.cyan}${c.bold}Migrating to TODO.md as Single Source of Truth${c.reset}
1320
+ `);
1321
+
1322
+ // Step 1: Ensure we have build state with tasks
1323
+ let state = buildState.load(projectRoot);
1324
+
1325
+ if (!state || !state.implementationQueue || state.implementationQueue.length === 0) {
1326
+ if (fs.existsSync(queuePath)) {
1327
+ console.log(`${c.dim}No build state — syncing tasks from TASK_QUEUE.md first...${c.reset}`);
1328
+ try {
1329
+ const taskExtractor = require('../core/task-extractor');
1330
+ const result = taskExtractor.syncFromTaskQueue(projectRoot);
1331
+ if (result.total > 0) {
1332
+ console.log(`${c.green}✓${c.reset} Synced ${result.total} tasks from TASK_QUEUE.md`);
1333
+ state = buildState.load(projectRoot);
1334
+ }
1335
+ } catch {
1336
+ // Fall through
1337
+ }
1338
+ }
1339
+
1340
+ if (!state || !state.implementationQueue || state.implementationQueue.length === 0) {
1341
+ console.log(`${c.red}No tasks found. Run 'bootspring seed build' first or ensure TASK_QUEUE.md exists.${c.reset}`);
1342
+ return;
1343
+ }
1344
+ }
1345
+
1346
+ // Step 2: Extract non-task content from TASK_QUEUE.md (context notes, diagrams, guidelines)
1347
+ let supplementaryContent = '';
1348
+ if (fs.existsSync(queuePath)) {
1349
+ const queueContent = fs.readFileSync(queuePath, 'utf-8');
1350
+ supplementaryContent = extractSupplementaryContent(queueContent);
1351
+ }
1352
+
1353
+ // Step 3: Generate enriched TODO.md with full task metadata
1354
+ const planningTemplate = require('../generators/templates/build-planning.template');
1355
+ let todo = planningTemplate.generateTodo(state.implementationQueue, {
1356
+ projectName: state.projectName
1357
+ });
1358
+
1359
+ // Append supplementary content if any was extracted
1360
+ if (supplementaryContent) {
1361
+ todo += `\n---\n\n## Reference (from TASK_QUEUE.md)\n\n${supplementaryContent}\n`;
1362
+ }
1363
+
1364
+ fs.writeFileSync(todoPath, todo);
1365
+
1366
+ const stats = buildState.getStats(projectRoot);
1367
+ console.log(`${c.green}✓${c.reset} Generated TODO.md with ${state.implementationQueue.length} tasks (${stats?.completed || 0} completed)`);
1368
+ if (supplementaryContent) {
1369
+ console.log(`${c.green}✓${c.reset} Preserved supplementary content (context notes, diagrams, guidelines)`);
1370
+ }
1371
+
1372
+ // Step 4: Archive TASK_QUEUE.md unless --keep-queue
1373
+ if (fs.existsSync(queuePath) && !keepQueue) {
1374
+ const archivePath = path.join(planningDir, 'TASK_QUEUE.md.bak');
1375
+ fs.renameSync(queuePath, archivePath);
1376
+ console.log(`${c.green}✓${c.reset} Archived TASK_QUEUE.md → TASK_QUEUE.md.bak`);
1377
+ } else if (fs.existsSync(queuePath) && keepQueue) {
1378
+ console.log(`${c.dim}Kept TASK_QUEUE.md (--keep-queue)${c.reset}`);
1379
+ }
1380
+
1381
+ console.log(`
1382
+ ${c.green}${c.bold}Migration complete${c.reset}
1383
+
1384
+ ${c.bold}What changed:${c.reset}
1385
+ - ${c.cyan}planning/TODO.md${c.reset} is now the single source of truth
1386
+ - Task IDs, source, description, acceptance criteria, and status are all inline
1387
+ - Non-task content (context notes, diagrams, guidelines) preserved in Reference section
1388
+ - Build loop reads TODO.md instead of TASK_QUEUE.md
1389
+ ${!keepQueue && fs.existsSync(path.join(planningDir, 'TASK_QUEUE.md.bak')) ? ` - Old TASK_QUEUE.md archived as TASK_QUEUE.md.bak
1390
+ ` : ''}
1391
+ ${c.bold}Next steps:${c.reset}
1392
+ Run ${c.cyan}bootspring build status${c.reset} to verify
1393
+ Run ${c.cyan}bootspring build next${c.reset} to continue building
1394
+ `);
1395
+ }
1396
+
1228
1397
  /**
1229
1398
  * Show help
1230
1399
  */
@@ -1252,6 +1421,7 @@ ${c.bold}Commands:${c.reset}
1252
1421
  ${c.cyan}bootspring build status${c.reset} View detailed progress
1253
1422
  ${c.cyan}bootspring build task${c.reset} View current task details
1254
1423
  ${c.cyan}bootspring build plan${c.reset} View the full master plan
1424
+ ${c.cyan}bootspring build migrate-todo${c.reset} Migrate TASK_QUEUE.md → TODO.md (single source of truth)
1255
1425
  ${c.cyan}bootspring build backfill${c.reset} Recover preseed + SEED + TASK_QUEUE artifacts
1256
1426
  ${c.cyan}bootspring build reset${c.reset} Start over
1257
1427
 
@@ -52,7 +52,7 @@ function generate(config) {
52
52
  sections.push('');
53
53
  sections.push('This loops through all tasks:');
54
54
  sections.push('1. Run `bootspring build task` (current task comes from `planning/BUILD_STATE.json`)');
55
- sections.push('2. Read that task\'s acceptance criteria in `planning/TASK_QUEUE.md`');
55
+ sections.push('2. Read that task\'s acceptance criteria in `planning/TODO.md`');
56
56
  sections.push('3. Implement and validate the task');
57
57
  sections.push('4. Run `bootspring build done` (auto-queues next)');
58
58
  sections.push('');
@@ -153,8 +153,7 @@ function generatePlanningAgents(_config) {
153
153
  sections.push('');
154
154
  sections.push('| File | Purpose |');
155
155
  sections.push('|------|---------|');
156
- sections.push('| `TASK_QUEUE.md` | Ordered task queue and acceptance criteria |');
157
- sections.push('| `TODO.md` | Full task checklist |');
156
+ sections.push('| `TODO.md` | **Source of truth** — task checklist with acceptance criteria |');
158
157
  sections.push('| `BUILD_STATE.json` | Build state and current task ID (do not edit) |');
159
158
  sections.push('| `CONTEXT.md` | Build context summary |');
160
159
  sections.push('');
@@ -162,7 +161,7 @@ function generatePlanningAgents(_config) {
162
161
  sections.push('## Workflow');
163
162
  sections.push('');
164
163
  sections.push('1. Run `bootspring build task` to identify the current task ID');
165
- sections.push('2. Read the matching section in `TASK_QUEUE.md` for acceptance criteria');
164
+ sections.push('2. Read the matching task in `TODO.md` for acceptance criteria');
166
165
  sections.push('3. Implement in the main codebase (not here)');
167
166
  sections.push('4. Ensure acceptance criteria are met');
168
167
  sections.push('5. Run `npm run lint && npm run test`');
@@ -93,7 +93,7 @@ Run \`bootspring build task\` to get the current \`in_progress\` task from \`BUI
93
93
  ## Workflow
94
94
 
95
95
  1. Run \`bootspring build task\` to identify the current task ID
96
- 2. Read the matching section in \`TASK_QUEUE.md\` for acceptance criteria
96
+ 2. Read the matching task in \`TODO.md\` for acceptance criteria
97
97
  3. Implement in the main codebase (not in this folder)
98
98
  4. Ensure acceptance criteria are met
99
99
  5. Run quality checks: \`npm run lint && npm run test\`
@@ -104,8 +104,7 @@ Run \`bootspring build task\` to get the current \`in_progress\` task from \`BUI
104
104
 
105
105
  | File | Purpose |
106
106
  |------|---------|
107
- | \`TASK_QUEUE.md\` | Ordered task queue and acceptance criteria |
108
- | \`TODO.md\` | Full task checklist |
107
+ | \`TODO.md\` | **Source of truth** — task checklist with acceptance criteria |
109
108
  | \`BUILD_STATE.json\` | Build state and current task ID (do not edit directly) |
110
109
  | \`CONTEXT.md\` | Build context summary |
111
110
 
@@ -254,7 +253,19 @@ function generateTodo(tasks, options = {}) {
254
253
 
255
254
  let content = `# ${projectName} - Build Todo
256
255
 
257
- > Checklist for autonomous build completion
256
+ > Single source of truth for autonomous build execution
257
+ > Updated: ${new Date().toISOString().split('T')[0]}
258
+
259
+ ---
260
+
261
+ ## Program Status
262
+
263
+ | Metric | Value |
264
+ |---|---:|
265
+ | Total Tasks | ${tasks.length} |
266
+ | Completed | ${tasks.filter(t => t.status === 'completed').length} |
267
+ | Remaining | ${tasks.filter(t => t.status === 'pending').length} |
268
+ | In Progress | ${tasks.filter(t => t.status === 'in_progress').length} |
258
269
 
259
270
  ---
260
271
 
@@ -265,17 +276,42 @@ function generateTodo(tasks, options = {}) {
265
276
  const total = phaseTasks.length;
266
277
  const percent = total > 0 ? Math.round((completed / total) * 100) : 0;
267
278
 
268
- content += `## ${formatPhaseName(phaseName)} (${percent}%)
279
+ content += `## ${formatPhaseName(phaseName)} (${completed}/${total} — ${percent}%)
269
280
 
270
281
  `;
271
282
 
272
283
  for (const task of phaseTasks) {
273
284
  const checkbox = task.status === 'completed' ? '[x]' : '[ ]';
274
- content += `- ${checkbox} ${task.title}\n`;
285
+ const statusTag = task.status === 'completed' ? ' (`completed`)' :
286
+ task.status === 'in_progress' ? ' (`in_progress`)' :
287
+ task.status === 'blocked' ? ' (`blocked`)' :
288
+ task.status === 'skipped' ? ' (`skipped`)' : '';
289
+ content += `- ${checkbox} \`${task.id}\` ${task.title}${statusTag}\n`;
290
+
291
+ // Source traceability
292
+ if (task.source) {
293
+ const sourceStr = task.sourceSection
294
+ ? `${task.source} (${task.sourceSection})`
295
+ : task.source;
296
+ content += ` - **Source:** ${sourceStr}\n`;
297
+ }
298
+
299
+ // Description
300
+ if (task.description) {
301
+ content += ` - **Description:** ${task.description}\n`;
302
+ }
303
+
304
+ // Complexity and dependencies
305
+ if (task.estimatedComplexity && task.estimatedComplexity !== 'medium') {
306
+ content += ` - **Complexity:** ${task.estimatedComplexity}\n`;
307
+ }
308
+ if (task.dependencies && task.dependencies.length > 0) {
309
+ content += ` - **Dependencies:** ${task.dependencies.join(', ')}\n`;
310
+ }
275
311
 
276
- // Add acceptance criteria as sub-items
312
+ // Acceptance criteria as sub-items (all of them — this is the source of truth)
277
313
  if (task.acceptanceCriteria && task.acceptanceCriteria.length > 0) {
278
- for (const criteria of task.acceptanceCriteria.slice(0, 3)) {
314
+ for (const criteria of task.acceptanceCriteria) {
279
315
  const subCheckbox = task.status === 'completed' ? '[x]' : '[ ]';
280
316
  content += ` - ${subCheckbox} ${criteria}\n`;
281
317
  }
@@ -285,18 +321,7 @@ function generateTodo(tasks, options = {}) {
285
321
  content += '\n---\n\n';
286
322
  }
287
323
 
288
- content += `## Progress Summary
289
-
290
- | Metric | Value |
291
- |--------|-------|
292
- | Total Tasks | ${tasks.length} |
293
- | Completed | ${tasks.filter(t => t.status === 'completed').length} |
294
- | Remaining | ${tasks.filter(t => t.status === 'pending').length} |
295
- | In Progress | ${tasks.filter(t => t.status === 'in_progress').length} |
296
-
297
- ---
298
-
299
- *Updated: ${new Date().toISOString()}*
324
+ content += `*Updated: ${new Date().toISOString()}*
300
325
  `;
301
326
 
302
327
  return content;
@@ -643,27 +668,28 @@ function updateFromState(projectRoot, state) {
643
668
 
644
669
  const tasks = state.implementationQueue;
645
670
  const planningDir = path.join(projectRoot, 'planning');
646
- const queuePath = path.join(planningDir, 'TASK_QUEUE.md');
647
- const existingQueue = fs.existsSync(queuePath)
648
- ? fs.readFileSync(queuePath, 'utf-8')
649
- : '';
650
- const existingVersionMatch = existingQueue.match(/\*\*Current Version:\*\*\s*([^\n]+)/i)
651
- || existingQueue.match(/>\s*Current Version:\s*([^\n]+)/i);
652
- const queueVersion = existingVersionMatch
653
- ? existingVersionMatch[1].trim()
654
- : DEFAULT_QUEUE_VERSION;
655
-
656
- // Regenerate TODO.md with current status
671
+
672
+ // Regenerate TODO.md — the single source of truth
657
673
  const todo = generateTodo(tasks, { projectName: state.projectName });
658
674
  fs.writeFileSync(path.join(planningDir, 'TODO.md'), todo);
659
675
 
660
- // Regenerate TASK_QUEUE.md
661
- const queue = generateImplementationQueue(tasks, {
662
- projectName: state.projectName,
663
- lastUpdated: new Date().toISOString().split('T')[0],
664
- queueVersion
665
- });
666
- fs.writeFileSync(queuePath, queue);
676
+ // Legacy: also regenerate TASK_QUEUE.md if it already exists (backward compat)
677
+ const queuePath = path.join(planningDir, 'TASK_QUEUE.md');
678
+ if (fs.existsSync(queuePath)) {
679
+ const existingQueue = fs.readFileSync(queuePath, 'utf-8');
680
+ const existingVersionMatch = existingQueue.match(/\*\*Current Version:\*\*\s*([^\n]+)/i)
681
+ || existingQueue.match(/>\s*Current Version:\s*([^\n]+)/i);
682
+ const queueVersion = existingVersionMatch
683
+ ? existingVersionMatch[1].trim()
684
+ : DEFAULT_QUEUE_VERSION;
685
+
686
+ const queue = generateImplementationQueue(tasks, {
687
+ projectName: state.projectName,
688
+ lastUpdated: new Date().toISOString().split('T')[0],
689
+ queueVersion
690
+ });
691
+ fs.writeFileSync(queuePath, queue);
692
+ }
667
693
  }
668
694
 
669
695
  module.exports = {
@@ -33,7 +33,7 @@ function generate(config) {
33
33
  sections.push('### Before Writing ANY Code');
34
34
  sections.push('1. Read `planning/MASTER_PLAN.md` - Overall vision and phases');
35
35
  sections.push('2. Run `bootspring build task` - Current task ID comes from `planning/BUILD_STATE.json`');
36
- sections.push('3. Read `planning/TASK_QUEUE.md` - Acceptance criteria for that task ID');
36
+ sections.push('3. Read `planning/TODO.md` - Find the task ID and its acceptance criteria');
37
37
  sections.push('4. Read `planning/CONTEXT.md` - Current build context');
38
38
  sections.push('5. Understand the acceptance criteria before implementing');
39
39
  sections.push('');
@@ -53,9 +53,8 @@ function generate(config) {
53
53
  sections.push('| File | Purpose | When to Read |');
54
54
  sections.push('|------|---------|--------------|');
55
55
  sections.push('| `MASTER_PLAN.md` | Overall vision, phases, success criteria | **Always read first** |');
56
- sections.push('| `TASK_QUEUE.md` | All tasks with acceptance criteria (look up current task ID here) | **Before each task** |');
56
+ sections.push('| `TODO.md` | **Source of truth** — all tasks with acceptance criteria | **Before each task** |');
57
57
  sections.push('| `CONTEXT.md` | Current build context and decisions | Before each task |');
58
- sections.push('| `TODO.md` | Checkbox task list | Update after completing |');
59
58
  sections.push('| `BUILD_STATE.json` | Machine state - source of current task ID | Reference only |');
60
59
  sections.push('');
61
60
  sections.push('---');
@@ -231,7 +230,7 @@ function generate(config) {
231
230
  sections.push('│ ↓ │');
232
231
  sections.push('│ bootspring build task → get in_progress task ID │');
233
232
  sections.push('│ ↓ │');
234
- sections.push('│ Read planning/TASK_QUEUE.md for acceptance criteria │');
233
+ sections.push('│ Read planning/TODO.md for acceptance criteria │');
235
234
  sections.push('│ ↓ │');
236
235
  sections.push('│ Implement task (meet acceptance criteria) │');
237
236
  sections.push('│ ↓ │');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@girardmedia/bootspring",
3
- "version": "2.1.0",
3
+ "version": "2.1.2",
4
4
  "description": "Thin client for Bootspring cloud MCP, hosted agents, and paywalled workflow intelligence",
5
5
  "keywords": [
6
6
  "ai",