@dv.nghiem/flowdeck 0.3.4 → 0.3.6

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 (103) hide show
  1. package/README.md +154 -3
  2. package/dist/agents/coder.d.ts +3 -1
  3. package/dist/agents/coder.d.ts.map +1 -1
  4. package/dist/agents/design.d.ts +3 -0
  5. package/dist/agents/design.d.ts.map +1 -0
  6. package/dist/agents/index.d.ts +4 -3
  7. package/dist/agents/index.d.ts.map +1 -1
  8. package/dist/agents/orchestrator.d.ts.map +1 -1
  9. package/dist/agents/reviewer.d.ts.map +1 -1
  10. package/dist/agents/specialist.d.ts +0 -1
  11. package/dist/agents/specialist.d.ts.map +1 -1
  12. package/dist/config/index.d.ts +1 -1
  13. package/dist/config/index.d.ts.map +1 -1
  14. package/dist/config/loader.d.ts +8 -0
  15. package/dist/config/loader.d.ts.map +1 -1
  16. package/dist/config/schema.d.ts +55 -2
  17. package/dist/config/schema.d.ts.map +1 -1
  18. package/dist/dashboard/server.mjs +24 -1
  19. package/dist/dashboard/types.d.ts +72 -0
  20. package/dist/dashboard/types.d.ts.map +1 -1
  21. package/dist/hooks/guard-rails.d.ts.map +1 -1
  22. package/dist/hooks/memory-hook.d.ts +7 -0
  23. package/dist/hooks/memory-hook.d.ts.map +1 -1
  24. package/dist/hooks/orchestrator-guard-hook.d.ts.map +1 -1
  25. package/dist/hooks/tool-guard.d.ts.map +1 -1
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +749 -503
  28. package/dist/services/agent-contract-registry.d.ts +32 -0
  29. package/dist/services/agent-contract-registry.d.ts.map +1 -0
  30. package/dist/services/agent-performance.d.ts +1 -1
  31. package/dist/services/agent-performance.d.ts.map +1 -1
  32. package/dist/services/agent-trace-graph.d.ts +94 -0
  33. package/dist/services/agent-trace-graph.d.ts.map +1 -0
  34. package/dist/services/agent-validator.d.ts +56 -0
  35. package/dist/services/agent-validator.d.ts.map +1 -0
  36. package/dist/services/deadlock-detector.d.ts +34 -0
  37. package/dist/services/deadlock-detector.d.ts.map +1 -0
  38. package/dist/services/delegation-budget.d.ts +54 -0
  39. package/dist/services/delegation-budget.d.ts.map +1 -0
  40. package/dist/services/governance.test.d.ts +11 -0
  41. package/dist/services/governance.test.d.ts.map +1 -0
  42. package/dist/services/index.d.ts +6 -1
  43. package/dist/services/index.d.ts.map +1 -1
  44. package/dist/services/memory-store.d.ts +34 -1
  45. package/dist/services/memory-store.d.ts.map +1 -1
  46. package/dist/services/memory-store.test.d.ts +2 -0
  47. package/dist/services/memory-store.test.d.ts.map +1 -0
  48. package/dist/services/telemetry.d.ts +1 -1
  49. package/dist/services/telemetry.d.ts.map +1 -1
  50. package/dist/services/workflow-scorecard.d.ts +76 -0
  51. package/dist/services/workflow-scorecard.d.ts.map +1 -0
  52. package/dist/tools/delegate.d.ts.map +1 -1
  53. package/dist/tools/dispatch-routing.d.ts +4 -1
  54. package/dist/tools/dispatch-routing.d.ts.map +1 -1
  55. package/dist/tools/dispatch-routing.test.d.ts +2 -0
  56. package/dist/tools/dispatch-routing.test.d.ts.map +1 -0
  57. package/dist/tools/memory-search.d.ts.map +1 -1
  58. package/dist/tools/memory-status.d.ts.map +1 -1
  59. package/dist/tools/planning-state-lib.d.ts +8 -0
  60. package/dist/tools/planning-state-lib.d.ts.map +1 -1
  61. package/dist/tools/planning-state.d.ts.map +1 -1
  62. package/dist/tools/run-pipeline.d.ts.map +1 -1
  63. package/docs/agents.md +104 -74
  64. package/docs/best-practices.md +1 -1
  65. package/docs/commands/fd-ask.md +2 -2
  66. package/docs/commands/fd-fix-bug.md +2 -2
  67. package/docs/commands/fd-new-feature.md +2 -2
  68. package/docs/commands/fd-quick.md +3 -1
  69. package/docs/commands.md +37 -7
  70. package/docs/configuration.md +76 -46
  71. package/docs/design-first-workflow.md +94 -0
  72. package/docs/feature-integration-architecture.md +3 -31
  73. package/docs/index.md +5 -2
  74. package/docs/intelligence.md +92 -1
  75. package/docs/multi-repo.md +1 -1
  76. package/docs/rules.md +1 -1
  77. package/docs/skills.md +24 -15
  78. package/docs/workflows.md +11 -6
  79. package/package.json +1 -1
  80. package/src/commands/fd-ask.md +1 -0
  81. package/src/commands/fd-design.md +64 -0
  82. package/src/commands/fd-discuss.md +2 -0
  83. package/src/commands/fd-execute.md +7 -3
  84. package/src/commands/fd-fix-bug.md +2 -2
  85. package/src/commands/fd-multi-repo.md +3 -3
  86. package/src/commands/fd-plan.md +2 -0
  87. package/src/commands/fd-quick.md +4 -1
  88. package/src/commands/fd-verify.md +6 -0
  89. package/src/rules/common/agent-orchestration.md +6 -6
  90. package/src/skills/app-shell-design/SKILL.md +31 -0
  91. package/src/skills/dashboard-design/SKILL.md +32 -0
  92. package/src/skills/decision-trace/SKILL.md +1 -1
  93. package/src/skills/design-audit/SKILL.md +37 -0
  94. package/src/skills/design-system-definition/SKILL.md +33 -0
  95. package/src/skills/frontend-handoff/SKILL.md +31 -0
  96. package/src/skills/landing-page-design/SKILL.md +32 -0
  97. package/src/skills/multi-repo/SKILL.md +3 -3
  98. package/src/skills/plan-task/SKILL.md +2 -2
  99. package/src/skills/responsive-review/SKILL.md +31 -0
  100. package/src/skills/ui-ux-planning/SKILL.md +32 -0
  101. package/src/skills/wireframe-planning/SKILL.md +30 -0
  102. package/dist/services/model-router.d.ts +0 -35
  103. package/dist/services/model-router.d.ts.map +0 -1
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // src/index.ts
2
- import { readdirSync as readdirSync3, readFileSync as readFileSync24, existsSync as existsSync28 } from "fs";
3
- import { join as join27, basename } from "path";
2
+ import { readdirSync as readdirSync3, readFileSync as readFileSync23, existsSync as existsSync27 } from "fs";
3
+ import { join as join26, basename } from "path";
4
4
  import { dirname as dirname4 } from "path";
5
5
  import { fileURLToPath as fileURLToPath2 } from "url";
6
6
 
