@dv.nghiem/flowdeck 0.3.3 → 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 (116) hide show
  1. package/README.md +160 -8
  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 +4 -1
  23. package/dist/hooks/orchestrator-guard-hook.d.ts.map +1 -1
  24. package/dist/hooks/session-idle-hook.d.ts.map +1 -1
  25. package/dist/hooks/telemetry-hook.d.ts +14 -1
  26. package/dist/hooks/telemetry-hook.d.ts.map +1 -1
  27. package/dist/hooks/telemetry-hook.test.d.ts +2 -0
  28. package/dist/hooks/telemetry-hook.test.d.ts.map +1 -0
  29. package/dist/hooks/tool-guard.d.ts.map +1 -1
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +810 -474
  32. package/dist/services/agent-contract-registry.d.ts +32 -0
  33. package/dist/services/agent-contract-registry.d.ts.map +1 -0
  34. package/dist/services/agent-performance.d.ts +1 -1
  35. package/dist/services/agent-performance.d.ts.map +1 -1
  36. package/dist/services/agent-trace-graph.d.ts +94 -0
  37. package/dist/services/agent-trace-graph.d.ts.map +1 -0
  38. package/dist/services/agent-validator.d.ts +56 -0
  39. package/dist/services/agent-validator.d.ts.map +1 -0
  40. package/dist/services/deadlock-detector.d.ts +34 -0
  41. package/dist/services/deadlock-detector.d.ts.map +1 -0
  42. package/dist/services/delegation-budget.d.ts +54 -0
  43. package/dist/services/delegation-budget.d.ts.map +1 -0
  44. package/dist/services/governance.test.d.ts +11 -0
  45. package/dist/services/governance.test.d.ts.map +1 -0
  46. package/dist/services/index.d.ts +6 -1
  47. package/dist/services/index.d.ts.map +1 -1
  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/council.d.ts.map +1 -1
  53. package/dist/tools/delegate.d.ts.map +1 -1
  54. package/dist/tools/dispatch-routing.d.ts +9 -0
  55. package/dist/tools/dispatch-routing.d.ts.map +1 -0
  56. package/dist/tools/dispatch-routing.test.d.ts +2 -0
  57. package/dist/tools/dispatch-routing.test.d.ts.map +1 -0
  58. package/dist/tools/planning-state-lib.d.ts +8 -0
  59. package/dist/tools/planning-state-lib.d.ts.map +1 -1
  60. package/dist/tools/planning-state.d.ts.map +1 -1
  61. package/dist/tools/run-pipeline.d.ts.map +1 -1
  62. package/docs/agents.md +104 -74
  63. package/docs/best-practices.md +1 -1
  64. package/docs/commands/fd-ask.md +2 -2
  65. package/docs/commands/fd-fix-bug.md +2 -2
  66. package/docs/commands/fd-new-feature.md +2 -2
  67. package/docs/commands/fd-quick.md +3 -1
  68. package/docs/commands.md +37 -7
  69. package/docs/configuration.md +76 -46
  70. package/docs/design-first-workflow.md +94 -0
  71. package/docs/feature-integration-architecture.md +3 -31
  72. package/docs/index.md +5 -2
  73. package/docs/installation.md +6 -17
  74. package/docs/intelligence.md +110 -34
  75. package/docs/multi-repo.md +1 -1
  76. package/docs/optimization-baseline.md +21 -0
  77. package/docs/rules.md +10 -37
  78. package/docs/skills.md +24 -15
  79. package/docs/workflows.md +18 -14
  80. package/package.json +4 -2
  81. package/src/commands/fd-ask.md +1 -0
  82. package/src/commands/fd-design.md +64 -0
  83. package/src/commands/fd-discuss.md +2 -0
  84. package/src/commands/fd-execute.md +7 -3
  85. package/src/commands/fd-fix-bug.md +2 -2
  86. package/src/commands/fd-multi-repo.md +3 -3
  87. package/src/commands/fd-plan.md +2 -0
  88. package/src/commands/fd-quick.md +4 -1
  89. package/src/commands/fd-verify.md +6 -0
  90. package/src/rules/README.md +10 -0
  91. package/src/rules/common/agent-orchestration.md +6 -6
  92. package/src/rules/common/coding-style.md +2 -2
  93. package/src/rules/typescript/patterns.md +1 -1
  94. package/src/skills/app-shell-design/SKILL.md +31 -0
  95. package/src/skills/backend-patterns/SKILL.md +6 -0
  96. package/src/skills/clean-architecture/SKILL.md +6 -0
  97. package/src/skills/cqrs/SKILL.md +6 -0
  98. package/src/skills/dashboard-design/SKILL.md +32 -0
  99. package/src/skills/ddd-architecture/SKILL.md +6 -0
  100. package/src/skills/decision-trace/SKILL.md +1 -1
  101. package/src/skills/design-audit/SKILL.md +37 -0
  102. package/src/skills/design-system-definition/SKILL.md +33 -0
  103. package/src/skills/event-driven-architecture/SKILL.md +6 -0
  104. package/src/skills/frontend-handoff/SKILL.md +31 -0
  105. package/src/skills/hexagonal-architecture/SKILL.md +6 -0
  106. package/src/skills/landing-page-design/SKILL.md +32 -0
  107. package/src/skills/layered-architecture/SKILL.md +6 -0
  108. package/src/skills/multi-repo/SKILL.md +3 -3
  109. package/src/skills/plan-task/SKILL.md +2 -2
  110. package/src/skills/postgres-patterns/SKILL.md +6 -0
  111. package/src/skills/responsive-review/SKILL.md +31 -0
  112. package/src/skills/saga-architecture/SKILL.md +6 -0
  113. package/src/skills/ui-ux-planning/SKILL.md +32 -0
  114. package/src/skills/wireframe-planning/SKILL.md +30 -0
  115. package/dist/services/model-router.d.ts +0 -35
  116. 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 readFileSync22, existsSync as existsSync25 } from "fs";
3
- import { join as join24, 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
  }
