@dv.nghiem/flowdeck 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/dist/index.d.ts.map +1 -1
  2. package/dist/index.js +14 -4197
  3. package/package.json +2 -1
  4. package/src/commands/fd-analyze-change.md +57 -0
  5. package/src/commands/fd-approve.md +64 -0
  6. package/src/commands/fd-ask.md +39 -0
  7. package/src/commands/fd-blast-radius.md +49 -0
  8. package/src/commands/fd-checkpoint.md +46 -0
  9. package/src/commands/fd-dashboard.md +57 -0
  10. package/src/commands/fd-deploy-check.md +58 -0
  11. package/src/commands/fd-discuss.md +61 -0
  12. package/src/commands/fd-doctor.md +37 -0
  13. package/src/commands/fd-evaluate-risk.md +62 -0
  14. package/src/commands/fd-fix-bug.md +93 -0
  15. package/src/commands/fd-guarded-edit.md +69 -0
  16. package/src/commands/fd-impact-radar.md +51 -0
  17. package/src/commands/fd-map-codebase.md +36 -0
  18. package/src/commands/fd-multi-repo.md +63 -0
  19. package/src/commands/fd-new-feature.md +49 -0
  20. package/src/commands/fd-new-project.md +103 -0
  21. package/src/commands/fd-plan.md +80 -0
  22. package/src/commands/fd-progress.md +50 -0
  23. package/src/commands/fd-regression-predict.md +57 -0
  24. package/src/commands/fd-resume.md +46 -0
  25. package/src/commands/fd-review-code.md +62 -0
  26. package/src/commands/fd-review-route.md +54 -0
  27. package/src/commands/fd-roadmap.md +46 -0
  28. package/src/commands/fd-settings.md +57 -0
  29. package/src/commands/fd-test-gap.md +54 -0
  30. package/src/commands/fd-translate-intent.md +56 -0
  31. package/src/commands/fd-volatility-map.md +64 -0
  32. package/src/commands/fd-workspace-status.md +34 -0
  33. package/src/commands/fd-write-docs.md +50 -0
  34. package/dist/commands/analysis/analysis.test.d.ts +0 -2
  35. package/dist/commands/analysis/analysis.test.d.ts.map +0 -1
  36. package/dist/commands/analysis/analyze-change.d.ts +0 -148
  37. package/dist/commands/analysis/analyze-change.d.ts.map +0 -1
  38. package/dist/commands/analysis/evaluate-risk.d.ts +0 -77
  39. package/dist/commands/analysis/evaluate-risk.d.ts.map +0 -1
  40. package/dist/commands/analysis/guarded-edit.d.ts +0 -72
  41. package/dist/commands/analysis/guarded-edit.d.ts.map +0 -1
  42. package/dist/commands/execution/deploy-check.d.ts +0 -91
  43. package/dist/commands/execution/deploy-check.d.ts.map +0 -1
  44. package/dist/commands/execution/fix-bug.d.ts +0 -187
  45. package/dist/commands/execution/fix-bug.d.ts.map +0 -1
  46. package/dist/commands/execution/new-feature.d.ts +0 -171
  47. package/dist/commands/execution/new-feature.d.ts.map +0 -1
  48. package/dist/commands/execution/review-code.d.ts +0 -130
  49. package/dist/commands/execution/review-code.d.ts.map +0 -1
  50. package/dist/commands/execution/write-docs.d.ts +0 -94
  51. package/dist/commands/execution/write-docs.d.ts.map +0 -1
  52. package/dist/commands/governance/approve.d.ts +0 -80
  53. package/dist/commands/governance/approve.d.ts.map +0 -1
  54. package/dist/commands/intelligence/blast-radius.d.ts +0 -67
  55. package/dist/commands/intelligence/blast-radius.d.ts.map +0 -1
  56. package/dist/commands/intelligence/impact-radar.d.ts +0 -71
  57. package/dist/commands/intelligence/impact-radar.d.ts.map +0 -1
  58. package/dist/commands/intelligence/intelligence.test.d.ts +0 -2
  59. package/dist/commands/intelligence/intelligence.test.d.ts.map +0 -1
  60. package/dist/commands/intelligence/regression-predict.d.ts +0 -75
  61. package/dist/commands/intelligence/regression-predict.d.ts.map +0 -1
  62. package/dist/commands/intelligence/review-route.d.ts +0 -65
  63. package/dist/commands/intelligence/review-route.d.ts.map +0 -1
  64. package/dist/commands/intelligence/test-gap.d.ts +0 -73
  65. package/dist/commands/intelligence/test-gap.d.ts.map +0 -1
  66. package/dist/commands/intelligence/translate-intent.d.ts +0 -87
  67. package/dist/commands/intelligence/translate-intent.d.ts.map +0 -1
  68. package/dist/commands/intelligence/volatility-map-cmd.d.ts +0 -68
  69. package/dist/commands/intelligence/volatility-map-cmd.d.ts.map +0 -1
  70. package/dist/commands/planning/ask.d.ts +0 -62
  71. package/dist/commands/planning/ask.d.ts.map +0 -1
  72. package/dist/commands/planning/ask.test.d.ts +0 -2
  73. package/dist/commands/planning/ask.test.d.ts.map +0 -1
  74. package/dist/commands/planning/dashboard.d.ts +0 -30
  75. package/dist/commands/planning/dashboard.d.ts.map +0 -1
  76. package/dist/commands/planning/discuss.d.ts +0 -39
  77. package/dist/commands/planning/discuss.d.ts.map +0 -1
  78. package/dist/commands/planning/plan.d.ts +0 -67
  79. package/dist/commands/planning/plan.d.ts.map +0 -1
  80. package/dist/commands/planning/roadmap.d.ts +0 -105
  81. package/dist/commands/planning/roadmap.d.ts.map +0 -1
  82. package/dist/commands/setup/doctor.d.ts +0 -10
  83. package/dist/commands/setup/doctor.d.ts.map +0 -1
  84. package/dist/commands/setup/map-codebase.d.ts +0 -62
  85. package/dist/commands/setup/map-codebase.d.ts.map +0 -1
  86. package/dist/commands/setup/new-project.d.ts +0 -19
  87. package/dist/commands/setup/new-project.d.ts.map +0 -1
  88. package/dist/commands/setup/settings.d.ts +0 -57
  89. package/dist/commands/setup/settings.d.ts.map +0 -1
  90. package/dist/commands/state/checkpoint.d.ts +0 -27
  91. package/dist/commands/state/checkpoint.d.ts.map +0 -1
  92. package/dist/commands/state/multi-repo.d.ts +0 -63
  93. package/dist/commands/state/multi-repo.d.ts.map +0 -1
  94. package/dist/commands/state/progress.d.ts +0 -57
  95. package/dist/commands/state/progress.d.ts.map +0 -1
  96. package/dist/commands/state/resume.d.ts +0 -11
  97. package/dist/commands/state/resume.d.ts.map +0 -1
  98. package/dist/commands/state/workspace-commands.d.ts +0 -207
  99. package/dist/commands/state/workspace-commands.d.ts.map +0 -1
package/dist/index.js CHANGED
@@ -185,90 +185,6 @@ function parseTDDState(parsed) {
185
185
  return;
186
186
  }
187
187
  }
