@dv.nghiem/flowdeck 0.3.4 → 0.3.5

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 (95) 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/orchestrator-guard-hook.d.ts.map +1 -1
  23. package/dist/hooks/tool-guard.d.ts.map +1 -1
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +490 -418
  26. package/dist/services/agent-contract-registry.d.ts +32 -0
  27. package/dist/services/agent-contract-registry.d.ts.map +1 -0
  28. package/dist/services/agent-performance.d.ts +1 -1
  29. package/dist/services/agent-performance.d.ts.map +1 -1
  30. package/dist/services/agent-trace-graph.d.ts +94 -0
  31. package/dist/services/agent-trace-graph.d.ts.map +1 -0
  32. package/dist/services/agent-validator.d.ts +56 -0
  33. package/dist/services/agent-validator.d.ts.map +1 -0
  34. package/dist/services/deadlock-detector.d.ts +34 -0
  35. package/dist/services/deadlock-detector.d.ts.map +1 -0
  36. package/dist/services/delegation-budget.d.ts +54 -0
  37. package/dist/services/delegation-budget.d.ts.map +1 -0
  38. package/dist/services/governance.test.d.ts +11 -0
  39. package/dist/services/governance.test.d.ts.map +1 -0
  40. package/dist/services/index.d.ts +6 -1
  41. package/dist/services/index.d.ts.map +1 -1
  42. package/dist/services/telemetry.d.ts +1 -1
  43. package/dist/services/telemetry.d.ts.map +1 -1
  44. package/dist/services/workflow-scorecard.d.ts +76 -0
  45. package/dist/services/workflow-scorecard.d.ts.map +1 -0
  46. package/dist/tools/delegate.d.ts.map +1 -1
  47. package/dist/tools/dispatch-routing.d.ts +4 -1
  48. package/dist/tools/dispatch-routing.d.ts.map +1 -1
  49. package/dist/tools/dispatch-routing.test.d.ts +2 -0
  50. package/dist/tools/dispatch-routing.test.d.ts.map +1 -0
  51. package/dist/tools/planning-state-lib.d.ts +8 -0
  52. package/dist/tools/planning-state-lib.d.ts.map +1 -1
  53. package/dist/tools/planning-state.d.ts.map +1 -1
  54. package/dist/tools/run-pipeline.d.ts.map +1 -1
  55. package/docs/agents.md +104 -74
  56. package/docs/best-practices.md +1 -1
  57. package/docs/commands/fd-ask.md +2 -2
  58. package/docs/commands/fd-fix-bug.md +2 -2
  59. package/docs/commands/fd-new-feature.md +2 -2
  60. package/docs/commands/fd-quick.md +3 -1
  61. package/docs/commands.md +37 -7
  62. package/docs/configuration.md +76 -46
  63. package/docs/design-first-workflow.md +94 -0
  64. package/docs/feature-integration-architecture.md +3 -31
  65. package/docs/index.md +5 -2
  66. package/docs/intelligence.md +92 -1
  67. package/docs/multi-repo.md +1 -1
  68. package/docs/rules.md +1 -1
  69. package/docs/skills.md +24 -15
  70. package/docs/workflows.md +11 -6
  71. package/package.json +1 -1
  72. package/src/commands/fd-ask.md +1 -0
  73. package/src/commands/fd-design.md +64 -0
  74. package/src/commands/fd-discuss.md +2 -0
  75. package/src/commands/fd-execute.md +7 -3
  76. package/src/commands/fd-fix-bug.md +2 -2
  77. package/src/commands/fd-multi-repo.md +3 -3
  78. package/src/commands/fd-plan.md +2 -0
  79. package/src/commands/fd-quick.md +4 -1
  80. package/src/commands/fd-verify.md +6 -0
  81. package/src/rules/common/agent-orchestration.md +6 -6
  82. package/src/skills/app-shell-design/SKILL.md +31 -0
  83. package/src/skills/dashboard-design/SKILL.md +32 -0
  84. package/src/skills/decision-trace/SKILL.md +1 -1
  85. package/src/skills/design-audit/SKILL.md +37 -0
  86. package/src/skills/design-system-definition/SKILL.md +33 -0
  87. package/src/skills/frontend-handoff/SKILL.md +31 -0
  88. package/src/skills/landing-page-design/SKILL.md +32 -0
  89. package/src/skills/multi-repo/SKILL.md +3 -3
  90. package/src/skills/plan-task/SKILL.md +2 -2
  91. package/src/skills/responsive-review/SKILL.md +31 -0
  92. package/src/skills/ui-ux-planning/SKILL.md +32 -0
  93. package/src/skills/wireframe-planning/SKILL.md +30 -0
  94. package/dist/services/model-router.d.ts +0 -35
  95. 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,13 +1706,13 @@ 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");