@@ -509,6 +561,152 @@ var workspaceStateTool = tool3({
509
561
 
510
562
  // src/tools/run-pipeline.ts
511
563
  import { tool as tool4 } from "@opencode-ai/plugin";
564
+
565
+ // src/services/agent-performance.ts
566
+ import { existsSync as existsSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync5, mkdirSync as mkdirSync2 } from "fs";
567
+ import { join as join5 } from "path";
568
+ function perfPath(dir) {
569
+ return join5(codebaseDir(dir), "AGENT_PERF.json");
570
+ }
571
+ function loadStore(dir) {
572
+ const p = perfPath(dir);
573
+ if (!existsSync5(p))
574
+ return { entries: [], updated_at: new Date().toISOString() };
575
+ try {
576
+ return JSON.parse(readFileSync5(p, "utf-8"));
577
+ } catch {
578
+ return { entries: [], updated_at: new Date().toISOString() };
579
+ }
580
+ }
581
+ function saveStore(dir, store) {
582
+ const cd = codebaseDir(dir);
583
+ if (!existsSync5(cd))
584
+ mkdirSync2(cd, { recursive: true });
585
+ writeFileSync5(perfPath(dir), JSON.stringify(store, null, 2), "utf-8");
586
+ }
587
+ function makeKey(agent, model, task_type) {
588
+ return `${agent}::${model}::${task_type}`;
589
+ }
590
+ function recordRun(dir, agent, model, task_type, success, duration_ms, cost = 0) {
591
+ const store = loadStore(dir);
592
+ const key = makeKey(agent, model, task_type);
593
+ const existing = store.entries.find((e) => makeKey(e.agent, e.model, e.task_type) === key);
594
+ if (existing) {
595
+ existing.runs++;
596
+ if (success)
597
+ existing.successes++;
598
+ else
599
+ existing.failures++;
600
+ existing.total_duration_ms += duration_ms;
601
+ existing.total_cost += cost;
602
+ existing.last_run = new Date().toISOString();
603
+ existing.last_status = success ? "success" : "failure";
604
+ } else {
605
+ store.entries.push({
606
+ agent,
607
+ model,
608
+ task_type,
609
+ runs: 1,
610
+ successes: success ? 1 : 0,
611
+ failures: success ? 0 : 1,
612
+ total_duration_ms: duration_ms,
613
+ total_cost: cost,
614
+ last_run: new Date().toISOString(),
615
+ last_status: success ? "success" : "failure"
616
+ });
617
+ }
618
+ store.updated_at = new Date().toISOString();
619
+ saveStore(dir, store);
620
+ }
621
+
622
+ // src/tools/dispatch-routing.ts
623
+ function shouldRetry(promptRes) {
624
+ if (!promptRes)
625
+ return false;
626
+ const detail = promptRes.error?.detail;
627
+ if (isTransientError(detail))
628
+ return true;
629
+ const infoError = promptRes.data?.info?.error;
630
+ const text = typeof infoError === "string" ? infoError : JSON.stringify(infoError ?? "");
631
+ return isTransientError(text);
632
+ }
633
+ function isTransientError(text) {
634
+ if (!text)
635
+ return false;
636
+ const haystack = text.toLowerCase();
637
+ return haystack.includes("overload") || haystack.includes("rate limit") || haystack.includes("timeout") || haystack.includes("temporar") || haystack.includes("econnreset");
638
+ }
639
+ function normalizeTaskType(taskType, agent) {
640
+ const normalized = (taskType ?? "").trim().toLowerCase();
641
+ if (isTaskType(normalized))
642
+ return normalized;
643
+ const a = agent.toLowerCase();
644
+ if (a.includes("design") || a.includes("ui-ux"))
645
+ return "design";
646
+ if (a.includes("review"))
647
+ return "review";
648
+ if (a.includes("test"))
649
+ return "testing";
650
+ if (a.includes("debug"))
651
+ return "debugging";
652
+ if (a.includes("security"))
653
+ return "security";
654
+ if (a.includes("doc"))
655
+ return "documentation";
656
+ if (a.includes("architect") || a.includes("planner"))
657
+ return "planning";
658
+ if (a.includes("orchestrator") || a.includes("coordinator"))
659
+ return "orchestration";
660
+ if (a.includes("analyst") || a.includes("research"))
661
+ return "analysis";
662
+ return "implementation";
663
+ }
664
+ function isTaskType(value) {
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;
707
+ }
708
+
709
+ // src/tools/run-pipeline.ts
512
710
  function extractText(parts) {
513
711
  return parts.filter((p) => p.type === "text" && typeof p.text === "string").map((p) => p.text).join(`
514
712
  `);
@@ -519,16 +717,20 @@ function createRunPipelineTool(client) {
519
717
  args: {
520
718
  steps: tool4.schema.array(tool4.schema.object({
521
719
  agent: tool4.schema.string(),
522
- prompt: tool4.schema.string()
720
+ prompt: tool4.schema.string(),
721
+ task_type: tool4.schema.string().optional()
523
722
  })),
524
723
  initial_context: tool4.schema.string().optional(),
525
- abort_on_failure: tool4.schema.boolean().optional().default(true)
724
+ abort_on_failure: tool4.schema.boolean().optional().default(true),
725
+ retry_attempts: tool4.schema.number().optional().default(1)
526
726
  },
527
727
  async execute(args, context) {
528
728
  const startTime = Date.now();
529
729
  const trace = [];
530
730
  let carryContext = args.initial_context ?? "";
531
731
  let aborted = false;
732
+ const retryAttempts = typeof args.retry_attempts === "number" ? args.retry_attempts : 1;
733
+ const maxRetries = Math.max(0, Math.floor(retryAttempts));
532
734
  let inflightChildId = null;
533
735
  const abortHandler = () => {
534
736
  if (inflightChildId) {
@@ -546,6 +748,7 @@ function createRunPipelineTool(client) {
546
748
  break;
547
749
  }
548
750
  const stepStart = Date.now();
751
+ const taskType = normalizeTaskType(step.task_type, step.agent);
549
752
  const stepInput = carryContext ? `${carryContext}
550
753
 
551
754
  ---
@@ -557,28 +760,36 @@ ${step.prompt}` : step.prompt;
557
760
  });
558
761
  if (createRes.error || !createRes.data?.id) {
559
762
  const errMsg = `Failed to create session: ${createRes.error?.detail ?? "unknown"}`;
560
- trace.push({ agent: step.agent, 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 });
561
764
  aborted = true;
562
765
  break;
563
766
  }
564
767
  inflightChildId = createRes.data.id;
565
- const promptRes = await client.session.prompt({
566
- path: { id: inflightChildId },
567
- body: {
568
- agent: step.agent,
569
- parts: [{ type: "text", text: stepInput }],
570
- tools: { question: false }
571
- },
572
- query: { directory: context.directory }
573
- });
768
+ let promptRes = null;
769
+ let retriesUsed = 0;
770
+ for (let attempt = 0;attempt <= maxRetries; attempt++) {
771
+ promptRes = await client.session.prompt({
772
+ path: { id: inflightChildId },
773
+ body: {
774
+ agent: step.agent,
775
+ parts: [{ type: "text", text: stepInput }],
776
+ tools: { question: false }
777
+ },
778
+ query: { directory: context.directory }
779
+ });
780
+ if (!shouldRetry(promptRes) || attempt === maxRetries)
781
+ break;
782
+ retriesUsed++;
783
+ }
574
784
  inflightChildId = null;
575
785
  if (context.abort.aborted) {
576
786
  aborted = true;
577
787
  break;
578
788
  }
579
- if (promptRes.error) {
580
- const errMsg = `Prompt failed: ${promptRes.error?.detail ?? "unknown"}`;
581
- trace.push({ agent: step.agent, session_id: createRes.data.id, input: stepInput, output: errMsg, duration_ms: Date.now() - stepStart, success: false });
789
+ if (!promptRes || promptRes.error) {
790
+ const errMsg = `Prompt failed: ${promptRes?.error?.detail ?? "unknown"}`;
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);
582
793
  if (args.abort_on_failure) {
583
794
  aborted = true;
584
795
  break;
@@ -588,7 +799,8 @@ ${step.prompt}` : step.prompt;
588
799
  const info = promptRes.data?.info;
589
800
  if (info?.error) {
590
801
  const errMsg = `Agent error: ${JSON.stringify(info.error)}`;
591
- trace.push({ agent: step.agent, session_id: createRes.data.id, input: stepInput, output: errMsg, duration_ms: Date.now() - stepStart, success: false });
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);
592
804
  if (args.abort_on_failure) {
593
805
  aborted = true;
594
806
  break;
@@ -596,7 +808,8 @@ ${step.prompt}` : step.prompt;
596
808
  continue;
597
809
  }
598
810
  const output = extractText(promptRes.data?.parts ?? []);
599
- trace.push({ agent: step.agent, session_id: createRes.data.id, input: stepInput, output: output || "(no text output)", duration_ms: Date.now() - stepStart, success: true });
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);
600
813
  carryContext = output;
601
814
  }
602
815
  } finally {
@@ -623,10 +836,15 @@ function createDelegateTool(client) {
623
836
  args: {
624
837
  agent: tool5.schema.string(),
625
838
  prompt: tool5.schema.string(),
626
- context: tool5.schema.string().optional()
839
+ context: tool5.schema.string().optional(),
840
+ task_type: tool5.schema.string().optional(),
841
+ retry_attempts: tool5.schema.number().optional().default(1)
627
842
  },
628
843
  async execute(args, context) {
629
844
  const startTime = Date.now();
845
+ const taskType = normalizeTaskType(args.task_type, args.agent);
846
+ const retryAttempts = typeof args.retry_attempts === "number" ? args.retry_attempts : 1;
847
+ const maxRetries = Math.max(0, Math.floor(retryAttempts));
630
848
  const createRes = await client.session.create({
631
849
  body: { parentID: context.sessionID, title: `${args.agent}-delegate` },
632
850
  query: { directory: context.directory }
@@ -651,40 +869,59 @@ function createDelegateTool(client) {
651
869
  ---
652
870
 
653
871
  ${args.prompt}` : args.prompt;
654
- const promptRes = await client.session.prompt({
655
- path: { id: childId },
656
- body: {
657
- agent: args.agent,
658
- parts: [{ type: "text", text: fullPrompt }],
659
- tools: { question: false }
660
- },
661
- query: { directory: context.directory }
662
- });
663
- if (promptRes.error) {
872
+ let promptRes = null;
873
+ let retriesUsed = 0;
874
+ for (let attempt = 0;attempt <= maxRetries; attempt++) {
875
+ promptRes = await client.session.prompt({
876
+ path: { id: childId },
877
+ body: {
878
+ agent: args.agent,
879
+ parts: [{ type: "text", text: fullPrompt }],
880
+ tools: { question: false }
881
+ },
882
+ query: { directory: context.directory }
883
+ });
884
+ if (!shouldRetry(promptRes) || attempt === maxRetries)
885
+ break;
886
+ retriesUsed++;
887
+ }
888
+ if (!promptRes || promptRes.error) {
889
+ recordRun(context.directory, args.agent, "", taskType, false, Date.now() - startTime);
664
890
  return JSON.stringify({
665
891
  agent: args.agent,
666
892
  session_id: childId,
667
893
  success: false,
668
- error: `Prompt failed: ${promptRes.error?.detail ?? "unknown"}`,
894
+ error: `Prompt failed: ${promptRes?.error?.detail ?? "unknown"}`,
895
+ task_type: taskType,
896
+ model: "",
897
+ retries_used: retriesUsed,
669
898
  duration_ms: Date.now() - startTime
670
899
  });
671
900
  }
672
901
  const info = promptRes.data?.info;
673
902
  if (info?.error) {
903
+ recordRun(context.directory, args.agent, "", taskType, false, Date.now() - startTime);
674
904
  return JSON.stringify({
675
905
  agent: args.agent,
676
906
  session_id: childId,
677
907
  success: false,
678
908
  error: `Agent error: ${JSON.stringify(info.error)}`,
909
+ task_type: taskType,
910
+ model: "",
911
+ retries_used: retriesUsed,
679
912
  duration_ms: Date.now() - startTime
680
913
  });
681
914
  }
682
915
  const output = extractText2(promptRes.data?.parts ?? []);
916
+ recordRun(context.directory, args.agent, "", taskType, true, Date.now() - startTime);
683
917
  return JSON.stringify({
684
918
  agent: args.agent,
685
919
  session_id: childId,
686
920
  success: true,
687
921
  output: output || "(no text output)",
922
+ task_type: taskType,
923
+ model: "",
924
+ retries_used: retriesUsed,
688
925
  duration_ms: Date.now() - startTime
689
926
  });
690
927
  }
@@ -693,31 +930,31 @@ ${args.prompt}` : args.prompt;
693
930
 
694
931
  // src/tools/repo-memory.ts
695
932
  import { tool as tool6 } from "@opencode-ai/plugin";
696
- import { readFileSync as readFileSync5, writeFileSync as writeFileSync5, existsSync as existsSync5, mkdirSync as mkdirSync2 } from "fs";
697
- import { join as join5 } 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";
698
935
  var MEMORY_FILE = "MEMORY.json";
699
936
  function memoryPath(directory) {
700
- return join5(codebaseDir(directory), MEMORY_FILE);
937
+ return join6(codebaseDir(directory), MEMORY_FILE);
701
938
  }
702
939
  function emptyMemory() {
703
940
  return { version: "1.0", last_updated: new Date().toISOString(), nodes: {} };
704
941
  }
705
942
  function readMemory(directory) {
706
943
  const p = memoryPath(directory);
707
- if (!existsSync5(p))
944
+ if (!existsSync6(p))
708
945
  return emptyMemory();
709
946
  try {
710
- return JSON.parse(readFileSync5(p, "utf-8"));
947
+ return JSON.parse(readFileSync6(p, "utf-8"));
711
948
  } catch {
712
949
  return emptyMemory();
713
950
  }
714
951
  }
715
952
  function writeMemory(directory, memory) {
716
953
  const base = codebaseDir(directory);
717
- if (!existsSync5(base))
718
- mkdirSync2(base, { recursive: true });
954
+ if (!existsSync6(base))
955
+ mkdirSync3(base, { recursive: true });
719
956
  memory.last_updated = new Date().toISOString();
720
- writeFileSync5(memoryPath(directory), JSON.stringify(memory, null, 2), "utf-8");
957
+ writeFileSync6(memoryPath(directory), JSON.stringify(memory, null, 2), "utf-8");
721
958
  }
722
959
  var repoMemoryTool = tool6({
723
960
  description: "Repo Memory Graph: read/write/query persistent architecture graph in .codebase/MEMORY.json (modules, dependencies, ownership, bug history, conventions)",
@@ -794,28 +1031,28 @@ var repoMemoryTool = tool6({
794
1031
 
795
1032
  // src/tools/failure-replay.ts
796
1033
  import { tool as tool7 } from "@opencode-ai/plugin";
797
- import { readFileSync as readFileSync6, writeFileSync as writeFileSync6, existsSync as existsSync6, mkdirSync as mkdirSync3 } from "fs";
798
- import { join as join6 } 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";
799
1036
  var FAILURES_FILE = "FAILURES.json";
800
1037
  function failuresPath(directory) {
801
- return join6(codebaseDir(directory), FAILURES_FILE);
1038
+ return join7(codebaseDir(directory), FAILURES_FILE);
802
1039
  }
803
1040
  function readStore(directory) {
804
1041
  const p = failuresPath(directory);
805
- if (!existsSync6(p))
1042
+ if (!existsSync7(p))
806
1043
  return { version: "1.0", last_updated: new Date().toISOString(), entries: [] };
807
1044
  try {
808
- return JSON.parse(readFileSync6(p, "utf-8"));
1045
+ return JSON.parse(readFileSync7(p, "utf-8"));
809
1046
  } catch {
810
1047
  return { version: "1.0", last_updated: new Date().toISOString(), entries: [] };
811
1048
  }
812
1049
  }
813
1050
  function writeStore(directory, store) {
814
1051
  const base = codebaseDir(directory);
815
- if (!existsSync6(base))
816
- mkdirSync3(base, { recursive: true });
1052
+ if (!existsSync7(base))
1053
+ mkdirSync4(base, { recursive: true });
817
1054
  store.last_updated = new Date().toISOString();
818
- writeFileSync6(failuresPath(directory), JSON.stringify(store, null, 2), "utf-8");
1055
+ writeFileSync7(failuresPath(directory), JSON.stringify(store, null, 2), "utf-8");
819
1056
  }
820
1057
  var failureReplayTool = tool7({
821
1058
  description: "Failure Replay Engine: record and query past failures (reverted commits, failed deployments, flaky tests, bug fixes) in .codebase/FAILURES.json so the agent avoids repeating mistakes",
@@ -899,17 +1136,17 @@ var failureReplayTool = tool7({
899
1136
 
900
1137
  // src/tools/decision-trace.ts
901
1138
  import { tool as tool8 } from "@opencode-ai/plugin";
902
- import { readFileSync as readFileSync7, existsSync as existsSync7, mkdirSync as mkdirSync4, appendFileSync } from "fs";
903
- import { join as join7 } from "path";
1139
+ import { readFileSync as readFileSync8, existsSync as existsSync8, mkdirSync as mkdirSync5, appendFileSync } from "fs";
1140
+ import { join as join8 } from "path";
904
1141
  var DECISIONS_FILE = "DECISIONS.jsonl";
905
1142
  function decisionsPath(directory) {
906
- return join7(codebaseDir(directory), DECISIONS_FILE);
1143
+ return join8(codebaseDir(directory), DECISIONS_FILE);
907
1144
  }
908
1145
  function readDecisions(directory) {
909
1146
  const p = decisionsPath(directory);
910
- if (!existsSync7(p))
1147
+ if (!existsSync8(p))
911
1148
  return [];
912
- return readFileSync7(p, "utf-8").split(`
1149
+ return readFileSync8(p, "utf-8").split(`
913
1150
  `).filter((l) => l.trim()).map((l) => {
914
1151
  try {
915
1152
  return JSON.parse(l);
@@ -949,8 +1186,8 @@ var decisionTraceTool = tool8({
949
1186
  case "record": {
950
1187
  if (!args.entry)
951
1188
  return JSON.stringify({ error: "entry required" });
952
- if (!existsSync7(base))
953
- mkdirSync4(base, { recursive: true });
1189
+ if (!existsSync8(base))
1190
+ mkdirSync5(base, { recursive: true });
954
1191
  const entry = { ...args.entry, timestamp: new Date().toISOString() };
955
1192
  appendFileSync(decisionsPath(dir), JSON.stringify(entry) + `
956
1193
  `, "utf-8");
@@ -984,28 +1221,28 @@ var decisionTraceTool = tool8({
984
1221
 
985
1222
  // src/tools/volatility-map.ts
986
1223
  import { tool as tool9 } from "@opencode-ai/plugin";
987
- import { readFileSync as readFileSync8, writeFileSync as writeFileSync8, existsSync as existsSync8, mkdirSync as mkdirSync5 } from "fs";
988
- import { join as join8 } 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";
989
1226
  var VOLATILITY_FILE = "VOLATILITY.json";
990
1227
  function volatilityPath(directory) {
991
- return join8(codebaseDir(directory), VOLATILITY_FILE);
1228
+ return join9(codebaseDir(directory), VOLATILITY_FILE);
992
1229
  }
993
1230
  function readStore2(directory) {
994
1231
  const p = volatilityPath(directory);
995
- if (!existsSync8(p))
1232
+ if (!existsSync9(p))
996
1233
  return { version: "1.0", last_updated: new Date().toISOString(), generated_at: new Date().toISOString(), entries: [] };
997
1234
  try {
998
- return JSON.parse(readFileSync8(p, "utf-8"));
1235
+ return JSON.parse(readFileSync9(p, "utf-8"));
999
1236
  } catch {
1000
1237
  return { version: "1.0", last_updated: new Date().toISOString(), generated_at: new Date().toISOString(), entries: [] };
1001
1238
  }
1002
1239
  }
1003
1240
  function writeStore2(directory, store) {
1004
1241
  const base = codebaseDir(directory);
1005
- if (!existsSync8(base))
1006
- mkdirSync5(base, { recursive: true });
1242
+ if (!existsSync9(base))
1243
+ mkdirSync6(base, { recursive: true });
1007
1244
  store.last_updated = new Date().toISOString();
1008
- writeFileSync8(volatilityPath(directory), JSON.stringify(store, null, 2), "utf-8");
1245
+ writeFileSync9(volatilityPath(directory), JSON.stringify(store, null, 2), "utf-8");
1009
1246
  }
1010
1247
  function stabilityLabel(churn, hotfixes, todos) {
1011
1248
  const score = churn + hotfixes * 10 + todos * 2;
@@ -1092,28 +1329,28 @@ var volatilityMapTool = tool9({
1092
1329
 
1093
1330
  // src/tools/policy-engine.ts
1094
1331
  import { tool as tool10 } from "@opencode-ai/plugin";
1095
- import { readFileSync as readFileSync9, writeFileSync as writeFileSync9, existsSync as existsSync9, mkdirSync as mkdirSync6 } from "fs";
1096
- import { join as join9 } 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";
1097
1334
  var POLICIES_FILE = "POLICIES.json";
1098
1335
  function policiesPath(directory) {
1099
- return join9(codebaseDir(directory), POLICIES_FILE);
1336
+ return join10(codebaseDir(directory), POLICIES_FILE);
1100
1337
  }
1101
1338
  function readStore3(directory) {
1102
1339
  const p = policiesPath(directory);
1103
- if (!existsSync9(p))
1340
+ if (!existsSync10(p))
1104
1341
  return { version: "1.0", last_updated: new Date().toISOString(), policies: [] };
1105
1342
  try {
1106
- return JSON.parse(readFileSync9(p, "utf-8"));
1343
+ return JSON.parse(readFileSync10(p, "utf-8"));
1107
1344
  } catch {
1108
1345
  return { version: "1.0", last_updated: new Date().toISOString(), policies: [] };
1109
1346
  }
1110
1347
  }
1111
1348
  function writeStore3(directory, store) {
1112
1349
  const base = codebaseDir(directory);
1113
- if (!existsSync9(base))
1114
- mkdirSync6(base, { recursive: true });
1350
+ if (!existsSync10(base))
1351
+ mkdirSync7(base, { recursive: true });
1115
1352
  store.last_updated = new Date().toISOString();
1116
- writeFileSync9(policiesPath(directory), JSON.stringify(store, null, 2), "utf-8");
1353
+ writeFileSync10(policiesPath(directory), JSON.stringify(store, null, 2), "utf-8");
1117
1354
  }
1118
1355
  var policyEngineTool = tool10({
1119
1356
  description: "Self-Healing Policy Engine: manage .codebase/POLICIES.json — add, list, query, toggle, and record violations of editing policies learned from past failures",
@@ -1195,7 +1432,7 @@ var policyEngineTool = tool10({
1195
1432
 
1196
1433
  // src/tools/hash-edit.ts
1197
1434
  import { tool as tool11 } from "@opencode-ai/plugin";
1198
- import { readFileSync as readFileSync10, writeFileSync as writeFileSync10 } from "fs";
1435
+ import { readFileSync as readFileSync11, writeFileSync as writeFileSync11 } from "fs";
1199
1436
  import { createHash } from "crypto";
1200
1437
  var hashEditTool = tool11({
1201
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.",
@@ -1209,7 +1446,7 @@ var hashEditTool = tool11({
1209
1446
  const fullPath = args.filePath.startsWith("/") ? args.filePath : `${context.directory}/${args.filePath}`;
1210
1447
  let content;
1211
1448
  try {
1212
- content = readFileSync10(fullPath, "utf-8");
1449
+ content = readFileSync11(fullPath, "utf-8");
1213
1450
  } catch (e) {
1214
1451
  return `Error: Could not read file ${args.filePath}`;
1215
1452
  }
@@ -1223,13 +1460,15 @@ var hashEditTool = tool11({
1223
1460
  }
1224
1461
  }
1225
1462
  const newContent = content.replace(args.targetContent, args.replacementContent);
1226
- writeFileSync10(fullPath, newContent, "utf-8");
1463
+ writeFileSync11(fullPath, newContent, "utf-8");
1227
1464
  return `Successfully updated ${args.filePath} using hash-anchored edit.`;
1228
1465
  }
1229
1466
  });
1230
1467
 
1231
1468
  // src/tools/council.ts
1232
1469
  import { tool as tool12 } from "@opencode-ai/plugin";
1470
+ import { appendFileSync as appendFileSync2, existsSync as existsSync11, mkdirSync as mkdirSync8 } from "fs";
1471
+ import { join as join11 } from "path";
1233
1472
  function createCouncilTool(client) {
1234
1473
  return tool12({
1235
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.",
@@ -1238,7 +1477,7 @@ function createCouncilTool(client) {
1238
1477
  agents: tool12.schema.array(tool12.schema.string()).optional()
1239
1478
  },
1240
1479
  async execute(args, context) {
1241
- const agents = args.agents || ["architect", "reviewer", "coder"];
1480
+ const agents = args.agents || ["architect", "reviewer", "backend-coder"];
1242
1481
  const tasks = agents.map((agent) => ({
1243
1482
  agent,
1244
1483
  prompt: `TASK: ${args.task}
@@ -1284,16 +1523,34 @@ Please synthesize these results. Identify areas of agreement, resolve conflicts,
1284
1523
  },
1285
1524
  query: { directory: context.directory }
1286
1525
  });
1287
- return (finalRes.data?.parts ?? []).filter((p) => p.type === "text").map((p) => p.text).join(`
1526
+ const synthesis = (finalRes.data?.parts ?? []).filter((p) => p.type === "text").map((p) => p.text).join(`
1288
1527
  `);
1528
+ persistCouncilResult(context.directory, {
1529
+ task: args.task,
1530
+ agents,
1531
+ results,
1532
+ synthesis,
1533
+ created_at: new Date().toISOString()
1534
+ });
1535
+ return synthesis;
1289
1536
  }
1290
1537
  });
1291
1538
  }
1539
+ function persistCouncilResult(directory, payload) {
1540
+ try {
1541
+ const base = codebaseDir(directory);
1542
+ if (!existsSync11(base))
1543
+ mkdirSync8(base, { recursive: true });
1544
+ const path = join11(base, "COUNCILS.jsonl");
1545
+ appendFileSync2(path, JSON.stringify(payload) + `
1546
+ `, "utf-8");
1547
+ } catch {}
1548
+ }
1292
1549
 
1293
1550
  // src/tools/context-generator.ts
1294
1551
  import { tool as tool13 } from "@opencode-ai/plugin";
1295
- import { writeFileSync as writeFileSync11, existsSync as existsSync10, readFileSync as readFileSync11, readdirSync as readdirSync2, statSync } from "fs";
1296
- import { join as join10 } 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";
1297
1554
  var contextGeneratorTool = tool13({
1298
1555
  description: "Auto-generate or update hierarchical context files (AGENTS.md, CLAUDE.md) throughout the project. These files provide critical grounding for AI agents.",
1299
1556
  args: {
@@ -1302,20 +1559,20 @@ var contextGeneratorTool = tool13({
1302
1559
  },
1303
1560
  async execute(args, context) {
1304
1561
  const root = context.directory;
1305
- const target = args.targetDir ? join10(root, args.targetDir) : root;
1306
- if (!existsSync10(target)) {
1562
+ const target = args.targetDir ? join12(root, args.targetDir) : root;
1563
+ if (!existsSync12(target)) {
1307
1564
  return `Error: Directory ${target} does not exist.`;
1308
1565
  }
1309
- const agentsMdPath = join10(target, "AGENTS.md");
1310
- if (existsSync10(agentsMdPath) && !args.force) {
1566
+ const agentsMdPath = join12(target, "AGENTS.md");
1567
+ if (existsSync12(agentsMdPath) && !args.force) {
1311
1568
  return `AGENTS.md already exists in ${target}. Use force: true to overwrite.`;
1312
1569
  }
1313
- const pkgPath = join10(root, "package.json");
1570
+ const pkgPath = join12(root, "package.json");
1314
1571
  let projectName = "Project";
1315
1572
  let techStack = "Unknown";
1316
- if (existsSync10(pkgPath)) {
1573
+ if (existsSync12(pkgPath)) {
1317
1574
  try {
1318
- const pkg = JSON.parse(readFileSync11(pkgPath, "utf-8"));
1575
+ const pkg = JSON.parse(readFileSync12(pkgPath, "utf-8"));
1319
1576
  projectName = pkg.name || projectName;
1320
1577
  techStack = Object.keys(pkg.dependencies || {}).slice(0, 5).join(", ");
1321
1578
  } catch {}
@@ -1333,7 +1590,7 @@ var contextGeneratorTool = tool13({
1333
1590
 
1334
1591
  ## Directory Map
1335
1592
  ${readdirSync2(target).slice(0, 10).map((f) => {
1336
- const s = statSync(join10(target, f));
1593
+ const s = statSync(join12(target, f));
1337
1594
  return `- \`${f}\`${s.isDirectory() ? "/" : ""} : [Description]`;
1338
1595
  }).join(`
1339
1596
  `)}
@@ -1341,17 +1598,17 @@ ${readdirSync2(target).slice(0, 10).map((f) => {
1341
1598
  ---
1342
1599
  Generated by FlowDeck Context Generator.
1343
1600
  `;
1344
- writeFileSync11(agentsMdPath, content, "utf-8");
1601
+ writeFileSync12(agentsMdPath, content, "utf-8");
1345
1602
  return `Successfully generated AGENTS.md in ${target}.`;
1346
1603
  }
1347
1604
  });
1348
1605
 
1349
1606
  // src/tools/create-skill.ts
1350
1607
  import { tool as tool14 } from "@opencode-ai/plugin";
1351
- import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync12, existsSync as existsSync11 } from "fs";
1352
- import { join as join11, 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";
1353
1610
  import { fileURLToPath } from "url";
1354
- var SKILLS_DIR = join11(dirname3(fileURLToPath(import.meta.url)), "..", "skills");
1611
+ var SKILLS_DIR = join13(dirname3(fileURLToPath(import.meta.url)), "..", "skills");
1355
1612
  var createSkillTool = tool14({
1356
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.",
1357
1614
  args: {
@@ -1361,9 +1618,9 @@ var createSkillTool = tool14({
1361
1618
  tags: tool14.schema.array(tool14.schema.string()).optional().describe("Optional tags for categorisation, e.g. ['performance', 'typescript']")
1362
1619
  },
1363
1620
  async execute(args) {
1364
- const skillDir = join11(SKILLS_DIR, args.name);
1365
- const skillFile = join11(skillDir, "SKILL.md");
1366
- if (existsSync11(skillFile)) {
1621
+ const skillDir = join13(SKILLS_DIR, args.name);
1622
+ const skillFile = join13(skillDir, "SKILL.md");
1623
+ if (existsSync13(skillFile)) {
1367
1624
  return `Skill '${args.name}' already exists at ${skillFile}.
1368
1625
  ` + `Use a different name or delete the existing skill directory first.`;
1369
1626
  }
@@ -1378,8 +1635,8 @@ origin: FlowDeck (self-learned)${tagLine}
1378
1635
  `;
1379
1636
  const fullContent = frontmatter + args.content.trimStart();
1380
1637
  try {
1381
- mkdirSync7(skillDir, { recursive: true });
1382
- writeFileSync12(skillFile, fullContent, "utf-8");
1638
+ mkdirSync9(skillDir, { recursive: true });
1639
+ writeFileSync13(skillFile, fullContent, "utf-8");
1383
1640
  return `✓ Skill '${args.name}' created at ${skillFile}
1384
1641
 
1385
1642
  ` + `The skill is now part of the FlowDeck library. Restart OpenCode to load it into the active session.`;
@@ -1391,8 +1648,8 @@ origin: FlowDeck (self-learned)${tagLine}
1391
1648
 
1392
1649
  // src/tools/reflect.ts
1393
1650
  import { tool as tool15 } from "@opencode-ai/plugin";
1394
- import { existsSync as existsSync12, readFileSync as readFileSync12 } from "fs";
1395
- import { join as join12 } from "path";
1651
+ import { existsSync as existsSync14, readFileSync as readFileSync13 } from "fs";
1652
+ import { join as join14 } from "path";
1396
1653
  var MAX_ARTIFACT_BYTES = 4000;
1397
1654
  function tail(text, maxBytes) {
1398
1655
  if (text.length <= maxBytes)
@@ -1421,11 +1678,11 @@ var reflectTool = tool15({
1421
1678
  ];
1422
1679
  let found = 0;
1423
1680
  for (const [rel, label] of ARTIFACT_PATHS) {
1424
- const full = join12(root, rel);
1425
- if (!existsSync12(full))
1681
+ const full = join14(root, rel);
1682
+ if (!existsSync14(full))
1426
1683
  continue;
1427
1684
  try {
1428
- const raw = readFileSync12(full, "utf-8").trim();
1685
+ const raw = readFileSync13(full, "utf-8").trim();
1429
1686
  if (!raw)
1430
1687
  continue;
1431
1688
  const count = raw.split(`
@@ -1449,14 +1706,14 @@ import { tool as tool16 } from "@opencode-ai/plugin";
1449
1706
 
1450
1707
  // src/services/memory-store.ts
1451
1708
  import { Database } from "bun:sqlite";
1452
- import { existsSync as existsSync13, mkdirSync as mkdirSync8 } from "fs";
1453
- import { join as join13 } from "path";
1709
+ import { existsSync as existsSync15, mkdirSync as mkdirSync10 } from "fs";
1710
+ import { join as join15 } from "path";
1454
1711
  import { homedir } from "os";
1455
- var MEMORY_DIR = join13(homedir(), ".flowdeck-memory");
1456
- var DB_PATH = join13(MEMORY_DIR, "memory.db");
1712
+ var MEMORY_DIR = join15(homedir(), ".flowdeck-memory");
1713
+ var DB_PATH = join15(MEMORY_DIR, "memory.db");
1457
1714
  function ensureDir() {
1458
- if (!existsSync13(MEMORY_DIR)) {
1459
- mkdirSync8(MEMORY_DIR, { recursive: true });
1715
+ if (!existsSync15(MEMORY_DIR)) {
1716
+ mkdirSync10(MEMORY_DIR, { recursive: true });
1460
1717
  }
1461
1718
  }
1462
1719
  var db = null;
@@ -1734,16 +1991,16 @@ var memorySearchTool = tool16({
1734
1991
  // src/tools/memory-status.ts
1735
1992
  import { tool as tool17 } from "@opencode-ai/plugin";
1736
1993
  import { Database as Database2 } from "bun:sqlite";
1737
- import { existsSync as existsSync14 } from "fs";
1738
- import { join as join14 } from "path";
1994
+ import { existsSync as existsSync16 } from "fs";
1995
+ import { join as join16 } from "path";
1739
1996
  import { homedir as homedir2 } from "os";
1740
- var DB_PATH2 = join14(homedir2(), ".flowdeck-memory", "memory.db");
1997
+ var DB_PATH2 = join16(homedir2(), ".flowdeck-memory", "memory.db");
1741
1998
  var memoryStatusTool = tool17({
1742
1999
  description: "Check FlowDeck memory database status, statistics, and recent sessions",
1743
2000
  args: {},
1744
2001
  async execute(_args, _context) {
1745
2002
  try {
1746
- const exists = existsSync14(DB_PATH2);
2003
+ const exists = existsSync16(DB_PATH2);
1747
2004
  const result = {
1748
2005
  database_exists: exists,
1749
2006
  path: DB_PATH2,
@@ -1900,15 +2157,58 @@ var memoryHook = {
1900
2157
  };
1901
2158
 
1902
2159
  // src/hooks/guard-rails.ts
1903
- import { existsSync as existsSync15, readFileSync as readFileSync13 } from "fs";
1904
- import { join as join15 } from "path";
2160
+ import { existsSync as existsSync18, readFileSync as readFileSync15 } from "fs";
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
1905
2205
  var PLANNING_DIR2 = ".planning";
1906
2206
  var CONFIG_FILE = "config.json";
1907
2207
  var STATE_FILE2 = "STATE.md";
1908
2208
  function resolveExecutionMode(configPath, trustScore, volatility) {
1909
- if (existsSync15(configPath)) {
2209
+ if (existsSync18(configPath)) {
1910
2210
  try {
1911
- const config = JSON.parse(readFileSync13(configPath, "utf-8"));
2211
+ const config = JSON.parse(readFileSync15(configPath, "utf-8"));
1912
2212
  if (config.execution_mode === "review-only")
1913
2213
  return "review-only";
1914
2214
  if (config.execution_mode === "guarded")
@@ -1962,22 +2262,22 @@ async function guardRailsHook(ctx, input, _output) {
1962
2262
  if (!ENABLED)
1963
2263
  return;
1964
2264
  const dir = ctx.directory;
1965
- const planningDirPath = join15(dir, PLANNING_DIR2);
2265
+ const planningDirPath = join18(dir, PLANNING_DIR2);
1966
2266
  const codebaseDirectory = codebaseDir(dir);
1967
- const configPath = join15(planningDirPath, CONFIG_FILE);
1968
- const statePath2 = join15(planningDirPath, STATE_FILE2);
2267
+ const configPath = join18(planningDirPath, CONFIG_FILE);
2268
+ const statePath2 = join18(planningDirPath, STATE_FILE2);
1969
2269
  const workspaceRoot = findWorkspaceRoot(dir);
1970
2270
  if (workspaceRoot && dir !== workspaceRoot) {
1971
2271
  const config = getWorkspaceConfig(dir);
1972
- if (config && config.workspace_mode === "shared" && !existsSync15(planningDirPath)) {
2272
+ if (config && config.workspace_mode === "shared" && !existsSync18(planningDirPath)) {
1973
2273
  const msg = `No .planning/ in this sub-repo. Switch to workspace root: cd ${workspaceRoot}`;
1974
2274
  throw new Error(`[flowdeck] BLOCK: ${msg}`);
1975
2275
  }
1976
2276
  }
1977
2277
  if (input.tool === "write" || input.tool === "edit") {
1978
- if (!existsSync15(planningDirPath))
2278
+ if (!existsSync18(planningDirPath))
1979
2279
  return;
1980
- if (!existsSync15(codebaseDirectory)) {
2280
+ if (!existsSync18(codebaseDirectory)) {
1981
2281
  throw new Error(`[flowdeck] WARNING: .codebase/ not found. Run /map-codebase to map the codebase.`);
1982
2282
  }
1983
2283
  const execMode = resolveExecutionMode(configPath, null);
@@ -1987,6 +2287,10 @@ async function guardRailsHook(ctx, input, _output) {
1987
2287
  if (execMode === "guarded") {
1988
2288
  throw new Error(`[flowdeck] GUARDED MODE: edit will proceed but flag for human review.`);
1989
2289
  }
2290
+ const designGateMessage = getDesignGateMessage(dir);
2291
+ if (designGateMessage) {
2292
+ throw new Error(designGateMessage);
2293
+ }
1990
2294
  const effectiveSeverity = getEffectiveSeverity(configPath, statePath2);
1991
2295
  if (effectiveSeverity === null)
1992
2296
  return;
@@ -2009,10 +2313,35 @@ async function guardRailsHook(ctx, input, _output) {
2009
2313
  }
2010
2314
  }
2011
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
+ }
2012
2341
  function effectiveSeverity(configPath, statePath2) {
2013
- if (existsSync15(configPath)) {
2342
+ if (existsSync18(configPath)) {
2014
2343
  try {
2015
- const configContent = readFileSync13(configPath, "utf-8");
2344
+ const configContent = readFileSync15(configPath, "utf-8");
2016
2345
  const config = JSON.parse(configContent);
2017
2346
  if (config.guard_enforcement === "warn")
2018
2347
  return "warn";
@@ -2028,10 +2357,10 @@ function getEffectiveSeverity(configPath, statePath2) {
2028
2357
  return effectiveSeverity(configPath, statePath2);
2029
2358
  }
2030
2359
  function getPlanConfirmed(statePath2) {
2031
- if (!existsSync15(statePath2))
2360
+ if (!existsSync18(statePath2))
2032
2361
  return false;
2033
2362
  try {
2034
- const content = readFileSync13(statePath2, "utf-8");
2363
+ const content = readFileSync15(statePath2, "utf-8");
2035
2364
  const match = content.match(/plan_confirmed:\s*(true|false)/i);
2036
2365
  return match ? match[1].toLowerCase() === "true" : false;
2037
2366
  } catch {
@@ -2039,21 +2368,21 @@ function getPlanConfirmed(statePath2) {
2039
2368
  }
2040
2369
  }
2041
2370
  function getWarningMessage(planningDir2) {
2042
- if (!existsSync15(join15(planningDir2, STATE_FILE2))) {
2371
+ if (!existsSync18(join18(planningDir2, STATE_FILE2))) {
2043
2372
  return "No .planning/ found. Run /new-project first.";
2044
2373
  }
2045
2374
  return "Plan not confirmed. Run /plan and confirm to enable execution.";
2046
2375
  }
2047
2376
  function getBlockMessage(planningDir2) {
2048
- if (!existsSync15(join15(planningDir2, STATE_FILE2))) {
2377
+ if (!existsSync18(join18(planningDir2, STATE_FILE2))) {
2049
2378
  return "No .planning/ found. Run /new-project first.";
2050
2379
  }
2051
2380
  return "Plan not confirmed. Run /plan and confirm to enable execution.";
2052
2381
  }
2053
2382
 
2054
2383
  // src/hooks/tool-guard.ts
2055
- import { existsSync as existsSync16, readFileSync as readFileSync14 } from "fs";
2056
- import { join as join16 } from "path";
2384
+ import { existsSync as existsSync19, readFileSync as readFileSync16 } from "fs";
2385
+ import { join as join19 } from "path";
2057
2386
  var IS_ENABLED = () => process.env.FLOWDECK_TOOL_GUARD_ENABLED === "on";
2058
2387
  var BLOCKED_PATTERNS = {
2059
2388
  read: [".env", ".pem", ".key", ".secret"],
@@ -2100,11 +2429,11 @@ function isBlocked(tool18, args) {
2100
2429
  return null;
2101
2430
  }
2102
2431
  function checkArchConstraint(directory, filePath) {
2103
- const constraintsPath = join16(codebaseDir(directory), "CONSTRAINTS.md");
2104
- if (!existsSync16(constraintsPath))
2432
+ const constraintsPath = join19(codebaseDir(directory), "CONSTRAINTS.md");
2433
+ if (!existsSync19(constraintsPath))
2105
2434
  return null;
2106
2435
  try {
2107
- const content = readFileSync14(constraintsPath, "utf-8");
2436
+ const content = readFileSync16(constraintsPath, "utf-8");
2108
2437
  const match = content.match(/## Forbidden Paths\n([\s\S]*?)(?:\n##|$)/);
2109
2438
  if (!match)
2110
2439
  return null;
@@ -2121,12 +2450,37 @@ function checkArchConstraint(directory, filePath) {
2121
2450
  function checkPhaseEnforcement(directory) {
2122
2451
  try {
2123
2452
  const state = readPlanningState(directory);
2453
+ const flowdeckConfig = resolveDesignFirstConfig(loadFlowDeckConfig(directory));
2124
2454
  if (state.phase > 0 && state.phase < 3) {
2125
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.`;
2126
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
+ }
2127
2463
  } catch {}
2128
2464
  return null;
2129
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
+ }
2130
2484
  async function toolGuardHook(ctx, input, output) {
2131
2485
  if (!IS_ENABLED())
2132
2486
  return;
@@ -2151,18 +2505,18 @@ async function toolGuardHook(ctx, input, output) {
2151
2505
  }
2152
2506
 
2153
2507
  // src/hooks/session-start.ts
2154
- import { existsSync as existsSync17, readFileSync as readFileSync15 } from "fs";
2508
+ import { existsSync as existsSync20, readFileSync as readFileSync17 } from "fs";
2155
2509
  async function sessionStartHook(ctx) {
2156
2510
  const planningDir2 = ctx.directory + "/.planning";
2157
2511
  const codebaseDirectory = codebaseDir(ctx.directory);
2158
2512
  const workspaceRoot = findWorkspaceRoot(ctx.directory);
2159
2513
  const config = workspaceRoot ? getWorkspaceConfig(ctx.directory) : null;
2160
- if (!existsSync17(planningDir2)) {
2514
+ if (!existsSync20(planningDir2)) {
2161
2515
  return {
2162
2516
  flowdeck_phase: null,
2163
2517
  flowdeck_status: "no_plan",
2164
2518
  flowdeck_warning: "Run /new-project or /map-codebase to initialize.",
2165
- flowdeck_has_codebase: existsSync17(codebaseDirectory),
2519
+ flowdeck_has_codebase: existsSync20(codebaseDirectory),
2166
2520
  ...workspaceRoot && config?.sub_repos ? {
2167
2521
  flowdeck_workspace_root: workspaceRoot,
2168
2522
  flowdeck_sub_repos: config.sub_repos,
@@ -2173,7 +2527,7 @@ async function sessionStartHook(ctx) {
2173
2527
  }
2174
2528
  try {
2175
2529
  const stateFilePath = statePath(ctx.directory);
2176
- const content = readFileSync15(stateFilePath, "utf-8");
2530
+ const content = readFileSync17(stateFilePath, "utf-8");
2177
2531
  const state = parseState(content);
2178
2532
  const currentPhase = state["current_phase"] || {};
2179
2533
  const result = {
@@ -2181,7 +2535,7 @@ async function sessionStartHook(ctx) {
2181
2535
  flowdeck_status: currentPhase["status"] ?? null,
2182
2536
  flowdeck_steps_pending: currentPhase["steps_pending"] ?? null,
2183
2537
  flowdeck_last_action: currentPhase["last_action"] ?? null,
2184
- flowdeck_has_codebase: existsSync17(codebaseDirectory)
2538
+ flowdeck_has_codebase: existsSync20(codebaseDirectory)
2185
2539
  };
2186
2540
  if (workspaceRoot && config?.sub_repos && config.sub_repos.length > 0) {
2187
2541
  result.flowdeck_workspace_root = workspaceRoot;
@@ -2196,7 +2550,7 @@ async function sessionStartHook(ctx) {
2196
2550
  flowdeck_phase: null,
2197
2551
  flowdeck_status: "error",
2198
2552
  flowdeck_warning: "State file unreadable. Continuing without flowdeck context.",
2199
- flowdeck_has_codebase: existsSync17(codebaseDirectory)
2553
+ flowdeck_has_codebase: existsSync20(codebaseDirectory)
2200
2554
  };
2201
2555
  if (workspaceRoot && config?.sub_repos && config.sub_repos.length > 0) {
2202
2556
  result.flowdeck_workspace_root = workspaceRoot;
@@ -2262,8 +2616,8 @@ function notifyPermissionNeeded(tool18) {
2262
2616
  }
2263
2617
 
2264
2618
  // src/hooks/patch-trust.ts
2265
- import { existsSync as existsSync18, readFileSync as readFileSync16 } from "fs";
2266
- import { join as join17 } from "path";
2619
+ import { existsSync as existsSync21, readFileSync as readFileSync18 } from "fs";
2620
+ import { join as join20 } from "path";
2267
2621
  var HIGH_RISK_KEYWORDS = [
2268
2622
  "password",
2269
2623
  "secret",
@@ -2285,11 +2639,11 @@ var HIGH_RISK_KEYWORDS = [
2285
2639
  "privilege"
2286
2640
  ];
2287
2641
  function loadVolatility(directory) {
2288
- const p = join17(codebaseDir(directory), "VOLATILITY.json");
2289
- if (!existsSync18(p))
2642
+ const p = join20(codebaseDir(directory), "VOLATILITY.json");
2643
+ if (!existsSync21(p))
2290
2644
  return {};
2291
2645
  try {
2292
- const data = JSON.parse(readFileSync16(p, "utf-8"));
2646
+ const data = JSON.parse(readFileSync18(p, "utf-8"));
2293
2647
  const map = {};
2294
2648
  for (const entry of data.entries ?? [])
2295
2649
  map[entry.path] = entry.stability;
@@ -2299,11 +2653,11 @@ function loadVolatility(directory) {
2299
2653
  }
2300
2654
  }
2301
2655
  function loadFailedPaths(directory) {
2302
- const p = join17(codebaseDir(directory), "FAILURES.json");
2303
- if (!existsSync18(p))
2656
+ const p = join20(codebaseDir(directory), "FAILURES.json");
2657
+ if (!existsSync21(p))
2304
2658
  return [];
2305
2659
  try {
2306
- const data = JSON.parse(readFileSync16(p, "utf-8"));
2660
+ const data = JSON.parse(readFileSync18(p, "utf-8"));
2307
2661
  return (data.entries ?? []).flatMap((e) => e.affected_paths ?? []);
2308
2662
  } catch {
2309
2663
  return [];
@@ -2368,8 +2722,8 @@ async function patchTrustHook(ctx, input, output) {
2368
2722
  }
2369
2723
 
2370
2724
  // src/hooks/decision-trace-hook.ts
2371
- import { existsSync as existsSync19, mkdirSync as mkdirSync9, appendFileSync as appendFileSync2 } from "fs";
2372
- import { join as join18 } from "path";
2725
+ import { existsSync as existsSync22, mkdirSync as mkdirSync11, appendFileSync as appendFileSync3 } from "fs";
2726
+ import { join as join21 } from "path";
2373
2727
  async function decisionTraceHook(ctx, input, output) {
2374
2728
  if (input.tool !== "write" && input.tool !== "edit")
2375
2729
  return;
@@ -2378,8 +2732,8 @@ async function decisionTraceHook(ctx, input, output) {
2378
2732
  return;
2379
2733
  const base = codebaseDir(ctx.directory);
2380
2734
  try {
2381
- if (!existsSync19(base))
2382
- mkdirSync9(base, { recursive: true });
2735
+ if (!existsSync22(base))
2736
+ mkdirSync11(base, { recursive: true });
2383
2737
  const entry = {
2384
2738
  timestamp: new Date().toISOString(),
2385
2739
  file_path: filePath,
@@ -2391,62 +2745,87 @@ async function decisionTraceHook(ctx, input, output) {
2391
2745
  risk_level: "unknown",
2392
2746
  auto_recorded: true
2393
2747
  };
2394
- appendFileSync2(join18(base, "DECISIONS.jsonl"), JSON.stringify(entry) + `
2748
+ appendFileSync3(join21(base, "DECISIONS.jsonl"), JSON.stringify(entry) + `
2395
2749
  `, "utf-8");
2396
2750
  } catch {}
2397
2751
  }
2398
2752
 
2399
2753
  // src/services/telemetry.ts
2400
- import { existsSync as existsSync20, readFileSync as readFileSync17, appendFileSync as appendFileSync3, mkdirSync as mkdirSync10 } from "fs";
2401
- import { join as join19 } from "path";
2754
+ import { existsSync as existsSync23, readFileSync as readFileSync19, appendFileSync as appendFileSync4, mkdirSync as mkdirSync12 } from "fs";
2755
+ import { join as join22 } from "path";
2402
2756
  import { randomUUID } from "crypto";
2403
2757
  function telemetryPath(dir) {
2404
- return join19(codebaseDir(dir), "TELEMETRY.jsonl");
2758
+ return join22(codebaseDir(dir), "TELEMETRY.jsonl");
2405
2759
  }
2406
2760
  function appendEvent(dir, partial) {
2407
2761
  if (process.env.TELEMETRY_ENABLED !== "true")
2408
2762
  return null;
2409
2763
  const cd = codebaseDir(dir);
2410
- if (!existsSync20(cd))
2411
- mkdirSync10(cd, { recursive: true });
2764
+ if (!existsSync23(cd))
2765
+ mkdirSync12(cd, { recursive: true });
2412
2766
  const event = {
2413
2767
  id: randomUUID(),
2414
2768
  ts: new Date().toISOString(),
2415
2769
  ...partial
2416
2770
  };
2417
- appendFileSync3(telemetryPath(dir), JSON.stringify(event) + `
2771
+ appendFileSync4(telemetryPath(dir), JSON.stringify(event) + `
2418
2772
  `, "utf-8");
2419
2773
  return event;
2420
2774
  }
2421
2775
 
2422
2776
  // src/hooks/telemetry-hook.ts
2777
+ function resolveIds(toolInput) {
2778
+ const session_id = toolInput.sessionID ?? toolInput.sessionId ?? process.env.OPENCODE_SESSION_ID ?? "session-0";
2779
+ const run_id = toolInput.messageID ?? toolInput.messageId ?? toolInput.runID ?? toolInput.runId ?? process.env.OPENCODE_RUN_ID ?? "run-0";
2780
+ return { session_id, run_id };
2781
+ }
2782
+ function inferStatus(output) {
2783
+ if (output.error)
2784
+ return "error";
2785
+ if (typeof output.output !== "string")
2786
+ return "ok";
2787
+ const text = output.output.trim();
2788
+ if (!text)
2789
+ return "ok";
2790
+ try {
2791
+ const parsed = JSON.parse(text);
2792
+ if (parsed.success === false || parsed.error || parsed.status === "error")
2793
+ return "error";
2794
+ return "ok";
2795
+ } catch {
2796
+ return "ok";
2797
+ }
2798
+ }
2423
2799
  async function telemetryHook(context, toolInput, output) {
2424
2800
  const dir = context.directory ?? process.cwd();
2425
2801
  const tool18 = toolInput.name ?? toolInput.tool ?? "unknown";
2802
+ const ids = resolveIds(toolInput);
2426
2803
  appendEvent(dir, {
2427
- session_id: process.env.OPENCODE_SESSION_ID ?? "session-0",
2428
- run_id: process.env.OPENCODE_RUN_ID ?? "run-0",
2804
+ session_id: ids.session_id,
2805
+ run_id: ids.run_id,
2429
2806
  event: "tool.call",
2430
2807
  tool: tool18,
2431
2808
  status: "ok",
2432
2809
  meta: { parameters: output.args ?? {} }
2433
2810
  });
2434
2811
  }
2435
- async function telemetryAfterHook(context, toolInput, _output) {
2812
+ async function telemetryAfterHook(context, toolInput, output) {
2436
2813
  const dir = context.directory ?? process.cwd();
2437
2814
  const tool18 = toolInput.name ?? toolInput.tool ?? "unknown";
2815
+ const ids = resolveIds(toolInput);
2816
+ const status = inferStatus(output);
2438
2817
  appendEvent(dir, {
2439
- session_id: process.env.OPENCODE_SESSION_ID ?? "session-0",
2440
- run_id: process.env.OPENCODE_RUN_ID ?? "run-0",
2818
+ session_id: ids.session_id,
2819
+ run_id: ids.run_id,
2441
2820
  event: "tool.complete",
2442
2821
  tool: tool18,
2443
- status: "ok"
2822
+ status
2444
2823
  });
2445
2824
  }
2446
2825
 
2447
2826
  // src/services/approval-manager.ts
2448
- import { existsSync as existsSync21, readFileSync as readFileSync18, writeFileSync as writeFileSync13, mkdirSync as mkdirSync11 } from "fs";
2449
- import { join as join20 } from "path";
2827
+ import { existsSync as existsSync24, readFileSync as readFileSync20, writeFileSync as writeFileSync14, mkdirSync as mkdirSync13 } from "fs";
2828
+ import { join as join23 } from "path";
2450
2829
  var APPROVAL_TTL_MS = 30 * 60 * 1000;
2451
2830
  var SENSITIVE_PATTERNS = [
2452
2831
  /auth/i,
@@ -2483,20 +2862,20 @@ function isSensitivePath(filePath) {
2483
2862
  return SENSITIVE_PATTERNS.some((p) => p.test(filePath));
2484
2863
  }
2485
2864
  function approvalsPath(dir) {
2486
- return join20(codebaseDir(dir), "APPROVALS.json");
2865
+ return join23(codebaseDir(dir), "APPROVALS.json");
2487
2866
  }
2488
- function loadStore(dir) {
2867
+ function loadStore2(dir) {
2489
2868
  const p = approvalsPath(dir);
2490
- if (!existsSync21(p))
2869
+ if (!existsSync24(p))
2491
2870
  return { requests: [] };
2492
2871
  try {
2493
- return JSON.parse(readFileSync18(p, "utf-8"));
2872
+ return JSON.parse(readFileSync20(p, "utf-8"));
2494
2873
  } catch {
2495
2874
  return { requests: [] };
2496
2875
  }
2497
2876
  }
2498
2877
  function checkApproval(dir, file_path, command) {
2499
- const store = loadStore(dir);
2878
+ const store = loadStore2(dir);
2500
2879
  const now = Date.now();
2501
2880
  return store.requests.filter((r) => r.status === "approved" && r.resolved_at && (r.file_path === file_path || r.trigger === command) && now - new Date(r.resolved_at).getTime() < APPROVAL_TTL_MS).sort((a, b) => b.resolved_at.localeCompare(a.resolved_at)).at(0) ?? null;
2502
2881
  }
@@ -2585,8 +2964,8 @@ function createContextWindowMonitorHook() {
2585
2964
  }
2586
2965
 
2587
2966
  // src/hooks/shell-env-hook.ts
2588
- import { existsSync as existsSync22, readFileSync as readFileSync19 } from "fs";
2589
- import { join as join21 } from "path";
2967
+ import { existsSync as existsSync25, readFileSync as readFileSync21 } from "fs";
2968
+ import { join as join24 } from "path";
2590
2969
  import { createRequire } from "module";
2591
2970
  var _version;
2592
2971
  function getVersion() {
@@ -2622,7 +3001,7 @@ var MARKER_TO_LANG = {
2622
3001
  };
2623
3002
  function detectPackageManager(root) {
2624
3003
  for (const [lockfile, pm] of Object.entries(LOCKFILE_TO_PM)) {
2625
- if (existsSync22(join21(root, lockfile)))
3004
+ if (existsSync25(join24(root, lockfile)))
2626
3005
  return pm;
2627
3006
  }
2628
3007
  return;
@@ -2631,7 +3010,7 @@ function detectLanguages(root) {
2631
3010
  const langs = [];
2632
3011
  const seen = new Set;
2633
3012
  for (const [marker, lang] of Object.entries(MARKER_TO_LANG)) {
2634
- if (!seen.has(lang) && existsSync22(join21(root, marker))) {
3013
+ if (!seen.has(lang) && existsSync25(join24(root, marker))) {
2635
3014
  langs.push(lang);
2636
3015
  seen.add(lang);
2637
3016
  }
@@ -2639,11 +3018,11 @@ function detectLanguages(root) {
2639
3018
  return langs;
2640
3019
  }
2641
3020
  function readCurrentPhase(root) {
2642
- const statePath2 = join21(root, ".planning", "STATE.md");
2643
- if (!existsSync22(statePath2))
3021
+ const statePath2 = join24(root, ".planning", "STATE.md");
3022
+ if (!existsSync25(statePath2))
2644
3023
  return;
2645
3024
  try {
2646
- const content = readFileSync19(statePath2, "utf-8");
3025
+ const content = readFileSync21(statePath2, "utf-8");
2647
3026
  const match = content.match(/phase:\s*(\S+)/i);
2648
3027
  return match?.[1];
2649
3028
  } catch {
@@ -2737,14 +3116,13 @@ function createSessionIdleHook(client, tracker) {
2737
3116
  if (edited.length > 10) {
2738
3117
  await client.app.log({ body: { service: "flowdeck", level: "info", message: ` … and ${edited.length - 10} more` } }).catch(() => {});
2739
3118
  }
2740
- tracker.clear();
2741
3119
  } catch {}
2742
3120
  };
2743
3121
  }
2744
3122
 
2745
3123
  // src/hooks/compaction-hook.ts
2746
- import { existsSync as existsSync23, readFileSync as readFileSync20 } from "fs";
2747
- import { join as join22 } from "path";
3124
+ import { existsSync as existsSync26, readFileSync as readFileSync22 } from "fs";
3125
+ import { join as join25 } from "path";
2748
3126
  var STRUCTURED_SUMMARY_PROMPT = `
2749
3127
  When summarizing this session, you MUST include the following sections:
2750
3128
 
@@ -2783,11 +3161,11 @@ For each: agent name, status, description, session_id.
2783
3161
  **RESUME, DON'T RESTART.** Use session_id to continue existing sessions.
2784
3162
  `;
2785
3163
  function readPlanningState2(directory) {
2786
- const statePath2 = join22(directory, ".planning", "STATE.md");
2787
- if (!existsSync23(statePath2))
3164
+ const statePath2 = join25(directory, ".planning", "STATE.md");
3165
+ if (!existsSync26(statePath2))
2788
3166
  return null;
2789
3167
  try {
2790
- const content = readFileSync20(statePath2, "utf-8");
3168
+ const content = readFileSync22(statePath2, "utf-8");
2791
3169
  return content.slice(0, 1500);
2792
3170
  } catch {
2793
3171
  return null;
@@ -2871,7 +3249,9 @@ function blockMessage(toolName) {
2871
3249
  ` + `The orchestrator is a coordinator — it must delegate all implementation work.
2872
3250
 
2873
3251
  ` + `Use the \`delegate\` tool to hand this off:
2874
- ` + ` 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
2875
3255
  ` + ` delegate({ agent: "@mapper", prompt: "..." }) — codebase mapping
2876
3256
  ` + ` delegate({ agent: "@researcher", prompt: "..." }) — research / file analysis
2877
3257
  ` + ` delegate({ agent: "@tester", prompt: "..." }) — tests / commands
@@ -2882,13 +3262,24 @@ function blockMessage(toolName) {
2882
3262
  class OrchestratorGuard {
2883
3263
  primarySessionId = null;
2884
3264
  onEvent(event) {
2885
- if ((event.type === "session.created" || event.type === "session.started") && this.primarySessionId === null) {
2886
- const props = event.properties;
2887
- const id = props?.info?.id;
2888
- if (id) {
2889
- this.primarySessionId = id;
3265
+ const eventType = event.type ?? "";
3266
+ if (eventType === "session.deleted") {
3267
+ const deletedId = extractSessionId(event);
3268
+ if (deletedId && deletedId === this.primarySessionId) {
3269
+ this.primarySessionId = null;
2890
3270
  }
3271
+ return;
2891
3272
  }
3273
+ if (eventType !== "session.created" && eventType !== "session.started")
3274
+ return;
3275
+ if (this.primarySessionId !== null)
3276
+ return;
3277
+ const id = extractSessionId(event);
3278
+ if (!id)
3279
+ return;
3280
+ if (extractParentSessionId(event))
3281
+ return;
3282
+ this.primarySessionId = id;
2892
3283
  }
2893
3284
  check(sessionId, toolName) {
2894
3285
  if (DISABLED)
@@ -2904,6 +3295,20 @@ class OrchestratorGuard {
2904
3295
  }
2905
3296
  }
2906
3297
  }
3298
+ function extractSessionId(event) {
3299
+ const props = event.properties;
3300
+ const inner = event.event;
3301
+ const info = props?.info;
3302
+ const id = event.sessionID ?? event.sessionId ?? inner?.sessionID ?? inner?.sessionId ?? info?.id;
3303
+ return id ?? null;
3304
+ }
3305
+ function extractParentSessionId(event) {
3306
+ const props = event.properties;
3307
+ const inner = event.event;
3308
+ const info = props?.info;
3309
+ const parentId = inner?.parentID ?? inner?.parentId ?? info?.parentID ?? info?.parentId;
3310
+ return parentId ?? null;
3311
+ }
2907
3312
 
2908
3313
  // src/hooks/auto-learn-hook.ts
2909
3314
  var MIN_EDITS = 1;
@@ -3048,11 +3453,22 @@ For each incomplete step in PLAN.md:
3048
3453
  5. Re-read STATE.md to confirm state
3049
3454
  6. Move to the next incomplete step
3050
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
+
3051
3464
  ## Agent Team
3052
3465
 
3053
3466
  | Agent | Invoke | Best For |
3054
3467
  |-------|--------|----------|
3055
- | 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 |
3056
3472
  | Researcher | @researcher | API docs, library usage |
3057
3473
  | Tester | @tester | Writing and running tests |
3058
3474
  | Reviewer | @reviewer | Code quality review |
@@ -3063,7 +3479,6 @@ For each incomplete step in PLAN.md:
3063
3479
  | Code Explorer | @code-explorer | Reading unfamiliar code |
3064
3480
  | Debug Specialist | @debug-specialist | Root cause analysis |
3065
3481
  | Build Resolver | @build-error-resolver | Build/compile failures |
3066
- | Parallel Coordinator | @parallel-coordinator | Multi-track parallel work |
3067
3482
  | Doc Updater | @doc-updater | Updating existing docs |
3068
3483
  | Task Splitter | @task-splitter | Decomposing complex tasks |
3069
3484
  | Discusser | @discusser | Requirements extraction |
@@ -3076,12 +3491,13 @@ For each incomplete step in PLAN.md:
3076
3491
  ## Phase State Machine
3077
3492
 
3078
3493
  \`\`\`
3079
- discuss → plan → execute → review
3494
+ discuss → plan → design (for UI-heavy tasks) → execute → review
3080
3495
  \`\`\`
3081
3496
 
3082
3497
  - **discuss**: Requirements extraction with @discusser
3083
3498
  - **plan**: Plan creation with @planner, review with @plan-checker
3084
- - **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
3085
3501
  - **review**: Review with @reviewer, @security-auditor
3086
3502
 
3087
3503
  ## Tracking
@@ -3103,7 +3519,7 @@ If a delegated agent fails:
3103
3519
  3. If still failing, escalate:
3104
3520
 
3105
3521
  \`\`\`
3106
- BLOCKED: @coder failed on step 3 (add payment endpoint).
3522
+ BLOCKED: implementation agent failed on step 3 (add payment endpoint).
3107
3523
  Error: [exact error message]
3108
3524
  Retried once with clarification. Still failing.
3109
3525
 
@@ -3124,11 +3540,26 @@ When a task required unusual human guidance, a novel solution strategy, or expos
3124
3540
 
3125
3541
  Do NOT create a skill for routine tasks. Only capture genuinely novel or reusable patterns.`;
3126
3542
  var AGENT_DESCRIPTIONS = {
3127
- coder: `@coder
3128
- - 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
3550
+ - Permissions: Read/write files
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
3129
3560
  - Permissions: Read/write files
3130
- - Best for: All code implementation tasks
3131
- - **Delegate when:** Implementation work, following a plan`,
3561
+ - Best for: CI/CD, deployment config, infra scripts, and runtime operations
3562
+ - **Delegate when:** Infrastructure, pipeline, or operations implementation work`,
3132
3563
  researcher: `@researcher
3133
3564
  - Role: Researches documentation, APIs, and best practices
3134
3565
  - Permissions: Read files
@@ -3200,11 +3631,6 @@ var AGENT_DESCRIPTIONS = {
3200
3631
  - Permissions: Read/write files
3201
3632
  - Best for: Requirements extraction
3202
3633
  - **Delegate when:** Starting new feature or project phase`,
3203
- "parallel-coordinator": `@parallel-coordinator
3204
- - Role: Coordinates multi-wave parallel execution
3205
- - Permissions: Read files
3206
- - Best for: Multi-track parallel work
3207
- - **Delegate when:** Need to execute multiple tasks in parallel`,
3208
3634
  planner: `@planner
3209
3635
  - Role: Creates detailed implementation plans
3210
3636
  - Permissions: Read files
@@ -3506,7 +3932,7 @@ var createPlanCheckerAgent = (model, customPrompt, customAppendPrompt) => {
3506
3932
  };
3507
3933
 
3508
3934
  // src/agents/coder.ts
3509
- 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.
3510
3936
 
3511
3937
  ## Before Writing Code
3512
3938
 
@@ -3616,11 +4042,62 @@ After implementing, report:
3616
4042
  - Tests added or updated
3617
4043
  - Any deviations from the plan and why
3618
4044
  - Next step ready to execute`;
3619
- var createCoderAgent = (model, customPrompt, customAppendPrompt) => {
3620
- 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);
3621
4074
  return {
3622
- name: "coder",
3623
- description: "Implements features and fixes based on confirmed plans. Follows existing code patterns and project conventions. Use for all code implementation tasks.",
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);
4086
+ return {
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.",
3624
4101
  config: {
3625
4102
  model,
3626
4103
  temperature: 0.1,
@@ -3767,6 +4244,13 @@ var REVIEWER_PROMPT = `You review code for correctness, security, and quality. Y
3767
4244
  4. Apply the checklist below
3768
4245
  5. Report by severity — CRITICAL first, then HIGH, MEDIUM, PASS
3769
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
+
3770
4254
  ## Security Checklist — CRITICAL
3771
4255
 
3772
4256
  **Hardcoded credentials:**
@@ -3971,7 +4455,7 @@ Never fabricate information to appear more helpful.
3971
4455
  ## Scope Boundaries
3972
4456
 
3973
4457
  - Report facts only. Do not make implementation decisions.
3974
- - 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.
3975
4459
  - If you find a better approach than what was requested, mention it as an option — do not substitute it.
3976
4460
 
3977
4461
  ## Research Areas
@@ -4091,7 +4575,7 @@ var createWriterAgent = (model, customPrompt, customAppendPrompt) => {
4091
4575
  };
4092
4576
 
4093
4577
  // src/agents/security-auditor.ts
4094
- 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).
4095
4579
 
4096
4580
  ## Audit Scope
4097
4581
 
@@ -4194,7 +4678,7 @@ For high/critical vulnerabilities: report exact package, CVE ID, and whether it'
4194
4678
 
4195
4679
  ## After Finding Issues
4196
4680
 
4197
- 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.`;
4198
4682
  var createSecurityAuditorAgent = (model, customPrompt, customAppendPrompt) => {
4199
4683
  const prompt = resolvePrompt(SECURITY_AUDITOR_PROMPT, customPrompt, customAppendPrompt);
4200
4684
  return {
@@ -4542,7 +5026,7 @@ request → router → UserController.create() → UserService.create() → ❌
4542
5026
 
4543
5027
  ## Scope
4544
5028
 
4545
- 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.`;
4546
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.
4547
5031
 
4548
5032
  ## Diagnostic Commands
@@ -4645,7 +5129,7 @@ npx tsc --noEmit src/path/to/file.ts
4645
5129
 
4646
5130
  - Build fails because of architectural problems → @architect
4647
5131
  - A feature is not working correctly → @debug-specialist
4648
- - Missing functionality needs to be written → @coder`;
5132
+ - Missing functionality needs to be written → @backend-coder/@frontend-coder/@devops`;
4649
5133
  var createDebugSpecialistAgent = (model, customPrompt, customAppendPrompt) => {
4650
5134
  const prompt = resolvePrompt(DEBUG_SPECIALIST_PROMPT, customPrompt, customAppendPrompt);
4651
5135
  return {
@@ -4672,7 +5156,7 @@ var createBuildErrorResolverAgent = (model, customPrompt, customAppendPrompt) =>
4672
5156
  };
4673
5157
 
4674
5158
  // src/agents/specialist.ts
4675
- 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.
4676
5160
 
4677
5161
  ## Wave-Structured Output
4678
5162
 
@@ -4682,7 +5166,7 @@ var TASK_SPLITTER_PROMPT = `You decompose complex tasks into parallel workstream
4682
5166
  ### Wave 1 (parallel — start simultaneously)
4683
5167
 
4684
5168
  **Track A — [description]**
4685
- - Agent: @coder
5169
+ - Agent: @backend-coder
4686
5170
  - Files: \`src/auth/user.ts\`, \`src/auth/types.ts\`
4687
5171
  - Task: [specific implementation task]
4688
5172
  - Verify: [how to confirm it's done]
@@ -4702,7 +5186,7 @@ var TASK_SPLITTER_PROMPT = `You decompose complex tasks into parallel workstream
4702
5186
  ### Wave 2 (after Wave 1 completes)
4703
5187
 
4704
5188
  **Track D — Integration**
4705
- - Agent: @coder
5189
+ - Agent: @backend-coder
4706
5190
  - Depends on: Track A, Track C
4707
5191
  - Task: Wire together outputs from Wave 1
4708
5192
 
@@ -4742,7 +5226,9 @@ After Wave 2: @reviewer reviews all changes together
4742
5226
  | Agent | Best For |
4743
5227
  |-------|---------|
4744
5228
  | @architect | Interface contracts, ADRs |
4745
- | @coder | Implementation |
5229
+ | @backend-coder | Backend implementation |
5230
+ | @frontend-coder | Frontend implementation |
5231
+ | @devops | Infrastructure implementation |
4746
5232
  | @researcher | API docs, library research |
4747
5233
  | @tester | Test writing and coverage |
4748
5234
  | @reviewer | Code quality review |
@@ -4880,200 +5366,6 @@ Discussion is complete when:
4880
5366
  - No open questions remain
4881
5367
 
4882
5368
  Report: "Requirements gathering complete. N decisions recorded. Ready for /plan."`;
4883
- 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.
4884
-
4885
- ## Your Outputs
4886
-
4887
- 1. **WAVE TABLE** — printed at job start, shows every agent slot and its dependencies
4888
- 2. **Agent briefings** — full context packet per agent (they are stateless — give them everything)
4889
- 3. **Wave reports** — status after each wave closes
4890
- 4. **Merge resolution** — reconcile outputs when two tracks touched the same conceptual area
4891
-
4892
- ## WAVE TABLE Format
4893
-
4894
- Print this at the start of every job before delegating any agents:
4895
-
4896
- \`\`\`
4897
- ╔══════════════════════════════════════════════════════════════╗
4898
- ║ WAVE TABLE — [Job Title] ║
4899
- ╠══════════════════════════════════════════════════════════════╣
4900
- ║ Wave 1 (parallel) │ @researcher + @code-explorer ║
4901
- ║ Wave 2 (serial) │ @architect ║
4902
- ║ Wave 3 (parallel) │ @coder + @tester ║
4903
- ║ Wave 4 (parallel) │ @reviewer + @security-auditor ║
4904
- ╠══════════════════════════════════════════════════════════════╣
4905
- ║ Est. sequential: │ 8h ║
4906
- ║ Est. parallel: │ 4.5h ║
4907
- ║ Dependency locks: │ Wave 3 blocked on Wave 2 output ║
4908
- ╚══════════════════════════════════════════════════════════════╝
4909
- \`\`\`
4910
-
4911
- Adjust lanes based on actual task content. Remove any wave whose agents have no work.
4912
-
4913
- ## Standard Wave Delegation Syntax
4914
-
4915
- **Wave 1 — Discovery (parallelize):**
4916
- \`\`\`
4917
- @researcher: [exact research task with sources to check]
4918
- @code-explorer: [exact files/modules to map — list paths]
4919
- \`\`\`
4920
- Start both simultaneously. Do not wait for one before sending the other.
4921
-
4922
- **Wave 2 — Architecture (serial, depends on Wave 1):**
4923
- \`\`\`
4924
- @architect: [design task — attach Wave 1 outputs as context]
4925
- \`\`\`
4926
- One agent. Must complete before Wave 3 starts.
4927
-
4928
- **Wave 3 — Implementation (parallelize, depends on Wave 2):**
4929
- \`\`\`
4930
- @coder: [implementation task — attach @architect output + relevant Wave 1 findings]
4931
- @tester: [test task — attach interface contracts from @architect, NOT @coder output]
4932
- \`\`\`
4933
- Start both simultaneously once Wave 2 output is in hand. @tester works from contracts, not @coder's code, so they are truly parallel.
4934
-
4935
- **Wave 4 — Validation (parallelize):**
4936
- \`\`\`
4937
- @reviewer: [review scope — list files changed by Wave 3]
4938
- @security-auditor: [audit scope — list entry points, auth surfaces, data flows]
4939
- \`\`\`
4940
- Start both once Wave 3 is complete.
4941
-
4942
- ## Parallelism Rules
4943
-
4944
- **Safe to parallelize:**
4945
- - Tasks touching different files with no shared output
4946
- - Research alongside implementation (research produces inputs, not outputs of implementation)
4947
- - Test writing from interface contracts alongside implementation
4948
- - Documentation alongside implementation when writing to different files
4949
-
4950
- **Must be sequential:**
4951
- - Task B's design depends on decisions Task A makes
4952
- - Task B reads a file Task A will write
4953
- - Both tasks modify the same file
4954
-
4955
- **Not worth parallelizing:**
4956
- - Total estimated work is under 20 minutes
4957
- - File ownership is ambiguous — if unclear who owns a file, serialize it
4958
-
4959
- ## Agent Team
4960
-
4961
- | Agent | Best For |
4962
- |-------|---------|
4963
- | @architect | Interface contracts, ADRs, system design |
4964
- | @coder | All code implementation |
4965
- | @researcher | API docs, library usage, best practices |
4966
- | @tester | Test writing and coverage |
4967
- | @reviewer | Code quality review |
4968
- | @security-auditor | Security vulnerability audit |
4969
- | @writer | New documentation |
4970
- | @doc-updater | Updating existing documentation |
4971
- | @code-explorer | Mapping unfamiliar code |
4972
- | @debug-specialist | Root cause analysis |
4973
- | @build-error-resolver | Build and compile failures |
4974
-
4975
- ## Merging Parallel Outputs
4976
-
4977
- 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):
4978
-
4979
- **Step 1 — Detect the overlap.** After each wave, compare the file sets each agent reported touching. Any overlap is a merge candidate.
4980
-
4981
- **Step 2 — Classify the overlap:**
4982
- - **Additive** (different functions in the same file): safe to auto-merge, reconcile manually.
4983
- - **Structural** (same type, same interface, same function signature): do not auto-merge — escalate.
4984
- - **Contradictory** (one agent added a field, another removed it): escalate.
4985
-
4986
- **Step 3 — Resolve:**
4987
- - Additive: apply both changesets, verify no symbol collisions, verify tests pass.
4988
- - Structural or contradictory: invoke the conflict resolution protocol below.
4989
-
4990
- ## Conflict Resolution Protocol
4991
-
4992
- Trigger when two tracks produced incompatible changes to the same logical unit.
4993
-
4994
- \`\`\`
4995
- CONFLICT DETECTED
4996
- Track A (@coder): added \`refreshToken: string\` to UserSession in src/types/session.ts
4997
- Track B (@tester): wrote tests assuming UserSession has no refresh field
4998
- Classification: Structural — interface mismatch
4999
-
5000
- RESOLUTION PLAN
5001
- 1. Suspend Track B output (do not apply tests yet)
5002
- 2. Delegate to @coder: reconcile both versions sequentially
5003
- - Brief: "Track A and Track B produced incompatible changes. [Attach both outputs.]
5004
- Produce a single unified version that satisfies both intents."
5005
- 3. Once @coder delivers unified version: re-run @tester against it
5006
- 4. Mark original conflict as resolved, continue to Wave 4
5007
- \`\`\`
5008
-
5009
- Never silently pick one side. Always surface what was lost in the merge and why.
5010
-
5011
- ## Failure Handling
5012
-
5013
- **Wave failure does not block independent waves.**
5014
-
5015
- Before each wave starts, classify each task as:
5016
- - **Blocking** — downstream waves need its output
5017
- - **Independent** — downstream waves do not depend on it
5018
-
5019
- If a blocking task fails:
5020
- \`\`\`
5021
- Wave 1 FAILURE — @researcher: could not retrieve bcrypt API docs
5022
- Impact: Wave 3 @coder task "implement password hashing" is blocked.
5023
- Action: Pause that specific Wave 3 slot. Continue all other Wave 3 slots.
5024
- Retry: Re-run @researcher with a fallback source list, then unblock the Wave 3 slot.
5025
- \`\`\`
5026
-
5027
- If an independent task fails:
5028
- \`\`\`
5029
- Wave 4 FAILURE — @security-auditor: process timed out
5030
- Impact: None — @reviewer completed independently.
5031
- Action: Log failure. Do not block Wave 4 close. Re-run @security-auditor as a follow-up.
5032
- \`\`\`
5033
-
5034
- Wave gates work per-slot, not per-wave: a wave closes when all blocking slots complete. Independent failures are retried async.
5035
-
5036
- ## Full Execution Report Format
5037
-
5038
- \`\`\`markdown
5039
- ## Parallel Execution Report — [Job Title]
5040
-
5041
- ### Wave 1 Results (Discovery)
5042
- | Track | Agent | Status | Output |
5043
- |-------|-------|--------|--------|
5044
- | A | @researcher | ✅ | \`.planning/research/bcrypt.md\` |
5045
- | B | @code-explorer | ✅ | \`.codebase/auth-module-map.md\` |
5046
-
5047
- ### Wave 1 → Wave 2 Gate
5048
- - All blocking slots complete: ✅
5049
- - Merge check: no file conflicts
5050
-
5051
- ### Wave 2 Results (Architecture)
5052
- | Track | Agent | Status | Output |
5053
- |-------|-------|--------|--------|
5054
- | A | @architect | ✅ | \`.planning/adr/auth-design.md\`, interface contracts |
5055
-
5056
- ### Wave 3 Results (Implementation)
5057
- | Track | Agent | Status | Output |
5058
- |-------|-------|--------|--------|
5059
- | A | @coder | ✅ | \`src/auth/service.ts\`, \`src/auth/session.ts\` |
5060
- | B | @tester | ✅ | \`src/auth/service.test.ts\` — 14 tests, 14 passing |
5061
-
5062
- ### Wave 3 Merge Check
5063
- - File overlap: none
5064
- - Conceptual overlap: @coder and @tester both reference UserSession — compatible ✅
5065
-
5066
- ### Wave 4 Results (Validation)
5067
- | Track | Agent | Status | Output |
5068
- |-------|-------|--------|--------|
5069
- | A | @reviewer | ✅ | 2 non-blocking suggestions filed |
5070
- | B | @security-auditor | ⚠️ FAILED | Timeout — retrying async |
5071
-
5072
- ### Final Status
5073
- - All blocking work complete ✅
5074
- - @security-auditor re-run scheduled as follow-up
5075
- - Elapsed: 4h 20m (vs 8h sequential)
5076
- \`\`\``;
5077
5369
  var createTaskSplitterAgent = (model, customPrompt, customAppendPrompt) => {
5078
5370
  const prompt = resolvePrompt(TASK_SPLITTER_PROMPT, customPrompt, customAppendPrompt);
5079
5371
  return {
@@ -5098,18 +5390,6 @@ var createDiscusserAgent = (model, customPrompt, customAppendPrompt) => {
5098
5390
  }
5099
5391
  };
5100
5392
  };
5101
- var createParallelCoordinatorAgent = (model, customPrompt, customAppendPrompt) => {
5102
- const prompt = resolvePrompt(PARALLEL_COORDINATOR_PROMPT, customPrompt, customAppendPrompt);
5103
- return {
5104
- name: "parallel-coordinator",
5105
- description: "Coordinates parallel agent execution for multi-track workstreams. Manages wave execution, handles merge conflicts, and maximizes throughput.",
5106
- config: {
5107
- model,
5108
- temperature: 0.1,
5109
- prompt
5110
- }
5111
- };
5112
- };
5113
5393
 
5114
5394
  // src/agents/architect.ts
5115
5395
  var ARCHITECT_PROMPT = `You design system architecture, create Architecture Decision Records (ADRs), and define API contracts before implementation begins.
@@ -5756,11 +6036,81 @@ function createAutoLearnerAgent(model) {
5756
6036
  return definition;
5757
6037
  }
5758
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
+
5759
6107
  // src/agents/index.ts
5760
6108
  var AGENT_NAMES = [
5761
6109
  "orchestrator",
5762
6110
  "planner",
5763
- "coder",
6111
+ "backend-coder",
6112
+ "frontend-coder",
6113
+ "devops",
5764
6114
  "plan-checker",
5765
6115
  "tester",
5766
6116
  "reviewer",
@@ -5774,13 +6124,13 @@ var AGENT_NAMES = [
5774
6124
  "build-error-resolver",
5775
6125
  "task-splitter",
5776
6126
  "discusser",
5777
- "parallel-coordinator",
5778
6127
  "architect",
5779
6128
  "risk-analyst",
5780
6129
  "policy-enforcer",
5781
6130
  "performance-optimizer",
5782
6131
  "refactor-guide",
5783
- "auto-learner"
6132
+ "auto-learner",
6133
+ "design"
5784
6134
  ];
5785
6135
  var PRIMARY_AGENTS = new Set(["orchestrator"]);
5786
6136
  var ALL_MODES_AGENTS = new Set;
@@ -5800,8 +6150,12 @@ function createAgent(name, model, customPrompt, customAppendPrompt) {
5800
6150
  return createOrchestratorAgent(model, customPrompt, customAppendPrompt);
5801
6151
  case "planner":
5802
6152
  return createPlannerAgent(model, customPrompt, customAppendPrompt);
5803
- case "coder":
5804
- 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);
5805
6159
  case "plan-checker":
5806
6160
  return createPlanCheckerAgent(model, customPrompt, customAppendPrompt);
5807
6161
  case "tester":
@@ -5828,8 +6182,6 @@ function createAgent(name, model, customPrompt, customAppendPrompt) {
5828
6182
  return createTaskSplitterAgent(model, customPrompt, customAppendPrompt);
5829
6183
  case "discusser":
5830
6184
  return createDiscusserAgent(model, customPrompt, customAppendPrompt);
5831
- case "parallel-coordinator":
5832
- return createParallelCoordinatorAgent(model, customPrompt, customAppendPrompt);
5833
6185
  case "architect":
5834
6186
  return createArchitectAgent(model, customPrompt, customAppendPrompt);
5835
6187
  case "risk-analyst":
@@ -5842,6 +6194,8 @@ function createAgent(name, model, customPrompt, customAppendPrompt) {
5842
6194
  return createRefactorGuideAgent(model, customPrompt, customAppendPrompt);
5843
6195
  case "auto-learner":
5844
6196
  return createAutoLearnerAgent(model);
6197
+ case "design":
6198
+ return createDesignAgent(model, customPrompt, customAppendPrompt);
5845
6199
  default:
5846
6200
  console.warn(`[flowdeck] Unknown agent: ${name}`);
5847
6201
  return;
@@ -5879,42 +6233,16 @@ function getAgentConfigs(agentModels) {
5879
6233
  return configs;
5880
6234
  }
5881
6235
 
5882
- // src/config/loader.ts
5883
- import { existsSync as existsSync24, readFileSync as readFileSync21 } from "fs";
5884
- import { join as join23 } from "path";
5885
- import { homedir as homedir3 } from "os";
5886
- var CONFIG_FILENAME = "flowdeck.json";
5887
- function getGlobalConfigDir() {
5888
- return process.env.OPENCODE_CONFIG_DIR || (process.env.XDG_CONFIG_HOME ? join23(process.env.XDG_CONFIG_HOME, "opencode") : join23(homedir3(), ".config", "opencode"));
5889
- }
5890
- function loadFlowDeckConfig(directory) {
5891
- const candidates = [];
5892
- if (directory) {
5893
- candidates.push(join23(directory, ".opencode", CONFIG_FILENAME));
5894
- }
5895
- candidates.push(join23(getGlobalConfigDir(), CONFIG_FILENAME));
5896
- for (const configPath of candidates) {
5897
- if (existsSync24(configPath)) {
5898
- try {
5899
- const content = readFileSync21(configPath, "utf-8");
5900
- return JSON.parse(content);
5901
- } catch {
5902
- console.warn(`[flowdeck] Failed to load config from ${configPath}`);
5903
- }
5904
- }
5905
- }
5906
- return {};
5907
- }
5908
6236
  // src/index.ts
5909
6237
  function loadRulePaths() {
5910
6238
  const __dir = dirname4(fileURLToPath2(import.meta.url));
5911
- const rulesDir = join24(__dir, "..", "src", "rules");
5912
- if (!existsSync25(rulesDir))
6239
+ const rulesDir = join26(__dir, "..", "src", "rules");
6240
+ if (!existsSync27(rulesDir))
5913
6241
  return [];
5914
6242
  const paths = [];
5915
6243
  function walk(dir) {
5916
6244
  for (const entry of readdirSync3(dir, { withFileTypes: true })) {
5917
- const full = join24(dir, entry.name);
6245
+ const full = join26(dir, entry.name);
5918
6246
  if (entry.isDirectory()) {
5919
6247
  walk(full);
5920
6248
  } else if (entry.isFile() && entry.name.endsWith(".md") && entry.name !== "README.md") {
@@ -5927,8 +6255,8 @@ function loadRulePaths() {
5927
6255
  }
5928
6256
  function loadCommands() {
5929
6257
  const __dir = dirname4(fileURLToPath2(import.meta.url));
5930
- const commandsDir = join24(__dir, "..", "src", "commands");
5931
- if (!existsSync25(commandsDir))
6258
+ const commandsDir = join26(__dir, "..", "src", "commands");
6259
+ if (!existsSync27(commandsDir))
5932
6260
  return {};
5933
6261
  const commands = {};
5934
6262
  try {
@@ -5936,7 +6264,7 @@ function loadCommands() {
5936
6264
  if (!file.endsWith(".md"))
5937
6265
  continue;
5938
6266
  const name = basename(file, ".md");
5939
- const raw = readFileSync22(join24(commandsDir, file), "utf-8");
6267
+ const raw = readFileSync23(join26(commandsDir, file), "utf-8");
5940
6268
  let description;
5941
6269
  let template = raw;
5942
6270
  const fmMatch = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/);
@@ -5977,12 +6305,16 @@ var plugin = async (input, _options) => {
5977
6305
  cfg.default_agent = "orchestrator";
5978
6306
  }
5979
6307
  const flowdeckConfig = loadFlowDeckConfig(directory);
6308
+ const designFirstConfig = resolveDesignFirstConfig(flowdeckConfig);
5980
6309
  const agentModels = {};
5981
6310
  for (const [name, agentCfg] of Object.entries(flowdeckConfig.agents ?? {})) {
5982
6311
  if (agentCfg.model) {
5983
6312
  agentModels[name] = agentCfg.model;
5984
6313
  }
5985
6314
  }
6315
+ if (designFirstConfig.modelOverrides.design) {
6316
+ agentModels.design = designFirstConfig.modelOverrides.design;
6317
+ }
5986
6318
  const resolvedAgentConfigs = getAgentConfigs(agentModels);
5987
6319
  if (!cfg.agent) {
5988
6320
  cfg.agent = { ...resolvedAgentConfigs };
@@ -6013,8 +6345,8 @@ var plugin = async (input, _options) => {
6013
6345
  }
6014
6346
  }
6015
6347
  }
6016
- const skillsDir = join24(dirname4(fileURLToPath2(import.meta.url)), "..", "src", "skills");
6017
- if (existsSync25(skillsDir)) {
6348
+ const skillsDir = join26(dirname4(fileURLToPath2(import.meta.url)), "..", "src", "skills");
6349
+ if (existsSync27(skillsDir)) {
6018
6350
  const cfgAny = cfg;
6019
6351
  if (!cfgAny.skills || typeof cfgAny.skills !== "object") {
6020
6352
  cfgAny.skills = { paths: [] };
@@ -6100,8 +6432,12 @@ var plugin = async (input, _options) => {
6100
6432
  await contextMonitor.event({ event });
6101
6433
  orchestratorGuard.onEvent(event);
6102
6434
  if (type === "session.idle") {
6103
- await sessionIdleHook();
6104
- await autoLearnHook();
6435
+ try {
6436
+ await sessionIdleHook();
6437
+ await autoLearnHook();
6438
+ } finally {
6439
+ fileTracker.clear();
6440
+ }
6105
6441
  }
6106
6442
  },
6107
6443
  "tool.execute.before": async (toolInput, toolOutput) => {