@girardmedia/bootspring 2.3.7 → 2.5.0
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/dist/cli/index.cjs +991 -123
- package/dist/core/index.d.ts +86 -3
- package/dist/core.js +9 -6
- package/dist/mcp/index.d.ts +1 -1
- package/dist/mcp-server.js +9 -6
- package/generators/templates/build-planning.template.js +15 -11
- package/package.json +9 -6
- package/scripts/postinstall.js +54 -21
package/dist/cli/index.cjs
CHANGED
|
@@ -3214,13 +3214,15 @@ function isMCPContext() {
|
|
|
3214
3214
|
function deepClone(obj) {
|
|
3215
3215
|
return JSON.parse(JSON.stringify(obj));
|
|
3216
3216
|
}
|
|
3217
|
-
var import_fs, import_path, REDACTED, SENSITIVE_KEY_PATTERN, COLORS, print;
|
|
3217
|
+
var import_fs, import_path, BOOTSPRING_VERSION, BOOTSPRING_PACKAGE_NAME, REDACTED, SENSITIVE_KEY_PATTERN, COLORS, print;
|
|
3218
3218
|
var init_dist = __esm({
|
|
3219
3219
|
"../../packages/shared/dist/index.mjs"() {
|
|
3220
3220
|
"use strict";
|
|
3221
3221
|
init_cjs_shims();
|
|
3222
3222
|
import_fs = __toESM(require("fs"), 1);
|
|
3223
3223
|
import_path = __toESM(require("path"), 1);
|
|
3224
|
+
BOOTSPRING_VERSION = "2.5.0";
|
|
3225
|
+
BOOTSPRING_PACKAGE_NAME = "@girardmedia/bootspring";
|
|
3224
3226
|
REDACTED = "[REDACTED]";
|
|
3225
3227
|
SENSITIVE_KEY_PATTERN = /(?:^|[_-])(api[_-]?key|token|refresh[_-]?token|authorization|x[_-]?api[_-]?key|project[_-]?id)$/i;
|
|
3226
3228
|
COLORS = {
|
|
@@ -3269,6 +3271,8 @@ __export(dist_exports, {
|
|
|
3269
3271
|
API_BASE: () => API_BASE,
|
|
3270
3272
|
API_VERSION: () => API_VERSION,
|
|
3271
3273
|
BOOTSPRING_DIR: () => BOOTSPRING_DIR,
|
|
3274
|
+
BOOTSPRING_PACKAGE_NAME: () => BOOTSPRING_PACKAGE_NAME,
|
|
3275
|
+
BOOTSPRING_VERSION: () => BOOTSPRING_VERSION,
|
|
3272
3276
|
COLORS: () => COLORS,
|
|
3273
3277
|
CONFIG_FILE: () => CONFIG_FILE,
|
|
3274
3278
|
CREDENTIALS_FILE: () => CREDENTIALS_FILE,
|
|
@@ -3333,7 +3337,9 @@ __export(dist_exports, {
|
|
|
3333
3337
|
login: () => login,
|
|
3334
3338
|
loginWithApiKey: () => loginWithApiKey,
|
|
3335
3339
|
logout: () => logout,
|
|
3340
|
+
maybeAutoUploadTelemetry: () => maybeAutoUploadTelemetry,
|
|
3336
3341
|
pollDeviceToken: () => pollDeviceToken,
|
|
3342
|
+
presence: () => presence_exports,
|
|
3337
3343
|
print: () => print,
|
|
3338
3344
|
readFile: () => readFile,
|
|
3339
3345
|
readNearestProjectConfig: () => readNearestProjectConfig,
|
|
@@ -3349,6 +3355,8 @@ __export(dist_exports, {
|
|
|
3349
3355
|
saveProjectScopedSession: () => saveProjectScopedSession,
|
|
3350
3356
|
saveSession: () => saveSession,
|
|
3351
3357
|
selfHeal: () => self_heal_exports,
|
|
3358
|
+
sendHealthReport: () => sendHealthReport,
|
|
3359
|
+
sendHeartbeat: () => sendHeartbeat,
|
|
3352
3360
|
session: () => session_exports,
|
|
3353
3361
|
setAuthFailureHandler: () => setAuthFailureHandler,
|
|
3354
3362
|
setCurrentProject: () => setCurrentProject,
|
|
@@ -4363,7 +4371,89 @@ function runDiagnostics(autoFix = false) {
|
|
|
4363
4371
|
}
|
|
4364
4372
|
return results;
|
|
4365
4373
|
}
|
|
4366
|
-
|
|
4374
|
+
function getAuthHeaders() {
|
|
4375
|
+
const headers = {
|
|
4376
|
+
"content-type": "application/json"
|
|
4377
|
+
};
|
|
4378
|
+
const apiKey = getApiKey();
|
|
4379
|
+
if (apiKey) {
|
|
4380
|
+
headers["x-api-key"] = apiKey;
|
|
4381
|
+
} else {
|
|
4382
|
+
const token = getToken();
|
|
4383
|
+
if (token) {
|
|
4384
|
+
headers["authorization"] = `Bearer ${token}`;
|
|
4385
|
+
} else {
|
|
4386
|
+
return null;
|
|
4387
|
+
}
|
|
4388
|
+
}
|
|
4389
|
+
return headers;
|
|
4390
|
+
}
|
|
4391
|
+
function sendHeartbeat(options) {
|
|
4392
|
+
if (!isAuthenticated()) return;
|
|
4393
|
+
const project = getEffectiveProject();
|
|
4394
|
+
if (!project?.id) return;
|
|
4395
|
+
const now = Date.now();
|
|
4396
|
+
if (now - _lastHeartbeatAt < HEARTBEAT_DEBOUNCE_MS) return;
|
|
4397
|
+
_lastHeartbeatAt = now;
|
|
4398
|
+
const headers = getAuthHeaders();
|
|
4399
|
+
if (!headers) return;
|
|
4400
|
+
headers["x-project-id"] = project.id;
|
|
4401
|
+
let deviceId;
|
|
4402
|
+
try {
|
|
4403
|
+
deviceId = getDeviceId();
|
|
4404
|
+
} catch {
|
|
4405
|
+
deviceId = "unknown";
|
|
4406
|
+
}
|
|
4407
|
+
const body = JSON.stringify({
|
|
4408
|
+
projectId: project.id,
|
|
4409
|
+
status: options?.status || "online",
|
|
4410
|
+
activity: options?.activity || "cli",
|
|
4411
|
+
deviceId
|
|
4412
|
+
});
|
|
4413
|
+
const controller = new AbortController();
|
|
4414
|
+
const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|
|
4415
|
+
fetch(`${DASHBOARD_BASE}/api/v1/presence/heartbeat`, {
|
|
4416
|
+
method: "POST",
|
|
4417
|
+
headers,
|
|
4418
|
+
body,
|
|
4419
|
+
signal: controller.signal
|
|
4420
|
+
}).catch(() => {
|
|
4421
|
+
}).finally(() => clearTimeout(timer));
|
|
4422
|
+
}
|
|
4423
|
+
async function sendHealthReport(payload) {
|
|
4424
|
+
if (!isAuthenticated()) return;
|
|
4425
|
+
const project = getEffectiveProject();
|
|
4426
|
+
if (!project?.id) return;
|
|
4427
|
+
const headers = getAuthHeaders();
|
|
4428
|
+
if (!headers) return;
|
|
4429
|
+
headers["x-project-id"] = project.id;
|
|
4430
|
+
const controller = new AbortController();
|
|
4431
|
+
const timer = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|
|
4432
|
+
try {
|
|
4433
|
+
await fetch(`${DASHBOARD_BASE}/api/v1/health/report`, {
|
|
4434
|
+
method: "POST",
|
|
4435
|
+
headers,
|
|
4436
|
+
body: JSON.stringify({
|
|
4437
|
+
projectId: project.id,
|
|
4438
|
+
score: payload.score,
|
|
4439
|
+
grade: payload.grade,
|
|
4440
|
+
data: payload.data
|
|
4441
|
+
}),
|
|
4442
|
+
signal: controller.signal
|
|
4443
|
+
});
|
|
4444
|
+
} catch {
|
|
4445
|
+
} finally {
|
|
4446
|
+
clearTimeout(timer);
|
|
4447
|
+
}
|
|
4448
|
+
}
|
|
4449
|
+
function maybeAutoUploadTelemetry() {
|
|
4450
|
+
if (!isAuthenticated()) return;
|
|
4451
|
+
const now = Date.now();
|
|
4452
|
+
const ONE_HOUR = 60 * 60 * 1e3;
|
|
4453
|
+
if (now - _lastTelemetryUploadAt < ONE_HOUR) return;
|
|
4454
|
+
_lastTelemetryUploadAt = now;
|
|
4455
|
+
}
|
|
4456
|
+
var import_fs2, import_path2, import_os, import_crypto, import_https, import_http, import_fs3, import_path3, import_fs4, import_path4, import_os2, import_child_process, __defProp2, __export2, auth_exports, BOOTSPRING_DIR, CREDENTIALS_FILE, CONFIG_FILE, DEVICE_FILE, _credentialCache, _credentialDecryptFailed, getEncryptionKey, api_client_exports, API_BASE, API_VERSION, _onAuthFailure, cache, CACHE_TTL, session_exports, SESSION_FILE, LOCAL_CONFIG_NAME, PROJECT_SCOPE_MARKERS, LIMITS, PATTERNS, SHELL_DANGEROUS_CHARS, self_heal_exports, healers, presence_exports, DASHBOARD_BASE, HEARTBEAT_DEBOUNCE_MS, REQUEST_TIMEOUT_MS, _lastHeartbeatAt, _lastTelemetryUploadAt;
|
|
4367
4457
|
var init_dist2 = __esm({
|
|
4368
4458
|
"../../packages/core/dist/index.mjs"() {
|
|
4369
4459
|
"use strict";
|
|
@@ -4558,6 +4648,17 @@ var init_dist2 = __esm({
|
|
|
4558
4648
|
return { issue: classifyError({ message: "cannot find module" }), healed: false };
|
|
4559
4649
|
}
|
|
4560
4650
|
};
|
|
4651
|
+
presence_exports = {};
|
|
4652
|
+
__export2(presence_exports, {
|
|
4653
|
+
maybeAutoUploadTelemetry: () => maybeAutoUploadTelemetry,
|
|
4654
|
+
sendHealthReport: () => sendHealthReport,
|
|
4655
|
+
sendHeartbeat: () => sendHeartbeat
|
|
4656
|
+
});
|
|
4657
|
+
DASHBOARD_BASE = process.env.BOOTSPRING_SITE_URL || "https://www.bootspring.com";
|
|
4658
|
+
HEARTBEAT_DEBOUNCE_MS = 2e4;
|
|
4659
|
+
REQUEST_TIMEOUT_MS = 5e3;
|
|
4660
|
+
_lastHeartbeatAt = 0;
|
|
4661
|
+
_lastTelemetryUploadAt = 0;
|
|
4561
4662
|
}
|
|
4562
4663
|
});
|
|
4563
4664
|
|
|
@@ -4584,6 +4685,7 @@ var {
|
|
|
4584
4685
|
|
|
4585
4686
|
// src/index.ts
|
|
4586
4687
|
init_dist2();
|
|
4688
|
+
init_dist();
|
|
4587
4689
|
|
|
4588
4690
|
// src/middleware.ts
|
|
4589
4691
|
init_cjs_shims();
|
|
@@ -4596,6 +4698,9 @@ function wrapCommand(cmd) {
|
|
|
4596
4698
|
if (listeners) {
|
|
4597
4699
|
const originalAction = listeners;
|
|
4598
4700
|
cmd._actionHandler = async (...args) => {
|
|
4701
|
+
const commandName = cmd.name();
|
|
4702
|
+
presence_exports.sendHeartbeat({ activity: `bootspring ${commandName}` });
|
|
4703
|
+
presence_exports.maybeAutoUploadTelemetry();
|
|
4599
4704
|
try {
|
|
4600
4705
|
return await originalAction(...args);
|
|
4601
4706
|
} catch (err) {
|
|
@@ -7598,6 +7703,367 @@ init_cjs_shims();
|
|
|
7598
7703
|
var fs8 = __toESM(require("fs"), 1);
|
|
7599
7704
|
var path9 = __toESM(require("path"), 1);
|
|
7600
7705
|
init_dist();
|
|
7706
|
+
function getPlanningSurfaceStatus() {
|
|
7707
|
+
const planningDir = path9.join(process.cwd(), "planning");
|
|
7708
|
+
return {
|
|
7709
|
+
hasTodo: fs8.existsSync(path9.join(planningDir, "TODO.md")),
|
|
7710
|
+
hasQueue: fs8.existsSync(path9.join(planningDir, "TASK_QUEUE.md"))
|
|
7711
|
+
};
|
|
7712
|
+
}
|
|
7713
|
+
function printLegacyQueueWarningIfNeeded() {
|
|
7714
|
+
const { hasTodo, hasQueue } = getPlanningSurfaceStatus();
|
|
7715
|
+
if (hasTodo && hasQueue) {
|
|
7716
|
+
print.warning("Legacy planning conflict detected: planning/TODO.md is canonical, but planning/TASK_QUEUE.md is still present.");
|
|
7717
|
+
print.info("Run `bootspring build migrate-todo` to archive the lingering queue file.");
|
|
7718
|
+
}
|
|
7719
|
+
}
|
|
7720
|
+
function formatPlanningSourceLabel(source) {
|
|
7721
|
+
if (source === "TODO.md") return "planning/TODO.md";
|
|
7722
|
+
if (source === "TASK_QUEUE.md") return "planning/TASK_QUEUE.md (legacy fallback)";
|
|
7723
|
+
if (source === "BUILD_STATE.json") return "planning/BUILD_STATE.json";
|
|
7724
|
+
return "not detected";
|
|
7725
|
+
}
|
|
7726
|
+
function normalizeStatus(value) {
|
|
7727
|
+
const normalized = String(value || "pending").trim().toLowerCase().replace(/[\s-]+/g, "_");
|
|
7728
|
+
if (normalized === "done" || normalized === "complete" || normalized === "completed") {
|
|
7729
|
+
return "completed";
|
|
7730
|
+
}
|
|
7731
|
+
if (normalized === "inprogress" || normalized === "in_progress") {
|
|
7732
|
+
return "in_progress";
|
|
7733
|
+
}
|
|
7734
|
+
if (normalized === "blocked" || normalized === "skipped") {
|
|
7735
|
+
return normalized;
|
|
7736
|
+
}
|
|
7737
|
+
return normalized === "pending" ? "pending" : "pending";
|
|
7738
|
+
}
|
|
7739
|
+
function normalizePhase(value) {
|
|
7740
|
+
const normalized = String(value || "mvp").trim().toLowerCase().replace(/[\s-]+/g, "_");
|
|
7741
|
+
return ["foundation", "mvp", "launch"].includes(normalized) ? normalized : "mvp";
|
|
7742
|
+
}
|
|
7743
|
+
function formatPhaseName(phase) {
|
|
7744
|
+
if (!phase) return "Unknown";
|
|
7745
|
+
if (phase.toLowerCase() === "mvp") return "MVP";
|
|
7746
|
+
const normalized = normalizePhase(phase);
|
|
7747
|
+
return normalized.charAt(0).toUpperCase() + normalized.slice(1);
|
|
7748
|
+
}
|
|
7749
|
+
function parseDependencyIds(rawText) {
|
|
7750
|
+
const matches = rawText.match(/[a-z0-9][a-z0-9_-]*/gi) || [];
|
|
7751
|
+
const ids = [];
|
|
7752
|
+
const seen = /* @__PURE__ */ new Set();
|
|
7753
|
+
for (const match of matches) {
|
|
7754
|
+
const token = match.trim();
|
|
7755
|
+
if (!token.includes("-") || !/\d/.test(token) || seen.has(token)) continue;
|
|
7756
|
+
seen.add(token);
|
|
7757
|
+
ids.push(token);
|
|
7758
|
+
}
|
|
7759
|
+
return ids;
|
|
7760
|
+
}
|
|
7761
|
+
function parseSourceMetadata(rawSource) {
|
|
7762
|
+
const trimmed = rawSource.trim();
|
|
7763
|
+
const match = trimmed.match(/^(.+?)\s+\((.+)\)$/);
|
|
7764
|
+
if (match?.[1] && match?.[2]) {
|
|
7765
|
+
return {
|
|
7766
|
+
source: match[1].trim(),
|
|
7767
|
+
sourceSection: match[2].trim()
|
|
7768
|
+
};
|
|
7769
|
+
}
|
|
7770
|
+
return { source: trimmed };
|
|
7771
|
+
}
|
|
7772
|
+
function parseTodoTasks(content) {
|
|
7773
|
+
const tasks = [];
|
|
7774
|
+
const lines = content.split("\n");
|
|
7775
|
+
let currentPhase = "mvp";
|
|
7776
|
+
let currentTask = null;
|
|
7777
|
+
const pushTask = () => {
|
|
7778
|
+
if (!currentTask) return;
|
|
7779
|
+
tasks.push(currentTask);
|
|
7780
|
+
currentTask = null;
|
|
7781
|
+
};
|
|
7782
|
+
for (const line of lines) {
|
|
7783
|
+
const phaseMatch = line.match(/^##\s+(Foundation|MVP|Launch)\b/i);
|
|
7784
|
+
if (phaseMatch?.[1]) {
|
|
7785
|
+
pushTask();
|
|
7786
|
+
currentPhase = normalizePhase(phaseMatch[1]);
|
|
7787
|
+
continue;
|
|
7788
|
+
}
|
|
7789
|
+
const taskMatch = line.match(/^-\s+\[([ xX])\]\s+`([^`]+)`\s+(.+?)(?:\s+\(`?([\w-]+)`?\))?\s*$/);
|
|
7790
|
+
if (taskMatch?.[2] && taskMatch?.[3]) {
|
|
7791
|
+
pushTask();
|
|
7792
|
+
const checked = taskMatch[1]?.toLowerCase() === "x";
|
|
7793
|
+
currentTask = {
|
|
7794
|
+
id: taskMatch[2].trim(),
|
|
7795
|
+
title: taskMatch[3].trim(),
|
|
7796
|
+
phase: currentPhase,
|
|
7797
|
+
status: normalizeStatus(taskMatch[4] || (checked ? "completed" : "pending")),
|
|
7798
|
+
acceptanceCriteria: [],
|
|
7799
|
+
dependencies: []
|
|
7800
|
+
};
|
|
7801
|
+
continue;
|
|
7802
|
+
}
|
|
7803
|
+
if (!currentTask) {
|
|
7804
|
+
continue;
|
|
7805
|
+
}
|
|
7806
|
+
const sourceMatch = line.match(/^\s{2,}-\s+\*\*Source:\*\*\s+(.+)\s*$/);
|
|
7807
|
+
if (sourceMatch?.[1]) {
|
|
7808
|
+
const sourceMetadata = parseSourceMetadata(sourceMatch[1]);
|
|
7809
|
+
currentTask.source = sourceMetadata.source;
|
|
7810
|
+
currentTask.sourceSection = sourceMetadata.sourceSection;
|
|
7811
|
+
continue;
|
|
7812
|
+
}
|
|
7813
|
+
const descriptionMatch = line.match(/^\s{2,}-\s+\*\*Description:\*\*\s+(.+)\s*$/);
|
|
7814
|
+
if (descriptionMatch?.[1]) {
|
|
7815
|
+
currentTask.description = descriptionMatch[1].trim();
|
|
7816
|
+
continue;
|
|
7817
|
+
}
|
|
7818
|
+
const complexityMatch = line.match(/^\s{2,}-\s+\*\*Complexity:\*\*\s+(.+)\s*$/);
|
|
7819
|
+
if (complexityMatch?.[1]) {
|
|
7820
|
+
currentTask.complexity = complexityMatch[1].trim().toLowerCase();
|
|
7821
|
+
continue;
|
|
7822
|
+
}
|
|
7823
|
+
const dependenciesMatch = line.match(/^\s{2,}-\s+\*\*Dependencies:\*\*\s+(.+)\s*$/);
|
|
7824
|
+
if (dependenciesMatch?.[1]) {
|
|
7825
|
+
currentTask.dependencies = parseDependencyIds(dependenciesMatch[1]);
|
|
7826
|
+
continue;
|
|
7827
|
+
}
|
|
7828
|
+
const criteriaMatch = line.match(/^\s{2,}-\s+\[[ xX]\]\s+(.+)\s*$/);
|
|
7829
|
+
if (criteriaMatch?.[1]) {
|
|
7830
|
+
currentTask.acceptanceCriteria?.push(criteriaMatch[1].trim());
|
|
7831
|
+
}
|
|
7832
|
+
}
|
|
7833
|
+
pushTask();
|
|
7834
|
+
return tasks;
|
|
7835
|
+
}
|
|
7836
|
+
function parseQueueTasks(content) {
|
|
7837
|
+
const tasks = [];
|
|
7838
|
+
const tableMatches = content.matchAll(/^\|\s*([^|]+)\s*\|\s*([a-z0-9][a-z0-9_-]*)\s*\|\s*([^|]+)\s*\|\s*([^|]+)\s*\|\s*([^|]+)\s*\|\s*([^|]+)\s*\|$/gmi);
|
|
7839
|
+
for (const match of tableMatches) {
|
|
7840
|
+
const id = match[2]?.trim() ?? "";
|
|
7841
|
+
const title = match[3]?.trim() ?? "";
|
|
7842
|
+
if (id.toLowerCase() === "id" || title.toLowerCase() === "task" || id.includes("---") || title.includes("---")) {
|
|
7843
|
+
continue;
|
|
7844
|
+
}
|
|
7845
|
+
tasks.push({
|
|
7846
|
+
id,
|
|
7847
|
+
title,
|
|
7848
|
+
phase: normalizePhase(match[4]?.trim()),
|
|
7849
|
+
complexity: String(match[5] || "medium").trim().toLowerCase(),
|
|
7850
|
+
status: normalizeStatus(match[6]?.trim()),
|
|
7851
|
+
acceptanceCriteria: [],
|
|
7852
|
+
dependencies: []
|
|
7853
|
+
});
|
|
7854
|
+
}
|
|
7855
|
+
const detailMatches = content.matchAll(/^###\s+([a-z0-9][a-z0-9_-]*):\s*(.+)$/gmi);
|
|
7856
|
+
for (const match of detailMatches) {
|
|
7857
|
+
const id = match[1]?.trim() ?? "";
|
|
7858
|
+
const sectionStart = (match.index ?? 0) + match[0].length;
|
|
7859
|
+
const nextSectionOffset = content.slice(sectionStart).search(/^###\s+/m);
|
|
7860
|
+
const sectionContent = nextSectionOffset === -1 ? content.slice(sectionStart) : content.slice(sectionStart, sectionStart + nextSectionOffset);
|
|
7861
|
+
const existing = tasks.find((task) => task.id === id);
|
|
7862
|
+
const acceptanceCriteria = [];
|
|
7863
|
+
const criteriaBlock = sectionContent.match(/\*{0,2}Acceptance\s+Criteria:\*{0,2}\s*([\s\S]*?)(?=\n\*{0,2}[A-Z]|\n---|\n###|$)/i);
|
|
7864
|
+
if (criteriaBlock?.[1]) {
|
|
7865
|
+
for (const criteriaMatch of criteriaBlock[1].matchAll(/^\s*[-*]\s+(?:\[[ xX]\]\s*)?(.+)$/gm)) {
|
|
7866
|
+
if (criteriaMatch[1]) {
|
|
7867
|
+
acceptanceCriteria.push(criteriaMatch[1].trim());
|
|
7868
|
+
}
|
|
7869
|
+
}
|
|
7870
|
+
}
|
|
7871
|
+
const dependencies = (() => {
|
|
7872
|
+
const dependencyMatch = sectionContent.match(/\*{0,2}(?:Dependencies|Depends on):\*{0,2}\s*([\s\S]*?)(?=\n\*{0,2}[A-Z]|\n---|\n###|$)/i);
|
|
7873
|
+
return dependencyMatch?.[1] ? parseDependencyIds(dependencyMatch[1]) : [];
|
|
7874
|
+
})();
|
|
7875
|
+
if (existing) {
|
|
7876
|
+
existing.acceptanceCriteria = acceptanceCriteria;
|
|
7877
|
+
existing.dependencies = dependencies;
|
|
7878
|
+
}
|
|
7879
|
+
}
|
|
7880
|
+
return tasks;
|
|
7881
|
+
}
|
|
7882
|
+
function loadPlanningTasks() {
|
|
7883
|
+
const todoFile = path9.join(process.cwd(), "planning", "TODO.md");
|
|
7884
|
+
const queueFile = path9.join(process.cwd(), "planning", "TASK_QUEUE.md");
|
|
7885
|
+
if (fs8.existsSync(todoFile)) {
|
|
7886
|
+
try {
|
|
7887
|
+
const tasks = parseTodoTasks(fs8.readFileSync(todoFile, "utf-8"));
|
|
7888
|
+
if (tasks.length > 0) {
|
|
7889
|
+
return { tasks, source: "TODO.md" };
|
|
7890
|
+
}
|
|
7891
|
+
} catch {
|
|
7892
|
+
}
|
|
7893
|
+
}
|
|
7894
|
+
if (fs8.existsSync(queueFile)) {
|
|
7895
|
+
try {
|
|
7896
|
+
const tasks = parseQueueTasks(fs8.readFileSync(queueFile, "utf-8"));
|
|
7897
|
+
if (tasks.length > 0) {
|
|
7898
|
+
return { tasks, source: "TASK_QUEUE.md" };
|
|
7899
|
+
}
|
|
7900
|
+
} catch {
|
|
7901
|
+
}
|
|
7902
|
+
}
|
|
7903
|
+
return { tasks: [], source: null };
|
|
7904
|
+
}
|
|
7905
|
+
function renderTodo(tasks, projectName) {
|
|
7906
|
+
const byPhase = /* @__PURE__ */ new Map();
|
|
7907
|
+
for (const task of tasks) {
|
|
7908
|
+
const phase = normalizePhase(task.phase);
|
|
7909
|
+
const phaseTasks = byPhase.get(phase) ?? [];
|
|
7910
|
+
phaseTasks.push({ ...task, phase });
|
|
7911
|
+
byPhase.set(phase, phaseTasks);
|
|
7912
|
+
}
|
|
7913
|
+
const orderedPhases = ["foundation", "mvp", "launch", ...Array.from(byPhase.keys()).filter((phase) => !["foundation", "mvp", "launch"].includes(phase))];
|
|
7914
|
+
const total = tasks.length;
|
|
7915
|
+
const completed = tasks.filter((task) => normalizeStatus(task.status) === "completed").length;
|
|
7916
|
+
const pending = tasks.filter((task) => normalizeStatus(task.status) === "pending").length;
|
|
7917
|
+
const inProgress = tasks.filter((task) => normalizeStatus(task.status) === "in_progress").length;
|
|
7918
|
+
let content = `# ${projectName} - Build Todo
|
|
7919
|
+
|
|
7920
|
+
> Single source of truth for autonomous build execution
|
|
7921
|
+
> Updated: ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}
|
|
7922
|
+
|
|
7923
|
+
---
|
|
7924
|
+
|
|
7925
|
+
## Program Status
|
|
7926
|
+
|
|
7927
|
+
| Metric | Value |
|
|
7928
|
+
|---|---:|
|
|
7929
|
+
| Total Tasks | ${total} |
|
|
7930
|
+
| Completed | ${completed} |
|
|
7931
|
+
| Remaining | ${pending} |
|
|
7932
|
+
| In Progress | ${inProgress} |
|
|
7933
|
+
|
|
7934
|
+
---
|
|
7935
|
+
|
|
7936
|
+
`;
|
|
7937
|
+
for (const phase of orderedPhases) {
|
|
7938
|
+
const phaseTasks = byPhase.get(phase);
|
|
7939
|
+
if (!phaseTasks || phaseTasks.length === 0) continue;
|
|
7940
|
+
const phaseCompleted = phaseTasks.filter((task) => normalizeStatus(task.status) === "completed").length;
|
|
7941
|
+
const percent = phaseTasks.length > 0 ? Math.round(phaseCompleted / phaseTasks.length * 100) : 0;
|
|
7942
|
+
content += `## ${formatPhaseName(phase)} (${phaseCompleted}/${phaseTasks.length} \u2014 ${percent}%)
|
|
7943
|
+
|
|
7944
|
+
`;
|
|
7945
|
+
for (const task of phaseTasks) {
|
|
7946
|
+
const status = normalizeStatus(task.status);
|
|
7947
|
+
const checkbox = status === "completed" ? "[x]" : "[ ]";
|
|
7948
|
+
const statusTag = status === "pending" ? "" : ` (\`${status}\`)`;
|
|
7949
|
+
content += `- ${checkbox} \`${task.id}\` ${task.title}${statusTag}
|
|
7950
|
+
`;
|
|
7951
|
+
if (task.source) {
|
|
7952
|
+
const sourceLabel = task.sourceSection ? `${task.source} (${task.sourceSection})` : task.source;
|
|
7953
|
+
content += ` - **Source:** ${sourceLabel}
|
|
7954
|
+
`;
|
|
7955
|
+
}
|
|
7956
|
+
if (task.description) {
|
|
7957
|
+
content += ` - **Description:** ${task.description}
|
|
7958
|
+
`;
|
|
7959
|
+
}
|
|
7960
|
+
if (task.complexity && task.complexity !== "medium") {
|
|
7961
|
+
content += ` - **Complexity:** ${task.complexity}
|
|
7962
|
+
`;
|
|
7963
|
+
}
|
|
7964
|
+
if (task.dependencies && task.dependencies.length > 0) {
|
|
7965
|
+
content += ` - **Dependencies:** ${task.dependencies.join(", ")}
|
|
7966
|
+
`;
|
|
7967
|
+
}
|
|
7968
|
+
if (task.acceptanceCriteria && task.acceptanceCriteria.length > 0) {
|
|
7969
|
+
const criteriaCheckbox = status === "completed" ? "[x]" : "[ ]";
|
|
7970
|
+
for (const criterion of task.acceptanceCriteria) {
|
|
7971
|
+
content += ` - ${criteriaCheckbox} ${criterion}
|
|
7972
|
+
`;
|
|
7973
|
+
}
|
|
7974
|
+
}
|
|
7975
|
+
}
|
|
7976
|
+
content += "\n---\n\n";
|
|
7977
|
+
}
|
|
7978
|
+
content += `*Updated: ${(/* @__PURE__ */ new Date()).toISOString()}*
|
|
7979
|
+
`;
|
|
7980
|
+
return content;
|
|
7981
|
+
}
|
|
7982
|
+
function getCurrentPhase(queue) {
|
|
7983
|
+
const active = queue?.find((task) => normalizeStatus(task.status) === "in_progress") ?? queue?.find((task) => normalizeStatus(task.status) === "pending");
|
|
7984
|
+
return active?.phase ? formatPhaseName(active.phase) : "N/A";
|
|
7985
|
+
}
|
|
7986
|
+
function getTaskEntriesFromState(state) {
|
|
7987
|
+
return (state?.implementationQueue ?? []).map((task) => ({
|
|
7988
|
+
id: task.id,
|
|
7989
|
+
title: task.title,
|
|
7990
|
+
phase: task.phase,
|
|
7991
|
+
complexity: task.estimatedComplexity,
|
|
7992
|
+
status: normalizeStatus(task.status),
|
|
7993
|
+
description: task.description,
|
|
7994
|
+
source: task.source,
|
|
7995
|
+
sourceSection: task.sourceSection,
|
|
7996
|
+
acceptanceCriteria: task.acceptanceCriteria ?? [],
|
|
7997
|
+
dependencies: task.dependencies ?? []
|
|
7998
|
+
}));
|
|
7999
|
+
}
|
|
8000
|
+
function extractSupplementaryContent(queueContent) {
|
|
8001
|
+
const lines = queueContent.split("\n");
|
|
8002
|
+
const sections = [];
|
|
8003
|
+
let currentSection = null;
|
|
8004
|
+
let inTaskTable = false;
|
|
8005
|
+
let inTaskDetail = false;
|
|
8006
|
+
for (const rawLine of lines) {
|
|
8007
|
+
const line = rawLine ?? "";
|
|
8008
|
+
if (/^\|\s*(Priority|Position|#)\s*\|/i.test(line)) {
|
|
8009
|
+
inTaskTable = true;
|
|
8010
|
+
continue;
|
|
8011
|
+
}
|
|
8012
|
+
if (inTaskTable && /^\|/.test(line)) continue;
|
|
8013
|
+
if (inTaskTable && !/^\|/.test(line)) inTaskTable = false;
|
|
8014
|
+
if (/^###\s+([a-z0-9][a-z0-9_-]*)\s*:/.test(line)) {
|
|
8015
|
+
inTaskDetail = true;
|
|
8016
|
+
continue;
|
|
8017
|
+
}
|
|
8018
|
+
if (inTaskDetail && /^#{2,3}\s+/.test(line) && !/^###\s+([a-z0-9][a-z0-9_-]*)\s*:/.test(line)) {
|
|
8019
|
+
inTaskDetail = false;
|
|
8020
|
+
}
|
|
8021
|
+
if (inTaskDetail) continue;
|
|
8022
|
+
if (/^#\s+.*(?:Implementation Queue|Task Queue)\b/i.test(line)) continue;
|
|
8023
|
+
if (/^>\s*(Ordered task queue|Last Updated|Current Version)/i.test(line)) continue;
|
|
8024
|
+
if (/^##\s+(Queue Status|Task Details)\b/i.test(line)) {
|
|
8025
|
+
if (/^##\s+Queue Status\b/i.test(line)) inTaskTable = true;
|
|
8026
|
+
if (/^##\s+Task Details\b/i.test(line)) inTaskDetail = true;
|
|
8027
|
+
continue;
|
|
8028
|
+
}
|
|
8029
|
+
if (/^\*Generated by/i.test(line)) continue;
|
|
8030
|
+
if (/^---$/.test(line.trim())) continue;
|
|
8031
|
+
if (/^##\s+/.test(line)) {
|
|
8032
|
+
if (currentSection) sections.push(currentSection);
|
|
8033
|
+
currentSection = { heading: line, lines: [] };
|
|
8034
|
+
continue;
|
|
8035
|
+
}
|
|
8036
|
+
if (currentSection) {
|
|
8037
|
+
currentSection.lines.push(line);
|
|
8038
|
+
}
|
|
8039
|
+
}
|
|
8040
|
+
if (currentSection) {
|
|
8041
|
+
sections.push(currentSection);
|
|
8042
|
+
}
|
|
8043
|
+
return sections.filter((section) => section.lines.some((line) => line.trim().length > 0)).map((section) => `${section.heading}
|
|
8044
|
+
|
|
8045
|
+
${section.lines.join("\n").trim()}`).join("\n\n---\n\n").trim();
|
|
8046
|
+
}
|
|
8047
|
+
function getAvailableArchivePath(filePath) {
|
|
8048
|
+
const baseArchivePath = `${filePath}.bak`;
|
|
8049
|
+
if (!fs8.existsSync(baseArchivePath)) {
|
|
8050
|
+
return baseArchivePath;
|
|
8051
|
+
}
|
|
8052
|
+
let counter = 1;
|
|
8053
|
+
while (fs8.existsSync(`${baseArchivePath}.${counter}`)) {
|
|
8054
|
+
counter += 1;
|
|
8055
|
+
}
|
|
8056
|
+
return `${baseArchivePath}.${counter}`;
|
|
8057
|
+
}
|
|
8058
|
+
function syncTodoFromState(state) {
|
|
8059
|
+
const queue = state.implementationQueue ?? [];
|
|
8060
|
+
if (queue.length === 0) return;
|
|
8061
|
+
const dir = path9.join(process.cwd(), "planning");
|
|
8062
|
+
if (!fs8.existsSync(dir)) fs8.mkdirSync(dir, { recursive: true });
|
|
8063
|
+
const todoPath = path9.join(dir, "TODO.md");
|
|
8064
|
+
const tasks = getTaskEntriesFromState(state);
|
|
8065
|
+
fs8.writeFileSync(todoPath, renderTodo(tasks, state.projectName ?? path9.basename(process.cwd())));
|
|
8066
|
+
}
|
|
7601
8067
|
function loadBuildState() {
|
|
7602
8068
|
const stateFile = path9.join(process.cwd(), "planning", "BUILD_STATE.json");
|
|
7603
8069
|
if (!fs8.existsSync(stateFile)) return null;
|
|
@@ -7610,67 +8076,21 @@ function loadBuildState() {
|
|
|
7610
8076
|
function saveBuildState(state) {
|
|
7611
8077
|
const dir = path9.join(process.cwd(), "planning");
|
|
7612
8078
|
if (!fs8.existsSync(dir)) fs8.mkdirSync(dir, { recursive: true });
|
|
8079
|
+
if (state.implementationQueue?.length) {
|
|
8080
|
+
state.currentPhase = getCurrentPhase(state.implementationQueue);
|
|
8081
|
+
}
|
|
7613
8082
|
if (state.metadata) state.metadata.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
7614
8083
|
fs8.writeFileSync(path9.join(dir, "BUILD_STATE.json"), JSON.stringify(state, null, 2));
|
|
8084
|
+
syncTodoFromState(state);
|
|
7615
8085
|
}
|
|
7616
8086
|
function loadTasks() {
|
|
7617
|
-
const
|
|
7618
|
-
|
|
7619
|
-
|
|
7620
|
-
if (fs8.existsSync(todoFile)) files.push(todoFile);
|
|
7621
|
-
if (fs8.existsSync(queueFile)) files.push(queueFile);
|
|
7622
|
-
for (const file of files) {
|
|
7623
|
-
try {
|
|
7624
|
-
const content = fs8.readFileSync(file, "utf-8");
|
|
7625
|
-
const tasks = [];
|
|
7626
|
-
if (file.endsWith("TODO.md")) {
|
|
7627
|
-
let currentPhase = "Unknown";
|
|
7628
|
-
for (const line of content.split("\n")) {
|
|
7629
|
-
const phaseMatch = line.match(/^##\s+(Foundation|MVP|Launch)\b/i);
|
|
7630
|
-
if (phaseMatch) {
|
|
7631
|
-
currentPhase = phaseMatch[1];
|
|
7632
|
-
continue;
|
|
7633
|
-
}
|
|
7634
|
-
const todoMatch = line.match(/^-\s+\[([ xX])\]\s+`(bs-\d+)`\s+(.+?)(?:\s+\(`?(\w+)`?\))?$/);
|
|
7635
|
-
if (todoMatch) {
|
|
7636
|
-
const checked = todoMatch[1].toLowerCase() === "x";
|
|
7637
|
-
const explicitStatus = todoMatch[4]?.toLowerCase();
|
|
7638
|
-
tasks.push({
|
|
7639
|
-
id: todoMatch[2],
|
|
7640
|
-
title: todoMatch[3].trim(),
|
|
7641
|
-
phase: currentPhase,
|
|
7642
|
-
status: explicitStatus || (checked ? "completed" : "pending")
|
|
7643
|
-
});
|
|
7644
|
-
}
|
|
7645
|
-
}
|
|
7646
|
-
if (tasks.length > 0) return tasks;
|
|
7647
|
-
}
|
|
7648
|
-
for (const line of content.split("\n")) {
|
|
7649
|
-
const match = line.match(/\|\s*\d+\s*\|\s*(bs-\d+)\s*\|\s*(.+?)\s*\|\s*(\w+)\s*\|\s*(\w+)\s*\|\s*(\w+)\s*\|/);
|
|
7650
|
-
if (match) {
|
|
7651
|
-
tasks.push({
|
|
7652
|
-
id: match[1],
|
|
7653
|
-
title: match[2].trim(),
|
|
7654
|
-
phase: match[3],
|
|
7655
|
-
complexity: match[4],
|
|
7656
|
-
status: match[5]
|
|
7657
|
-
});
|
|
7658
|
-
}
|
|
7659
|
-
}
|
|
7660
|
-
if (tasks.length > 0) return tasks;
|
|
7661
|
-
} catch {
|
|
7662
|
-
continue;
|
|
7663
|
-
}
|
|
8087
|
+
const planningTasks = loadPlanningTasks();
|
|
8088
|
+
if (planningTasks.tasks.length > 0) {
|
|
8089
|
+
return planningTasks.tasks;
|
|
7664
8090
|
}
|
|
7665
8091
|
const state = loadBuildState();
|
|
7666
8092
|
if (state?.implementationQueue?.length) {
|
|
7667
|
-
return state
|
|
7668
|
-
id: t.id,
|
|
7669
|
-
title: t.title,
|
|
7670
|
-
phase: t.phase,
|
|
7671
|
-
complexity: t.estimatedComplexity,
|
|
7672
|
-
status: t.status
|
|
7673
|
-
}));
|
|
8093
|
+
return getTaskEntriesFromState(state);
|
|
7674
8094
|
}
|
|
7675
8095
|
return [];
|
|
7676
8096
|
}
|
|
@@ -7678,8 +8098,10 @@ function registerBuildCommand(program3) {
|
|
|
7678
8098
|
const build = program3.command("build").description("Manage the build loop");
|
|
7679
8099
|
build.command("status").description("Check build progress").action(() => {
|
|
7680
8100
|
const state = loadBuildState();
|
|
7681
|
-
const
|
|
8101
|
+
const planningTasks = loadPlanningTasks();
|
|
8102
|
+
const tasks = planningTasks.tasks.length > 0 ? planningTasks.tasks : getTaskEntriesFromState(state);
|
|
7682
8103
|
print.header("Build Status");
|
|
8104
|
+
printLegacyQueueWarningIfNeeded();
|
|
7683
8105
|
if (!state) {
|
|
7684
8106
|
print.warning("No build state found. Run `bootspring build start` to begin.");
|
|
7685
8107
|
return;
|
|
@@ -7697,11 +8119,16 @@ function registerBuildCommand(program3) {
|
|
|
7697
8119
|
const sessionId = state.loopSession?.sessionId ?? "N/A";
|
|
7698
8120
|
print.info(`Project: ${state.projectName ?? "Unknown"}`);
|
|
7699
8121
|
print.info(`Status: ${state.status}`);
|
|
7700
|
-
print.info(`Phase: ${state.currentPhase ??
|
|
8122
|
+
print.info(`Phase: ${formatPhaseName(state.currentPhase ?? getCurrentPhase(state.implementationQueue))}`);
|
|
8123
|
+
print.info(`Planning Source: ${formatPlanningSourceLabel(planningTasks.source ?? (state ? "BUILD_STATE.json" : null))}`);
|
|
7701
8124
|
print.info(`Progress: ${completed}/${total} (${pct}%)`);
|
|
7702
8125
|
print.info(`Pending: ${pending}`);
|
|
7703
8126
|
if (inProgress > 0) print.info(`In Progress: ${inProgress}`);
|
|
7704
|
-
|
|
8127
|
+
if (iteration > maxIter) {
|
|
8128
|
+
print.info(`Iteration: ${iteration} (session max ${maxIter})`);
|
|
8129
|
+
} else {
|
|
8130
|
+
print.info(`Iteration: ${iteration}/${maxIter}`);
|
|
8131
|
+
}
|
|
7705
8132
|
print.info(`Session: ${sessionId}`);
|
|
7706
8133
|
const currentTask = tasks.find((t) => t.status.toLowerCase() === "in_progress");
|
|
7707
8134
|
if (currentTask) {
|
|
@@ -7713,6 +8140,7 @@ function registerBuildCommand(program3) {
|
|
|
7713
8140
|
console.log(` [${bar}] ${pct}%`);
|
|
7714
8141
|
});
|
|
7715
8142
|
build.command("task").description("Show the current task").action(() => {
|
|
8143
|
+
printLegacyQueueWarningIfNeeded();
|
|
7716
8144
|
const tasks = loadTasks();
|
|
7717
8145
|
const current = tasks.find((t) => t.status.toLowerCase() === "in_progress");
|
|
7718
8146
|
const next = current ?? tasks.find((t) => t.status.toLowerCase() === "pending");
|
|
@@ -7757,6 +8185,7 @@ function registerBuildCommand(program3) {
|
|
|
7757
8185
|
}
|
|
7758
8186
|
});
|
|
7759
8187
|
build.command("plan").description("View the full build plan").action(() => {
|
|
8188
|
+
printLegacyQueueWarningIfNeeded();
|
|
7760
8189
|
const tasks = loadTasks();
|
|
7761
8190
|
if (tasks.length === 0) {
|
|
7762
8191
|
print.warning("No tasks found in planning/TODO.md or planning/TASK_QUEUE.md");
|
|
@@ -7856,10 +8285,11 @@ function registerBuildCommand(program3) {
|
|
|
7856
8285
|
saveBuildState(state);
|
|
7857
8286
|
print.info("Build resumed");
|
|
7858
8287
|
});
|
|
7859
|
-
build.command("sync").description("Sync tasks from
|
|
7860
|
-
|
|
7861
|
-
|
|
7862
|
-
|
|
8288
|
+
build.command("sync").description("Sync planning tasks from TODO.md into BUILD_STATE.json").option("--replace", "Replace runtime queue instead of merging").action((opts) => {
|
|
8289
|
+
printLegacyQueueWarningIfNeeded();
|
|
8290
|
+
const planningTasks = loadPlanningTasks();
|
|
8291
|
+
if (planningTasks.tasks.length === 0) {
|
|
8292
|
+
print.warning("No planning/TODO.md found (legacy fallback: planning/TASK_QUEUE.md)");
|
|
7863
8293
|
return;
|
|
7864
8294
|
}
|
|
7865
8295
|
const state = loadBuildState();
|
|
@@ -7867,24 +8297,62 @@ function registerBuildCommand(program3) {
|
|
|
7867
8297
|
print.error("No build state found");
|
|
7868
8298
|
return;
|
|
7869
8299
|
}
|
|
7870
|
-
const
|
|
7871
|
-
const
|
|
8300
|
+
const replace = Boolean(opts.replace);
|
|
8301
|
+
const planningIds = new Set(planningTasks.tasks.map((task) => task.id));
|
|
8302
|
+
const existingQueue = replace ? [] : state.implementationQueue ?? [];
|
|
8303
|
+
const existingIds = new Set(existingQueue.map((t) => t.id));
|
|
7872
8304
|
let added = 0;
|
|
7873
|
-
|
|
8305
|
+
let updated = 0;
|
|
8306
|
+
for (const task of planningTasks.tasks) {
|
|
7874
8307
|
if (!existingIds.has(task.id)) {
|
|
7875
|
-
|
|
7876
|
-
state.implementationQueue.push({
|
|
8308
|
+
existingQueue.push({
|
|
7877
8309
|
id: task.id,
|
|
7878
8310
|
title: task.title,
|
|
7879
8311
|
phase: task.phase,
|
|
7880
8312
|
status: task.status,
|
|
7881
|
-
estimatedComplexity: task.complexity
|
|
8313
|
+
estimatedComplexity: task.complexity,
|
|
8314
|
+
description: task.description,
|
|
8315
|
+
source: task.source,
|
|
8316
|
+
sourceSection: task.sourceSection,
|
|
8317
|
+
acceptanceCriteria: task.acceptanceCriteria,
|
|
8318
|
+
dependencies: task.dependencies
|
|
7882
8319
|
});
|
|
7883
8320
|
added++;
|
|
8321
|
+
continue;
|
|
8322
|
+
}
|
|
8323
|
+
const index = existingQueue.findIndex((existing) => existing.id === task.id);
|
|
8324
|
+
if (index >= 0) {
|
|
8325
|
+
const previous = existingQueue[index];
|
|
8326
|
+
const nextTask = {
|
|
8327
|
+
...previous,
|
|
8328
|
+
id: task.id,
|
|
8329
|
+
title: task.title,
|
|
8330
|
+
phase: task.phase,
|
|
8331
|
+
status: task.status,
|
|
8332
|
+
estimatedComplexity: task.complexity,
|
|
8333
|
+
description: task.description,
|
|
8334
|
+
source: task.source,
|
|
8335
|
+
sourceSection: task.sourceSection,
|
|
8336
|
+
acceptanceCriteria: task.acceptanceCriteria,
|
|
8337
|
+
dependencies: task.dependencies
|
|
8338
|
+
};
|
|
8339
|
+
if (JSON.stringify(previous) !== JSON.stringify(nextTask)) {
|
|
8340
|
+
existingQueue[index] = nextTask;
|
|
8341
|
+
updated++;
|
|
8342
|
+
}
|
|
7884
8343
|
}
|
|
7885
8344
|
}
|
|
8345
|
+
const orderedQueue = [
|
|
8346
|
+
...planningTasks.tasks.map((task) => existingQueue.find((existing) => existing.id === task.id)).filter(Boolean),
|
|
8347
|
+
...existingQueue.filter((task) => !planningIds.has(task.id))
|
|
8348
|
+
];
|
|
8349
|
+
state.implementationQueue = orderedQueue;
|
|
8350
|
+
if (!state.metadata) {
|
|
8351
|
+
state.metadata = { updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
8352
|
+
}
|
|
7886
8353
|
saveBuildState(state);
|
|
7887
|
-
print.success(`Synced ${added} new tasks (${state.implementationQueue?.length ?? 0} total)`);
|
|
8354
|
+
print.success(`Synced ${added} new tasks and updated ${updated} tasks (${state.implementationQueue?.length ?? 0} total)`);
|
|
8355
|
+
print.info(`Planning source: ${planningTasks.source ?? "unknown"}`);
|
|
7888
8356
|
});
|
|
7889
8357
|
build.command("stop").description("Graceful stop the build loop").action(() => {
|
|
7890
8358
|
const state = loadBuildState();
|
|
@@ -7940,6 +8408,7 @@ function registerBuildCommand(program3) {
|
|
|
7940
8408
|
print.success(`Started: ${first.id} \u2014 ${first.title}`);
|
|
7941
8409
|
});
|
|
7942
8410
|
build.command("start").description("Initialize build from seed/planning documents").action(() => {
|
|
8411
|
+
printLegacyQueueWarningIfNeeded();
|
|
7943
8412
|
const state = loadBuildState();
|
|
7944
8413
|
if (state) {
|
|
7945
8414
|
print.warning("Build already initialized. Use `bootspring build status` to check progress.");
|
|
@@ -7947,7 +8416,7 @@ function registerBuildCommand(program3) {
|
|
|
7947
8416
|
}
|
|
7948
8417
|
const tasks = loadTasks();
|
|
7949
8418
|
if (tasks.length === 0) {
|
|
7950
|
-
print.warning("No tasks found in planning/TODO.md
|
|
8419
|
+
print.warning("No tasks found in planning/TODO.md (legacy fallback: planning/TASK_QUEUE.md)");
|
|
7951
8420
|
print.info("Run `bootspring preseed` or `bootspring prd create` first to generate tasks.");
|
|
7952
8421
|
return;
|
|
7953
8422
|
}
|
|
@@ -7975,54 +8444,56 @@ function registerBuildCommand(program3) {
|
|
|
7975
8444
|
print.success(`Build initialized with ${tasks.length} tasks`);
|
|
7976
8445
|
print.info("Run `bootspring build next` to start the first task");
|
|
7977
8446
|
});
|
|
7978
|
-
build.command("migrate-todo").alias("migrate").description("Convert TASK_QUEUE.md
|
|
8447
|
+
build.command("migrate-todo").alias("migrate").description("Convert legacy TASK_QUEUE.md into canonical TODO.md format and archive the queue by default").option("--keep-queue", "Keep legacy planning/TASK_QUEUE.md instead of archiving it").action((options) => {
|
|
7979
8448
|
const queueFile = path9.join(process.cwd(), "planning", "TASK_QUEUE.md");
|
|
7980
8449
|
const todoFile = path9.join(process.cwd(), "planning", "TODO.md");
|
|
7981
|
-
|
|
7982
|
-
|
|
7983
|
-
|
|
7984
|
-
|
|
7985
|
-
if (fs8.existsSync(todoFile)) {
|
|
7986
|
-
print.warning("planning/TODO.md already exists. Delete it first to re-migrate.");
|
|
7987
|
-
return;
|
|
7988
|
-
}
|
|
8450
|
+
const state = loadBuildState();
|
|
8451
|
+
const keepQueue = Boolean(options.keepQueue);
|
|
8452
|
+
const hasTodo = fs8.existsSync(todoFile);
|
|
8453
|
+
const hasQueue = fs8.existsSync(queueFile);
|
|
7989
8454
|
try {
|
|
7990
|
-
const
|
|
7991
|
-
|
|
7992
|
-
|
|
7993
|
-
|
|
7994
|
-
|
|
7995
|
-
|
|
7996
|
-
id: match[1],
|
|
7997
|
-
title: match[2].trim(),
|
|
7998
|
-
phase: match[3],
|
|
7999
|
-
complexity: match[4],
|
|
8000
|
-
status: match[5]
|
|
8001
|
-
});
|
|
8002
|
-
}
|
|
8455
|
+
const planningTasks = loadPlanningTasks();
|
|
8456
|
+
let tasks = planningTasks.tasks;
|
|
8457
|
+
let source = planningTasks.source;
|
|
8458
|
+
if (tasks.length === 0) {
|
|
8459
|
+
tasks = getTaskEntriesFromState(state);
|
|
8460
|
+
source = tasks.length > 0 ? "BUILD_STATE.json" : null;
|
|
8003
8461
|
}
|
|
8004
8462
|
if (tasks.length === 0) {
|
|
8005
|
-
print.warning("No tasks found in TASK_QUEUE.md");
|
|
8463
|
+
print.warning("No tasks found in planning/TODO.md, planning/TASK_QUEUE.md, or planning/BUILD_STATE.json");
|
|
8006
8464
|
return;
|
|
8007
8465
|
}
|
|
8008
|
-
const
|
|
8009
|
-
|
|
8010
|
-
|
|
8011
|
-
|
|
8466
|
+
const dir = path9.dirname(todoFile);
|
|
8467
|
+
if (!fs8.existsSync(dir)) fs8.mkdirSync(dir, { recursive: true });
|
|
8468
|
+
const projectName = state?.projectName ?? path9.basename(process.cwd());
|
|
8469
|
+
if (!hasTodo || source !== "TODO.md") {
|
|
8470
|
+
let todoContent = renderTodo(tasks, projectName);
|
|
8471
|
+
if (hasQueue && source === "TASK_QUEUE.md") {
|
|
8472
|
+
const supplementaryContent = extractSupplementaryContent(fs8.readFileSync(queueFile, "utf-8"));
|
|
8473
|
+
if (supplementaryContent) {
|
|
8474
|
+
todoContent += `
|
|
8475
|
+
---
|
|
8012
8476
|
|
|
8477
|
+
## Reference (from TASK_QUEUE.md)
|
|
8478
|
+
|
|
8479
|
+
${supplementaryContent}
|
|
8013
8480
|
`;
|
|
8014
|
-
|
|
8015
|
-
for (const task of phaseTasks) {
|
|
8016
|
-
const checked = task.status.toLowerCase() === "completed" || task.status.toLowerCase() === "done";
|
|
8017
|
-
todoContent += `- [${checked ? "x" : " "}] \`${task.id}\` ${task.title}
|
|
8018
|
-
`;
|
|
8481
|
+
}
|
|
8019
8482
|
}
|
|
8020
|
-
todoContent
|
|
8483
|
+
fs8.writeFileSync(todoFile, todoContent);
|
|
8484
|
+
print.success(`Wrote planning/TODO.md from ${source ?? "planning data"} (${tasks.length} tasks)`);
|
|
8485
|
+
} else {
|
|
8486
|
+
print.info("Retained existing planning/TODO.md as the canonical task file");
|
|
8487
|
+
}
|
|
8488
|
+
if (hasQueue && !keepQueue) {
|
|
8489
|
+
const archivePath = getAvailableArchivePath(queueFile);
|
|
8490
|
+
fs8.renameSync(queueFile, archivePath);
|
|
8491
|
+
print.success(`Archived legacy TASK_QUEUE.md to planning/${path9.basename(archivePath)}`);
|
|
8492
|
+
} else if (hasQueue && keepQueue) {
|
|
8493
|
+
print.info("Retained legacy TASK_QUEUE.md for compatibility (--keep-queue)");
|
|
8494
|
+
} else {
|
|
8495
|
+
print.info("No planning/TASK_QUEUE.md present; nothing to archive");
|
|
8021
8496
|
}
|
|
8022
|
-
const dir = path9.dirname(todoFile);
|
|
8023
|
-
if (!fs8.existsSync(dir)) fs8.mkdirSync(dir, { recursive: true });
|
|
8024
|
-
fs8.writeFileSync(todoFile, todoContent);
|
|
8025
|
-
print.success(`Migrated ${tasks.length} tasks to planning/TODO.md`);
|
|
8026
8497
|
} catch (err) {
|
|
8027
8498
|
print.error(`Migration failed: ${err.message}`);
|
|
8028
8499
|
}
|
|
@@ -8030,6 +8501,7 @@ function registerBuildCommand(program3) {
|
|
|
8030
8501
|
build.command("backfill").aliases(["recover", "hydrate", "bootstrap"]).description("Recover build artifacts from existing codebase").action(() => {
|
|
8031
8502
|
const planningDir = path9.join(process.cwd(), "planning");
|
|
8032
8503
|
print.header("Build Artifact Recovery");
|
|
8504
|
+
printLegacyQueueWarningIfNeeded();
|
|
8033
8505
|
const hasState = fs8.existsSync(path9.join(planningDir, "BUILD_STATE.json"));
|
|
8034
8506
|
const hasTodo = fs8.existsSync(path9.join(planningDir, "TODO.md"));
|
|
8035
8507
|
const hasQueue = fs8.existsSync(path9.join(planningDir, "TASK_QUEUE.md"));
|
|
@@ -8250,6 +8722,16 @@ function registerHealthCommand(program3) {
|
|
|
8250
8722
|
}
|
|
8251
8723
|
console.log(`
|
|
8252
8724
|
${passCount} passed, ${warnCount} warnings, ${failCount} failed`);
|
|
8725
|
+
try {
|
|
8726
|
+
const score = passCount > 0 ? Math.round(passCount / (passCount + failCount + warnCount) * 100) : 0;
|
|
8727
|
+
const grade = failCount === 0 ? warnCount === 0 ? "A" : "B" : "F";
|
|
8728
|
+
await presence_exports.sendHealthReport({
|
|
8729
|
+
score,
|
|
8730
|
+
grade,
|
|
8731
|
+
data: { checks, passCount, warnCount, failCount }
|
|
8732
|
+
});
|
|
8733
|
+
} catch {
|
|
8734
|
+
}
|
|
8253
8735
|
if (failCount > 0) {
|
|
8254
8736
|
process.exitCode = 1;
|
|
8255
8737
|
}
|
|
@@ -8310,7 +8792,7 @@ function registerGenerateCommand(program3) {
|
|
|
8310
8792
|
const sections = [
|
|
8311
8793
|
`# ${projectName} - AI Context`,
|
|
8312
8794
|
"",
|
|
8313
|
-
|
|
8795
|
+
`**Generated by**: Bootspring v${BOOTSPRING_VERSION}`,
|
|
8314
8796
|
`**Last Updated**: ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}`,
|
|
8315
8797
|
"",
|
|
8316
8798
|
"---",
|
|
@@ -9215,7 +9697,7 @@ function registerLoopCommand(program3) {
|
|
|
9215
9697
|
}
|
|
9216
9698
|
const tasks = loadTasks2();
|
|
9217
9699
|
if (tasks.length === 0) {
|
|
9218
|
-
print.warning("No tasks found in planning/TODO.md
|
|
9700
|
+
print.warning("No tasks found in planning/TODO.md (legacy fallback: planning/TASK_QUEUE.md)");
|
|
9219
9701
|
return;
|
|
9220
9702
|
}
|
|
9221
9703
|
const pending = tasks.filter((t) => t.status.toLowerCase() === "pending");
|
|
@@ -13556,9 +14038,227 @@ init_cjs_shims();
|
|
|
13556
14038
|
var fs30 = __toESM(require("fs"), 1);
|
|
13557
14039
|
var path31 = __toESM(require("path"), 1);
|
|
13558
14040
|
init_dist();
|
|
13559
|
-
|
|
13560
|
-
|
|
14041
|
+
|
|
14042
|
+
// src/commands/business-templates.ts
|
|
14043
|
+
init_cjs_shims();
|
|
14044
|
+
|
|
14045
|
+
// src/commands/business-strategy-modules.ts
|
|
14046
|
+
init_cjs_shims();
|
|
14047
|
+
var STRATEGY_MODULES = [
|
|
14048
|
+
{
|
|
14049
|
+
title: "Precision ICP Builder",
|
|
14050
|
+
useWhen: "You need a buyer definition that changes product, sales, and messaging decisions immediately.",
|
|
14051
|
+
prompt: ["You are a market research strategist for early-stage software and services.", "Build a decision-ready ideal customer profile for the project."],
|
|
14052
|
+
returns: ["buyer filters that materially affect purchase behavior", "identity, professional self-image, status fears, and desired reputation", "urgent pains versus tolerable pains", "active buying triggers and decision style", "exact customer language, reachable communities, objections, first-buyer profile, and one opening sentence"],
|
|
14053
|
+
inputs: ["Offer: <describe what you sell>", "Current customer guess: <who you think buys today>"]
|
|
14054
|
+
},
|
|
14055
|
+
{
|
|
14056
|
+
title: "Distinctive Positioning Builder",
|
|
14057
|
+
useWhen: "You need messaging that sounds like you, not a generic competitor.",
|
|
14058
|
+
prompt: ["You are a positioning strategist.", "Write positioning for the project that survives a specificity test."],
|
|
14059
|
+
returns: ["one sharp positioning statement", "a rewrite pass that removes vague phrases", "a competitor difference matrix on the 4 factors buyers actually care about", "variations for cold outreach, homepage copy, and spoken pitch", "a short validation question to ask live prospects"],
|
|
14060
|
+
inputs: ["Product: <what you do>", "Target customer: <who buys this>", "Main alternative: <what buyers do instead>", "Real differentiator: <what is true and hard to copy>"]
|
|
14061
|
+
},
|
|
14062
|
+
{
|
|
14063
|
+
title: "Pricing Architecture Builder",
|
|
14064
|
+
useWhen: "You want pricing to steer buyers toward the right plan instead of just listing options.",
|
|
14065
|
+
prompt: ["You are a monetization strategist.", "Design a 3-tier pricing system for the project with strong anchoring."],
|
|
14066
|
+
returns: ["entry, core, and premium tier structure", "why each tier exists and what should stay out of it", "recommended monthly and annual price points", "upgrade triggers between tiers", "page-ordering, trial logic, and the pricing mistake most likely to hurt us"],
|
|
14067
|
+
inputs: ["Offer: <describe what you sell>", "Pricing instinct: <what you were planning to charge>", "Target customer: <who buys>"]
|
|
14068
|
+
},
|
|
14069
|
+
{
|
|
14070
|
+
title: "90-Day GTM Sprint Planner",
|
|
14071
|
+
useWhen: "You need a week-by-week launch plan with zero fluff and no paid ads.",
|
|
14072
|
+
prompt: ["You are a zero-to-one go-to-market operator.", "Create a 90-day GTM plan for the project."],
|
|
14073
|
+
returns: ["one-line ICP restatement", "top 3 launch channels ranked by likely first-90-day ROI", "week-by-week actions for validation, first closes, system building, and momentum", "measurable weekly targets", "2 daily non-negotiables for month 1", "one signal that proves the market is responding"],
|
|
14074
|
+
inputs: ["Product: <describe the product or service>", "Ideal buyer: <specific role and context>", "Available channels: <email, LinkedIn, communities, network, etc.>"]
|
|
14075
|
+
},
|
|
14076
|
+
{
|
|
14077
|
+
title: "Partnership Lever Scanner",
|
|
14078
|
+
useWhen: "You want distribution through other people\u2019s audiences instead of only direct outreach.",
|
|
14079
|
+
prompt: ["You are a partnership strategist.", "Find partnership and co-marketing opportunities for the project."],
|
|
14080
|
+
returns: ["complementary product partners", "audience owners and the co-marketing format that fits each", "referral partners and incentive logic", "integration partners that place us near the moment of need", "first outreach message and top 3 fastest opportunities"],
|
|
14081
|
+
inputs: ["Product: <what you sell>", "ICP: <who you want to reach>", "Category: <your niche or market>"]
|
|
14082
|
+
},
|
|
14083
|
+
{
|
|
14084
|
+
title: "Competitive Gap and Threat Review",
|
|
14085
|
+
useWhen: "You need actionable white space, not a generic list of rivals.",
|
|
14086
|
+
prompt: ["You are a competitive intelligence analyst.", "Map the competitive field for the project and show where we can win."],
|
|
14087
|
+
returns: ["the 5 most relevant competitors ranked by directness", "each rival\u2019s model, strengths, recurring weaknesses, and ignored segment", "the open category gap buyers would pay for", "the competitor most likely to copy us and how to raise the bar before they do", "3 quick wins to take buyers from a named rival this month"],
|
|
14088
|
+
inputs: ["Product: <what you sell>", "Known competitors: <names or descriptions>"]
|
|
14089
|
+
},
|
|
14090
|
+
{
|
|
14091
|
+
title: "Moat and Defensibility Planner",
|
|
14092
|
+
useWhen: "You need a credible path to defensibility before scale makes copying easier.",
|
|
14093
|
+
prompt: ["You are a startup strategy advisor.", "Evaluate defensibility for the project."],
|
|
14094
|
+
returns: ["current moat score across network effects, switching costs, cost advantage, intangible assets, and efficient scale", "the 1 or 2 moat types most realistic right now", "90-day actions that start compounding defensibility", "an incumbent attack scenario and how to reduce vulnerability", "a 2-sentence investor explanation of the moat"],
|
|
14095
|
+
inputs: ["Business model: <describe product and monetization>", "Current unfair advantage: <what is hard to copy today>"]
|
|
14096
|
+
},
|
|
14097
|
+
{
|
|
14098
|
+
title: "Revenue Model Stress Test",
|
|
14099
|
+
useWhen: "Your model looks fine on paper but you need to find the weak assumptions early.",
|
|
14100
|
+
prompt: ["You are a financial model reviewer.", "Pressure-test the revenue model for the project."],
|
|
14101
|
+
returns: ["unit economics summary", "break-even math and runway implications", "churn and concentration risks", "pricing-pressure scenario analysis", "customer growth math to reach key revenue milestones", "the single assumption that could break the model", "the one improvement lever with the highest 90-day impact"],
|
|
14102
|
+
inputs: ["Revenue model: <how money is made>", "Current pricing: <what you charge>", "Cost structure: <rough CAC and cost to serve>"]
|
|
14103
|
+
},
|
|
14104
|
+
{
|
|
14105
|
+
title: "Investor One-Pager Draft Builder",
|
|
14106
|
+
useWhen: "You need a fundraising artifact that is short, specific, and credible.",
|
|
14107
|
+
prompt: ["You are a fundraising narrative advisor.", "Write a one-page investor brief for the project."],
|
|
14108
|
+
returns: ["clear problem, solution, market framing, model, traction, team, and ask", "a competition section that differentiates without posturing", "a one-page layout with suggested word count per section", "the closing line that makes a meeting feel like the next obvious step"],
|
|
14109
|
+
inputs: ["Product: <what is being built>", "Traction: <revenue, pilots, waitlist, design partners, etc.>", "Team: <who is building it>"]
|
|
14110
|
+
},
|
|
14111
|
+
{
|
|
14112
|
+
title: "Startup Launch Brief",
|
|
14113
|
+
useWhen: "You want one document that turns strategy into immediate execution.",
|
|
14114
|
+
prompt: ["You are a founding-team operator.", "Synthesize the current plan for the project into a one-page execution brief."],
|
|
14115
|
+
returns: ["business in one line", "ICP in one line", "positioning in one line", "pricing summary", "30-day action plan", "first 5 customers with outreach angle", "top 3 risks and the mitigation for each", "the single highest-leverage action for the next 7 days", "the measurable 90-day success definition"],
|
|
14116
|
+
inputs: ["Idea context: <product, market, stage, founder context>", "Biggest uncertainty: <what feels most risky>", "90-day goal: <what success looks like>"]
|
|
14117
|
+
},
|
|
14118
|
+
{
|
|
14119
|
+
title: "Customer Interview Guide",
|
|
14120
|
+
useWhen: "You want interviews that produce decisions, not polite anecdotes.",
|
|
14121
|
+
prompt: ["You are a customer interview coach.", "Create a buyer interview guide for the project."],
|
|
14122
|
+
returns: ["12 interview questions in the right order", "follow-up probes for pain, urgency, alternatives, and budget", "signals that indicate real buying intent", "the 3 mistakes that bias the conversation"],
|
|
14123
|
+
inputs: ["Who you want to interview: <specific buyer type>", "Hypothesis to test: <belief you want validated or disproved>"]
|
|
14124
|
+
},
|
|
14125
|
+
{
|
|
14126
|
+
title: "Offer Packaging Designer",
|
|
14127
|
+
useWhen: "Your product is clear but the way it is sold still feels muddy.",
|
|
14128
|
+
prompt: ["You are an offer designer.", "Turn the project into a clear, high-conversion offer."],
|
|
14129
|
+
returns: ["core deliverable and boundary definition", "what is included, excluded, guaranteed, and time-bound", "add-ons, bonuses, urgency devices, and risk reversal", "the offer version best suited for first customers"],
|
|
14130
|
+
inputs: ["Product: <what you sell>", "Buyer: <who buys>", "Delivery model: <software, service, hybrid, one-time, recurring>"]
|
|
14131
|
+
},
|
|
14132
|
+
{
|
|
14133
|
+
title: "Homepage Messaging Brief",
|
|
14134
|
+
useWhen: "The product is real but the homepage does not yet explain why anyone should care.",
|
|
14135
|
+
prompt: ["You are a conversion copy strategist.", "Draft the messaging architecture for the homepage."],
|
|
14136
|
+
returns: ["hero headline and subhead", "proof strip, problem framing, value explanation, objection handling, and CTA logic", "section order for a high-intent homepage", "copy notes for designers and implementers"],
|
|
14137
|
+
inputs: ["ICP: <who lands on the page>", "Offer: <what you sell>", "Proof available: <logos, results, founder story, testimonials>"]
|
|
14138
|
+
},
|
|
14139
|
+
{
|
|
14140
|
+
title: "Outbound Sequence Builder",
|
|
14141
|
+
useWhen: "You need a repeatable outbound message system rooted in real buyer pain.",
|
|
14142
|
+
prompt: ["You are an outbound operator.", "Write a short outbound sequence for the project."],
|
|
14143
|
+
returns: ["first-touch email or DM", "3 follow-ups with different angles", "subject lines or hooks", "reply handling for curiosity, no timing, wrong fit, and no response"],
|
|
14144
|
+
inputs: ["Target role: <who you are contacting>", "Trigger event: <what happened that makes them timely>", "Offer angle: <why they should care now>"]
|
|
14145
|
+
},
|
|
14146
|
+
{
|
|
14147
|
+
title: "Sales Discovery Blueprint",
|
|
14148
|
+
useWhen: "You need a better structure for live calls and demos.",
|
|
14149
|
+
prompt: ["You are a sales discovery coach.", "Design a discovery and demo flow for the project."],
|
|
14150
|
+
returns: ["call agenda", "key diagnostic questions", "what to show in the product and in what order", "deal qualification criteria", "close question and next-step framing"],
|
|
14151
|
+
inputs: ["Buyer type: <role and company>", "Typical use case: <what they want done>", "Common objection: <what usually stalls the sale>"]
|
|
14152
|
+
},
|
|
14153
|
+
{
|
|
14154
|
+
title: "Activation Friction Audit",
|
|
14155
|
+
useWhen: "People sign up or book demos but stall before reaching value.",
|
|
14156
|
+
prompt: ["You are a product activation strategist.", "Audit the first-use journey for the project."],
|
|
14157
|
+
returns: ["the first-value milestone", "the top friction points before that milestone", "manual assists, product changes, and lifecycle messaging fixes", "one metric that best measures activation progress"],
|
|
14158
|
+
inputs: ["Onboarding flow: <how new users start>", "Desired outcome: <what first value looks like>", "Current drop-off: <where people stall today>"]
|
|
14159
|
+
},
|
|
14160
|
+
{
|
|
14161
|
+
title: "Referral Engine Designer",
|
|
14162
|
+
useWhen: "You have some happy users but no deliberate referral motion.",
|
|
14163
|
+
prompt: ["You are a referral growth strategist.", "Design a referral loop for the project."],
|
|
14164
|
+
returns: ["the ideal moment to ask for a referral", "what the referrer and referred person each get", "the message and CTA to use", "how to track quality, not just volume"],
|
|
14165
|
+
inputs: ["Best-fit customer: <who is happiest today>", "Product outcome: <what result they got>", "Incentive limits: <what you can realistically offer>"]
|
|
14166
|
+
},
|
|
14167
|
+
{
|
|
14168
|
+
title: "Case Study Capture Kit",
|
|
14169
|
+
useWhen: "You are getting wins but not turning them into reusable proof.",
|
|
14170
|
+
prompt: ["You are a case-study editor.", "Create a case-study capture system for the project."],
|
|
14171
|
+
returns: ["interview questions for customers", "the before/during/after story arc", "proof formats for website, outbound, investor updates, and sales calls", "release and approval checklist"],
|
|
14172
|
+
inputs: ["Customer win: <what changed for them>", "Available evidence: <quotes, screenshots, metrics, call notes>"]
|
|
14173
|
+
},
|
|
14174
|
+
{
|
|
14175
|
+
title: "Founder Narrative Builder",
|
|
14176
|
+
useWhen: "You need a tighter founder story for customers, hires, and investors.",
|
|
14177
|
+
prompt: ["You are a founder narrative strategist.", "Shape the founder story for the project."],
|
|
14178
|
+
returns: ["short founder story for homepage and deck", "longer origin story for meetings and recruiting", "why-now logic", "credibility anchors that feel earned rather than inflated"],
|
|
14179
|
+
inputs: ["Founder background: <relevant experience>", "Why this matters: <personal reason or market insight>", "What gives credibility: <evidence you can execute>"]
|
|
14180
|
+
},
|
|
14181
|
+
{
|
|
14182
|
+
title: "Channel Experiment Scorecard",
|
|
14183
|
+
useWhen: "You have too many growth ideas and need to prioritize the next 30 days.",
|
|
14184
|
+
prompt: ["You are a growth experiment lead.", "Build a channel experiment scorecard for the project."],
|
|
14185
|
+
returns: ["10 experiments scored by impact, speed, confidence, and cost", "one experiment to run this week", "kill criteria, success metric, and owner for each", "the reporting cadence for a 30-day sprint"],
|
|
14186
|
+
inputs: ["Current channels: <where you can reach buyers today>", "Team capacity: <how much time or headcount you have>", "Revenue goal: <what outcome matters most next>"]
|
|
14187
|
+
}
|
|
14188
|
+
];
|
|
14189
|
+
|
|
14190
|
+
// src/commands/business-templates.ts
|
|
14191
|
+
function renderList(items) {
|
|
14192
|
+
return items.map((item) => `- ${item}`).join("\n");
|
|
14193
|
+
}
|
|
14194
|
+
function renderStrategyModule(module2, index) {
|
|
14195
|
+
return `### ${index}. ${module2.title}
|
|
14196
|
+
|
|
14197
|
+
**Use when**: ${module2.useWhen}
|
|
14198
|
+
|
|
14199
|
+
**Prompt**
|
|
14200
|
+
${module2.prompt.join("\n")}
|
|
14201
|
+
|
|
14202
|
+
Return:
|
|
14203
|
+
${renderList(module2.returns)}
|
|
14204
|
+
|
|
14205
|
+
Inputs:
|
|
14206
|
+
${renderList(module2.inputs)}`;
|
|
14207
|
+
}
|
|
14208
|
+
function buildFounderStrategyPack(projectName, date) {
|
|
14209
|
+
const coreModules = STRATEGY_MODULES.slice(0, 10).map((module2, index) => renderStrategyModule(module2, index + 1)).join("\n\n");
|
|
14210
|
+
const expansionModules = STRATEGY_MODULES.slice(10).map((module2, index) => renderStrategyModule(module2, index + 11)).join("\n\n");
|
|
14211
|
+
return `# Founder Strategy Pack: ${projectName}
|
|
14212
|
+
|
|
14213
|
+
> **Version**: 1.0 | **Created**: ${date} | **Purpose**: Founder strategy prompts for customer research, positioning, pricing, GTM, fundraising, and growth
|
|
14214
|
+
|
|
14215
|
+
---
|
|
14216
|
+
|
|
14217
|
+
## How to Use This Pack
|
|
14218
|
+
|
|
14219
|
+
- Start with modules 1 through 10 to sharpen customer, market, and business fundamentals.
|
|
14220
|
+
- Use modules 11 through 20 to tighten execution, messaging, activation, and growth loops.
|
|
14221
|
+
- Feed the outputs back into \`AUDIENCE.md\`, \`BUSINESS_MODEL.md\`, \`COMPETITORS.md\`, \`ROADMAP.md\`, and your launch docs.
|
|
14222
|
+
- Replace the angle-bracket inputs before pasting a prompt into your preferred assistant or Bootspring workflow.
|
|
14223
|
+
|
|
14224
|
+
---
|
|
14225
|
+
|
|
14226
|
+
## Core Strategy Modules
|
|
14227
|
+
|
|
14228
|
+
${coreModules}
|
|
14229
|
+
|
|
14230
|
+
---
|
|
14231
|
+
|
|
14232
|
+
## Execution Expansion Modules
|
|
14233
|
+
|
|
14234
|
+
${expansionModules}
|
|
14235
|
+
|
|
14236
|
+
---
|
|
14237
|
+
|
|
14238
|
+
## Recommended Order
|
|
14239
|
+
|
|
14240
|
+
1. Precision ICP Builder
|
|
14241
|
+
2. Distinctive Positioning Builder
|
|
14242
|
+
3. Pricing Architecture Builder
|
|
14243
|
+
4. 90-Day GTM Sprint Planner
|
|
14244
|
+
5. Competitive Gap and Threat Review
|
|
14245
|
+
6. Startup Launch Brief
|
|
14246
|
+
|
|
14247
|
+
Then use the expansion modules based on the bottleneck you hit first.
|
|
14248
|
+
|
|
14249
|
+
---
|
|
14250
|
+
|
|
14251
|
+
*Generated with Bootspring*
|
|
14252
|
+
`;
|
|
14253
|
+
}
|
|
14254
|
+
function getBusinessTemplates(projectName, date) {
|
|
13561
14255
|
return {
|
|
14256
|
+
strategy: {
|
|
14257
|
+
name: "Founder Strategy Pack",
|
|
14258
|
+
output: "FOUNDER_STRATEGY_PACK.md",
|
|
14259
|
+
description: "20 founder strategy prompts for ICP, pricing, GTM, fundraising, and growth",
|
|
14260
|
+
content: buildFounderStrategyPack(projectName, date)
|
|
14261
|
+
},
|
|
13562
14262
|
plan: {
|
|
13563
14263
|
name: "Business Plan",
|
|
13564
14264
|
output: "BUSINESS_PLAN.md",
|
|
@@ -13731,6 +14431,9 @@ For [target customer] who [need], [product] is a [category] that [key benefit].
|
|
|
13731
14431
|
}
|
|
13732
14432
|
};
|
|
13733
14433
|
}
|
|
14434
|
+
|
|
14435
|
+
// src/commands/business.ts
|
|
14436
|
+
var OUTPUT_DIR = "planning";
|
|
13734
14437
|
function registerBusinessCommand(program3) {
|
|
13735
14438
|
const business = program3.command("business").description("Business planning and strategy tools");
|
|
13736
14439
|
business.command("init").description("Initialize all business planning documents").action(async () => {
|
|
@@ -13743,7 +14446,7 @@ function registerBusinessCommand(program3) {
|
|
|
13743
14446
|
if (!fs30.existsSync(outputDir)) {
|
|
13744
14447
|
fs30.mkdirSync(outputDir, { recursive: true });
|
|
13745
14448
|
}
|
|
13746
|
-
const templates =
|
|
14449
|
+
const templates = getBusinessTemplates(projectName, date);
|
|
13747
14450
|
const created = [];
|
|
13748
14451
|
const skipped = [];
|
|
13749
14452
|
const spinner = createSpinner("Creating business documents...").start();
|
|
@@ -13770,15 +14473,37 @@ function registerBusinessCommand(program3) {
|
|
|
13770
14473
|
}
|
|
13771
14474
|
}
|
|
13772
14475
|
console.log("\nNext Steps:");
|
|
13773
|
-
console.log(` 1. Edit ${OUTPUT_DIR}/
|
|
13774
|
-
console.log(
|
|
13775
|
-
console.log(" 3. Run `bootspring business
|
|
14476
|
+
console.log(` 1. Edit ${OUTPUT_DIR}/FOUNDER_STRATEGY_PACK.md`);
|
|
14477
|
+
console.log(` 2. Edit ${OUTPUT_DIR}/BUSINESS_PLAN.md`);
|
|
14478
|
+
console.log(" 3. Run `bootspring agent invoke business-strategy-expert`");
|
|
14479
|
+
console.log(" 4. Run `bootspring business status` to check progress");
|
|
14480
|
+
});
|
|
14481
|
+
business.command("strategy").description("Create founder strategy pack").action(async () => {
|
|
14482
|
+
const projectRoot = process.cwd();
|
|
14483
|
+
const projectName = path31.basename(projectRoot);
|
|
14484
|
+
const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
14485
|
+
const templates = getBusinessTemplates(projectName, date);
|
|
14486
|
+
const template = templates.strategy;
|
|
14487
|
+
print.header(template.name);
|
|
14488
|
+
const outputDir = path31.join(projectRoot, OUTPUT_DIR);
|
|
14489
|
+
const outputPath = path31.join(outputDir, template.output);
|
|
14490
|
+
if (fs30.existsSync(outputPath)) {
|
|
14491
|
+
print.warning(`File already exists: ${OUTPUT_DIR}/${template.output}`);
|
|
14492
|
+
return;
|
|
14493
|
+
}
|
|
14494
|
+
if (!fs30.existsSync(outputDir)) {
|
|
14495
|
+
fs30.mkdirSync(outputDir, { recursive: true });
|
|
14496
|
+
}
|
|
14497
|
+
fs30.writeFileSync(outputPath, template.content);
|
|
14498
|
+
print.success(`Created ${template.name}`);
|
|
14499
|
+
console.log(`
|
|
14500
|
+
File: ${OUTPUT_DIR}/${template.output}`);
|
|
13776
14501
|
});
|
|
13777
14502
|
business.command("plan").description("Create business plan").action(async () => {
|
|
13778
14503
|
const projectRoot = process.cwd();
|
|
13779
14504
|
const projectName = path31.basename(projectRoot);
|
|
13780
14505
|
const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
13781
|
-
const templates =
|
|
14506
|
+
const templates = getBusinessTemplates(projectName, date);
|
|
13782
14507
|
const template = templates.plan;
|
|
13783
14508
|
print.header(template.name);
|
|
13784
14509
|
const outputDir = path31.join(projectRoot, OUTPUT_DIR);
|
|
@@ -13799,7 +14524,7 @@ File: ${OUTPUT_DIR}/${template.output}`);
|
|
|
13799
14524
|
const projectRoot = process.cwd();
|
|
13800
14525
|
const projectName = path31.basename(projectRoot);
|
|
13801
14526
|
const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
13802
|
-
const templates =
|
|
14527
|
+
const templates = getBusinessTemplates(projectName, date);
|
|
13803
14528
|
const template = templates.model;
|
|
13804
14529
|
print.header(template.name);
|
|
13805
14530
|
const outputDir = path31.join(projectRoot, OUTPUT_DIR);
|
|
@@ -13820,7 +14545,7 @@ File: ${OUTPUT_DIR}/${template.output}`);
|
|
|
13820
14545
|
const projectRoot = process.cwd();
|
|
13821
14546
|
const projectName = path31.basename(projectRoot);
|
|
13822
14547
|
const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
13823
|
-
const templates =
|
|
14548
|
+
const templates = getBusinessTemplates(projectName, date);
|
|
13824
14549
|
const template = templates.competitors;
|
|
13825
14550
|
print.header(template.name);
|
|
13826
14551
|
const outputDir = path31.join(projectRoot, OUTPUT_DIR);
|
|
@@ -13843,7 +14568,7 @@ File: ${OUTPUT_DIR}/${template.output}`);
|
|
|
13843
14568
|
const projectName = path31.basename(projectRoot);
|
|
13844
14569
|
const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
13845
14570
|
const outputDir = path31.join(projectRoot, OUTPUT_DIR);
|
|
13846
|
-
const templates =
|
|
14571
|
+
const templates = getBusinessTemplates(projectName, date);
|
|
13847
14572
|
let found = 0;
|
|
13848
14573
|
const total = Object.keys(templates).length;
|
|
13849
14574
|
for (const [key, template] of Object.entries(templates)) {
|
|
@@ -13868,11 +14593,11 @@ Progress: ${found}/${total} documents created`);
|
|
|
13868
14593
|
console.log(" bootspring business init - Create all documents");
|
|
13869
14594
|
}
|
|
13870
14595
|
});
|
|
13871
|
-
business.command("show").description("Preview a business document").argument("<type>", "Document type (plan, model, competitors)").action((type) => {
|
|
14596
|
+
business.command("show").description("Preview a business document").argument("<type>", "Document type (strategy, plan, model, competitors)").action((type) => {
|
|
13872
14597
|
const projectRoot = process.cwd();
|
|
13873
14598
|
const projectName = path31.basename(projectRoot);
|
|
13874
14599
|
const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
13875
|
-
const templates =
|
|
14600
|
+
const templates = getBusinessTemplates(projectName, date);
|
|
13876
14601
|
const template = templates[type];
|
|
13877
14602
|
if (!template) {
|
|
13878
14603
|
print.error(`Unknown template: ${type}`);
|
|
@@ -19668,6 +20393,9 @@ function registerSetupCommand(program3) {
|
|
|
19668
20393
|
setup.command("status").description("Show current assistant MCP setup status").action(() => {
|
|
19669
20394
|
showStatus4();
|
|
19670
20395
|
});
|
|
20396
|
+
setup.command("ecosystem").description("Configure popular third-party MCP servers alongside Bootspring").action(async () => {
|
|
20397
|
+
await setupEcosystem();
|
|
20398
|
+
});
|
|
19671
20399
|
setup.action(async () => {
|
|
19672
20400
|
await setupAssistants();
|
|
19673
20401
|
});
|
|
@@ -19827,10 +20555,150 @@ ${COLORS.cyan}${COLORS.bold}Bootspring Setup Status${COLORS.reset}
|
|
|
19827
20555
|
}
|
|
19828
20556
|
console.log();
|
|
19829
20557
|
}
|
|
20558
|
+
var ECOSYSTEM_SERVERS = [
|
|
20559
|
+
{
|
|
20560
|
+
key: "context7",
|
|
20561
|
+
label: "Context7",
|
|
20562
|
+
description: "Library documentation via MCP (Bootspring also provides this via bootspring_docs)",
|
|
20563
|
+
mcp: { command: "npx", args: ["-y", "@upstash/context7-mcp"] },
|
|
20564
|
+
tomlBlock: `[mcp_servers.context7]
|
|
20565
|
+
command = "npx"
|
|
20566
|
+
args = ["-y", "@upstash/context7-mcp"]
|
|
20567
|
+
`
|
|
20568
|
+
},
|
|
20569
|
+
{
|
|
20570
|
+
key: "sequential-thinking",
|
|
20571
|
+
label: "Sequential Thinking",
|
|
20572
|
+
description: "Step-by-step reasoning for complex problem solving",
|
|
20573
|
+
mcp: { command: "npx", args: ["-y", "@modelcontextprotocol/server-sequential-thinking"] },
|
|
20574
|
+
tomlBlock: `[mcp_servers.sequential-thinking]
|
|
20575
|
+
command = "npx"
|
|
20576
|
+
args = ["-y", "@modelcontextprotocol/server-sequential-thinking"]
|
|
20577
|
+
`
|
|
20578
|
+
}
|
|
20579
|
+
];
|
|
20580
|
+
function upsertEcosystemJsonMcp(configPath, servers) {
|
|
20581
|
+
const added = [];
|
|
20582
|
+
const skipped = [];
|
|
20583
|
+
try {
|
|
20584
|
+
const dir = path55.dirname(configPath);
|
|
20585
|
+
if (!fs54.existsSync(dir)) fs54.mkdirSync(dir, { recursive: true });
|
|
20586
|
+
let settings = {};
|
|
20587
|
+
if (fs54.existsSync(configPath)) {
|
|
20588
|
+
settings = JSON.parse(fs54.readFileSync(configPath, "utf-8"));
|
|
20589
|
+
}
|
|
20590
|
+
if (!settings.mcpServers) settings.mcpServers = {};
|
|
20591
|
+
for (const server of servers) {
|
|
20592
|
+
if (settings.mcpServers[server.key]) {
|
|
20593
|
+
skipped.push(server.key);
|
|
20594
|
+
} else {
|
|
20595
|
+
settings.mcpServers[server.key] = server.mcp;
|
|
20596
|
+
added.push(server.key);
|
|
20597
|
+
}
|
|
20598
|
+
}
|
|
20599
|
+
if (added.length > 0) {
|
|
20600
|
+
fs54.writeFileSync(configPath, `${JSON.stringify(settings, null, 2)}
|
|
20601
|
+
`, "utf-8");
|
|
20602
|
+
}
|
|
20603
|
+
} catch {
|
|
20604
|
+
}
|
|
20605
|
+
return { added, skipped };
|
|
20606
|
+
}
|
|
20607
|
+
function upsertEcosystemCodexToml(servers) {
|
|
20608
|
+
const configPath = path55.join(HOME, ".codex", "config.toml");
|
|
20609
|
+
const added = [];
|
|
20610
|
+
const skipped = [];
|
|
20611
|
+
try {
|
|
20612
|
+
const dir = path55.dirname(configPath);
|
|
20613
|
+
if (!fs54.existsSync(dir)) fs54.mkdirSync(dir, { recursive: true });
|
|
20614
|
+
let existing = fs54.existsSync(configPath) ? fs54.readFileSync(configPath, "utf-8") : "";
|
|
20615
|
+
for (const server of servers) {
|
|
20616
|
+
const marker = `[mcp_servers.${server.key}]`;
|
|
20617
|
+
if (existing.includes(marker)) {
|
|
20618
|
+
skipped.push(server.key);
|
|
20619
|
+
} else {
|
|
20620
|
+
existing = existing.length > 0 ? `${existing.trimEnd()}
|
|
20621
|
+
|
|
20622
|
+
${server.tomlBlock}` : server.tomlBlock;
|
|
20623
|
+
added.push(server.key);
|
|
20624
|
+
}
|
|
20625
|
+
}
|
|
20626
|
+
if (added.length > 0) {
|
|
20627
|
+
fs54.writeFileSync(configPath, existing, "utf-8");
|
|
20628
|
+
}
|
|
20629
|
+
} catch {
|
|
20630
|
+
}
|
|
20631
|
+
return { added, skipped };
|
|
20632
|
+
}
|
|
20633
|
+
async function setupEcosystem() {
|
|
20634
|
+
console.log(`
|
|
20635
|
+
${COLORS.cyan}${COLORS.bold}Bootspring Ecosystem Setup${COLORS.reset}
|
|
20636
|
+
`);
|
|
20637
|
+
console.log(`Configuring third-party MCP servers alongside Bootspring...
|
|
20638
|
+
`);
|
|
20639
|
+
const claudePath = findConfigPath([
|
|
20640
|
+
path55.join(HOME, ".claude", "settings.json"),
|
|
20641
|
+
path55.join(HOME, ".config", "claude-code", "settings.json")
|
|
20642
|
+
]);
|
|
20643
|
+
const claudeResult = upsertEcosystemJsonMcp(claudePath, ECOSYSTEM_SERVERS);
|
|
20644
|
+
console.log(`${COLORS.bold}Claude Code${COLORS.reset} ${COLORS.dim}(${claudePath})${COLORS.reset}`);
|
|
20645
|
+
for (const key of claudeResult.added) {
|
|
20646
|
+
const srv = ECOSYSTEM_SERVERS.find((s) => s.key === key);
|
|
20647
|
+
console.log(` ${COLORS.green}+${COLORS.reset} ${srv.label}: ${srv.description}`);
|
|
20648
|
+
}
|
|
20649
|
+
for (const key of claudeResult.skipped) {
|
|
20650
|
+
const srv = ECOSYSTEM_SERVERS.find((s) => s.key === key);
|
|
20651
|
+
console.log(` ${COLORS.dim}-${COLORS.reset} ${srv.label}: already configured`);
|
|
20652
|
+
}
|
|
20653
|
+
const codexResult = upsertEcosystemCodexToml(ECOSYSTEM_SERVERS);
|
|
20654
|
+
const codexPath = path55.join(HOME, ".codex", "config.toml");
|
|
20655
|
+
console.log(`
|
|
20656
|
+
${COLORS.bold}Codex${COLORS.reset} ${COLORS.dim}(${codexPath})${COLORS.reset}`);
|
|
20657
|
+
for (const key of codexResult.added) {
|
|
20658
|
+
const srv = ECOSYSTEM_SERVERS.find((s) => s.key === key);
|
|
20659
|
+
console.log(` ${COLORS.green}+${COLORS.reset} ${srv.label}: ${srv.description}`);
|
|
20660
|
+
}
|
|
20661
|
+
for (const key of codexResult.skipped) {
|
|
20662
|
+
const srv = ECOSYSTEM_SERVERS.find((s) => s.key === key);
|
|
20663
|
+
console.log(` ${COLORS.dim}-${COLORS.reset} ${srv.label}: already configured`);
|
|
20664
|
+
}
|
|
20665
|
+
const geminiPath = findConfigPath([
|
|
20666
|
+
path55.join(HOME, ".gemini", "settings.json"),
|
|
20667
|
+
path55.join(HOME, ".config", "gemini", "settings.json")
|
|
20668
|
+
]);
|
|
20669
|
+
const geminiResult = upsertEcosystemJsonMcp(geminiPath, ECOSYSTEM_SERVERS);
|
|
20670
|
+
console.log(`
|
|
20671
|
+
${COLORS.bold}Gemini CLI${COLORS.reset} ${COLORS.dim}(${geminiPath})${COLORS.reset}`);
|
|
20672
|
+
for (const key of geminiResult.added) {
|
|
20673
|
+
const srv = ECOSYSTEM_SERVERS.find((s) => s.key === key);
|
|
20674
|
+
console.log(` ${COLORS.green}+${COLORS.reset} ${srv.label}: ${srv.description}`);
|
|
20675
|
+
}
|
|
20676
|
+
for (const key of geminiResult.skipped) {
|
|
20677
|
+
const srv = ECOSYSTEM_SERVERS.find((s) => s.key === key);
|
|
20678
|
+
console.log(` ${COLORS.dim}-${COLORS.reset} ${srv.label}: already configured`);
|
|
20679
|
+
}
|
|
20680
|
+
const totalAdded = claudeResult.added.length + codexResult.added.length + geminiResult.added.length;
|
|
20681
|
+
const totalSkipped = claudeResult.skipped.length + codexResult.skipped.length + geminiResult.skipped.length;
|
|
20682
|
+
console.log(`
|
|
20683
|
+
${COLORS.bold}Summary${COLORS.reset}`);
|
|
20684
|
+
console.log(` ${COLORS.green}${totalAdded}${COLORS.reset} server entries added`);
|
|
20685
|
+
if (totalSkipped > 0) {
|
|
20686
|
+
console.log(` ${COLORS.dim}${totalSkipped} already configured (skipped)${COLORS.reset}`);
|
|
20687
|
+
}
|
|
20688
|
+
console.log(`
|
|
20689
|
+
${COLORS.dim}Available MCP servers after setup:${COLORS.reset}`);
|
|
20690
|
+
console.log(` ${COLORS.cyan}bootspring${COLORS.reset} \u2014 Full dev workflow (context, skills, agents, quality, build)`);
|
|
20691
|
+
for (const srv of ECOSYSTEM_SERVERS) {
|
|
20692
|
+
console.log(` ${COLORS.cyan}${srv.key.padEnd(22)}${COLORS.reset} \u2014 ${srv.description}`);
|
|
20693
|
+
}
|
|
20694
|
+
console.log(`
|
|
20695
|
+
${COLORS.green}Done.${COLORS.reset} Restart assistant clients to load new MCP servers.
|
|
20696
|
+
`);
|
|
20697
|
+
}
|
|
19830
20698
|
|
|
19831
20699
|
// src/index.ts
|
|
19832
20700
|
var program2 = new Command();
|
|
19833
|
-
program2.name("bootspring").description("Development scaffolding with intelligence - CLI for AI-assisted workflows").version(
|
|
20701
|
+
program2.name("bootspring").description("Development scaffolding with intelligence - CLI for AI-assisted workflows").version(BOOTSPRING_VERSION);
|
|
19834
20702
|
registerAuthCommand(program2);
|
|
19835
20703
|
registerProjectCommand(program2);
|
|
19836
20704
|
registerSwitchCommand(program2);
|