1712
+ var MEMORY_DIR = join15(homedir(), ".flowdeck-memory");
1713
+ var DB_PATH = join15(MEMORY_DIR, "memory.db");
1668
1714
  function ensureDir() {
1669
- if (!existsSync16(MEMORY_DIR)) {
1715
+ if (!existsSync15(MEMORY_DIR)) {
1670
1716
  mkdirSync10(MEMORY_DIR, { recursive: true });
1671
1717
  }
1672
1718
  }
@@ -1945,16 +1991,16 @@ var memorySearchTool = tool16({
1945
1991
  // src/tools/memory-status.ts
1946
1992
  import { tool as tool17 } from "@opencode-ai/plugin";
1947
1993
  import { Database as Database2 } from "bun:sqlite";
1948
- import { existsSync as existsSync17 } from "fs";
1949
- import { join as join17 } from "path";
1994
+ import { existsSync as existsSync16 } from "fs";
1995
+ import { join as join16 } from "path";
1950
1996
  import { homedir as homedir2 } from "os";
1951
- var DB_PATH2 = join17(homedir2(), ".flowdeck-memory", "memory.db");
1997
+ var DB_PATH2 = join16(homedir2(), ".flowdeck-memory", "memory.db");
1952
1998
  var memoryStatusTool = tool17({
1953
1999
  description: "Check FlowDeck memory database status, statistics, and recent sessions",
1954
2000
  args: {},
1955
2001
  async execute(_args, _context) {
1956
2002
  try {
1957
- const exists = existsSync17(DB_PATH2);
2003
+ const exists = existsSync16(DB_PATH2);
1958
2004
  const result = {
1959
2005
  database_exists: exists,
1960
2006
  path: DB_PATH2,
@@ -2113,6 +2159,49 @@ var memoryHook = {
2113
2159
  // src/hooks/guard-rails.ts
2114
2160
  import { existsSync as existsSync18, readFileSync as readFileSync15 } from "fs";
2115
2161
  import { join as join18 } from "path";
2162
+
2163
+ // src/config/loader.ts
2164
+ import { existsSync as existsSync17, readFileSync as readFileSync14 } from "fs";
2165
+ import { join as join17 } from "path";
2166
+ import { homedir as homedir3 } from "os";
2167
+ var CONFIG_FILENAME = "flowdeck.json";
2168
+ function getGlobalConfigDir() {
2169
+ return process.env.OPENCODE_CONFIG_DIR || (process.env.XDG_CONFIG_HOME ? join17(process.env.XDG_CONFIG_HOME, "opencode") : join17(homedir3(), ".config", "opencode"));
2170
+ }
2171
+ function loadFlowDeckConfig(directory) {
2172
+ const candidates = [];
2173
+ if (directory) {
2174
+ candidates.push(join17(directory, ".opencode", CONFIG_FILENAME));
2175
+ }
2176
+ candidates.push(join17(getGlobalConfigDir(), CONFIG_FILENAME));
2177
+ for (const configPath of candidates) {
2178
+ if (existsSync17(configPath)) {
2179
+ try {
2180
+ const content = readFileSync14(configPath, "utf-8");
2181
+ return JSON.parse(content);
2182
+ } catch {
2183
+ console.warn(`[flowdeck] Failed to load config from ${configPath}`);
2184
+ }
2185
+ }
2186
+ }
2187
+ return {};
2188
+ }
2189
+ function resolveDesignFirstConfig(config) {
2190
+ return {
2191
+ enabled: config.designFirst?.enabled ?? true,
2192
+ enforcement: config.designFirst?.enforcement ?? "strict",
2193
+ requireApprovalBeforeImplementation: config.designFirst?.requireApprovalBeforeImplementation ?? true,
2194
+ modelOverrides: config.designFirst?.modelOverrides ?? {},
2195
+ defaultSkillsByTaskType: config.designFirst?.defaultSkillsByTaskType ?? {
2196
+ "landing-page": ["landing-page-design", "wireframe-planning", "design-system-definition", "frontend-handoff"],
2197
+ dashboard: ["dashboard-design", "ui-ux-planning", "wireframe-planning", "responsive-review"],
2198
+ "admin-panel": ["ui-ux-planning", "wireframe-planning", "design-system-definition", "frontend-handoff"],
2199
+ "app-screen": ["app-shell-design", "ui-ux-planning", "wireframe-planning", "responsive-review"],
2200
+ "general-ui": ["ui-ux-planning", "wireframe-planning", "design-system-definition", "frontend-handoff"]
2201
+ }
2202
+ };
2203
+ }
2204
+ // src/hooks/guard-rails.ts
2116
2205
  var PLANNING_DIR2 = ".planning";
2117
2206
  var CONFIG_FILE = "config.json";
2118
2207
  var STATE_FILE2 = "STATE.md";
@@ -2198,6 +2287,10 @@ async function guardRailsHook(ctx, input, _output) {
2198
2287
  if (execMode === "guarded") {
2199
2288
  throw new Error(`[flowdeck] GUARDED MODE: edit will proceed but flag for human review.`);
2200
2289
  }
2290
+ const designGateMessage = getDesignGateMessage(dir);
2291
+ if (designGateMessage) {
2292
+ throw new Error(designGateMessage);
2293
+ }
2201
2294
  const effectiveSeverity = getEffectiveSeverity(configPath, statePath2);
2202
2295
  if (effectiveSeverity === null)
2203
2296
  return;
@@ -2220,6 +2313,31 @@ async function guardRailsHook(ctx, input, _output) {
2220
2313
  }
2221
2314
  }
2222
2315
  }
2316
+ function getDesignGateMessage(dir) {
2317
+ const designConfig = resolveDesignFirstConfig(loadFlowDeckConfig(dir));
2318
+ if (!designConfig.enabled || !designConfig.requireApprovalBeforeImplementation)
2319
+ return null;
2320
+ const state = readPlanningState(dir);
2321
+ if (state.design_override && state.design_override_reason && state.design_override_reason.trim().length > 0)
2322
+ return null;
2323
+ const designApproved = state.design_stage === "handoff_complete" && state.design_approved;
2324
+ if (state.requires_design_first || state.task_type && isUiHeavyTask(state.task_type) || planSuggestsUiHeavy(dir, state.phase || 1)) {
2325
+ if (designApproved)
2326
+ return null;
2327
+ if (designConfig.enforcement === "advisory") {
2328
+ return "[flowdeck] WARNING: UI-heavy task detected without approved design handoff. Run /fd-design --mode=draft first.";
2329
+ }
2330
+ return "[flowdeck] BLOCK: UI-heavy task requires approved design handoff. Run /fd-design --mode=draft or set explicit design override in STATE.md.";
2331
+ }
2332
+ return null;
2333
+ }
2334
+ function planSuggestsUiHeavy(dir, phase) {
2335
+ const planPath = phasePlanPath(dir, phase);
2336
+ if (!existsSync18(planPath))
2337
+ return false;
2338
+ const planContent = readFileSync15(planPath, "utf-8");
2339
+ return isUiHeavyTask(planContent);
2340
+ }
2223
2341
  function effectiveSeverity(configPath, statePath2) {
2224
2342
  if (existsSync18(configPath)) {
2225
2343
  try {
@@ -2332,12 +2450,37 @@ function checkArchConstraint(directory, filePath) {
2332
2450
  function checkPhaseEnforcement(directory) {
2333
2451
  try {
2334
2452
  const state = readPlanningState(directory);
2453
+ const flowdeckConfig = resolveDesignFirstConfig(loadFlowDeckConfig(directory));
2335
2454
  if (state.phase > 0 && state.phase < 3) {
2336
2455
  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
2456
  }
2457
+ if (flowdeckConfig.enabled && flowdeckConfig.requireApprovalBeforeImplementation && isUiDesignApprovalRequired(directory)) {
2458
+ if (flowdeckConfig.enforcement === "advisory") {
2459
+ return `FLOWDECK [design-gate]: advisory design-first mode detected missing approval. Run /fd-design --mode=draft or set design_override=true in STATE.md.`;
2460
+ }
2461
+ 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.`;
2462
+ }
2338
2463
  } catch {}
2339
2464
  return null;
2340
2465
  }
2466
+ function isUiDesignApprovalRequired(directory) {
2467
+ const state = readPlanningState(directory);
2468
+ if (state.design_override && state.design_override_reason && state.design_override_reason.trim().length > 0)
2469
+ return false;
2470
+ if (state.requires_design_first) {
2471
+ return !(state.design_stage === "handoff_complete" && state.design_approved);
2472
+ }
2473
+ if (state.task_type && isUiHeavyTask(state.task_type)) {
2474
+ return !(state.design_stage === "handoff_complete" && state.design_approved);
2475
+ }
2476
+ const planPath = phasePlanPath(directory, state.phase || 1);
2477
+ if (!existsSync19(planPath))
2478
+ return false;
2479
+ const planContent = readFileSync16(planPath, "utf-8");
2480
+ if (!isUiHeavyTask(planContent))
2481
+ return false;
2482
+ return !(state.design_stage === "handoff_complete" && state.design_approved);
2483
+ }
2341
2484
  async function toolGuardHook(ctx, input, output) {
2342
2485
  if (!IS_ENABLED())
2343
2486
  return;
@@ -3106,7 +3249,9 @@ function blockMessage(toolName) {
3106
3249
  ` + `The orchestrator is a coordinator — it must delegate all implementation work.
3107
3250
 
3108
3251
  ` + `Use the \`delegate\` tool to hand this off:
3109
- ` + ` delegate({ agent: "@coder", prompt: "..." }) — code writing / editing
3252
+ ` + ` delegate({ agent: "@backend-coder", prompt: "..." }) — backend code writing / editing
3253
+ ` + ` delegate({ agent: "@frontend-coder", prompt: "..." }) — frontend code writing / editing
3254
+ ` + ` delegate({ agent: "@devops", prompt: "..." }) — CI/CD, deploy, and infra changes
3110
3255
  ` + ` delegate({ agent: "@mapper", prompt: "..." }) — codebase mapping
3111
3256
  ` + ` delegate({ agent: "@researcher", prompt: "..." }) — research / file analysis
3112
3257
  ` + ` delegate({ agent: "@tester", prompt: "..." }) — tests / commands
@@ -3308,11 +3453,22 @@ For each incomplete step in PLAN.md:
3308
3453
  5. Re-read STATE.md to confirm state
3309
3454
  6. Move to the next incomplete step
3310
3455
 
3456
+ ## Implementation Routing
3457
+
3458
+ When a plan step requires implementation, route to a role-specific agent:
3459
+ - Use @backend-coder for server, API, business logic, database, and non-UI application code.
3460
+ - Use @frontend-coder for UI components, client state, styling, and interaction behavior.
3461
+ - Use @devops for CI/CD workflows, deployment, infrastructure, runtime config, and operations scripts.
3462
+ - If a step mixes multiple domains, split it into multiple delegated tasks by domain.
3463
+
3311
3464
  ## Agent Team
3312
3465
 
3313
3466
  | Agent | Invoke | Best For |
3314
3467
  |-------|--------|----------|
3315
- | Coder | @coder | All code implementation |
3468
+ | Design | @design | Discovery, UX planning, wireframes, visual system, implementation handoff, design fidelity review |
3469
+ | Backend Coder | @backend-coder | Backend code implementation |
3470
+ | Frontend Coder | @frontend-coder | Frontend code implementation |
3471
+ | DevOps | @devops | CI/CD and infrastructure implementation |
3316
3472
  | Researcher | @researcher | API docs, library usage |
3317
3473
  | Tester | @tester | Writing and running tests |
3318
3474
  | Reviewer | @reviewer | Code quality review |
@@ -3323,7 +3479,6 @@ For each incomplete step in PLAN.md:
3323
3479
  | Code Explorer | @code-explorer | Reading unfamiliar code |
3324
3480
  | Debug Specialist | @debug-specialist | Root cause analysis |
3325
3481
  | Build Resolver | @build-error-resolver | Build/compile failures |
3326
- | Parallel Coordinator | @parallel-coordinator | Multi-track parallel work |
3327
3482
  | Doc Updater | @doc-updater | Updating existing docs |
3328
3483
  | Task Splitter | @task-splitter | Decomposing complex tasks |
3329
3484
  | Discusser | @discusser | Requirements extraction |
@@ -3336,12 +3491,13 @@ For each incomplete step in PLAN.md:
3336
3491
  ## Phase State Machine
3337
3492
 
3338
3493
  \`\`\`
3339
- discuss → plan → execute → review
3494
+ discuss → plan → design (for UI-heavy tasks) → execute → review
3340
3495
  \`\`\`
3341
3496
 
3342
3497
  - **discuss**: Requirements extraction with @discusser
3343
3498
  - **plan**: Plan creation with @planner, review with @plan-checker
3344
- - **execute**: Implementation with @coder, @tester, @researcher in parallel where possible
3499
+ - **design**: UX structure, wireframe/layout planning, and visual system definition with @design
3500
+ - **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
3501
  - **review**: Review with @reviewer, @security-auditor
3346
3502
 
3347
3503
  ## Tracking
@@ -3363,7 +3519,7 @@ If a delegated agent fails:
3363
3519
  3. If still failing, escalate:
3364
3520
 
3365
3521
  \`\`\`
3366
- BLOCKED: @coder failed on step 3 (add payment endpoint).
3522
+ BLOCKED: implementation agent failed on step 3 (add payment endpoint).
3367
3523
  Error: [exact error message]
3368
3524
  Retried once with clarification. Still failing.
3369
3525
 
@@ -3384,11 +3540,26 @@ When a task required unusual human guidance, a novel solution strategy, or expos
3384
3540
 
3385
3541
  Do NOT create a skill for routine tasks. Only capture genuinely novel or reusable patterns.`;
3386
3542
  var AGENT_DESCRIPTIONS = {
3387
- coder: `@coder
3388
- - Role: Implements features and fixes based on confirmed plans
3543
+ design: `@design
3544
+ - Role: Runs design-first workflow for user-facing tasks
3545
+ - Permissions: Read/write files
3546
+ - Best for: UX structure, wireframes, visual direction, tokens, and frontend handoff
3547
+ - **Delegate when:** Task includes website/app/dashboard/admin/user-facing UI work`,
3548
+ "backend-coder": `@backend-coder
3549
+ - Role: Implements backend features and fixes based on confirmed plans
3389
3550
  - Permissions: Read/write files
3390
- - Best for: All code implementation tasks
3391
- - **Delegate when:** Implementation work, following a plan`,
3551
+ - Best for: API, services, data layer, and business logic
3552
+ - **Delegate when:** Backend or server-side implementation work`,
3553
+ "frontend-coder": `@frontend-coder
3554
+ - Role: Implements frontend features and fixes based on confirmed plans
3555
+ - Permissions: Read/write files
3556
+ - Best for: UI components, client state, rendering, and interaction behavior
3557
+ - **Delegate when:** Frontend implementation work`,
3558
+ devops: `@devops
3559
+ - Role: Implements DevOps and infrastructure changes based on confirmed plans
3560
+ - Permissions: Read/write files
3561
+ - Best for: CI/CD, deployment config, infra scripts, and runtime operations
3562
+ - **Delegate when:** Infrastructure, pipeline, or operations implementation work`,
3392
3563
  researcher: `@researcher
3393
3564
  - Role: Researches documentation, APIs, and best practices
3394
3565
  - Permissions: Read files
@@ -3460,11 +3631,6 @@ var AGENT_DESCRIPTIONS = {
3460
3631
  - Permissions: Read/write files
3461
3632
  - Best for: Requirements extraction
3462
3633
  - **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
3634
  planner: `@planner
3469
3635
  - Role: Creates detailed implementation plans
3470
3636
  - Permissions: Read files
@@ -3766,7 +3932,7 @@ var createPlanCheckerAgent = (model, customPrompt, customAppendPrompt) => {
3766
3932
  };
3767
3933
 
3768
3934
  // src/agents/coder.ts
3769
- var CODER_PROMPT = `You implement features and fix bugs. You follow the plan exactly. You do not invent requirements.
3935
+ var BASE_IMPLEMENTER_PROMPT = `You implement features and fix bugs. You follow the plan exactly. You do not invent requirements.
3770
3936
 
3771
3937
  ## Before Writing Code
3772
3938
 
@@ -3876,11 +4042,62 @@ After implementing, report:
3876
4042
  - Tests added or updated
3877
4043
  - Any deviations from the plan and why
3878
4044
  - Next step ready to execute`;
3879
- var createCoderAgent = (model, customPrompt, customAppendPrompt) => {
3880
- const prompt = resolvePrompt(CODER_PROMPT, customPrompt, customAppendPrompt);
4045
+ var BACKEND_CODER_PROMPT = `${BASE_IMPLEMENTER_PROMPT}
4046
+
4047
+ ## Domain Focus
4048
+
4049
+ Prioritize backend and platform code:
4050
+ - Server handlers, services, repositories, jobs, and business logic
4051
+ - Database and persistence-layer changes
4052
+ - API contracts and boundary validation
4053
+ `;
4054
+ var FRONTEND_CODER_PROMPT = `${BASE_IMPLEMENTER_PROMPT}
4055
+
4056
+ ## Domain Focus
4057
+
4058
+ Prioritize frontend implementation quality:
4059
+ - UI components, client state, accessibility, and interaction behavior
4060
+ - Styling consistency with existing design system/tokens
4061
+ - Browser/runtime safety (no server-only assumptions in client code)
4062
+ `;
4063
+ var DEVOPS_PROMPT = `${BASE_IMPLEMENTER_PROMPT}
4064
+
4065
+ ## Domain Focus
4066
+
4067
+ Prioritize infrastructure and delivery tasks:
4068
+ - CI/CD workflows, build pipelines, deployment configuration
4069
+ - Environment/runtime configuration and operational scripts
4070
+ - Reliability and rollback safety for production-facing changes
4071
+ `;
4072
+ var createBackendCoderAgent = (model, customPrompt, customAppendPrompt) => {
4073
+ const prompt = resolvePrompt(BACKEND_CODER_PROMPT, customPrompt, customAppendPrompt);
4074
+ return {
4075
+ name: "backend-coder",
4076
+ description: "Implements backend features and fixes based on confirmed plans. Follows existing code patterns and project conventions.",
4077
+ config: {
4078
+ model,
4079
+ temperature: 0.1,
4080
+ prompt
4081
+ }
4082
+ };
4083
+ };
4084
+ var createFrontendCoderAgent = (model, customPrompt, customAppendPrompt) => {
4085
+ const prompt = resolvePrompt(FRONTEND_CODER_PROMPT, customPrompt, customAppendPrompt);
3881
4086
  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.",
4087
+ name: "frontend-coder",
4088
+ description: "Implements frontend features and fixes based on confirmed plans. Follows existing code patterns and project conventions.",
4089
+ config: {
4090
+ model,
4091
+ temperature: 0.1,
4092
+ prompt
4093
+ }
4094
+ };
4095
+ };
4096
+ var createDevopsAgent = (model, customPrompt, customAppendPrompt) => {
4097
+ const prompt = resolvePrompt(DEVOPS_PROMPT, customPrompt, customAppendPrompt);
4098
+ return {
4099
+ name: "devops",
4100
+ description: "Implements DevOps and infrastructure changes based on confirmed plans. Follows existing repo conventions and operational safety practices.",
3884
4101
  config: {
3885
4102
  model,
3886
4103
  temperature: 0.1,
@@ -4027,6 +4244,13 @@ var REVIEWER_PROMPT = `You review code for correctness, security, and quality. Y
4027
4244
  4. Apply the checklist below
4028
4245
  5. Report by severity — CRITICAL first, then HIGH, MEDIUM, PASS
4029
4246
 
4247
+ If the task is UI-heavy and a design artifact is available, include design fidelity checks:
4248
+ - visual hierarchy and spacing consistency
4249
+ - CTA flow quality
4250
+ - responsive behavior
4251
+ - accessibility semantics and states
4252
+ - empty/loading/error/success state coverage
4253
+
4030
4254
  ## Security Checklist — CRITICAL
4031
4255
 
4032
4256
  **Hardcoded credentials:**
@@ -4231,7 +4455,7 @@ Never fabricate information to appear more helpful.
4231
4455
  ## Scope Boundaries
4232
4456
 
4233
4457
  - Report facts only. Do not make implementation decisions.
4234
- - Do not write code unless asked. Return research findings for the coder to act on.
4458
+ - Do not write code unless asked. Return research findings for the implementation agent to act on.
4235
4459
  - If you find a better approach than what was requested, mention it as an option — do not substitute it.
4236
4460
 
4237
4461
  ## Research Areas
@@ -4351,7 +4575,7 @@ var createWriterAgent = (model, customPrompt, customAppendPrompt) => {
4351
4575
  };
4352
4576
 
4353
4577
  // 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.
4578
+ 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
4579
 
4356
4580
  ## Audit Scope
4357
4581
 
@@ -4454,7 +4678,7 @@ For high/critical vulnerabilities: report exact package, CVE ID, and whether it'
4454
4678
 
4455
4679
  ## After Finding Issues
4456
4680
 
4457
- Report only. Do not fix. Tag @coder with specific remediations for each finding.`;
4681
+ Report only. Do not fix. Tag the appropriate implementation agent (@backend-coder, @frontend-coder, or @devops) with specific remediations for each finding.`;
4458
4682
  var createSecurityAuditorAgent = (model, customPrompt, customAppendPrompt) => {
4459
4683
  const prompt = resolvePrompt(SECURITY_AUDITOR_PROMPT, customPrompt, customAppendPrompt);
4460
4684
  return {
@@ -4802,7 +5026,7 @@ request → router → UserController.create() → UserService.create() → ❌
4802
5026
 
4803
5027
  ## Scope
4804
5028
 
4805
- Report only. Do not implement the fix. Tag @coder with the recommended fix.`;
5029
+ Report only. Do not implement the fix. Tag the appropriate implementation agent (@backend-coder, @frontend-coder, or @devops) with the recommended fix.`;
4806
5030
  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
5031
 
4808
5032
  ## Diagnostic Commands
@@ -4905,7 +5129,7 @@ npx tsc --noEmit src/path/to/file.ts
4905
5129
 
4906
5130
  - Build fails because of architectural problems → @architect
4907
5131
  - A feature is not working correctly → @debug-specialist
4908
- - Missing functionality needs to be written → @coder`;
5132
+ - Missing functionality needs to be written → @backend-coder/@frontend-coder/@devops`;
4909
5133
  var createDebugSpecialistAgent = (model, customPrompt, customAppendPrompt) => {
4910
5134
  const prompt = resolvePrompt(DEBUG_SPECIALIST_PROMPT, customPrompt, customAppendPrompt);
4911
5135
  return {
@@ -4932,7 +5156,7 @@ var createBuildErrorResolverAgent = (model, customPrompt, customAppendPrompt) =>
4932
5156
  };
4933
5157
 
4934
5158
  // 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.
5159
+ 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
5160
 
4937
5161
  ## Wave-Structured Output
4938
5162
 
@@ -4942,7 +5166,7 @@ var TASK_SPLITTER_PROMPT = `You decompose complex tasks into parallel workstream
4942
5166
  ### Wave 1 (parallel — start simultaneously)
4943
5167
 
4944
5168
  **Track A — [description]**
4945
- - Agent: @coder
5169
+ - Agent: @backend-coder
4946
5170
  - Files: \`src/auth/user.ts\`, \`src/auth/types.ts\`
4947
5171
  - Task: [specific implementation task]
4948
5172
  - Verify: [how to confirm it's done]
@@ -4962,7 +5186,7 @@ var TASK_SPLITTER_PROMPT = `You decompose complex tasks into parallel workstream
4962
5186
  ### Wave 2 (after Wave 1 completes)
4963
5187
 
4964
5188
  **Track D — Integration**
4965
- - Agent: @coder
5189
+ - Agent: @backend-coder
4966
5190
  - Depends on: Track A, Track C
4967
5191
  - Task: Wire together outputs from Wave 1
4968
5192
 
@@ -5002,7 +5226,9 @@ After Wave 2: @reviewer reviews all changes together
5002
5226
  | Agent | Best For |
5003
5227
  |-------|---------|
5004
5228
  | @architect | Interface contracts, ADRs |
5005
- | @coder | Implementation |
5229
+ | @backend-coder | Backend implementation |
5230
+ | @frontend-coder | Frontend implementation |
5231
+ | @devops | Infrastructure implementation |
5006
5232
  | @researcher | API docs, library research |
5007
5233
  | @tester | Test writing and coverage |
5008
5234
  | @reviewer | Code quality review |
@@ -5140,200 +5366,6 @@ Discussion is complete when:
5140
5366
  - No open questions remain
5141
5367
 
5142
5368
  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
5369
  var createTaskSplitterAgent = (model, customPrompt, customAppendPrompt) => {
5338
5370
  const prompt = resolvePrompt(TASK_SPLITTER_PROMPT, customPrompt, customAppendPrompt);
5339
5371
  return {
@@ -5358,18 +5390,6 @@ var createDiscusserAgent = (model, customPrompt, customAppendPrompt) => {
5358
5390
  }
5359
5391
  };
5360
5392
  };
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
5393
 
5374
5394
  // src/agents/architect.ts
5375
5395
  var ARCHITECT_PROMPT = `You design system architecture, create Architecture Decision Records (ADRs), and define API contracts before implementation begins.
@@ -6016,11 +6036,81 @@ function createAutoLearnerAgent(model) {
6016
6036
  return definition;
6017
6037
  }
6018
6038
 
6039
+ // src/agents/design.ts
6040
+ 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.
6041
+
6042
+ ## Scope
6043
+
6044
+ Use this workflow for website, web app, mobile app, dashboard, admin panel, landing page, SaaS interface, onboarding UX, and other user-facing surfaces.
6045
+
6046
+ ## Required Execution Stages
6047
+
6048
+ For UI-heavy tasks, produce all stages in order:
6049
+ 1. discovery
6050
+ 2. ux_planning
6051
+ 3. wireframe_layout
6052
+ 4. visual_system_definition
6053
+ 5. design_approval
6054
+ 6. implementation_handoff
6055
+
6056
+ Do not skip or merge stages.
6057
+
6058
+ ## Structured Output Contract
6059
+
6060
+ Always return machine-readable markdown sections with these keys:
6061
+ - task_type
6062
+ - user_goals
6063
+ - target_audience
6064
+ - core_user_flows
6065
+ - page_map_or_screen_map
6066
+ - section_structure
6067
+ - layout_plan
6068
+ - component_list
6069
+ - state_list (loading, empty, error, success)
6070
+ - responsive_behavior_notes
6071
+ - visual_direction
6072
+ - design_tokens_guidance
6073
+ - accessibility_notes
6074
+ - implementation_handoff_checklist
6075
+ - approval_status
6076
+
6077
+ ## Design Review Mode
6078
+
6079
+ When asked to review an implemented UI, compare against approved design artifacts and report:
6080
+ - design mismatches
6081
+ - hierarchy issues
6082
+ - spacing inconsistency
6083
+ - weak call-to-action flow
6084
+ - responsiveness issues
6085
+ - accessibility concerns
6086
+ - component inconsistency
6087
+ - missing empty/error states
6088
+
6089
+ ## Constraints
6090
+
6091
+ - Do not write implementation code in design mode.
6092
+ - Do not claim approval without explicit pass/fail rationale.
6093
+ - Keep output concise but complete enough for frontend handoff.`;
6094
+ var createDesignAgent = (model, customPrompt, customAppendPrompt) => {
6095
+ const prompt = resolvePrompt(DESIGN_PROMPT, customPrompt, customAppendPrompt);
6096
+ return {
6097
+ name: "design",
6098
+ description: "Design-first specialist for UX structure, wireframe planning, visual system definition, and frontend handoff before implementation.",
6099
+ config: {
6100
+ model,
6101
+ temperature: 0.1,
6102
+ prompt
6103
+ }
6104
+ };
6105
+ };
6106
+
6019
6107
  // src/agents/index.ts
6020
6108
  var AGENT_NAMES = [
6021
6109
  "orchestrator",
6022
6110
  "planner",
6023
- "coder",
6111
+ "backend-coder",
6112
+ "frontend-coder",
6113
+ "devops",
6024
6114
  "plan-checker",
6025
6115
  "tester",
6026
6116
  "reviewer",
@@ -6034,13 +6124,13 @@ var AGENT_NAMES = [
6034
6124
  "build-error-resolver",
6035
6125
  "task-splitter",
6036
6126
  "discusser",
6037
- "parallel-coordinator",
6038
6127
  "architect",
6039
6128
  "risk-analyst",
6040
6129
  "policy-enforcer",
6041
6130
  "performance-optimizer",
6042
6131
  "refactor-guide",
6043
- "auto-learner"
6132
+ "auto-learner",
6133
+ "design"
6044
6134
  ];
6045
6135
  var PRIMARY_AGENTS = new Set(["orchestrator"]);
6046
6136
  var ALL_MODES_AGENTS = new Set;
@@ -6060,8 +6150,12 @@ function createAgent(name, model, customPrompt, customAppendPrompt) {
6060
6150
  return createOrchestratorAgent(model, customPrompt, customAppendPrompt);
6061
6151
  case "planner":
6062
6152
  return createPlannerAgent(model, customPrompt, customAppendPrompt);
6063
- case "coder":
6064
- return createCoderAgent(model, customPrompt, customAppendPrompt);
6153
+ case "backend-coder":
6154
+ return createBackendCoderAgent(model, customPrompt, customAppendPrompt);
6155
+ case "frontend-coder":
6156
+ return createFrontendCoderAgent(model, customPrompt, customAppendPrompt);
6157
+ case "devops":
6158
+ return createDevopsAgent(model, customPrompt, customAppendPrompt);
6065
6159
  case "plan-checker":
6066
6160
  return createPlanCheckerAgent(model, customPrompt, customAppendPrompt);
6067
6161
  case "tester":
@@ -6088,8 +6182,6 @@ function createAgent(name, model, customPrompt, customAppendPrompt) {
6088
6182
  return createTaskSplitterAgent(model, customPrompt, customAppendPrompt);
6089
6183
  case "discusser":
6090
6184
  return createDiscusserAgent(model, customPrompt, customAppendPrompt);
6091
- case "parallel-coordinator":
6092
- return createParallelCoordinatorAgent(model, customPrompt, customAppendPrompt);
6093
6185
  case "architect":
6094
6186
  return createArchitectAgent(model, customPrompt, customAppendPrompt);
6095
6187
  case "risk-analyst":
@@ -6102,6 +6194,8 @@ function createAgent(name, model, customPrompt, customAppendPrompt) {
6102
6194
  return createRefactorGuideAgent(model, customPrompt, customAppendPrompt);
6103
6195
  case "auto-learner":
6104
6196
  return createAutoLearnerAgent(model);
6197
+ case "design":
6198
+ return createDesignAgent(model, customPrompt, customAppendPrompt);
6105
6199
  default:
6106
6200
  console.warn(`[flowdeck] Unknown agent: ${name}`);
6107
6201
  return;
@@ -6139,42 +6233,16 @@ function getAgentConfigs(agentModels) {
6139
6233
  return configs;
6140
6234
  }
6141
6235
 
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
6236
  // src/index.ts
6169
6237
  function loadRulePaths() {
6170
6238
  const __dir = dirname4(fileURLToPath2(import.meta.url));
6171
- const rulesDir = join27(__dir, "..", "src", "rules");
6172
- if (!existsSync28(rulesDir))
6239
+ const rulesDir = join26(__dir, "..", "src", "rules");
6240
+ if (!existsSync27(rulesDir))
6173
6241
  return [];
6174
6242
  const paths = [];
6175
6243
  function walk(dir) {
6176
6244
  for (const entry of readdirSync3(dir, { withFileTypes: true })) {
6177
- const full = join27(dir, entry.name);
6245
+ const full = join26(dir, entry.name);
6178
6246
  if (entry.isDirectory()) {
6179
6247
  walk(full);
6180
6248
  } else if (entry.isFile() && entry.name.endsWith(".md") && entry.name !== "README.md") {
@@ -6187,8 +6255,8 @@ function loadRulePaths() {
6187
6255
  }
6188
6256
  function loadCommands() {
6189
6257
  const __dir = dirname4(fileURLToPath2(import.meta.url));
6190
- const commandsDir = join27(__dir, "..", "src", "commands");
6191
- if (!existsSync28(commandsDir))
6258
+ const commandsDir = join26(__dir, "..", "src", "commands");
6259
+ if (!existsSync27(commandsDir))
6192
6260
  return {};
6193
6261
  const commands = {};
6194
6262
  try {
@@ -6196,7 +6264,7 @@ function loadCommands() {
6196
6264
  if (!file.endsWith(".md"))
6197
6265
  continue;
6198
6266
  const name = basename(file, ".md");
6199
- const raw = readFileSync24(join27(commandsDir, file), "utf-8");
6267
+ const raw = readFileSync23(join26(commandsDir, file), "utf-8");
6200
6268
  let description;
6201
6269
  let template = raw;
6202
6270
  const fmMatch = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/);
@@ -6237,12 +6305,16 @@ var plugin = async (input, _options) => {
6237
6305
  cfg.default_agent = "orchestrator";
6238
6306
  }
6239
6307
  const flowdeckConfig = loadFlowDeckConfig(directory);
6308
+ const designFirstConfig = resolveDesignFirstConfig(flowdeckConfig);
6240
6309
  const agentModels = {};
6241
6310
  for (const [name, agentCfg] of Object.entries(flowdeckConfig.agents ?? {})) {
6242
6311
  if (agentCfg.model) {
6243
6312
  agentModels[name] = agentCfg.model;
6244
6313
  }
6245
6314
  }
6315
+ if (designFirstConfig.modelOverrides.design) {
6316
+ agentModels.design = designFirstConfig.modelOverrides.design;
6317
+ }
6246
6318
  const resolvedAgentConfigs = getAgentConfigs(agentModels);
6247
6319
  if (!cfg.agent) {
6248
6320
  cfg.agent = { ...resolvedAgentConfigs };
@@ -6273,8 +6345,8 @@ var plugin = async (input, _options) => {
6273
6345
  }
6274
6346
  }
6275
6347
  }
6276
- const skillsDir = join27(dirname4(fileURLToPath2(import.meta.url)), "..", "src", "skills");
6277
- if (existsSync28(skillsDir)) {
6348
+ const skillsDir = join26(dirname4(fileURLToPath2(import.meta.url)), "..", "src", "skills");
6349
+ if (existsSync27(skillsDir)) {
6278
6350
  const cfgAny = cfg;
6279
6351
  if (!cfgAny.skills || typeof cfgAny.skills !== "object") {
6280
6352
  cfgAny.skills = { paths: [] };