@@ -128,6 +128,8 @@ function parseState(content) {
128
128
  result[key] = value.replace(/[\[\]]/g, "").split(",").map((s) => s.trim()).filter(Boolean);
129
129
  } else if (key === "plan_confirmed") {
130
130
  result[key] = value === "true";
131
+ } else if (key === "requires_design_first" || key === "design_approved" || key === "design_override") {
132
+ result[key] = value === "true";
131
133
  } else if (value !== "" && !isNaN(Number(value)) && key !== "plan_file" && key !== "confirmed_at") {
132
134
  result[key] = Number(value);
133
135
  } else {
@@ -155,7 +157,21 @@ ${entry}
155
157
  function readPlanningState(dir) {
156
158
  const sp = statePath(dir);
157
159
  if (!existsSync2(sp)) {
158
- return { phase: 0, status: "", plan_confirmed: false, steps_complete: [], steps_pending: [], last_action: "", next_action: "", blockers: [], tdd: undefined };
160
+ return {
161
+ phase: 0,
162
+ status: "",
163
+ plan_confirmed: false,
164
+ requires_design_first: false,
165
+ design_stage: "pending",
166
+ design_approved: false,
167
+ design_override: false,
168
+ steps_complete: [],
169
+ steps_pending: [],
170
+ last_action: "",
171
+ next_action: "",
172
+ blockers: [],
173
+ tdd: undefined
174
+ };
159
175
  }
160
176
  const content = readFileSync2(sp, "utf-8");
161
177
  const parsed = parseState(content);
@@ -163,6 +179,13 @@ function readPlanningState(dir) {
163
179
  phase: parsed.phase || 1,
164
180
  status: parsed.status || "",
165
181
  plan_confirmed: Boolean(parsed.plan_confirmed),
182
+ task_type: parsed.task_type || undefined,
183
+ requires_design_first: Boolean(parsed.requires_design_first),
184
+ design_stage: parsed.design_stage || "pending",
185
+ design_approved: Boolean(parsed.design_approved),
186
+ design_override: Boolean(parsed.design_override),
187
+ design_override_reason: parsed.design_override_reason || undefined,
188
+ design_artifact: parsed.design_artifact || undefined,
166
189
  steps_complete: parsed.steps_complete || [],
167
190
  steps_pending: parsed.steps_pending || [],
168
191
  last_action: parsed.last_action || "",
@@ -251,6 +274,13 @@ var planningStateTool = tool2({
251
274
  plan_file: tool2.schema.string().optional(),
252
275
  plan_confirmed: tool2.schema.boolean().optional(),
253
276
  confirmed_at: tool2.schema.string().optional(),
277
+ task_type: tool2.schema.string().optional(),
278
+ requires_design_first: tool2.schema.boolean().optional(),
279
+ design_stage: tool2.schema.enum(["pending", "discovery", "ux_planning", "wireframe_layout", "visual_system_definition", "design_approval", "handoff_complete"]).optional(),
280
+ design_approved: tool2.schema.boolean().optional(),
281
+ design_override: tool2.schema.boolean().optional(),
282
+ design_override_reason: tool2.schema.string().optional(),
283
+ design_artifact: tool2.schema.string().optional(),
254
284
  steps_complete: tool2.schema.array(tool2.schema.number()).optional(),
255
285
  steps_pending: tool2.schema.array(tool2.schema.number()).optional()
256
286
  }).optional(),
@@ -273,16 +303,24 @@ var planningStateTool = tool2({
273
303
  if (!u)
274
304
  return JSON.stringify({ error: "No updates provided" });
275
305
  let content = readFileSync3(sp, "utf-8");
306
+ const upsertLine = (current, key, value) => {
307
+ const pattern = new RegExp(`^${key}:\\s*.*$`, "m");
308
+ if (pattern.test(current))
309
+ return current.replace(pattern, `${key}: ${value}`);
310
+ return `${current.trimEnd()}
311
+ ${key}: ${value}
312
+ `;
313
+ };
276
314
  if (u.phase !== undefined)
277
- content = content.replace(/^phase:\s*.*/m, `phase: ${u.phase}`);
315
+ content = upsertLine(content, "phase", `${u.phase}`);
278
316
  if (u.status !== undefined)
279
- content = content.replace(/^status:\s*.*/m, `status: ${u.status}`);
317
+ content = upsertLine(content, "status", `${u.status}`);
280
318
  if (u.last_action !== undefined) {
281
- content = content.replace(/^last_action:\s*.*/m, `last_action: "${u.last_action}"`);
319
+ content = upsertLine(content, "last_action", `"${u.last_action}"`);
282
320
  content = appendHistory(content, u.last_action);
283
321
  }
284
322
  if (u.next_action !== undefined)
285
- content = content.replace(/^next_action:\s*.*/m, `next_action: "${u.next_action}"`);
323
+ content = upsertLine(content, "next_action", `"${u.next_action}"`);
286
324
  if (u.blockers !== undefined) {
287
325
  const blockersMd = u.blockers.map((b) => `- ${b}`).join(`
288
326
  `);
@@ -291,15 +329,29 @@ ${blockersMd}
291
329
  `);
292
330
  }
293
331
  if (u.plan_file !== undefined)
294
- content = content.replace(/^plan_file:\s*.*/m, `plan_file: ${u.plan_file}`);
332
+ content = upsertLine(content, "plan_file", `${u.plan_file}`);
295
333
  if (u.plan_confirmed !== undefined)
296
- content = content.replace(/^plan_confirmed:\s*.*/m, `plan_confirmed: ${u.plan_confirmed}`);
334
+ content = upsertLine(content, "plan_confirmed", `${u.plan_confirmed}`);
297
335
  if (u.confirmed_at !== undefined)
298
- content = content.replace(/^confirmed_at:\s*.*/m, `confirmed_at: ${u.confirmed_at}`);
336
+ content = upsertLine(content, "confirmed_at", `${u.confirmed_at}`);
337
+ if (u.task_type !== undefined)
338
+ content = upsertLine(content, "task_type", `"${u.task_type}"`);
339
+ if (u.requires_design_first !== undefined)
340
+ content = upsertLine(content, "requires_design_first", `${u.requires_design_first}`);
341
+ if (u.design_stage !== undefined)
342
+ content = upsertLine(content, "design_stage", `"${u.design_stage}"`);
343
+ if (u.design_approved !== undefined)
344
+ content = upsertLine(content, "design_approved", `${u.design_approved}`);
345
+ if (u.design_override !== undefined)
346
+ content = upsertLine(content, "design_override", `${u.design_override}`);
347
+ if (u.design_override_reason !== undefined)
348
+ content = upsertLine(content, "design_override_reason", `"${u.design_override_reason}"`);
349
+ if (u.design_artifact !== undefined)
350
+ content = upsertLine(content, "design_artifact", `'${u.design_artifact.replace(/'/g, "''")}'`);
299
351
  if (u.steps_complete !== undefined)
300
- content = content.replace(/^steps_complete:\s*.*/m, `steps_complete: [${u.steps_complete.join(", ")}]`);
352
+ content = upsertLine(content, "steps_complete", `[${u.steps_complete.join(", ")}]`);
301
353
  if (u.steps_pending !== undefined)
302
- content = content.replace(/^steps_pending:\s*.*/m, `steps_pending: [${u.steps_pending.join(", ")}]`);
354
+ content = upsertLine(content, "steps_pending", `[${u.steps_pending.join(", ")}]`);
303
355
  writeFileSync3(sp, content, "utf-8");
304
356
  return JSON.stringify({ success: true, updated_at: timestamp() });
305
357
  }
@@ -567,51 +619,6 @@ function recordRun(dir, agent, model, task_type, success, duration_ms, cost = 0)
567
619
  saveStore(dir, store);
568
620
  }
569
621
 
570
- // src/services/model-router.ts
571
- import { existsSync as existsSync6, readFileSync as readFileSync6 } from "fs";
572
- import { join as join6 } from "path";
573
- var DEFAULT_ROUTING = {
574
- planning: { primary: "claude-sonnet-4-5", temperature: 0.3, reasoning_effort: "medium" },
575
- implementation: { primary: "claude-opus-4-5", fallback: "claude-sonnet-4-5", high_risk_override: "claude-opus-4-5", temperature: 0.2, reasoning_effort: "high" },
576
- debugging: { primary: "claude-sonnet-4-5", high_risk_override: "claude-opus-4-5", temperature: 0.2, reasoning_effort: "high" },
577
- review: { primary: "gemini-2.5-flash", fallback: "claude-haiku-4-5", temperature: 0.1, reasoning_effort: "medium" },
578
- testing: { primary: "claude-haiku-4-5", fallback: "gemini-2.5-flash", temperature: 0.1, reasoning_effort: "low" },
579
- documentation: { primary: "claude-sonnet-4-5", fallback: "gemini-2.5-flash", temperature: 0.3, reasoning_effort: "low" },
580
- analysis: { primary: "claude-sonnet-4-5", temperature: 0.3, reasoning_effort: "medium" },
581
- security: { primary: "claude-opus-4-5", high_risk_override: "claude-opus-4-5", temperature: 0.1, reasoning_effort: "high" },
582
- orchestration: { primary: "claude-sonnet-4-5", temperature: 0.3, reasoning_effort: "medium" }
583
- };
584
- function getRouterConfig(dir) {
585
- const p = join6(codebaseDir(dir), "MODEL_ROUTER.json");
586
- if (!existsSync6(p))
587
- return DEFAULT_ROUTING;
588
- try {
589
- const overrides = JSON.parse(readFileSync6(p, "utf-8"));
590
- return { ...DEFAULT_ROUTING, ...overrides };
591
- } catch {
592
- return DEFAULT_ROUTING;
593
- }
594
- }
595
- function routeModel(dir, task_type, risk_score = 100) {
596
- const config = getRouterConfig(dir);
597
- const route = config[task_type] ?? DEFAULT_ROUTING.implementation;
598
- const is_high_risk = risk_score < 40;
599
- let model = route.primary;
600
- let is_override = false;
601
- if (is_high_risk && route.high_risk_override) {
602
- model = route.high_risk_override;
603
- is_override = true;
604
- }
605
- return {
606
- model,
607
- temperature: route.temperature ?? 0.3,
608
- reasoning_effort: route.reasoning_effort,
609
- task_type,
610
- is_high_risk,
611
- is_override
612
- };
613
- }
614
-
615
622
  // src/tools/dispatch-routing.ts
616
623
  function shouldRetry(promptRes) {
617
624
  if (!promptRes)
@@ -634,6 +641,8 @@ function normalizeTaskType(taskType, agent) {
634
641
  if (isTaskType(normalized))
635
642
  return normalized;
636
643
  const a = agent.toLowerCase();
644
+ if (a.includes("design") || a.includes("ui-ux"))
645
+ return "design";
637
646
  if (a.includes("review"))
638
647
  return "review";
639
648
  if (a.includes("test"))
@@ -653,7 +662,48 @@ function normalizeTaskType(taskType, agent) {
653
662
  return "implementation";
654
663
  }
655
664
  function isTaskType(value) {
656
- return value === "planning" || value === "implementation" || value === "debugging" || value === "review" || value === "testing" || value === "documentation" || value === "analysis" || value === "security" || value === "orchestration";
665
+ return value === "planning" || value === "design" || value === "implementation" || value === "debugging" || value === "review" || value === "testing" || value === "documentation" || value === "analysis" || value === "security" || value === "orchestration";
666
+ }
667
+ var UI_HEAVY_KEYWORDS = [
668
+ "landing page",
669
+ "marketing site",
670
+ "website",
671
+ "web app",
672
+ "mobile app",
673
+ "app screen",
674
+ "dashboard",
675
+ "admin panel",
676
+ "settings page",
677
+ "onboarding ux",
678
+ "kanban",
679
+ "design system",
680
+ "responsive",
681
+ "ui",
682
+ "ux",
683
+ "cta",
684
+ "conversion flow",
685
+ "saas interface",
686
+ "user-facing"
687
+ ];
688
+ var NON_UI_KEYWORDS = [
689
+ "backend",
690
+ "infrastructure",
691
+ "migration",
692
+ "pipeline",
693
+ "api only",
694
+ "database only",
695
+ "cli",
696
+ "worker"
697
+ ];
698
+ function isUiHeavyTask(input) {
699
+ const normalized = input.trim().toLowerCase();
700
+ if (!normalized)
701
+ return false;
702
+ const hasUiSignal = UI_HEAVY_KEYWORDS.some((keyword) => normalized.includes(keyword));
703
+ if (!hasUiSignal)
704
+ return false;
705
+ const hasOnlyNonUiSignals = NON_UI_KEYWORDS.some((keyword) => normalized.includes(keyword)) && !normalized.includes("frontend");
706
+ return !hasOnlyNonUiSignals;
657
707
  }
658
708
 
659
709
  // src/tools/run-pipeline.ts
@@ -699,7 +749,6 @@ function createRunPipelineTool(client) {
699
749
  }
700
750
  const stepStart = Date.now();
701
751
  const taskType = normalizeTaskType(step.task_type, step.agent);
702
- const routing = routeModel(context.directory, taskType);
703
752
  const stepInput = carryContext ? `${carryContext}
704
753
 
705
754
  ---
@@ -711,7 +760,7 @@ ${step.prompt}` : step.prompt;
711
760
  });
712
761
  if (createRes.error || !createRes.data?.id) {
713
762
  const errMsg = `Failed to create session: ${createRes.error?.detail ?? "unknown"}`;
714
- trace.push({ agent: step.agent, task_type: taskType, model: routing.model, input: stepInput, output: errMsg, duration_ms: Date.now() - stepStart, success: false });
763
+ trace.push({ agent: step.agent, task_type: taskType, model: "", input: stepInput, output: errMsg, duration_ms: Date.now() - stepStart, success: false });
715
764
  aborted = true;
716
765
  break;
717
766
  }
@@ -723,7 +772,6 @@ ${step.prompt}` : step.prompt;
723
772
  path: { id: inflightChildId },
724
773
  body: {
725
774
  agent: step.agent,
726
- model: routing.model,
727
775
  parts: [{ type: "text", text: stepInput }],
728
776
  tools: { question: false }
729
777
  },
@@ -740,8 +788,8 @@ ${step.prompt}` : step.prompt;
740
788
  }
741
789
  if (!promptRes || promptRes.error) {
742
790
  const errMsg = `Prompt failed: ${promptRes?.error?.detail ?? "unknown"}`;
743
- trace.push({ agent: step.agent, session_id: createRes.data.id, task_type: taskType, model: routing.model, input: stepInput, output: `${errMsg}${retriesUsed > 0 ? ` (retries: ${retriesUsed})` : ""}`, duration_ms: Date.now() - stepStart, success: false });
744
- recordRun(context.directory, step.agent, routing.model, taskType, false, Date.now() - stepStart);
791
+ trace.push({ agent: step.agent, session_id: createRes.data.id, task_type: taskType, model: "", input: stepInput, output: `${errMsg}${retriesUsed > 0 ? ` (retries: ${retriesUsed})` : ""}`, duration_ms: Date.now() - stepStart, success: false });
792
+ recordRun(context.directory, step.agent, "", taskType, false, Date.now() - stepStart);
745
793
  if (args.abort_on_failure) {
746
794
  aborted = true;
747
795
  break;
@@ -751,8 +799,8 @@ ${step.prompt}` : step.prompt;
751
799
  const info = promptRes.data?.info;
752
800
  if (info?.error) {
753
801
  const errMsg = `Agent error: ${JSON.stringify(info.error)}`;
754
- trace.push({ agent: step.agent, session_id: createRes.data.id, task_type: taskType, model: routing.model, input: stepInput, output: `${errMsg}${retriesUsed > 0 ? ` (retries: ${retriesUsed})` : ""}`, duration_ms: Date.now() - stepStart, success: false });
755
- recordRun(context.directory, step.agent, routing.model, taskType, false, Date.now() - stepStart);
802
+ trace.push({ agent: step.agent, session_id: createRes.data.id, task_type: taskType, model: "", input: stepInput, output: `${errMsg}${retriesUsed > 0 ? ` (retries: ${retriesUsed})` : ""}`, duration_ms: Date.now() - stepStart, success: false });
803
+ recordRun(context.directory, step.agent, "", taskType, false, Date.now() - stepStart);
756
804
  if (args.abort_on_failure) {
757
805
  aborted = true;
758
806
  break;
@@ -760,8 +808,8 @@ ${step.prompt}` : step.prompt;
760
808
  continue;
761
809
  }
762
810
  const output = extractText(promptRes.data?.parts ?? []);
763
- trace.push({ agent: step.agent, session_id: createRes.data.id, task_type: taskType, model: routing.model, input: stepInput, output: output || "(no text output)", duration_ms: Date.now() - stepStart, success: true });
764
- recordRun(context.directory, step.agent, routing.model, taskType, true, Date.now() - stepStart);
811
+ trace.push({ agent: step.agent, session_id: createRes.data.id, task_type: taskType, model: "", input: stepInput, output: output || "(no text output)", duration_ms: Date.now() - stepStart, success: true });
812
+ recordRun(context.directory, step.agent, "", taskType, true, Date.now() - stepStart);
765
813
  carryContext = output;
766
814
  }
767
815
  } finally {
@@ -795,7 +843,6 @@ function createDelegateTool(client) {
795
843
  async execute(args, context) {
796
844
  const startTime = Date.now();
797
845
  const taskType = normalizeTaskType(args.task_type, args.agent);
798
- const routing = routeModel(context.directory, taskType);
799
846
  const retryAttempts = typeof args.retry_attempts === "number" ? args.retry_attempts : 1;
800
847
  const maxRetries = Math.max(0, Math.floor(retryAttempts));
801
848
  const createRes = await client.session.create({
@@ -829,7 +876,6 @@ ${args.prompt}` : args.prompt;
829
876
  path: { id: childId },
830
877
  body: {
831
878
  agent: args.agent,
832
- model: routing.model,
833
879
  parts: [{ type: "text", text: fullPrompt }],
834
880
  tools: { question: false }
835
881
  },
@@ -840,41 +886,41 @@ ${args.prompt}` : args.prompt;
840
886
  retriesUsed++;
841
887
  }
842
888
  if (!promptRes || promptRes.error) {
843
- recordRun(context.directory, args.agent, routing.model, taskType, false, Date.now() - startTime);
889
+ recordRun(context.directory, args.agent, "", taskType, false, Date.now() - startTime);
844
890
  return JSON.stringify({
845
891
  agent: args.agent,
846
892
  session_id: childId,
847
893
  success: false,
848
894
  error: `Prompt failed: ${promptRes?.error?.detail ?? "unknown"}`,
849
895
  task_type: taskType,
850
- model: routing.model,
896
+ model: "",
851
897
  retries_used: retriesUsed,
852
898
  duration_ms: Date.now() - startTime
853
899
  });
854
900
  }
855
901
  const info = promptRes.data?.info;
856
902
  if (info?.error) {
857
- recordRun(context.directory, args.agent, routing.model, taskType, false, Date.now() - startTime);
903
+ recordRun(context.directory, args.agent, "", taskType, false, Date.now() - startTime);
858
904
  return JSON.stringify({
859
905
  agent: args.agent,
860
906
  session_id: childId,
861
907
  success: false,
862
908
  error: `Agent error: ${JSON.stringify(info.error)}`,
863
909
  task_type: taskType,
864
- model: routing.model,
910
+ model: "",
865
911
  retries_used: retriesUsed,
866
912
  duration_ms: Date.now() - startTime
867
913
  });
868
914
  }
869
915
  const output = extractText2(promptRes.data?.parts ?? []);
870
- recordRun(context.directory, args.agent, routing.model, taskType, true, Date.now() - startTime);
916
+ recordRun(context.directory, args.agent, "", taskType, true, Date.now() - startTime);
871
917
  return JSON.stringify({
872
918
  agent: args.agent,
873
919
  session_id: childId,
874
920
  success: true,
875
921
  output: output || "(no text output)",
876
922
  task_type: taskType,
877
- model: routing.model,
923
+ model: "",
878
924
  retries_used: retriesUsed,
879
925
  duration_ms: Date.now() - startTime
880
926
  });
@@ -884,28 +930,28 @@ ${args.prompt}` : args.prompt;
884
930
 
885
931
  // src/tools/repo-memory.ts
886
932
  import { tool as tool6 } from "@opencode-ai/plugin";
887
- import { readFileSync as readFileSync7, writeFileSync as writeFileSync6, existsSync as existsSync7, mkdirSync as mkdirSync3 } from "fs";
888
- import { join as join7 } from "path";
933
+ import { readFileSync as readFileSync6, writeFileSync as writeFileSync6, existsSync as existsSync6, mkdirSync as mkdirSync3 } from "fs";
934
+ import { join as join6 } from "path";
889
935
  var MEMORY_FILE = "MEMORY.json";
890
936
  function memoryPath(directory) {
891
- return join7(codebaseDir(directory), MEMORY_FILE);
937
+ return join6(codebaseDir(directory), MEMORY_FILE);
892
938
  }
893
939
  function emptyMemory() {
894
940
  return { version: "1.0", last_updated: new Date().toISOString(), nodes: {} };
895
941
  }
896
942
  function readMemory(directory) {
897
943
  const p = memoryPath(directory);
898
- if (!existsSync7(p))
944
+ if (!existsSync6(p))
899
945
  return emptyMemory();
900
946
  try {
901
- return JSON.parse(readFileSync7(p, "utf-8"));
947
+ return JSON.parse(readFileSync6(p, "utf-8"));
902
948
  } catch {
903
949
  return emptyMemory();
904
950
  }
905
951
  }
906
952
  function writeMemory(directory, memory) {
907
953
  const base = codebaseDir(directory);
908
- if (!existsSync7(base))
954
+ if (!existsSync6(base))
909
955
  mkdirSync3(base, { recursive: true });
910
956
  memory.last_updated = new Date().toISOString();
911
957
  writeFileSync6(memoryPath(directory), JSON.stringify(memory, null, 2), "utf-8");
@@ -985,25 +1031,25 @@ var repoMemoryTool = tool6({
985
1031
 
986
1032
  // src/tools/failure-replay.ts
987
1033
  import { tool as tool7 } from "@opencode-ai/plugin";
988
- import { readFileSync as readFileSync8, writeFileSync as writeFileSync7, existsSync as existsSync8, mkdirSync as mkdirSync4 } from "fs";
989
- import { join as join8 } from "path";
1034
+ import { readFileSync as readFileSync7, writeFileSync as writeFileSync7, existsSync as existsSync7, mkdirSync as mkdirSync4 } from "fs";
1035
+ import { join as join7 } from "path";
990
1036
  var FAILURES_FILE = "FAILURES.json";
991
1037
  function failuresPath(directory) {
992
- return join8(codebaseDir(directory), FAILURES_FILE);
1038
+ return join7(codebaseDir(directory), FAILURES_FILE);
993
1039
  }
994
1040
  function readStore(directory) {
995
1041
  const p = failuresPath(directory);
996
- if (!existsSync8(p))
1042
+ if (!existsSync7(p))
997
1043
  return { version: "1.0", last_updated: new Date().toISOString(), entries: [] };
998
1044
  try {
999
- return JSON.parse(readFileSync8(p, "utf-8"));
1045
+ return JSON.parse(readFileSync7(p, "utf-8"));
1000
1046
  } catch {
1001
1047
  return { version: "1.0", last_updated: new Date().toISOString(), entries: [] };
1002
1048
  }
1003
1049
  }
1004
1050
  function writeStore(directory, store) {
1005
1051
  const base = codebaseDir(directory);
1006
- if (!existsSync8(base))
1052
+ if (!existsSync7(base))
1007
1053
  mkdirSync4(base, { recursive: true });
1008
1054
  store.last_updated = new Date().toISOString();
1009
1055
  writeFileSync7(failuresPath(directory), JSON.stringify(store, null, 2), "utf-8");
@@ -1090,17 +1136,17 @@ var failureReplayTool = tool7({
1090
1136
 
1091
1137
  // src/tools/decision-trace.ts
1092
1138
  import { tool as tool8 } from "@opencode-ai/plugin";
1093
- import { readFileSync as readFileSync9, existsSync as existsSync9, mkdirSync as mkdirSync5, appendFileSync } from "fs";
1094
- import { join as join9 } from "path";
1139
+ import { readFileSync as readFileSync8, existsSync as existsSync8, mkdirSync as mkdirSync5, appendFileSync } from "fs";
1140
+ import { join as join8 } from "path";
1095
1141
  var DECISIONS_FILE = "DECISIONS.jsonl";
1096
1142
  function decisionsPath(directory) {
1097
- return join9(codebaseDir(directory), DECISIONS_FILE);
1143
+ return join8(codebaseDir(directory), DECISIONS_FILE);
1098
1144
  }
1099
1145
  function readDecisions(directory) {
1100
1146
  const p = decisionsPath(directory);
1101
- if (!existsSync9(p))
1147
+ if (!existsSync8(p))
1102
1148
  return [];
1103
- return readFileSync9(p, "utf-8").split(`
1149
+ return readFileSync8(p, "utf-8").split(`
1104
1150
  `).filter((l) => l.trim()).map((l) => {
1105
1151
  try {
1106
1152
  return JSON.parse(l);
@@ -1140,7 +1186,7 @@ var decisionTraceTool = tool8({
1140
1186
  case "record": {
1141
1187
  if (!args.entry)
1142
1188
  return JSON.stringify({ error: "entry required" });
1143
- if (!existsSync9(base))
1189
+ if (!existsSync8(base))
1144
1190
  mkdirSync5(base, { recursive: true });
1145
1191
  const entry = { ...args.entry, timestamp: new Date().toISOString() };
1146
1192
  appendFileSync(decisionsPath(dir), JSON.stringify(entry) + `
@@ -1175,25 +1221,25 @@ var decisionTraceTool = tool8({
1175
1221
 
1176
1222
  // src/tools/volatility-map.ts
1177
1223
  import { tool as tool9 } from "@opencode-ai/plugin";
1178
- import { readFileSync as readFileSync10, writeFileSync as writeFileSync9, existsSync as existsSync10, mkdirSync as mkdirSync6 } from "fs";
1179
- import { join as join10 } from "path";
1224
+ import { readFileSync as readFileSync9, writeFileSync as writeFileSync9, existsSync as existsSync9, mkdirSync as mkdirSync6 } from "fs";
1225
+ import { join as join9 } from "path";
1180
1226
  var VOLATILITY_FILE = "VOLATILITY.json";
1181
1227
  function volatilityPath(directory) {
1182
- return join10(codebaseDir(directory), VOLATILITY_FILE);
1228
+ return join9(codebaseDir(directory), VOLATILITY_FILE);
1183
1229
  }
1184
1230
  function readStore2(directory) {
1185
1231
  const p = volatilityPath(directory);
1186
- if (!existsSync10(p))
1232
+ if (!existsSync9(p))
1187
1233
  return { version: "1.0", last_updated: new Date().toISOString(), generated_at: new Date().toISOString(), entries: [] };
1188
1234
  try {
1189
- return JSON.parse(readFileSync10(p, "utf-8"));
1235
+ return JSON.parse(readFileSync9(p, "utf-8"));
1190
1236
  } catch {
1191
1237
  return { version: "1.0", last_updated: new Date().toISOString(), generated_at: new Date().toISOString(), entries: [] };
1192
1238
  }
1193
1239
  }
1194
1240
  function writeStore2(directory, store) {
1195
1241
  const base = codebaseDir(directory);
1196
- if (!existsSync10(base))
1242
+ if (!existsSync9(base))
1197
1243
  mkdirSync6(base, { recursive: true });
1198
1244
  store.last_updated = new Date().toISOString();
1199
1245
  writeFileSync9(volatilityPath(directory), JSON.stringify(store, null, 2), "utf-8");
@@ -1283,25 +1329,25 @@ var volatilityMapTool = tool9({
1283
1329
 
1284
1330
  // src/tools/policy-engine.ts
1285
1331
  import { tool as tool10 } from "@opencode-ai/plugin";
1286
- import { readFileSync as readFileSync11, writeFileSync as writeFileSync10, existsSync as existsSync11, mkdirSync as mkdirSync7 } from "fs";
1287
- import { join as join11 } from "path";
1332
+ import { readFileSync as readFileSync10, writeFileSync as writeFileSync10, existsSync as existsSync10, mkdirSync as mkdirSync7 } from "fs";
1333
+ import { join as join10 } from "path";
1288
1334
  var POLICIES_FILE = "POLICIES.json";
1289
1335
  function policiesPath(directory) {
1290
- return join11(codebaseDir(directory), POLICIES_FILE);
1336
+ return join10(codebaseDir(directory), POLICIES_FILE);
1291
1337
  }
1292
1338
  function readStore3(directory) {
1293
1339
  const p = policiesPath(directory);
1294
- if (!existsSync11(p))
1340
+ if (!existsSync10(p))
1295
1341
  return { version: "1.0", last_updated: new Date().toISOString(), policies: [] };
1296
1342
  try {
1297
- return JSON.parse(readFileSync11(p, "utf-8"));
1343
+ return JSON.parse(readFileSync10(p, "utf-8"));
1298
1344
  } catch {
1299
1345
  return { version: "1.0", last_updated: new Date().toISOString(), policies: [] };
1300
1346
  }
1301
1347
  }
1302
1348
  function writeStore3(directory, store) {
1303
1349
  const base = codebaseDir(directory);
1304
- if (!existsSync11(base))
1350
+ if (!existsSync10(base))
1305
1351
  mkdirSync7(base, { recursive: true });
1306
1352
  store.last_updated = new Date().toISOString();
1307
1353
  writeFileSync10(policiesPath(directory), JSON.stringify(store, null, 2), "utf-8");
@@ -1386,7 +1432,7 @@ var policyEngineTool = tool10({
1386
1432
 
1387
1433
  // src/tools/hash-edit.ts
1388
1434
  import { tool as tool11 } from "@opencode-ai/plugin";
1389
- import { readFileSync as readFileSync12, writeFileSync as writeFileSync11 } from "fs";
1435
+ import { readFileSync as readFileSync11, writeFileSync as writeFileSync11 } from "fs";
1390
1436
  import { createHash } from "crypto";
1391
1437
  var hashEditTool = tool11({
1392
1438
  description: "Reliable file editing with content verification. Takes a target content, its expected MD5 hash, and replacement content. Only applies if the hash matches, preventing edits on stale file versions.",
@@ -1400,7 +1446,7 @@ var hashEditTool = tool11({
1400
1446
  const fullPath = args.filePath.startsWith("/") ? args.filePath : `${context.directory}/${args.filePath}`;
1401
1447
  let content;
1402
1448
  try {
1403
- content = readFileSync12(fullPath, "utf-8");
1449
+ content = readFileSync11(fullPath, "utf-8");
1404
1450
  } catch (e) {
1405
1451
  return `Error: Could not read file ${args.filePath}`;
1406
1452
  }
@@ -1421,8 +1467,8 @@ var hashEditTool = tool11({
1421
1467
 
1422
1468
  // src/tools/council.ts
1423
1469
  import { tool as tool12 } from "@opencode-ai/plugin";
1424
- import { appendFileSync as appendFileSync2, existsSync as existsSync12, mkdirSync as mkdirSync8 } from "fs";
1425
- import { join as join12 } from "path";
1470
+ import { appendFileSync as appendFileSync2, existsSync as existsSync11, mkdirSync as mkdirSync8 } from "fs";
1471
+ import { join as join11 } from "path";
1426
1472
  function createCouncilTool(client) {
1427
1473
  return tool12({
1428
1474
  description: "Run an ensemble of agents (Council) on the same task to reach consensus or compare approaches. Runs 3 specialized agents in parallel and returns their synthesized outputs.",
@@ -1431,7 +1477,7 @@ function createCouncilTool(client) {
1431
1477
  agents: tool12.schema.array(tool12.schema.string()).optional()
1432
1478
  },
1433
1479
  async execute(args, context) {
1434
- const agents = args.agents || ["architect", "reviewer", "coder"];
1480
+ const agents = args.agents || ["architect", "reviewer", "backend-coder"];
1435
1481
  const tasks = agents.map((agent) => ({
1436
1482
  agent,
1437
1483
  prompt: `TASK: ${args.task}
@@ -1493,9 +1539,9 @@ Please synthesize these results. Identify areas of agreement, resolve conflicts,
1493
1539
  function persistCouncilResult(directory, payload) {
1494
1540
  try {
1495
1541
  const base = codebaseDir(directory);
1496
- if (!existsSync12(base))
1542
+ if (!existsSync11(base))
1497
1543
  mkdirSync8(base, { recursive: true });
1498
- const path = join12(base, "COUNCILS.jsonl");
1544
+ const path = join11(base, "COUNCILS.jsonl");
1499
1545
  appendFileSync2(path, JSON.stringify(payload) + `
1500
1546
  `, "utf-8");
1501
1547
  } catch {}
@@ -1503,8 +1549,8 @@ function persistCouncilResult(directory, payload) {
1503
1549
 
1504
1550
  // src/tools/context-generator.ts
1505
1551
  import { tool as tool13 } from "@opencode-ai/plugin";
1506
- import { writeFileSync as writeFileSync12, existsSync as existsSync13, readFileSync as readFileSync13, readdirSync as readdirSync2, statSync } from "fs";
1507
- import { join as join13 } from "path";
1552
+ import { writeFileSync as writeFileSync12, existsSync as existsSync12, readFileSync as readFileSync12, readdirSync as readdirSync2, statSync } from "fs";
1553
+ import { join as join12 } from "path";
1508
1554
  var contextGeneratorTool = tool13({
1509
1555
  description: "Auto-generate or update hierarchical context files (AGENTS.md, CLAUDE.md) throughout the project. These files provide critical grounding for AI agents.",
1510
1556
  args: {
@@ -1513,20 +1559,20 @@ var contextGeneratorTool = tool13({
1513
1559
  },
1514
1560
  async execute(args, context) {
1515
1561
  const root = context.directory;
1516
- const target = args.targetDir ? join13(root, args.targetDir) : root;
1517
- if (!existsSync13(target)) {
1562
+ const target = args.targetDir ? join12(root, args.targetDir) : root;
1563
+ if (!existsSync12(target)) {
1518
1564
  return `Error: Directory ${target} does not exist.`;
1519
1565
  }
1520
- const agentsMdPath = join13(target, "AGENTS.md");
1521
- if (existsSync13(agentsMdPath) && !args.force) {
1566
+ const agentsMdPath = join12(target, "AGENTS.md");
1567
+ if (existsSync12(agentsMdPath) && !args.force) {
1522
1568
  return `AGENTS.md already exists in ${target}. Use force: true to overwrite.`;
1523
1569
  }
1524
- const pkgPath = join13(root, "package.json");
1570
+ const pkgPath = join12(root, "package.json");
1525
1571
  let projectName = "Project";
1526
1572
  let techStack = "Unknown";
1527
- if (existsSync13(pkgPath)) {
1573
+ if (existsSync12(pkgPath)) {
1528
1574
  try {
1529
- const pkg = JSON.parse(readFileSync13(pkgPath, "utf-8"));
1575
+ const pkg = JSON.parse(readFileSync12(pkgPath, "utf-8"));
1530
1576
  projectName = pkg.name || projectName;
1531
1577
  techStack = Object.keys(pkg.dependencies || {}).slice(0, 5).join(", ");
1532
1578
  } catch {}
@@ -1544,7 +1590,7 @@ var contextGeneratorTool = tool13({
1544
1590
 
1545
1591
  ## Directory Map
1546
1592
  ${readdirSync2(target).slice(0, 10).map((f) => {
1547
- const s = statSync(join13(target, f));
1593
+ const s = statSync(join12(target, f));
1548
1594
  return `- \`${f}\`${s.isDirectory() ? "/" : ""} : [Description]`;
1549
1595
  }).join(`
1550
1596
  `)}
@@ -1559,10 +1605,10 @@ Generated by FlowDeck Context Generator.
1559
1605
 
1560
1606
  // src/tools/create-skill.ts
1561
1607
  import { tool as tool14 } from "@opencode-ai/plugin";
1562
- import { mkdirSync as mkdirSync9, writeFileSync as writeFileSync13, existsSync as existsSync14 } from "fs";
1563
- import { join as join14, dirname as dirname3 } from "path";
1608
+ import { mkdirSync as mkdirSync9, writeFileSync as writeFileSync13, existsSync as existsSync13 } from "fs";
1609
+ import { join as join13, dirname as dirname3 } from "path";
1564
1610
  import { fileURLToPath } from "url";
1565
- var SKILLS_DIR = join14(dirname3(fileURLToPath(import.meta.url)), "..", "skills");
1611
+ var SKILLS_DIR = join13(dirname3(fileURLToPath(import.meta.url)), "..", "skills");
1566
1612
  var createSkillTool = tool14({
1567
1613
  description: "Create a new reusable skill in the FlowDeck skill library (src/skills/). " + "Use this when you discover a repeatable pattern, solve a novel problem with human guidance, " + "or want to capture domain knowledge for future sessions.",
1568
1614
  args: {
@@ -1572,9 +1618,9 @@ var createSkillTool = tool14({
1572
1618
  tags: tool14.schema.array(tool14.schema.string()).optional().describe("Optional tags for categorisation, e.g. ['performance', 'typescript']")
1573
1619
  },
1574
1620
  async execute(args) {
1575
- const skillDir = join14(SKILLS_DIR, args.name);
1576
- const skillFile = join14(skillDir, "SKILL.md");
1577
- if (existsSync14(skillFile)) {
1621
+ const skillDir = join13(SKILLS_DIR, args.name);
1622
+ const skillFile = join13(skillDir, "SKILL.md");
1623
+ if (existsSync13(skillFile)) {
1578
1624
  return `Skill '${args.name}' already exists at ${skillFile}.
1579
1625
  ` + `Use a different name or delete the existing skill directory first.`;
1580
1626
  }
@@ -1602,8 +1648,8 @@ origin: FlowDeck (self-learned)${tagLine}
1602
1648
 
1603
1649
  // src/tools/reflect.ts
1604
1650
  import { tool as tool15 } from "@opencode-ai/plugin";
1605
- import { existsSync as existsSync15, readFileSync as readFileSync14 } from "fs";
1606
- import { join as join15 } from "path";
1651
+ import { existsSync as existsSync14, readFileSync as readFileSync13 } from "fs";
1652
+ import { join as join14 } from "path";
1607
1653
  var MAX_ARTIFACT_BYTES = 4000;
1608
1654
  function tail(text, maxBytes) {
1609
1655
  if (text.length <= maxBytes)
@@ -1632,11 +1678,11 @@ var reflectTool = tool15({
1632
1678
  ];
1633
1679
  let found = 0;
1634
1680
  for (const [rel, label] of ARTIFACT_PATHS) {
1635
- const full = join15(root, rel);
1636
- if (!existsSync15(full))
1681
+ const full = join14(root, rel);
1682
+ if (!existsSync14(full))
1637
1683
  continue;
1638
1684
  try {
1639
- const raw = readFileSync14(full, "utf-8").trim();
1685
+ const raw = readFileSync13(full, "utf-8").trim();
1640
1686
  if (!raw)
1641
1687
  continue;
1642
1688
  const count = raw.split(`
@@ -1660,26 +1706,37 @@ import { tool as tool16 } from "@opencode-ai/plugin";
1660
1706
 
1661
1707
  // src/services/memory-store.ts
1662
1708
  import { Database } from "bun:sqlite";
1663
- import { existsSync as existsSync16, mkdirSync as mkdirSync10 } from "fs";
1664
- import { join as join16 } from "path";
1709
+ import { existsSync as existsSync15, mkdirSync as mkdirSync10 } from "fs";
1710
+ import { join as join15 } from "path";
1665
1711
  import { homedir } from "os";
1666
- var MEMORY_DIR = join16(homedir(), ".flowdeck-memory");
1667
- var DB_PATH = join16(MEMORY_DIR, "memory.db");
1668
- function ensureDir() {
1669
- if (!existsSync16(MEMORY_DIR)) {
1670
- mkdirSync10(MEMORY_DIR, { recursive: true });
1671
- }
1712
+ function resolveMemoryDir() {
1713
+ return process.env.FLOWDECK_MEMORY_DIR ?? join15(homedir(), ".flowdeck-memory");
1672
1714
  }
1715
+ var JS_RETRY_COUNT = 3;
1716
+ var JS_RETRY_BASE_MS = 50;
1673
1717
  var db = null;
1718
+ function debugLog(msg) {
1719
+ if (process.env.FLOWDECK_MEMORY_DEBUG) {
1720
+ console.error(`[FlowDeck Memory] ${msg}`);
1721
+ }
1722
+ }
1674
1723
  function getDb() {
1675
1724
  if (!db) {
1676
- ensureDir();
1677
- db = new Database(DB_PATH);
1725
+ const dir = resolveMemoryDir();
1726
+ if (!existsSync15(dir))
1727
+ mkdirSync10(dir, { recursive: true });
1728
+ const dbPath = join15(dir, "memory.db");
1729
+ db = new Database(dbPath);
1730
+ debugLog(`DB opened: ${dbPath}`);
1678
1731
  initializeSchema(db);
1679
1732
  }
1680
1733
  return db;
1681
1734
  }
1682
1735
  function initializeSchema(database) {
1736
+ database.run("PRAGMA journal_mode = WAL");
1737
+ database.run("PRAGMA busy_timeout = 5000");
1738
+ database.run("PRAGMA synchronous = NORMAL");
1739
+ database.run("PRAGMA wal_autocheckpoint = 1000");
1683
1740
  const schema = `
1684
1741
  CREATE TABLE IF NOT EXISTS sessions (
1685
1742
  id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -1707,6 +1764,7 @@ function initializeSchema(database) {
1707
1764
  id INTEGER PRIMARY KEY AUTOINCREMENT,
1708
1765
  session_id INTEGER NOT NULL UNIQUE,
1709
1766
  content TEXT NOT NULL,
1767
+ metadata TEXT,
1710
1768
  created_at TEXT NOT NULL DEFAULT (datetime('now')),
1711
1769
  FOREIGN KEY (session_id) REFERENCES sessions(id)
1712
1770
  );
@@ -1718,6 +1776,49 @@ function initializeSchema(database) {
1718
1776
  CREATE INDEX IF NOT EXISTS idx_sessions_directory ON sessions(directory);
1719
1777
  `;
1720
1778
  database.run(schema);
1779
+ const summaryColumns = database.prepare("PRAGMA table_info(summaries)").all().map((c) => c.name);
1780
+ if (!summaryColumns.includes("metadata")) {
1781
+ database.run("ALTER TABLE summaries ADD COLUMN metadata TEXT");
1782
+ debugLog("Migrated summaries table: added metadata column");
1783
+ }
1784
+ }
1785
+ function isBusyError(err) {
1786
+ if (!err || typeof err !== "object")
1787
+ return false;
1788
+ const e = err;
1789
+ return e.code === "SQLITE_BUSY" || (e.message?.includes("database is locked") ?? false);
1790
+ }
1791
+ function sleepSync(ms) {
1792
+ try {
1793
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
1794
+ } catch {
1795
+ const end = Date.now() + ms;
1796
+ while (Date.now() < end) {}
1797
+ }
1798
+ }
1799
+ function executeWrite(fn, context) {
1800
+ for (let attempt = 0;attempt <= JS_RETRY_COUNT; attempt++) {
1801
+ const start = Date.now();
1802
+ try {
1803
+ const result = fn();
1804
+ const duration = Date.now() - start;
1805
+ if (attempt > 0) {
1806
+ debugLog(`${context}: succeeded after ${attempt} JS retr${attempt === 1 ? "y" : "ies"} (${duration}ms)`);
1807
+ } else {
1808
+ debugLog(`${context}: completed in ${duration}ms`);
1809
+ }
1810
+ return result;
1811
+ } catch (err) {
1812
+ if (isBusyError(err) && attempt < JS_RETRY_COUNT) {
1813
+ const delay = JS_RETRY_BASE_MS * (attempt + 1);
1814
+ debugLog(`${context}: SQLITE_BUSY — JS retry ${attempt + 1}/${JS_RETRY_COUNT} after ${delay}ms`);
1815
+ sleepSync(delay);
1816
+ continue;
1817
+ }
1818
+ throw err;
1819
+ }
1820
+ }
1821
+ throw new Error(`${context}: exhausted all retries`);
1721
1822
  }
1722
1823
  function serializeToolInput(input) {
1723
1824
  if (!input)
@@ -1745,7 +1846,7 @@ function initSession(contentSessionId, project, directory) {
1745
1846
  database.prepare("UPDATE sessions SET last_active_at = ?, prompt_count = prompt_count + 1 WHERE id = ?").run(now, existing.id);
1746
1847
  return { ...existing, last_active_at: now, prompt_count: (existing.prompt_count || 0) + 1 };
1747
1848
  }
1748
- const result = database.prepare("INSERT INTO sessions (content_session_id, project, directory, created_at, last_active_at) VALUES (?, ?, ?, ?, ?)").run(contentSessionId, project, directory, now, now);
1849
+ const result = database.prepare("INSERT INTO sessions (content_session_id, project, directory, created_at, last_active_at, prompt_count) VALUES (?, ?, ?, ?, ?, ?)").run(contentSessionId, project, directory, now, now, 1);
1749
1850
  return {
1750
1851
  id: result.lastInsertRowid,
1751
1852
  content_session_id: contentSessionId,
@@ -1759,27 +1860,38 @@ function initSession(contentSessionId, project, directory) {
1759
1860
  function storeObservation(sessionId, toolName, toolInput, toolResponse, directory) {
1760
1861
  const database = getDb();
1761
1862
  const now = new Date().toISOString();
1762
- const result = database.prepare("INSERT INTO observations (session_id, tool_name, tool_input, tool_response, directory, created_at) VALUES (?, ?, ?, ?, ?, ?)").run(sessionId, toolName, serializeToolInput(toolInput), toolResponse ? toolResponse.slice(0, 1e4) : null, directory, now);
1763
- database.prepare("UPDATE sessions SET last_active_at = ? WHERE id = ?").run(now, sessionId);
1863
+ const serializedInput = serializeToolInput(toolInput);
1864
+ const truncatedResponse = toolResponse ? toolResponse.slice(0, 1e4) : null;
1865
+ const result = executeWrite(database.transaction(() => {
1866
+ const r = database.prepare("INSERT INTO observations (session_id, tool_name, tool_input, tool_response, directory, created_at) VALUES (?, ?, ?, ?, ?, ?)").run(sessionId, toolName, serializedInput, truncatedResponse, directory, now);
1867
+ database.prepare("UPDATE sessions SET last_active_at = ? WHERE id = ?").run(now, sessionId);
1868
+ return r;
1869
+ }), `storeObservation(${toolName})`);
1764
1870
  return {
1765
1871
  id: result.lastInsertRowid,
1766
1872
  session_id: sessionId,
1767
1873
  tool_name: toolName,
1768
- tool_input: parseToolInput(serializeToolInput(toolInput)),
1769
- tool_response: toolResponse ? toolResponse.slice(0, 1e4) : null,
1874
+ tool_input: parseToolInput(serializedInput),
1875
+ tool_response: truncatedResponse,
1770
1876
  directory,
1771
1877
  created_at: now
1772
1878
  };
1773
1879
  }
1774
- function storeSummary(sessionId, content) {
1880
+ function storeSummary(sessionId, content, metadata) {
1775
1881
  const database = getDb();
1776
1882
  const now = new Date().toISOString();
1777
- database.prepare("INSERT OR REPLACE INTO summaries (session_id, content, created_at) VALUES (?, ?, ?)").run(sessionId, content, now);
1778
- database.prepare("UPDATE sessions SET summary = ? WHERE id = ?").run(content, sessionId);
1883
+ const serializedMetadata = metadata ? JSON.stringify(metadata) : null;
1884
+ const id = executeWrite(database.transaction(() => {
1885
+ database.prepare("INSERT OR REPLACE INTO summaries (session_id, content, metadata, created_at) VALUES (?, ?, ?, ?)").run(sessionId, content, serializedMetadata, now);
1886
+ database.prepare("UPDATE sessions SET summary = ? WHERE id = ?").run(content.slice(0, 2000), sessionId);
1887
+ return database.prepare("SELECT last_insert_rowid() as id").get().id;
1888
+ }), `storeSummary(session=${sessionId})`);
1889
+ debugLog(`storeSummary: wrote ${content.length} chars${metadata ? ` + ${JSON.stringify(metadata).length}B metadata` : ""} for session ${sessionId}`);
1779
1890
  return {
1780
- id: database.prepare("SELECT last_insert_rowid() as id").get().id,
1891
+ id,
1781
1892
  session_id: sessionId,
1782
1893
  content,
1894
+ metadata: metadata ?? null,
1783
1895
  created_at: now
1784
1896
  };
1785
1897
  }
@@ -1798,6 +1910,20 @@ function getObservationsForSession(sessionId) {
1798
1910
  tool_input: parseToolInput(obs.tool_input)
1799
1911
  }));
1800
1912
  }
1913
+ function getSessionSummary(sessionId) {
1914
+ const database = getDb();
1915
+ const row = database.prepare("SELECT * FROM summaries WHERE session_id = ?").get(sessionId);
1916
+ if (!row)
1917
+ return null;
1918
+ return {
1919
+ ...row,
1920
+ metadata: row.metadata ? JSON.parse(row.metadata) : null
1921
+ };
1922
+ }
1923
+ function getSessionByContentSessionId(contentSessionId) {
1924
+ const database = getDb();
1925
+ return database.prepare("SELECT * FROM sessions WHERE content_session_id = ?").get(contentSessionId) || null;
1926
+ }
1801
1927
  function getRecentObservations(directory, limit = 50) {
1802
1928
  const database = getDb();
1803
1929
  const rows = database.prepare(`SELECT o.*, s.project, s.content_session_id, s.created_at as session_created
@@ -1859,11 +1985,15 @@ function getContextForDirectory(directory, maxObservations = 20) {
1859
1985
  lines.push(`Output: ${preview}${observation.tool_response.length > 300 ? "..." : ""}`);
1860
1986
  }
1861
1987
  }
1862
- const summaries = getDb().prepare(`SELECT su.* FROM summaries su
1988
+ const summaryRows = getDb().prepare(`SELECT su.* FROM summaries su
1863
1989
  JOIN sessions s ON su.session_id = s.id
1864
1990
  WHERE s.directory = ?
1865
1991
  ORDER BY su.created_at DESC
1866
1992
  LIMIT 3`).all(directory);
1993
+ const summaries = summaryRows.map((r) => ({
1994
+ ...r,
1995
+ metadata: r.metadata ? JSON.parse(r.metadata) : null
1996
+ }));
1867
1997
  if (summaries.length > 0) {
1868
1998
  lines.push(`
1869
1999
  ## Session Summaries`);
@@ -1871,12 +2001,20 @@ function getContextForDirectory(directory, maxObservations = 20) {
1871
2001
  const date = sum.created_at ? new Date(sum.created_at).toLocaleDateString() : "unknown";
1872
2002
  lines.push(`
1873
2003
  ### [${date}]`);
1874
- lines.push(sum.content.slice(0, 500));
2004
+ lines.push(sum.content.slice(0, 2000));
1875
2005
  }
1876
2006
  }
1877
2007
  return lines.join(`
1878
2008
  `);
1879
2009
  }
2010
+ function getDbSettings() {
2011
+ const database = getDb();
2012
+ const journalMode = database.prepare("PRAGMA journal_mode").get().journal_mode;
2013
+ const busyTimeout = database.prepare("PRAGMA busy_timeout").get().timeout;
2014
+ const synchronous = database.prepare("PRAGMA synchronous").get().synchronous;
2015
+ const walAutocheckpoint = database.prepare("PRAGMA wal_autocheckpoint").get().wal_autocheckpoint;
2016
+ return { journal_mode: journalMode, busy_timeout: busyTimeout, synchronous, wal_autocheckpoint: walAutocheckpoint };
2017
+ }
1880
2018
 
1881
2019
  // src/tools/memory-search.ts
1882
2020
  var memorySearchTool = tool16({
@@ -1896,8 +2034,14 @@ var memorySearchTool = tool16({
1896
2034
  return JSON.stringify({ error: "Session not found", session_id: args.session_id });
1897
2035
  }
1898
2036
  const observations = getObservationsForSession(targetSession.id);
2037
+ const summary = getSessionSummary(targetSession.id);
1899
2038
  return JSON.stringify({
1900
2039
  session: targetSession,
2040
+ summary: summary ? {
2041
+ content: summary.content,
2042
+ metadata: summary.metadata,
2043
+ created_at: summary.created_at
2044
+ } : null,
1901
2045
  observations: observations.map((o) => ({
1902
2046
  tool_name: o.tool_name,
1903
2047
  tool_input: o.tool_input,
@@ -1944,69 +2088,57 @@ var memorySearchTool = tool16({
1944
2088
 
1945
2089
  // src/tools/memory-status.ts
1946
2090
  import { tool as tool17 } from "@opencode-ai/plugin";
1947
- import { Database as Database2 } from "bun:sqlite";
1948
- import { existsSync as existsSync17 } from "fs";
1949
- import { join as join17 } from "path";
2091
+ import { existsSync as existsSync16 } from "fs";
2092
+ import { join as join16 } from "path";
1950
2093
  import { homedir as homedir2 } from "os";
1951
- var DB_PATH2 = join17(homedir2(), ".flowdeck-memory", "memory.db");
2094
+ function resolveDbPath() {
2095
+ return join16(process.env.FLOWDECK_MEMORY_DIR ?? join16(homedir2(), ".flowdeck-memory"), "memory.db");
2096
+ }
1952
2097
  var memoryStatusTool = tool17({
1953
2098
  description: "Check FlowDeck memory database status, statistics, and recent sessions",
1954
2099
  args: {},
1955
- async execute(_args, _context) {
2100
+ async execute(_args, context) {
2101
+ const directory = context?.directory ?? process.cwd();
2102
+ const dbPath = resolveDbPath();
1956
2103
  try {
1957
- const exists = existsSync17(DB_PATH2);
1958
- const result = {
1959
- database_exists: exists,
1960
- path: DB_PATH2,
1961
- status: exists ? "ACTIVE" : "NOT_INITIALIZED",
1962
- statistics: null
1963
- };
1964
- if (exists) {
1965
- try {
1966
- const db2 = new Database2(DB_PATH2);
1967
- const sessions = db2.prepare("SELECT COUNT(*) as count FROM sessions").get();
1968
- const observations = db2.prepare("SELECT COUNT(*) as count FROM observations").get();
1969
- const summaries = db2.prepare("SELECT COUNT(*) as count FROM summaries").get();
1970
- const recentSessions = db2.prepare(`
1971
- SELECT
1972
- id,
1973
- content_session_id,
1974
- project,
1975
- directory,
1976
- created_at,
1977
- last_active_at,
1978
- prompt_count
1979
- FROM sessions
1980
- ORDER BY last_active_at DESC
1981
- LIMIT 5
1982
- `).all();
1983
- result.statistics = {
1984
- sessions: sessions.count,
1985
- observations: observations.count,
1986
- summaries: summaries.count,
1987
- recent_sessions: recentSessions.map((s) => {
1988
- const obsCount = db2.prepare("SELECT COUNT(*) as count FROM observations WHERE session_id = ?").get(s.id);
1989
- return {
1990
- project: s.project,
1991
- directory: s.directory,
1992
- observations_in_session: obsCount.count,
1993
- last_active: s.last_active_at,
1994
- prompt_count: s.prompt_count
1995
- };
1996
- })
1997
- };
1998
- db2.close();
1999
- } catch (err) {
2000
- result.status = "ERROR";
2001
- result.statistics = { error: String(err) };
2002
- }
2104
+ const exists = existsSync16(dbPath);
2105
+ if (!exists) {
2106
+ return JSON.stringify({
2107
+ database_exists: false,
2108
+ path: dbPath,
2109
+ status: "NOT_INITIALIZED"
2110
+ }, null, 2);
2003
2111
  }
2004
- return JSON.stringify(result, null, 2);
2112
+ const settings = getDbSettings();
2113
+ const recentSessions = getRecentSessions(directory, 5);
2114
+ const sessionStats = recentSessions.map((s) => {
2115
+ const observations = getObservationsForSession(s.id);
2116
+ const summary = getSessionSummary(s.id);
2117
+ return {
2118
+ project: s.project,
2119
+ directory: s.directory,
2120
+ content_session_id: s.content_session_id,
2121
+ observations_in_session: observations.length,
2122
+ last_active: s.last_active_at,
2123
+ prompt_count: s.prompt_count,
2124
+ has_summary: !!summary,
2125
+ summary_length: summary?.content.length ?? 0,
2126
+ summary_preview: summary?.content.slice(0, 200) ?? null,
2127
+ handoff_metadata: summary?.metadata ?? null
2128
+ };
2129
+ });
2130
+ return JSON.stringify({
2131
+ database_exists: true,
2132
+ path: dbPath,
2133
+ status: "ACTIVE",
2134
+ pragma_settings: settings,
2135
+ recent_sessions_in_directory: sessionStats
2136
+ }, null, 2);
2005
2137
  } catch (err) {
2006
2138
  return JSON.stringify({
2007
2139
  status: "ERROR",
2008
2140
  error: String(err),
2009
- path: DB_PATH2
2141
+ path: dbPath
2010
2142
  }, null, 2);
2011
2143
  }
2012
2144
  }
@@ -2014,7 +2146,7 @@ var memoryStatusTool = tool17({
2014
2146
 
2015
2147
  // src/hooks/memory-hook.ts
2016
2148
  var MAX_TOOL_RESPONSE = 1e4;
2017
- var MAX_PROMPT_LENGTH = 2000;
2149
+ var MAX_SUMMARY_STORAGE = 50000;
2018
2150
  var activeSessions = new Map;
2019
2151
  function extractProjectFromDirectory(directory) {
2020
2152
  const parts = directory.split("/");
@@ -2025,6 +2157,59 @@ function truncate(str, max) {
2025
2157
  return str || "";
2026
2158
  return str.slice(0, max);
2027
2159
  }
2160
+ function buildHandoffMetadata(sessionId, directory, summaryText, observations) {
2161
+ const fileTools = new Set(["edit", "create", "view", "read", "hash-edit", "str-replace-editor"]);
2162
+ const importantFilesSet = new Set;
2163
+ for (const obs of observations) {
2164
+ if (fileTools.has(obs.tool_name) && obs.tool_input) {
2165
+ const path = obs.tool_input.path;
2166
+ if (path)
2167
+ importantFilesSet.add(path);
2168
+ }
2169
+ }
2170
+ const toolNamesUsed = [...new Set(observations.map((o) => o.tool_name).filter((t) => t !== "assistant_message"))];
2171
+ function extractBullets(text) {
2172
+ return text.split(`
2173
+ `).filter((l) => /^\s*[-*]/.test(l)).map((l) => l.replace(/^\s*[-*]\s+/, "").trim()).filter(Boolean);
2174
+ }
2175
+ const sections = {};
2176
+ let currentSection = "";
2177
+ const currentLines = [];
2178
+ for (const line of summaryText.split(`
2179
+ `)) {
2180
+ const header = line.match(/^##\s+\d+\.\s+(.+)/);
2181
+ if (header) {
2182
+ if (currentSection)
2183
+ sections[currentSection] = currentLines.join(`
2184
+ `).trim();
2185
+ currentSection = header[1].trim();
2186
+ currentLines.length = 0;
2187
+ } else {
2188
+ currentLines.push(line);
2189
+ }
2190
+ }
2191
+ if (currentSection)
2192
+ sections[currentSection] = currentLines.join(`
2193
+ `).trim();
2194
+ const completed = extractBullets(sections["Work Completed"] ?? "");
2195
+ const pending = extractBullets(sections["Remaining Tasks"] ?? "");
2196
+ return {
2197
+ workflow_name: extractProjectFromDirectory(directory),
2198
+ current_status: "compacted",
2199
+ current_stage: null,
2200
+ completed_stages: completed,
2201
+ pending_stages: pending,
2202
+ key_decisions: [],
2203
+ blockers: [],
2204
+ important_files: [...importantFilesSet].slice(0, 30),
2205
+ approvals: [],
2206
+ open_questions: [],
2207
+ next_steps: pending.slice(0, 5),
2208
+ tool_names_used: toolNamesUsed.slice(0, 20),
2209
+ observation_count: observations.length,
2210
+ updated_at: new Date().toISOString()
2211
+ };
2212
+ }
2028
2213
  function onSessionCreated(directory, contentSessionId, prompt) {
2029
2214
  const project = extractProjectFromDirectory(directory);
2030
2215
  const session = initSession(contentSessionId, project, directory);
@@ -2049,7 +2234,11 @@ function onToolExecuted(contentSessionId, toolName, toolInput, toolResponse, dir
2049
2234
  };
2050
2235
  activeSessions.set(contentSessionId, ctx);
2051
2236
  }
2052
- storeObservation(ctx.sessionId, truncate(toolName, 200), toolInput, toolResponse ? truncate(toolResponse, MAX_TOOL_RESPONSE) : null, directory);
2237
+ try {
2238
+ storeObservation(ctx.sessionId, truncate(toolName, 200), toolInput, toolResponse ? truncate(toolResponse, MAX_TOOL_RESPONSE) : null, directory);
2239
+ } catch (err) {
2240
+ console.warn(`[FlowDeck Memory] Failed to store observation for tool "${toolName}":`, err);
2241
+ }
2053
2242
  }
2054
2243
  function onMessageUpdated(contentSessionId, role, content, directory) {
2055
2244
  if (role !== "assistant")
@@ -2068,33 +2257,64 @@ function onMessageUpdated(contentSessionId, role, content, directory) {
2068
2257
  };
2069
2258
  activeSessions.set(contentSessionId, ctx);
2070
2259
  }
2071
- storeObservation(ctx.sessionId, "assistant_message", { role }, truncate(content, MAX_TOOL_RESPONSE), directory);
2260
+ try {
2261
+ storeObservation(ctx.sessionId, "assistant_message", { role }, truncate(content, MAX_TOOL_RESPONSE), directory);
2262
+ } catch (err) {
2263
+ console.warn("[FlowDeck Memory] Failed to store assistant message observation:", err);
2264
+ }
2072
2265
  }
2073
2266
  function onSessionCompact(contentSessionId, summary) {
2074
- const ctx = activeSessions.get(contentSessionId);
2075
- if (!ctx)
2076
- return;
2077
- storeSummary(ctx.sessionId, truncate(summary, MAX_PROMPT_LENGTH));
2267
+ let ctx = activeSessions.get(contentSessionId);
2268
+ if (!ctx) {
2269
+ const dbSession = getSessionByContentSessionId(contentSessionId);
2270
+ if (!dbSession) {
2271
+ console.warn(`[FlowDeck Memory] onSessionCompact: no session found for contentSessionId=${contentSessionId} — summary discarded`);
2272
+ return;
2273
+ }
2274
+ ctx = {
2275
+ sessionId: dbSession.id,
2276
+ contentSessionId,
2277
+ project: dbSession.project,
2278
+ directory: dbSession.directory
2279
+ };
2280
+ activeSessions.set(contentSessionId, ctx);
2281
+ }
2282
+ const storedContent = truncate(summary, MAX_SUMMARY_STORAGE);
2283
+ try {
2284
+ const observations = getObservationsForSession(ctx.sessionId);
2285
+ const metadata = buildHandoffMetadata(ctx.sessionId, ctx.directory, summary, observations);
2286
+ storeSummary(ctx.sessionId, storedContent, metadata);
2287
+ } catch (err) {
2288
+ console.warn(`[FlowDeck Memory] Failed to store compaction summary for session ${ctx.sessionId}:`, err);
2289
+ }
2078
2290
  }
2079
2291
  function onSessionEnd(contentSessionId, lastMessage) {
2080
- const ctx = activeSessions.get(contentSessionId);
2081
- if (!ctx)
2082
- return;
2083
- if (lastMessage && lastMessage.trim()) {
2084
- storeSummary(ctx.sessionId, truncate(lastMessage, MAX_PROMPT_LENGTH));
2292
+ let ctx = activeSessions.get(contentSessionId);
2293
+ if (!ctx) {
2294
+ const dbSession = getSessionByContentSessionId(contentSessionId);
2295
+ if (dbSession) {
2296
+ ctx = {
2297
+ sessionId: dbSession.id,
2298
+ contentSessionId,
2299
+ project: dbSession.project,
2300
+ directory: dbSession.directory
2301
+ };
2302
+ }
2303
+ }
2304
+ if (ctx && lastMessage && lastMessage.trim()) {
2305
+ try {
2306
+ const observations = getObservationsForSession(ctx.sessionId);
2307
+ const metadata = buildHandoffMetadata(ctx.sessionId, ctx.directory, lastMessage, observations);
2308
+ storeSummary(ctx.sessionId, truncate(lastMessage, MAX_SUMMARY_STORAGE), metadata);
2309
+ } catch (err) {
2310
+ console.warn(`[FlowDeck Memory] Failed to store end-of-session summary for session ${ctx?.sessionId}:`, err);
2311
+ }
2085
2312
  }
2086
2313
  activeSessions.delete(contentSessionId);
2087
2314
  }
2088
2315
  function getSessionContext(directory, contentSessionId) {
2089
2316
  const context = getContextForDirectory(directory, 30);
2090
2317
  const previousSessions = getRecentSessions(directory, 5);
2091
- if (previousSessions.length > 0 && activeSessions.has(contentSessionId)) {
2092
- const ctx = activeSessions.get(contentSessionId);
2093
- for (const prev of previousSessions) {
2094
- if (prev.content_session_id === contentSessionId)
2095
- continue;
2096
- }
2097
- }
2098
2318
  return { context, previousSessions };
2099
2319
  }
2100
2320
  function clearSession(contentSessionId) {
@@ -2113,6 +2333,49 @@ var memoryHook = {
2113
2333
  // src/hooks/guard-rails.ts
2114
2334
  import { existsSync as existsSync18, readFileSync as readFileSync15 } from "fs";
2115
2335
  import { join as join18 } from "path";
2336
+
2337
+ // src/config/loader.ts
2338
+ import { existsSync as existsSync17, readFileSync as readFileSync14 } from "fs";
2339
+ import { join as join17 } from "path";
2340
+ import { homedir as homedir3 } from "os";
2341
+ var CONFIG_FILENAME = "flowdeck.json";
2342
+ function getGlobalConfigDir() {
2343
+ return process.env.OPENCODE_CONFIG_DIR || (process.env.XDG_CONFIG_HOME ? join17(process.env.XDG_CONFIG_HOME, "opencode") : join17(homedir3(), ".config", "opencode"));
2344
+ }
2345
+ function loadFlowDeckConfig(directory) {
2346
+ const candidates = [];
2347
+ if (directory) {
2348
+ candidates.push(join17(directory, ".opencode", CONFIG_FILENAME));
2349
+ }
2350
+ candidates.push(join17(getGlobalConfigDir(), CONFIG_FILENAME));
2351
+ for (const configPath of candidates) {
2352
+ if (existsSync17(configPath)) {
2353
+ try {
2354
+ const content = readFileSync14(configPath, "utf-8");
2355
+ return JSON.parse(content);
2356
+ } catch {
2357
+ console.warn(`[flowdeck] Failed to load config from ${configPath}`);
2358
+ }
2359
+ }
2360
+ }
2361
+ return {};
2362
+ }
2363
+ function resolveDesignFirstConfig(config) {
2364
+ return {
2365
+ enabled: config.designFirst?.enabled ?? true,
2366
+ enforcement: config.designFirst?.enforcement ?? "strict",
2367
+ requireApprovalBeforeImplementation: config.designFirst?.requireApprovalBeforeImplementation ?? true,
2368
+ modelOverrides: config.designFirst?.modelOverrides ?? {},
2369
+ defaultSkillsByTaskType: config.designFirst?.defaultSkillsByTaskType ?? {
2370
+ "landing-page": ["landing-page-design", "wireframe-planning", "design-system-definition", "frontend-handoff"],
2371
+ dashboard: ["dashboard-design", "ui-ux-planning", "wireframe-planning", "responsive-review"],
2372
+ "admin-panel": ["ui-ux-planning", "wireframe-planning", "design-system-definition", "frontend-handoff"],
2373
+ "app-screen": ["app-shell-design", "ui-ux-planning", "wireframe-planning", "responsive-review"],
2374
+ "general-ui": ["ui-ux-planning", "wireframe-planning", "design-system-definition", "frontend-handoff"]
2375
+ }
2376
+ };
2377
+ }
2378
+ // src/hooks/guard-rails.ts
2116
2379
  var PLANNING_DIR2 = ".planning";
2117
2380
  var CONFIG_FILE = "config.json";
2118
2381
  var STATE_FILE2 = "STATE.md";
@@ -2198,6 +2461,10 @@ async function guardRailsHook(ctx, input, _output) {
2198
2461
  if (execMode === "guarded") {
2199
2462
  throw new Error(`[flowdeck] GUARDED MODE: edit will proceed but flag for human review.`);
2200
2463
  }
2464
+ const designGateMessage = getDesignGateMessage(dir);
2465
+ if (designGateMessage) {
2466
+ throw new Error(designGateMessage);
2467
+ }
2201
2468
  const effectiveSeverity = getEffectiveSeverity(configPath, statePath2);
2202
2469
  if (effectiveSeverity === null)
2203
2470
  return;
@@ -2220,6 +2487,31 @@ async function guardRailsHook(ctx, input, _output) {
2220
2487
  }
2221
2488
  }
2222
2489
  }
2490
+ function getDesignGateMessage(dir) {
2491
+ const designConfig = resolveDesignFirstConfig(loadFlowDeckConfig(dir));
2492
+ if (!designConfig.enabled || !designConfig.requireApprovalBeforeImplementation)
2493
+ return null;
2494
+ const state = readPlanningState(dir);
2495
+ if (state.design_override && state.design_override_reason && state.design_override_reason.trim().length > 0)
2496
+ return null;
2497
+ const designApproved = state.design_stage === "handoff_complete" && state.design_approved;
2498
+ if (state.requires_design_first || state.task_type && isUiHeavyTask(state.task_type) || planSuggestsUiHeavy(dir, state.phase || 1)) {
2499
+ if (designApproved)
2500
+ return null;
2501
+ if (designConfig.enforcement === "advisory") {
2502
+ return "[flowdeck] WARNING: UI-heavy task detected without approved design handoff. Run /fd-design --mode=draft first.";
2503
+ }
2504
+ return "[flowdeck] BLOCK: UI-heavy task requires approved design handoff. Run /fd-design --mode=draft or set explicit design override in STATE.md.";
2505
+ }
2506
+ return null;
2507
+ }
2508
+ function planSuggestsUiHeavy(dir, phase) {
2509
+ const planPath = phasePlanPath(dir, phase);
2510
+ if (!existsSync18(planPath))
2511
+ return false;
2512
+ const planContent = readFileSync15(planPath, "utf-8");
2513
+ return isUiHeavyTask(planContent);
2514
+ }
2223
2515
  function effectiveSeverity(configPath, statePath2) {
2224
2516
  if (existsSync18(configPath)) {
2225
2517
  try {
@@ -2332,12 +2624,37 @@ function checkArchConstraint(directory, filePath) {
2332
2624
  function checkPhaseEnforcement(directory) {
2333
2625
  try {
2334
2626
  const state = readPlanningState(directory);
2627
+ const flowdeckConfig = resolveDesignFirstConfig(loadFlowDeckConfig(directory));
2335
2628
  if (state.phase > 0 && state.phase < 3) {
2336
2629
  return `FLOWDECK [phase-gate]: writing to codebase is blocked in phase ${state.phase} (${state.phase === 1 ? "discuss" : "plan"}). Run /fd-plan --confirm to enter execute phase.`;
2337
2630
  }
2631
+ if (flowdeckConfig.enabled && flowdeckConfig.requireApprovalBeforeImplementation && isUiDesignApprovalRequired(directory)) {
2632
+ if (flowdeckConfig.enforcement === "advisory") {
2633
+ return `FLOWDECK [design-gate]: advisory design-first mode detected missing approval. Run /fd-design --mode=draft or set design_override=true in STATE.md.`;
2634
+ }
2635
+ return `FLOWDECK [design-gate]: UI-heavy task requires approved design handoff before implementation. Run /fd-design --mode=draft and ensure design_stage=handoff_complete + design_approved=true, or set explicit design_override with reason.`;
2636
+ }
2338
2637
  } catch {}
2339
2638
  return null;
2340
2639
  }
2640
+ function isUiDesignApprovalRequired(directory) {
2641
+ const state = readPlanningState(directory);
2642
+ if (state.design_override && state.design_override_reason && state.design_override_reason.trim().length > 0)
2643
+ return false;
2644
+ if (state.requires_design_first) {
2645
+ return !(state.design_stage === "handoff_complete" && state.design_approved);
2646
+ }
2647
+ if (state.task_type && isUiHeavyTask(state.task_type)) {
2648
+ return !(state.design_stage === "handoff_complete" && state.design_approved);
2649
+ }
2650
+ const planPath = phasePlanPath(directory, state.phase || 1);
2651
+ if (!existsSync19(planPath))
2652
+ return false;
2653
+ const planContent = readFileSync16(planPath, "utf-8");
2654
+ if (!isUiHeavyTask(planContent))
2655
+ return false;
2656
+ return !(state.design_stage === "handoff_complete" && state.design_approved);
2657
+ }
2341
2658
  async function toolGuardHook(ctx, input, output) {
2342
2659
  if (!IS_ENABLED())
2343
2660
  return;
@@ -3106,7 +3423,9 @@ function blockMessage(toolName) {
3106
3423
  ` + `The orchestrator is a coordinator — it must delegate all implementation work.
3107
3424
 
3108
3425
  ` + `Use the \`delegate\` tool to hand this off:
3109
- ` + ` delegate({ agent: "@coder", prompt: "..." }) — code writing / editing
3426
+ ` + ` delegate({ agent: "@backend-coder", prompt: "..." }) — backend code writing / editing
3427
+ ` + ` delegate({ agent: "@frontend-coder", prompt: "..." }) — frontend code writing / editing
3428
+ ` + ` delegate({ agent: "@devops", prompt: "..." }) — CI/CD, deploy, and infra changes
3110
3429
  ` + ` delegate({ agent: "@mapper", prompt: "..." }) — codebase mapping
3111
3430
  ` + ` delegate({ agent: "@researcher", prompt: "..." }) — research / file analysis
3112
3431
  ` + ` delegate({ agent: "@tester", prompt: "..." }) — tests / commands
@@ -3308,11 +3627,22 @@ For each incomplete step in PLAN.md:
3308
3627
  5. Re-read STATE.md to confirm state
3309
3628
  6. Move to the next incomplete step
3310
3629
 
3630
+ ## Implementation Routing
3631
+
3632
+ When a plan step requires implementation, route to a role-specific agent:
3633
+ - Use @backend-coder for server, API, business logic, database, and non-UI application code.
3634
+ - Use @frontend-coder for UI components, client state, styling, and interaction behavior.
3635
+ - Use @devops for CI/CD workflows, deployment, infrastructure, runtime config, and operations scripts.
3636
+ - If a step mixes multiple domains, split it into multiple delegated tasks by domain.
3637
+
3311
3638
  ## Agent Team
3312
3639
 
3313
3640
  | Agent | Invoke | Best For |
3314
3641
  |-------|--------|----------|
3315
- | Coder | @coder | All code implementation |
3642
+ | Design | @design | Discovery, UX planning, wireframes, visual system, implementation handoff, design fidelity review |
3643
+ | Backend Coder | @backend-coder | Backend code implementation |
3644
+ | Frontend Coder | @frontend-coder | Frontend code implementation |
3645
+ | DevOps | @devops | CI/CD and infrastructure implementation |
3316
3646
  | Researcher | @researcher | API docs, library usage |
3317
3647
  | Tester | @tester | Writing and running tests |
3318
3648
  | Reviewer | @reviewer | Code quality review |
@@ -3323,7 +3653,6 @@ For each incomplete step in PLAN.md:
3323
3653
  | Code Explorer | @code-explorer | Reading unfamiliar code |
3324
3654
  | Debug Specialist | @debug-specialist | Root cause analysis |
3325
3655
  | Build Resolver | @build-error-resolver | Build/compile failures |
3326
- | Parallel Coordinator | @parallel-coordinator | Multi-track parallel work |
3327
3656
  | Doc Updater | @doc-updater | Updating existing docs |
3328
3657
  | Task Splitter | @task-splitter | Decomposing complex tasks |
3329
3658
  | Discusser | @discusser | Requirements extraction |
@@ -3336,12 +3665,13 @@ For each incomplete step in PLAN.md:
3336
3665
  ## Phase State Machine
3337
3666
 
3338
3667
  \`\`\`
3339
- discuss → plan → execute → review
3668
+ discuss → plan → design (for UI-heavy tasks) → execute → review
3340
3669
  \`\`\`
3341
3670
 
3342
3671
  - **discuss**: Requirements extraction with @discusser
3343
3672
  - **plan**: Plan creation with @planner, review with @plan-checker
3344
- - **execute**: Implementation with @coder, @tester, @researcher in parallel where possible
3673
+ - **design**: UX structure, wireframe/layout planning, and visual system definition with @design
3674
+ - **execute**: Implementation with @backend-coder, @frontend-coder, @devops, @tester, and @researcher in parallel where possible, only after approved design handoff for UI-heavy tasks
3345
3675
  - **review**: Review with @reviewer, @security-auditor
3346
3676
 
3347
3677
  ## Tracking
@@ -3363,7 +3693,7 @@ If a delegated agent fails:
3363
3693
  3. If still failing, escalate:
3364
3694
 
3365
3695
  \`\`\`
3366
- BLOCKED: @coder failed on step 3 (add payment endpoint).
3696
+ BLOCKED: implementation agent failed on step 3 (add payment endpoint).
3367
3697
  Error: [exact error message]
3368
3698
  Retried once with clarification. Still failing.
3369
3699
 
@@ -3384,11 +3714,26 @@ When a task required unusual human guidance, a novel solution strategy, or expos
3384
3714
 
3385
3715
  Do NOT create a skill for routine tasks. Only capture genuinely novel or reusable patterns.`;
3386
3716
  var AGENT_DESCRIPTIONS = {
3387
- coder: `@coder
3388
- - Role: Implements features and fixes based on confirmed plans
3717
+ design: `@design
3718
+ - Role: Runs design-first workflow for user-facing tasks
3389
3719
  - Permissions: Read/write files
3390
- - Best for: All code implementation tasks
3391
- - **Delegate when:** Implementation work, following a plan`,
3720
+ - Best for: UX structure, wireframes, visual direction, tokens, and frontend handoff
3721
+ - **Delegate when:** Task includes website/app/dashboard/admin/user-facing UI work`,
3722
+ "backend-coder": `@backend-coder
3723
+ - Role: Implements backend features and fixes based on confirmed plans
3724
+ - Permissions: Read/write files
3725
+ - Best for: API, services, data layer, and business logic
3726
+ - **Delegate when:** Backend or server-side implementation work`,
3727
+ "frontend-coder": `@frontend-coder
3728
+ - Role: Implements frontend features and fixes based on confirmed plans
3729
+ - Permissions: Read/write files
3730
+ - Best for: UI components, client state, rendering, and interaction behavior
3731
+ - **Delegate when:** Frontend implementation work`,
3732
+ devops: `@devops
3733
+ - Role: Implements DevOps and infrastructure changes based on confirmed plans
3734
+ - Permissions: Read/write files
3735
+ - Best for: CI/CD, deployment config, infra scripts, and runtime operations
3736
+ - **Delegate when:** Infrastructure, pipeline, or operations implementation work`,
3392
3737
  researcher: `@researcher
3393
3738
  - Role: Researches documentation, APIs, and best practices
3394
3739
  - Permissions: Read files
@@ -3460,11 +3805,6 @@ var AGENT_DESCRIPTIONS = {
3460
3805
  - Permissions: Read/write files
3461
3806
  - Best for: Requirements extraction
3462
3807
  - **Delegate when:** Starting new feature or project phase`,
3463
- "parallel-coordinator": `@parallel-coordinator
3464
- - Role: Coordinates multi-wave parallel execution
3465
- - Permissions: Read files
3466
- - Best for: Multi-track parallel work
3467
- - **Delegate when:** Need to execute multiple tasks in parallel`,
3468
3808
  planner: `@planner
3469
3809
  - Role: Creates detailed implementation plans
3470
3810
  - Permissions: Read files
@@ -3766,7 +4106,7 @@ var createPlanCheckerAgent = (model, customPrompt, customAppendPrompt) => {
3766
4106
  };
3767
4107
 
3768
4108
  // src/agents/coder.ts
3769
- var CODER_PROMPT = `You implement features and fix bugs. You follow the plan exactly. You do not invent requirements.
4109
+ var BASE_IMPLEMENTER_PROMPT = `You implement features and fix bugs. You follow the plan exactly. You do not invent requirements.
3770
4110
 
3771
4111
  ## Before Writing Code
3772
4112
 
@@ -3876,11 +4216,62 @@ After implementing, report:
3876
4216
  - Tests added or updated
3877
4217
  - Any deviations from the plan and why
3878
4218
  - Next step ready to execute`;
3879
- var createCoderAgent = (model, customPrompt, customAppendPrompt) => {
3880
- const prompt = resolvePrompt(CODER_PROMPT, customPrompt, customAppendPrompt);
4219
+ var BACKEND_CODER_PROMPT = `${BASE_IMPLEMENTER_PROMPT}
4220
+
4221
+ ## Domain Focus
4222
+
4223
+ Prioritize backend and platform code:
4224
+ - Server handlers, services, repositories, jobs, and business logic
4225
+ - Database and persistence-layer changes
4226
+ - API contracts and boundary validation
4227
+ `;
4228
+ var FRONTEND_CODER_PROMPT = `${BASE_IMPLEMENTER_PROMPT}
4229
+
4230
+ ## Domain Focus
4231
+
4232
+ Prioritize frontend implementation quality:
4233
+ - UI components, client state, accessibility, and interaction behavior
4234
+ - Styling consistency with existing design system/tokens
4235
+ - Browser/runtime safety (no server-only assumptions in client code)
4236
+ `;
4237
+ var DEVOPS_PROMPT = `${BASE_IMPLEMENTER_PROMPT}
4238
+
4239
+ ## Domain Focus
4240
+
4241
+ Prioritize infrastructure and delivery tasks:
4242
+ - CI/CD workflows, build pipelines, deployment configuration
4243
+ - Environment/runtime configuration and operational scripts
4244
+ - Reliability and rollback safety for production-facing changes
4245
+ `;
4246
+ var createBackendCoderAgent = (model, customPrompt, customAppendPrompt) => {
4247
+ const prompt = resolvePrompt(BACKEND_CODER_PROMPT, customPrompt, customAppendPrompt);
3881
4248
  return {
3882
- name: "coder",
3883
- description: "Implements features and fixes based on confirmed plans. Follows existing code patterns and project conventions. Use for all code implementation tasks.",
4249
+ name: "backend-coder",
4250
+ description: "Implements backend features and fixes based on confirmed plans. Follows existing code patterns and project conventions.",
4251
+ config: {
4252
+ model,
4253
+ temperature: 0.1,
4254
+ prompt
4255
+ }
4256
+ };
4257
+ };
4258
+ var createFrontendCoderAgent = (model, customPrompt, customAppendPrompt) => {
4259
+ const prompt = resolvePrompt(FRONTEND_CODER_PROMPT, customPrompt, customAppendPrompt);
4260
+ return {
4261
+ name: "frontend-coder",
4262
+ description: "Implements frontend features and fixes based on confirmed plans. Follows existing code patterns and project conventions.",
4263
+ config: {
4264
+ model,
4265
+ temperature: 0.1,
4266
+ prompt
4267
+ }
4268
+ };
4269
+ };
4270
+ var createDevopsAgent = (model, customPrompt, customAppendPrompt) => {
4271
+ const prompt = resolvePrompt(DEVOPS_PROMPT, customPrompt, customAppendPrompt);
4272
+ return {
4273
+ name: "devops",
4274
+ description: "Implements DevOps and infrastructure changes based on confirmed plans. Follows existing repo conventions and operational safety practices.",
3884
4275
  config: {
3885
4276
  model,
3886
4277
  temperature: 0.1,
@@ -4027,6 +4418,13 @@ var REVIEWER_PROMPT = `You review code for correctness, security, and quality. Y
4027
4418
  4. Apply the checklist below
4028
4419
  5. Report by severity — CRITICAL first, then HIGH, MEDIUM, PASS
4029
4420
 
4421
+ If the task is UI-heavy and a design artifact is available, include design fidelity checks:
4422
+ - visual hierarchy and spacing consistency
4423
+ - CTA flow quality
4424
+ - responsive behavior
4425
+ - accessibility semantics and states
4426
+ - empty/loading/error/success state coverage
4427
+
4030
4428
  ## Security Checklist — CRITICAL
4031
4429
 
4032
4430
  **Hardcoded credentials:**
@@ -4231,7 +4629,7 @@ Never fabricate information to appear more helpful.
4231
4629
  ## Scope Boundaries
4232
4630
 
4233
4631
  - Report facts only. Do not make implementation decisions.
4234
- - Do not write code unless asked. Return research findings for the coder to act on.
4632
+ - Do not write code unless asked. Return research findings for the implementation agent to act on.
4235
4633
  - If you find a better approach than what was requested, mention it as an option — do not substitute it.
4236
4634
 
4237
4635
  ## Research Areas
@@ -4351,7 +4749,7 @@ var createWriterAgent = (model, customPrompt, customAppendPrompt) => {
4351
4749
  };
4352
4750
 
4353
4751
  // src/agents/security-auditor.ts
4354
- var SECURITY_AUDITOR_PROMPT = `You audit code for security vulnerabilities. You report findings with severity and specific remediation. You do not fix — that is @coder's job.
4752
+ var SECURITY_AUDITOR_PROMPT = `You audit code for security vulnerabilities. You report findings with severity and specific remediation. You do not fix — that is the implementation agent's job (@backend-coder, @frontend-coder, or @devops).
4355
4753
 
4356
4754
  ## Audit Scope
4357
4755
 
@@ -4454,7 +4852,7 @@ For high/critical vulnerabilities: report exact package, CVE ID, and whether it'
4454
4852
 
4455
4853
  ## After Finding Issues
4456
4854
 
4457
- Report only. Do not fix. Tag @coder with specific remediations for each finding.`;
4855
+ Report only. Do not fix. Tag the appropriate implementation agent (@backend-coder, @frontend-coder, or @devops) with specific remediations for each finding.`;
4458
4856
  var createSecurityAuditorAgent = (model, customPrompt, customAppendPrompt) => {
4459
4857
  const prompt = resolvePrompt(SECURITY_AUDITOR_PROMPT, customPrompt, customAppendPrompt);
4460
4858
  return {
@@ -4802,7 +5200,7 @@ request → router → UserController.create() → UserService.create() → ❌
4802
5200
 
4803
5201
  ## Scope
4804
5202
 
4805
- Report only. Do not implement the fix. Tag @coder with the recommended fix.`;
5203
+ Report only. Do not implement the fix. Tag the appropriate implementation agent (@backend-coder, @frontend-coder, or @devops) with the recommended fix.`;
4806
5204
  var BUILD_ERROR_RESOLVER_PROMPT = `You fix build failures. You read the full error output, find the root cause, and apply the minimum fix to get the build green.
4807
5205
 
4808
5206
  ## Diagnostic Commands
@@ -4905,7 +5303,7 @@ npx tsc --noEmit src/path/to/file.ts
4905
5303
 
4906
5304
  - Build fails because of architectural problems → @architect
4907
5305
  - A feature is not working correctly → @debug-specialist
4908
- - Missing functionality needs to be written → @coder`;
5306
+ - Missing functionality needs to be written → @backend-coder/@frontend-coder/@devops`;
4909
5307
  var createDebugSpecialistAgent = (model, customPrompt, customAppendPrompt) => {
4910
5308
  const prompt = resolvePrompt(DEBUG_SPECIALIST_PROMPT, customPrompt, customAppendPrompt);
4911
5309
  return {
@@ -4932,7 +5330,7 @@ var createBuildErrorResolverAgent = (model, customPrompt, customAppendPrompt) =>
4932
5330
  };
4933
5331
 
4934
5332
  // src/agents/specialist.ts
4935
- var TASK_SPLITTER_PROMPT = `You decompose complex tasks into parallel workstreams. You identify dependencies, group independent work into waves, and produce a plan that @parallel-coordinator can execute.
5333
+ var TASK_SPLITTER_PROMPT = `You decompose complex tasks into parallel workstreams. You identify dependencies, group independent work into waves, and produce a plan that @orchestrator can execute.
4936
5334
 
4937
5335
  ## Wave-Structured Output
4938
5336
 
@@ -4942,7 +5340,7 @@ var TASK_SPLITTER_PROMPT = `You decompose complex tasks into parallel workstream
4942
5340
  ### Wave 1 (parallel — start simultaneously)
4943
5341
 
4944
5342
  **Track A — [description]**
4945
- - Agent: @coder
5343
+ - Agent: @backend-coder
4946
5344
  - Files: \`src/auth/user.ts\`, \`src/auth/types.ts\`
4947
5345
  - Task: [specific implementation task]
4948
5346
  - Verify: [how to confirm it's done]
@@ -4962,7 +5360,7 @@ var TASK_SPLITTER_PROMPT = `You decompose complex tasks into parallel workstream
4962
5360
  ### Wave 2 (after Wave 1 completes)
4963
5361
 
4964
5362
  **Track D — Integration**
4965
- - Agent: @coder
5363
+ - Agent: @backend-coder
4966
5364
  - Depends on: Track A, Track C
4967
5365
  - Task: Wire together outputs from Wave 1
4968
5366
 
@@ -5002,7 +5400,9 @@ After Wave 2: @reviewer reviews all changes together
5002
5400
  | Agent | Best For |
5003
5401
  |-------|---------|
5004
5402
  | @architect | Interface contracts, ADRs |
5005
- | @coder | Implementation |
5403
+ | @backend-coder | Backend implementation |
5404
+ | @frontend-coder | Frontend implementation |
5405
+ | @devops | Infrastructure implementation |
5006
5406
  | @researcher | API docs, library research |
5007
5407
  | @tester | Test writing and coverage |
5008
5408
  | @reviewer | Code quality review |
@@ -5140,200 +5540,6 @@ Discussion is complete when:
5140
5540
  - No open questions remain
5141
5541
 
5142
5542
  Report: "Requirements gathering complete. N decisions recorded. Ready for /plan."`;
5143
- var PARALLEL_COORDINATOR_PROMPT = `You orchestrate multi-wave parallel execution. At the start of every job you emit a WAVE TABLE, then delegate agents by wave, wait for wave completion before advancing, and merge outputs when parallel tracks converge.
5144
-
5145
- ## Your Outputs
5146
-
5147
- 1. **WAVE TABLE** — printed at job start, shows every agent slot and its dependencies
5148
- 2. **Agent briefings** — full context packet per agent (they are stateless — give them everything)
5149
- 3. **Wave reports** — status after each wave closes
5150
- 4. **Merge resolution** — reconcile outputs when two tracks touched the same conceptual area
5151
-
5152
- ## WAVE TABLE Format
5153
-
5154
- Print this at the start of every job before delegating any agents:
5155
-
5156
- \`\`\`
5157
- ╔══════════════════════════════════════════════════════════════╗
5158
- ║ WAVE TABLE — [Job Title] ║
5159
- ╠══════════════════════════════════════════════════════════════╣
5160
- ║ Wave 1 (parallel) │ @researcher + @code-explorer ║
5161
- ║ Wave 2 (serial) │ @architect ║
5162
- ║ Wave 3 (parallel) │ @coder + @tester ║
5163
- ║ Wave 4 (parallel) │ @reviewer + @security-auditor ║
5164
- ╠══════════════════════════════════════════════════════════════╣
5165
- ║ Est. sequential: │ 8h ║
5166
- ║ Est. parallel: │ 4.5h ║
5167
- ║ Dependency locks: │ Wave 3 blocked on Wave 2 output ║
5168
- ╚══════════════════════════════════════════════════════════════╝
5169
- \`\`\`
5170
-
5171
- Adjust lanes based on actual task content. Remove any wave whose agents have no work.
5172
-
5173
- ## Standard Wave Delegation Syntax
5174
-
5175
- **Wave 1 — Discovery (parallelize):**
5176
- \`\`\`
5177
- @researcher: [exact research task with sources to check]
5178
- @code-explorer: [exact files/modules to map — list paths]
5179
- \`\`\`
5180
- Start both simultaneously. Do not wait for one before sending the other.
5181
-
5182
- **Wave 2 — Architecture (serial, depends on Wave 1):**
5183
- \`\`\`
5184
- @architect: [design task — attach Wave 1 outputs as context]
5185
- \`\`\`
5186
- One agent. Must complete before Wave 3 starts.
5187
-
5188
- **Wave 3 — Implementation (parallelize, depends on Wave 2):**
5189
- \`\`\`
5190
- @coder: [implementation task — attach @architect output + relevant Wave 1 findings]
5191
- @tester: [test task — attach interface contracts from @architect, NOT @coder output]
5192
- \`\`\`
5193
- Start both simultaneously once Wave 2 output is in hand. @tester works from contracts, not @coder's code, so they are truly parallel.
5194
-
5195
- **Wave 4 — Validation (parallelize):**
5196
- \`\`\`
5197
- @reviewer: [review scope — list files changed by Wave 3]
5198
- @security-auditor: [audit scope — list entry points, auth surfaces, data flows]
5199
- \`\`\`
5200
- Start both once Wave 3 is complete.
5201
-
5202
- ## Parallelism Rules
5203
-
5204
- **Safe to parallelize:**
5205
- - Tasks touching different files with no shared output
5206
- - Research alongside implementation (research produces inputs, not outputs of implementation)
5207
- - Test writing from interface contracts alongside implementation
5208
- - Documentation alongside implementation when writing to different files
5209
-
5210
- **Must be sequential:**
5211
- - Task B's design depends on decisions Task A makes
5212
- - Task B reads a file Task A will write
5213
- - Both tasks modify the same file
5214
-
5215
- **Not worth parallelizing:**
5216
- - Total estimated work is under 20 minutes
5217
- - File ownership is ambiguous — if unclear who owns a file, serialize it
5218
-
5219
- ## Agent Team
5220
-
5221
- | Agent | Best For |
5222
- |-------|---------|
5223
- | @architect | Interface contracts, ADRs, system design |
5224
- | @coder | All code implementation |
5225
- | @researcher | API docs, library usage, best practices |
5226
- | @tester | Test writing and coverage |
5227
- | @reviewer | Code quality review |
5228
- | @security-auditor | Security vulnerability audit |
5229
- | @writer | New documentation |
5230
- | @doc-updater | Updating existing documentation |
5231
- | @code-explorer | Mapping unfamiliar code |
5232
- | @debug-specialist | Root cause analysis |
5233
- | @build-error-resolver | Build and compile failures |
5234
-
5235
- ## Merging Parallel Outputs
5236
-
5237
- When two Wave 3+ agents both worked on the same conceptual area (e.g., both touched auth logic, both proposed an interface for the same type):
5238
-
5239
- **Step 1 — Detect the overlap.** After each wave, compare the file sets each agent reported touching. Any overlap is a merge candidate.
5240
-
5241
- **Step 2 — Classify the overlap:**
5242
- - **Additive** (different functions in the same file): safe to auto-merge, reconcile manually.
5243
- - **Structural** (same type, same interface, same function signature): do not auto-merge — escalate.
5244
- - **Contradictory** (one agent added a field, another removed it): escalate.
5245
-
5246
- **Step 3 — Resolve:**
5247
- - Additive: apply both changesets, verify no symbol collisions, verify tests pass.
5248
- - Structural or contradictory: invoke the conflict resolution protocol below.
5249
-
5250
- ## Conflict Resolution Protocol
5251
-
5252
- Trigger when two tracks produced incompatible changes to the same logical unit.
5253
-
5254
- \`\`\`
5255
- CONFLICT DETECTED
5256
- Track A (@coder): added \`refreshToken: string\` to UserSession in src/types/session.ts
5257
- Track B (@tester): wrote tests assuming UserSession has no refresh field
5258
- Classification: Structural — interface mismatch
5259
-
5260
- RESOLUTION PLAN
5261
- 1. Suspend Track B output (do not apply tests yet)
5262
- 2. Delegate to @coder: reconcile both versions sequentially
5263
- - Brief: "Track A and Track B produced incompatible changes. [Attach both outputs.]
5264
- Produce a single unified version that satisfies both intents."
5265
- 3. Once @coder delivers unified version: re-run @tester against it
5266
- 4. Mark original conflict as resolved, continue to Wave 4
5267
- \`\`\`
5268
-
5269
- Never silently pick one side. Always surface what was lost in the merge and why.
5270
-
5271
- ## Failure Handling
5272
-
5273
- **Wave failure does not block independent waves.**
5274
-
5275
- Before each wave starts, classify each task as:
5276
- - **Blocking** — downstream waves need its output
5277
- - **Independent** — downstream waves do not depend on it
5278
-
5279
- If a blocking task fails:
5280
- \`\`\`
5281
- Wave 1 FAILURE — @researcher: could not retrieve bcrypt API docs
5282
- Impact: Wave 3 @coder task "implement password hashing" is blocked.
5283
- Action: Pause that specific Wave 3 slot. Continue all other Wave 3 slots.
5284
- Retry: Re-run @researcher with a fallback source list, then unblock the Wave 3 slot.
5285
- \`\`\`
5286
-
5287
- If an independent task fails:
5288
- \`\`\`
5289
- Wave 4 FAILURE — @security-auditor: process timed out
5290
- Impact: None — @reviewer completed independently.
5291
- Action: Log failure. Do not block Wave 4 close. Re-run @security-auditor as a follow-up.
5292
- \`\`\`
5293
-
5294
- Wave gates work per-slot, not per-wave: a wave closes when all blocking slots complete. Independent failures are retried async.
5295
-
5296
- ## Full Execution Report Format
5297
-
5298
- \`\`\`markdown
5299
- ## Parallel Execution Report — [Job Title]
5300
-
5301
- ### Wave 1 Results (Discovery)
5302
- | Track | Agent | Status | Output |
5303
- |-------|-------|--------|--------|
5304
- | A | @researcher | ✅ | \`.planning/research/bcrypt.md\` |
5305
- | B | @code-explorer | ✅ | \`.codebase/auth-module-map.md\` |
5306
-
5307
- ### Wave 1 → Wave 2 Gate
5308
- - All blocking slots complete: ✅
5309
- - Merge check: no file conflicts
5310
-
5311
- ### Wave 2 Results (Architecture)
5312
- | Track | Agent | Status | Output |
5313
- |-------|-------|--------|--------|
5314
- | A | @architect | ✅ | \`.planning/adr/auth-design.md\`, interface contracts |
5315
-
5316
- ### Wave 3 Results (Implementation)
5317
- | Track | Agent | Status | Output |
5318
- |-------|-------|--------|--------|
5319
- | A | @coder | ✅ | \`src/auth/service.ts\`, \`src/auth/session.ts\` |
5320
- | B | @tester | ✅ | \`src/auth/service.test.ts\` — 14 tests, 14 passing |
5321
-
5322
- ### Wave 3 Merge Check
5323
- - File overlap: none
5324
- - Conceptual overlap: @coder and @tester both reference UserSession — compatible ✅
5325
-
5326
- ### Wave 4 Results (Validation)
5327
- | Track | Agent | Status | Output |
5328
- |-------|-------|--------|--------|
5329
- | A | @reviewer | ✅ | 2 non-blocking suggestions filed |
5330
- | B | @security-auditor | ⚠️ FAILED | Timeout — retrying async |
5331
-
5332
- ### Final Status
5333
- - All blocking work complete ✅
5334
- - @security-auditor re-run scheduled as follow-up
5335
- - Elapsed: 4h 20m (vs 8h sequential)
5336
- \`\`\``;
5337
5543
  var createTaskSplitterAgent = (model, customPrompt, customAppendPrompt) => {
5338
5544
  const prompt = resolvePrompt(TASK_SPLITTER_PROMPT, customPrompt, customAppendPrompt);
5339
5545
  return {
@@ -5358,18 +5564,6 @@ var createDiscusserAgent = (model, customPrompt, customAppendPrompt) => {
5358
5564
  }
5359
5565
  };
5360
5566
  };
5361
- var createParallelCoordinatorAgent = (model, customPrompt, customAppendPrompt) => {
5362
- const prompt = resolvePrompt(PARALLEL_COORDINATOR_PROMPT, customPrompt, customAppendPrompt);
5363
- return {
5364
- name: "parallel-coordinator",
5365
- description: "Coordinates parallel agent execution for multi-track workstreams. Manages wave execution, handles merge conflicts, and maximizes throughput.",
5366
- config: {
5367
- model,
5368
- temperature: 0.1,
5369
- prompt
5370
- }
5371
- };
5372
- };
5373
5567
 
5374
5568
  // src/agents/architect.ts
5375
5569
  var ARCHITECT_PROMPT = `You design system architecture, create Architecture Decision Records (ADRs), and define API contracts before implementation begins.
@@ -6016,11 +6210,81 @@ function createAutoLearnerAgent(model) {
6016
6210
  return definition;
6017
6211
  }
6018
6212
 
6213
+ // src/agents/design.ts
6214
+ var DESIGN_PROMPT = `You are the dedicated design architect for user-facing products. Your work is mandatory before coding for UI-heavy tasks unless an explicit override is recorded.
6215
+
6216
+ ## Scope
6217
+
6218
+ Use this workflow for website, web app, mobile app, dashboard, admin panel, landing page, SaaS interface, onboarding UX, and other user-facing surfaces.
6219
+
6220
+ ## Required Execution Stages
6221
+
6222
+ For UI-heavy tasks, produce all stages in order:
6223
+ 1. discovery
6224
+ 2. ux_planning
6225
+ 3. wireframe_layout
6226
+ 4. visual_system_definition
6227
+ 5. design_approval
6228
+ 6. implementation_handoff
6229
+
6230
+ Do not skip or merge stages.
6231
+
6232
+ ## Structured Output Contract
6233
+
6234
+ Always return machine-readable markdown sections with these keys:
6235
+ - task_type
6236
+ - user_goals
6237
+ - target_audience
6238
+ - core_user_flows
6239
+ - page_map_or_screen_map
6240
+ - section_structure
6241
+ - layout_plan
6242
+ - component_list
6243
+ - state_list (loading, empty, error, success)
6244
+ - responsive_behavior_notes
6245
+ - visual_direction
6246
+ - design_tokens_guidance
6247
+ - accessibility_notes
6248
+ - implementation_handoff_checklist
6249
+ - approval_status
6250
+
6251
+ ## Design Review Mode
6252
+
6253
+ When asked to review an implemented UI, compare against approved design artifacts and report:
6254
+ - design mismatches
6255
+ - hierarchy issues
6256
+ - spacing inconsistency
6257
+ - weak call-to-action flow
6258
+ - responsiveness issues
6259
+ - accessibility concerns
6260
+ - component inconsistency
6261
+ - missing empty/error states
6262
+
6263
+ ## Constraints
6264
+
6265
+ - Do not write implementation code in design mode.
6266
+ - Do not claim approval without explicit pass/fail rationale.
6267
+ - Keep output concise but complete enough for frontend handoff.`;
6268
+ var createDesignAgent = (model, customPrompt, customAppendPrompt) => {
6269
+ const prompt = resolvePrompt(DESIGN_PROMPT, customPrompt, customAppendPrompt);
6270
+ return {
6271
+ name: "design",
6272
+ description: "Design-first specialist for UX structure, wireframe planning, visual system definition, and frontend handoff before implementation.",
6273
+ config: {
6274
+ model,
6275
+ temperature: 0.1,
6276
+ prompt
6277
+ }
6278
+ };
6279
+ };
6280
+
6019
6281
  // src/agents/index.ts
6020
6282
  var AGENT_NAMES = [
6021
6283
  "orchestrator",
6022
6284
  "planner",
6023
- "coder",
6285
+ "backend-coder",
6286
+ "frontend-coder",
6287
+ "devops",
6024
6288
  "plan-checker",
6025
6289
  "tester",
6026
6290
  "reviewer",
@@ -6034,13 +6298,13 @@ var AGENT_NAMES = [
6034
6298
  "build-error-resolver",
6035
6299
  "task-splitter",
6036
6300
  "discusser",
6037
- "parallel-coordinator",
6038
6301
  "architect",
6039
6302
  "risk-analyst",
6040
6303
  "policy-enforcer",
6041
6304
  "performance-optimizer",
6042
6305
  "refactor-guide",
6043
- "auto-learner"
6306
+ "auto-learner",
6307
+ "design"
6044
6308
  ];
6045
6309
  var PRIMARY_AGENTS = new Set(["orchestrator"]);
6046
6310
  var ALL_MODES_AGENTS = new Set;
@@ -6060,8 +6324,12 @@ function createAgent(name, model, customPrompt, customAppendPrompt) {
6060
6324
  return createOrchestratorAgent(model, customPrompt, customAppendPrompt);
6061
6325
  case "planner":
6062
6326
  return createPlannerAgent(model, customPrompt, customAppendPrompt);
6063
- case "coder":
6064
- return createCoderAgent(model, customPrompt, customAppendPrompt);
6327
+ case "backend-coder":
6328
+ return createBackendCoderAgent(model, customPrompt, customAppendPrompt);
6329
+ case "frontend-coder":
6330
+ return createFrontendCoderAgent(model, customPrompt, customAppendPrompt);
6331
+ case "devops":
6332
+ return createDevopsAgent(model, customPrompt, customAppendPrompt);
6065
6333
  case "plan-checker":
6066
6334
  return createPlanCheckerAgent(model, customPrompt, customAppendPrompt);
6067
6335
  case "tester":
@@ -6088,8 +6356,6 @@ function createAgent(name, model, customPrompt, customAppendPrompt) {
6088
6356
  return createTaskSplitterAgent(model, customPrompt, customAppendPrompt);
6089
6357
  case "discusser":
6090
6358
  return createDiscusserAgent(model, customPrompt, customAppendPrompt);
6091
- case "parallel-coordinator":
6092
- return createParallelCoordinatorAgent(model, customPrompt, customAppendPrompt);
6093
6359
  case "architect":
6094
6360
  return createArchitectAgent(model, customPrompt, customAppendPrompt);
6095
6361
  case "risk-analyst":
@@ -6102,6 +6368,8 @@ function createAgent(name, model, customPrompt, customAppendPrompt) {
6102
6368
  return createRefactorGuideAgent(model, customPrompt, customAppendPrompt);
6103
6369
  case "auto-learner":
6104
6370
  return createAutoLearnerAgent(model);
6371
+ case "design":
6372
+ return createDesignAgent(model, customPrompt, customAppendPrompt);
6105
6373
  default:
6106
6374
  console.warn(`[flowdeck] Unknown agent: ${name}`);
6107
6375
  return;
@@ -6139,42 +6407,16 @@ function getAgentConfigs(agentModels) {
6139
6407
  return configs;
6140
6408
  }
6141
6409
 
6142
- // src/config/loader.ts
6143
- import { existsSync as existsSync27, readFileSync as readFileSync23 } from "fs";
6144
- import { join as join26 } from "path";
6145
- import { homedir as homedir3 } from "os";
6146
- var CONFIG_FILENAME = "flowdeck.json";
6147
- function getGlobalConfigDir() {
6148
- return process.env.OPENCODE_CONFIG_DIR || (process.env.XDG_CONFIG_HOME ? join26(process.env.XDG_CONFIG_HOME, "opencode") : join26(homedir3(), ".config", "opencode"));
6149
- }
6150
- function loadFlowDeckConfig(directory) {
6151
- const candidates = [];
6152
- if (directory) {
6153
- candidates.push(join26(directory, ".opencode", CONFIG_FILENAME));
6154
- }
6155
- candidates.push(join26(getGlobalConfigDir(), CONFIG_FILENAME));
6156
- for (const configPath of candidates) {
6157
- if (existsSync27(configPath)) {
6158
- try {
6159
- const content = readFileSync23(configPath, "utf-8");
6160
- return JSON.parse(content);
6161
- } catch {
6162
- console.warn(`[flowdeck] Failed to load config from ${configPath}`);
6163
- }
6164
- }
6165
- }
6166
- return {};
6167
- }
6168
6410
  // src/index.ts
6169
6411
  function loadRulePaths() {
6170
6412
  const __dir = dirname4(fileURLToPath2(import.meta.url));
6171
- const rulesDir = join27(__dir, "..", "src", "rules");
6172
- if (!existsSync28(rulesDir))
6413
+ const rulesDir = join26(__dir, "..", "src", "rules");
6414
+ if (!existsSync27(rulesDir))
6173
6415
  return [];
6174
6416
  const paths = [];
6175
6417
  function walk(dir) {
6176
6418
  for (const entry of readdirSync3(dir, { withFileTypes: true })) {
6177
- const full = join27(dir, entry.name);
6419
+ const full = join26(dir, entry.name);
6178
6420
  if (entry.isDirectory()) {
6179
6421
  walk(full);
6180
6422
  } else if (entry.isFile() && entry.name.endsWith(".md") && entry.name !== "README.md") {
@@ -6187,8 +6429,8 @@ function loadRulePaths() {
6187
6429
  }
6188
6430
  function loadCommands() {
6189
6431
  const __dir = dirname4(fileURLToPath2(import.meta.url));
6190
- const commandsDir = join27(__dir, "..", "src", "commands");
6191
- if (!existsSync28(commandsDir))
6432
+ const commandsDir = join26(__dir, "..", "src", "commands");
6433
+ if (!existsSync27(commandsDir))
6192
6434
  return {};
6193
6435
  const commands = {};
6194
6436
  try {
@@ -6196,7 +6438,7 @@ function loadCommands() {
6196
6438
  if (!file.endsWith(".md"))
6197
6439
  continue;
6198
6440
  const name = basename(file, ".md");
6199
- const raw = readFileSync24(join27(commandsDir, file), "utf-8");
6441
+ const raw = readFileSync23(join26(commandsDir, file), "utf-8");
6200
6442
  let description;
6201
6443
  let template = raw;
6202
6444
  const fmMatch = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/);
@@ -6237,12 +6479,16 @@ var plugin = async (input, _options) => {
6237
6479
  cfg.default_agent = "orchestrator";
6238
6480
  }
6239
6481
  const flowdeckConfig = loadFlowDeckConfig(directory);
6482
+ const designFirstConfig = resolveDesignFirstConfig(flowdeckConfig);
6240
6483
  const agentModels = {};
6241
6484
  for (const [name, agentCfg] of Object.entries(flowdeckConfig.agents ?? {})) {
6242
6485
  if (agentCfg.model) {
6243
6486
  agentModels[name] = agentCfg.model;
6244
6487
  }
6245
6488
  }
6489
+ if (designFirstConfig.modelOverrides.design) {
6490
+ agentModels.design = designFirstConfig.modelOverrides.design;
6491
+ }
6246
6492
  const resolvedAgentConfigs = getAgentConfigs(agentModels);
6247
6493
  if (!cfg.agent) {
6248
6494
  cfg.agent = { ...resolvedAgentConfigs };
@@ -6273,8 +6519,8 @@ var plugin = async (input, _options) => {
6273
6519
  }
6274
6520
  }
6275
6521
  }
6276
- const skillsDir = join27(dirname4(fileURLToPath2(import.meta.url)), "..", "src", "skills");
6277
- if (existsSync28(skillsDir)) {
6522
+ const skillsDir = join26(dirname4(fileURLToPath2(import.meta.url)), "..", "src", "skills");
6523
+ if (existsSync27(skillsDir)) {
6278
6524
  const cfgAny = cfg;
6279
6525
  if (!cfgAny.skills || typeof cfgAny.skills !== "object") {
6280
6526
  cfgAny.skills = { paths: [] };
@@ -6351,7 +6597,7 @@ var plugin = async (input, _options) => {
6351
6597
  const delEvent = event?.event ?? event;
6352
6598
  const sessionId = delEvent?.sessionID ?? delEvent?.sessionId ?? "";
6353
6599
  if (sessionId) {
6354
- memoryHook.clearSession(sessionId);
6600
+ memoryHook.onSessionEnd(sessionId);
6355
6601
  }
6356
6602
  }
6357
6603
  } catch (err) {