188
- function serializeTDDState(tdd) {
189
- return JSON.stringify({
190
- stage: tdd.stage,
191
- cycle: tdd.cycle,
192
- behaviors: tdd.behaviors,
193
- regression_test_links: tdd.regression_test_links,
194
- override_log: tdd.override_log,
195
- failing_tests: tdd.failing_tests,
196
- passing_tests: tdd.passing_tests
197
- });
198
- }
199
- function updateTDDState(dir, updates) {
200
- const sp = statePath(dir);
201
- if (!existsSync2(sp))
202
- return;
203
- const state = readPlanningState(dir);
204
- const existingTdd = state["tdd"];
205
- const current = existingTdd ?? {
206
- stage: "behavior",
207
- cycle: 1,
208
- behaviors: [],
209
- regression_test_links: [],
210
- override_log: [],
211
- failing_tests: 0,
212
- passing_tests: 0
213
- };
214
- const updated = { ...current, ...updates };
215
- const tddJson = serializeTDDState(updated);
216
- let content = readFileSync2(sp, "utf-8");
217
- if (content.includes("tdd:")) {
218
- content = content.replace(/^tdd:.*$/m, `tdd: '${tddJson}'`);
219
- } else if (content.startsWith("---")) {
220
- const end = content.indexOf("---", 3);
221
- if (end !== -1) {
222
- content = content.slice(0, end) + `
223
- tdd: '` + tddJson.replace(/'/g, "''") + "'" + content.slice(end);
224
- }
225
- }
226
- content = appendHistory(content, `TDD state updated: stage=${updated.stage}, cycle=${updated.cycle}`);
227
- writeFileSync2(sp, content, "utf-8");
228
- }
229
- function updatePlanningState(dir, updates) {
230
- const sp = statePath(dir);
231
- if (!existsSync2(sp))
232
- return;
233
- let content = readFileSync2(sp, "utf-8");
234
- if (updates.phase !== undefined) {
235
- content = content.replace(/^phase:\s*.*/m, `phase: ${updates.phase}`);
236
- content = appendHistory(content, `Phase changed to ${updates.phase}`);
237
- }
238
- if (updates.status !== undefined) {
239
- content = content.replace(/^status:\s*.*/m, `status: ${updates.status}`);
240
- content = appendHistory(content, `Status changed to ${updates.status}`);
241
- }
242
- if (updates.last_action !== undefined) {
243
- content = content.replace(/^last_action:\s*.*/m, `last_action: "${updates.last_action}"`);
244
- content = appendHistory(content, updates.last_action);
245
- }
246
- if (updates.next_action !== undefined) {
247
- content = content.replace(/^next_action:\s*.*/m, `next_action: "${updates.next_action}"`);
248
- content = appendHistory(content, `Next action: ${updates.next_action}`);
249
- }
250
- if (updates.blockers !== undefined) {
251
- const blockersMd = updates.blockers.length > 0 ? updates.blockers.map((b) => `- ${b}`).join(`
252
- `) : "- none";
253
- content = content.replace(/^## Blockers\n[\s\S]*?(?=\n##|\n#$)/m, `## Blockers
254
- ${blockersMd}
255
- `);
256
- content = appendHistory(content, `Blockers updated: ${updates.blockers.length} item(s)`);
257
- }
258
- if (updates.plan_confirmed !== undefined) {
259
- content = content.replace(/^plan_confirmed:\s*.*/m, `plan_confirmed: ${updates.plan_confirmed}`);
260
- content = appendHistory(content, `Plan confirmed: ${updates.plan_confirmed}`);
261
- }
262
- if (updates.steps_complete !== undefined) {
263
- content = content.replace(/^steps_complete:\s*.*/m, `steps_complete: [${updates.steps_complete.join(", ")}]`);
264
- content = appendHistory(content, `Steps complete: [${updates.steps_complete.join(", ")}]`);
265
- }
266
- if (updates.steps_pending !== undefined) {
267
- content = content.replace(/^steps_pending:\s*.*/m, `steps_pending: [${updates.steps_pending.join(", ")}]`);
268
- content = appendHistory(content, `Steps pending: [${updates.steps_pending.join(", ")}]`);
269
- }
270
- writeFileSync2(sp, content, "utf-8");
271
- }
272
188
  function findWorkspaceRoot(startDir) {
273
189
  let current = startDir;
274
190
  for (;; ) {
@@ -1887,14 +1803,6 @@ function tryTerminalBell() {
1887
1803
  process.stdout.write("\x07");
1888
1804
  } catch {}
1889
1805
  }
1890
- function notifyCommandInteraction(command) {
1891
- const name = command.replace(/^\//, "");
1892
- if (INTERACTIVE_COMMANDS.has(name)) {
1893
- notify(`FlowDeck: /${name}`, "Your input is needed \u2014 please check OpenCode", "critical");
1894
- } else if (COMPLETION_COMMANDS.has(name)) {
1895
- notify(`FlowDeck: /${name} complete`, "Review the output and choose your next step", "info");
1896
- }
1897
- }
1898
1806
  function notifySessionIdle() {
1899
1807
  notify("FlowDeck Task Completed", "Agent is idle and waiting for your next instruction", "info");
1900
1808
  }
@@ -2118,34 +2026,11 @@ function loadStore(dir) {
2118
2026
  return { requests: [] };
2119
2027
  }
2120
2028
  }
2121
- function saveStore(dir, store) {
2122
- const cd = codebaseDir(dir);
2123
- if (!existsSync17(cd))
2124
- mkdirSync9(cd, { recursive: true });
2125
- writeFileSync12(approvalsPath(dir), JSON.stringify(store, null, 2), "utf-8");
2126
- }
2127
- function resolveApproval(dir, approval_id, decision) {
2128
- const store = loadStore(dir);
2129
- const req = store.requests.find((r) => r.id === approval_id);
2130
- if (!req)
2131
- return false;
2132
- req.status = decision;
2133
- req.resolved_at = new Date().toISOString();
2134
- saveStore(dir, store);
2135
- return true;
2136
- }
2137
2029
  function checkApproval(dir, file_path, command) {
2138
2030
  const store = loadStore(dir);
2139
2031
  const now = Date.now();
2140
2032
  return store.requests.filter((r) => r.status === "approved" && r.resolved_at && (r.file_path === file_path || r.trigger === command) && now - new Date(r.resolved_at).getTime() < APPROVAL_TTL_MS).sort((a, b) => b.resolved_at.localeCompare(a.resolved_at)).at(0) ?? null;
2141
2033
  }
2142
- function getPendingApprovals(dir) {
2143
- return loadStore(dir).requests.filter((r) => r.status === "pending");
2144
- }
2145
- function getRecentApprovals(dir, limit = 10) {
2146
- const store = loadStore(dir);
2147
- return store.requests.slice(-limit).reverse();
2148
- }
2149
2034
 
2150
2035
  // src/hooks/approval-hook.ts
2151
2036
  var WRITE_TOOLS = new Set(["write_file", "edit_file", "create_file", "apply_patch", "str_replace_editor", "write"]);
@@ -2498,4073 +2383,20 @@ function createFlowDeckMcps() {
2498
2383
  return mcps;
2499
2384
  }
2500
2385
 
2501
- // src/commands/setup/new-project.ts
2502
- import { writeFileSync as writeFileSync13, existsSync as existsSync20, mkdirSync as mkdirSync10 } from "fs";
2503
- import { join as join19 } from "path";
2504
- var PLANNING_FILES = {
2505
- "PROJECT.md": `# Project
2506
-
2507
- **Name:** (set via /discuss)
2508
- **Description:** (set via /discuss)
2509
- **Tech stack:** (set via /discuss)
2510
-
2511
- ---
2512
-
2513
- ## Goals
2514
-
2515
- -
2516
-
2517
- ## Non-negotiables
2518
-
2519
- -
2520
-
2521
- ## Out of Scope
2522
-
2523
- -
2524
- `,
2525
- "REQUIREMENTS.md": `# Requirements
2526
-
2527
- **Project:** (set via /discuss)
2528
- **Version:** 1.0
2529
-
2530
- ---
2531
-
2532
- ## v1 Requirements
2533
-
2534
-
2535
- `,
2536
- "ROADMAP.md": `# Roadmap
2537
-
2538
- **Project:** (set via /discuss)
2539
- **Version:** 1.0
2540
-
2541
- ---
2542
-
2543
- ## Overview
2544
-
2545
- | Phase | Name | Purpose |
2546
- |-------|------|---------|
2547
- | 1 | Plugin Infrastructure & Core Tools | |
2548
- | 2 | Hooks & Session Lifecycle | |
2549
- | 3 | Agent Definitions | |
2550
- | 4 | Setup & Planning Commands | |
2551
- | 5 | Execution Commands | |
2552
- | 6 | Installation & Documentation | |
2553
-
2554
- ---
2555
- `,
2556
- "STATE.md": `---
2557
- flowdeck_state_version: 1.0
2558
- milestone: v1.0
2559
- last_updated: "TEMPLATE_TIMESTAMP"
2560
- progress:
2561
- total_phases: 6
2562
- completed_phases: 0
2563
- ---
2564
-
2565
- # State
2566
-
2567
- **Project:** (set via /discuss)
2568
- **Last updated:** TEMPLATE_TIMESTAMP
2569
-
2570
- ## Current Phase
2571
-
2572
- phase: 1
2573
- status: planned
2574
- plan_file: none
2575
- plan_confirmed: false
2576
- confirmed_at: none
2577
-
2578
- ## Progress
2579
-
2580
- last_action: ""
2581
- next_action: "Run /discuss to start planning"
2582
- steps_complete: []
2583
- steps_pending: []
2584
-
2585
- ## Blockers
2586
-
2587
- - none
2588
-
2589
- ## Session History
2590
-
2591
- `,
2592
- "config.json": `{
2593
- "workflow": {
2594
- "parallelization": true,
2595
- "auto_advance": false,
2596
- "discuss_mode": "discuss"
2597
- },
2598
- "agents": {
2599
- "orchestrator": "anthropic/claude-sonnet-4-5",
2600
- "discusser": "anthropic/claude-sonnet-4-5",
2601
- "mapper": "google/gemini-2.5-flash",
2602
- "coder": "anthropic/claude-opus-4-5",
2603
- "reviewer": "google/gemini-2.5-flash",
2604
- "researcher": "openai/gpt-4o",
2605
- "tester": "anthropic/claude-haiku-4-5",
2606
- "writer": "anthropic/claude-haiku-4-5"
2607
- }
2608
- }
2609
- `
2610
- };
2611
- var newProjectCommand = {
2612
- name: "fd-new-project",
2613
- description: "Initialize .planning/ structure for greenfield projects",
2614
- async execute(context) {
2615
- const dir = context.directory ?? process.cwd();
2616
- const pd = planningDir(dir);
2617
- if (existsSync20(pd)) {
2618
- return {
2619
- error: `.planning/ already exists. Use /resume to continue or /progress to see state.`,
2620
- code: "ALREADY_EXISTS"
2621
- };
2622
- }
2623
- mkdirSync10(pd, { recursive: true });
2624
- mkdirSync10(join19(pd, "phases"), { recursive: true });
2625
- const timestamp2 = new Date().toISOString();
2626
- for (const [filename, content] of Object.entries(PLANNING_FILES)) {
2627
- const filePath = join19(pd, filename);
2628
- if (filename === "STATE.md") {
2629
- const stateContent = content.replace(/TEMPLATE_TIMESTAMP/g, timestamp2);
2630
- writeFileSync13(filePath, stateContent, "utf-8");
2631
- } else if (filename === "config.json") {
2632
- writeFileSync13(filePath, content, "utf-8");
2633
- } else {
2634
- writeFileSync13(filePath, content, "utf-8");
2635
- }
2636
- }
2637
- return {
2638
- success: true,
2639
- message: `.planning/ structure created. Run /discuss to capture project decisions.`,
2640
- files: Object.keys(PLANNING_FILES)
2641
- };
2642
- }
2643
- };
2644
-
2645
- // src/commands/setup/map-codebase.ts
2646
- import { existsSync as existsSync22, readdirSync as readdirSync3, readFileSync as readFileSync21 } from "fs";
2647
- import { join as join21, relative, extname as extname2 } from "path";
2648
-
2649
- // src/lib/timestamps.ts
2650
- import { readFileSync as readFileSync20, writeFileSync as writeFileSync14, existsSync as existsSync21, mkdirSync as mkdirSync11, statSync as statSync2 } from "fs";
2651
- import { join as join20, extname } from "path";
2652
- import { createHash as createHash2 } from "crypto";
2653
- function loadTimestamps(dir) {
2654
- const path = join20(dir, ".codebase", ".meta", "timestamps.json");
2655
- if (!existsSync21(path))
2656
- return null;
2657
- try {
2658
- return JSON.parse(readFileSync20(path, "utf-8"));
2659
- } catch {
2660
- return null;
2661
- }
2662
- }
2663
- function saveTimestamps(dir, data) {
2664
- const metaDir = join20(dir, ".codebase", ".meta");
2665
- if (!existsSync21(metaDir))
2666
- mkdirSync11(metaDir, { recursive: true });
2667
- writeFileSync14(join20(metaDir, "timestamps.json"), JSON.stringify(data, null, 2), "utf-8");
2668
- }
2669
- function computeFileHash(filePath) {
2670
- const content = readFileSync20(filePath);
2671
- return createHash2("sha256").update(content).digest("hex");
2672
- }
2673
- function getFileMetadata(filePath, _baseDir) {
2674
- const stat = statSync2(filePath);
2675
- const type = extname(filePath);
2676
- const hash = computeFileHash(filePath);
2677
- return { mtime: stat.mtimeMs, size: stat.size, hash, type };
2678
- }
2679
- function checkFileChanged(filePath, storedMeta, _baseDir) {
2680
- if (!existsSync21(filePath)) {
2681
- return { changed: false };
2682
- }
2683
- const stat = statSync2(filePath);
2684
- const currentMtime = stat.mtimeMs;
2685
- if (!storedMeta) {
2686
- return { changed: true, reason: "new", currentMeta: getFileMetadata(filePath, _baseDir) };
2687
- }
2688
- if (Math.abs(currentMtime - storedMeta.mtime) > 1000) {
2689
- return { changed: true, reason: "mtime_changed", currentMeta: getFileMetadata(filePath, _baseDir) };
2690
- }
2691
- return { changed: false };
2692
- }
2693
-
2694
- // src/lib/signatures.ts
2695
- var SIGNATURE_PATTERNS = [
2696
- /import\s+(?:(?:type\s+)?(?:\{[^}]*\}|\*\s+as\s+\w+|\w+)\s+from\s+)?['"]([^'"]+)['"]/g,
2697
- /export\s+(?:\{[^}]*\}|\*)\s+from\s+['"]([^'"]+)['"]/g,
2698
- /require\s*\(\s*['"]([^'"]+)['"]\s*\)/g
2699
- ];
2700
- function extractSignatures(content) {
2701
- const signatures = [];
2702
- const lines = content.split(`
2703
- `);
2704
- for (let lineIndex = 0;lineIndex < lines.length; lineIndex++) {
2705
- const line = lines[lineIndex];
2706
- for (const pattern of SIGNATURE_PATTERNS) {
2707
- pattern.lastIndex = 0;
2708
- let match;
2709
- while ((match = pattern.exec(line)) !== null) {
2710
- if (match[1]) {
2711
- signatures.push(`import:${match[1]}:${lineIndex + 1}`);
2712
- }
2713
- }
2714
- }
2715
- }
2716
- return signatures;
2717
- }
2718
- function detectConflicts(oldSigs, newSigs) {
2719
- const conflicts = [];
2720
- for (const [file, newFileSigs] of Object.entries(newSigs)) {
2721
- const oldFileSigs = oldSigs[file] || [];
2722
- if (JSON.stringify(oldFileSigs.sort()) !== JSON.stringify(newFileSigs.sort())) {
2723
- conflicts.push({
2724
- file,
2725
- oldCount: oldFileSigs.length,
2726
- newCount: newFileSigs.length,
2727
- affectedImports: newFileSigs.filter((s) => !oldFileSigs.includes(s))
2728
- });
2729
- }
2730
- }
2731
- return { conflicts };
2732
- }
2733
-
2734
- // src/lib/confirmation.ts
2735
- function confirmPrompt(operation, message) {
2736
- return {
2737
- success: true,
2738
- message,
2739
- status: "AWAITING_CONFIRM",
2740
- confirm_mode: "y/n",
2741
- operation
2742
- };
2743
- }
2744
- function skipResponse(operation) {
2745
- return {
2746
- success: true,
2747
- message: `${operation} skipped. No changes made.`,
2748
- skipped: true,
2749
- operation
2750
- };
2751
- }
2752
-
2753
- // src/commands/setup/map-codebase.ts
2754
- var SUPPORTED_EXTENSIONS = new Set([".ts", ".tsx", ".js", ".jsx", ".json"]);
2755
- function discoverSourceFiles(dir) {
2756
- const files = [];
2757
- const walk = (d) => {
2758
- const entries = readdirSync3(d, { withFileTypes: true });
2759
- for (const entry of entries) {
2760
- const full = join21(d, entry.name);
2761
- if (entry.name === ".codebase" || entry.name === "node_modules" || entry.name === ".planning")
2762
- continue;
2763
- if (entry.isDirectory()) {
2764
- walk(full);
2765
- } else if (SUPPORTED_EXTENSIONS.has(extname2(entry.name))) {
2766
- files.push(relative(dir, full));
2767
- }
2768
- }
2769
- };
2770
- walk(dir);
2771
- return files;
2772
- }
2773
- function extractSignaturesFromFile(filePath) {
2774
- if (!existsSync22(filePath))
2775
- return [];
2776
- const content = readFileSync21(filePath, "utf-8");
2777
- return extractSignatures(content);
2778
- }
2779
- var mapCodebaseCommand = {
2780
- name: "fd-map-codebase",
2781
- description: "Parallel analysis agents \u2192 .codebase/ docs (STACK, ARCHITECTURE, STRUCTURE, CONVENTIONS, TESTING, CONCERNS). Use --incremental to process only changed files.",
2782
- async execute(context, args) {
2783
- const dir = context.directory ?? process.cwd();
2784
- const pd = planningDir(dir);
2785
- const incremental = args?.incremental === true;
2786
- const statePath2 = join21(pd, "STATE.md");
2787
- if (!existsSync22(statePath2)) {
2788
- return {
2789
- error: "STATE.md not found. Run /new-project first to initialize the project.",
2790
- code: "NOT_INITIALIZED"
2791
- };
2792
- }
2793
- if (incremental) {
2794
- const timestamps = loadTimestamps(dir);
2795
- if (!timestamps) {
2796
- return {
2797
- error: "Incremental mode requires prior run. Run `/map-codebase` without --incremental first.",
2798
- code: "NO_TIMESTAMPS"
2799
- };
2800
- }
2801
- return this.runIncremental(dir, timestamps);
2802
- }
2803
- return this.runFullRebuild(dir, args);
2804
- },
2805
- async runFullRebuild(dir, args) {
2806
- const codebasePath = join21(dir, ".codebase");
2807
- if (existsSync22(codebasePath)) {
2808
- if (!args?.yes) {
2809
- return {
2810
- ...confirmPrompt("map-codebase-overwrite", ".codebase/ already exists. Running /map-codebase will overwrite existing docs. Proceed? [y/n]"),
2811
- code: "EXISTS",
2812
- hint: "Use --yes to skip this confirmation"
2813
- };
2814
- }
2815
- }
2816
- const sourceFiles = discoverSourceFiles(dir);
2817
- const files = {};
2818
- const signatures = {};
2819
- const now = new Date().toISOString();
2820
- for (const relPath of sourceFiles) {
2821
- const fullPath = join21(dir, relPath);
2822
- const meta = getFileMetadata(fullPath, dir);
2823
- files[relPath] = meta;
2824
- signatures[relPath] = extractSignaturesFromFile(fullPath);
2825
- }
2826
- const timestampsData = {
2827
- version: "1.0",
2828
- last_run: now,
2829
- files,
2830
- signatures
2831
- };
2832
- saveTimestamps(dir, timestampsData);
2833
- return {
2834
- success: true,
2835
- message: `Starting parallel codebase mapping.`,
2836
- workflow: "map-codebase-flow.md",
2837
- output_dir: ".codebase/",
2838
- docs: ["STACK.md", "ARCHITECTURE.md", "STRUCTURE.md", "CONVENTIONS.md", "TESTING.md", "CONCERNS.md"],
2839
- next_step: "Workflow will spawn 6 mapper agents in parallel",
2840
- incremental_mode: false,
2841
- files_processed: sourceFiles.length
2842
- };
2843
- },
2844
- async runIncremental(dir, timestamps) {
2845
- const sourceFiles = discoverSourceFiles(dir);
2846
- const toProcess = [];
2847
- const unchanged = [];
2848
- for (const relPath of sourceFiles) {
2849
- const fullPath = join21(dir, relPath);
2850
- const storedMeta = timestamps.files[relPath];
2851
- const result = checkFileChanged(fullPath, storedMeta, dir);
2852
- if (result.changed) {
2853
- toProcess.push(relPath);
2854
- } else {
2855
- unchanged.push(relPath);
2856
- }
2857
- }
2858
- const newFiles = {};
2859
- const newSignatures = {};
2860
- const now = new Date().toISOString();
2861
- for (const relPath of toProcess) {
2862
- const fullPath = join21(dir, relPath);
2863
- const meta = getFileMetadata(fullPath, dir);
2864
- newFiles[relPath] = meta;
2865
- newSignatures[relPath] = extractSignaturesFromFile(fullPath);
2866
- }
2867
- const mergedFiles = { ...timestamps.files };
2868
- const mergedSignatures = { ...timestamps.signatures };
2869
- for (const relPath of toProcess) {
2870
- mergedFiles[relPath] = newFiles[relPath];
2871
- mergedSignatures[relPath] = newSignatures[relPath];
2872
- }
2873
- const conflictReport = detectConflicts(timestamps.signatures, mergedSignatures);
2874
- const timestampsData = {
2875
- version: "1.0",
2876
- last_run: now,
2877
- files: mergedFiles,
2878
- signatures: mergedSignatures
2879
- };
2880
- saveTimestamps(dir, timestampsData);
2881
- const response = {
2882
- success: true,
2883
- message: `Incremental mapping complete.`,
2884
- incremental_mode: true,
2885
- files_processed: toProcess.length,
2886
- files_unchanged: unchanged.length,
2887
- workflow: "map-codebase-flow.md",
2888
- output_dir: ".codebase/",
2889
- next_step: "Workflow will spawn mapper agents for changed files"
2890
- };
2891
- if (conflictReport.conflicts.length > 0) {
2892
- response.warning = `Cross-file reference conflicts detected in ${conflictReport.conflicts.length} file(s):
2893
- ` + conflictReport.conflicts.map((c) => ` - ${c.file}: ${c.oldCount} -> ${c.newCount} signatures (${c.affectedImports.length} changed)`).join(`
2894
- `);
2895
- response.code = "SIGNATURE_CONFLICTS";
2896
- }
2897
- return response;
2898
- }
2899
- };
2900
-
2901
- // src/commands/setup/settings.ts
2902
- import { existsSync as existsSync23, readFileSync as readFileSync22, writeFileSync as writeFileSync15 } from "fs";
2903
- import { join as join22 } from "path";
2904
- var DEFAULT_MODELS = {
2905
- orchestrator: "anthropic/claude-sonnet-4-5",
2906
- discusser: "anthropic/claude-sonnet-4-5",
2907
- mapper: "google/gemini-2.5-flash",
2908
- coder: "anthropic/claude-opus-4-5",
2909
- reviewer: "google/gemini-2.5-flash",
2910
- researcher: "openai/gpt-4o",
2911
- tester: "anthropic/claude-haiku-4-5",
2912
- writer: "anthropic/claude-haiku-4-5"
2913
- };
2914
- var MODEL_PROFILES = {
2915
- quality: {
2916
- description: "Best quality (higher cost)",
2917
- agents: {
2918
- orchestrator: "anthropic/claude-opus-4-5",
2919
- discusser: "anthropic/claude-opus-4-5",
2920
- coder: "anthropic/claude-opus-4-5"
2921
- }
2922
- },
2923
- balanced: {
2924
- description: "Balanced quality/cost",
2925
- agents: {
2926
- orchestrator: "anthropic/claude-sonnet-4-5",
2927
- discusser: "anthropic/claude-sonnet-4-5",
2928
- coder: "anthropic/claude-sonnet-4-5"
2929
- }
2930
- },
2931
- budget: {
2932
- description: "Lowest cost",
2933
- agents: {
2934
- orchestrator: "anthropic/claude-haiku-4-5",
2935
- discusser: "anthropic/claude-haiku-4-5",
2936
- coder: "anthropic/claude-haiku-4-5"
2937
- }
2938
- }
2939
- };
2940
- var settingsCommand = {
2941
- name: "fd-settings",
2942
- description: "Interactive configurator for agent models, profiles, and workflow toggles",
2943
- async execute(context, args) {
2944
- const dir = context.directory ?? process.cwd();
2945
- const configPath = join22(dir, "opencode.json");
2946
- if (!args || !args.profile && !args.agent && !args.toggle) {
2947
- return showCurrentSettings(configPath);
2948
- }
2949
- if (args.profile && MODEL_PROFILES[args.profile]) {
2950
- return applyProfile(configPath, args.profile);
2951
- }
2952
- if (args.agent && args.model) {
2953
- return setAgentModel(configPath, args.agent, args.model);
2954
- }
2955
- if (args.toggle) {
2956
- return toggleWorkflowPhase(configPath, args.toggle, args.value === "true" || args.value === "on");
2957
- }
2958
- return {
2959
- error: "Invalid settings command. Use --profile, --agent, or --toggle",
2960
- code: "INVALID_ARGS"
2961
- };
2962
- }
2963
- };
2964
- function showCurrentSettings(configPath) {
2965
- const profileExists = existsSync23(configPath);
2966
- let config = {};
2967
- if (profileExists) {
2968
- try {
2969
- config = JSON.parse(readFileSync22(configPath, "utf-8"));
2970
- } catch {}
2971
- }
2972
- const agents = config.agents || DEFAULT_MODELS;
2973
- const output = [
2974
- "\u2550".repeat(55),
2975
- "FLOWDECK SETTINGS",
2976
- "\u2550".repeat(55),
2977
- "",
2978
- "Available profiles:",
2979
- " --profile quality | Best quality (higher cost)",
2980
- " --profile balanced | Balanced quality/cost",
2981
- " --profile budget | Lowest cost",
2982
- "",
2983
- "Agent model overrides:",
2984
- " --agent <name> --model <model>",
2985
- " Valid agents: orchestrator, discusser, mapper, coder, reviewer, researcher, tester, writer",
2986
- "",
2987
- "Workflow toggles:",
2988
- " --toggle <phase> --value <true|false>",
2989
- " Example: --toggle research --value true",
2990
- "",
2991
- "Current agent models:"
2992
- ];
2993
- for (const [agent, model] of Object.entries(agents)) {
2994
- output.push(` ${agent}: ${model}`);
2995
- }
2996
- output.push("\u2550".repeat(55));
2997
- return {
2998
- success: true,
2999
- message: output.join(`
3000
- `),
3001
- meta: { formatted: "table", timestamp: timestamp() }
3002
- };
3003
- }
3004
- function applyProfile(configPath, profile) {
3005
- const profileData = MODEL_PROFILES[profile];
3006
- let config = {};
3007
- if (existsSync23(configPath)) {
3008
- try {
3009
- config = JSON.parse(readFileSync22(configPath, "utf-8"));
3010
- } catch {
3011
- config = {};
3012
- }
3013
- }
3014
- config.agents = config.agents || {};
3015
- for (const [agent, model] of Object.entries(profileData.agents)) {
3016
- config.agents[agent] = model;
3017
- }
3018
- config.model_profile = profile;
3019
- writeFileSync15(configPath, JSON.stringify(config, null, 2), "utf-8");
3020
- return {
3021
- success: true,
3022
- message: `Applied "${profile}" profile: ${profileData.description}`,
3023
- profile,
3024
- agents: profileData.agents
3025
- };
3026
- }
3027
- function setAgentModel(configPath, agent, model) {
3028
- const validAgents = Object.keys(DEFAULT_MODELS);
3029
- if (!validAgents.includes(agent)) {
3030
- return {
3031
- error: `Invalid agent "${agent}". Valid: ${validAgents.join(", ")}`,
3032
- code: "INVALID_AGENT"
3033
- };
3034
- }
3035
- let config = {};
3036
- if (existsSync23(configPath)) {
3037
- try {
3038
- config = JSON.parse(readFileSync22(configPath, "utf-8"));
3039
- } catch {
3040
- config = {};
3041
- }
3042
- }
3043
- config.agents = config.agents || {};
3044
- config.agents[agent] = model;
3045
- writeFileSync15(configPath, JSON.stringify(config, null, 2), "utf-8");
3046
- return {
3047
- success: true,
3048
- message: `Set ${agent} model to ${model}`,
3049
- agent,
3050
- model
3051
- };
3052
- }
3053
- function toggleWorkflowPhase(configPath, phase, enabled) {
3054
- let config = {};
3055
- if (existsSync23(configPath)) {
3056
- try {
3057
- config = JSON.parse(readFileSync22(configPath, "utf-8"));
3058
- } catch {
3059
- config = {};
3060
- }
3061
- }
3062
- config.workflow = config.workflow || {};
3063
- config.workflow[phase] = enabled;
3064
- writeFileSync15(configPath, JSON.stringify(config, null, 2), "utf-8");
3065
- return {
3066
- success: true,
3067
- message: `Workflow phase "${phase}" ${enabled ? "enabled" : "disabled"}`,
3068
- phase,
3069
- enabled
3070
- };
3071
- }
3072
-
3073
- // src/commands/setup/doctor.ts
3074
- import { execSync } from "child_process";
3075
- import { existsSync as existsSync24, readFileSync as readFileSync23 } from "fs";
3076
- import { join as join23 } from "path";
3077
- var doctorCommand = {
3078
- name: "fd-doctor",
3079
- description: "Check FlowDeck installation and environment health",
3080
- async execute(context) {
3081
- const results = ["# FlowDeck Doctor Report", ""];
3082
- let healthy = true;
3083
- try {
3084
- const version = execSync("opencode --version", { encoding: "utf-8" }).trim();
3085
- results.push(`- [x] OpenCode detected: ${version}`);
3086
- } catch {
3087
- results.push("- [ ] OpenCode CLI not found on PATH");
3088
- healthy = false;
3089
- }
3090
- const configDir = process.env.OPENCODE_CONFIG_DIR || join23(process.env.HOME || "", ".config", "opencode");
3091
- const configFile = join23(configDir, "opencode.json");
3092
- if (existsSync24(configFile)) {
3093
- try {
3094
- const cfg = JSON.parse(readFileSync23(configFile, "utf-8"));
3095
- if (cfg.plugin && cfg.plugin.includes("@dv.nghiem/flowdeck")) {
3096
- results.push(`- [x] FlowDeck registered in ${configFile}`);
3097
- } else {
3098
- results.push(`- [ ] FlowDeck NOT registered in ${configFile}`);
3099
- healthy = false;
3100
- }
3101
- } catch {
3102
- results.push("- [ ] opencode.json is malformed");
3103
- healthy = false;
3104
- }
3105
- } else {
3106
- results.push(`- [ ] Configuration file ${configFile} not found`);
3107
- healthy = false;
3108
- }
3109
- const dir = context.directory ?? process.cwd();
3110
- const statePath2 = join23(dir, ".planning", "STATE.md");
3111
- if (existsSync24(statePath2)) {
3112
- results.push("- [x] .planning/STATE.md exists in current workspace");
3113
- } else {
3114
- results.push("- [!] No .planning/STATE.md found (run /new-project to initialize)");
3115
- }
3116
- results.push("", healthy ? "\u2705 Environment looks healthy!" : "\u274C Some issues were found. Please check the report above.");
3117
- return {
3118
- content: results.join(`
3119
- `),
3120
- healthy
3121
- };
3122
- }
3123
- };
3124
-
3125
- // src/commands/planning/discuss.ts
3126
- import { readFileSync as readFileSync25, existsSync as existsSync26, mkdirSync as mkdirSync13 } from "fs";
3127
- import { join as join25 } from "path";
3128
-
3129
- // src/lib/impact-radar.ts
3130
- import { existsSync as existsSync25, readFileSync as readFileSync24 } from "fs";
3131
- import { join as join24 } from "path";
3132
- function matchWords(text, words) {
3133
- const lower = text.toLowerCase();
3134
- return words.some((w) => w.length > 3 && lower.includes(w));
3135
- }
3136
- function runImpactRadar(dir, changeText) {
3137
- const cd = codebaseDir(dir);
3138
- const words = changeText.toLowerCase().split(/\s+/);
3139
- const hotspots = [];
3140
- const known_failures = [];
3141
- const related_modules = [];
3142
- const volatilityPath2 = join24(cd, "VOLATILITY.json");
3143
- if (existsSync25(volatilityPath2)) {
3144
- try {
3145
- const v = JSON.parse(readFileSync24(volatilityPath2, "utf-8"));
3146
- for (const e of v.entries ?? []) {
3147
- if ((e.stability === "volatile" || e.stability === "critical") && matchWords(e.path, words)) {
3148
- hotspots.push({ path: e.path, stability: e.stability });
3149
- }
3150
- }
3151
- } catch {}
3152
- }
3153
- const failuresPath2 = join24(cd, "FAILURES.json");
3154
- if (existsSync25(failuresPath2)) {
3155
- try {
3156
- const f = JSON.parse(readFileSync24(failuresPath2, "utf-8"));
3157
- for (const e of f.entries ?? []) {
3158
- if (!e.tags?.includes("resolved") && matchWords(e.description ?? "", words)) {
3159
- known_failures.push({
3160
- id: e.id,
3161
- description: e.description,
3162
- affected_paths: e.affected_paths ?? [],
3163
- recurrence_count: e.recurrence_count ?? 1
3164
- });
3165
- }
3166
- }
3167
- } catch {}
3168
- }
3169
- const memoryPath2 = join24(cd, "MEMORY.json");
3170
- if (existsSync25(memoryPath2)) {
3171
- try {
3172
- const m = JSON.parse(readFileSync24(memoryPath2, "utf-8"));
3173
- for (const node of Object.values(m.nodes ?? {})) {
3174
- if (matchWords(node.path ?? "", words)) {
3175
- related_modules.push({ path: node.path, type: node.type, owner: node.owner });
3176
- }
3177
- }
3178
- } catch {}
3179
- }
3180
- const risk_flag = hotspots.length > 0 || known_failures.length > 0;
3181
- const advisory = risk_flag ? `\u26A0 Impact Radar: ${hotspots.length} volatile zone(s) and ${known_failures.length} known failure(s) match this change. Review before proceeding.` : null;
3182
- return { hotspots, known_failures, related_modules, risk_flag, advisory, score: risk_flag ? 0.7 : 0.3 };
3183
- }
3184
- function impactRadarSummaryLines(radar) {
3185
- if (!radar.risk_flag && radar.related_modules.length === 0)
3186
- return [];
3187
- const lines = ["\u2500".repeat(55), " Impact Radar:"];
3188
- if (radar.hotspots.length > 0) {
3189
- lines.push(` \u26A0 Volatile zones: ${radar.hotspots.map((h) => h.path).join(", ")}`);
3190
- }
3191
- if (radar.known_failures.length > 0) {
3192
- lines.push(` \u26A0 Known failures: ${radar.known_failures.map((f) => f.id).join(", ")}`);
3193
- }
3194
- if (radar.related_modules.length > 0) {
3195
- lines.push(` \u2139 Related modules: ${radar.related_modules.map((m) => m.path).join(", ")}`);
3196
- }
3197
- return lines;
3198
- }
3199
- function lookupPriorFailures(dir, scope, bugText, limit = 5) {
3200
- const cd = codebaseDir(dir);
3201
- const failuresPath2 = join24(cd, "FAILURES.json");
3202
- if (!existsSync25(failuresPath2))
3203
- return [];
3204
- try {
3205
- const store = JSON.parse(readFileSync24(failuresPath2, "utf-8"));
3206
- const entries = store.entries ?? [];
3207
- const words = bugText.toLowerCase().split(/\s+/).filter((w) => w.length > 3);
3208
- const scopePrefix = scope !== "all" ? scope.replace(/^\.\//, "") : "";
3209
- const matched = entries.filter((e) => !e.tags?.includes("resolved")).filter((e) => {
3210
- const pathMatch = scopePrefix ? e.affected_paths.some((p) => p.includes(scopePrefix)) : false;
3211
- const keywordMatch = words.some((w) => (e.description ?? "").toLowerCase().includes(w));
3212
- return pathMatch || keywordMatch;
3213
- }).sort((a, b) => (b.recurrence_count ?? 1) - (a.recurrence_count ?? 1));
3214
- return matched.slice(0, limit);
3215
- } catch {
3216
- return [];
3217
- }
3218
- }
3219
-
3220
- // src/commands/planning/discuss.ts
3221
- var discussCommand = {
3222
- name: "fd-discuss",
3223
- description: "Extract requirements via @discusser Q&A \u2014 saves decisions to .planning/phases/phase-N/DISCUSS.md with D-XX numbering",
3224
- async execute(context, args) {
3225
- const dir = context.directory ?? process.cwd();
3226
- const sp = statePath(dir);
3227
- const pd = planningDir(dir);
3228
- if (!existsSync26(sp)) {
3229
- return {
3230
- error: "STATE.md not found. Run /new-project first to initialize the project.",
3231
- code: "NOT_INITIALIZED"
3232
- };
3233
- }
3234
- const stateContent = readFileSync25(sp, "utf-8");
3235
- const phaseMatch = stateContent.match(/^phase:\s*(\d+)/m);
3236
- if (!phaseMatch) {
3237
- return { error: "No phase found in STATE.md. Project may be corrupted." };
3238
- }
3239
- const phase = parseInt(phaseMatch[1], 10);
3240
- const projectPath = join25(pd, "PROJECT.md");
3241
- if (!existsSync26(projectPath)) {
3242
- return { error: "PROJECT.md not found. Run /new-project first." };
3243
- }
3244
- const phaseDir = join25(pd, "phases", `phase-${phase}`);
3245
- if (!existsSync26(phaseDir)) {
3246
- mkdirSync13(phaseDir, { recursive: true });
3247
- }
3248
- const topic = args?.topic ?? "general";
3249
- const radar = runImpactRadar(dir, topic);
3250
- return {
3251
- success: true,
3252
- message: `Discuss phase started for phase ${phase}.`,
3253
- topic,
3254
- workflow: "discuss-flow.md",
3255
- phase_dir: phaseDir,
3256
- impact_radar: radar,
3257
- next_step: radar.risk_flag ? "Review impact_radar risks before finalizing decisions with @discusser" : "Review workflow output and respond to @discusser questions"
3258
- };
3259
- }
3260
- };
3261
-
3262
- // src/commands/planning/plan.ts
3263
- import { readFileSync as readFileSync26, existsSync as existsSync27, writeFileSync as writeFileSync16, mkdirSync as mkdirSync14 } from "fs";
3264
- import { join as join26 } from "path";
3265
- function buildImpactRadarSection(dir, changeText) {
3266
- const radar = runImpactRadar(dir, changeText);
3267
- const lines = ["## Change Impact Radar", ""];
3268
- if (!radar.risk_flag && radar.related_modules.length === 0) {
3269
- lines.push("_No volatility or failure signals found for this change. Proceed with standard review._");
3270
- lines.push("");
3271
- return lines.join(`
3272
- `);
3273
- }
3274
- if (radar.hotspots.length > 0) {
3275
- lines.push("### Volatile Zones Touched");
3276
- for (const h of radar.hotspots)
3277
- lines.push(`- \`${h.path}\` \u2014 ${h.stability}`);
3278
- lines.push("");
3279
- }
3280
- if (radar.known_failures.length > 0) {
3281
- lines.push("### Known Failures (Unresolved)");
3282
- for (const e of radar.known_failures.slice(0, 5)) {
3283
- lines.push(`- **${e.id}** (\xD7${e.recurrence_count}): ${e.description.substring(0, 80)}`);
3284
- }
3285
- lines.push("");
3286
- }
3287
- if (radar.related_modules.length > 0) {
3288
- lines.push("### Related Architecture Nodes");
3289
- for (const n of radar.related_modules.slice(0, 8)) {
3290
- lines.push(`- \`${n.path}\` (${n.type})${n.owner ? ` \u2014 owner: ${n.owner}` : ""}`);
3291
- }
3292
- lines.push("");
3293
- }
3294
- return lines.join(`
3295
- `);
3296
- }
3297
- var planCommand = {
3298
- name: "fd-plan",
3299
- description: "Create detailed implementation plan from DISCUSS.md decisions \u2014 save PLAN.md, update STATE.md, require CONFIRM before execution",
3300
- async execute(context, args) {
3301
- const dir = context.directory ?? process.cwd();
3302
- const sp = statePath(dir);
3303
- const pd = planningDir(dir);
3304
- if (!existsSync27(sp)) {
3305
- return {
3306
- error: "STATE.md not found. Run /new-project first.",
3307
- code: "NOT_INITIALIZED"
3308
- };
3309
- }
3310
- const stateContent = readFileSync26(sp, "utf-8");
3311
- const confirmedMatch = stateContent.match(/^plan_confirmed:\s*(true|false)/m);
3312
- const isConfirmed = confirmedMatch && confirmedMatch[1] === "true";
3313
- let phase;
3314
- if (args?.phase) {
3315
- phase = parseInt(args.phase, 10);
3316
- } else {
3317
- const phaseMatch = stateContent.match(/^phase:\s*(\d+)/m);
3318
- if (!phaseMatch) {
3319
- return { error: "No phase found in STATE.md." };
3320
- }
3321
- phase = parseInt(phaseMatch[1], 10);
3322
- }
3323
- const discussPath = join26(pd, "phases", `phase-${phase}`, "DISCUSS.md");
3324
- if (!existsSync27(discussPath)) {
3325
- return {
3326
- error: "DISCUSS.md not found. Run /discuss [topic] first to capture decisions.",
3327
- code: "NO_DISCUSS",
3328
- hint: `No DISCUSS.md found for phase ${phase}`
3329
- };
3330
- }
3331
- if (args?.yes && !isConfirmed) {
3332
- args = { ...args, confirm: true };
3333
- }
3334
- if (!isConfirmed && !args?.confirm) {
3335
- const discussContent2 = readFileSync26(discussPath, "utf-8");
3336
- const decisions = (discussContent2.match(/^D-\d+/gm) || []).length;
3337
- const lines = discussContent2.split(`
3338
- `).slice(0, 50);
3339
- const preview = [
3340
- "\u2550".repeat(55),
3341
- `PLAN PHASE ${phase} \u2014 AWAITING CONFIRMATION`,
3342
- "\u2550".repeat(55),
3343
- "",
3344
- `DISCUSS.md: ${discussPath}`,
3345
- `Decisions found: ${decisions}`,
3346
- "",
3347
- "Preview:",
3348
- ...lines.map((l) => ` ${l}`),
3349
- "",
3350
- "\u2500".repeat(55),
3351
- "Type CONFIRM to save PLAN.md and enable execution",
3352
- "\u2550".repeat(55)
3353
- ];
3354
- return {
3355
- ...confirmPrompt("plan-confirm", preview.join(`
3356
- `)),
3357
- phase,
3358
- decisions_found: decisions,
3359
- workflow: "plan-flow.md",
3360
- next_step: "Type CONFIRM to save, or run /discuss to modify decisions first"
3361
- };
3362
- }
3363
- const discussContent = readFileSync26(discussPath, "utf-8");
3364
- const planFile = phasePlanPath(dir, phase);
3365
- const decisionLines = [];
3366
- const inDecisions = false;
3367
- let currentDecision = "";
3368
- for (const line of discussContent.split(`
3369
- `)) {
3370
- const dm = line.match(/^(D-\d+):\s+(.+)/);
3371
- if (dm) {
3372
- currentDecision = `${dm[1]}: ${dm[2]}`;
3373
- decisionLines.push(currentDecision);
3374
- }
3375
- }
3376
- const changeDescription = decisionLines.join("; ").substring(0, 200);
3377
- const impactRadarSection = buildImpactRadarSection(dir, changeDescription);
3378
- const planContent = [
3379
- "# Implementation Plan",
3380
- "",
3381
- `**Phase:** ${phase}`,
3382
- `**Created:** ${timestamp()}`,
3383
- `**Source:** DISCUSS.md (${decisionLines.length} decisions)`,
3384
- "",
3385
- "## Decisions",
3386
- "",
3387
- ...decisionLines.map((d) => `- ${d}`),
3388
- "",
3389
- impactRadarSection,
3390
- "## Steps",
3391
- "",
3392
- "- [ ] Step 1: [Implementation step]",
3393
- "- [ ] Step 2: [Implementation step]",
3394
- "- [ ] Step 3: [Implementation step]",
3395
- "",
3396
- "## Acceptance Criteria",
3397
- "",
3398
- "- [ ] Criterion 1",
3399
- "- [ ] Criterion 2",
3400
- "",
3401
- "## Status",
3402
- "",
3403
- "CONFIRMED"
3404
- ].join(`
3405
- `);
3406
- const phaseDir = join26(pd, "phases", `phase-${phase}`);
3407
- if (!existsSync27(phaseDir)) {
3408
- mkdirSync14(phaseDir, { recursive: true });
3409
- }
3410
- writeFileSync16(planFile, planContent, "utf-8");
3411
- let state = readFileSync26(sp, "utf-8");
3412
- state = state.replace(/^plan_confirmed:\s*.*/m, "plan_confirmed: true");
3413
- state = state.replace(/^confirmed_at:\s*.*/m, `confirmed_at: ${timestamp()}`);
3414
- state = state.replace(/^status:\s*.*/m, "status: in_progress");
3415
- writeFileSync16(sp, state, "utf-8");
3416
- return {
3417
- success: true,
3418
- message: `PLAN.md saved for phase ${phase}. Execution enabled.`,
3419
- phase,
3420
- decisions_count: decisionLines.length,
3421
- plan_file: planFile,
3422
- status: "CONFIRMED"
3423
- };
3424
- }
3425
- };
3426
-
3427
- // src/commands/planning/roadmap.ts
3428
- import { readFileSync as readFileSync27, existsSync as existsSync28, writeFileSync as writeFileSync17 } from "fs";
3429
- import { join as join27 } from "path";
3430
- var roadmapCommand = {
3431
- name: "fd-roadmap",
3432
- description: "View or update project roadmap \u2014 displays ROADMAP.md, shows phase statuses, add new phase, or mark phase complete",
3433
- async execute(context, args) {
3434
- const dir = context.directory ?? process.cwd();
3435
- const pd = planningDir(dir);
3436
- const roadmapPath = join27(pd, "ROADMAP.md");
3437
- if (!existsSync28(pd)) {
3438
- return {
3439
- error: ".planning/ not found. Run /new-project first.",
3440
- code: "NOT_INITIALIZED"
3441
- };
3442
- }
3443
- if (!existsSync28(roadmapPath)) {
3444
- return {
3445
- error: "ROADMAP.md not found.",
3446
- code: "NO_ROADMAP"
3447
- };
3448
- }
3449
- const content = readFileSync27(roadmapPath, "utf-8");
3450
- if (args?.complete) {
3451
- const phaseNum = parseInt(args.complete, 10);
3452
- if (isNaN(phaseNum)) {
3453
- return { error: "Phase number must be a numeric value", code: "INVALID_PHASE" };
3454
- }
3455
- const phaseExists = content.match(new RegExp(`\\[ \\] Phase ${phaseNum}:`));
3456
- if (!phaseExists) {
3457
- return { error: `Phase ${phaseNum} not found in roadmap. Check /roadmap for valid phase numbers.`, code: "PHASE_NOT_FOUND" };
3458
- }
3459
- if (!args?.yes) {
3460
- return {
3461
- ...confirmPrompt("roadmap-complete", `Mark Phase ${args.complete} complete? This will update ROADMAP.md and STATE.md. [y/n]`),
3462
- phase: parseInt(args.complete, 10)
3463
- };
3464
- }
3465
- const phasePattern = new RegExp(`(\\-\\[ \\] Phase ${phaseNum}:)`, "i");
3466
- const completedPattern = new RegExp(`Phase ${phaseNum}: ([^\\(]+)`);
3467
- const dateStr = new Date().toISOString().split("T")[0];
3468
- let updated = content.replace(phasePattern, `[x] Phase ${phaseNum}:`);
3469
- const milestoneMatch = content.match(/### \uD83D\uDCCB (\S+) \(([^)]+)\)/);
3470
- if (milestoneMatch) {
3471
- const currentStatus = milestoneMatch[2];
3472
- updated = updated.replace(new RegExp(`### \uD83D\uDCCB ${milestoneMatch[1]} \\(${currentStatus}\\)(?!.*Shipped)`), `### \u2705 ${milestoneMatch[1]} (Shipped: ${dateStr})`);
3473
- }
3474
- if (args?.dryRun) {
3475
- const nextPhase2 = phaseNum + 1;
3476
- const proposedState = `phase: ${nextPhase2}
3477
- status: in_progress`;
3478
- return {
3479
- success: true,
3480
- message: `[DRY-RUN] Would update ROADMAP.md:
3481
- ${updated}
3482
-
3483
- [DRY-RUN] Would update STATE.md:
3484
- ${proposedState}`,
3485
- meta: { dryRun: true, phase: phaseNum }
3486
- };
3487
- }
3488
- writeFileSync17(roadmapPath, updated, "utf-8");
3489
- const nextPhase = phaseNum + 1;
3490
- updatePlanningState(dir, {
3491
- phase: nextPhase,
3492
- status: "in_progress"
3493
- });
3494
- return {
3495
- success: true,
3496
- message: `Phase ${phaseNum} marked complete in ROADMAP.md and STATE.md updated (now Phase ${nextPhase})`,
3497
- phase: phaseNum
3498
- };
3499
- }
3500
- if (args?.add) {
3501
- const newPhase = args.add;
3502
- const sanitized = newPhase.replace(/[^\w\s\-]/g, "");
3503
- const newPhaseMd = `
3504
- - [ ] Phase ${sanitized}: ${sanitized}
3505
- `;
3506
- if (args?.dryRun) {
3507
- return {
3508
- success: true,
3509
- message: `[DRY-RUN] Would add to ROADMAP.md:
3510
- ${newPhaseMd}`,
3511
- meta: { dryRun: true, phase: sanitized }
3512
- };
3513
- }
3514
- const insertPoint = content.lastIndexOf("### \uD83D\uDCCB");
3515
- if (insertPoint === -1) {
3516
- return { error: "Could not find insertion point for new phase", code: "INSERT_FAILED" };
3517
- }
3518
- const updated = content.slice(0, insertPoint) + newPhaseMd + content.slice(insertPoint);
3519
- writeFileSync17(roadmapPath, updated, "utf-8");
3520
- return {
3521
- success: true,
3522
- message: `Phase "${newPhase}" added to ROADMAP.md`,
3523
- phase: newPhase
3524
- };
3525
- }
3526
- if (args?.json) {
3527
- return {
3528
- success: true,
3529
- data: { roadmap: content },
3530
- meta: { formatted: "json", timestamp: timestamp() }
3531
- };
3532
- }
3533
- if (args?.filter && !["complete", "in-progress", "planned"].includes(args.filter)) {
3534
- return { error: "Filter must be one of: complete, in-progress, planned", code: "INVALID_FILTER" };
3535
- }
3536
- const lines = content.split(`
3537
- `);
3538
- const state = readPlanningState(dir);
3539
- const stepsComplete = Array.isArray(state.steps_complete) ? state.steps_complete : [];
3540
- const stepsPending = Array.isArray(state.steps_pending) ? state.steps_pending : [];
3541
- const currentPhase = state.phase ?? 0;
3542
- const phaseEntries = [];
3543
- let currentSection = "";
3544
- for (const line of lines) {
3545
- if (line.startsWith("### ")) {
3546
- currentSection = line.replace("### ", "").replace("[x] ", "\u2705 ").replace("[ ] ", "\u25CB ");
3547
- } else if (line.startsWith("- [x] Phase")) {
3548
- const phaseMatch = line.match(/Phase (\d+)/);
3549
- const nameMatch = line.match(/Phase \d+: ([^\u2014]+)/);
3550
- phaseEntries.push({
3551
- raw: line,
3552
- type: "complete",
3553
- phaseNum: phaseMatch ? parseInt(phaseMatch[1]) : null,
3554
- name: nameMatch ? nameMatch[1].trim() : "",
3555
- section: currentSection,
3556
- line
3557
- });
3558
- } else if (line.startsWith("- [ ] Phase")) {
3559
- const phaseMatch = line.match(/Phase (\d+)/);
3560
- const nameMatch = line.match(/Phase \d+: ([^\u2014]+)/);
3561
- phaseEntries.push({
3562
- raw: line,
3563
- type: "planned",
3564
- phaseNum: phaseMatch ? parseInt(phaseMatch[1]) : null,
3565
- name: nameMatch ? nameMatch[1].trim() : "",
3566
- section: currentSection,
3567
- line
3568
- });
3569
- } else if (line.startsWith("- \u2705")) {
3570
- phaseEntries.push({
3571
- raw: line,
3572
- type: "shipped",
3573
- phaseNum: null,
3574
- name: "",
3575
- section: currentSection,
3576
- line
3577
- });
3578
- }
3579
- }
3580
- const filterSet = new Set;
3581
- if (args?.filter === "complete") {
3582
- phaseEntries.forEach((p) => {
3583
- if (p.type === "complete" && p.phaseNum !== null)
3584
- filterSet.add(p.phaseNum);
3585
- });
3586
- } else if (args?.filter === "planned") {
3587
- phaseEntries.forEach((p) => {
3588
- if (p.type === "planned" && p.phaseNum !== null)
3589
- filterSet.add(p.phaseNum);
3590
- });
3591
- } else if (args?.filter === "in-progress") {
3592
- if (currentPhase > 0)
3593
- filterSet.add(currentPhase);
3594
- }
3595
- if (args?.sort && args.sort !== "number") {
3596
- phaseEntries.sort((a, b) => {
3597
- if (args.sort === "name") {
3598
- const nameA = a.name.toLowerCase();
3599
- const nameB = b.name.toLowerCase();
3600
- return nameA.localeCompare(nameB);
3601
- } else if (args.sort === "status") {
3602
- const order = { complete: 0, "in-progress": 1, planned: 2 };
3603
- const statusA = a.phaseNum === currentPhase ? "in-progress" : a.type;
3604
- const statusB = b.phaseNum === currentPhase ? "in-progress" : b.type;
3605
- const ordA = statusA === "in-progress" ? 1 : order[statusA] ?? 2;
3606
- const ordB = statusB === "in-progress" ? 1 : order[statusB] ?? 2;
3607
- if (ordA !== ordB)
3608
- return ordA - ordB;
3609
- return (a.phaseNum ?? 0) - (b.phaseNum ?? 0);
3610
- }
3611
- return 0;
3612
- });
3613
- }
3614
- function boldMatch(text, query) {
3615
- if (!query)
3616
- return text;
3617
- const regex = new RegExp(`(${query.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")})`, "gi");
3618
- return text.replace(regex, "**$1**");
3619
- }
3620
- let output = ["\u2550".repeat(55), "ROADMAP", "\u2550".repeat(55)];
3621
- currentSection = "";
3622
- for (const entry of phaseEntries) {
3623
- if (filterSet.size > 0 && entry.phaseNum !== null && !filterSet.has(entry.phaseNum)) {
3624
- continue;
3625
- }
3626
- if (entry.section !== currentSection) {
3627
- currentSection = entry.section;
3628
- output.push("", currentSection);
3629
- }
3630
- if (entry.type === "shipped") {
3631
- output.push(` ${entry.raw}`);
3632
- } else if (entry.type === "complete" || entry.type === "planned") {
3633
- const phaseInfo = entry.raw.replace(/- \[[x ]\] Phase \d+: /, "").replace(" \u2014 ", " | ");
3634
- let stepInfo = "";
3635
- if (entry.phaseNum !== null) {
3636
- if (entry.type === "complete") {
3637
- const pComplete = stepsComplete.filter((s) => s === entry.phaseNum).length;
3638
- const pPending = stepsPending.filter((s) => s === entry.phaseNum).length;
3639
- if (pComplete > 0 || pPending > 0) {
3640
- stepInfo = ` [${pComplete}/${pComplete + pPending} steps]`;
3641
- }
3642
- } else {
3643
- const pPending = stepsPending.filter((s) => s === entry.phaseNum).length;
3644
- if (pPending > 0) {
3645
- stepInfo = ` [${pPending} pending]`;
3646
- }
3647
- }
3648
- }
3649
- const isCurrent = entry.phaseNum === currentPhase;
3650
- const marker = isCurrent ? ">> " : " ";
3651
- const icon = entry.type === "complete" ? "\u2713" : "\u25CB";
3652
- let displayInfo = phaseInfo;
3653
- if (args?.search) {
3654
- displayInfo = boldMatch(phaseInfo, args.search);
3655
- }
3656
- output.push(`${marker}${icon} ${displayInfo}${stepInfo}`);
3657
- } else if (entry.raw.trim().startsWith("|")) {
3658
- output.push(` ${entry.raw.trim()}`);
3659
- }
3660
- }
3661
- output.push("", "\u2500".repeat(55));
3662
- output.push(" Use: /roadmap --complete <N> to mark phase N complete");
3663
- output.push(" Use: /roadmap --add <name> to add a new phase");
3664
- output.push(" Use: /roadmap --dry-run to preview changes without writing");
3665
- output.push(" Use: /roadmap --filter <F> to filter by status (complete|in-progress|planned)");
3666
- output.push(" Use: /roadmap --search <Q> to highlight matching phases");
3667
- output.push(" Use: /roadmap --sort <F> to sort by field (number|name|status)");
3668
- output.push("\u2550".repeat(55));
3669
- return {
3670
- success: true,
3671
- message: output.join(`
3672
- `),
3673
- meta: { formatted: "table", timestamp: timestamp() }
3674
- };
3675
- }
3676
- };
3677
-
3678
- // src/commands/planning/dashboard.ts
3679
- import { spawn } from "child_process";
3680
- import { existsSync as existsSync29, readFileSync as readFileSync28 } from "fs";
3681
- import path from "path";
3682
- import { fileURLToPath } from "url";
3683
-
3684
- // src/dashboard/lib/port-finder.ts
3685
- import http from "http";
3686
- async function findOpenPort(startPort = 3456, maxRetries = 100) {
3687
- let port = startPort;
3688
- let retries = 0;
3689
- while (retries < maxRetries) {
3690
- const result = await tryPort(port);
3691
- if (result.available) {
3692
- return { port, host: "localhost" };
3693
- }
3694
- port++;
3695
- retries++;
3696
- }
3697
- throw new Error(`Could not find open port after ${maxRetries} attempts starting from ${startPort}`);
3698
- }
3699
- function tryPort(port) {
3700
- return new Promise((resolve3) => {
3701
- const server = http.createServer();
3702
- server.once("error", () => {
3703
- resolve3({ available: false });
3704
- });
3705
- server.once("listening", () => {
3706
- server.close(() => {
3707
- resolve3({ available: true });
3708
- });
3709
- });
3710
- server.listen(port, "127.0.0.1");
3711
- });
3712
- }
3713
-
3714
- // src/commands/planning/dashboard.ts
3715
- var SERVER_PATH = path.join(path.dirname(fileURLToPath(import.meta.url)), "dashboard", "server.mjs");
3716
- var dashboardCommand = {
3717
- name: "fd-dashboard",
3718
- description: "Open project dashboard in browser \u2014 displays phase progress, milestones, and blockers",
3719
- async execute(context, args) {
3720
- const dir = context.directory ?? process.cwd();
3721
- const dashboardDir = path.join(dir, ".dashboard");
3722
- if (args?.refresh) {
3723
- const portFile = path.join(dashboardDir, "port");
3724
- if (!existsSync29(portFile)) {
3725
- return { success: false, error: "No dashboard server running. Run /dashboard first.", code: "NO_SERVER" };
3726
- }
3727
- const port2 = readFileSync28(portFile, "utf-8").trim();
3728
- try {
3729
- const resp = await fetch(`http://localhost:${port2}/refresh`);
3730
- if (resp.ok) {
3731
- return { success: true, message: `Dashboard refreshed at http://localhost:${port2}` };
3732
- }
3733
- } catch {
3734
- return { success: false, error: `Could not reach dashboard at port ${port2}`, code: "REFRESH_FAILED" };
3735
- }
3736
- }
3737
- const { port } = await findOpenPort(3456, 100);
3738
- const child = spawn("bun", [SERVER_PATH, `--port=${port}`, `--dir=${dir}`], {
3739
- detached: true,
3740
- stdio: "ignore"
3741
- });
3742
- child.unref();
3743
- await new Promise((r) => setTimeout(r, 500));
3744
- const url = `http://localhost:${port}`;
3745
- return {
3746
- success: true,
3747
- message: `Dashboard running at ${url} \u2014 opening browser`,
3748
- meta: { port, url }
3749
- };
3750
- }
3751
- };
3752
-
3753
- // src/commands/planning/ask.ts
3754
- var ROUTES = [
3755
- {
3756
- keywords: ["system design", "design", "architecture", "diagram", "structure", "component", "microservice", "schema design"],
3757
- agent: "architect",
3758
- focus: "system design and architecture",
3759
- description: "Design systems, components, and architecture"
3760
- },
3761
- {
3762
- keywords: ["impact", "blast radius", "affected", "downstream", "dependency", "ripple", "change radar"],
3763
- agent: "researcher",
3764
- focus: "change impact and dependency analysis",
3765
- description: "Assess impact and downstream effects of a change",
3766
- useImpactRadar: true
3767
- },
3768
- {
3769
- keywords: ["security", "vulnerability", "cve", "exploit", "injection", "xss", "csrf", "auth bypass", "pentest", "audit", "owasp"],
3770
- agent: "security-auditor",
3771
- focus: "security analysis",
3772
- description: "Security audit, vulnerability assessment, threat analysis"
3773
- },
3774
- {
3775
- keywords: ["performance", "bottleneck", "slow", "latency", "optimize", "benchmark", "profil", "memory leak", "cpu", "throughput"],
3776
- agent: "performance-optimizer",
3777
- focus: "performance analysis and optimization",
3778
- description: "Profile, benchmark, and optimize performance"
3779
- },
3780
- {
3781
- keywords: ["debug", "error", "exception", "crash", "panic", "traceback", "stack trace", "not working", "broken", "bug"],
3782
- agent: "debug-specialist",
3783
- focus: "root cause investigation",
3784
- description: "Investigate errors, crashes, and unexpected behavior"
3785
- },
3786
- {
3787
- keywords: ["test", "coverage", "spec", "unit test", "integration test", "mock", "fixture", "tdd", "assert"],
3788
- agent: "tester",
3789
- focus: "test generation and coverage",
3790
- description: "Write tests, improve coverage, and validate behavior"
3791
- },
3792
- {
3793
- keywords: ["refactor", "cleanup", "simplify", "extract", "rename", "reorganize", "decouple", "dry", "dead code"],
3794
- agent: "coder",
3795
- focus: "refactoring and code quality",
3796
- description: "Refactor and improve code without changing behavior"
3797
- },
3798
- {
3799
- keywords: ["document", "docs", "readme", "wiki", "jsdoc", "docstring", "comment", "explain to", "write up"],
3800
- agent: "writer",
3801
- focus: "documentation generation",
3802
- description: "Write or update documentation"
3803
- },
3804
- {
3805
- keywords: ["explain", "how does", "what does", "what is", "understand", "query", "search", "find", "where", "explore", "trace"],
3806
- agent: "code-explorer",
3807
- focus: "code exploration and explanation",
3808
- description: "Explore and explain code, find patterns, trace flows"
3809
- },
3810
- {
3811
- keywords: ["deploy", "release", "production", "rollback", "migration", "upgrade", "version"],
3812
- agent: "reviewer",
3813
- focus: "deployment readiness",
3814
- description: "Review for deployment readiness and risk"
3815
- },
3816
- {
3817
- keywords: ["plan", "roadmap", "breakdown", "estimate", "milestone", "task", "phase", "sprint"],
3818
- agent: "planner",
3819
- focus: "planning and task breakdown",
3820
- description: "Create plans, roadmaps, and task breakdowns"
3821
- }
3822
- ];
3823
- function scoreRoute(task, rule) {
3824
- const lower = task.toLowerCase();
3825
- let score = 0;
3826
- for (const kw of rule.keywords) {
3827
- if (lower.includes(kw.toLowerCase())) {
3828
- score += kw.split(" ").length;
3829
- }
3830
- }
3831
- return score;
3832
- }
3833
- function pickRoute(task) {
3834
- let best = null;
3835
- for (const rule of ROUTES) {
3836
- const score = scoreRoute(task, rule);
3837
- if (score > 0 && (!best || score > best.score)) {
3838
- best = { ...rule, score };
3839
- }
3840
- }
3841
- return best ?? { ...ROUTES[ROUTES.length - 1], agent: "orchestrator", focus: "general task", description: "General purpose task", score: 0 };
3842
- }
3843
- var askCommand = {
3844
- name: "fd-ask",
3845
- description: "Smart dispatch \u2014 routes a free-form task to the appropriate specialized agent without a workflow",
3846
- async execute(context, args) {
3847
- const dir = context.directory ?? process.cwd();
3848
- if (!args?.task && !args?.agent) {
3849
- return {
3850
- error: "Provide a task description: /fd-ask --task 'system design for notifications'",
3851
- code: "NO_TASK",
3852
- hint: "Examples: 'explain how auth works', 'security audit of payments', 'design a caching layer'",
3853
- examples: ROUTES.slice(0, 5).map((r) => ({ agent: `@${r.agent}`, example: r.keywords[0] }))
3854
- };
3855
- }
3856
- const task = args?.task ?? "";
3857
- const route = args?.agent ? { agent: args.agent, focus: "user-specified", description: "Manually specified agent", score: -1, keywords: [], useImpactRadar: false } : pickRoute(task);
3858
- const radar = route.useImpactRadar ? runImpactRadar(dir, task) : null;
3859
- const dispatch = {
3860
- agent: `@${route.agent}`,
3861
- task,
3862
- focus: route.focus,
3863
- routed_by: args?.agent ? "user-override" : "keyword-match",
3864
- impact_radar: radar ?? undefined
3865
- };
3866
- if (args?.json) {
3867
- return {
3868
- success: true,
3869
- data: dispatch,
3870
- meta: { formatted: "json", timestamp: timestamp() }
3871
- };
3872
- }
3873
- const lines = [
3874
- "\u2500".repeat(55),
3875
- ` /fd-ask \u2192 routing to ${dispatch.agent}`,
3876
- ` Task: ${task}`,
3877
- ` Focus: ${route.focus}`
3878
- ];
3879
- if (radar?.risk_flag) {
3880
- lines.push("\u2500".repeat(55));
3881
- lines.push(" \u26A0 Impact Radar:");
3882
- if (radar.hotspots.length > 0) {
3883
- lines.push(` Volatile zones: ${radar.hotspots.map((h) => h.path).join(", ")}`);
3884
- }
3885
- if (radar.known_failures.length > 0) {
3886
- lines.push(` Known failures: ${radar.known_failures.map((f) => f.id).join(", ")}`);
3887
- }
3888
- }
3889
- lines.push("\u2550".repeat(55));
3890
- return {
3891
- success: true,
3892
- message: lines.join(`
3893
- `),
3894
- dispatch,
3895
- meta: { formatted: "table", timestamp: timestamp() }
3896
- };
3897
- }
3898
- };
3899
-
3900
- // src/commands/execution/new-feature.ts
3901
- import { existsSync as existsSync32, readFileSync as readFileSync31 } from "fs";
3902
-
3903
- // src/services/model-router.ts
3904
- import { existsSync as existsSync30, readFileSync as readFileSync29 } from "fs";
3905
- import { join as join28 } from "path";
3906
- var DEFAULT_ROUTING = {
3907
- planning: { primary: "claude-sonnet-4-5", temperature: 0.3, reasoning_effort: "medium" },
3908
- implementation: { primary: "claude-opus-4-5", fallback: "claude-sonnet-4-5", high_risk_override: "claude-opus-4-5", temperature: 0.2, reasoning_effort: "high" },
3909
- debugging: { primary: "claude-sonnet-4-5", high_risk_override: "claude-opus-4-5", temperature: 0.2, reasoning_effort: "high" },
3910
- review: { primary: "gemini-2.5-flash", fallback: "claude-haiku-4-5", temperature: 0.1, reasoning_effort: "medium" },
3911
- testing: { primary: "claude-haiku-4-5", fallback: "gemini-2.5-flash", temperature: 0.1, reasoning_effort: "low" },
3912
- documentation: { primary: "claude-sonnet-4-5", fallback: "gemini-2.5-flash", temperature: 0.3, reasoning_effort: "low" },
3913
- analysis: { primary: "claude-sonnet-4-5", temperature: 0.3, reasoning_effort: "medium" },
3914
- security: { primary: "claude-opus-4-5", high_risk_override: "claude-opus-4-5", temperature: 0.1, reasoning_effort: "high" },
3915
- orchestration: { primary: "claude-sonnet-4-5", temperature: 0.3, reasoning_effort: "medium" }
3916
- };
3917
- function getRouterConfig(dir) {
3918
- const p = join28(codebaseDir(dir), "MODEL_ROUTER.json");
3919
- if (!existsSync30(p))
3920
- return DEFAULT_ROUTING;
3921
- try {
3922
- const overrides = JSON.parse(readFileSync29(p, "utf-8"));
3923
- return { ...DEFAULT_ROUTING, ...overrides };
3924
- } catch {
3925
- return DEFAULT_ROUTING;
3926
- }
3927
- }
3928
- function routeModel(dir, task_type, risk_score = 100) {
3929
- const config = getRouterConfig(dir);
3930
- const route = config[task_type] ?? DEFAULT_ROUTING.implementation;
3931
- const is_high_risk = risk_score < 40;
3932
- let model = route.primary;
3933
- let is_override = false;
3934
- if (is_high_risk && route.high_risk_override) {
3935
- model = route.high_risk_override;
3936
- is_override = true;
3937
- }
3938
- return {
3939
- model,
3940
- temperature: route.temperature ?? 0.3,
3941
- reasoning_effort: route.reasoning_effort,
3942
- task_type,
3943
- is_high_risk,
3944
- is_override
3945
- };
3946
- }
3947
- function buildAgentConfig(dir, agents) {
3948
- return agents.map((a) => {
3949
- const routed = routeModel(dir, a.task_type, a.risk_score ?? 100);
3950
- return {
3951
- name: a.name,
3952
- model: routed.model,
3953
- temperature: routed.temperature,
3954
- ...routed.reasoning_effort ? { reasoningEffort: routed.reasoning_effort } : {}
3955
- };
3956
- });
3957
- }
3958
-
3959
- // src/services/run-trace.ts
3960
- import { existsSync as existsSync31, readFileSync as readFileSync30, appendFileSync as appendFileSync4, writeFileSync as writeFileSync18, mkdirSync as mkdirSync15 } from "fs";
3961
- import { join as join29 } from "path";
3962
- import { randomUUID as randomUUID2 } from "crypto";
3963
- function runsPath(dir) {
3964
- return join29(codebaseDir(dir), "RUNS.jsonl");
3965
- }
3966
- function startTrace(dir, command, args, session_id = "session-0") {
3967
- const cd = codebaseDir(dir);
3968
- if (!existsSync31(cd))
3969
- mkdirSync15(cd, { recursive: true });
3970
- const trace = {
3971
- run_id: randomUUID2(),
3972
- session_id,
3973
- command,
3974
- args,
3975
- started_at: new Date().toISOString(),
3976
- status: "running",
3977
- files_touched: [],
3978
- event_ids: [],
3979
- risk_score: 0
3980
- };
3981
- appendFileSync4(runsPath(dir), JSON.stringify(trace) + `
3982
- `, "utf-8");
3983
- return trace;
3984
- }
3985
-
3986
- // src/commands/execution/new-feature.ts
3987
- var newFeatureCommand = {
3988
- name: "fd-new-feature",
3989
- description: "Execute feature implementation \u2014 guard check, orchestrator coordination, parallel coder+researcher, reviewer, tester, STATE.md update",
3990
- async execute(context, args) {
3991
- const dir = context.directory ?? process.cwd();
3992
- const sp = statePath(dir);
3993
- const pd = planningDir(dir);
3994
- const cd = codebaseDir(dir);
3995
- const checks = {
3996
- ".planning/": existsSync32(pd),
3997
- ".codebase/": existsSync32(cd),
3998
- "PLAN.md confirmed": () => {
3999
- if (!existsSync32(sp))
4000
- return false;
4001
- const state2 = readPlanningState(dir);
4002
- return state2.plan_confirmed === true;
4003
- }
4004
- };
4005
- const missing = Object.entries(checks).filter(([, v]) => typeof v === "boolean" ? !v : !v()).map(([k]) => k);
4006
- if (missing.length > 0) {
4007
- return {
4008
- error: `Missing prerequisites: ${missing.join(", ")}`,
4009
- code: "GUARD_FAILED",
4010
- hint: "Run /new-project, /map-codebase, and /plan first"
4011
- };
4012
- }
4013
- const state = readPlanningState(dir);
4014
- const phase = state.phase;
4015
- const planPath = phasePlanPath(dir, phase);
4016
- if (!existsSync32(planPath)) {
4017
- return {
4018
- error: "PLAN.md not found. Run /plan first.",
4019
- code: "NO_PLAN"
4020
- };
4021
- }
4022
- const featureText = args?.feature ?? readFileSync31(planPath, "utf-8").split(`
4023
- `).slice(0, 10).join(" ");
4024
- const radar = runImpactRadar(dir, featureText);
4025
- const priorFailures = lookupPriorFailures(dir, "all", featureText);
4026
- const trace = startTrace(dir, "fd-new-feature", { feature: args?.feature ?? "", phase }, process.env.OPENCODE_SESSION_ID);
4027
- appendEvent(dir, {
4028
- session_id: process.env.OPENCODE_SESSION_ID ?? "session-0",
4029
- run_id: trace.run_id,
4030
- event: "command.start",
4031
- command: "fd-new-feature",
4032
- risk_score: radar.score,
4033
- meta: { phase, plan_file: planPath }
4034
- });
4035
- let tddState = state["tdd"];
4036
- if (!tddState) {
4037
- updateTDDState(dir, {
4038
- stage: "behavior",
4039
- cycle: 1,
4040
- behaviors: [],
4041
- regression_test_links: [],
4042
- override_log: [],
4043
- failing_tests: 0,
4044
- passing_tests: 0
4045
- });
4046
- tddState = readPlanningState(dir)["tdd"];
4047
- }
4048
- const codebaseResult = codebaseStateTool.execute({ action: "read", files: ["STACK.md", "ARCHITECTURE.md"] }, context);
4049
- const riskScore = radar.score;
4050
- const agentConfigs = buildAgentConfig(dir, [
4051
- { name: "coder", task_type: "implementation", risk_score: riskScore },
4052
- { name: "researcher", task_type: "analysis", risk_score: riskScore },
4053
- { name: "reviewer", task_type: "review", risk_score: riskScore },
4054
- { name: "tester", task_type: "testing", risk_score: riskScore }
4055
- ]);
4056
- const workflow = "execute-flow.md";
4057
- const config = {
4058
- orchestrator: {
4059
- model: agentConfigs.find((a) => a.name === "coder")?.model ?? "claude-sonnet-4-5",
4060
- temperature: 0.3,
4061
- maxSteps: 60
4062
- },
4063
- agents: agentConfigs,
4064
- run_id: trace.run_id,
4065
- parallel: {
4066
- coder: true,
4067
- researcher: true
4068
- },
4069
- worktree: true,
4070
- impact_radar: radar,
4071
- prior_failures: priorFailures.map((f) => ({
4072
- id: f.id,
4073
- type: f.type,
4074
- description: f.description,
4075
- affected_paths: f.affected_paths,
4076
- root_cause: f.root_cause ?? null,
4077
- fix_applied: f.fix_applied ?? null,
4078
- recurrence_count: f.recurrence_count
4079
- })),
4080
- post_execution: {
4081
- step: "record",
4082
- agent: "orchestrator",
4083
- actions: [
4084
- { tool: "repo-memory", action: "record", note: "add new module to MEMORY.json" },
4085
- { tool: "failure-replay", action: "record", condition: "if any build, test, or deployment failure occurred during this feature", note: "log to FAILURES.json for future reference" }
4086
- ]
4087
- }
4088
- };
4089
- if (args?.json) {
4090
- return {
4091
- success: true,
4092
- data: { workflow, config, phase, plan_file: planPath },
4093
- meta: { formatted: "json", timestamp: timestamp() }
4094
- };
4095
- }
4096
- const radarLines = impactRadarSummaryLines(radar);
4097
- const priorFailureLines = priorFailures.length > 0 ? [
4098
- "\u2500".repeat(55),
4099
- ` Prior failures in this area (${priorFailures.length}):`,
4100
- ...priorFailures.map((f) => {
4101
- const rc = f.recurrence_count > 1 ? ` (\xD7${f.recurrence_count})` : "";
4102
- const cause = f.root_cause ? ` \u2014 ${f.root_cause.substring(0, 60)}` : "";
4103
- return ` \u26A0 [${f.id}]${rc}${cause}`;
4104
- })
4105
- ] : [];
4106
- const tddStage = tddState ? tddState.stage.toUpperCase() : "NONE";
4107
- const tddFailing = tddState ? tddState.failing_tests : 0;
4108
- const tddPassing = tddState ? tddState.passing_tests : 0;
4109
- const tddBehaviors = tddState ? tddState.behaviors.length : 0;
4110
- const tddCycles = tddState ? tddState.cycle : 0;
4111
- const tableLines = [
4112
- "\u2550".repeat(55),
4113
- `New Feature: phase ${phase}`,
4114
- "\u2500".repeat(55),
4115
- ` Guard: .planning/ \u2713 .codebase/ \u2713 plan_confirmed \u2713`,
4116
- "\u2500".repeat(55),
4117
- ` TDD Stage: ${tddStage} | Cycle: ${tddCycles}`,
4118
- ` Tests: ${tddFailing} failing | ${tddPassing} passing`,
4119
- ` Behaviors: ${tddBehaviors} defined`,
4120
- ...priorFailureLines,
4121
- ...radarLines,
4122
- "\u2500".repeat(55),
4123
- " orchestrator \u2192 coordinates TDD execution",
4124
- " TDD cycle: \u2192 RED (write failing tests) \u2192 GREEN (minimum impl) \u2192 REFACTOR",
4125
- " sequential: \u2192 @reviewer, @tester",
4126
- " post-exec: \u2192 record module (repo-memory) + any failures (failure-replay)",
4127
- "\u2500".repeat(55),
4128
- ` plan: ${planPath.split("/").pop()}`,
4129
- "\u2550".repeat(55)
4130
- ];
4131
- return {
4132
- success: true,
4133
- message: tableLines.join(`
4134
- `),
4135
- workflow,
4136
- config,
4137
- phase,
4138
- plan_file: planPath,
4139
- impact_radar: radar,
4140
- prior_failures: config.prior_failures,
4141
- meta: { formatted: "table", timestamp: timestamp() }
4142
- };
4143
- }
4144
- };
4145
-
4146
- // src/commands/execution/fix-bug.ts
4147
- import { existsSync as existsSync34, readFileSync as readFileSync33 } from "fs";
4148
-
4149
- // src/services/policy-compiler.ts
4150
- import { existsSync as existsSync33, readFileSync as readFileSync32 } from "fs";
4151
- import { join as join30 } from "path";
4152
- function loadPolicies(dir) {
4153
- const p = join30(codebaseDir(dir), "POLICIES.json");
4154
- if (!existsSync33(p))
4155
- return [];
4156
- try {
4157
- const store = JSON.parse(readFileSync32(p, "utf-8"));
4158
- return store.policies.filter((p2) => p2.active);
4159
- } catch {
4160
- return [];
4161
- }
4162
- }
4163
- function matchesTrigger(trigger, ctx) {
4164
- const t = trigger.toLowerCase();
4165
- const fields = [
4166
- ctx.command ?? "",
4167
- ctx.file_path ?? "",
4168
- ctx.change_description ?? "",
4169
- ctx.tool ?? ""
4170
- ].map((s) => s.toLowerCase());
4171
- const words = t.split(/\s+/).filter(Boolean);
4172
- return words.some((word) => fields.some((f) => f.includes(word)));
4173
- }
4174
- function deriveSeverity(rule) {
4175
- const blocking = ["never", "always block", "must not", "forbidden", "require approval", "requires approval"];
4176
- const lower = rule.toLowerCase();
4177
- return blocking.some((kw) => lower.includes(kw)) ? "block" : "warn";
4178
- }
4179
- function evaluatePolicies(dir, ctx) {
4180
- const policies = loadPolicies(dir);
4181
- const violations = [];
4182
- for (const policy of policies) {
4183
- if (matchesTrigger(policy.trigger, ctx)) {
4184
- violations.push({
4185
- policy_id: policy.id,
4186
- policy_name: policy.name,
4187
- rule: policy.rule,
4188
- trigger: policy.trigger,
4189
- severity: deriveSeverity(policy.rule)
4190
- });
4191
- }
4192
- }
4193
- for (const tddPolicy of TDD_POLICIES) {
4194
- if (matchesTrigger(tddPolicy.trigger, ctx)) {
4195
- violations.push({
4196
- policy_id: `tdd-${tddPolicy.name.replace(/\s+/g, "-").toLowerCase()}`,
4197
- policy_name: tddPolicy.name,
4198
- rule: tddPolicy.rule,
4199
- trigger: tddPolicy.trigger,
4200
- severity: tddPolicy.severity
4201
- });
4202
- }
4203
- }
4204
- return violations;
4205
- }
4206
- var FAILURE_RULES = [
4207
- {
4208
- path_pattern: /auth|login|password|jwt|session|oauth/i,
4209
- name: "Require approval for auth changes",
4210
- trigger: "auth change",
4211
- rule: "Require human approval before editing authentication or session logic",
4212
- rationale: "Auth changes have high blast radius and are frequent sources of security regressions"
4213
- },
4214
- {
4215
- path_pattern: /payment|billing|stripe|credit/i,
4216
- name: "Require approval for payment changes",
4217
- trigger: "payment file",
4218
- rule: "Require human approval before editing payment or billing logic",
4219
- rationale: "Payment changes risk revenue loss and PCI compliance violations"
4220
- },
4221
- {
4222
- path_pattern: /migration|migrate|schema|alembic/i,
4223
- name: "Always add tests for schema changes",
4224
- trigger: "database migration",
4225
- rule: "Always add integration tests before applying a database migration",
4226
- rationale: "Schema migrations are irreversible and have caused data loss in past failures"
4227
- },
4228
- {
4229
- path_pattern: /infra|terraform|k8s|kubernetes|ansible|helm/i,
4230
- name: "Never edit infra without review",
4231
- trigger: "infra change",
4232
- rule: "Never edit infrastructure configuration without a reviewer sign-off",
4233
- rationale: "Infrastructure changes caused downtime in past incidents"
4234
- },
4235
- {
4236
- path_pattern: /\.env|secrets\.|config\/prod/i,
4237
- name: "Block writes to secrets files",
4238
- trigger: "secrets file",
4239
- rule: "Never write directly to .env or secrets files \u2014 use vault/config management",
4240
- rationale: "Direct writes to secrets files risk credential leaks"
4241
- }
4242
- ];
4243
- var TDD_POLICIES = [
4244
- {
4245
- name: "No implementation without failing test",
4246
- trigger: "fd-new-feature",
4247
- rule: "Never begin implementation until a failing test exists for the target behavior",
4248
- severity: "warn"
4249
- },
4250
- {
4251
- name: "No refactor without green tests",
4252
- trigger: "refactor implementation",
4253
- rule: "Never refactor while tests are not green \u2014 maintain passing test state",
4254
- severity: "block"
4255
- },
4256
- {
4257
- name: "Bugfix requires regression test",
4258
- trigger: "fd-fix-bug",
4259
- rule: "Every bugfix must include a regression test unless override is explicitly granted",
4260
- severity: "warn"
4261
- },
4262
- {
4263
- name: "Missing tests are major findings",
4264
- trigger: "code review",
4265
- rule: "Flag missing or weak tests as major findings in code review \u2014 not minor",
4266
- severity: "warn"
4267
- },
4268
- {
4269
- name: "Deploy blocked without test coverage",
4270
- trigger: "fd-deploy-check",
4271
- rule: "Fail deploy check when expected tests are missing for changed code",
4272
- severity: "block"
4273
- },
4274
- {
4275
- name: "TDD override must be logged",
4276
- trigger: "override TDD",
4277
- rule: "Every TDD stage override must be logged in override_log and surfaced in review",
4278
- severity: "warn"
4279
- }
4280
- ];
4281
- function learnFromFailure(failure_type, affected_paths, root_cause) {
4282
- const allPaths = [failure_type, ...affected_paths ?? [], root_cause ?? ""].join(" ");
4283
- for (const rule of FAILURE_RULES) {
4284
- if (rule.path_pattern.test(allPaths)) {
4285
- const id = `learned-${rule.trigger.replace(/\s+/g, "-")}-${Date.now()}`;
4286
- return {
4287
- id,
4288
- name: rule.name,
4289
- trigger: rule.trigger,
4290
- rule: rule.rule,
4291
- source: "learned",
4292
- failure_count: 1,
4293
- rationale: rule.rationale
4294
- };
4295
- }
4296
- }
4297
- return null;
4298
- }
4299
- function formatViolations(violations) {
4300
- if (violations.length === 0)
4301
- return "";
4302
- const lines = [
4303
- ` Policy violations (${violations.length}):`,
4304
- ...violations.map((v) => {
4305
- const icon = v.severity === "block" ? "\u2717" : "\u26A0";
4306
- return ` ${icon} [${v.policy_id}] ${v.policy_name}: ${v.rule}`;
4307
- })
4308
- ];
4309
- return lines.join(`
4310
- `);
4311
- }
4312
-
4313
- // src/commands/execution/fix-bug.ts
4314
- var fixBugCommand = {
4315
- name: "fd-fix-bug",
4316
- description: "Load STATE.md + ARCHITECTURE.md \u2014 explore scope \u2014 researcher \u2014 mini-plan \u2014 coder fix \u2014 regression test \u2014 reviewer confirmation",
4317
- async execute(context, args) {
4318
- const dir = context.directory ?? process.cwd();
4319
- const sp = statePath(dir);
4320
- if (!existsSync34(sp)) {
4321
- return {
4322
- error: "STATE.md not found. Run /new-project first.",
4323
- code: "NOT_INITIALIZED"
4324
- };
4325
- }
4326
- const scope = args?.scope || "all";
4327
- if (scope.includes("/") && !scope.startsWith("./") && scope !== "all") {
4328
- return {
4329
- error: "Invalid scope: absolute paths not allowed",
4330
- code: "INVALID_SCOPE",
4331
- hint: "Use relative path like ./src or 'all'"
4332
- };
4333
- }
4334
- const state = readPlanningState(dir);
4335
- let tddState = state["tdd"];
4336
- if (!tddState) {
4337
- updateTDDState(dir, {
4338
- stage: "behavior",
4339
- cycle: 1,
4340
- behaviors: [],
4341
- regression_test_links: [],
4342
- override_log: [],
4343
- failing_tests: 0,
4344
- passing_tests: 0
4345
- });
4346
- tddState = readPlanningState(dir).tdd;
4347
- }
4348
- const cd = codebaseDir(dir);
4349
- const archPath = `${cd}/ARCHITECTURE.md`;
4350
- let architectureContext = null;
4351
- if (existsSync34(archPath)) {
4352
- architectureContext = readFileSync33(archPath, "utf-8");
4353
- }
4354
- const bugText = [args?.bug ?? "", scope !== "all" ? scope : ""].filter(Boolean).join(" ");
4355
- const radar = runImpactRadar(dir, bugText);
4356
- const priorFailures = lookupPriorFailures(dir, scope, args?.bug ?? "");
4357
- const policyViolations = evaluatePolicies(dir, {
4358
- command: "fd-fix-bug",
4359
- change_description: bugText
4360
- });
4361
- const proposedPolicies = priorFailures.map((f) => learnFromFailure(f.type, f.affected_paths, f.root_cause ?? "")).filter(Boolean);
4362
- const trace = startTrace(dir, "fd-fix-bug", { bug: args?.bug ?? "", scope }, process.env.OPENCODE_SESSION_ID);
4363
- appendEvent(dir, {
4364
- session_id: process.env.OPENCODE_SESSION_ID ?? "session-0",
4365
- run_id: trace.run_id,
4366
- event: "command.start",
4367
- command: "fd-fix-bug",
4368
- risk_score: radar.score,
4369
- meta: { scope, prior_failure_count: priorFailures.length, policy_violations: policyViolations.length }
4370
- });
4371
- const workflow = "fix-bug-flow.md";
4372
- const config = {
4373
- phases: [
4374
- { step: 1, name: "explore", agent: "researcher", action: "investigate bug scope via ARCHITECTURE.md" },
4375
- { step: 2, name: "research", agent: "researcher", action: "identify root cause and affected components" },
4376
- { step: 3, name: "mini-plan", agent: "orchestrator", action: "create fix plan from research findings" },
4377
- { step: 4, name: "fix", agent: "coder", action: "implement bug fix" },
4378
- { step: 5, name: "regression", agent: "tester", action: "write and run regression test (MUST PASS)", mode: "regression" },
4379
- { step: 6, name: "verify", agent: "reviewer", action: "confirm fix after regression passes", require_regression_pass: true },
4380
- { step: 7, name: "record", agent: "orchestrator", action: "call failure-replay record to log resolved bug in .codebase/FAILURES.json", tool: "failure-replay", tool_action: "record" }
4381
- ],
4382
- run_id: trace.run_id,
4383
- scope,
4384
- architecture_context: architectureContext ? architectureContext.substring(0, 500) : null,
4385
- impact_radar: radar,
4386
- prior_failures: priorFailures.map((f) => ({
4387
- id: f.id,
4388
- type: f.type,
4389
- description: f.description,
4390
- affected_paths: f.affected_paths,
4391
- root_cause: f.root_cause ?? null,
4392
- fix_applied: f.fix_applied ?? null,
4393
- recurrence_count: f.recurrence_count
4394
- })),
4395
- policy_violations: policyViolations,
4396
- proposed_policies: proposedPolicies
4397
- };
4398
- if (args?.json) {
4399
- return {
4400
- success: true,
4401
- data: { workflow, config, phase: state.phase },
4402
- meta: { formatted: "json", timestamp: timestamp() }
4403
- };
4404
- }
4405
- const radarLines = impactRadarSummaryLines(radar);
4406
- const priorFailureLines = priorFailures.length > 0 ? [
4407
- "\u2500".repeat(55),
4408
- ` Prior failures in this area (${priorFailures.length}):`,
4409
- ...priorFailures.map((f) => {
4410
- const rc = f.recurrence_count > 1 ? ` (\xD7${f.recurrence_count})` : "";
4411
- const cause = f.root_cause ? ` \u2014 ${f.root_cause.substring(0, 60)}` : "";
4412
- return ` \u26A0 [${f.id}]${rc}${cause}`;
4413
- })
4414
- ] : [];
4415
- const tddStage = tddState ? tddState.stage.toUpperCase() : "NONE";
4416
- const tddFailing = tddState ? tddState.failing_tests : 0;
4417
- const tddPassing = tddState ? tddState.passing_tests : 0;
4418
- const tddOverrides = tddState ? tddState.override_log.length : 0;
4419
- const tableLines = [
4420
- "\u2500".repeat(55),
4421
- `Fix Bug: scope=${scope}`,
4422
- `Phase ${state.phase} | TDD-enforced 12-step workflow`,
4423
- "\u2500".repeat(55),
4424
- ` TDD Stage: ${tddStage} | Cycle: ${tddState?.cycle ?? 1}`,
4425
- ` Tests: ${tddFailing} failing | ${tddPassing} passing`,
4426
- ` Overrides used: ${tddOverrides}`,
4427
- "\u2500".repeat(55),
4428
- " [1-2] explore + research \u2192 isolate root cause",
4429
- " [3] define behaviors \u2192 acceptance cases for fix",
4430
- " [4] RED \u2192 @tester writes failing regression test",
4431
- " [5] confirm \u2192 test MUST fail before proceeding",
4432
- " [6] GREEN \u2192 @coder implements minimum fix",
4433
- " [7] confirm \u2192 test MUST pass before proceeding",
4434
- " [8] REFACTOR \u2192 clean up (only if GREEN)",
4435
- " [9-10] verify \u2192 full test suite passes",
4436
- " [11] review \u2192 @reviewer confirms + TDD discipline",
4437
- " [12] record \u2192 log fix + regression test in FAILURES.json",
4438
- ...priorFailureLines,
4439
- ...radarLines,
4440
- ...policyViolations.length > 0 ? ["\u2500".repeat(55), formatViolations(policyViolations)] : [],
4441
- "\u2500".repeat(55),
4442
- "\u26A0 GUARD: Regression test must fail RED \u2192 pass GREEN \u2192 refactor",
4443
- "\u2550".repeat(55)
4444
- ];
4445
- return {
4446
- success: true,
4447
- message: tableLines.join(`
4448
- `),
4449
- workflow,
4450
- config,
4451
- phase: state.phase,
4452
- impact_radar: radar,
4453
- prior_failures: config.prior_failures,
4454
- meta: { formatted: "table", timestamp: timestamp() }
4455
- };
4456
- }
4457
- };
4458
-
4459
- // src/commands/execution/review-code.ts
4460
- import { existsSync as existsSync35 } from "fs";
4461
- var reviewCodeCommand = {
4462
- name: "fd-review-code",
4463
- description: "Parallel reviewer + researcher + tester \u2014 aggregates into critical/major/minor report",
4464
- async execute(context, args) {
4465
- const dir = context.directory ?? process.cwd();
4466
- const sp = statePath(dir);
4467
- if (!existsSync35(sp)) {
4468
- return {
4469
- error: "STATE.md not found. Run /new-project first.",
4470
- code: "NOT_INITIALIZED"
4471
- };
4472
- }
4473
- const scope = args?.scope || "all";
4474
- const state = readPlanningState(dir);
4475
- if (scope.includes("/") && !scope.startsWith("./")) {
4476
- return {
4477
- error: "Invalid scope: absolute paths not allowed",
4478
- code: "INVALID_SCOPE",
4479
- hint: "Use relative paths like ./src or module name"
4480
- };
4481
- }
4482
- const radar = runImpactRadar(dir, scope !== "all" ? scope : "");
4483
- const policyViolations = evaluatePolicies(dir, {
4484
- command: "fd-review-code",
4485
- file_path: scope !== "all" ? scope : undefined
4486
- });
4487
- const workflow = "review-code-flow.md";
4488
- const config = {
4489
- agents: [
4490
- { name: "reviewer", focus: "quality,security,conventions,TDD_discipline", severity: "critical,high" },
4491
- { name: "researcher", focus: "api contracts,edge cases" },
4492
- { name: "tester", mode: "coverage" }
4493
- ],
4494
- scope,
4495
- aggregate: {
4496
- critical: "reviewer.critical + researcher.critical",
4497
- major: "reviewer.major + researcher.major + tester.missing_tests",
4498
- minor: "reviewer.minor + tester.minor"
4499
- },
4500
- tdd_checks: {
4501
- new_behavior_without_test: "flag as major",
4502
- bugfix_without_regression: "flag as critical",
4503
- implementation_larger_than_tests: "flag as major",
4504
- refactor_without_green: "flag as block"
4505
- },
4506
- impact_radar: radar,
4507
- policy_violations: policyViolations
4508
- };
4509
- if (args?.json) {
4510
- return {
4511
- success: true,
4512
- data: { workflow, config, phase: state.phase },
4513
- meta: { formatted: "json", timestamp: timestamp() }
4514
- };
4515
- }
4516
- const radarLines = impactRadarSummaryLines(radar);
4517
- const tableLines = [
4518
- "\u2500".repeat(50),
4519
- `Code Review: scope=${scope}`,
4520
- `Phase ${state.phase} | spawning 3 agents in parallel`,
4521
- "\u2500".repeat(50),
4522
- " reviewer \u2192 quality, security, conventions",
4523
- " researcher \u2192 API contracts, edge cases",
4524
- " tester \u2192 test coverage",
4525
- ...radarLines,
4526
- ...policyViolations.length > 0 ? ["\u2500".repeat(50), formatViolations(policyViolations)] : [],
4527
- "\u2500".repeat(50),
4528
- "Aggregating results into critical/major/minor report...",
4529
- "\u2550".repeat(50)
4530
- ];
4531
- return {
4532
- success: true,
4533
- message: tableLines.join(`
4534
- `),
4535
- workflow,
4536
- config,
4537
- phase: state.phase,
4538
- impact_radar: radar,
4539
- meta: { formatted: "table", timestamp: timestamp() }
4540
- };
4541
- }
4542
- };
4543
-
4544
- // src/commands/execution/write-docs.ts
4545
- import { existsSync as existsSync36 } from "fs";
4546
- var writeDocsCommand = {
4547
- name: "fd-write-docs",
4548
- description: "Explore public APIs \u2014 writer drafts \u2014 reviewer accuracy check \u2014 writer final",
4549
- async execute(context, args) {
4550
- const dir = context.directory ?? process.cwd();
4551
- const sp = statePath(dir);
4552
- if (!existsSync36(sp)) {
4553
- return {
4554
- error: "STATE.md not found. Run /new-project first.",
4555
- code: "NOT_INITIALIZED"
4556
- };
4557
- }
4558
- const scope = args?.scope || "public";
4559
- const state = readPlanningState(dir);
4560
- if (scope.includes("/") && !scope.startsWith("./")) {
4561
- return {
4562
- error: "Invalid scope: absolute paths not allowed",
4563
- code: "INVALID_SCOPE"
4564
- };
4565
- }
4566
- const workflow = "write-docs-flow.md";
4567
- const config = {
4568
- agents: [
4569
- { name: "writer", model: "claude-haiku-4-5", temperature: 0.6, maxSteps: 20 },
4570
- { name: "reviewer", focus: "accuracy", check: "consistency with actual implementation" }
4571
- ],
4572
- phases: [
4573
- { step: "explore", agent: "writer", action: "discover public APIs and their signatures" },
4574
- { step: "draft", agent: "writer", action: "generate documentation draft" },
4575
- { step: "review", agent: "reviewer", action: "verify accuracy against source code" },
4576
- { step: "finalize", agent: "writer", action: "incorporate reviewer feedback and produce final docs" }
4577
- ],
4578
- scope
4579
- };
4580
- if (args?.json) {
4581
- return {
4582
- success: true,
4583
- data: { workflow, config, phase: state.phase },
4584
- meta: { formatted: "json", timestamp: timestamp() }
4585
- };
4586
- }
4587
- const tableLines = [
4588
- "\u2500".repeat(50),
4589
- `Write Docs: scope=${scope}`,
4590
- `Phase ${state.phase} | 4-step workflow`,
4591
- "\u2500".repeat(50),
4592
- " [1] explore \u2192 discover public APIs",
4593
- " [2] draft \u2192 writer generates draft",
4594
- " [3] review \u2192 reviewer verifies accuracy",
4595
- " [4] finalize \u2192 writer produces final",
4596
- "\u2550".repeat(50)
4597
- ];
4598
- return {
4599
- success: true,
4600
- message: tableLines.join(`
4601
- `),
4602
- workflow,
4603
- config,
4604
- phase: state.phase,
4605
- meta: { formatted: "table", timestamp: timestamp() }
4606
- };
4607
- }
4608
- };
4609
-
4610
- // src/commands/execution/deploy-check.ts
4611
- import { existsSync as existsSync37 } from "fs";
4612
- var deployCheckCommand = {
4613
- name: "fd-deploy-check",
4614
- description: "Parallel tester + reviewer + researcher CVE check \u2014 orchestrator go/no-go decision",
4615
- async execute(context, args) {
4616
- const dir = context.directory ?? process.cwd();
4617
- const sp = statePath(dir);
4618
- if (!existsSync37(sp)) {
4619
- return {
4620
- error: "STATE.md not found. Run /new-project first.",
4621
- code: "NOT_INITIALIZED"
4622
- };
4623
- }
4624
- const state = readPlanningState(dir);
4625
- const workflow = "deploy-check-flow.md";
4626
- const config = {
4627
- agents: [
4628
- { name: "tester", budget: 10, action: "run test suite + TDD coverage check" },
4629
- { name: "reviewer", budget: 8, action: "quality and security review + TDD discipline" },
4630
- { name: "researcher", budget: 12, action: "CVE check (OSV.dev API, no critical false negatives)" }
4631
- ],
4632
- parallel: true,
4633
- aggregation: {
4634
- go_signals: ["testResult.passed", "reviewResult.approved", "cveResult.no_critical_cves", "tddResult.all_behaviors_tested"],
4635
- no_go_signals: ["testResult.failures", "reviewResult.critical_issues", "cveResult.critical_cves_found", "tddResult.missing_tests", "tddResult.bugfix_no_regression"]
4636
- },
4637
- decision: {
4638
- go: "All checks passed \u2014 safe to deploy",
4639
- no_go: "Deploy blocked by: {blocked_by}"
4640
- },
4641
- tdd_aware_checks: {
4642
- new_feature_changes_have_tests: "verify test delta matches code delta",
4643
- bugfix_has_regression_coverage: "ensure regression test exists and passes",
4644
- no_suspicious_test_omissions: "flag if code changed but no corresponding test change",
4645
- overrides_logged: "fail if TDD override used but not surfaced in review"
4646
- }
4647
- };
4648
- if (args?.json) {
4649
- return {
4650
- success: true,
4651
- data: { workflow, config, phase: state.phase },
4652
- meta: { formatted: "json", timestamp: timestamp() }
4653
- };
4654
- }
4655
- const tableLines = [
4656
- "\u2550".repeat(55),
4657
- `Deploy Check: phase ${state.phase}`,
4658
- "\u2500".repeat(55),
4659
- " Parallel agents (step budget):",
4660
- " tester \u2192 10 steps (run test suite + TDD coverage)",
4661
- " reviewer \u2192 8 steps (quality + security + TDD discipline)",
4662
- " researcher \u2192 12 steps (CVE check via OSV.dev)",
4663
- "\u2500".repeat(55),
4664
- " TDD-aware checks:",
4665
- " \u2022 new feature changes have corresponding tests",
4666
- " \u2022 bugfixes have regression coverage",
4667
- " \u2022 test deltas match code deltas",
4668
- " \u2022 no suspicious test omissions",
4669
- " Aggregating results for go/no-go...",
4670
- "\u2500".repeat(55),
4671
- " GO signals: test passed, review approved, no critical CVEs",
4672
- " NO-GO signals: test failures, critical issues, critical CVEs found",
4673
- "\u2550".repeat(55)
4674
- ];
4675
- return {
4676
- success: true,
4677
- message: tableLines.join(`
4678
- `),
4679
- workflow,
4680
- config,
4681
- phase: state.phase,
4682
- meta: { formatted: "table", timestamp: timestamp() }
4683
- };
4684
- }
4685
- };
4686
-
4687
- // src/commands/state/progress.ts
4688
- import { existsSync as existsSync38, readFileSync as readFileSync34 } from "fs";
4689
- var progressCommand = {
4690
- name: "fd-progress",
4691
- description: "Display STATE.md, active PLAN.md, and recent RESULT.md files",
4692
- async execute(context, args) {
4693
- const dir = context.directory ?? process.cwd();
4694
- const sp = statePath(dir);
4695
- if (!existsSync38(sp)) {
4696
- return {
4697
- error: "STATE.md not found. Initialize project first with /new-project.",
4698
- code: "NOT_INITIALIZED",
4699
- hint: "Run /new-project to initialize the project"
4700
- };
4701
- }
4702
- const stateContent = readFileSync34(sp, "utf-8");
4703
- const state = parseState(stateContent);
4704
- const phaseMatch = stateContent.match(/^phase:\s*(\d+)/m);
4705
- const phase = phaseMatch ? parseInt(phaseMatch[1], 10) : 1;
4706
- let planContent = null;
4707
- const planPath = phasePlanPath(dir, phase);
4708
- if (existsSync38(planPath)) {
4709
- planContent = readFileSync34(planPath, "utf-8");
4710
- }
4711
- const recentResults = [];
4712
- for (let p = Math.max(1, phase - 2);p <= phase; p++) {
4713
- const rp = resultPath(dir, p);
4714
- if (existsSync38(rp)) {
4715
- recentResults.push({ phase: p, content: readFileSync34(rp, "utf-8") });
4716
- }
4717
- }
4718
- const output = {
4719
- state,
4720
- phase,
4721
- plan_preview: planContent ? planContent.substring(0, 500) : null,
4722
- recent_results: recentResults.map((r) => ({
4723
- phase: r.phase,
4724
- preview: r.content.substring(0, 200)
4725
- })),
4726
- last_updated: state.last_updated || timestamp()
4727
- };
4728
- if (args?.json) {
4729
- return {
4730
- success: true,
4731
- data: output,
4732
- meta: { formatted: "json", timestamp: timestamp() }
4733
- };
4734
- }
4735
- const tableLines = [
4736
- "\u2550".repeat(60),
4737
- `Phase: ${phase} | Status: ${state.status || "unknown"} | Updated: ${output.last_updated}`,
4738
- "\u2500".repeat(60)
4739
- ];
4740
- if (planContent) {
4741
- const stepsComplete = (stateContent.match(/steps_complete:\s*\[([^\]]*)\]/)?.[1] || "").split(",").filter((s) => s.trim()).length;
4742
- const totalSteps = (planContent.match(/Step\s+\d+/g) || []).length;
4743
- tableLines.push(`Plan: ${totalSteps} steps (${stepsComplete} complete)`);
4744
- } else {
4745
- tableLines.push("Plan: No active plan");
4746
- }
4747
- if (recentResults.length > 0) {
4748
- tableLines.push(`Recent results: ${recentResults.map((r) => `Phase ${r.phase}`).join(", ")}`);
4749
- }
4750
- tableLines.push("\u2550".repeat(60));
4751
- return {
4752
- success: true,
4753
- message: tableLines.join(`
4754
- `),
4755
- data: output,
4756
- meta: { formatted: "table", timestamp: timestamp() }
4757
- };
4758
- }
4759
- };
4760
-
4761
- // src/commands/state/resume.ts
4762
- import { existsSync as existsSync39, readFileSync as readFileSync35 } from "fs";
4763
- import { join as join31 } from "path";
4764
- var resumeCommand = {
4765
- name: "fd-resume",
4766
- description: "Reload STATE.md + last PLAN.md + DISCUSS.md \u2014 brief user, PAUSE for confirmation, then continue from where stopped",
4767
- async execute(context, args) {
4768
- const dir = context.directory ?? process.cwd();
4769
- const sp = statePath(dir);
4770
- const pd = planningDir(dir);
4771
- if (!existsSync39(sp)) {
4772
- return {
4773
- error: "STATE.md not found. Run /new-project first to initialize the project.",
4774
- code: "NOT_INITIALIZED"
4775
- };
4776
- }
4777
- const stateContent = readFileSync35(sp, "utf-8");
4778
- const phaseMatch = stateContent.match(/^phase:\s*(\d+)/m);
4779
- if (!phaseMatch) {
4780
- return {
4781
- error: "No phase found in STATE.md. Project may be corrupted.",
4782
- code: "CORRUPTED"
4783
- };
4784
- }
4785
- const phase = parseInt(phaseMatch[1], 10);
4786
- const state = parseState(stateContent);
4787
- const planFile = state.plan_file || phasePlanPath(dir, phase);
4788
- const discussFile = join31(pd, "phases", `phase-${phase}`, "DISCUSS.md");
4789
- let planContent = null;
4790
- let discussContent = null;
4791
- if (existsSync39(planFile)) {
4792
- planContent = readFileSync35(planFile, "utf-8");
4793
- }
4794
- if (existsSync39(discussFile)) {
4795
- discussContent = readFileSync35(discussFile, "utf-8");
4796
- }
4797
- const stepsCompleteMatch = stateContent.match(/^steps_complete:\s*\[([^\]]*)\]/m);
4798
- const stepsComplete = stepsCompleteMatch ? stepsCompleteMatch[1].split(",").filter((s) => s.trim()) : [];
4799
- const stepsPendingMatch = stateContent.match(/^steps_pending:\s*\[([^\]]*)\]/m);
4800
- const stepsPending = stepsPendingMatch ? stepsPendingMatch[1].split(",").filter((s) => s.trim()) : [];
4801
- const lastActionMatch = stateContent.match(/^last_action:\s*"?([^"\n]+)"?/m);
4802
- const lastAction = lastActionMatch ? lastActionMatch[1] : "unknown";
4803
- const stepsDone = stepsComplete.length;
4804
- const stepsTotal = stepsDone + stepsPending.length;
4805
- if (args?.yes) {
4806
- args = { ...args, confirm: true };
4807
- }
4808
- if (args?.confirm === false) {
4809
- return skipResponse("session-resume");
4810
- }
4811
- if (!args?.confirm) {
4812
- const briefing = [
4813
- "\u2550".repeat(55),
4814
- `RESUME \u2014 Phase ${phase} Brief`,
4815
- "\u2550".repeat(55),
4816
- "",
4817
- `Last session ended at step ${stepsDone}/${stepsTotal}.`,
4818
- "",
4819
- `Completed (${stepsDone}):`,
4820
- ...stepsComplete.map((s) => ` \u2713 Step ${s}`),
4821
- "",
4822
- `Next (${stepsPending.length}):`,
4823
- ...stepsPending.slice(0, 5).map((s) => ` \u25CB Step ${s}`),
4824
- ...stepsPending.length > 5 ? [` ... and ${stepsPending.length - 5} more`] : [],
4825
- "",
4826
- `Last action: ${lastAction}`,
4827
- "",
4828
- "\u2500".repeat(55),
4829
- "Type CONFIRM to resume from where you left off",
4830
- "\u2550".repeat(55)
4831
- ];
4832
- return {
4833
- success: true,
4834
- message: briefing.join(`
4835
- `),
4836
- position: {
4837
- steps_done: stepsDone,
4838
- steps_remaining: stepsPending.length,
4839
- last_action: lastAction
4840
- },
4841
- status: "AWAITING_CONFIRM",
4842
- phase,
4843
- next_step: stepsPending[0] ? `Step ${stepsPending[0]}` : "All steps complete"
4844
- };
4845
- }
4846
- const restored = {
4847
- state: { phase, status: state.status, steps_complete: stepsComplete, steps_pending: stepsPending },
4848
- plan: planContent,
4849
- discuss: discussContent,
4850
- position: {
4851
- steps_done: stepsDone,
4852
- steps_remaining: stepsPending.length,
4853
- last_action: lastAction
4854
- }
4855
- };
4856
- const message = [
4857
- `\u2713 Resumed phase ${phase}: ${state.status}`,
4858
- `${stepsPending.length} steps remaining`,
4859
- `Last action: ${lastAction}`,
4860
- `Next: Step ${stepsPending[0] || "none"}`
4861
- ].join(" | ");
4862
- return {
4863
- success: true,
4864
- message,
4865
- restored,
4866
- meta: { formatted: "table", timestamp: timestamp() }
4867
- };
4868
- }
4869
- };
4870
-
4871
- // src/commands/state/checkpoint.ts
4872
- import { readFileSync as readFileSync36, writeFileSync as writeFileSync19, existsSync as existsSync40 } from "fs";
4873
- var checkpointCommand = {
4874
- name: "fd-checkpoint",
4875
- description: "Force-save current state to STATE.md \u2014 safe to close session",
4876
- async execute(context, args) {
4877
- const dir = context.directory ?? process.cwd();
4878
- const sp = statePath(dir);
4879
- if (!existsSync40(sp)) {
4880
- return { error: "STATE.md not found. Initialize project first with /new-project." };
4881
- }
4882
- if (!args?.yes) {
4883
- return {
4884
- ...confirmPrompt("checkpoint-save", "Save checkpoint to STATE.md? [y/n]"),
4885
- saved_to: sp
4886
- };
4887
- }
4888
- let content = readFileSync36(sp, "utf-8");
4889
- const updated = timestamp();
4890
- if (content.includes("last_updated:")) {
4891
- content = content.replace(/^last_updated:.*/m, `last_updated: "${updated}"`);
4892
- }
4893
- if (content.includes("**Last updated:**")) {
4894
- content = content.replace(/\*\*Last updated:\*\*.*/, `**Last updated:** ${updated}`);
4895
- }
4896
- if (content.includes("## Session History")) {
4897
- const checkpointEntry = `- ${updated} \u2014 Checkpoint saved`;
4898
- content = content.replace(/(\n## Session History\n)/, `$1${checkpointEntry}
4899
- `);
4900
- }
4901
- writeFileSync19(sp, content, "utf-8");
4902
- return {
4903
- success: true,
4904
- message: `Checkpoint saved at ${updated}`,
4905
- saved_to: sp
4906
- };
4907
- }
4908
- };
4909
-
4910
- // src/commands/state/workspace-commands.ts
4911
- import { readFileSync as readFileSync37, writeFileSync as writeFileSync20, existsSync as existsSync41 } from "fs";
4912
- import { join as join32, resolve as resolve3 } from "path";
4913
- function getRepoName2(repoPath) {
4914
- return repoPath.split(/[/\\]/).pop() || repoPath;
4915
- }
4916
- function parseWorkspaceState2(content) {
4917
- const result = {};
4918
- for (const line of content.split(`
4919
- `)) {
4920
- const kvMatch = line.match(/^\*\*([^:]+):\*\*\s*(.+)/);
4921
- if (kvMatch) {
4922
- result[kvMatch[1].trim()] = kvMatch[2].trim();
4923
- }
4924
- }
4925
- return result;
4926
- }
4927
- function readRepoState(repoPath) {
4928
- const sp = statePath(repoPath);
4929
- if (!existsSync41(sp))
4930
- return null;
4931
- const content = readFileSync37(sp, "utf-8");
4932
- return parseWorkspaceState2(content);
4933
- }
4934
- async function listSubRepos(dir, subRepos, workspaceRoot) {
4935
- const configPath = join32(workspaceRoot, ".planning", "config.json");
4936
- const resolved = resolveSubRepos(configPath, subRepos);
4937
- const repos = [];
4938
- for (const repoPath of resolved) {
4939
- const repoName = getRepoName2(repoPath);
4940
- const planningPath = planningDir(repoPath);
4941
- const hasPlanning = existsSync41(planningPath);
4942
- repos.push({
4943
- name: repoName,
4944
- path: repoPath,
4945
- status: hasPlanning ? "active" : "not_initialized"
4946
- });
4947
- }
4948
- return repos;
4949
- }
4950
- function renderStatusTable(repos, repoStates) {
4951
- const lines = [];
4952
- lines.push("\u2550".repeat(60));
4953
- lines.push("Workspace Status");
4954
- lines.push("\u2500".repeat(60));
4955
- const header = " Repo | Phase | Status | Progress";
4956
- lines.push(header);
4957
- lines.push("\u2500".repeat(60));
4958
- for (const repo of repos) {
4959
- const state = repoStates[repo.name];
4960
- const repoName = repo.name.padEnd(20);
4961
- let phase = "\u2014".padEnd(8);
4962
- let status = repo.status.padEnd(10);
4963
- let progress = "N/A".padEnd(15);
4964
- if (repo.status === "active" && state) {
4965
- if (state.phase)
4966
- phase = String(state.phase).padEnd(8);
4967
- if (state.status)
4968
- status = String(state.status).padEnd(10);
4969
- if (state.progress) {
4970
- const p = state.progress;
4971
- const percent = p.percent;
4972
- if (percent !== undefined) {
4973
- progress = `${percent}%`.padEnd(15);
4974
- }
4975
- }
4976
- }
4977
- lines.push(` ${repoName} | ${phase} | ${status} | ${progress}`);
4978
- }
4979
- lines.push("\u2550".repeat(60));
4980
- const notInitialized = repos.filter((r) => r.status === "not_initialized");
4981
- if (notInitialized.length > 0) {
4982
- lines.push("");
4983
- lines.push(`\u26A0 ${notInitialized.length} repo(s) not initialized: ${notInitialized.map((r) => r.name).join(", ")}`);
4984
- lines.push("Run /workspace add to add repos, or /workspace sync to sync state.");
4985
- }
4986
- return lines;
4987
- }
4988
- var statusCommand = {
4989
- name: "fd-workspace status",
4990
- description: "Display workspace overview: all repos, their current phase, status, and progress",
4991
- async execute(context, args) {
4992
- const dir = context.directory ?? process.cwd();
4993
- const workspaceRoot = findWorkspaceRoot(dir);
4994
- if (!workspaceRoot) {
4995
- return { error: "Workspace root not found. No config.json with sub_repos found." };
4996
- }
4997
- const config = getWorkspaceConfig(dir);
4998
- if (!config) {
4999
- return { error: "Could not read workspace config." };
5000
- }
5001
- const subRepos = config.sub_repos || [];
5002
- if (subRepos.length === 0) {
5003
- return {
5004
- success: true,
5005
- message: "No sub-repos configured. Add repos with /workspace add <path> or configure sub_repos in config.json."
5006
- };
5007
- }
5008
- const repos = await listSubRepos(dir, subRepos, workspaceRoot);
5009
- const repoStates = {};
5010
- for (const repo of repos) {
5011
- if (repo.status === "active") {
5012
- repoStates[repo.name] = readRepoState(repo.path) || {};
5013
- }
5014
- }
5015
- const tableLines = renderStatusTable(repos, repoStates);
5016
- return {
5017
- success: true,
5018
- message: tableLines.join(`
5019
- `),
5020
- data: {
5021
- workspace_root: workspaceRoot,
5022
- workspace_mode: config.workspace_mode,
5023
- repos: repos.map((r) => ({ name: r.name, status: r.status, state: repoStates[r.name] || null }))
5024
- }
5025
- };
5026
- }
5027
- };
5028
- var syncCommand = {
5029
- name: "fd-workspace sync",
5030
- description: "Sync workspace root STATE.md with all sub-repo states (shared mode)",
5031
- async execute(context) {
5032
- const dir = context.directory ?? process.cwd();
5033
- const workspaceRoot = findWorkspaceRoot(dir);
5034
- if (!workspaceRoot) {
5035
- return { error: "No workspace found. Ensure config.json has sub_repos configured." };
5036
- }
5037
- const config = getWorkspaceConfig(dir);
5038
- if (!config) {
5039
- return { error: "Could not read workspace config." };
5040
- }
5041
- if (config.workspace_mode === "per-repo") {
5042
- return {
5043
- success: true,
5044
- message: "Sync not needed in per-repo mode. Each repo manages its own .planning/."
5045
- };
5046
- }
5047
- const subRepos = config.sub_repos || [];
5048
- const repos = await listSubRepos(dir, subRepos, workspaceRoot);
5049
- const activeRepos = repos.filter((r) => r.status === "active");
5050
- if (activeRepos.length === 0) {
5051
- return {
5052
- success: true,
5053
- message: "No active repos to sync."
5054
- };
5055
- }
5056
- const tableLines = ["\u2550".repeat(60), "Workspace Sync Results", "\u2500".repeat(60)];
5057
- let syncCount = 0;
5058
- for (const repo of activeRepos) {
5059
- const state = readRepoState(repo.path);
5060
- if (state) {
5061
- syncCount++;
5062
- const phase = state.phase ? String(state.phase) : "\u2014";
5063
- const status = state.status ? String(state.status) : "unknown";
5064
- tableLines.push(` ${repo.name.padEnd(22)} | Phase ${phase.padEnd(4)} | ${status}`);
5065
- }
5066
- }
5067
- let workspaceStateContent = "";
5068
- const workspaceStatePath = statePath(workspaceRoot);
5069
- if (existsSync41(workspaceStatePath)) {
5070
- workspaceStateContent = readFileSync37(workspaceStatePath, "utf-8");
5071
- } else {
5072
- workspaceStateContent = `---
5073
-
5074
- ## Sub-Repo Status
5075
- `;
5076
- }
5077
- const repoEntries = activeRepos.map((r) => {
5078
- const s = readRepoState(r.path);
5079
- return `**${r.name}:** Phase ${s?.phase ?? "?"} | ${s?.status ?? "?"} | Updated ${timestamp()}`;
5080
- }).join(`
5081
- `);
5082
- if (workspaceStateContent.includes("## Sub-Repo Status")) {
5083
- workspaceStateContent = workspaceStateContent.replace(/## Sub-Repo Status\n[\s\S]*/, `## Sub-Repo Status
5084
- ${repoEntries}
5085
- `);
5086
- } else {
5087
- workspaceStateContent += `
5088
- ## Sub-Repo Status
5089
- ${repoEntries}
5090
- `;
5091
- }
5092
- writeFileSync20(workspaceStatePath, workspaceStateContent, "utf-8");
5093
- tableLines.push("\u2550".repeat(60));
5094
- tableLines.push(`Sync complete. ${syncCount}/${activeRepos.length} repos synchronized.`);
5095
- tableLines.push(`Workspace STATE.md updated at: ${workspaceRoot}`);
5096
- return {
5097
- success: true,
5098
- message: tableLines.join(`
5099
- `)
5100
- };
5101
- }
5102
- };
5103
- var switchCommand = {
5104
- name: "fd-workspace switch",
5105
- description: "Switch current active repo in workspace context",
5106
- async execute(context, args) {
5107
- const dir = context.directory ?? process.cwd();
5108
- if (!args?.repo) {
5109
- return { error: "Repo name required. Usage: /workspace switch [repo]" };
5110
- }
5111
- const workspaceRoot = findWorkspaceRoot(dir);
5112
- if (!workspaceRoot) {
5113
- return { error: "No workspace found. Ensure config.json has sub_repos configured." };
5114
- }
5115
- const config = getWorkspaceConfig(dir);
5116
- if (!config) {
5117
- return { error: "Could not read workspace config." };
5118
- }
5119
- const subRepos = config.sub_repos || [];
5120
- const resolved = resolveSubRepos(join32(workspaceRoot, ".planning", "config.json"), subRepos);
5121
- const targetPath = resolved.find((p) => getRepoName2(p) === args.repo);
5122
- if (!targetPath) {
5123
- return { error: `Repo '${args.repo}' not found in workspace sub_repos.` };
5124
- }
5125
- const workspaceStatePath = statePath(workspaceRoot);
5126
- let content = existsSync41(workspaceStatePath) ? readFileSync37(workspaceStatePath, "utf-8") : `---
5127
-
5128
- `;
5129
- const currentRepoLine = `**current_repo:** ${args.repo}`;
5130
- if (content.includes("**current_repo:**")) {
5131
- content = content.replace(/\*\*current_repo:\*\*.*/m, currentRepoLine);
5132
- } else {
5133
- content = content.replace(/^---\n/, `---
5134
-
5135
- ${currentRepoLine}
5136
- `);
5137
- }
5138
- writeFileSync20(workspaceStatePath, content, "utf-8");
5139
- return {
5140
- success: true,
5141
- message: `Switched to repo '${args.repo}'. Subsequent commands will target this repo.`
5142
- };
5143
- }
5144
- };
5145
- var addCommand = {
5146
- name: "fd-workspace add",
5147
- description: "Add a repository path to workspace sub_repos",
5148
- async execute(context, args) {
5149
- const dir = context.directory ?? process.cwd();
5150
- if (!args?.path) {
5151
- return { error: "Path required. Usage: /workspace add [path]" };
5152
- }
5153
- const workspaceRoot = findWorkspaceRoot(dir);
5154
- if (!workspaceRoot) {
5155
- return { error: "No workspace found. Ensure config.json has sub_repos configured." };
5156
- }
5157
- const resolvedPath = resolve3(dir, args.path);
5158
- const planningPath = planningDir(resolvedPath);
5159
- if (!existsSync41(planningPath)) {
5160
- return { error: "Path does not have a .planning/ directory. Run /new-project in that repo first." };
5161
- }
5162
- const configPath = join32(workspaceRoot, ".planning", "config.json");
5163
- let configContent = readFileSync37(configPath, "utf-8");
5164
- let config = JSON.parse(configContent);
5165
- if (!config.sub_repos) {
5166
- config.sub_repos = [];
5167
- }
5168
- if (!config.sub_repos.includes(args.path)) {
5169
- config.sub_repos.push(args.path);
5170
- configContent = JSON.stringify(config, null, 2);
5171
- writeFileSync20(configPath, configContent, "utf-8");
5172
- }
5173
- return {
5174
- success: true,
5175
- message: `Added '${args.path}' to workspace. Run /workspace sync to include it in workspace state.`
5176
- };
5177
- }
5178
- };
5179
- var removeCommand = {
5180
- name: "fd-workspace remove",
5181
- description: "Remove a repository from workspace sub_repos (by repo name, not path)",
5182
- async execute(context, args) {
5183
- const dir = context.directory ?? process.cwd();
5184
- if (!args?.repo) {
5185
- return { error: "Repo name required. Usage: /workspace remove [repo]" };
5186
- }
5187
- const workspaceRoot = findWorkspaceRoot(dir);
5188
- if (!workspaceRoot) {
5189
- return { error: "No workspace found. Ensure config.json has sub_repos configured." };
5190
- }
5191
- const configPath = join32(workspaceRoot, ".planning", "config.json");
5192
- const configContent = readFileSync37(configPath, "utf-8");
5193
- const config = JSON.parse(configContent);
5194
- if (!config.sub_repos || !Array.isArray(config.sub_repos)) {
5195
- return { error: `Repo '${args.repo}' not found in workspace sub_repos.` };
5196
- }
5197
- const resolved = resolveSubRepos(configPath, config.sub_repos);
5198
- const targetPath = resolved.find((p) => getRepoName2(p) === args.repo);
5199
- if (!targetPath) {
5200
- return { error: `Repo '${args.repo}' not found in workspace sub_repos.` };
5201
- }
5202
- const originalPath = config.sub_repos.find((p) => {
5203
- const resolvedP = resolve3(workspaceRoot, p);
5204
- return resolvedP === targetPath || getRepoName2(resolvedP) === args.repo;
5205
- });
5206
- if (originalPath) {
5207
- config.sub_repos = config.sub_repos.filter((p) => p !== originalPath);
5208
- writeFileSync20(configPath, JSON.stringify(config, null, 2), "utf-8");
5209
- }
5210
- return {
5211
- success: true,
5212
- message: `Removed '${args.repo}' from workspace. Files unchanged.`
5213
- };
5214
- }
5215
- };
5216
- var defaultCommand = {
5217
- name: "fd-workspace",
5218
- description: "Workspace management commands (status, sync, switch, add, remove)",
5219
- async execute(context, args) {
5220
- return statusCommand.execute(context, args);
5221
- }
5222
- };
5223
- var workspaceCommands = [
5224
- statusCommand,
5225
- syncCommand,
5226
- switchCommand,
5227
- addCommand,
5228
- removeCommand,
5229
- defaultCommand
5230
- ];
5231
-
5232
- // src/commands/state/multi-repo.ts
5233
- import { readFileSync as readFileSync38, writeFileSync as writeFileSync21, existsSync as existsSync42, mkdirSync as mkdirSync16 } from "fs";
5234
- import { join as join33, resolve as resolve4, basename } from "path";
5235
- import { execSync as execSync2 } from "child_process";
5236
- var VALID_ROLES = ["upstream-api", "downstream-consumer", "shared-lib", "gateway", "worker"];
5237
- function configPath(dir) {
5238
- return join33(planningDir(dir), "config.json");
5239
- }
5240
- function readConfig(dir) {
5241
- const cfg = configPath(dir);
5242
- if (!existsSync42(cfg)) {
5243
- return { multi_repos: [] };
5244
- }
5245
- try {
5246
- const parsed = JSON.parse(readFileSync38(cfg, "utf-8"));
5247
- if (!Array.isArray(parsed.multi_repos))
5248
- parsed.multi_repos = [];
5249
- return parsed;
5250
- } catch {
5251
- return { multi_repos: [] };
5252
- }
5253
- }
5254
- function writeConfig(dir, config) {
5255
- const pd = planningDir(dir);
5256
- if (!existsSync42(pd))
5257
- mkdirSync16(pd, { recursive: true });
5258
- writeFileSync21(configPath(dir), JSON.stringify(config, null, 2), "utf-8");
5259
- }
5260
- function getGitBranch(repoPath) {
5261
- try {
5262
- return execSync2(`git -C "${repoPath}" branch --show-current`, {
5263
- encoding: "utf-8",
5264
- stdio: ["pipe", "pipe", "ignore"]
5265
- }).trim() || "\u2014";
5266
- } catch {
5267
- return "\u2014";
5268
- }
5269
- }
5270
- function renderListTable(repos) {
5271
- if (repos.length === 0) {
5272
- return "No repos registered. Use /fd-multi-repo --add <path> <role> to add one.";
5273
- }
5274
- const sep = "\u2500".repeat(90);
5275
- const header = ` ${"Name".padEnd(20)} ${"Path".padEnd(25)} ${"Role".padEnd(20)} ${"Stack".padEnd(16)} Team`;
5276
- const rows = repos.map((r) => ` ${r.name.padEnd(20)} ${r.path.padEnd(25)} ${r.role.padEnd(20)} ${r.tech_stack.padEnd(16)} ${r.owner_team}`);
5277
- const lines = [
5278
- "\u2550".repeat(90),
5279
- `Multi-Repo Registry (.planning/config.json) \u2014 ${repos.length} repo(s)`,
5280
- sep,
5281
- header,
5282
- sep,
5283
- ...rows,
5284
- "\u2550".repeat(90),
5285
- `
5286
- Run /fd-multi-repo --status to check path health.`
5287
- ];
5288
- return lines.join(`
5289
- `);
5290
- }
5291
- function renderStatusTable2(repos, dir) {
5292
- if (repos.length === 0) {
5293
- return "No repos registered. Use /fd-multi-repo --add <path> <role> to add one.";
5294
- }
5295
- const sep = "\u2500".repeat(80);
5296
- const header = ` ${"Name".padEnd(18)} ${"Path".padEnd(22)} ${"Exists".padEnd(7)} ${"Branch".padEnd(18)} .planning/`;
5297
- const rows = repos.map((r) => {
5298
- const absPath = resolve4(dir, r.path);
5299
- const exists = existsSync42(absPath);
5300
- const branch = exists ? getGitBranch(absPath) : "\u2014";
5301
- const hasPlanning = exists && existsSync42(planningDir(absPath));
5302
- return ` ${r.name.padEnd(18)} ${r.path.padEnd(22)} ${exists ? "\u2705" : "\u274C".padEnd(6)} ${branch.padEnd(18)} ${hasPlanning ? "\u2705" : "\u274C"}`;
5303
- });
5304
- const warnings = [];
5305
- for (const r of repos) {
5306
- const absPath = resolve4(dir, r.path);
5307
- if (!existsSync42(absPath))
5308
- warnings.push(`Warning: ${r.name} path does not exist on disk.`);
5309
- else if (!existsSync42(planningDir(absPath)))
5310
- warnings.push(`Warning: ${r.name} has no .planning/ \u2014 cross-repo planning context unavailable.`);
5311
- }
5312
- const lines = [
5313
- "\u2550".repeat(80),
5314
- "Multi-Repo Status",
5315
- sep,
5316
- header,
5317
- sep,
5318
- ...rows,
5319
- "\u2550".repeat(80),
5320
- ...warnings.length > 0 ? ["", ...warnings] : []
5321
- ];
5322
- return lines.join(`
5323
- `);
5324
- }
5325
- var multiRepoCommand = {
5326
- name: "fd-multi-repo",
5327
- description: "Manage multi-repo registry in .planning/config.json \u2014 add, list, status, remove repos",
5328
- async execute(context, args) {
5329
- const dir = context.directory ?? process.cwd();
5330
- if (args?.add) {
5331
- const repoPath = args.add;
5332
- const role = args.role;
5333
- if (!role || !VALID_ROLES.includes(role)) {
5334
- return {
5335
- error: `Role is required and must be one of: ${VALID_ROLES.join(", ")}`,
5336
- code: "INVALID_ROLE",
5337
- hint: `Usage: /fd-multi-repo --add <path> --role <role>`
5338
- };
5339
- }
5340
- const name = args.name || basename(resolve4(dir, repoPath));
5341
- const config2 = readConfig(dir);
5342
- if (config2.multi_repos.some((r) => r.name === name || r.path === repoPath)) {
5343
- return {
5344
- error: `Repo '${name}' (or path '${repoPath}') is already registered.`,
5345
- code: "ALREADY_EXISTS"
5346
- };
5347
- }
5348
- const entry = {
5349
- name,
5350
- path: repoPath,
5351
- role,
5352
- tech_stack: args.tech_stack ?? "",
5353
- owner_team: args.owner_team ?? "",
5354
- added_at: timestamp()
5355
- };
5356
- config2.multi_repos.push(entry);
5357
- writeConfig(dir, config2);
5358
- return {
5359
- success: true,
5360
- message: `Registered '${name}' (${role}) at ${repoPath}.
5361
- Run /fd-multi-repo --status to verify path health.`,
5362
- data: entry
5363
- };
5364
- }
5365
- if (args?.remove) {
5366
- const config2 = readConfig(dir);
5367
- const before = config2.multi_repos.length;
5368
- config2.multi_repos = config2.multi_repos.filter((r) => r.name !== args.remove);
5369
- if (config2.multi_repos.length === before) {
5370
- return {
5371
- error: `Repo '${args.remove}' not found in registry.`,
5372
- code: "NOT_FOUND",
5373
- hint: `Run /fd-multi-repo --list to see registered repos.`
5374
- };
5375
- }
5376
- writeConfig(dir, config2);
5377
- return {
5378
- success: true,
5379
- message: `Removed '${args.remove}' from registry. Files on disk are unchanged.`
5380
- };
5381
- }
5382
- if (args?.status) {
5383
- const config2 = readConfig(dir);
5384
- if (args.json)
5385
- return { success: true, data: config2.multi_repos };
5386
- return { success: true, message: renderStatusTable2(config2.multi_repos, dir) };
5387
- }
5388
- const config = readConfig(dir);
5389
- if (args?.json)
5390
- return { success: true, data: config.multi_repos };
5391
- return { success: true, message: renderListTable(config.multi_repos) };
5392
- }
5393
- };
5394
-
5395
- // src/commands/intelligence/impact-radar.ts
5396
- import { existsSync as existsSync43, readFileSync as readFileSync39 } from "fs";
5397
- import { join as join34 } from "path";
5398
- var impactRadarCommand = {
5399
- name: "fd-impact-radar",
5400
- description: "Change Impact Radar \u2014 predict which files, modules, APIs, tests, and DB paths are likely affected before the AI edits anything",
5401
- async execute(context, args) {
5402
- const dir = context.directory ?? process.cwd();
5403
- const sp = statePath(dir);
5404
- if (!existsSync43(sp)) {
5405
- return { error: "STATE.md not found. Run /new-project first.", code: "NOT_INITIALIZED" };
5406
- }
5407
- const change = args?.change || "";
5408
- const scope = args?.scope || "all";
5409
- const state = readPlanningState(dir);
5410
- const cd = codebaseDir(dir);
5411
- const archPath = join34(cd, "ARCHITECTURE.md");
5412
- const stackPath = join34(cd, "STACK.md");
5413
- const architectureContext = existsSync43(archPath) ? readFileSync39(archPath, "utf-8").substring(0, 800) : null;
5414
- const stackContext = existsSync43(stackPath) ? readFileSync39(stackPath, "utf-8").substring(0, 400) : null;
5415
- const volatilityPath2 = join34(cd, "VOLATILITY.json");
5416
- let hotspots = [];
5417
- if (existsSync43(volatilityPath2)) {
5418
- try {
5419
- const v = JSON.parse(readFileSync39(volatilityPath2, "utf-8"));
5420
- hotspots = (v.entries ?? []).filter((e) => e.stability === "volatile" || e.stability === "critical").map((e) => e.path).slice(0, 10);
5421
- } catch {}
5422
- }
5423
- const config = {
5424
- agents: [
5425
- { name: "researcher", role: "trace dependency graph from changed paths" },
5426
- { name: "architect", role: "identify API contracts and service boundaries at risk" },
5427
- { name: "tester", role: "find test files that cover the affected paths" }
5428
- ],
5429
- change_description: change,
5430
- scope,
5431
- architecture_context: architectureContext,
5432
- stack_context: stackContext,
5433
- known_hotspots: hotspots
5434
- };
5435
- const workflow = "impact-radar-flow.md";
5436
- if (args?.json) {
5437
- return { success: true, data: { workflow, config, phase: state.phase }, meta: { formatted: "json", timestamp: timestamp() } };
5438
- }
5439
- const lines = [
5440
- "\u2550".repeat(58),
5441
- "Change Impact Radar",
5442
- "\u2500".repeat(58),
5443
- ` Change: ${change || "(describe with --change)"}`,
5444
- ` Scope: ${scope}`,
5445
- ` Hotspot files tracked: ${hotspots.length}`,
5446
- "\u2500".repeat(58),
5447
- " researcher \u2192 dependency graph scan",
5448
- " architect \u2192 API / service boundary risk",
5449
- " tester \u2192 affected test coverage",
5450
- "\u2500".repeat(58),
5451
- " Outputs: affected files, APIs, tests, DB paths",
5452
- "\u2550".repeat(58)
5453
- ];
5454
- return { success: true, message: lines.join(`
5455
- `), workflow, config, phase: state.phase, meta: { formatted: "table", timestamp: timestamp() } };
5456
- }
5457
- };
5458
-
5459
- // src/commands/intelligence/blast-radius.ts
5460
- import { existsSync as existsSync44, readFileSync as readFileSync40 } from "fs";
5461
- import { join as join35 } from "path";
5462
- var blastRadiusCommand = {
5463
- name: "fd-blast-radius",
5464
- description: "Blast Radius Preview \u2014 show likely downstream consequences of a proposed change including hidden dependencies and fragile integration points",
5465
- async execute(context, args) {
5466
- const dir = context.directory ?? process.cwd();
5467
- const sp = statePath(dir);
5468
- if (!existsSync44(sp)) {
5469
- return { error: "STATE.md not found. Run /new-project first.", code: "NOT_INITIALIZED" };
5470
- }
5471
- const change = args?.change || "";
5472
- const depth = parseInt(args?.depth ?? "2", 10);
5473
- const state = readPlanningState(dir);
5474
- const cd = codebaseDir(dir);
5475
- const memoryPath2 = join35(cd, "MEMORY.json");
5476
- let moduleCount = 0;
5477
- if (existsSync44(memoryPath2)) {
5478
- try {
5479
- const m = JSON.parse(readFileSync40(memoryPath2, "utf-8"));
5480
- moduleCount = Object.keys(m.nodes ?? {}).length;
5481
- } catch {}
5482
- }
5483
- const failuresPath2 = join35(cd, "FAILURES.json");
5484
- let knownFragileCount = 0;
5485
- if (existsSync44(failuresPath2)) {
5486
- try {
5487
- const f = JSON.parse(readFileSync40(failuresPath2, "utf-8"));
5488
- knownFragileCount = (f.entries ?? []).filter((e) => e.recurrence_count >= 2).length;
5489
- } catch {}
5490
- }
5491
- const config = {
5492
- agents: [
5493
- { name: "architect", role: "trace dependency graph to depth " + depth + ", flag integration points" },
5494
- { name: "researcher", role: "identify hidden couplings, shared state, event flows" },
5495
- { name: "tester", role: "predict test breakage categories (unit, integration, e2e)" }
5496
- ],
5497
- change_description: change,
5498
- traversal_depth: depth,
5499
- repo_memory_nodes: moduleCount,
5500
- known_fragile_patterns: knownFragileCount,
5501
- workflow: "blast-radius-flow.md"
5502
- };
5503
- if (args?.json) {
5504
- return { success: true, data: { config, phase: state.phase }, meta: { formatted: "json", timestamp: timestamp() } };
5505
- }
5506
- const lines = [
5507
- "\u2550".repeat(58),
5508
- "Blast Radius Preview",
5509
- "\u2500".repeat(58),
5510
- ` Change: ${change || "(describe with --change)"}`,
5511
- ` Depth: ${depth} hops`,
5512
- ` Graph: ${moduleCount} nodes in Repo Memory`,
5513
- ` Fragile: ${knownFragileCount} recurring failure patterns`,
5514
- "\u2500".repeat(58),
5515
- " architect \u2192 dependency traversal + integration risk",
5516
- " researcher \u2192 hidden couplings, shared state",
5517
- " tester \u2192 predicted test breakage",
5518
- "\u2500".repeat(58),
5519
- " Output: blast-radius report with downstream consequence map",
5520
- "\u2550".repeat(58)
5521
- ];
5522
- return { success: true, message: lines.join(`
5523
- `), config, phase: state.phase, meta: { formatted: "table", timestamp: timestamp() } };
5524
- }
5525
- };
5526
-
5527
- // src/commands/intelligence/translate-intent.ts
5528
- import { existsSync as existsSync45 } from "fs";
5529
- var translateIntentCommand = {
5530
- name: "fd-translate-intent",
5531
- description: "Intent-to-Change Translator \u2014 converts vague requests like 'make checkout faster' into 3\u20135 concrete, ranked implementation options with tradeoffs, assumptions, and clarifying questions",
5532
- async execute(context, args) {
5533
- const dir = context.directory ?? process.cwd();
5534
- const sp = statePath(dir);
5535
- if (!existsSync45(sp)) {
5536
- return { error: "STATE.md not found. Run /fd-new-project first.", code: "NOT_INITIALIZED" };
5537
- }
5538
- if (!args?.intent) {
5539
- return {
5540
- error: 'No intent provided. Use: /fd-translate-intent --intent "make checkout faster"',
5541
- code: "NO_INTENT",
5542
- hint: "Describe what you want in plain language"
5543
- };
5544
- }
5545
- const state = readPlanningState(dir);
5546
- const config = {
5547
- intent: args.intent,
5548
- agents: [
5549
- { name: "architect", role: "decompose intent into 3\u20135 concrete implementation options, each with name, description, files_affected, effort (S/M/L), risk (low/med/high), and tradeoffs" },
5550
- { name: "researcher", role: "fetch relevant codebase context, prior art, and constraints for each option" },
5551
- { name: "reviewer", role: "rank options by impact/effort/risk; select recommended option; list assumptions and clarifying questions" }
5552
- ],
5553
- output_format: {
5554
- options: "ranked list (1\u20135) with: name, description, files_affected, effort, risk, tradeoffs",
5555
- recommended_option: "index of recommended option with rationale (e.g. 'Option 2 \u2014 best risk/effort ratio')",
5556
- assumptions: "list of assumptions made about the codebase, user intent, or constraints",
5557
- clarifying_questions: "list any ambiguities that need user input before proceeding"
5558
- },
5559
- rank_options: args["rank-options"] !== false,
5560
- workflow: "translate-intent-flow.md"
5561
- };
5562
- if (args?.json) {
5563
- return { success: true, data: { config, phase: state.phase }, meta: { formatted: "json", timestamp: timestamp() } };
5564
- }
5565
- const lines = [
5566
- "\u2550".repeat(62),
5567
- "fd-translate-intent",
5568
- "\u2500".repeat(62),
5569
- ` Intent: "${args.intent}"`,
5570
- "\u2500".repeat(62),
5571
- " architect \u2192 decompose into 3\u20135 concrete options",
5572
- " researcher \u2192 fetch codebase context + prior art",
5573
- " reviewer \u2192 rank by impact/effort/risk",
5574
- "\u2500".repeat(62),
5575
- " Output:",
5576
- " \u2022 ranked options table (name, effort, risk, tradeoffs)",
5577
- " \u2022 recommended option with rationale",
5578
- " \u2022 assumptions the agent made",
5579
- " \u2022 clarifying questions if intent is ambiguous",
5580
- "\u2550".repeat(62)
5581
- ];
5582
- return { success: true, message: lines.join(`
5583
- `), config, phase: state.phase, meta: { formatted: "table", timestamp: timestamp() } };
5584
- }
5585
- };
5586
-
5587
- // src/commands/intelligence/volatility-map-cmd.ts
5588
- import { existsSync as existsSync46 } from "fs";
5589
- import { join as join36 } from "path";
5590
- var volatilityMapCommand = {
5591
- name: "fd-volatility-map",
5592
- description: "Codebase Volatility Map \u2014 highlight unstable zones based on git churn, hotfix frequency, and unresolved TODO clusters. Updates .codebase/VOLATILITY.json.",
5593
- async execute(context, args) {
5594
- const dir = context.directory ?? process.cwd();
5595
- const sp = statePath(dir);
5596
- if (!existsSync46(sp)) {
5597
- return { error: "STATE.md not found. Run /new-project first.", code: "NOT_INITIALIZED" };
5598
- }
5599
- const threshold = args?.threshold ?? "volatile";
5600
- const state = readPlanningState(dir);
5601
- const cd = codebaseDir(dir);
5602
- const existingPath = join36(cd, "VOLATILITY.json");
5603
- const hasExisting = existsSync46(existingPath);
5604
- const config = {
5605
- threshold,
5606
- agents: [
5607
- { name: "researcher", role: "run git log --follow to count commits per file (last 90 days)" },
5608
- { name: "researcher", role: "scan TODO/FIXME/HACK/XXX comments per file" },
5609
- { name: "researcher", role: "find commits with 'hotfix', 'revert', 'urgent' in message" }
5610
- ],
5611
- output_tool: "volatility-map",
5612
- output_action: "write",
5613
- output_file: ".codebase/VOLATILITY.json",
5614
- has_existing_data: hasExisting,
5615
- workflow: "volatility-map-flow.md"
5616
- };
5617
- if (args?.json) {
5618
- return { success: true, data: { config, phase: state.phase }, meta: { formatted: "json", timestamp: timestamp() } };
5619
- }
5620
- const lines = [
5621
- "\u2550".repeat(60),
5622
- "Codebase Volatility Map",
5623
- "\u2500".repeat(60),
5624
- ` Threshold: ${threshold}+ zones`,
5625
- ` Existing data: ${hasExisting ? "yes (will update)" : "no (first run)"}`,
5626
- "\u2500".repeat(60),
5627
- " researcher \u2192 git churn analysis (90 days)",
5628
- " researcher \u2192 TODO/FIXME/HACK cluster scan",
5629
- " researcher \u2192 hotfix/revert commit detection",
5630
- "\u2500".repeat(60),
5631
- " Output: .codebase/VOLATILITY.json + summary of hotspots",
5632
- "\u2550".repeat(60)
5633
- ];
5634
- return { success: true, message: lines.join(`
5635
- `), config, phase: state.phase, meta: { formatted: "table", timestamp: timestamp() } };
5636
- }
5637
- };
5638
-
5639
- // src/commands/intelligence/regression-predict.ts
5640
- import { existsSync as existsSync47, readFileSync as readFileSync41 } from "fs";
5641
- import { join as join37 } from "path";
5642
- var REGRESSION_CATEGORIES = [
5643
- "performance",
5644
- "auth",
5645
- "schema",
5646
- "ui-state",
5647
- "async-flow",
5648
- "api-contract",
5649
- "data-integrity",
5650
- "security",
5651
- "config",
5652
- "i18n"
5653
- ];
5654
- var regressionPredictCommand = {
5655
- name: "fd-regression-predict",
5656
- description: "Regression Prediction \u2014 estimate the most likely regression categories (performance, auth, schema, UI states, async flows) for a proposed change",
5657
- async execute(context, args) {
5658
- const dir = context.directory ?? process.cwd();
5659
- const sp = statePath(dir);
5660
- if (!existsSync47(sp)) {
5661
- return { error: "STATE.md not found. Run /new-project first.", code: "NOT_INITIALIZED" };
5662
- }
5663
- const change = args?.change || "";
5664
- const files = args?.files || "";
5665
- const state = readPlanningState(dir);
5666
- const cd = codebaseDir(dir);
5667
- const failuresPath2 = join37(cd, "FAILURES.json");
5668
- let pastRegressions = [];
5669
- if (existsSync47(failuresPath2)) {
5670
- try {
5671
- const data = JSON.parse(readFileSync41(failuresPath2, "utf-8"));
5672
- pastRegressions = (data.entries ?? []).flatMap((e) => e.tags ?? []);
5673
- } catch {}
5674
- }
5675
- const config = {
5676
- change_description: change,
5677
- affected_files: files ? files.split(",").map((s) => s.trim()) : [],
5678
- regression_categories: REGRESSION_CATEGORIES,
5679
- past_regression_signals: pastRegressions.slice(0, 20),
5680
- agents: [
5681
- { name: "researcher", role: "map changed code to regression category keywords and patterns" },
5682
- { name: "tester", role: "estimate coverage gaps per predicted regression category" },
5683
- { name: "reviewer", role: "rank categories by probability and severity" }
5684
- ],
5685
- output_format: {
5686
- predictions: "ranked list: category, probability (high/med/low), reason, suggested test",
5687
- top_risk: "single highest-risk regression to watch"
5688
- },
5689
- workflow: "regression-predict-flow.md"
5690
- };
5691
- if (args?.json) {
5692
- return { success: true, data: { config, phase: state.phase }, meta: { formatted: "json", timestamp: timestamp() } };
5693
- }
5694
- const lines = [
5695
- "\u2550".repeat(60),
5696
- "Regression Prediction",
5697
- "\u2500".repeat(60),
5698
- ` Change: ${change || "(describe with --change)"}`,
5699
- ` Files: ${files || "(all affected)"}`,
5700
- ` Categories: ${REGRESSION_CATEGORIES.join(", ")}`,
5701
- "\u2500".repeat(60),
5702
- " researcher \u2192 keyword/pattern mapping",
5703
- " tester \u2192 coverage gap per category",
5704
- " reviewer \u2192 ranked probability + severity",
5705
- "\u2500".repeat(60),
5706
- " Output: regression risk table with suggested tests",
5707
- "\u2550".repeat(60)
5708
- ];
5709
- return { success: true, message: lines.join(`
5710
- `), config, phase: state.phase, meta: { formatted: "table", timestamp: timestamp() } };
5711
- }
5712
- };
5713
-
5714
- // src/commands/intelligence/test-gap.ts
5715
- import { existsSync as existsSync48 } from "fs";
5716
- var testGapCommand = {
5717
- name: "fd-test-gap",
5718
- description: "Test Gap Detector \u2014 identify areas of a proposed change weakly covered by tests and suggest the minimum high-value tests to add first",
5719
- async execute(context, args) {
5720
- const dir = context.directory ?? process.cwd();
5721
- const sp = statePath(dir);
5722
- if (!existsSync48(sp)) {
5723
- return { error: "STATE.md not found. Run /new-project first.", code: "NOT_INITIALIZED" };
5724
- }
5725
- const scope = args?.scope || "all";
5726
- const change = args?.change || "";
5727
- const state = readPlanningState(dir);
5728
- const cd = codebaseDir(dir);
5729
- const config = {
5730
- scope,
5731
- change_description: change,
5732
- agents: [
5733
- { name: "tester", role: "find source files changed with no corresponding test file" },
5734
- { name: "tester", role: "detect untested branches in changed functions (if/else, error paths)" },
5735
- { name: "researcher", role: "identify integration boundaries without contract tests" },
5736
- { name: "reviewer", role: "rank gaps by risk and suggest minimum viable test additions" }
5737
- ],
5738
- gap_types: [
5739
- "missing test file for changed module",
5740
- "untested error path",
5741
- "untested branch (if/else/switch)",
5742
- "no integration test for external call",
5743
- "no regression test for previously-failed path"
5744
- ],
5745
- output_format: {
5746
- gaps: "ranked list: file, gap_type, risk, suggested_test_name, test_skeleton",
5747
- minimum_viable_set: "top 3\u20135 tests that give the most coverage per effort"
5748
- },
5749
- workflow: "test-gap-flow.md"
5750
- };
5751
- if (args?.json) {
5752
- return { success: true, data: { config, phase: state.phase }, meta: { formatted: "json", timestamp: timestamp() } };
5753
- }
5754
- const lines = [
5755
- "\u2550".repeat(60),
5756
- "Test Gap Detector",
5757
- "\u2500".repeat(60),
5758
- ` Scope: ${scope}`,
5759
- ` Change: ${change || "(describe with --change)"}`,
5760
- "\u2500".repeat(60),
5761
- " tester \u2192 missing test files + untested branches",
5762
- " researcher \u2192 integration boundary gaps",
5763
- " reviewer \u2192 ranked gaps + minimum viable test set",
5764
- "\u2500".repeat(60),
5765
- " Output: gap report + top 3\u20135 suggested tests with skeletons",
5766
- "\u2550".repeat(60)
5767
- ];
5768
- return { success: true, message: lines.join(`
5769
- `), config, phase: state.phase, meta: { formatted: "table", timestamp: timestamp() } };
5770
- }
5771
- };
5772
-
5773
- // src/commands/intelligence/review-route.ts
5774
- import { existsSync as existsSync49 } from "fs";
5775
- var ROUTING_KEYWORDS = {
5776
- security: ["auth", "token", "password", "crypto", "secret", "jwt", "permission", "rbac", "xss", "sql"],
5777
- backend: ["api", "route", "controller", "service", "database", "query", "migration"],
5778
- infra: ["docker", "kubernetes", "terraform", "ci", "cd", "deploy", "helm", "nginx", "aws", "gcp"],
5779
- "domain-owner": ["business", "billing", "payment", "checkout", "order", "subscription", "pricing"],
5780
- frontend: ["component", "css", "html", "react", "vue", "angular", "ui", "ux", "style"],
5781
- data: ["schema", "migration", "model", "index", "constraint", "foreign key", "partition"],
5782
- devops: ["pipeline", "workflow", ".yml", ".yaml", "action", "cron", "schedule", "artifact"]
5783
- };
5784
- function routeReview(filePaths, trustVerdict) {
5785
- const routes = new Set;
5786
- const combined = filePaths.join(" ").toLowerCase();
5787
- for (const [type, keywords] of Object.entries(ROUTING_KEYWORDS)) {
5788
- if (keywords.some((kw) => combined.includes(kw)))
5789
- routes.add(type);
5790
- }
5791
- if (trustVerdict === "high-risk")
5792
- routes.add("security");
5793
- return Array.from(routes);
5794
- }
5795
- var reviewRouteCommand = {
5796
- name: "fd-review-route",
5797
- description: "Human Review Routing \u2014 route risky patches to the right reviewer type (security, backend, infra, domain-owner) based on change nature and patch trust score",
5798
- async execute(context, args) {
5799
- const dir = context.directory ?? process.cwd();
5800
- const sp = statePath(dir);
5801
- if (!existsSync49(sp)) {
5802
- return { error: "STATE.md not found. Run /new-project first.", code: "NOT_INITIALIZED" };
5803
- }
5804
- const files = args?.files ? args.files.split(",").map((s) => s.trim()) : [];
5805
- const change = args?.change || "";
5806
- const state = readPlanningState(dir);
5807
- let trustVerdict = "safe";
5808
- if (files.length > 0) {
5809
- const ts = scorePatch(dir, files[0]);
5810
- trustVerdict = ts.verdict;
5811
- }
5812
- const routes = routeReview(files, trustVerdict);
5813
- const config = {
5814
- files,
5815
- change_description: change,
5816
- trust_verdict: trustVerdict,
5817
- routed_to: routes,
5818
- routing_rationale: routes.map((r) => `${r}: triggered by keywords in file paths/change description`),
5819
- workflow: "review-route-flow.md"
5820
- };
5821
- if (args?.json) {
5822
- return { success: true, data: { config, phase: state.phase }, meta: { formatted: "json", timestamp: timestamp() } };
5823
- }
5824
- const lines = [
5825
- "\u2550".repeat(60),
5826
- "Human Review Routing",
5827
- "\u2500".repeat(60),
5828
- ` Files: ${files.length > 0 ? files.join(", ") : "(all changed files)"}`,
5829
- ` Trust verdict: ${trustVerdict}`,
5830
- ` Route to: ${routes.length > 0 ? routes.join(", ") : "general reviewer"}`,
5831
- "\u2500".repeat(60),
5832
- " Routing logic: keyword match + patch trust score",
5833
- "\u2550".repeat(60)
5834
- ];
5835
- return { success: true, message: lines.join(`
5836
- `), config, phase: state.phase, routed_to: routes, meta: { formatted: "table", timestamp: timestamp() } };
5837
- }
5838
- };
5839
-
5840
- // src/commands/analysis/analyze-change.ts
5841
- import { existsSync as existsSync50, readFileSync as readFileSync43 } from "fs";
5842
- import { join as join38 } from "path";
5843
- var REGRESSION_CATEGORIES2 = [
5844
- "performance",
5845
- "auth",
5846
- "schema",
5847
- "ui-state",
5848
- "async-flow",
5849
- "api-contract",
5850
- "data-integrity",
5851
- "security",
5852
- "config",
5853
- "i18n"
5854
- ];
5855
- var REVIEWER_KEYWORDS = {
5856
- security: ["auth", "token", "password", "crypto", "jwt", "permission", "xss"],
5857
- backend: ["api", "route", "controller", "service", "database", "query", "migration"],
5858
- infra: ["docker", "kubernetes", "terraform", "ci", "deploy", "helm", "aws", "gcp"],
5859
- "domain-owner": ["billing", "payment", "checkout", "order", "subscription"],
5860
- frontend: ["component", "css", "react", "vue", "angular", "ui"],
5861
- data: ["schema", "migration", "model", "index", "constraint"],
5862
- devops: ["pipeline", "workflow", ".yml", ".yaml", "action", "cron"]
5863
- };
5864
- function routeReviewers(change, files) {
5865
- const combined = [change, ...files].join(" ").toLowerCase();
5866
- return Object.entries(REVIEWER_KEYWORDS).filter(([, kws]) => kws.some((kw) => combined.includes(kw))).map(([type]) => type);
5867
- }
5868
- var analyzeChangeCommand = {
5869
- name: "fd-analyze-change",
5870
- description: "Pre-change analysis \u2014 runs impact radar, blast radius, regression prediction, test gaps, volatility, and reviewer routing in one report",
5871
- async execute(context, args) {
5872
- const dir = context.directory ?? process.cwd();
5873
- const sp = statePath(dir);
5874
- const cd = codebaseDir(dir);
5875
- if (!existsSync50(sp)) {
5876
- return { error: "STATE.md not found. Run /fd-new-project first.", code: "NOT_INITIALIZED" };
5877
- }
5878
- const change = args?.change || "";
5879
- const scope = args?.scope || "all";
5880
- const fileList = args?.files ? args.files.split(",").map((s) => s.trim()) : [];
5881
- const depth = parseInt(args?.depth ?? "2", 10);
5882
- const runAll = !args?.impact && !args?.["blast-radius"] && !args?.regression && !args?.["test-gap"] && !args?.volatility && !args?.["review-route"] || args?.all === true;
5883
- const runImpact = runAll || !!args?.impact;
5884
- const runBlast = runAll || !!args?.["blast-radius"];
5885
- const runRegression = runAll || !!args?.regression;
5886
- const runTestGap = runAll || !!args?.["test-gap"];
5887
- const runVolatility = runAll || !!args?.volatility;
5888
- const runReviewRoute = runAll || !!args?.["review-route"];
5889
- const modulesRun = [];
5890
- const state = readPlanningState(dir);
5891
- let hotspots = [];
5892
- let knownFailures = [];
5893
- let relatedModules = [];
5894
- let riskFlag = false;
5895
- if (runImpact && change) {
5896
- modulesRun.push("impact-radar");
5897
- const radar = runImpactRadar(dir, change);
5898
- hotspots = radar.hotspots.map((h) => h.path);
5899
- knownFailures = radar.known_failures.map((f) => f.id);
5900
- relatedModules = radar.related_modules.map((m) => m.path);
5901
- riskFlag = radar.risk_flag;
5902
- }
5903
- let moduleCount = 0;
5904
- let fragileCount = 0;
5905
- if (runBlast) {
5906
- modulesRun.push("blast-radius");
5907
- const memPath = join38(cd, "MEMORY.json");
5908
- if (existsSync50(memPath)) {
5909
- try {
5910
- moduleCount = Object.keys(JSON.parse(readFileSync43(memPath, "utf-8")).nodes ?? {}).length;
5911
- } catch {}
5912
- }
5913
- const failPath = join38(cd, "FAILURES.json");
5914
- if (existsSync50(failPath)) {
5915
- try {
5916
- const data = JSON.parse(readFileSync43(failPath, "utf-8"));
5917
- fragileCount = (data.entries ?? []).filter((e) => e.recurrence_count >= 2).length;
5918
- } catch {}
5919
- }
5920
- }
5921
- let pastRegressionSignals = [];
5922
- if (runRegression) {
5923
- modulesRun.push("regression-predict");
5924
- const failPath = join38(cd, "FAILURES.json");
5925
- if (existsSync50(failPath)) {
5926
- try {
5927
- const data = JSON.parse(readFileSync43(failPath, "utf-8"));
5928
- pastRegressionSignals = (data.entries ?? []).flatMap((e) => e.tags ?? []).slice(0, 20);
5929
- } catch {}
5930
- }
5931
- }
5932
- const gapTypes = runTestGap ? [
5933
- "missing test file for changed module",
5934
- "untested error path",
5935
- "untested branch (if/else/switch)",
5936
- "no integration test for external call",
5937
- "no regression test for previously-failed path"
5938
- ] : [];
5939
- if (runTestGap)
5940
- modulesRun.push("test-gap");
5941
- let volatileZones = [];
5942
- if (runVolatility) {
5943
- modulesRun.push("volatility-map");
5944
- const volPath = join38(cd, "VOLATILITY.json");
5945
- if (existsSync50(volPath)) {
5946
- try {
5947
- const v = JSON.parse(readFileSync43(volPath, "utf-8"));
5948
- volatileZones = (v.entries ?? []).filter((e) => e.stability === "volatile" || e.stability === "critical").slice(0, 15);
5949
- } catch {}
5950
- }
5951
- }
5952
- let reviewers = [];
5953
- let trustScore = null;
5954
- let trustVerdict = "safe";
5955
- if (runReviewRoute) {
5956
- modulesRun.push("review-route");
5957
- if (fileList.length > 0) {
5958
- const ts = scorePatch(dir, fileList[0]);
5959
- trustScore = ts.score;
5960
- trustVerdict = ts.verdict;
5961
- }
5962
- reviewers = routeReviewers(change, fileList);
5963
- }
5964
- const priorFailures = runImpact || runBlast ? lookupPriorFailures(dir, scope, change, 5) : [];
5965
- const affectedZones = [...new Set([...hotspots, ...volatileZones.map((v) => v.path)])];
5966
- const riskScore = trustScore ?? Math.max(0, 100 - hotspots.length * 15 - knownFailures.length * 10 - fragileCount * 8);
5967
- const riskSummary = riskFlag ? `\u26A0 HIGH RISK: ${hotspots.length} volatile zone(s), ${knownFailures.length} known failure(s), ${fragileCount} fragile pattern(s)` : affectedZones.length > 0 ? `\u26A1 MODERATE: ${affectedZones.length} affected zone(s) detected \u2014 review before proceeding` : "\u2713 LOW RISK: No volatile zones or known failures match this change";
5968
- const config = {
5969
- change_description: change,
5970
- scope,
5971
- files: fileList,
5972
- modules_run: modulesRun,
5973
- agents: [
5974
- runImpact && { name: "researcher", role: "trace dependency graph from changed paths" },
5975
- runBlast && { name: "architect", role: `trace blast radius to depth ${depth}, flag integration points` },
5976
- runRegression && { name: "tester", role: "estimate coverage gaps per predicted regression category" },
5977
- runTestGap && { name: "tester", role: "find source files changed with no test file" },
5978
- runReviewRoute && { name: "reviewer", role: "rank gaps by risk and confirm routing" }
5979
- ].filter(Boolean),
5980
- data: {
5981
- hotspots,
5982
- known_failures: knownFailures,
5983
- related_modules: relatedModules,
5984
- volatile_zones: volatileZones,
5985
- fragile_patterns: fragileCount,
5986
- repo_memory_nodes: moduleCount,
5987
- regression_categories: runRegression ? REGRESSION_CATEGORIES2 : [],
5988
- past_regression_signals: pastRegressionSignals,
5989
- test_gap_types: gapTypes,
5990
- recommended_reviewers: reviewers,
5991
- trust_score: trustScore,
5992
- trust_verdict: trustVerdict,
5993
- prior_failures: priorFailures.map((f) => f.id)
5994
- },
5995
- risk_score: riskScore,
5996
- risk_summary: riskSummary,
5997
- traversal_depth: depth
5998
- };
5999
- if (args?.json) {
6000
- return {
6001
- success: true,
6002
- data: {
6003
- modules_run: modulesRun,
6004
- affected_zones: affectedZones,
6005
- regression_categories: runRegression ? REGRESSION_CATEGORIES2 : [],
6006
- test_gap_types: gapTypes,
6007
- recommended_reviewers: reviewers,
6008
- risk_summary: riskSummary,
6009
- risk_score: riskScore,
6010
- config,
6011
- phase: state.phase
6012
- },
6013
- meta: { formatted: "json", timestamp: timestamp() }
6014
- };
6015
- }
6016
- const lines = [
6017
- "\u2550".repeat(62),
6018
- "fd-analyze-change",
6019
- "\u2500".repeat(62),
6020
- ` Change: ${change || "(describe with --change)"}`,
6021
- ` Scope: ${scope}`,
6022
- ` Modules: ${modulesRun.join(", ") || "none selected"}`,
6023
- "\u2500".repeat(62)
6024
- ];
6025
- if (affectedZones.length > 0) {
6026
- lines.push(` \u26A0 Affected zones: ${affectedZones.slice(0, 5).join(", ")}${affectedZones.length > 5 ? ` +${affectedZones.length - 5} more` : ""}`);
6027
- }
6028
- if (knownFailures.length > 0) {
6029
- lines.push(` \u26A0 Known failures: ${knownFailures.join(", ")}`);
6030
- }
6031
- if (runRegression) {
6032
- lines.push(` \u2248 Regression cats: ${REGRESSION_CATEGORIES2.slice(0, 5).join(", ")}...`);
6033
- }
6034
- if (gapTypes.length > 0) {
6035
- lines.push(` \u2717 Test gap types: ${gapTypes.length} gap patterns checked`);
6036
- }
6037
- if (reviewers.length > 0) {
6038
- lines.push(` \u2192 Route to: ${reviewers.join(", ")}`);
6039
- }
6040
- lines.push("\u2500".repeat(62));
6041
- lines.push(` ${riskSummary}`);
6042
- lines.push("\u2550".repeat(62));
6043
- return {
6044
- success: true,
6045
- message: lines.join(`
6046
- `),
6047
- modules_run: modulesRun,
6048
- affected_zones: affectedZones,
6049
- recommended_reviewers: reviewers,
6050
- risk_summary: riskSummary,
6051
- risk_score: riskScore,
6052
- config,
6053
- phase: state.phase,
6054
- meta: { formatted: "table", timestamp: timestamp() }
6055
- };
6056
- }
6057
- };
6058
-
6059
- // src/commands/analysis/guarded-edit.ts
6060
- import { existsSync as existsSync51, readFileSync as readFileSync44 } from "fs";
6061
- import { join as join39 } from "path";
6062
- function loadActivePolicies(directory) {
6063
- const p = join39(codebaseDir(directory), "POLICIES.json");
6064
- if (!existsSync51(p))
6065
- return [];
6066
- try {
6067
- const store = JSON.parse(readFileSync44(p, "utf-8"));
6068
- return (store.policies ?? []).filter((pol) => pol.active);
6069
- } catch {
6070
- return [];
6071
- }
6072
- }
6073
- function loadVolatileFiles(directory) {
6074
- const p = join39(codebaseDir(directory), "VOLATILITY.json");
6075
- if (!existsSync51(p))
6076
- return new Set;
6077
- try {
6078
- const data = JSON.parse(readFileSync44(p, "utf-8"));
6079
- return new Set((data.entries ?? []).filter((e) => e.stability === "volatile" || e.stability === "critical").map((e) => e.path));
6080
- } catch {
6081
- return new Set;
6082
- }
6083
- }
6084
- function loadPriorFailurePaths(directory) {
6085
- const p = join39(codebaseDir(directory), "FAILURES.json");
6086
- if (!existsSync51(p))
6087
- return new Map;
6088
- try {
6089
- const data = JSON.parse(readFileSync44(p, "utf-8"));
6090
- const result = new Map;
6091
- for (const entry of data.entries ?? []) {
6092
- if (entry.tags?.includes("resolved"))
6093
- continue;
6094
- for (const path2 of entry.affected_paths ?? []) {
6095
- const existing = result.get(path2) ?? [];
6096
- result.set(path2, [...existing, entry.id]);
6097
- }
6098
- }
6099
- return result;
6100
- } catch {
6101
- return new Map;
6102
- }
6103
- }
6104
- function decideGate(trustScore, execMode, policyViolations, archConstrained, isVolatile, hasPriorFailures) {
6105
- if (archConstrained) {
6106
- return { decision: "block", reason: "Architectural constraint violation \u2014 path is forbidden by CONSTRAINTS.md" };
6107
- }
6108
- if (policyViolations.length > 0 && trustScore < 30) {
6109
- return { decision: "block", reason: `Policy violation + low trust score (${trustScore}): ${policyViolations[0]}` };
6110
- }
6111
- if (execMode === "review-only") {
6112
- return { decision: "require-review", reason: "Repository is in review-only execution mode" };
6113
- }
6114
- if (trustScore < 40 || policyViolations.length > 0) {
6115
- return { decision: "require-review", reason: `High risk: trust score ${trustScore}/100, ${policyViolations.length} policy violation(s)` };
6116
- }
6117
- if (execMode === "guarded" || isVolatile || hasPriorFailures) {
6118
- const signals = [];
6119
- if (execMode === "guarded")
6120
- signals.push("guarded execution mode");
6121
- if (isVolatile)
6122
- signals.push("volatile file");
6123
- if (hasPriorFailures)
6124
- signals.push("prior failures on this path");
6125
- return { decision: "require-confirmation", reason: `Moderate risk: ${signals.join(", ")}` };
6126
- }
6127
- return { decision: "auto-approve", reason: `Trust score ${trustScore}/100, no policy violations, stable file` };
6128
- }
6129
- var guardedEditCommand = {
6130
- name: "fd-guarded-edit",
6131
- description: "Edit gate \u2014 decides auto-approve / require-confirmation / require-review / block based on policy, trust score, volatility, and arch constraints",
6132
- async execute(context, args) {
6133
- const dir = context.directory ?? process.cwd();
6134
- if (!args?.file && !args?.change) {
6135
- return {
6136
- error: "Provide --file and/or --change to evaluate. Example: /fd-guarded-edit --file src/auth.ts --change 'update JWT expiry'",
6137
- code: "NO_INPUT",
6138
- hint: "Both --file and --change are optional, but at least one is needed for useful analysis"
6139
- };
6140
- }
6141
- const filePath = args?.file ?? "";
6142
- const change = args?.change ?? "";
6143
- const isDryRun = args?.["dry-run"] ?? false;
6144
- const trustScore = filePath ? scorePatch(dir, filePath, change || undefined) : { score: 70, verdict: "safe", signals: [] };
6145
- const archConstraint = filePath ? checkArchConstraint(dir, filePath) !== null : false;
6146
- const configPath2 = join39(planningDir(dir), "config.json");
6147
- const execMode = resolveExecutionMode(configPath2, trustScore.score);
6148
- const policies = loadActivePolicies(dir);
6149
- const policyViolations = policies.filter((p) => {
6150
- const combined = [filePath, change].join(" ").toLowerCase();
6151
- return combined.includes(p.trigger.toLowerCase());
6152
- }).map((p) => p.rule);
6153
- const volatileFiles = loadVolatileFiles(dir);
6154
- const priorFailureMap = loadPriorFailurePaths(dir);
6155
- const isVolatile = filePath ? Array.from(volatileFiles).some((vf) => filePath.includes(vf) || vf.includes(filePath)) : false;
6156
- const priorFailureIds = filePath ? Array.from(priorFailureMap.entries()).filter(([path2]) => filePath.includes(path2) || path2.includes(filePath)).flatMap(([, ids]) => ids) : [];
6157
- const { decision, reason } = decideGate(trustScore.score, execMode, policyViolations, archConstraint, isVolatile, priorFailureIds.length > 0);
6158
- const recommendedAction = {
6159
- "auto-approve": "Apply the change \u2014 no action needed",
6160
- "require-confirmation": "Review the diff carefully, then confirm to proceed",
6161
- "require-review": "Route to human reviewer before applying \u2014 do not auto-apply",
6162
- block: "Do NOT apply this change \u2014 resolve the violation first"
6163
- };
6164
- const result = {
6165
- decision,
6166
- reason,
6167
- risk_score: trustScore.score,
6168
- execution_mode: execMode,
6169
- policy_violations: policyViolations,
6170
- volatile_files: isVolatile ? [filePath] : [],
6171
- prior_failures: priorFailureIds,
6172
- arch_constraint: archConstraint,
6173
- recommended_action: recommendedAction[decision]
6174
- };
6175
- if (args?.json) {
6176
- return { success: true, data: result, meta: { formatted: "json", timestamp: timestamp() } };
6177
- }
6178
- const decisionIcon = {
6179
- "auto-approve": "\u2713",
6180
- "require-confirmation": "\u26A0",
6181
- "require-review": "\u2691",
6182
- block: "\u2717"
6183
- };
6184
- const lines = [
6185
- "\u2550".repeat(60),
6186
- `fd-guarded-edit${isDryRun ? " (dry-run)" : ""}`,
6187
- "\u2500".repeat(60),
6188
- ` File: ${filePath || "(not specified)"}`,
6189
- ` Change: ${change || "(not specified)"}`,
6190
- "\u2500".repeat(60),
6191
- ` ${decisionIcon[decision]} Decision: ${decision.toUpperCase()}`,
6192
- ` Reason: ${reason}`,
6193
- ` Risk score: ${trustScore.score}/100 (${trustScore.verdict})`,
6194
- ` Exec mode: ${execMode}`
6195
- ];
6196
- if (policyViolations.length > 0) {
6197
- lines.push(` Policies: ${policyViolations.join("; ").substring(0, 60)}`);
6198
- }
6199
- if (priorFailureIds.length > 0) {
6200
- lines.push(` Prior fails: ${priorFailureIds.join(", ")}`);
6201
- }
6202
- lines.push("\u2500".repeat(60));
6203
- lines.push(` \u2192 ${recommendedAction[decision]}`);
6204
- lines.push("\u2550".repeat(60));
6205
- return {
6206
- success: true,
6207
- message: lines.join(`
6208
- `),
6209
- ...result,
6210
- meta: { formatted: "table", timestamp: timestamp() }
6211
- };
6212
- }
6213
- };
6214
-
6215
- // src/commands/analysis/evaluate-risk.ts
6216
- import { existsSync as existsSync52, readFileSync as readFileSync45 } from "fs";
6217
- import { join as join40 } from "path";
6218
- var REGRESSION_CATEGORIES3 = [
6219
- "performance",
6220
- "auth",
6221
- "schema",
6222
- "ui-state",
6223
- "async-flow",
6224
- "api-contract",
6225
- "data-integrity",
6226
- "security",
6227
- "config",
6228
- "i18n"
6229
- ];
6230
- var CATEGORY_KEYWORDS = {
6231
- performance: ["slow", "latency", "cache", "query", "index", "bulk", "batch", "load"],
6232
- auth: ["auth", "token", "session", "jwt", "oauth", "permission", "rbac", "login"],
6233
- schema: ["schema", "migration", "column", "table", "foreign key", "constraint", "index"],
6234
- "ui-state": ["state", "redux", "context", "store", "hook", "render", "component"],
6235
- "async-flow": ["async", "await", "promise", "callback", "event", "queue", "worker"],
6236
- "api-contract": ["api", "endpoint", "route", "request", "response", "payload", "version"],
6237
- "data-integrity": ["transaction", "rollback", "constraint", "unique", "required", "nullable"],
6238
- security: ["secret", "password", "encrypt", "decrypt", "hash", "sanitize", "inject"],
6239
- config: ["env", "config", "setting", "flag", "feature flag", "toggle", "env var"],
6240
- i18n: ["locale", "translation", "i18n", "l10n", "format", "timezone", "language"]
6241
- };
6242
- function predictRegressions(changeText) {
6243
- const lower = changeText.toLowerCase();
6244
- return REGRESSION_CATEGORIES3.filter((cat) => (CATEGORY_KEYWORDS[cat] ?? []).some((kw) => lower.includes(kw)));
6245
- }
6246
- function toRiskLevel(score) {
6247
- if (score >= 80)
6248
- return "low";
6249
- if (score >= 50)
6250
- return "medium";
6251
- if (score >= 25)
6252
- return "high";
6253
- return "critical";
6254
- }
6255
- function computeConfidence(directory) {
6256
- const cd = codebaseDir(directory);
6257
- let score = 20;
6258
- if (existsSync52(join40(cd, "ARCHITECTURE.md")))
6259
- score += 20;
6260
- if (existsSync52(join40(cd, "STACK.md")))
6261
- score += 10;
6262
- if (existsSync52(join40(cd, "MEMORY.json"))) {
6263
- try {
6264
- const nodes = Object.keys(JSON.parse(readFileSync45(join40(cd, "MEMORY.json"), "utf-8")).nodes ?? {}).length;
6265
- score += Math.min(25, nodes * 2);
6266
- } catch {}
6267
- }
6268
- if (existsSync52(join40(cd, "VOLATILITY.json"))) {
6269
- try {
6270
- const entries = JSON.parse(readFileSync45(join40(cd, "VOLATILITY.json"), "utf-8")).entries?.length ?? 0;
6271
- score += Math.min(15, entries);
6272
- } catch {}
6273
- }
6274
- if (existsSync52(join40(cd, "FAILURES.json"))) {
6275
- try {
6276
- const entries = JSON.parse(readFileSync45(join40(cd, "FAILURES.json"), "utf-8")).entries?.length ?? 0;
6277
- score += Math.min(10, entries);
6278
- } catch {}
6279
- }
6280
- return Math.min(100, score);
6281
- }
6282
- function saferAlternative(riskLevel, change) {
6283
- if (riskLevel === "low" || riskLevel === "medium")
6284
- return null;
6285
- const lower = change.toLowerCase();
6286
- if (lower.includes("auth") || lower.includes("jwt")) {
6287
- return "Consider a feature-flag rollout or shadow mode before swapping auth tokens";
6288
- }
6289
- if (lower.includes("schema") || lower.includes("migration")) {
6290
- return "Consider a backward-compatible migration (add column, backfill, then drop old) to avoid data loss";
6291
- }
6292
- if (lower.includes("api") || lower.includes("endpoint")) {
6293
- return "Consider versioning the endpoint (/v2) and deprecating the old one with a sunset header";
6294
- }
6295
- if (lower.includes("payment") || lower.includes("billing")) {
6296
- return "Consider a canary deployment limited to internal users before full rollout";
6297
- }
6298
- return "Consider breaking the change into smaller, independently testable steps";
6299
- }
6300
- var evaluateRiskCommand = {
6301
- name: "fd-evaluate-risk",
6302
- description: "Risk assessment \u2014 estimates change risk, confidence, likely regressions, and whether approval is needed before proceeding",
6303
- async execute(context, args) {
6304
- const dir = context.directory ?? process.cwd();
6305
- const sp = statePath(dir);
6306
- if (!existsSync52(sp)) {
6307
- return { error: "STATE.md not found. Run /fd-new-project first.", code: "NOT_INITIALIZED" };
6308
- }
6309
- if (!args?.change && !args?.file) {
6310
- return {
6311
- error: "Provide --change and/or --file. Example: /fd-evaluate-risk --change 'refactor auth middleware'",
6312
- code: "NO_INPUT"
6313
- };
6314
- }
6315
- const change = args?.change ?? "";
6316
- const filePath = args?.file ?? "";
6317
- const state = readPlanningState(dir);
6318
- let riskScore;
6319
- let trustSignals = [];
6320
- if (filePath) {
6321
- const ts = scorePatch(dir, filePath, change || undefined);
6322
- riskScore = ts.score;
6323
- trustSignals = ts.signals;
6324
- } else {
6325
- const radar = runImpactRadar(dir, change);
6326
- const baseDeduction = radar.hotspots.length * 12 + radar.known_failures.length * 10;
6327
- riskScore = Math.max(0, 100 - baseDeduction);
6328
- if (radar.hotspots.length > 0)
6329
- trustSignals.push(`${radar.hotspots.length} volatile zone(s)`);
6330
- if (radar.known_failures.length > 0)
6331
- trustSignals.push(`${radar.known_failures.length} known failure(s)`);
6332
- }
6333
- const riskLevel = toRiskLevel(riskScore);
6334
- const confidence = computeConfidence(dir);
6335
- const likelyRegressions = change ? predictRegressions(change) : [];
6336
- const approvalNeeded = riskScore < 60 || likelyRegressions.length >= 3;
6337
- let volatileZoneCount = 0;
6338
- let volatileZones = [];
6339
- if (args?.volatility !== false) {
6340
- const volPath = join40(codebaseDir(dir), "VOLATILITY.json");
6341
- if (existsSync52(volPath)) {
6342
- try {
6343
- const v = JSON.parse(readFileSync45(volPath, "utf-8"));
6344
- const zones = (v.entries ?? []).filter((e) => e.stability === "volatile" || e.stability === "critical");
6345
- volatileZoneCount = zones.length;
6346
- if (change) {
6347
- const words = change.toLowerCase().split(/\s+/).filter((w) => w.length > 3);
6348
- volatileZones = zones.filter((e) => words.some((w) => e.path.toLowerCase().includes(w))).map((e) => e.path).slice(0, 5);
6349
- }
6350
- } catch {}
6351
- }
6352
- }
6353
- const saferAlt = saferAlternative(riskLevel, change);
6354
- const agents = [
6355
- { name: "researcher", role: "map change description to affected paths and modules" },
6356
- { name: "reviewer", role: "validate risk level and regression predictions" },
6357
- riskLevel === "high" || riskLevel === "critical" ? { name: "security-auditor", role: "perform targeted security review of high-risk areas" } : null
6358
- ].filter(Boolean);
6359
- const result = {
6360
- risk_score: riskScore,
6361
- risk_level: riskLevel,
6362
- confidence,
6363
- approval_needed: approvalNeeded,
6364
- likely_regressions: likelyRegressions,
6365
- volatile_zones: volatileZoneCount,
6366
- volatile_matches: volatileZones,
6367
- safer_alternative: saferAlt,
6368
- trust_signals: trustSignals
6369
- };
6370
- if (args?.json) {
6371
- return {
6372
- success: true,
6373
- data: { ...result, agents, phase: state.phase },
6374
- meta: { formatted: "json", timestamp: timestamp() }
6375
- };
6376
- }
6377
- const riskIcon = { low: "\u2713", medium: "\u26A1", high: "\u26A0", critical: "\u2717" };
6378
- const lines = [
6379
- "\u2550".repeat(60),
6380
- "fd-evaluate-risk",
6381
- "\u2500".repeat(60),
6382
- ` Change: ${change || "(not specified)"}`,
6383
- ` File: ${filePath || "(not specified)"}`,
6384
- "\u2500".repeat(60),
6385
- ` ${riskIcon[riskLevel]} Risk level: ${riskLevel.toUpperCase()} (score: ${riskScore}/100)`,
6386
- ` Confidence: ${confidence}/100 (codebase context coverage)`,
6387
- ` Approval: ${approvalNeeded ? "REQUIRED" : "not required"}`
6388
- ];
6389
- if (likelyRegressions.length > 0) {
6390
- lines.push(` Regressions: ${likelyRegressions.join(", ")}`);
6391
- }
6392
- if (volatileZones.length > 0) {
6393
- lines.push(` Hot zones: ${volatileZones.join(", ")}`);
6394
- }
6395
- if (trustSignals.length > 0) {
6396
- lines.push(` Signals: ${trustSignals.join(", ")}`);
6397
- }
6398
- if (saferAlt) {
6399
- lines.push("\u2500".repeat(60));
6400
- lines.push(` Safer alt: ${saferAlt}`);
6401
- }
6402
- lines.push("\u2500".repeat(60));
6403
- lines.push(` researcher \u2192 map to affected paths`);
6404
- lines.push(` reviewer \u2192 validate risk + regressions`);
6405
- if (riskLevel === "high" || riskLevel === "critical") {
6406
- lines.push(` security \u2192 targeted review of high-risk areas`);
6407
- }
6408
- lines.push("\u2550".repeat(60));
6409
- return {
6410
- success: true,
6411
- message: lines.join(`
6412
- `),
6413
- ...result,
6414
- agents,
6415
- phase: state.phase,
6416
- meta: { formatted: "table", timestamp: timestamp() }
6417
- };
6418
- }
6419
- };
6420
-
6421
- // src/commands/governance/approve.ts
6422
- import { existsSync as existsSync53 } from "fs";
6423
- var approveCommand = {
6424
- name: "fd-approve",
6425
- description: "Manage approval requests \u2014 list pending approvals, approve or reject a request by ID",
6426
- async execute(context, args) {
6427
- const dir = context.directory ?? process.cwd();
6428
- if (!existsSync53(statePath(dir))) {
6429
- return {
6430
- error: "STATE.md not found. Run /fd-new-project first.",
6431
- code: "NOT_INITIALIZED"
6432
- };
6433
- }
6434
- if (!args?.id || args?.list) {
6435
- const pending = getPendingApprovals(dir);
6436
- const recent = args?.recent ? getRecentApprovals(dir, 10) : [];
6437
- if (args?.json) {
6438
- return { success: true, data: { pending, recent }, meta: { formatted: "json", timestamp: timestamp() } };
6439
- }
6440
- if (pending.length === 0) {
6441
- return {
6442
- success: true,
6443
- message: ["\u2500".repeat(55), " No pending approvals", "\u2550".repeat(55)].join(`
6444
- `),
6445
- pending: [],
6446
- meta: { formatted: "table", timestamp: timestamp() }
6447
- };
6448
- }
6449
- const lines2 = [
6450
- "\u2500".repeat(55),
6451
- ` Pending Approvals (${pending.length})`,
6452
- "\u2500".repeat(55),
6453
- ...pending.map((a, i) => [
6454
- ` [${i + 1}] ID: ${a.id.slice(0, 8)}...`,
6455
- ` trigger: ${a.trigger}`,
6456
- ` reason: ${a.reason}`,
6457
- ...a.file_path ? [` file: ${a.file_path}`] : [],
6458
- ` risk: ${a.risk_score} | requested: ${a.requested_at.slice(0, 16).replace("T", " ")}`
6459
- ]).flat(),
6460
- "\u2500".repeat(55),
6461
- ` To approve: /fd-approve --id <full-id>`,
6462
- ` To reject: /fd-approve --id <full-id> --reject`,
6463
- "\u2550".repeat(55)
6464
- ];
6465
- return {
6466
- success: true,
6467
- message: lines2.join(`
6468
- `),
6469
- pending,
6470
- meta: { formatted: "table", timestamp: timestamp() }
6471
- };
6472
- }
6473
- const decision = args.reject ? "rejected" : "approved";
6474
- const ok = resolveApproval(dir, args.id, decision);
6475
- if (!ok) {
6476
- return {
6477
- success: false,
6478
- error: `Approval request not found: ${args.id}`,
6479
- hint: "Run /fd-approve to list pending approval IDs",
6480
- code: "NOT_FOUND"
6481
- };
6482
- }
6483
- appendEvent(dir, {
6484
- session_id: process.env.OPENCODE_SESSION_ID ?? "session-0",
6485
- run_id: process.env.OPENCODE_RUN_ID ?? "run-0",
6486
- event: "approval.resolve",
6487
- status: decision === "approved" ? "approved" : "rejected",
6488
- meta: { approval_id: args.id, decision }
6489
- });
6490
- const icon = decision === "approved" ? "\u2713" : "\u2717";
6491
- const lines = [
6492
- "\u2500".repeat(55),
6493
- ` ${icon} Approval ${decision}: ${args.id.slice(0, 8)}...`,
6494
- decision === "approved" ? " Operation will proceed on next attempt." : " Operation has been blocked.",
6495
- "\u2550".repeat(55)
6496
- ];
6497
- return {
6498
- success: true,
6499
- message: lines.join(`
6500
- `),
6501
- approval_id: args.id,
6502
- decision,
6503
- meta: { formatted: "table", timestamp: timestamp() }
6504
- };
6505
- }
6506
- };
6507
-
6508
- // src/index.ts
6509
- function parseArgs(rawArgs) {
6510
- if (!rawArgs || rawArgs.trim() === "")
6511
- return {};
6512
- try {
6513
- return JSON.parse(rawArgs);
6514
- } catch (err) {
6515
- console.warn(`[flowdeck] Failed to parse command arguments as JSON: ${err instanceof Error ? err.message : String(err)}`);
6516
- return { input: rawArgs };
6517
- }
6518
- }
6519
- var server = async (input, _options) => {
6520
- const { directory, client, worktree } = input;
6521
- const runParallelTool = createRunParallelTool(client);
6522
- const runPipelineTool = createRunPipelineTool(client);
6523
- const delegateTool = createDelegateTool(client);
6524
- const councilTool = createCouncilTool(client);
6525
- const fileTracker = new SessionFileTracker;
6526
- const { fileEdited, fileWatcherUpdated } = createFileTrackerHooks(fileTracker);
6527
- const contextMonitor = createContextWindowMonitorHook();
6528
- const shellEnvHook = createShellEnvHook({ directory, worktree });
6529
- const todoHook = createTodoHook(client);
6530
- const sessionIdleHook = createSessionIdleHook(client, fileTracker);
6531
- const compactionHook = createCompactionHook({ directory }, fileTracker);
6532
- const allCommands = [
6533
- newProjectCommand,
6534
- mapCodebaseCommand,
6535
- settingsCommand,
6536
- doctorCommand,
6537
- discussCommand,
6538
- planCommand,
6539
- roadmapCommand,
6540
- dashboardCommand,
6541
- askCommand,
6542
- newFeatureCommand,
6543
- fixBugCommand,
6544
- reviewCodeCommand,
6545
- writeDocsCommand,
6546
- deployCheckCommand,
6547
- progressCommand,
6548
- resumeCommand,
6549
- checkpointCommand,
6550
- ...workspaceCommands,
6551
- multiRepoCommand,
6552
- impactRadarCommand,
6553
- blastRadiusCommand,
6554
- translateIntentCommand,
6555
- volatilityMapCommand,
6556
- regressionPredictCommand,
6557
- testGapCommand,
6558
- reviewRouteCommand,
6559
- analyzeChangeCommand,
6560
- guardedEditCommand,
6561
- evaluateRiskCommand,
6562
- approveCommand
6563
- ];
6564
- const commandMap = {};
6565
- for (const cmd of allCommands) {
6566
- commandMap[cmd.name] = cmd;
6567
- }
2386
+ // src/index.ts
2387
+ var server = async (input, _options) => {
2388
+ const { directory, client, worktree } = input;
2389
+ const runParallelTool = createRunParallelTool(client);
2390
+ const runPipelineTool = createRunPipelineTool(client);
2391
+ const delegateTool = createDelegateTool(client);
2392
+ const councilTool = createCouncilTool(client);
2393
+ const fileTracker = new SessionFileTracker;
2394
+ const { fileEdited, fileWatcherUpdated } = createFileTrackerHooks(fileTracker);
2395
+ const contextMonitor = createContextWindowMonitorHook();
2396
+ const shellEnvHook = createShellEnvHook({ directory, worktree });
2397
+ const todoHook = createTodoHook(client);
2398
+ const sessionIdleHook = createSessionIdleHook(client, fileTracker);
2399
+ const compactionHook = createCompactionHook({ directory }, fileTracker);
6568
2400
  return {
6569
2401
  mcp: createFlowDeckMcps(),
6570
2402
  tool: {
@@ -6601,21 +2433,6 @@ var server = async (input, _options) => {
6601
2433
  await sessionIdleHook();
6602
2434
  }
6603
2435
  },
6604
- "command.execute.before": async (cmdInput, output) => {
6605
- const handler = commandMap[cmdInput.command];
6606
- if (!handler)
6607
- return;
6608
- try {
6609
- const args = parseArgs(cmdInput.arguments);
6610
- const result = await handler.execute({ directory }, args);
6611
- const text = typeof result === "string" ? result : JSON.stringify(result, null, 2);
6612
- output.parts.push({ type: "text", text });
6613
- notifyCommandInteraction(cmdInput.command);
6614
- } catch (err) {
6615
- const msg = err instanceof Error ? err.message : String(err);
6616
- output.parts.push({ type: "text", text: `FlowDeck error: ${msg}` });
6617
- }
6618
- },
6619
2436
  "tool.execute.before": async (toolInput, toolOutput) => {
6620
2437
  await telemetryHook({ directory }, toolInput, toolOutput);
6621
2438
  await approvalHook({ directory }, toolInput, toolOutput);