@dv.nghiem/flowdeck 0.4.7 → 0.4.8

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 (50) hide show
  1. package/README.md +14 -1
  2. package/dist/agents/orchestrator.d.ts +2 -5
  3. package/dist/agents/orchestrator.d.ts.map +1 -1
  4. package/dist/config/schema.d.ts +1 -15
  5. package/dist/config/schema.d.ts.map +1 -1
  6. package/dist/dashboard/server.mjs +14 -1
  7. package/dist/hooks/orchestrator-guard-hook.d.ts +5 -15
  8. package/dist/hooks/orchestrator-guard-hook.d.ts.map +1 -1
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +617 -2045
  11. package/dist/{tools/dispatch-routing.d.ts → lib/task-routing.d.ts} +1 -3
  12. package/dist/lib/task-routing.d.ts.map +1 -0
  13. package/dist/services/agent-contract-registry.d.ts.map +1 -1
  14. package/dist/services/agent-performance.d.ts +1 -1
  15. package/dist/services/agent-performance.d.ts.map +1 -1
  16. package/dist/services/cost-estimator.d.ts +0 -88
  17. package/dist/services/cost-estimator.d.ts.map +1 -1
  18. package/dist/services/event-logger.d.ts +1 -1
  19. package/dist/services/event-logger.d.ts.map +1 -1
  20. package/dist/services/quick-router.d.ts +24 -0
  21. package/dist/services/quick-router.d.ts.map +1 -1
  22. package/dist/services/supervisor-binding.d.ts +6 -0
  23. package/dist/services/supervisor-binding.d.ts.map +1 -1
  24. package/dist/services/workflow-router.d.ts +57 -0
  25. package/dist/services/workflow-router.d.ts.map +1 -0
  26. package/dist/services/workflow-scorecard.d.ts.map +1 -1
  27. package/dist/tools/planning-state-lib.d.ts +23 -0
  28. package/dist/tools/planning-state-lib.d.ts.map +1 -1
  29. package/docs/concepts/workflows.md +48 -0
  30. package/docs/index.md +15 -2
  31. package/docs/reference/workflow-router.md +337 -0
  32. package/package.json +1 -1
  33. package/src/commands/fd-discuss.md +8 -1
  34. package/src/commands/fd-execute.md +25 -3
  35. package/src/commands/fd-new-feature.md +97 -4
  36. package/src/commands/fd-plan.md +14 -4
  37. package/src/commands/fd-quick.md +43 -16
  38. package/src/rules/common/agent-orchestration.md +52 -18
  39. package/src/skills/agent-harness-construction/SKILL.md +5 -5
  40. package/dist/services/delegation-budget.d.ts +0 -54
  41. package/dist/services/delegation-budget.d.ts.map +0 -1
  42. package/dist/services/prompt-cache.d.ts +0 -61
  43. package/dist/services/prompt-cache.d.ts.map +0 -1
  44. package/dist/services/token-metrics.d.ts +0 -97
  45. package/dist/services/token-metrics.d.ts.map +0 -1
  46. package/dist/tools/delegate.d.ts +0 -4
  47. package/dist/tools/delegate.d.ts.map +0 -1
  48. package/dist/tools/dispatch-routing.d.ts.map +0 -1
  49. package/dist/tools/run-pipeline.d.ts +0 -4
  50. package/dist/tools/run-pipeline.d.ts.map +0 -1
package/dist/index.js CHANGED
@@ -1,9 +1,6 @@
1
- import { createRequire } from "node:module";
2
- var __require = /* @__PURE__ */ createRequire(import.meta.url);
3
-
4
1
  // src/index.ts
5
- import { readFileSync as readFileSync28, readdirSync as readdirSync4, existsSync as existsSync29 } from "fs";
6
- import { join as join27, basename as basename2 } from "path";
2
+ import { readFileSync as readFileSync24, readdirSync as readdirSync3, existsSync as existsSync25 } from "fs";
3
+ import { join as join24, basename as basename2 } from "path";
7
4
  import { dirname as dirname3 } from "path";
8
5
  import { fileURLToPath as fileURLToPath2 } from "url";
9
6
 
@@ -281,6 +278,14 @@ function parseState(content) {
281
278
  result[key] = value === "true";
282
279
  } else if (key === "requires_design_first" || key === "design_approved" || key === "design_override") {
283
280
  result[key] = value === "true";
281
+ } else if (key === "skippedStages") {
282
+ result[key] = value.replace(/[\[\]]/g, "").split(",").map((s) => s.trim()).filter(Boolean);
283
+ } else if (key === "escalationHistory" || key === "routingScores") {
284
+ try {
285
+ result[key] = JSON.parse(value);
286
+ } catch {
287
+ result[key] = undefined;
288
+ }
284
289
  } else if (value !== "" && !isNaN(Number(value)) && key !== "plan_file" && key !== "confirmed_at") {
285
290
  result[key] = Number(value);
286
291
  } else {
@@ -352,7 +357,12 @@ function readPlanningState(dir) {
352
357
  lastUpdatedBy: parsed.lastUpdatedBy || "",
353
358
  lastUpdatedPhase: parsed.lastUpdatedPhase || 1,
354
359
  summaryVersion: parsed.summaryVersion || 0,
355
- freshnessStatus: parsed.freshnessStatus || "unknown"
360
+ freshnessStatus: parsed.freshnessStatus || "unknown",
361
+ workflowClass: parsed.workflowClass || undefined,
362
+ skippedStages: parsed.skippedStages || undefined,
363
+ escalationHistory: parsed.escalationHistory || undefined,
364
+ routingScores: parsed.routingScores || undefined,
365
+ routingReason: parsed.routingReason || undefined
356
366
  };
357
367
  }
358
368
  function parseTDDState(parsed) {
@@ -569,986 +579,54 @@ ${entry}`, "utf-8");
569
579
  }
570
580
  });
571
581
 
572
- // src/tools/run-pipeline.ts
582
+ // src/tools/repo-memory.ts
573
583
  import { tool as tool3 } from "@opencode-ai/plugin";
574
-
575
- // src/services/agent-performance.ts
576
- import { existsSync as existsSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync4, mkdirSync as mkdirSync2 } from "fs";
584
+ import { readFileSync as readFileSync5, writeFileSync as writeFileSync4, existsSync as existsSync5, mkdirSync as mkdirSync2 } from "fs";
577
585
  import { join as join5 } from "path";
578
- function perfPath(dir) {
579
- return join5(codebaseDir(dir), "AGENT_PERF.json");
580
- }
581
- function loadStore(dir) {
582
- const p = perfPath(dir);
583
- if (!existsSync5(p))
584
- return { entries: [], updated_at: new Date().toISOString() };
585
- try {
586
- return JSON.parse(readFileSync5(p, "utf-8"));
587
- } catch {
588
- return { entries: [], updated_at: new Date().toISOString() };
589
- }
590
- }
591
- function saveStore(dir, store) {
592
- const cd = codebaseDir(dir);
593
- if (!existsSync5(cd))
594
- mkdirSync2(cd, { recursive: true });
595
- writeFileSync4(perfPath(dir), JSON.stringify(store, null, 2), "utf-8");
596
- }
597
- function makeKey(agent, model, task_type) {
598
- return `${agent}::${model}::${task_type}`;
599
- }
600
- function recordRun(dir, agent, model, task_type, success, duration_ms, cost = 0) {
601
- const store = loadStore(dir);
602
- const key = makeKey(agent, model, task_type);
603
- const existing = store.entries.find((e) => makeKey(e.agent, e.model, e.task_type) === key);
604
- if (existing) {
605
- existing.runs++;
606
- if (success)
607
- existing.successes++;
608
- else
609
- existing.failures++;
610
- existing.total_duration_ms += duration_ms;
611
- existing.total_cost += cost;
612
- existing.last_run = new Date().toISOString();
613
- existing.last_status = success ? "success" : "failure";
614
- } else {
615
- store.entries.push({
616
- agent,
617
- model,
618
- task_type,
619
- runs: 1,
620
- successes: success ? 1 : 0,
621
- failures: success ? 0 : 1,
622
- total_duration_ms: duration_ms,
623
- total_cost: cost,
624
- last_run: new Date().toISOString(),
625
- last_status: success ? "success" : "failure"
626
- });
627
- }
628
- store.updated_at = new Date().toISOString();
629
- saveStore(dir, store);
630
- }
631
-
632
- // src/tools/dispatch-routing.ts
633
- function shouldRetry(promptRes) {
634
- if (!promptRes)
635
- return false;
636
- const detail = promptRes.error?.detail;
637
- if (isTransientError(detail))
638
- return true;
639
- const infoError = promptRes.data?.info?.error;
640
- const text = typeof infoError === "string" ? infoError : JSON.stringify(infoError ?? "");
641
- return isTransientError(text);
642
- }
643
- function isTransientError(text) {
644
- if (!text)
645
- return false;
646
- const haystack = text.toLowerCase();
647
- return haystack.includes("overload") || haystack.includes("rate limit") || haystack.includes("timeout") || haystack.includes("temporar") || haystack.includes("econnreset");
648
- }
649
- function normalizeTaskType(taskType, agent) {
650
- const normalized = (taskType ?? "").trim().toLowerCase();
651
- if (isTaskType(normalized))
652
- return normalized;
653
- const a = agent.toLowerCase();
654
- if (a.includes("design") || a.includes("ui-ux"))
655
- return "design";
656
- if (a.includes("review"))
657
- return "review";
658
- if (a.includes("test"))
659
- return "testing";
660
- if (a.includes("debug"))
661
- return "debugging";
662
- if (a.includes("security"))
663
- return "security";
664
- if (a.includes("doc"))
665
- return "documentation";
666
- if (a.includes("architect") || a.includes("planner"))
667
- return "planning";
668
- if (a.includes("orchestrator") || a.includes("coordinator"))
669
- return "orchestration";
670
- if (a.includes("analyst") || a.includes("research"))
671
- return "analysis";
672
- return "implementation";
673
- }
674
- function isTaskType(value) {
675
- return value === "planning" || value === "design" || value === "implementation" || value === "debugging" || value === "review" || value === "testing" || value === "documentation" || value === "analysis" || value === "security" || value === "orchestration";
676
- }
677
- var UI_HEAVY_KEYWORDS = [
678
- "landing page",
679
- "marketing site",
680
- "website",
681
- "web app",
682
- "mobile app",
683
- "app screen",
684
- "dashboard",
685
- "admin panel",
686
- "settings page",
687
- "onboarding ux",
688
- "kanban",
689
- "design system",
690
- "responsive",
691
- "ui",
692
- "ux",
693
- "cta",
694
- "conversion flow",
695
- "saas interface",
696
- "user-facing"
697
- ];
698
- var NON_UI_KEYWORDS = [
699
- "backend",
700
- "infrastructure",
701
- "migration",
702
- "pipeline",
703
- "api only",
704
- "database only",
705
- "cli",
706
- "worker"
707
- ];
708
- function isUiHeavyTask(input) {
709
- const normalized = input.trim().toLowerCase();
710
- if (!normalized)
711
- return false;
712
- const hasUiSignal = UI_HEAVY_KEYWORDS.some((keyword) => normalized.includes(keyword));
713
- if (!hasUiSignal)
714
- return false;
715
- const hasOnlyNonUiSignals = NON_UI_KEYWORDS.some((keyword) => normalized.includes(keyword)) && !normalized.includes("frontend");
716
- return !hasOnlyNonUiSignals;
717
- }
718
-
719
- // src/tools/run-pipeline.ts
720
- function extractText(parts) {
721
- return parts.filter((p) => p.type === "text" && typeof p.text === "string").map((p) => p.text).join(`
722
- `);
723
- }
724
- function createRunPipelineTool(client) {
725
- return tool3({
726
- description: "Run agents in sequential pipeline. Each step's output is appended to the next step's context. One fresh child session per step. Returns full trace with session ID, input/output/duration per step.",
727
- args: {
728
- steps: tool3.schema.array(tool3.schema.object({
729
- agent: tool3.schema.string(),
730
- prompt: tool3.schema.string(),
731
- task_type: tool3.schema.string().optional()
732
- })),
733
- initial_context: tool3.schema.string().optional(),
734
- abort_on_failure: tool3.schema.boolean().optional().default(true),
735
- retry_attempts: tool3.schema.number().optional().default(1),
736
- max_carry_chars: tool3.schema.number().optional()
737
- },
738
- async execute(args, context) {
739
- const startTime = Date.now();
740
- const trace = [];
741
- let carryContext = args.initial_context ?? "";
742
- let aborted = false;
743
- const retryAttempts = typeof args.retry_attempts === "number" ? args.retry_attempts : 1;
744
- const maxRetries = Math.max(0, Math.floor(retryAttempts));
745
- const totalSteps = args.steps.length;
746
- let inflightChildId = null;
747
- const abortHandler = () => {
748
- if (inflightChildId) {
749
- client.session.abort({
750
- path: { id: inflightChildId },
751
- query: { directory: context.directory }
752
- }).catch(() => {});
753
- }
754
- };
755
- context.abort.addEventListener("abort", abortHandler);
756
- try {
757
- for (let stepIdx = 0;stepIdx < args.steps.length; stepIdx++) {
758
- const step = args.steps[stepIdx];
759
- if (context.abort.aborted) {
760
- aborted = true;
761
- break;
762
- }
763
- const stepStart = Date.now();
764
- const taskType = normalizeTaskType(step.task_type, step.agent);
765
- const stepInput = carryContext ? `${carryContext}
766
-
767
- ---
768
-
769
- ${step.prompt}` : step.prompt;
770
- const createRes = await client.session.create({
771
- body: { parentID: context.sessionID, title: `${step.agent}-pipeline` },
772
- query: { directory: context.directory }
773
- });
774
- if (createRes.error || !createRes.data?.id) {
775
- const errMsg = `Failed to create session: ${createRes.error?.detail ?? "unknown"}`;
776
- trace.push({ agent: step.agent, task_type: taskType, model: "", input: stepInput, output: errMsg, duration_ms: Date.now() - stepStart, success: false });
777
- aborted = true;
778
- break;
779
- }
780
- inflightChildId = createRes.data.id;
781
- let promptRes = null;
782
- let retriesUsed = 0;
783
- for (let attempt = 0;attempt <= maxRetries; attempt++) {
784
- promptRes = await client.session.prompt({
785
- path: { id: inflightChildId },
786
- body: {
787
- agent: step.agent,
788
- parts: [{ type: "text", text: stepInput }],
789
- tools: { question: false }
790
- },
791
- query: { directory: context.directory }
792
- });
793
- if (!shouldRetry(promptRes) || attempt === maxRetries)
794
- break;
795
- retriesUsed++;
796
- }
797
- inflightChildId = null;
798
- if (context.abort.aborted) {
799
- aborted = true;
800
- break;
801
- }
802
- if (!promptRes || promptRes.error) {
803
- const errMsg = `Prompt failed: ${promptRes?.error?.detail ?? "unknown"}`;
804
- 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 });
805
- recordRun(context.directory, step.agent, "", taskType, false, Date.now() - stepStart);
806
- if (args.abort_on_failure) {
807
- aborted = true;
808
- break;
809
- }
810
- continue;
811
- }
812
- const info = promptRes.data?.info;
813
- if (info?.error) {
814
- const errMsg = `Agent error: ${JSON.stringify(info.error)}`;
815
- 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 });
816
- recordRun(context.directory, step.agent, "", taskType, false, Date.now() - stepStart);
817
- if (args.abort_on_failure) {
818
- aborted = true;
819
- break;
820
- }
821
- continue;
822
- }
823
- const output = extractText(promptRes.data?.parts ?? []);
824
- 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, context_chars: carryContext.length });
825
- recordRun(context.directory, step.agent, "", taskType, true, Date.now() - stepStart);
826
- const rawOutput = output || "";
827
- carryContext = typeof args.max_carry_chars === "number" && rawOutput.length > args.max_carry_chars ? rawOutput.slice(rawOutput.length - args.max_carry_chars) : rawOutput;
828
- }
829
- } finally {
830
- context.abort.removeEventListener("abort", abortHandler);
831
- }
832
- const totalDuration = Date.now() - startTime;
833
- return JSON.stringify({
834
- steps: trace,
835
- total_duration_ms: totalDuration,
836
- aborted
837
- });
838
- }
839
- });
840
- }
841
-
842
- // src/tools/delegate.ts
843
- import { tool as tool4 } from "@opencode-ai/plugin";
844
- import { existsSync as existsSync10, readFileSync as readFileSync10 } from "fs";
845
-
846
- // src/services/prompt-cache.ts
847
- import { createHash } from "crypto";
848
- import { existsSync as existsSync6, readFileSync as readFileSync6, writeFileSync as writeFileSync5, readdirSync as readdirSync3, statSync, mkdirSync as mkdirSync3 } from "fs";
849
- import { join as join6 } from "path";
850
- var CACHEABLE_AGENTS = new Set([
851
- "researcher",
852
- "code-explorer",
853
- "reviewer",
854
- "plan-checker",
855
- "security-auditor",
856
- "question-guard",
857
- "quick-router"
858
- ]);
859
- var CACHE_DIR_NAME = "prompt-cache";
860
- var MAX_CACHE_ENTRIES = 200;
861
- var DEFAULT_TTL_MS = 30 * 60 * 1000;
862
- function cacheDir(dir) {
863
- return join6(codebaseDir(dir), CACHE_DIR_NAME);
864
- }
865
- function entryPath(dir, key) {
866
- return join6(cacheDir(dir), `${key}.json`);
867
- }
868
- function readEntry(dir, key, stateVersion, indexVersion) {
869
- const path = entryPath(dir, key);
870
- if (!existsSync6(path))
871
- return null;
872
- try {
873
- const entry = JSON.parse(readFileSync6(path, "utf-8"));
874
- const age = Date.now() - new Date(entry.created_at).getTime();
875
- if (age > entry.ttl_ms)
876
- return null;
877
- if (entry.state_version !== stateVersion || entry.index_version !== indexVersion)
878
- return null;
879
- return entry.response;
880
- } catch {
881
- return null;
882
- }
883
- }
884
- function hashKey(agent, prompt, context, stateVersion, indexVersion) {
885
- const raw = JSON.stringify({ agent, prompt: prompt.trim(), context: context.trim(), stateVersion, indexVersion });
886
- return createHash("sha256").update(raw).digest("hex").slice(0, 32);
887
- }
888
- function normalizeForCache(text) {
889
- return text.replace(/\s+/g, " ").trim();
890
- }
891
- function hashKeyNormalized(agent, prompt, context, stateVersion, indexVersion) {
892
- const normalized = JSON.stringify({
893
- agent,
894
- prompt: normalizeForCache(prompt),
895
- context: normalizeForCache(context),
896
- stateVersion,
897
- indexVersion
898
- });
899
- return createHash("sha256").update(normalized).digest("hex").slice(0, 32);
900
- }
901
- function getCached(dir, agent, prompt, context, stateVersion, indexVersion, safe_to_cache = false) {
902
- if (!safe_to_cache)
903
- return null;
904
- if (!CACHEABLE_AGENTS.has(agent))
905
- return null;
906
- const exactKey = hashKey(agent, prompt, context, stateVersion, indexVersion);
907
- const exactResult = readEntry(dir, exactKey, stateVersion, indexVersion);
908
- if (exactResult !== null)
909
- return exactResult;
910
- const normKey = hashKeyNormalized(agent, prompt, context, stateVersion, indexVersion);
911
- if (normKey === exactKey)
912
- return null;
913
- return readEntry(dir, normKey, stateVersion, indexVersion);
914
- }
915
- function setCached(dir, agent, prompt, context, stateVersion, indexVersion, response, safe_to_cache = false, ttl_ms = DEFAULT_TTL_MS) {
916
- if (!safe_to_cache)
917
- return;
918
- if (!CACHEABLE_AGENTS.has(agent))
919
- return;
920
- const cd = cacheDir(dir);
921
- if (!existsSync6(cd))
922
- mkdirSync3(cd, { recursive: true });
923
- const key = hashKey(agent, prompt, context, stateVersion, indexVersion);
924
- const entry = {
925
- key,
926
- agent,
927
- state_version: stateVersion,
928
- index_version: indexVersion,
929
- created_at: new Date().toISOString(),
930
- ttl_ms,
931
- response
932
- };
933
- writeFileSync5(entryPath(dir, key), JSON.stringify(entry, null, 2), "utf-8");
934
- pruneExpired(dir);
935
- }
936
- function pruneExpired(dir) {
937
- const cd = cacheDir(dir);
938
- if (!existsSync6(cd))
939
- return;
940
- try {
941
- const files = readdirSync3(cd).filter((f) => f.endsWith(".json"));
942
- const now = Date.now();
943
- const entries = [];
944
- for (const f of files) {
945
- const p = join6(cd, f);
946
- try {
947
- const entry = JSON.parse(readFileSync6(p, "utf-8"));
948
- const age = now - new Date(entry.created_at).getTime();
949
- entries.push({ path: p, created_at: new Date(entry.created_at).getTime(), expired: age > entry.ttl_ms });
950
- } catch {
951
- entries.push({ path: p, created_at: 0, expired: true });
952
- }
953
- }
954
- let deleted = 0;
955
- for (const e of entries) {
956
- if (e.expired) {
957
- try {
958
- __require("fs").unlinkSync(e.path);
959
- } catch {}
960
- deleted++;
961
- }
962
- }
963
- const valid = entries.filter((e) => !e.expired).sort((a, b) => a.created_at - b.created_at);
964
- const excess = valid.length - MAX_CACHE_ENTRIES;
965
- for (let i = 0;i < excess; i++) {
966
- try {
967
- __require("fs").unlinkSync(valid[i].path);
968
- } catch {}
969
- }
970
- } catch {}
971
- }
972
-
973
- // src/tools/codebase-index.ts
974
- import { readFileSync as readFileSync7, writeFileSync as writeFileSync6, existsSync as existsSync7, mkdirSync as mkdirSync4 } from "fs";
975
- import { join as join7 } from "path";
976
- var CODEBASE_INDEX_FILE = "CODEBASE_INDEX.md";
977
- function indexPath(dir) {
978
- return join7(planningDir(dir), CODEBASE_INDEX_FILE);
979
- }
980
- function readCodebaseIndex(dir) {
981
- const path = indexPath(dir);
982
- if (!existsSync7(path)) {
983
- return {
984
- exists: false,
985
- lastUpdatedAt: "",
986
- lastUpdatedBy: "",
987
- sourceStage: "",
988
- changedFiles: [],
989
- fileSnapshots: {},
990
- explorationHistory: [],
991
- summaryVersion: 0,
992
- freshnessStatus: "unknown"
993
- };
994
- }
995
- try {
996
- const content = readFileSync7(path, "utf-8");
997
- return parseCodebaseIndexContent(content);
998
- } catch {
999
- return {
1000
- exists: false,
1001
- lastUpdatedAt: "",
1002
- lastUpdatedBy: "",
1003
- sourceStage: "",
1004
- changedFiles: [],
1005
- fileSnapshots: {},
1006
- explorationHistory: [],
1007
- summaryVersion: 0,
1008
- freshnessStatus: "unknown"
1009
- };
1010
- }
1011
- }
1012
- function parseCodebaseIndexContent(content) {
1013
- const result = { exists: true };
1014
- for (const line of content.split(`
1015
- `)) {
1016
- if (line.startsWith("#") || line.trim() === "")
1017
- continue;
1018
- const strippedLine = line.replace(/\*\*/g, "").replace(/^([a-zA-Z_][a-zA-Z0-9_]*):\s*(.*)/, "$1: $2");
1019
- const kvMatch = strippedLine.match(/^([a-zA-Z_][a-zA-Z0-9_]*):\s*(.*)/);
1020
- if (!kvMatch)
1021
- continue;
1022
- const key = kvMatch[1].trim();
1023
- const value = kvMatch[2].trim();
1024
- if (key === "changedFiles") {
1025
- result.changedFiles = value.replace(/[\[\]]/g, "").split(",").map((s) => s.trim()).filter(Boolean);
1026
- } else if (key === "summaryVersion") {
1027
- result.summaryVersion = parseInt(value, 10) || 0;
1028
- } else if (key === "freshnessStatus") {
1029
- result.freshnessStatus = value;
1030
- } else if (key === "lastUpdatedAt" || key === "lastUpdatedBy" || key === "sourceStage") {
1031
- result[key] = value.replace(/^["']|["']$/g, "");
1032
- }
1033
- }
1034
- let blockCount = 0;
1035
- for (const jsonMatch of content.matchAll(/```json\n([\s\S]*?)\n```/g)) {
1036
- if (blockCount >= 2)
1037
- break;
1038
- blockCount++;
1039
- try {
1040
- const parsed = JSON.parse(jsonMatch[1]);
1041
- if (parsed.fileSnapshots)
1042
- result.fileSnapshots = parsed.fileSnapshots;
1043
- if (parsed.explorationHistory)
1044
- result.explorationHistory = parsed.explorationHistory;
1045
- if (!parsed.fileSnapshots && !parsed.explorationHistory) {
1046
- if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
1047
- if (!result.fileSnapshots)
1048
- result.fileSnapshots = {};
1049
- Object.assign(result.fileSnapshots, parsed);
1050
- } else if (Array.isArray(parsed)) {
1051
- result.explorationHistory = parsed;
1052
- }
1053
- }
1054
- } catch {
1055
- result.freshnessStatus = "unknown";
1056
- }
1057
- }
1058
- return {
1059
- exists: true,
1060
- lastUpdatedAt: result.lastUpdatedAt || "",
1061
- lastUpdatedBy: result.lastUpdatedBy || "",
1062
- sourceStage: result.sourceStage || "",
1063
- changedFiles: result.changedFiles || [],
1064
- fileSnapshots: result.fileSnapshots || {},
1065
- explorationHistory: result.explorationHistory || [],
1066
- summaryVersion: result.summaryVersion || 0,
1067
- freshnessStatus: result.freshnessStatus || "unknown"
1068
- };
1069
- }
1070
-
1071
- // src/services/token-metrics.ts
1072
- import { existsSync as existsSync8, readFileSync as readFileSync8, appendFileSync, mkdirSync as mkdirSync5 } from "fs";
1073
- import { join as join8 } from "path";
1074
- function estimateTokens(text) {
1075
- return Math.ceil(text.length / 4);
1076
- }
1077
- function metricsPath(dir) {
1078
- return join8(codebaseDir(dir), "TOKEN_METRICS.jsonl");
1079
- }
1080
- function appendEvent(dir, event) {
1081
- const cd = codebaseDir(dir);
1082
- if (!existsSync8(cd))
1083
- mkdirSync5(cd, { recursive: true });
1084
- appendFileSync(metricsPath(dir), JSON.stringify(event) + `
1085
- `, "utf-8");
1086
- }
1087
- function recordModelCall(dir, workflow_id, stage, inputText, outputText, agent, duration_ms, model, est_cost_usd) {
1088
- const est_input_tokens = estimateTokens(inputText);
1089
- const est_output_tokens = estimateTokens(outputText);
1090
- appendEvent(dir, {
1091
- ts: new Date().toISOString(),
1092
- workflow_id,
1093
- stage,
1094
- event: "model_call",
1095
- agent,
1096
- model,
1097
- est_input_tokens,
1098
- est_output_tokens,
1099
- input_chars: inputText.length,
1100
- output_chars: outputText.length,
1101
- duration_ms,
1102
- est_cost_usd
1103
- });
1104
- }
1105
- function recordCacheHit(dir, workflow_id, stage, inputText, agent, model) {
1106
- appendEvent(dir, {
1107
- ts: new Date().toISOString(),
1108
- workflow_id,
1109
- stage,
1110
- event: "cache_hit",
1111
- agent,
1112
- model,
1113
- est_input_tokens: estimateTokens(inputText),
1114
- est_output_tokens: 0,
1115
- input_chars: inputText.length,
1116
- output_chars: 0
1117
- });
1118
- }
1119
- function recordRetryCall(dir, workflow_id, stage, inputText, outputText, agent, duration_ms, model, est_cost_usd) {
1120
- const est_input_tokens = estimateTokens(inputText);
1121
- const est_output_tokens = estimateTokens(outputText);
1122
- appendEvent(dir, {
1123
- ts: new Date().toISOString(),
1124
- workflow_id,
1125
- stage,
1126
- event: "retry",
1127
- agent,
1128
- model,
1129
- est_input_tokens,
1130
- est_output_tokens,
1131
- input_chars: inputText.length,
1132
- output_chars: outputText.length,
1133
- duration_ms,
1134
- est_cost_usd
1135
- });
1136
- }
1137
- var _workflowTimers = new Map;
1138
-
1139
- // src/config/loader.ts
1140
- import { existsSync as existsSync9, readFileSync as readFileSync9 } from "fs";
1141
- import { join as join9 } from "path";
1142
- import { homedir } from "os";
1143
- var CONFIG_FILENAME = "flowdeck.json";
1144
- function getGlobalConfigDir() {
1145
- return process.env.OPENCODE_CONFIG_DIR || (process.env.XDG_CONFIG_HOME ? join9(process.env.XDG_CONFIG_HOME, "opencode") : join9(homedir(), ".config", "opencode"));
1146
- }
1147
- function loadFlowDeckConfig(directory) {
1148
- const candidates = [];
1149
- if (directory) {
1150
- candidates.push(join9(directory, ".opencode", CONFIG_FILENAME));
1151
- }
1152
- candidates.push(join9(getGlobalConfigDir(), CONFIG_FILENAME));
1153
- for (const configPath of candidates) {
1154
- if (existsSync9(configPath)) {
1155
- try {
1156
- const content = readFileSync9(configPath, "utf-8");
1157
- return JSON.parse(content);
1158
- } catch {}
1159
- }
1160
- }
1161
- return {};
1162
- }
1163
- function resolveDesignFirstConfig(config) {
1164
- return {
1165
- enabled: config.designFirst?.enabled ?? true,
1166
- enforcement: config.designFirst?.enforcement ?? "strict",
1167
- requireApprovalBeforeImplementation: config.designFirst?.requireApprovalBeforeImplementation ?? true,
1168
- modelOverrides: config.designFirst?.modelOverrides ?? {},
1169
- defaultSkillsByTaskType: config.designFirst?.defaultSkillsByTaskType ?? {
1170
- "landing-page": ["landing-page-design", "wireframe-planning", "design-system-definition", "frontend-handoff"],
1171
- dashboard: ["dashboard-design", "ui-ux-planning", "wireframe-planning", "responsive-review"],
1172
- "admin-panel": ["ui-ux-planning", "wireframe-planning", "design-system-definition", "frontend-handoff"],
1173
- "app-screen": ["app-shell-design", "ui-ux-planning", "wireframe-planning", "responsive-review"],
1174
- "general-ui": ["ui-ux-planning", "wireframe-planning", "design-system-definition", "frontend-handoff"]
1175
- }
1176
- };
1177
- }
1178
- // src/services/cost-estimator.ts
1179
- var PRICING_TABLE = [
1180
- { prefix: "claude-opus-4", pricing: { input: 15, output: 75 } },
1181
- { prefix: "claude-opus", pricing: { input: 15, output: 75 } },
1182
- { prefix: "claude-sonnet-4", pricing: { input: 3, output: 15 } },
1183
- { prefix: "claude-sonnet-3-5", pricing: { input: 3, output: 15 } },
1184
- { prefix: "claude-sonnet-3", pricing: { input: 3, output: 15 } },
1185
- { prefix: "claude-sonnet", pricing: { input: 3, output: 15 } },
1186
- { prefix: "claude-haiku-4", pricing: { input: 0.8, output: 4 } },
1187
- { prefix: "claude-haiku-3-5", pricing: { input: 0.8, output: 4 } },
1188
- { prefix: "claude-haiku", pricing: { input: 0.25, output: 1.25 } },
1189
- { prefix: "claude-3-opus", pricing: { input: 15, output: 75 } },
1190
- { prefix: "claude-3-5-sonnet", pricing: { input: 3, output: 15 } },
1191
- { prefix: "claude-3-sonnet", pricing: { input: 3, output: 15 } },
1192
- { prefix: "claude-3-haiku", pricing: { input: 0.25, output: 1.25 } },
1193
- { prefix: "claude", pricing: { input: 3, output: 15 } },
1194
- { prefix: "gpt-5.4-mini", pricing: { input: 0.15, output: 0.6 } },
1195
- { prefix: "gpt-5-mini", pricing: { input: 0.15, output: 0.6 } },
1196
- { prefix: "gpt-4.1", pricing: { input: 2, output: 8 } },
1197
- { prefix: "gpt-4o-mini", pricing: { input: 0.15, output: 0.6 } },
1198
- { prefix: "gpt-4o", pricing: { input: 2.5, output: 10 } },
1199
- { prefix: "gpt-4-turbo", pricing: { input: 10, output: 30 } },
1200
- { prefix: "gpt-4", pricing: { input: 30, output: 60 } },
1201
- { prefix: "gpt-3.5", pricing: { input: 0.5, output: 1.5 } },
1202
- { prefix: "gpt-5", pricing: { input: 10, output: 30 } },
1203
- { prefix: "o3-mini", pricing: { input: 1.1, output: 4.4 } },
1204
- { prefix: "o3", pricing: { input: 10, output: 40 } },
1205
- { prefix: "o1-mini", pricing: { input: 1.1, output: 4.4 } },
1206
- { prefix: "o1", pricing: { input: 15, output: 60 } },
1207
- { prefix: "gemini-2.0-flash", pricing: { input: 0.1, output: 0.4 } },
1208
- { prefix: "gemini-2.5-flash", pricing: { input: 0.15, output: 0.6 } },
1209
- { prefix: "gemini-2.5-pro", pricing: { input: 1.25, output: 5 } },
1210
- { prefix: "gemini-1.5-flash", pricing: { input: 0.075, output: 0.3 } },
1211
- { prefix: "gemini-1.5-pro", pricing: { input: 1.25, output: 5 } },
1212
- { prefix: "gemini", pricing: { input: 0.1, output: 0.4 } },
1213
- { prefix: "github-copilot/sonnet", pricing: { input: 3, output: 15 } },
1214
- { prefix: "github-copilot/haiku", pricing: { input: 0.25, output: 1.25 } },
1215
- { prefix: "github-copilot/gpt-4", pricing: { input: 2.5, output: 10 } },
1216
- { prefix: "github-copilot", pricing: { input: 3, output: 15 } }
1217
- ];
1218
- var FALLBACK_PRICING = { input: 3, output: 15 };
1219
- function getModelPricing(model) {
1220
- if (!model)
1221
- return FALLBACK_PRICING;
1222
- const lower = model.toLowerCase();
1223
- for (const entry of PRICING_TABLE) {
1224
- if (lower.startsWith(entry.prefix.toLowerCase()))
1225
- return entry.pricing;
1226
- }
1227
- return FALLBACK_PRICING;
1228
- }
1229
- function estimateCostUSD(model, inputTokens, outputTokens) {
1230
- const pricing = getModelPricing(model);
1231
- return inputTokens / 1e6 * pricing.input + outputTokens / 1e6 * pricing.output;
1232
- }
1233
-
1234
- // src/tools/delegate.ts
1235
- function extractText2(parts) {
1236
- return parts.filter((p) => p.type === "text" && typeof p.text === "string").map((p) => p.text).join(`
1237
- `);
1238
- }
1239
- async function runWithStreaming(client, childId, agentName, fullPrompt, toolsConfig, directory, abort, onTitle) {
1240
- const sseResult = await client.event.subscribe({ query: { directory } });
1241
- const stream = sseResult.stream;
1242
- const asyncRes = await client.session.promptAsync({
1243
- path: { id: childId },
1244
- query: { directory },
1245
- body: {
1246
- agent: agentName,
1247
- tools: toolsConfig,
1248
- parts: [{ type: "text", text: fullPrompt }]
1249
- }
1250
- });
1251
- if (asyncRes.error) {
1252
- return {
1253
- output: "",
1254
- error: `promptAsync failed: ${JSON.stringify(asyncRes.error)}`
1255
- };
1256
- }
1257
- let streamedText = "";
1258
- let currentTool = "";
1259
- onTitle(`⏳ ${agentName} — starting…`);
1260
- try {
1261
- for await (const raw of stream) {
1262
- if (abort.aborted)
1263
- break;
1264
- const event = typeof raw === "object" && raw !== null ? Object.values(raw)[0] ?? raw : raw;
1265
- if (!event || typeof event !== "object")
1266
- continue;
1267
- const sid = event.properties?.sessionID;
1268
- if (sid && sid !== childId)
1269
- continue;
1270
- switch (event.type) {
1271
- case "session.next.step.started": {
1272
- const model = event.properties?.model?.id ?? "";
1273
- onTitle(`\uD83E\uDD14 ${agentName} — thinking${model ? ` (${model})` : ""}…`);
1274
- break;
1275
- }
1276
- case "session.next.text.delta": {
1277
- const delta = event.properties?.delta ?? "";
1278
- streamedText += delta;
1279
- const preview = streamedText.slice(-80).replace(/\n/g, " ").trim();
1280
- onTitle(`✍️ ${agentName} — ${preview}`);
1281
- break;
1282
- }
1283
- case "session.next.text.ended": {
1284
- const text = event.properties?.text ?? streamedText;
1285
- streamedText = text;
1286
- break;
1287
- }
1288
- case "session.next.reasoning.delta": {
1289
- const delta = event.properties?.delta ?? "";
1290
- const preview = delta.slice(0, 60).replace(/\n/g, " ").trim();
1291
- onTitle(`\uD83D\uDCAD ${agentName} — ${preview}`);
1292
- break;
1293
- }
1294
- case "session.next.tool.called": {
1295
- currentTool = event.properties?.tool ?? "tool";
1296
- onTitle(`\uD83D\uDD27 ${agentName} → ${currentTool}…`);
1297
- break;
1298
- }
1299
- case "session.next.tool.progress": {
1300
- const content = event.properties?.content ?? [];
1301
- const progressText = content.filter((c) => c.type === "text").map((c) => c.text).join(" ").slice(0, 80).replace(/\n/g, " ").trim();
1302
- if (progressText) {
1303
- onTitle(`\uD83D\uDD27 ${agentName} → ${currentTool}: ${progressText}`);
1304
- }
1305
- break;
1306
- }
1307
- case "session.next.tool.success": {
1308
- onTitle(`✅ ${agentName} → ${currentTool} done`);
1309
- currentTool = "";
1310
- break;
1311
- }
1312
- case "session.next.tool.failed": {
1313
- onTitle(`❌ ${agentName} → ${currentTool} failed`);
1314
- currentTool = "";
1315
- break;
1316
- }
1317
- case "session.next.retried": {
1318
- onTitle(`↻ ${agentName} — retrying…`);
1319
- break;
1320
- }
1321
- case "session.next.step.ended": {
1322
- const cost = event.properties?.cost ?? 0;
1323
- const finish = event.properties?.finish ?? "";
1324
- if (cost > 0) {
1325
- onTitle(`\uD83D\uDCCA ${agentName} — step done ($${cost.toFixed(4)}) [${finish}]`);
1326
- } else {
1327
- onTitle(`\uD83D\uDCCA ${agentName} — step done [${finish}]`);
1328
- }
1329
- break;
1330
- }
1331
- case "session.error": {
1332
- const msg = event.properties?.error?.message ?? JSON.stringify(event.properties?.error);
1333
- return { output: streamedText, error: `Session error: ${msg}` };
1334
- }
1335
- case "session.idle": {
1336
- onTitle(`✓ ${agentName} — complete`);
1337
- goto_done:
1338
- break goto_done;
1339
- }
1340
- }
1341
- if (event.type === "session.idle")
1342
- break;
1343
- }
1344
- } catch (err) {
1345
- if (!abort.aborted) {
1346
- onTitle(`⚠️ ${agentName} — stream closed (${err?.message ?? err})`);
1347
- }
1348
- }
1349
- if (streamedText) {
1350
- return { output: streamedText };
1351
- }
1352
- try {
1353
- const msgsRes = await client.session.messages({
1354
- path: { id: childId },
1355
- query: { directory }
1356
- });
1357
- const messages = msgsRes.data ?? [];
1358
- for (let i = messages.length - 1;i >= 0; i--) {
1359
- const msg = messages[i];
1360
- if (msg.role === "assistant") {
1361
- const text = extractText2(msg.parts ?? []);
1362
- if (text)
1363
- return { output: text };
1364
- }
1365
- }
1366
- } catch {}
1367
- return { output: "" };
1368
- }
1369
- function createDelegateTool(client) {
1370
- return tool4({
1371
- description: "Delegate a task to a single agent via a child session. Returns the agent's output.",
1372
- args: {
1373
- agent: tool4.schema.string(),
1374
- prompt: tool4.schema.string(),
1375
- context: tool4.schema.string().optional(),
1376
- task_type: tool4.schema.string().optional(),
1377
- retry_attempts: tool4.schema.number().optional().default(1),
1378
- safe_to_cache: tool4.schema.boolean().optional().default(false),
1379
- cache_ttl_ms: tool4.schema.number().optional(),
1380
- workflow_id: tool4.schema.string().optional(),
1381
- stage: tool4.schema.string().optional()
1382
- },
1383
- async execute(args, execContext) {
1384
- const startTime = Date.now();
1385
- const taskType = normalizeTaskType(args.task_type, args.agent);
1386
- const retryAttempts = typeof args.retry_attempts === "number" ? args.retry_attempts : 1;
1387
- const maxRetries = Math.max(0, Math.floor(retryAttempts));
1388
- let agentModel = "";
1389
- try {
1390
- const cfg = loadFlowDeckConfig(execContext.directory);
1391
- agentModel = cfg.agents?.[args.agent]?.model ?? "";
1392
- } catch {}
1393
- const metricsWorkflowId = args.workflow_id ?? "";
1394
- const metricsStage = args.stage ?? "delegate";
1395
- const fullPrompt = args.context ? `${args.context}
1396
-
1397
- ---
1398
-
1399
- ${args.prompt}` : args.prompt;
1400
- const safe_to_cache = args.safe_to_cache === true && CACHEABLE_AGENTS.has(args.agent);
1401
- let stateVersion = 0;
1402
- let indexVersion = 0;
1403
- if (safe_to_cache) {
1404
- const index = readCodebaseIndex(execContext.directory);
1405
- const sp = statePath(execContext.directory);
1406
- const rawState = existsSync10(sp) ? readFileSync10(sp, "utf-8") : "";
1407
- const state = rawState ? parseState(rawState) : {};
1408
- stateVersion = typeof state.summaryVersion === "number" ? state.summaryVersion : 0;
1409
- indexVersion = typeof index.summaryVersion === "number" ? index.summaryVersion : 0;
1410
- const cached = getCached(execContext.directory, args.agent, fullPrompt, args.context ?? "", stateVersion, indexVersion, true);
1411
- if (cached !== null) {
1412
- if (metricsWorkflowId) {
1413
- recordCacheHit(execContext.directory, metricsWorkflowId, metricsStage, fullPrompt, args.agent, agentModel);
1414
- }
1415
- return JSON.stringify({
1416
- agent: args.agent,
1417
- success: true,
1418
- output: cached,
1419
- task_type: taskType,
1420
- model: "",
1421
- retries_used: 0,
1422
- duration_ms: Date.now() - startTime,
1423
- cached: true
1424
- });
1425
- }
1426
- }
1427
- const createRes = await client.session.create({
1428
- body: { parentID: execContext.sessionID, title: `${args.agent}-delegate` },
1429
- query: { directory: execContext.directory }
1430
- });
1431
- if (createRes.error || !createRes.data?.id) {
1432
- return JSON.stringify({
1433
- agent: args.agent,
1434
- success: false,
1435
- error: `Failed to create session: ${createRes.error?.detail ?? "unknown"}`,
1436
- duration_ms: Date.now() - startTime
1437
- });
1438
- }
1439
- const childId = createRes.data.id;
1440
- execContext.abort.addEventListener("abort", () => {
1441
- client.session.abort({
1442
- path: { id: childId },
1443
- query: { directory: execContext.directory }
1444
- }).catch(() => {});
1445
- });
1446
- let lastOutput = "";
1447
- let lastError;
1448
- let retriesUsed = 0;
1449
- for (let attempt = 0;attempt <= maxRetries; attempt++) {
1450
- const attemptStart = Date.now();
1451
- if (attempt > 0) {
1452
- execContext.metadata({ title: `↻ ${args.agent} — retry ${attempt}/${maxRetries}…` });
1453
- }
1454
- const result = await runWithStreaming(client, childId, args.agent, fullPrompt, { question: false }, execContext.directory, execContext.abort, (title) => execContext.metadata({ title }));
1455
- lastOutput = result.output;
1456
- lastError = result.error;
1457
- const shouldRetryAttempt = !!(lastError || !lastOutput.trim());
1458
- if (!shouldRetryAttempt || attempt === maxRetries)
1459
- break;
1460
- if (metricsWorkflowId) {
1461
- const retryInputTokens = estimateTokens(fullPrompt);
1462
- const retryCostUsd = agentModel ? estimateCostUSD(agentModel, retryInputTokens, 0) : undefined;
1463
- recordRetryCall(execContext.directory, metricsWorkflowId, metricsStage, fullPrompt, "", args.agent, Date.now() - attemptStart, agentModel, retryCostUsd);
1464
- }
1465
- retriesUsed++;
1466
- }
1467
- if (lastError && !lastOutput.trim()) {
1468
- recordRun(execContext.directory, args.agent, "", taskType, false, Date.now() - startTime);
1469
- return JSON.stringify({
1470
- agent: args.agent,
1471
- session_id: childId,
1472
- success: false,
1473
- error: lastError,
1474
- task_type: taskType,
1475
- model: "",
1476
- retries_used: retriesUsed,
1477
- duration_ms: Date.now() - startTime
1478
- });
1479
- }
1480
- recordRun(execContext.directory, args.agent, "", taskType, true, Date.now() - startTime);
1481
- if (metricsWorkflowId) {
1482
- const inputTokens = estimateTokens(fullPrompt);
1483
- const outputTokens = estimateTokens(lastOutput);
1484
- const costUsd = agentModel ? estimateCostUSD(agentModel, inputTokens, outputTokens) : undefined;
1485
- recordModelCall(execContext.directory, metricsWorkflowId, metricsStage, fullPrompt, lastOutput, args.agent, Date.now() - startTime, agentModel, costUsd);
1486
- }
1487
- if (safe_to_cache && lastOutput) {
1488
- setCached(execContext.directory, args.agent, fullPrompt, args.context ?? "", stateVersion, indexVersion, lastOutput, true, args.cache_ttl_ms);
1489
- }
1490
- return JSON.stringify({
1491
- agent: args.agent,
1492
- session_id: childId,
1493
- success: true,
1494
- output: lastOutput || "(no text output)",
1495
- task_type: taskType,
1496
- model: "",
1497
- retries_used: retriesUsed,
1498
- duration_ms: Date.now() - startTime
1499
- });
1500
- }
1501
- });
1502
- }
1503
-
1504
- // src/tools/repo-memory.ts
1505
- import { tool as tool5 } from "@opencode-ai/plugin";
1506
- import { readFileSync as readFileSync11, writeFileSync as writeFileSync7, existsSync as existsSync11, mkdirSync as mkdirSync6 } from "fs";
1507
- import { join as join10 } from "path";
1508
586
  var MEMORY_FILE = "MEMORY.json";
1509
587
  function memoryPath(directory) {
1510
- return join10(codebaseDir(directory), MEMORY_FILE);
588
+ return join5(codebaseDir(directory), MEMORY_FILE);
1511
589
  }
1512
590
  function emptyMemory() {
1513
591
  return { version: "1.0", last_updated: new Date().toISOString(), nodes: {} };
1514
592
  }
1515
593
  function readMemory(directory) {
1516
594
  const p = memoryPath(directory);
1517
- if (!existsSync11(p))
595
+ if (!existsSync5(p))
1518
596
  return emptyMemory();
1519
597
  try {
1520
- return JSON.parse(readFileSync11(p, "utf-8"));
598
+ return JSON.parse(readFileSync5(p, "utf-8"));
1521
599
  } catch {
1522
600
  return emptyMemory();
1523
601
  }
1524
602
  }
1525
603
  function writeMemory(directory, memory) {
1526
604
  const base = codebaseDir(directory);
1527
- if (!existsSync11(base))
1528
- mkdirSync6(base, { recursive: true });
605
+ if (!existsSync5(base))
606
+ mkdirSync2(base, { recursive: true });
1529
607
  memory.last_updated = new Date().toISOString();
1530
- writeFileSync7(memoryPath(directory), JSON.stringify(memory, null, 2), "utf-8");
608
+ writeFileSync4(memoryPath(directory), JSON.stringify(memory, null, 2), "utf-8");
1531
609
  }
1532
- var repoMemoryTool = tool5({
610
+ var repoMemoryTool = tool3({
1533
611
  description: "Repo Memory Graph: read/write/query persistent architecture graph in .codebase/MEMORY.json (modules, dependencies, ownership, bug history, conventions)",
1534
612
  args: {
1535
- action: tool5.schema.enum(["read", "write_node", "query", "delete_node"]),
1536
- node_id: tool5.schema.string().optional(),
1537
- node: tool5.schema.object({
1538
- type: tool5.schema.enum(["module", "service", "api", "schema", "config"]),
1539
- path: tool5.schema.string(),
1540
- owner: tool5.schema.string().optional(),
1541
- tags: tool5.schema.array(tool5.schema.string()),
1542
- dependencies: tool5.schema.array(tool5.schema.string()),
1543
- dependents: tool5.schema.array(tool5.schema.string()),
1544
- bug_history: tool5.schema.array(tool5.schema.string()),
1545
- conventions: tool5.schema.array(tool5.schema.string())
613
+ action: tool3.schema.enum(["read", "write_node", "query", "delete_node"]),
614
+ node_id: tool3.schema.string().optional(),
615
+ node: tool3.schema.object({
616
+ type: tool3.schema.enum(["module", "service", "api", "schema", "config"]),
617
+ path: tool3.schema.string(),
618
+ owner: tool3.schema.string().optional(),
619
+ tags: tool3.schema.array(tool3.schema.string()),
620
+ dependencies: tool3.schema.array(tool3.schema.string()),
621
+ dependents: tool3.schema.array(tool3.schema.string()),
622
+ bug_history: tool3.schema.array(tool3.schema.string()),
623
+ conventions: tool3.schema.array(tool3.schema.string())
1546
624
  }).optional(),
1547
- query: tool5.schema.object({
1548
- type: tool5.schema.enum(["module", "service", "api", "schema", "config"]).optional(),
1549
- owner: tool5.schema.string().optional(),
1550
- tag: tool5.schema.string().optional(),
1551
- path_prefix: tool5.schema.string().optional()
625
+ query: tool3.schema.object({
626
+ type: tool3.schema.enum(["module", "service", "api", "schema", "config"]).optional(),
627
+ owner: tool3.schema.string().optional(),
628
+ tag: tool3.schema.string().optional(),
629
+ path_prefix: tool3.schema.string().optional()
1552
630
  }).optional()
1553
631
  },
1554
632
  async execute(args, context) {
@@ -1603,50 +681,50 @@ var repoMemoryTool = tool5({
1603
681
  });
1604
682
 
1605
683
  // src/tools/failure-replay.ts
1606
- import { tool as tool6 } from "@opencode-ai/plugin";
1607
- import { readFileSync as readFileSync12, writeFileSync as writeFileSync8, existsSync as existsSync12, mkdirSync as mkdirSync7 } from "fs";
1608
- import { join as join11 } from "path";
684
+ import { tool as tool4 } from "@opencode-ai/plugin";
685
+ import { readFileSync as readFileSync6, writeFileSync as writeFileSync5, existsSync as existsSync6, mkdirSync as mkdirSync3 } from "fs";
686
+ import { join as join6 } from "path";
1609
687
  var FAILURES_FILE = "FAILURES.json";
1610
688
  function failuresPath(directory) {
1611
- return join11(codebaseDir(directory), FAILURES_FILE);
689
+ return join6(codebaseDir(directory), FAILURES_FILE);
1612
690
  }
1613
691
  function readStore(directory) {
1614
692
  const p = failuresPath(directory);
1615
- if (!existsSync12(p))
693
+ if (!existsSync6(p))
1616
694
  return { version: "1.0", last_updated: new Date().toISOString(), entries: [] };
1617
695
  try {
1618
- return JSON.parse(readFileSync12(p, "utf-8"));
696
+ return JSON.parse(readFileSync6(p, "utf-8"));
1619
697
  } catch {
1620
698
  return { version: "1.0", last_updated: new Date().toISOString(), entries: [] };
1621
699
  }
1622
700
  }
1623
701
  function writeStore(directory, store) {
1624
702
  const base = codebaseDir(directory);
1625
- if (!existsSync12(base))
1626
- mkdirSync7(base, { recursive: true });
703
+ if (!existsSync6(base))
704
+ mkdirSync3(base, { recursive: true });
1627
705
  store.last_updated = new Date().toISOString();
1628
- writeFileSync8(failuresPath(directory), JSON.stringify(store, null, 2), "utf-8");
706
+ writeFileSync5(failuresPath(directory), JSON.stringify(store, null, 2), "utf-8");
1629
707
  }
1630
- var failureReplayTool = tool6({
708
+ var failureReplayTool = tool4({
1631
709
  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",
1632
710
  args: {
1633
- action: tool6.schema.enum(["record", "query", "list", "mark_resolved"]),
1634
- entry: tool6.schema.object({
1635
- id: tool6.schema.string(),
1636
- type: tool6.schema.enum(["reverted_commit", "failed_deployment", "flaky_test", "bug_fix", "build_failure"]),
1637
- description: tool6.schema.string(),
1638
- affected_paths: tool6.schema.array(tool6.schema.string()),
1639
- root_cause: tool6.schema.string().optional(),
1640
- fix_applied: tool6.schema.string().optional(),
1641
- tags: tool6.schema.array(tool6.schema.string())
711
+ action: tool4.schema.enum(["record", "query", "list", "mark_resolved"]),
712
+ entry: tool4.schema.object({
713
+ id: tool4.schema.string(),
714
+ type: tool4.schema.enum(["reverted_commit", "failed_deployment", "flaky_test", "bug_fix", "build_failure"]),
715
+ description: tool4.schema.string(),
716
+ affected_paths: tool4.schema.array(tool4.schema.string()),
717
+ root_cause: tool4.schema.string().optional(),
718
+ fix_applied: tool4.schema.string().optional(),
719
+ tags: tool4.schema.array(tool4.schema.string())
1642
720
  }).optional(),
1643
- query: tool6.schema.object({
1644
- type: tool6.schema.enum(["reverted_commit", "failed_deployment", "flaky_test", "bug_fix", "build_failure"]).optional(),
1645
- path_prefix: tool6.schema.string().optional(),
1646
- tag: tool6.schema.string().optional(),
1647
- limit: tool6.schema.number().optional()
721
+ query: tool4.schema.object({
722
+ type: tool4.schema.enum(["reverted_commit", "failed_deployment", "flaky_test", "bug_fix", "build_failure"]).optional(),
723
+ path_prefix: tool4.schema.string().optional(),
724
+ tag: tool4.schema.string().optional(),
725
+ limit: tool4.schema.number().optional()
1648
726
  }).optional(),
1649
- entry_id: tool6.schema.string().optional()
727
+ entry_id: tool4.schema.string().optional()
1650
728
  },
1651
729
  async execute(args, context) {
1652
730
  const dir = context.directory ?? process.cwd();
@@ -1708,18 +786,18 @@ var failureReplayTool = tool6({
1708
786
  });
1709
787
 
1710
788
  // src/tools/decision-trace.ts
1711
- import { tool as tool7 } from "@opencode-ai/plugin";
1712
- import { readFileSync as readFileSync13, existsSync as existsSync13, mkdirSync as mkdirSync8, appendFileSync as appendFileSync2 } from "fs";
1713
- import { join as join12 } from "path";
789
+ import { tool as tool5 } from "@opencode-ai/plugin";
790
+ import { readFileSync as readFileSync7, existsSync as existsSync7, mkdirSync as mkdirSync4, appendFileSync } from "fs";
791
+ import { join as join7 } from "path";
1714
792
  var DECISIONS_FILE = "DECISIONS.jsonl";
1715
793
  function decisionsPath(directory) {
1716
- return join12(codebaseDir(directory), DECISIONS_FILE);
794
+ return join7(codebaseDir(directory), DECISIONS_FILE);
1717
795
  }
1718
796
  function readDecisions(directory) {
1719
797
  const p = decisionsPath(directory);
1720
- if (!existsSync13(p))
798
+ if (!existsSync7(p))
1721
799
  return [];
1722
- return readFileSync13(p, "utf-8").split(`
800
+ return readFileSync7(p, "utf-8").split(`
1723
801
  `).filter((l) => l.trim()).map((l) => {
1724
802
  try {
1725
803
  return JSON.parse(l);
@@ -1728,29 +806,29 @@ function readDecisions(directory) {
1728
806
  }
1729
807
  }).filter(Boolean);
1730
808
  }
1731
- var decisionTraceTool = tool7({
809
+ var decisionTraceTool = tool5({
1732
810
  description: "Decision Trace: record why the agent changed something, what evidence was used, and assumptions made. Stored in .codebase/DECISIONS.jsonl for fast review.",
1733
811
  args: {
1734
- action: tool7.schema.enum(["record", "query", "get_for_file"]),
1735
- entry: tool7.schema.object({
1736
- id: tool7.schema.string(),
1737
- file_path: tool7.schema.string(),
1738
- change_type: tool7.schema.enum(["create", "edit", "delete", "refactor"]),
1739
- rationale: tool7.schema.string(),
1740
- evidence: tool7.schema.array(tool7.schema.string()),
1741
- assumptions: tool7.schema.array(tool7.schema.string()),
1742
- alternatives_considered: tool7.schema.array(tool7.schema.string()),
1743
- risk_level: tool7.schema.enum(["low", "medium", "high"]),
1744
- agent: tool7.schema.string().optional(),
1745
- session_id: tool7.schema.string().optional()
812
+ action: tool5.schema.enum(["record", "query", "get_for_file"]),
813
+ entry: tool5.schema.object({
814
+ id: tool5.schema.string(),
815
+ file_path: tool5.schema.string(),
816
+ change_type: tool5.schema.enum(["create", "edit", "delete", "refactor"]),
817
+ rationale: tool5.schema.string(),
818
+ evidence: tool5.schema.array(tool5.schema.string()),
819
+ assumptions: tool5.schema.array(tool5.schema.string()),
820
+ alternatives_considered: tool5.schema.array(tool5.schema.string()),
821
+ risk_level: tool5.schema.enum(["low", "medium", "high"]),
822
+ agent: tool5.schema.string().optional(),
823
+ session_id: tool5.schema.string().optional()
1746
824
  }).optional(),
1747
- query: tool7.schema.object({
1748
- file_path: tool7.schema.string().optional(),
1749
- change_type: tool7.schema.enum(["create", "edit", "delete", "refactor"]).optional(),
1750
- risk_level: tool7.schema.enum(["low", "medium", "high"]).optional(),
1751
- limit: tool7.schema.number().optional()
825
+ query: tool5.schema.object({
826
+ file_path: tool5.schema.string().optional(),
827
+ change_type: tool5.schema.enum(["create", "edit", "delete", "refactor"]).optional(),
828
+ risk_level: tool5.schema.enum(["low", "medium", "high"]).optional(),
829
+ limit: tool5.schema.number().optional()
1752
830
  }).optional(),
1753
- file_path: tool7.schema.string().optional()
831
+ file_path: tool5.schema.string().optional()
1754
832
  },
1755
833
  async execute(args, context) {
1756
834
  const dir = context.directory ?? process.cwd();
@@ -1759,10 +837,10 @@ var decisionTraceTool = tool7({
1759
837
  case "record": {
1760
838
  if (!args.entry)
1761
839
  return JSON.stringify({ error: "entry required" });
1762
- if (!existsSync13(base))
1763
- mkdirSync8(base, { recursive: true });
840
+ if (!existsSync7(base))
841
+ mkdirSync4(base, { recursive: true });
1764
842
  const entry = { ...args.entry, timestamp: new Date().toISOString() };
1765
- appendFileSync2(decisionsPath(dir), JSON.stringify(entry) + `
843
+ appendFileSync(decisionsPath(dir), JSON.stringify(entry) + `
1766
844
  `, "utf-8");
1767
845
  return JSON.stringify({ success: true, id: args.entry.id });
1768
846
  }
@@ -1793,48 +871,48 @@ var decisionTraceTool = tool7({
1793
871
  });
1794
872
 
1795
873
  // src/tools/policy-engine.ts
1796
- import { tool as tool8 } from "@opencode-ai/plugin";
1797
- import { readFileSync as readFileSync14, writeFileSync as writeFileSync10, existsSync as existsSync14, mkdirSync as mkdirSync9 } from "fs";
1798
- import { join as join13 } from "path";
874
+ import { tool as tool6 } from "@opencode-ai/plugin";
875
+ import { readFileSync as readFileSync8, writeFileSync as writeFileSync7, existsSync as existsSync8, mkdirSync as mkdirSync5 } from "fs";
876
+ import { join as join8 } from "path";
1799
877
  var POLICIES_FILE = "POLICIES.json";
1800
878
  function policiesPath(directory) {
1801
- return join13(codebaseDir(directory), POLICIES_FILE);
879
+ return join8(codebaseDir(directory), POLICIES_FILE);
1802
880
  }
1803
881
  function readStore2(directory) {
1804
882
  const p = policiesPath(directory);
1805
- if (!existsSync14(p))
883
+ if (!existsSync8(p))
1806
884
  return { version: "1.0", last_updated: new Date().toISOString(), policies: [] };
1807
885
  try {
1808
- return JSON.parse(readFileSync14(p, "utf-8"));
886
+ return JSON.parse(readFileSync8(p, "utf-8"));
1809
887
  } catch {
1810
888
  return { version: "1.0", last_updated: new Date().toISOString(), policies: [] };
1811
889
  }
1812
890
  }
1813
891
  function writeStore2(directory, store) {
1814
892
  const base = codebaseDir(directory);
1815
- if (!existsSync14(base))
1816
- mkdirSync9(base, { recursive: true });
893
+ if (!existsSync8(base))
894
+ mkdirSync5(base, { recursive: true });
1817
895
  store.last_updated = new Date().toISOString();
1818
- writeFileSync10(policiesPath(directory), JSON.stringify(store, null, 2), "utf-8");
896
+ writeFileSync7(policiesPath(directory), JSON.stringify(store, null, 2), "utf-8");
1819
897
  }
1820
- var policyEngineTool = tool8({
898
+ var policyEngineTool = tool6({
1821
899
  description: "Self-Healing Policy Engine: manage .codebase/POLICIES.json — add, list, query, toggle, and record violations of editing policies learned from past failures",
1822
900
  args: {
1823
- action: tool8.schema.enum(["list", "add", "record_violation", "toggle", "query"]),
1824
- policy: tool8.schema.object({
1825
- id: tool8.schema.string(),
1826
- name: tool8.schema.string(),
1827
- trigger: tool8.schema.string(),
1828
- rule: tool8.schema.string(),
1829
- source: tool8.schema.enum(["manual", "learned"]),
1830
- failure_count: tool8.schema.number()
901
+ action: tool6.schema.enum(["list", "add", "record_violation", "toggle", "query"]),
902
+ policy: tool6.schema.object({
903
+ id: tool6.schema.string(),
904
+ name: tool6.schema.string(),
905
+ trigger: tool6.schema.string(),
906
+ rule: tool6.schema.string(),
907
+ source: tool6.schema.enum(["manual", "learned"]),
908
+ failure_count: tool6.schema.number()
1831
909
  }).optional(),
1832
- policy_id: tool8.schema.string().optional(),
1833
- active: tool8.schema.boolean().optional(),
1834
- query: tool8.schema.object({
1835
- source: tool8.schema.enum(["manual", "learned"]).optional(),
1836
- active_only: tool8.schema.boolean().optional(),
1837
- trigger_contains: tool8.schema.string().optional()
910
+ policy_id: tool6.schema.string().optional(),
911
+ active: tool6.schema.boolean().optional(),
912
+ query: tool6.schema.object({
913
+ source: tool6.schema.enum(["manual", "learned"]).optional(),
914
+ active_only: tool6.schema.boolean().optional(),
915
+ trigger_contains: tool6.schema.string().optional()
1838
916
  }).optional()
1839
917
  },
1840
918
  async execute(args, context) {
@@ -1893,54 +971,154 @@ var policyEngineTool = tool8({
1893
971
  }
1894
972
  }
1895
973
  }
1896
- });
1897
-
1898
- // src/tools/hash-edit.ts
1899
- import { tool as tool9 } from "@opencode-ai/plugin";
1900
- import { readFileSync as readFileSync15, writeFileSync as writeFileSync11 } from "fs";
1901
- import { createHash as createHash2 } from "crypto";
1902
- var hashEditTool = tool9({
1903
- 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.",
1904
- args: {
1905
- filePath: tool9.schema.string(),
1906
- targetContent: tool9.schema.string(),
1907
- expectedHash: tool9.schema.string().optional(),
1908
- replacementContent: tool9.schema.string()
1909
- },
1910
- async execute(args, context) {
1911
- const fullPath = args.filePath.startsWith("/") ? args.filePath : `${context.directory}/${args.filePath}`;
1912
- let content;
974
+ });
975
+
976
+ // src/tools/hash-edit.ts
977
+ import { tool as tool7 } from "@opencode-ai/plugin";
978
+ import { readFileSync as readFileSync9, writeFileSync as writeFileSync8 } from "fs";
979
+ import { createHash } from "crypto";
980
+ var hashEditTool = tool7({
981
+ 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.",
982
+ args: {
983
+ filePath: tool7.schema.string(),
984
+ targetContent: tool7.schema.string(),
985
+ expectedHash: tool7.schema.string().optional(),
986
+ replacementContent: tool7.schema.string()
987
+ },
988
+ async execute(args, context) {
989
+ const fullPath = args.filePath.startsWith("/") ? args.filePath : `${context.directory}/${args.filePath}`;
990
+ let content;
991
+ try {
992
+ content = readFileSync9(fullPath, "utf-8");
993
+ } catch (e) {
994
+ return `Error: Could not read file ${args.filePath}`;
995
+ }
996
+ if (!content.includes(args.targetContent)) {
997
+ return `Error: Target content not found in ${args.filePath}. It may have been modified by another agent.`;
998
+ }
999
+ if (args.expectedHash) {
1000
+ const actualHash = createHash("md5").update(args.targetContent).digest("hex");
1001
+ if (actualHash !== args.expectedHash) {
1002
+ return `Error: Hash mismatch for target content. Expected ${args.expectedHash}, got ${actualHash}. Refusing to edit stale content.`;
1003
+ }
1004
+ }
1005
+ const newContent = content.replace(args.targetContent, args.replacementContent);
1006
+ writeFileSync8(fullPath, newContent, "utf-8");
1007
+ return `Successfully updated ${args.filePath} using hash-anchored edit.`;
1008
+ }
1009
+ });
1010
+
1011
+ // src/tools/council.ts
1012
+ import { tool as tool8 } from "@opencode-ai/plugin";
1013
+ import { appendFileSync as appendFileSync2, existsSync as existsSync10, mkdirSync as mkdirSync7 } from "fs";
1014
+ import { join as join10 } from "path";
1015
+ import { createHash as createHash2 } from "crypto";
1016
+
1017
+ // src/tools/codebase-index.ts
1018
+ import { readFileSync as readFileSync10, writeFileSync as writeFileSync9, existsSync as existsSync9, mkdirSync as mkdirSync6 } from "fs";
1019
+ import { join as join9 } from "path";
1020
+ var CODEBASE_INDEX_FILE = "CODEBASE_INDEX.md";
1021
+ function indexPath(dir) {
1022
+ return join9(planningDir(dir), CODEBASE_INDEX_FILE);
1023
+ }
1024
+ function readCodebaseIndex(dir) {
1025
+ const path = indexPath(dir);
1026
+ if (!existsSync9(path)) {
1027
+ return {
1028
+ exists: false,
1029
+ lastUpdatedAt: "",
1030
+ lastUpdatedBy: "",
1031
+ sourceStage: "",
1032
+ changedFiles: [],
1033
+ fileSnapshots: {},
1034
+ explorationHistory: [],
1035
+ summaryVersion: 0,
1036
+ freshnessStatus: "unknown"
1037
+ };
1038
+ }
1039
+ try {
1040
+ const content = readFileSync10(path, "utf-8");
1041
+ return parseCodebaseIndexContent(content);
1042
+ } catch {
1043
+ return {
1044
+ exists: false,
1045
+ lastUpdatedAt: "",
1046
+ lastUpdatedBy: "",
1047
+ sourceStage: "",
1048
+ changedFiles: [],
1049
+ fileSnapshots: {},
1050
+ explorationHistory: [],
1051
+ summaryVersion: 0,
1052
+ freshnessStatus: "unknown"
1053
+ };
1054
+ }
1055
+ }
1056
+ function parseCodebaseIndexContent(content) {
1057
+ const result = { exists: true };
1058
+ for (const line of content.split(`
1059
+ `)) {
1060
+ if (line.startsWith("#") || line.trim() === "")
1061
+ continue;
1062
+ const strippedLine = line.replace(/\*\*/g, "").replace(/^([a-zA-Z_][a-zA-Z0-9_]*):\s*(.*)/, "$1: $2");
1063
+ const kvMatch = strippedLine.match(/^([a-zA-Z_][a-zA-Z0-9_]*):\s*(.*)/);
1064
+ if (!kvMatch)
1065
+ continue;
1066
+ const key = kvMatch[1].trim();
1067
+ const value = kvMatch[2].trim();
1068
+ if (key === "changedFiles") {
1069
+ result.changedFiles = value.replace(/[\[\]]/g, "").split(",").map((s) => s.trim()).filter(Boolean);
1070
+ } else if (key === "summaryVersion") {
1071
+ result.summaryVersion = parseInt(value, 10) || 0;
1072
+ } else if (key === "freshnessStatus") {
1073
+ result.freshnessStatus = value;
1074
+ } else if (key === "lastUpdatedAt" || key === "lastUpdatedBy" || key === "sourceStage") {
1075
+ result[key] = value.replace(/^["']|["']$/g, "");
1076
+ }
1077
+ }
1078
+ let blockCount = 0;
1079
+ for (const jsonMatch of content.matchAll(/```json\n([\s\S]*?)\n```/g)) {
1080
+ if (blockCount >= 2)
1081
+ break;
1082
+ blockCount++;
1913
1083
  try {
1914
- content = readFileSync15(fullPath, "utf-8");
1915
- } catch (e) {
1916
- return `Error: Could not read file ${args.filePath}`;
1917
- }
1918
- if (!content.includes(args.targetContent)) {
1919
- return `Error: Target content not found in ${args.filePath}. It may have been modified by another agent.`;
1920
- }
1921
- if (args.expectedHash) {
1922
- const actualHash = createHash2("md5").update(args.targetContent).digest("hex");
1923
- if (actualHash !== args.expectedHash) {
1924
- return `Error: Hash mismatch for target content. Expected ${args.expectedHash}, got ${actualHash}. Refusing to edit stale content.`;
1084
+ const parsed = JSON.parse(jsonMatch[1]);
1085
+ if (parsed.fileSnapshots)
1086
+ result.fileSnapshots = parsed.fileSnapshots;
1087
+ if (parsed.explorationHistory)
1088
+ result.explorationHistory = parsed.explorationHistory;
1089
+ if (!parsed.fileSnapshots && !parsed.explorationHistory) {
1090
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
1091
+ if (!result.fileSnapshots)
1092
+ result.fileSnapshots = {};
1093
+ Object.assign(result.fileSnapshots, parsed);
1094
+ } else if (Array.isArray(parsed)) {
1095
+ result.explorationHistory = parsed;
1096
+ }
1925
1097
  }
1098
+ } catch {
1099
+ result.freshnessStatus = "unknown";
1926
1100
  }
1927
- const newContent = content.replace(args.targetContent, args.replacementContent);
1928
- writeFileSync11(fullPath, newContent, "utf-8");
1929
- return `Successfully updated ${args.filePath} using hash-anchored edit.`;
1930
1101
  }
1931
- });
1102
+ return {
1103
+ exists: true,
1104
+ lastUpdatedAt: result.lastUpdatedAt || "",
1105
+ lastUpdatedBy: result.lastUpdatedBy || "",
1106
+ sourceStage: result.sourceStage || "",
1107
+ changedFiles: result.changedFiles || [],
1108
+ fileSnapshots: result.fileSnapshots || {},
1109
+ explorationHistory: result.explorationHistory || [],
1110
+ summaryVersion: result.summaryVersion || 0,
1111
+ freshnessStatus: result.freshnessStatus || "unknown"
1112
+ };
1113
+ }
1932
1114
 
1933
1115
  // src/tools/council.ts
1934
- import { tool as tool10 } from "@opencode-ai/plugin";
1935
- import { appendFileSync as appendFileSync3, existsSync as existsSync15, mkdirSync as mkdirSync10 } from "fs";
1936
- import { join as join14 } from "path";
1937
- import { createHash as createHash3 } from "crypto";
1938
- import { readFileSync as readFileSync16 } from "fs";
1116
+ import { readFileSync as readFileSync11 } from "fs";
1939
1117
  var _councilCache = new Map;
1940
1118
  var COUNCIL_CACHE_TTL_MS = 20 * 60 * 1000;
1941
1119
  function councilCacheKey(task, agents, stateVersion, indexVersion) {
1942
1120
  const sorted = [...agents].sort();
1943
- return createHash3("sha256").update(JSON.stringify({ task: task.trim(), agents: sorted, sv: stateVersion, iv: indexVersion })).digest("hex").slice(0, 32);
1121
+ return createHash2("sha256").update(JSON.stringify({ task: task.trim(), agents: sorted, sv: stateVersion, iv: indexVersion })).digest("hex").slice(0, 32);
1944
1122
  }
1945
1123
  async function runWithConcurrencyLimit(tasks, limit) {
1946
1124
  const results = new Array(tasks.length);
@@ -1956,20 +1134,20 @@ async function runWithConcurrencyLimit(tasks, limit) {
1956
1134
  return results;
1957
1135
  }
1958
1136
  function createCouncilTool(client) {
1959
- return tool10({
1137
+ return tool8({
1960
1138
  description: "Run an ensemble of agents (Council) on the same task to reach consensus or compare approaches. Runs specialized agents in parallel (bounded concurrency) and returns their synthesized outputs.",
1961
1139
  args: {
1962
- task: tool10.schema.string(),
1963
- agents: tool10.schema.array(tool10.schema.string()).optional(),
1964
- force_fresh: tool10.schema.boolean().optional().default(false),
1965
- max_concurrency: tool10.schema.number().optional().default(3)
1140
+ task: tool8.schema.string(),
1141
+ agents: tool8.schema.array(tool8.schema.string()).optional(),
1142
+ force_fresh: tool8.schema.boolean().optional().default(false),
1143
+ max_concurrency: tool8.schema.number().optional().default(3)
1966
1144
  },
1967
1145
  async execute(args, context) {
1968
1146
  const agents = args.agents || ["architect", "reviewer", "backend-coder"];
1969
1147
  const concurrencyLimit = Math.max(1, Math.min(5, typeof args.max_concurrency === "number" ? args.max_concurrency : 3));
1970
1148
  const index = readCodebaseIndex(context.directory);
1971
1149
  const sp = statePath(context.directory);
1972
- const rawState = existsSync15(sp) ? readFileSync16(sp, "utf-8") : "";
1150
+ const rawState = existsSync10(sp) ? readFileSync11(sp, "utf-8") : "";
1973
1151
  const state = rawState ? parseState(rawState) : {};
1974
1152
  const stateVersion = typeof state.summaryVersion === "number" ? state.summaryVersion : 0;
1975
1153
  const indexVersion = typeof index.summaryVersion === "number" ? index.summaryVersion : 0;
@@ -2045,18 +1223,18 @@ Please synthesize these results. Identify areas of agreement, resolve conflicts,
2045
1223
  function persistCouncilResult(directory, payload) {
2046
1224
  try {
2047
1225
  const base = codebaseDir(directory);
2048
- if (!existsSync15(base))
2049
- mkdirSync10(base, { recursive: true });
2050
- const path = join14(base, "COUNCILS.jsonl");
2051
- appendFileSync3(path, JSON.stringify(payload) + `
1226
+ if (!existsSync10(base))
1227
+ mkdirSync7(base, { recursive: true });
1228
+ const path = join10(base, "COUNCILS.jsonl");
1229
+ appendFileSync2(path, JSON.stringify(payload) + `
2052
1230
  `, "utf-8");
2053
1231
  } catch {}
2054
1232
  }
2055
1233
 
2056
1234
  // src/tools/reflect.ts
2057
- import { tool as tool11 } from "@opencode-ai/plugin";
2058
- import { existsSync as existsSync16, readFileSync as readFileSync17 } from "fs";
2059
- import { join as join15 } from "path";
1235
+ import { tool as tool9 } from "@opencode-ai/plugin";
1236
+ import { existsSync as existsSync11, readFileSync as readFileSync12 } from "fs";
1237
+ import { join as join11 } from "path";
2060
1238
  var MAX_ARTIFACT_BYTES = 4000;
2061
1239
  function tail(text, maxBytes) {
2062
1240
  if (text.length <= maxBytes)
@@ -2064,10 +1242,10 @@ function tail(text, maxBytes) {
2064
1242
  return `... (truncated) ...
2065
1243
  ` + text.slice(-maxBytes);
2066
1244
  }
2067
- var reflectTool = tool11({
1245
+ var reflectTool = tool9({
2068
1246
  description: "Gather session artifacts (decisions, telemetry, failures, policies) and return a structured " + "reflection context that the agent can reason over to produce self-improvement proposals.",
2069
1247
  args: {
2070
- scope: tool11.schema.enum(["session", "project"]).optional().describe("'session' (default) uses only recent artifacts; 'project' includes all historical data")
1248
+ scope: tool9.schema.enum(["session", "project"]).optional().describe("'session' (default) uses only recent artifacts; 'project' includes all historical data")
2071
1249
  },
2072
1250
  async execute(args, context) {
2073
1251
  const root = context.directory;
@@ -2085,11 +1263,11 @@ var reflectTool = tool11({
2085
1263
  ];
2086
1264
  let found = 0;
2087
1265
  for (const [rel, label] of ARTIFACT_PATHS) {
2088
- const full = join15(root, rel);
2089
- if (!existsSync16(full))
1266
+ const full = join11(root, rel);
1267
+ if (!existsSync11(full))
2090
1268
  continue;
2091
1269
  try {
2092
- const raw = readFileSync17(full, "utf-8").trim();
1270
+ const raw = readFileSync12(full, "utf-8").trim();
2093
1271
  if (!raw)
2094
1272
  continue;
2095
1273
  const count = raw.split(`
@@ -2109,16 +1287,16 @@ var reflectTool = tool11({
2109
1287
  });
2110
1288
 
2111
1289
  // src/tools/codegraph-tool.ts
2112
- import { tool as tool12 } from "@opencode-ai/plugin";
1290
+ import { tool as tool10 } from "@opencode-ai/plugin";
2113
1291
 
2114
1292
  // src/services/codegraph.ts
2115
1293
  import { spawnSync } from "child_process";
2116
- import { existsSync as existsSync17, readFileSync as readFileSync18, writeFileSync as writeFileSync12, mkdirSync as mkdirSync11 } from "fs";
2117
- import { join as join16 } from "path";
1294
+ import { existsSync as existsSync12, readFileSync as readFileSync13, writeFileSync as writeFileSync10, mkdirSync as mkdirSync8 } from "fs";
1295
+ import { join as join12 } from "path";
2118
1296
  var CODEGRAPH_META_FILE = "CODEGRAPH.md";
2119
1297
  var MAX_FRESHNESS_MS = 30 * 60 * 1000;
2120
1298
  function metaPath(dir) {
2121
- return join16(codebaseDir(dir), CODEGRAPH_META_FILE);
1299
+ return join12(codebaseDir(dir), CODEGRAPH_META_FILE);
2122
1300
  }
2123
1301
  function isCodegraphInstalled() {
2124
1302
  try {
@@ -2133,11 +1311,11 @@ function isCodegraphInstalled() {
2133
1311
  }
2134
1312
  }
2135
1313
  function isCodegraphIndexed(dir) {
2136
- return existsSync17(join16(dir, ".codegraph", "codegraph.db"));
1314
+ return existsSync12(join12(dir, ".codegraph", "codegraph.db"));
2137
1315
  }
2138
1316
  function readCodegraphMeta(dir) {
2139
1317
  const path = metaPath(dir);
2140
- if (!existsSync17(path)) {
1318
+ if (!existsSync12(path)) {
2141
1319
  return {
2142
1320
  installed: false,
2143
1321
  indexed: false,
@@ -2150,7 +1328,7 @@ function readCodegraphMeta(dir) {
2150
1328
  };
2151
1329
  }
2152
1330
  try {
2153
- const content = readFileSync18(path, "utf-8");
1331
+ const content = readFileSync13(path, "utf-8");
2154
1332
  return parseCodegraphMeta(content);
2155
1333
  } catch {
2156
1334
  return {
@@ -2217,8 +1395,8 @@ function parseCodegraphMeta(content) {
2217
1395
  }
2218
1396
  function writeCodegraphMeta(dir, meta) {
2219
1397
  const base = codebaseDir(dir);
2220
- if (!existsSync17(base))
2221
- mkdirSync11(base, { recursive: true });
1398
+ if (!existsSync12(base))
1399
+ mkdirSync8(base, { recursive: true });
2222
1400
  const lines = [
2223
1401
  "# Codegraph Metadata",
2224
1402
  "",
@@ -2231,7 +1409,7 @@ function writeCodegraphMeta(dir, meta) {
2231
1409
  `**installLog:** ${meta.installLog}`,
2232
1410
  `**indexLog:** ${meta.indexLog}`
2233
1411
  ];
2234
- writeFileSync12(metaPath(dir), lines.join(`
1412
+ writeFileSync10(metaPath(dir), lines.join(`
2235
1413
  `), "utf-8");
2236
1414
  }
2237
1415
  function isCodegraphFresh(dir, maxAgeMs = MAX_FRESHNESS_MS) {
@@ -2442,11 +1620,11 @@ function markCodegraphStale(dir) {
2442
1620
  }
2443
1621
 
2444
1622
  // src/tools/codegraph-tool.ts
2445
- var codegraphTool = tool12({
1623
+ var codegraphTool = tool10({
2446
1624
  description: "Manage codegraph lifecycle only: check installation, install, init/rebuild the index, refresh (incremental sync), " + "query status, or mark-stale. Valid actions: check | install | init | refresh | status | mark-stale. " + "Do NOT use this tool for code intelligence queries (files, search, callers, callees, etc.) — " + "those are available as codegraph MCP tools (codegraph_files, codegraph_search, codegraph_context, " + "codegraph_explore, codegraph_callers, codegraph_callees, codegraph_impact, codegraph_trace) " + "when the index is ready.",
2447
1625
  args: {
2448
- action: tool12.schema.enum(["check", "install", "init", "refresh", "status", "mark-stale"]),
2449
- agent: tool12.schema.string().optional()
1626
+ action: tool10.schema.enum(["check", "install", "init", "refresh", "status", "mark-stale"]),
1627
+ agent: tool10.schema.string().optional()
2450
1628
  },
2451
1629
  async execute(args, context) {
2452
1630
  const dir = context.directory ?? process.cwd();
@@ -2535,21 +1713,21 @@ var codegraphTool = tool12({
2535
1713
  });
2536
1714
 
2537
1715
  // src/tools/load-rules.ts
2538
- import { tool as tool13 } from "@opencode-ai/plugin";
2539
- import { existsSync as existsSync18, readFileSync as readFileSync19 } from "fs";
2540
- import { join as join17, dirname as dirname2 } from "path";
1716
+ import { tool as tool11 } from "@opencode-ai/plugin";
1717
+ import { existsSync as existsSync13, readFileSync as readFileSync14 } from "fs";
1718
+ import { join as join13, dirname as dirname2 } from "path";
2541
1719
  import { fileURLToPath } from "url";
2542
- var RULES_DIR = join17(dirname2(fileURLToPath(import.meta.url)), "..", "rules");
1720
+ var RULES_DIR = join13(dirname2(fileURLToPath(import.meta.url)), "..", "rules");
2543
1721
  var _loadedPaths = new Set;
2544
- var loadRulesTool = tool13({
1722
+ var loadRulesTool = tool11({
2545
1723
  description: "Load additional rule modules on demand for the current workflow stage. " + "Use this at the start of a new stage (execute, verify, fix-bug) to load " + "coding-style, security, testing, and language-specific rules that were not " + "injected at startup. Returns the full text of selected rules. " + "Already-loaded rules are not returned again (suppressed to avoid duplication).",
2546
1724
  args: {
2547
- stage: tool13.schema.string().optional().describe("Current workflow stage: discuss | plan | execute | verify | fix-bug | write-docs"),
2548
- languages: tool13.schema.array(tool13.schema.string()).optional().describe("Project languages to load rules for, e.g. ['typescript']. " + "Omit to use all languages (returns all matching stage rules)."),
2549
- force_reload: tool13.schema.boolean().optional().default(false).describe("When true, return rules even if they were already loaded in this session. " + "Use only when stage context has changed and you need a fresh load.")
1725
+ stage: tool11.schema.string().optional().describe("Current workflow stage: discuss | plan | execute | verify | fix-bug | write-docs"),
1726
+ languages: tool11.schema.array(tool11.schema.string()).optional().describe("Project languages to load rules for, e.g. ['typescript']. " + "Omit to use all languages (returns all matching stage rules)."),
1727
+ force_reload: tool11.schema.boolean().optional().default(false).describe("When true, return rules even if they were already loaded in this session. " + "Use only when stage context has changed and you need a fresh load.")
2550
1728
  },
2551
1729
  async execute(args) {
2552
- const rulesDir = existsSync18(RULES_DIR) ? RULES_DIR : null;
1730
+ const rulesDir = existsSync13(RULES_DIR) ? RULES_DIR : null;
2553
1731
  if (!rulesDir) {
2554
1732
  return JSON.stringify({
2555
1733
  loaded: [],
@@ -2575,7 +1753,7 @@ var loadRulesTool = tool13({
2575
1753
  continue;
2576
1754
  }
2577
1755
  try {
2578
- const text = readFileSync19(rule.path, "utf-8");
1756
+ const text = readFileSync14(rule.path, "utf-8");
2579
1757
  contents.push(`## ${name}
2580
1758
 
2581
1759
  ${text}`);
@@ -2606,11 +1784,11 @@ ${text}`);
2606
1784
  function ruleShortName(rule) {
2607
1785
  return rule.path.replace(RULES_DIR + "/", "").replace(/\.md$/, "");
2608
1786
  }
2609
- var listRulesTool = tool13({
1787
+ var listRulesTool = tool11({
2610
1788
  description: "List all available FlowDeck rule modules with their metadata (description, always_on, " + "stages, languages). Use this before calling load-rules to see what is available. " + "Does NOT load rule content — only returns metadata for discovery.",
2611
1789
  args: {},
2612
1790
  async execute() {
2613
- const rulesDir = existsSync18(RULES_DIR) ? RULES_DIR : null;
1791
+ const rulesDir = existsSync13(RULES_DIR) ? RULES_DIR : null;
2614
1792
  if (!rulesDir) {
2615
1793
  return JSON.stringify({ rules: [], error: `Rules directory not found at ${RULES_DIR}` });
2616
1794
  }
@@ -2630,13 +1808,13 @@ var listRulesTool = tool13({
2630
1808
  });
2631
1809
 
2632
1810
  // src/tools/rtk-setup.ts
2633
- import { tool as tool14 } from "@opencode-ai/plugin";
1811
+ import { tool as tool12 } from "@opencode-ai/plugin";
2634
1812
 
2635
1813
  // src/services/rtk-manager.ts
2636
1814
  import { spawnSync as spawnSync2 } from "child_process";
2637
- import { existsSync as existsSync19 } from "fs";
2638
- import { homedir as homedir2 } from "os";
2639
- import { join as join18 } from "path";
1815
+ import { existsSync as existsSync14 } from "fs";
1816
+ import { homedir } from "os";
1817
+ import { join as join14 } from "path";
2640
1818
 
2641
1819
  // src/services/rtk-policy.ts
2642
1820
  var SUPPORTED_COMMANDS = new Set([
@@ -2682,7 +1860,7 @@ var INSTALL_INSTRUCTIONS = [
2682
1860
  "After installation, call rtk-setup again to verify detection."
2683
1861
  ].join(`
2684
1862
  `);
2685
- var CANDIDATE_PATHS = [join18(homedir2(), ".local", "bin", "rtk"), "/usr/local/bin/rtk", "/usr/bin/rtk"];
1863
+ var CANDIDATE_PATHS = [join14(homedir(), ".local", "bin", "rtk"), "/usr/local/bin/rtk", "/usr/bin/rtk"];
2686
1864
  function detectRtk() {
2687
1865
  const fromPath = spawnSync2("rtk", ["--version"], { encoding: "utf-8", timeout: 5000 });
2688
1866
  if (fromPath.status === 0) {
@@ -2691,7 +1869,7 @@ function detectRtk() {
2691
1869
  return { installed: true, binPath: "rtk", version };
2692
1870
  }
2693
1871
  for (const candidate of CANDIDATE_PATHS) {
2694
- if (!existsSync19(candidate))
1872
+ if (!existsSync14(candidate))
2695
1873
  continue;
2696
1874
  const result = spawnSync2(candidate, ["--version"], { encoding: "utf-8", timeout: 5000 });
2697
1875
  if (result.status === 0) {
@@ -2769,7 +1947,7 @@ function getRtkStatus(opts) {
2769
1947
  }
2770
1948
 
2771
1949
  // src/tools/rtk-setup.ts
2772
- var rtkSetupTool = tool14({
1950
+ var rtkSetupTool = tool12({
2773
1951
  description: [
2774
1952
  "Detect, initialize, and report status of rtk (output compression proxy for CLI commands).",
2775
1953
  "rtk reduces noisy CLI output (git, npm, test runners, linters, docker) by 60-90%.",
@@ -2777,7 +1955,7 @@ var rtkSetupTool = tool14({
2777
1955
  "When RTK_INSTALLED=true in the environment, use `$RTK_BIN git status` for compressed output."
2778
1956
  ].join(" "),
2779
1957
  args: {
2780
- action: tool14.schema.enum(["status", "init"]).optional().describe("'status' — detect and report rtk state (default). " + "'init' — detect, then run `rtk init -g` to install the bash hook. " + "Use 'init' only once per environment setup.")
1958
+ action: tool12.schema.enum(["status", "init"]).optional().describe("'status' — detect and report rtk state (default). " + "'init' — detect, then run `rtk init -g` to install the bash hook. " + "Use 'init' only once per environment setup.")
2781
1959
  },
2782
1960
  async execute(args) {
2783
1961
  const action = args.action ?? "status";
@@ -2817,15 +1995,99 @@ var rtkSetupTool = tool14({
2817
1995
  });
2818
1996
 
2819
1997
  // src/hooks/guard-rails.ts
2820
- import { existsSync as existsSync20, readFileSync as readFileSync20 } from "fs";
2821
- import { join as join19 } from "path";
1998
+ import { existsSync as existsSync16, readFileSync as readFileSync16 } from "fs";
1999
+ import { join as join16 } from "path";
2000
+
2001
+ // src/lib/task-routing.ts
2002
+ var UI_HEAVY_KEYWORDS = [
2003
+ "landing page",
2004
+ "marketing site",
2005
+ "website",
2006
+ "web app",
2007
+ "mobile app",
2008
+ "app screen",
2009
+ "dashboard",
2010
+ "admin panel",
2011
+ "settings page",
2012
+ "onboarding ux",
2013
+ "kanban",
2014
+ "design system",
2015
+ "responsive",
2016
+ "ui",
2017
+ "ux",
2018
+ "cta",
2019
+ "conversion flow",
2020
+ "saas interface",
2021
+ "user-facing"
2022
+ ];
2023
+ var NON_UI_KEYWORDS = [
2024
+ "backend",
2025
+ "infrastructure",
2026
+ "migration",
2027
+ "pipeline",
2028
+ "api only",
2029
+ "database only",
2030
+ "cli",
2031
+ "worker"
2032
+ ];
2033
+ function isUiHeavyTask(input) {
2034
+ const normalized = input.trim().toLowerCase();
2035
+ if (!normalized)
2036
+ return false;
2037
+ const hasUiSignal = UI_HEAVY_KEYWORDS.some((keyword) => normalized.includes(keyword));
2038
+ if (!hasUiSignal)
2039
+ return false;
2040
+ const hasOnlyNonUiSignals = NON_UI_KEYWORDS.some((keyword) => normalized.includes(keyword)) && !normalized.includes("frontend");
2041
+ return !hasOnlyNonUiSignals;
2042
+ }
2043
+
2044
+ // src/config/loader.ts
2045
+ import { existsSync as existsSync15, readFileSync as readFileSync15 } from "fs";
2046
+ import { join as join15 } from "path";
2047
+ import { homedir as homedir2 } from "os";
2048
+ var CONFIG_FILENAME = "flowdeck.json";
2049
+ function getGlobalConfigDir() {
2050
+ return process.env.OPENCODE_CONFIG_DIR || (process.env.XDG_CONFIG_HOME ? join15(process.env.XDG_CONFIG_HOME, "opencode") : join15(homedir2(), ".config", "opencode"));
2051
+ }
2052
+ function loadFlowDeckConfig(directory) {
2053
+ const candidates = [];
2054
+ if (directory) {
2055
+ candidates.push(join15(directory, ".opencode", CONFIG_FILENAME));
2056
+ }
2057
+ candidates.push(join15(getGlobalConfigDir(), CONFIG_FILENAME));
2058
+ for (const configPath of candidates) {
2059
+ if (existsSync15(configPath)) {
2060
+ try {
2061
+ const content = readFileSync15(configPath, "utf-8");
2062
+ return JSON.parse(content);
2063
+ } catch {}
2064
+ }
2065
+ }
2066
+ return {};
2067
+ }
2068
+ function resolveDesignFirstConfig(config) {
2069
+ return {
2070
+ enabled: config.designFirst?.enabled ?? true,
2071
+ enforcement: config.designFirst?.enforcement ?? "strict",
2072
+ requireApprovalBeforeImplementation: config.designFirst?.requireApprovalBeforeImplementation ?? true,
2073
+ modelOverrides: config.designFirst?.modelOverrides ?? {},
2074
+ defaultSkillsByTaskType: config.designFirst?.defaultSkillsByTaskType ?? {
2075
+ "landing-page": ["landing-page-design", "wireframe-planning", "design-system-definition", "frontend-handoff"],
2076
+ dashboard: ["dashboard-design", "ui-ux-planning", "wireframe-planning", "responsive-review"],
2077
+ "admin-panel": ["ui-ux-planning", "wireframe-planning", "design-system-definition", "frontend-handoff"],
2078
+ "app-screen": ["app-shell-design", "ui-ux-planning", "wireframe-planning", "responsive-review"],
2079
+ "general-ui": ["ui-ux-planning", "wireframe-planning", "design-system-definition", "frontend-handoff"]
2080
+ }
2081
+ };
2082
+ }
2083
+ // src/hooks/guard-rails.ts
2822
2084
  var PLANNING_DIR2 = ".planning";
2823
2085
  var CONFIG_FILE = "config.json";
2824
2086
  var STATE_FILE2 = "STATE.md";
2825
2087
  function resolveExecutionMode(configPath, trustScore, volatility) {
2826
- if (existsSync20(configPath)) {
2088
+ if (existsSync16(configPath)) {
2827
2089
  try {
2828
- const config = JSON.parse(readFileSync20(configPath, "utf-8"));
2090
+ const config = JSON.parse(readFileSync16(configPath, "utf-8"));
2829
2091
  if (config.execution_mode === "review-only")
2830
2092
  return "review-only";
2831
2093
  if (config.execution_mode === "guarded")
@@ -2879,22 +2141,22 @@ async function guardRailsHook(ctx, input, _output) {
2879
2141
  if (!ENABLED)
2880
2142
  return;
2881
2143
  const dir = ctx.directory;
2882
- const planningDirPath = join19(dir, PLANNING_DIR2);
2144
+ const planningDirPath = join16(dir, PLANNING_DIR2);
2883
2145
  const codebaseDirectory = codebaseDir(dir);
2884
- const configPath = join19(planningDirPath, CONFIG_FILE);
2885
- const statePath2 = join19(planningDirPath, STATE_FILE2);
2146
+ const configPath = join16(planningDirPath, CONFIG_FILE);
2147
+ const statePath2 = join16(planningDirPath, STATE_FILE2);
2886
2148
  const workspaceRoot = findWorkspaceRoot(dir);
2887
2149
  if (workspaceRoot && dir !== workspaceRoot) {
2888
2150
  const config = getWorkspaceConfig(dir);
2889
- if (config && config.workspace_mode === "shared" && !existsSync20(planningDirPath)) {
2151
+ if (config && config.workspace_mode === "shared" && !existsSync16(planningDirPath)) {
2890
2152
  const msg = `No .planning/ in this sub-repo. Switch to workspace root: cd ${workspaceRoot}`;
2891
2153
  throw new Error(`[flowdeck] BLOCK: ${msg}`);
2892
2154
  }
2893
2155
  }
2894
2156
  if (input.tool === "write" || input.tool === "edit") {
2895
- if (!existsSync20(planningDirPath))
2157
+ if (!existsSync16(planningDirPath))
2896
2158
  return;
2897
- if (!existsSync20(codebaseDirectory)) {
2159
+ if (!existsSync16(codebaseDirectory)) {
2898
2160
  throw new Error(`[flowdeck] WARNING: .codebase/ not found. Run /fd-map-codebase to map the codebase.`);
2899
2161
  }
2900
2162
  const execMode = resolveExecutionMode(configPath, null);
@@ -2950,15 +2212,15 @@ function getDesignGateMessage(dir) {
2950
2212
  }
2951
2213
  function planSuggestsUiHeavy(dir, phase) {
2952
2214
  const planPath = phasePlanPath(dir, phase);
2953
- if (!existsSync20(planPath))
2215
+ if (!existsSync16(planPath))
2954
2216
  return false;
2955
- const planContent = readFileSync20(planPath, "utf-8");
2217
+ const planContent = readFileSync16(planPath, "utf-8");
2956
2218
  return isUiHeavyTask(planContent);
2957
2219
  }
2958
2220
  function effectiveSeverity(configPath, statePath2) {
2959
- if (existsSync20(configPath)) {
2221
+ if (existsSync16(configPath)) {
2960
2222
  try {
2961
- const configContent = readFileSync20(configPath, "utf-8");
2223
+ const configContent = readFileSync16(configPath, "utf-8");
2962
2224
  const config = JSON.parse(configContent);
2963
2225
  if (config.guard_enforcement === "warn")
2964
2226
  return "warn";
@@ -2974,10 +2236,10 @@ function getEffectiveSeverity(configPath, statePath2) {
2974
2236
  return effectiveSeverity(configPath, statePath2);
2975
2237
  }
2976
2238
  function getPlanConfirmed(statePath2) {
2977
- if (!existsSync20(statePath2))
2239
+ if (!existsSync16(statePath2))
2978
2240
  return false;
2979
2241
  try {
2980
- const content = readFileSync20(statePath2, "utf-8");
2242
+ const content = readFileSync16(statePath2, "utf-8");
2981
2243
  const match = content.match(/plan_confirmed:\s*(true|false)/i);
2982
2244
  return match ? match[1].toLowerCase() === "true" : false;
2983
2245
  } catch {
@@ -2985,32 +2247,32 @@ function getPlanConfirmed(statePath2) {
2985
2247
  }
2986
2248
  }
2987
2249
  function getWarningMessage(planningDir2) {
2988
- if (!existsSync20(join19(planningDir2, STATE_FILE2))) {
2250
+ if (!existsSync16(join16(planningDir2, STATE_FILE2))) {
2989
2251
  return "No STATE.md found. Run /fd-map-codebase then /fd-new-feature to start a feature.";
2990
2252
  }
2991
2253
  return "Plan not confirmed. Run /fd-plan and confirm to enable execution.";
2992
2254
  }
2993
2255
  function getBlockMessage(planningDir2) {
2994
- if (!existsSync20(join19(planningDir2, STATE_FILE2))) {
2256
+ if (!existsSync16(join16(planningDir2, STATE_FILE2))) {
2995
2257
  return "No STATE.md found. Run /fd-map-codebase then /fd-new-feature to start a feature.";
2996
2258
  }
2997
2259
  return "Plan not confirmed. Run /fd-plan and confirm to enable execution.";
2998
2260
  }
2999
2261
 
3000
2262
  // src/hooks/tool-guard.ts
3001
- import { existsSync as existsSync21, readFileSync as readFileSync21 } from "fs";
3002
- import { join as join20 } from "path";
2263
+ import { existsSync as existsSync17, readFileSync as readFileSync17 } from "fs";
2264
+ import { join as join17 } from "path";
3003
2265
  var IS_ENABLED = () => process.env.FLOWDECK_TOOL_GUARD_ENABLED === "on";
3004
2266
  var BLOCKED_PATTERNS = {
3005
2267
  read: [".env", ".pem", ".key", ".secret"],
3006
2268
  write: ["node_modules"],
3007
2269
  bash: ["rm -rf"]
3008
2270
  };
3009
- function isBlocked(tool15, args) {
3010
- const patterns = BLOCKED_PATTERNS[tool15];
2271
+ function isBlocked(tool13, args) {
2272
+ const patterns = BLOCKED_PATTERNS[tool13];
3011
2273
  if (!patterns)
3012
2274
  return null;
3013
- if (tool15 === "bash") {
2275
+ if (tool13 === "bash") {
3014
2276
  const cmd = args.command;
3015
2277
  if (!cmd)
3016
2278
  return null;
@@ -3021,7 +2283,7 @@ function isBlocked(tool15, args) {
3021
2283
  }
3022
2284
  return null;
3023
2285
  }
3024
- if (tool15 === "read") {
2286
+ if (tool13 === "read") {
3025
2287
  const filePath = args.filePath;
3026
2288
  if (!filePath)
3027
2289
  return null;
@@ -3032,7 +2294,7 @@ function isBlocked(tool15, args) {
3032
2294
  }
3033
2295
  return null;
3034
2296
  }
3035
- if (tool15 === "write") {
2297
+ if (tool13 === "write") {
3036
2298
  const filePath = args.filePath;
3037
2299
  if (!filePath)
3038
2300
  return null;
@@ -3046,11 +2308,11 @@ function isBlocked(tool15, args) {
3046
2308
  return null;
3047
2309
  }
3048
2310
  function checkArchConstraint(directory, filePath) {
3049
- const constraintsPath = join20(codebaseDir(directory), "CONSTRAINTS.md");
3050
- if (!existsSync21(constraintsPath))
2311
+ const constraintsPath = join17(codebaseDir(directory), "CONSTRAINTS.md");
2312
+ if (!existsSync17(constraintsPath))
3051
2313
  return null;
3052
2314
  try {
3053
- const content = readFileSync21(constraintsPath, "utf-8");
2315
+ const content = readFileSync17(constraintsPath, "utf-8");
3054
2316
  const match = content.match(/## Forbidden Paths\n([\s\S]*?)(?:\n##|$)/);
3055
2317
  if (!match)
3056
2318
  return null;
@@ -3091,9 +2353,9 @@ function isUiDesignApprovalRequired(directory) {
3091
2353
  return !(state.design_stage === "handoff_complete" && state.design_approved);
3092
2354
  }
3093
2355
  const planPath = phasePlanPath(directory, state.phase || 1);
3094
- if (!existsSync21(planPath))
2356
+ if (!existsSync17(planPath))
3095
2357
  return false;
3096
- const planContent = readFileSync21(planPath, "utf-8");
2358
+ const planContent = readFileSync17(planPath, "utf-8");
3097
2359
  if (!isUiHeavyTask(planContent))
3098
2360
  return false;
3099
2361
  return !(state.design_stage === "handoff_complete" && state.design_approved);
@@ -3122,18 +2384,18 @@ async function toolGuardHook(ctx, input, output) {
3122
2384
  }
3123
2385
 
3124
2386
  // src/hooks/session-start.ts
3125
- import { existsSync as existsSync22, readFileSync as readFileSync22 } from "fs";
2387
+ import { existsSync as existsSync18, readFileSync as readFileSync18 } from "fs";
3126
2388
  async function sessionStartHook(ctx) {
3127
2389
  const planningDir2 = ctx.directory + "/.planning";
3128
2390
  const codebaseDirectory = codebaseDir(ctx.directory);
3129
2391
  const workspaceRoot = findWorkspaceRoot(ctx.directory);
3130
2392
  const config = workspaceRoot ? getWorkspaceConfig(ctx.directory) : null;
3131
- if (!existsSync22(planningDir2)) {
2393
+ if (!existsSync18(planningDir2)) {
3132
2394
  return {
3133
2395
  flowdeck_phase: null,
3134
2396
  flowdeck_status: "no_plan",
3135
2397
  flowdeck_warning: "Run /fd-map-codebase to index the codebase, then /fd-new-feature to start a feature.",
3136
- flowdeck_has_codebase: existsSync22(codebaseDirectory),
2398
+ flowdeck_has_codebase: existsSync18(codebaseDirectory),
3137
2399
  ...workspaceRoot && config?.sub_repos ? {
3138
2400
  flowdeck_workspace_root: workspaceRoot,
3139
2401
  flowdeck_sub_repos: config.sub_repos,
@@ -3144,7 +2406,7 @@ async function sessionStartHook(ctx) {
3144
2406
  }
3145
2407
  try {
3146
2408
  const stateFilePath = statePath(ctx.directory);
3147
- const content = readFileSync22(stateFilePath, "utf-8");
2409
+ const content = readFileSync18(stateFilePath, "utf-8");
3148
2410
  const state = parseState(content);
3149
2411
  const currentPhase = state["current_phase"] || {};
3150
2412
  const result = {
@@ -3152,7 +2414,7 @@ async function sessionStartHook(ctx) {
3152
2414
  flowdeck_status: currentPhase["status"] ?? null,
3153
2415
  flowdeck_steps_pending: currentPhase["steps_pending"] ?? null,
3154
2416
  flowdeck_last_action: currentPhase["last_action"] ?? null,
3155
- flowdeck_has_codebase: existsSync22(codebaseDirectory)
2417
+ flowdeck_has_codebase: existsSync18(codebaseDirectory)
3156
2418
  };
3157
2419
  if (workspaceRoot && config?.sub_repos && config.sub_repos.length > 0) {
3158
2420
  result.flowdeck_workspace_root = workspaceRoot;
@@ -3166,7 +2428,7 @@ async function sessionStartHook(ctx) {
3166
2428
  flowdeck_phase: null,
3167
2429
  flowdeck_status: "error",
3168
2430
  flowdeck_warning: "State file unreadable. Continuing without flowdeck context.",
3169
- flowdeck_has_codebase: existsSync22(codebaseDirectory)
2431
+ flowdeck_has_codebase: existsSync18(codebaseDirectory)
3170
2432
  };
3171
2433
  if (workspaceRoot && config?.sub_repos && config.sub_repos.length > 0) {
3172
2434
  result.flowdeck_workspace_root = workspaceRoot;
@@ -3298,13 +2560,13 @@ class NotificationController {
3298
2560
  return this.lastNotifiedKey;
3299
2561
  }
3300
2562
  }
3301
- function notifyPermissionNeeded(tool15) {
3302
- notify("FlowDeck Permission Required", `Agent needs approval to use tool: ${tool15}`, "critical");
2563
+ function notifyPermissionNeeded(tool13) {
2564
+ notify("FlowDeck Permission Required", `Agent needs approval to use tool: ${tool13}`, "critical");
3303
2565
  }
3304
2566
 
3305
2567
  // src/hooks/patch-trust.ts
3306
- import { existsSync as existsSync23, readFileSync as readFileSync23 } from "fs";
3307
- import { join as join21 } from "path";
2568
+ import { existsSync as existsSync19, readFileSync as readFileSync19 } from "fs";
2569
+ import { join as join18 } from "path";
3308
2570
  var HIGH_RISK_KEYWORDS = [
3309
2571
  "password",
3310
2572
  "secret",
@@ -3326,11 +2588,11 @@ var HIGH_RISK_KEYWORDS = [
3326
2588
  "privilege"
3327
2589
  ];
3328
2590
  function loadFailedPaths(directory) {
3329
- const p = join21(codebaseDir(directory), "FAILURES.json");
3330
- if (!existsSync23(p))
2591
+ const p = join18(codebaseDir(directory), "FAILURES.json");
2592
+ if (!existsSync19(p))
3331
2593
  return [];
3332
2594
  try {
3333
- const data = JSON.parse(readFileSync23(p, "utf-8"));
2595
+ const data = JSON.parse(readFileSync19(p, "utf-8"));
3334
2596
  return (data.entries ?? []).flatMap((e) => e.affected_paths ?? []);
3335
2597
  } catch {
3336
2598
  return [];
@@ -3383,8 +2645,8 @@ async function patchTrustHook(ctx, input, output) {
3383
2645
  }
3384
2646
 
3385
2647
  // src/hooks/decision-trace-hook.ts
3386
- import { existsSync as existsSync24, mkdirSync as mkdirSync12, appendFileSync as appendFileSync4 } from "fs";
3387
- import { join as join22 } from "path";
2648
+ import { existsSync as existsSync20, mkdirSync as mkdirSync9, appendFileSync as appendFileSync3 } from "fs";
2649
+ import { join as join19 } from "path";
3388
2650
  async function decisionTraceHook(ctx, input, output) {
3389
2651
  if (input.tool !== "write" && input.tool !== "edit")
3390
2652
  return;
@@ -3393,8 +2655,8 @@ async function decisionTraceHook(ctx, input, output) {
3393
2655
  return;
3394
2656
  const base = codebaseDir(ctx.directory);
3395
2657
  try {
3396
- if (!existsSync24(base))
3397
- mkdirSync12(base, { recursive: true });
2658
+ if (!existsSync20(base))
2659
+ mkdirSync9(base, { recursive: true });
3398
2660
  const entry = {
3399
2661
  timestamp: new Date().toISOString(),
3400
2662
  file_path: filePath,
@@ -3406,14 +2668,14 @@ async function decisionTraceHook(ctx, input, output) {
3406
2668
  risk_level: "unknown",
3407
2669
  auto_recorded: true
3408
2670
  };
3409
- appendFileSync4(join22(base, "DECISIONS.jsonl"), JSON.stringify(entry) + `
2671
+ appendFileSync3(join19(base, "DECISIONS.jsonl"), JSON.stringify(entry) + `
3410
2672
  `, "utf-8");
3411
2673
  } catch {}
3412
2674
  }
3413
2675
 
3414
2676
  // src/services/approval-manager.ts
3415
- import { existsSync as existsSync25, readFileSync as readFileSync24, writeFileSync as writeFileSync13, mkdirSync as mkdirSync13 } from "fs";
3416
- import { join as join23 } from "path";
2677
+ import { existsSync as existsSync21, readFileSync as readFileSync20, writeFileSync as writeFileSync11, mkdirSync as mkdirSync10 } from "fs";
2678
+ import { join as join20 } from "path";
3417
2679
  var APPROVAL_TTL_MS = 30 * 60 * 1000;
3418
2680
  var SENSITIVE_PATTERNS = [
3419
2681
  /auth/i,
@@ -3450,20 +2712,20 @@ function isSensitivePath(filePath) {
3450
2712
  return SENSITIVE_PATTERNS.some((p) => p.test(filePath));
3451
2713
  }
3452
2714
  function approvalsPath(dir) {
3453
- return join23(codebaseDir(dir), "APPROVALS.json");
2715
+ return join20(codebaseDir(dir), "APPROVALS.json");
3454
2716
  }
3455
- function loadStore2(dir) {
2717
+ function loadStore(dir) {
3456
2718
  const p = approvalsPath(dir);
3457
- if (!existsSync25(p))
2719
+ if (!existsSync21(p))
3458
2720
  return { requests: [] };
3459
2721
  try {
3460
- return JSON.parse(readFileSync24(p, "utf-8"));
2722
+ return JSON.parse(readFileSync20(p, "utf-8"));
3461
2723
  } catch {
3462
2724
  return { requests: [] };
3463
2725
  }
3464
2726
  }
3465
2727
  function checkApproval(dir, file_path, command) {
3466
- const store = loadStore2(dir);
2728
+ const store = loadStore(dir);
3467
2729
  const now = Date.now();
3468
2730
  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;
3469
2731
  }
@@ -3475,8 +2737,8 @@ async function approvalHook(context, toolInput, output) {
3475
2737
  if (!ENABLED2)
3476
2738
  return;
3477
2739
  const dir = context.directory ?? process.cwd();
3478
- const tool15 = toolInput.name ?? toolInput.tool ?? "";
3479
- if (!WRITE_TOOLS.has(tool15))
2740
+ const tool13 = toolInput.name ?? toolInput.tool ?? "";
2741
+ if (!WRITE_TOOLS.has(tool13))
3480
2742
  return;
3481
2743
  const args = output.args ?? {};
3482
2744
  const filePath = String(args.path ?? args.file_path ?? args.filename ?? "");
@@ -3493,8 +2755,8 @@ async function approvalHook(context, toolInput, output) {
3493
2755
  }
3494
2756
 
3495
2757
  // src/services/event-logger.ts
3496
- import { existsSync as existsSync26, mkdirSync as mkdirSync14, appendFileSync as appendFileSync5, readFileSync as readFileSync25, writeFileSync as writeFileSync14, renameSync, unlinkSync, statSync as statSync2 } from "fs";
3497
- import { join as join24, resolve as resolve2, sep } from "path";
2758
+ import { existsSync as existsSync22, mkdirSync as mkdirSync11, appendFileSync as appendFileSync4, readFileSync as readFileSync21, writeFileSync as writeFileSync12, renameSync, unlinkSync, statSync } from "fs";
2759
+ import { join as join21, resolve as resolve2, sep } from "path";
3498
2760
  var SENSITIVE_KEYS = [
3499
2761
  "password",
3500
2762
  "token",
@@ -3548,7 +2810,7 @@ function isValidDirectory(directory) {
3548
2810
  return false;
3549
2811
  }
3550
2812
  try {
3551
- const stats = statSync2(directory);
2813
+ const stats = statSync(directory);
3552
2814
  return stats.isDirectory();
3553
2815
  } catch {
3554
2816
  return false;
@@ -3559,13 +2821,13 @@ function logEvent(directory, event, log) {
3559
2821
  return;
3560
2822
  if (!isValidDirectory(directory))
3561
2823
  return;
3562
- const logDir = join24(directory, ".opencode");
3563
- const logPath = join24(logDir, "flowdeck-events.jsonl");
2824
+ const logDir = join21(directory, ".opencode");
2825
+ const logPath = join21(logDir, "flowdeck-events.jsonl");
3564
2826
  try {
3565
- if (!existsSync26(logDir)) {
3566
- mkdirSync14(logDir, { recursive: true });
2827
+ if (!existsSync22(logDir)) {
2828
+ mkdirSync11(logDir, { recursive: true });
3567
2829
  }
3568
- appendFileSync5(logPath, JSON.stringify(event) + `
2830
+ appendFileSync4(logPath, JSON.stringify(event) + `
3569
2831
  `, "utf-8");
3570
2832
  rotateLogFile(logPath);
3571
2833
  if (log) {
@@ -3575,17 +2837,17 @@ function logEvent(directory, event, log) {
3575
2837
  }
3576
2838
  function rotateLogFile(logPath) {
3577
2839
  try {
3578
- const stats = statSync2(logPath);
2840
+ const stats = statSync(logPath);
3579
2841
  if (stats.size < 5000)
3580
2842
  return;
3581
- const content = readFileSync25(logPath, "utf-8");
2843
+ const content = readFileSync21(logPath, "utf-8");
3582
2844
  const lines = content.split(`
3583
2845
  `).filter((l) => l.trim());
3584
2846
  if (lines.length > 1000) {
3585
2847
  const backupPath = logPath + ".backup";
3586
2848
  renameSync(logPath, backupPath);
3587
2849
  const keep = lines.slice(-1000);
3588
- writeFileSync14(logPath, keep.join(`
2850
+ writeFileSync12(logPath, keep.join(`
3589
2851
  `) + `
3590
2852
  `, "utf-8");
3591
2853
  try {
@@ -3609,8 +2871,6 @@ function formatEventForStderr(event) {
3609
2871
  icon = "\uD83D\uDD0D";
3610
2872
  else if (event.tool === "bash" || event.tool === "shell")
3611
2873
  icon = "\uD83C\uDFC3";
3612
- else if (event.tool === "delegate")
3613
- icon = "\uD83E\uDD16";
3614
2874
  else
3615
2875
  icon = "\uD83D\uDD27";
3616
2876
  const argStr = formatArgs(event.args);
@@ -3638,10 +2898,6 @@ function formatEventForStderr(event) {
3638
2898
  const error = event.error ? ` error: ${event.error}` : "";
3639
2899
  return `${dim}[${time}]${reset} ${icon} ${cyan}${agent}${reset} → ${event.tool}(${argStr})${statusColor}${duration}${error}${reset}`;
3640
2900
  }
3641
- case "agent.delegated": {
3642
- const thinking = event.thinking ? ` "${event.thinking}"` : "";
3643
- return `${dim}[${time}]${reset} \uD83E\uDD16 ${cyan}${agent}${reset} → delegate(${thinking})`;
3644
- }
3645
2901
  case "session.created":
3646
2902
  return `${dim}[${time}]${reset} \uD83D\uDCC2 session created${event.session_id ? ` (${event.session_id})` : ""}`;
3647
2903
  case "session.idle":
@@ -3780,10 +3036,6 @@ function extractAgentFromEvent(props) {
3780
3036
  return props.agent;
3781
3037
  if (typeof props.name === "string")
3782
3038
  return props.name;
3783
- const title = typeof props.title === "string" ? props.title : "";
3784
- const match = title.match(/^(.+)-delegate$/);
3785
- if (match)
3786
- return match[1];
3787
3039
  return "unknown";
3788
3040
  }
3789
3041
 
@@ -3838,15 +3090,15 @@ function createContextWindowMonitorHook() {
3838
3090
  }
3839
3091
 
3840
3092
  // src/hooks/shell-env-hook.ts
3841
- import { existsSync as existsSync27, readFileSync as readFileSync26 } from "fs";
3842
- import { join as join25 } from "path";
3843
- import { createRequire as createRequire2 } from "module";
3093
+ import { existsSync as existsSync23, readFileSync as readFileSync22 } from "fs";
3094
+ import { join as join22 } from "path";
3095
+ import { createRequire } from "module";
3844
3096
  var _version;
3845
3097
  function getVersion() {
3846
3098
  if (_version)
3847
3099
  return _version;
3848
3100
  try {
3849
- const require2 = createRequire2(import.meta.url);
3101
+ const require2 = createRequire(import.meta.url);
3850
3102
  const pkg = require2("../../package.json");
3851
3103
  _version = pkg.version ?? "0.0.0";
3852
3104
  } catch {
@@ -3875,7 +3127,7 @@ var MARKER_TO_LANG = {
3875
3127
  };
3876
3128
  function detectPackageManager(root) {
3877
3129
  for (const [lockfile, pm] of Object.entries(LOCKFILE_TO_PM)) {
3878
- if (existsSync27(join25(root, lockfile)))
3130
+ if (existsSync23(join22(root, lockfile)))
3879
3131
  return pm;
3880
3132
  }
3881
3133
  return;
@@ -3884,7 +3136,7 @@ function detectLanguages(root) {
3884
3136
  const langs = [];
3885
3137
  const seen = new Set;
3886
3138
  for (const [marker, lang] of Object.entries(MARKER_TO_LANG)) {
3887
- if (!seen.has(lang) && existsSync27(join25(root, marker))) {
3139
+ if (!seen.has(lang) && existsSync23(join22(root, marker))) {
3888
3140
  langs.push(lang);
3889
3141
  seen.add(lang);
3890
3142
  }
@@ -3892,11 +3144,11 @@ function detectLanguages(root) {
3892
3144
  return langs;
3893
3145
  }
3894
3146
  function readCurrentPhase(root) {
3895
- const statePath2 = join25(root, ".planning", "STATE.md");
3896
- if (!existsSync27(statePath2))
3147
+ const statePath2 = join22(root, ".planning", "STATE.md");
3148
+ if (!existsSync23(statePath2))
3897
3149
  return;
3898
3150
  try {
3899
- const content = readFileSync26(statePath2, "utf-8");
3151
+ const content = readFileSync22(statePath2, "utf-8");
3900
3152
  const match = content.match(/phase:\s*(\S+)/i);
3901
3153
  return match?.[1];
3902
3154
  } catch {
@@ -4021,8 +3273,8 @@ function createSessionIdleHook(client, tracker) {
4021
3273
  }
4022
3274
 
4023
3275
  // src/hooks/compaction-hook.ts
4024
- import { existsSync as existsSync28, readFileSync as readFileSync27 } from "fs";
4025
- import { join as join26 } from "path";
3276
+ import { existsSync as existsSync24, readFileSync as readFileSync23 } from "fs";
3277
+ import { join as join23 } from "path";
4026
3278
  var STRUCTURED_SUMMARY_PROMPT = `
4027
3279
  When summarizing this session, you MUST include the following sections:
4028
3280
 
@@ -4063,10 +3315,10 @@ For each: agent name, status, description, session_id.
4063
3315
  var _lastInjected = new Map;
4064
3316
  function readPlanningState2(directory) {
4065
3317
  const sp = statePath(directory);
4066
- if (!existsSync28(sp))
3318
+ if (!existsSync24(sp))
4067
3319
  return null;
4068
3320
  try {
4069
- const content = readFileSync27(sp, "utf-8");
3321
+ const content = readFileSync23(sp, "utf-8");
4070
3322
  const parsed = parseState(content);
4071
3323
  const version = typeof parsed.summaryVersion === "number" ? parsed.summaryVersion : 0;
4072
3324
  return { content: content.slice(0, 1500), version };
@@ -4095,15 +3347,15 @@ function createCompactionHook(ctx, tracker) {
4095
3347
  sections.push(`_State unchanged since last compaction. summaryVersion=${currentStateVersion}_`);
4096
3348
  sections.push("");
4097
3349
  }
4098
- const indexPath2 = join26(ctx.directory, ".planning", "CODEBASE_INDEX.md");
4099
- if (indexChanged && existsSync28(indexPath2)) {
3350
+ const indexPath2 = join23(ctx.directory, ".planning", "CODEBASE_INDEX.md");
3351
+ if (indexChanged && existsSync24(indexPath2)) {
4100
3352
  try {
4101
- const indexContent = readFileSync27(indexPath2, "utf-8");
3353
+ const indexContent = readFileSync23(indexPath2, "utf-8");
4102
3354
  const indexSummary = "\n## Codebase Index\n```\n" + indexContent.slice(0, 800) + "\n```\n";
4103
3355
  sections.push(indexSummary);
4104
3356
  sections.push("");
4105
3357
  } catch {}
4106
- } else if (existsSync28(indexPath2)) {
3358
+ } else if (existsSync24(indexPath2)) {
4107
3359
  sections.push(`## Codebase Index (unchanged, v${currentIndexVersion})`);
4108
3360
  sections.push(`_Index unchanged since last compaction. summaryVersion=${currentIndexVersion}_`);
4109
3361
  sections.push("");
@@ -4149,9 +3401,6 @@ var BLOCKED_TOOLS = new Set([
4149
3401
  "shell"
4150
3402
  ]);
4151
3403
  var ALWAYS_ALLOWED = new Set([
4152
- "delegate",
4153
- "run-pipeline",
4154
- "council",
4155
3404
  "planning-state",
4156
3405
  "codebase-state",
4157
3406
  "repo-memory",
@@ -4159,9 +3408,6 @@ var ALWAYS_ALLOWED = new Set([
4159
3408
  "policy-engine",
4160
3409
  "reflect"
4161
3410
  ]);
4162
- function isDelegationTool(name) {
4163
- return ALWAYS_ALLOWED.has(name);
4164
- }
4165
3411
  function isBlocked2(name) {
4166
3412
  const norm = name.toLowerCase().replace(/[-_]/g, "");
4167
3413
  for (const b of BLOCKED_TOOLS) {
@@ -4173,15 +3419,15 @@ function isBlocked2(name) {
4173
3419
  function blockMessage(toolName) {
4174
3420
  return `[Orchestrator Guard] The orchestrator cannot use \`${toolName}\` directly.
4175
3421
 
4176
- ` + `The orchestrator is a coordinator it must delegate all implementation work.
3422
+ ` + `Use built-in read/search tools for lightweight inspection, then route execution with OpenCode's native @agent invocation.
4177
3423
 
4178
- ` + `Use the \`delegate\` tool to hand this off:
4179
- ` + ` delegate({ agent: "@backend-coder", prompt: "..." }) — backend code writing / editing
4180
- ` + ` delegate({ agent: "@frontend-coder", prompt: "..." }) — frontend code writing / editing
4181
- ` + ` delegate({ agent: "@devops", prompt: "..." }) — CI/CD, deploy, and infra changes
4182
- ` + ` delegate({ agent: "@mapper", prompt: "..." }) — codebase mapping
4183
- ` + ` delegate({ agent: "@researcher", prompt: "..." }) — research / file analysis
4184
- ` + ` delegate({ agent: "@tester", prompt: "..." }) — tests / commands
3424
+ ` + `Recommended handoffs:
3425
+ ` + ` @backend-coder — backend code writing and editing
3426
+ ` + ` @frontend-coder — frontend code writing and editing
3427
+ ` + ` @devops — CI/CD, deploy, and infrastructure changes
3428
+ ` + ` @mapper — codebase mapping
3429
+ ` + ` @researcher focused research and file analysis
3430
+ ` + ` @tester — tests, builds, and shell-heavy verification
4185
3431
 
4186
3432
  ` + `To enable this guard: set FLOWDECK_ORCHESTRATOR_GUARD=on`;
4187
3433
  }
@@ -4215,7 +3461,7 @@ class OrchestratorGuard {
4215
3461
  return;
4216
3462
  if (sessionId !== this.primarySessionId)
4217
3463
  return;
4218
- if (isDelegationTool(toolName))
3464
+ if (ALWAYS_ALLOWED.has(toolName))
4219
3465
  return;
4220
3466
  if (isBlocked2(toolName)) {
4221
3467
  throw new Error(blockMessage(toolName));
@@ -4351,74 +3597,57 @@ ${customAppendPrompt}`;
4351
3597
  return base;
4352
3598
  }
4353
3599
  // src/agents/orchestrator.ts
4354
- var ORCHESTRATOR_PROMPT = `You coordinate multi-agent execution. You read STATE.md and PLAN.md at startup, delegate work to specialists, and track progress.
4355
-
4356
- ## HARD RULES — Non-Negotiable
4357
-
4358
- **You are a coordinator. You NEVER do implementation work yourself.**
4359
-
4360
- 1. **Never read source files directly.** You may read STATE.md, PLAN.md, and .codebase/ summary files — nothing else. For all other file reading, delegate to @code-explorer or @researcher.
4361
- 2. **Never write or edit any file.** All file creation, editing, and patching is done by specialist agents. Use \`delegate\` to hand it off.
4362
- 3. **Never run shell commands, tests, or builds.** Delegate to @tester or @build-error-resolver.
4363
- 4. **Every step in PLAN.md is executed by a delegated agent**, never by you directly.
3600
+ var ORCHESTRATOR_PROMPT = `You coordinate multi-agent execution. Read planning state, inspect the codebase with built-in tools when needed, and route specialized work to the right agent using OpenCode's native agent invocation.
4364
3601
 
4365
- If you feel the urge to read a source file, write code, or run a command — stop. Identify the right specialist and delegate instead.
3602
+ ## Operating Model
4366
3603
 
4367
- **Delegation is not optional. It is your only mode of operation.**
3604
+ - Start by reading STATE.md and the active PLAN.md.
3605
+ - Use built-in read/search tools directly for lightweight inspection and progress tracking.
3606
+ - Use native agent routing for implementation, testing, deep research, reviews, and other specialist work.
3607
+ - Do not rely on the removed FlowDeck-specific delegation tools.
4368
3608
 
4369
3609
  ## Startup Behavior
4370
3610
 
4371
- MUST execute at session start:
4372
- 1. Read \`STATE.md\` identify current phase and active plan
4373
- 2. Read the active \`PLAN.md\` identify which steps are complete and which are next
4374
- 3. Check which steps are marked complete
4375
- 4. Begin execution from the first incomplete step
3611
+ At session start:
3612
+ 1. Read STATE.md to identify the current phase and active plan.
3613
+ 2. Read the active PLAN.md to identify complete and incomplete steps.
3614
+ 3. Resume from the first incomplete step.
4376
3615
 
4377
- If STATE.md does not exist, tell the user: "No STATE.md found. Run \`/fd-map-codebase\` then \`/fd-new-feature\` to start a feature."
3616
+ If STATE.md does not exist, tell the user: No STATE.md found. Run /fd-map-codebase then /fd-new-feature to start a feature.
4378
3617
 
4379
3618
  ## Phase Gating
4380
3619
 
4381
- Only orchestrate in the **execute** phase.
3620
+ Read STATE.md to determine the current phase and workflow class.
4382
3621
 
4383
- If the project is in another phase:
4384
- - **discuss** phase: "Run \`/fd-discuss\` to complete requirements gathering first."
4385
- - **plan** phase: "Run \`/fd-plan\` to create the implementation plan first."
4386
- - **review** phase: "Run \`/fd-verify\` to complete the review phase."
3622
+ The orchestrator may run in any phase, but should respect the workflow class:
3623
+ - For \`quick\` workflows: run directly in execute phase, skip discuss/plan.
3624
+ - For \`standard\` workflows: plan execute verify.
3625
+ - For \`explore\` workflows: discuss plan execute verify.
3626
+ - For \`ui-heavy\` workflows: discuss → design → plan → execute → verify.
3627
+ - For \`bugfix\` workflows: discuss → fix-bug → verify.
3628
+ - For \`docs-only\` workflows: write-docs → verify.
3629
+ - For \`verify-heavy\` workflows: plan → execute → verify (with enhanced checks).
3630
+
3631
+ If the project is in a different phase than expected:
3632
+ - Suggest the correct next command but allow override for adaptive workflows.
3633
+ - Log any phase skips with reasons.
4387
3634
 
4388
3635
  ## State-First Read Strategy
4389
3636
 
4390
- Before delegating any agent that needs codebase context:
4391
- 1. Read \`STATE.md\` check \`freshnessStatus\` and \`lastUpdatedAt\`
4392
- 2. Read \`.planning/CODEBASE_INDEX.md\` check \`freshnessStatus\`
4393
- 3. If \`freshnessStatus === "fresh"\` AND needed files exist in \`fileSnapshots\`:
4394
- Use the existing state. Do NOT re-explore the codebase.
4395
- → Log: "[StateManager] Skipped codebase exploration — state is fresh"
4396
- 4. If state is missing, stale, or insufficient:
4397
- → Delegate to @code-explorer with specific question
4398
- → After exploration completes, file-tracker auto-publishes to CODEBASE_INDEX.md
4399
- → Log: "[StateManager] Triggered re-exploration — state was stale"
4400
-
4401
- State becomes **stale** when:
4402
- - \`lastUpdatedAt\` > 5 minutes ago
4403
- - Phase transitions
4404
- - New plan confirmed
4405
- - User runs /fd-checkpoint or /fd-resume
4406
-
4407
- State becomes **fresh** when:
4408
- - Any agent writes to CODEBASE_INDEX.md
4409
- - updatePlanningState() is called
4410
- - file-tracker hook fires after a file edit
3637
+ Before invoking an agent that needs codebase context:
3638
+ 1. Read STATE.md and check freshnessStatus and lastUpdatedAt.
3639
+ 2. Read .planning/CODEBASE_INDEX.md when available.
3640
+ 3. Reuse fresh state when it already answers the question.
3641
+ 4. When state is stale or missing, inspect the relevant files directly or route focused exploration to @code-explorer or @researcher.
4411
3642
 
4412
3643
  ## Step Execution
4413
3644
 
4414
3645
  For each incomplete step in PLAN.md:
4415
-
4416
- 1. Identify the step's requirements and agent type
4417
- 2. Delegate to the appropriate agent with full context
4418
- 3. Wait for the agent to complete
4419
- 4. Mark the step complete in STATE.md
4420
- 5. Re-read STATE.md to confirm state
4421
- 6. Move to the next incomplete step
3646
+ 1. Identify the step requirements and the best agent for the work.
3647
+ 2. Gather only the context needed to brief that agent.
3648
+ 3. Invoke the specialist directly with native agent routing.
3649
+ 4. Wait for completion, then update and re-read STATE.md.
3650
+ 5. Move to the next incomplete step.
4422
3651
 
4423
3652
  ## Implementation Routing
4424
3653
 
@@ -4426,84 +3655,78 @@ When a plan step requires implementation, route to a role-specific agent:
4426
3655
  - Use @backend-coder for server, API, business logic, database, and non-UI application code.
4427
3656
  - Use @frontend-coder for UI components, client state, styling, and interaction behavior.
4428
3657
  - Use @devops for CI/CD workflows, deployment, infrastructure, runtime config, and operations scripts.
4429
- - If a step mixes multiple domains, split it into multiple delegated tasks by domain.
3658
+ - Split mixed-domain steps into smaller specialist handoffs when that reduces risk.
4430
3659
 
4431
3660
  ## Agent Team
4432
3661
 
4433
- | Agent | Invoke | Best For |
4434
- |-------|--------|----------|
4435
- | Design | @design | Discovery, UX planning, wireframes, visual system, implementation handoff, design fidelity review |
4436
- | Backend Coder | @backend-coder | Backend code implementation |
4437
- | Frontend Coder | @frontend-coder | Frontend code implementation |
4438
- | DevOps | @devops | CI/CD and infrastructure implementation |
4439
- | Researcher | @researcher | API docs, library usage |
4440
- | Tester | @tester | Writing and running tests |
4441
- | Reviewer | @reviewer | Code quality review |
4442
- | Writer | @writer | Documentation |
4443
- | Mapper | @mapper | Codebase mapping to .codebase/ |
4444
- | Architect | @architect | System design, ADRs |
4445
- | Security Auditor | @security-auditor | Security review |
4446
- | Code Explorer | @code-explorer | Reading unfamiliar code |
4447
- | Debug Specialist | @debug-specialist | Root cause analysis |
4448
- | Build Resolver | @build-error-resolver | Build/compile failures |
4449
- | Doc Updater | @doc-updater | Updating existing docs |
4450
- | Task Splitter | @task-splitter | Decomposing complex tasks |
4451
- | Discusser | @discusser | Requirements extraction |
4452
- | Plan Checker | @plan-checker | Plan quality review |
4453
- | Planner | @planner | Feature planning |
4454
- | Build Error Resolver | @build-error-resolver | Build error diagnosis |
4455
- | Performance Optimizer | @performance-optimizer | Performance analysis |
4456
- | Refactor Guide | @refactor-guide | Safe refactoring |
4457
-
4458
- ## Phase State Machine
4459
-
4460
- \`\`\`
4461
- discuss plandesign (for UI-heavy tasks) execute review
4462
- \`\`\`
4463
-
4464
- - **discuss**: Requirements extraction with @discusser
4465
- - **plan**: Plan creation with @planner, review with @plan-checker
4466
- - **design**: UX structure, wireframe/layout planning, and visual system definition with @design
4467
- - **execute**: Implementation with @backend-coder, @frontend-coder, @devops, @tester, and @researcher in parallel where possible, only after approved design handoff for UI-heavy tasks
4468
- - **review**: Review with @reviewer, @security-auditor
3662
+ - @design: discovery, UX planning, wireframes, visual system, implementation handoff, design fidelity review
3663
+ - @backend-coder: backend code implementation
3664
+ - @frontend-coder: frontend code implementation
3665
+ - @devops: CI/CD and infrastructure implementation
3666
+ - @researcher: API docs and library usage
3667
+ - @tester: writing and running tests
3668
+ - @reviewer: code quality review
3669
+ - @writer: documentation
3670
+ - @mapper: codebase mapping to .codebase/
3671
+ - @architect: system design and ADRs
3672
+ - @security-auditor: security review
3673
+ - @code-explorer: reading unfamiliar code
3674
+ - @debug-specialist: root cause analysis
3675
+ - @build-error-resolver: build and compile failures
3676
+ - @doc-updater: updating existing docs
3677
+ - @task-splitter: decomposing complex tasks
3678
+ - @discusser: requirements extraction
3679
+ - @plan-checker: plan quality review
3680
+ - @planner: feature planning
3681
+ - @performance-optimizer: performance analysis
3682
+ - @refactor-guide: safe refactoring
3683
+
3684
+ ## Adaptive Workflow Routing
3685
+
3686
+ The orchestrator reads the workflow class from STATE.md and adapts its behavior:
3687
+
3688
+ | Workflow Class | Stages | When Used |
3689
+ |----------------|--------|-----------|
3690
+ | \`quick\` | execute verify | Simple, low-risk tasks (< 5 files, no ambiguity) |
3691
+ | \`standard\` | plan → execute → verify | Normal implementation tasks |
3692
+ | \`explore\` | discuss → plan → execute → verify | Ambiguous or unfamiliar tasks |
3693
+ | \`ui-heavy\` | discuss design plan → execute → verify | UI/UX-heavy tasks |
3694
+ | \`bugfix\` | discuss fix-bug verify | Bug fixes |
3695
+ | \`docs-only\` | write-docs verify | Documentation-only changes |
3696
+ | \`verify-heavy\` | plan execute verify | High blast radius or sensitive paths |
3697
+
3698
+ - discuss: requirements extraction with @discusser (only for explore/bugfix/ui-heavy)
3699
+ - plan: plan creation with @planner, review with @plan-checker (skip for quick/docs-only)
3700
+ - design: UX structure with @design (only for ui-heavy)
3701
+ - execute: implementation with appropriate specialists
3702
+ - verify: review with @reviewer and @security-auditor (always run for edited code)
3703
+
3704
+ The workflow class is chosen by scoring task complexity, confidence, risk, and codebase familiarity. Prefer the lightest workflow that is sufficient. Escalate to a richer workflow only when evidence shows the current path is insufficient.
4469
3705
 
4470
3706
  ## Tracking
4471
3707
 
4472
3708
  After each step completes:
4473
- - Call \`mark_step_complete\` with the step ID
3709
+ - Call mark_step_complete with the step ID
4474
3710
  - Re-read STATE.md to confirm the update
4475
- - Update STATE.md \`current_step\` to the next step
3711
+ - Update STATE.md current_step to the next step
4476
3712
 
4477
3713
  On all steps complete:
4478
- - Update STATE.md \`phase\` to \`review\`
3714
+ - Update STATE.md phase to review
4479
3715
  - Summarize what was delivered
4480
3716
 
4481
3717
  ## Error Recovery
4482
3718
 
4483
- If a delegated agent fails:
4484
- 1. Log the failure with the error message
4485
- 2. Retry once with clarified instructions
4486
- 3. If still failing, escalate:
4487
-
4488
- \`\`\`
4489
- BLOCKED: implementation agent failed on step 3 (add payment endpoint).
4490
- Error: [exact error message]
4491
- Retried once with clarification. Still failing.
4492
-
4493
- Options:
4494
- 1. Skip this step and continue
4495
- 2. Replan step 3 with smaller scope
4496
- 3. Stop and debug manually
4497
-
4498
- Please advise.
4499
- \`\`\`
3719
+ If a specialist fails:
3720
+ 1. Log the failure with the exact error message.
3721
+ 2. Retry once with clearer context if the issue is recoverable.
3722
+ 3. If it still fails, surface a blocked summary with next options.
4500
3723
 
4501
3724
  ## Self-Learning
4502
3725
 
4503
3726
  When a task required unusual human guidance, a novel solution strategy, or exposed a knowledge gap:
4504
- 1. After the task completes successfully, write a new skill markdown file under \`src/skills/<name>/SKILL.md\` to capture the pattern
4505
- 2. Use a descriptive kebab-case name for the directory, a one-sentence description in the frontmatter, and structured Markdown content
4506
- 3. Include: When to Activate, Steps, Examples, and Pitfalls sections
3727
+ 1. After the task completes successfully, write a new skill markdown file under src/skills/<name>/SKILL.md to capture the pattern.
3728
+ 2. Use a descriptive kebab-case name for the directory, a one-sentence description in the frontmatter, and structured Markdown content.
3729
+ 3. Include: When to Activate, Steps, Examples, and Pitfalls sections.
4507
3730
 
4508
3731
  Do NOT create a skill for routine tasks. Only capture genuinely novel or reusable patterns.`;
4509
3732
  var AGENT_DESCRIPTIONS = {
@@ -4511,114 +3734,118 @@ var AGENT_DESCRIPTIONS = {
4511
3734
  - Role: Runs design-first workflow for user-facing tasks
4512
3735
  - Permissions: Read/write files
4513
3736
  - Best for: UX structure, wireframes, visual direction, tokens, and frontend handoff
4514
- - **Delegate when:** Task includes website/app/dashboard/admin/user-facing UI work`,
3737
+ - Use when: Task includes website/app/dashboard/admin/user-facing UI work`,
4515
3738
  "backend-coder": `@backend-coder
4516
3739
  - Role: Implements backend features and fixes based on confirmed plans
4517
3740
  - Permissions: Read/write files
4518
3741
  - Best for: API, services, data layer, and business logic
4519
- - **Delegate when:** Backend or server-side implementation work`,
3742
+ - Use when: Backend or server-side implementation work`,
4520
3743
  "frontend-coder": `@frontend-coder
4521
3744
  - Role: Implements frontend features and fixes based on confirmed plans
4522
3745
  - Permissions: Read/write files
4523
3746
  - Best for: UI components, client state, rendering, and interaction behavior
4524
- - **Delegate when:** Frontend implementation work`,
3747
+ - Use when: Frontend implementation work`,
4525
3748
  devops: `@devops
4526
3749
  - Role: Implements DevOps and infrastructure changes based on confirmed plans
4527
3750
  - Permissions: Read/write files
4528
3751
  - Best for: CI/CD, deployment config, infra scripts, and runtime operations
4529
- - **Delegate when:** Infrastructure, pipeline, or operations implementation work`,
3752
+ - Use when: Infrastructure, pipeline, or operations implementation work`,
4530
3753
  researcher: `@researcher
4531
3754
  - Role: Researches documentation, APIs, and best practices
4532
3755
  - Permissions: Read files
4533
3756
  - Stats: 10x better finding up-to-date library docs
4534
- - **Delegate when:** Need API docs, library usage, best practices
4535
- - **Don't delegate when:** Standard usage you're confident about`,
3757
+ - Use when: Need API docs, library usage, or best practices
3758
+ - Skip when: Standard usage you're already confident about`,
4536
3759
  tester: `@tester
4537
3760
  - Role: Writes and runs tests following TDD principles
4538
3761
  - Permissions: Read/write files
4539
3762
  - Best for: Writing tests before code (TDD), running test suites
4540
- - **Delegate when:** Implementing new features, fixing bugs, test coverage needed`,
3763
+ - Use when: Implementing new features, fixing bugs, or increasing coverage`,
4541
3764
  reviewer: `@reviewer
4542
3765
  - Role: Reviews code for quality, security, and adherence to conventions
4543
3766
  - Permissions: Read files
4544
3767
  - Best for: Code review before PRs
4545
- - **Delegate when:** After writing or modifying code, before opening PRs`,
3768
+ - Use when: After writing or modifying code, before opening PRs`,
4546
3769
  architect: `@architect
4547
3770
  - Role: Designs system architecture, creates ADRs, defines API contracts
4548
3771
  - Permissions: Read files
4549
3772
  - Best for: New modules, API changes, database schema changes, cross-cutting concerns
4550
- - **Delegate when:** Planning new features that need architectural decisions`,
3773
+ - Use when: Planning new features that need architectural decisions`,
4551
3774
  "security-auditor": `@security-auditor
4552
3775
  - Role: Deep security audit of code changes
4553
3776
  - Permissions: Read files
4554
3777
  - Best for: OWASP Top 10, injection vulnerabilities, auth issues
4555
- - **Delegate when:** Before merging security-sensitive code`,
3778
+ - Use when: Before merging security-sensitive code`,
4556
3779
  "code-explorer": `@code-explorer
4557
3780
  - Role: Explores and maps unfamiliar codebases
4558
3781
  - Permissions: Read files
4559
3782
  - Best for: Tracing call paths, building structural models
4560
- - **Delegate when:** Before making changes to unfamiliar code`,
3783
+ - Use when: Before making changes to unfamiliar code`,
4561
3784
  "debug-specialist": `@debug-specialist
4562
3785
  - Role: Diagnoses bugs through systematic root cause analysis
4563
3786
  - Permissions: Read files
4564
3787
  - Best for: Deep investigation before fixing
4565
- - **Delegate when:** Bug needs investigation, not just a quick fix`,
3788
+ - Use when: A bug needs investigation, not just a quick fix`,
4566
3789
  "build-error-resolver": `@build-error-resolver
4567
3790
  - Role: Fixes build errors, compilation failures, dependency issues
4568
3791
  - Permissions: Read/write files
4569
3792
  - Best for: Build failures, type errors, broken dependencies
4570
- - **Delegate when:** Build fails, types error out, dependencies broken`,
3793
+ - Use when: Build fails, types error out, or dependencies break`,
4571
3794
  "doc-updater": `@doc-updater
4572
3795
  - Role: Updates documentation after code changes
4573
3796
  - Permissions: Read/write files
4574
3797
  - Best for: API references, README, inline comments
4575
- - **Delegate when:** Implementation completes and docs need updating`,
3798
+ - Use when: Implementation completes and docs need syncing`,
4576
3799
  writer: `@writer
4577
3800
  - Role: Drafts project documentation
4578
3801
  - Permissions: Read/write files
4579
3802
  - Best for: README, API docs, user guides
4580
- - **Delegate when:** Creating new documentation from scratch`,
3803
+ - Use when: Creating new documentation from scratch`,
4581
3804
  mapper: `@mapper
4582
3805
  - Role: Maps codebase to structured documentation files
4583
3806
  - Permissions: Read/write files
4584
3807
  - Best for: .codebase/ directory documentation
4585
- - **Delegate when:** Need to document existing codebase structure`,
3808
+ - Use when: Need to document existing codebase structure`,
4586
3809
  "plan-checker": `@plan-checker
4587
3810
  - Role: Reviews PLAN.md for quality before execution
4588
3811
  - Permissions: Read files
4589
3812
  - Best for: Plan verification before execution
4590
- - **Delegate when:** PLAN.md needs review before execution`,
3813
+ - Use when: PLAN.md needs review before execution`,
4591
3814
  "task-splitter": `@task-splitter
4592
3815
  - Role: Decomposes complex tasks into parallel workstreams
4593
3816
  - Permissions: Read files
4594
3817
  - Best for: Multi-track work organization
4595
- - **Delegate when:** Complex task needs parallelization`,
3818
+ - Use when: Complex work needs parallelization`,
4596
3819
  discusser: `@discusser
4597
3820
  - Role: Extracts requirements via structured Q&A
4598
3821
  - Permissions: Read/write files
4599
3822
  - Best for: Requirements extraction
4600
- - **Delegate when:** Starting new feature or project phase`,
3823
+ - Use when: Starting a new feature or project phase`,
4601
3824
  planner: `@planner
4602
3825
  - Role: Creates detailed implementation plans
4603
3826
  - Permissions: Read files
4604
3827
  - Best for: Feature planning, step breakdown
4605
- - **Delegate when:** Need implementation plan for feature`,
3828
+ - Use when: Need an implementation plan for a feature`,
4606
3829
  "performance-optimizer": `@performance-optimizer
4607
3830
  - Role: Analyzes and optimizes performance
4608
3831
  - Permissions: Read files
4609
3832
  - Best for: Performance analysis
4610
- - **Delegate when:** Need to optimize slow code`,
3833
+ - Use when: Need to optimize slow code`,
4611
3834
  "refactor-guide": `@refactor-guide
4612
3835
  - Role: Guides safe refactoring
4613
3836
  - Permissions: Read files
4614
3837
  - Best for: Code restructuring
4615
- - **Delegate when:** Need to refactor existing code safely`
3838
+ - Use when: Need to refactor existing code safely`
4616
3839
  };
4617
- function buildOrchestratorPrompt(disabledAgents) {
3840
+ function buildOrchestratorPrompt(disabledAgents, workflowClass) {
4618
3841
  const enabledAgents = Object.entries(AGENT_DESCRIPTIONS).filter(([name]) => !disabledAgents?.has(name)).map(([, desc]) => desc).join(`
4619
3842
 
4620
3843
  `);
4621
- return `${ORCHESTRATOR_PROMPT}
3844
+ const workflowSection = workflowClass ? `
3845
+ ## Current Workflow
3846
+
3847
+ Active workflow class: ${workflowClass}` : "";
3848
+ return `${ORCHESTRATOR_PROMPT}${workflowSection}
4622
3849
 
4623
3850
  <Delegation>
4624
3851
 
@@ -4626,21 +3853,22 @@ function buildOrchestratorPrompt(disabledAgents) {
4626
3853
 
4627
3854
  ${enabledAgents}
4628
3855
 
4629
- ## Delegation Guidelines
3856
+ ## Routing Guidelines
4630
3857
 
4631
3858
  - Review available agents before acting
4632
- - Reference paths/lines, don't paste files (\`src/app.ts:42\`)
4633
- - Provide context summaries, let specialists read what they need
4634
- - Skip delegation if overhead doing it yourself
3859
+ - Reference paths and line numbers instead of pasting full files
3860
+ - Provide context summaries, then let specialists inspect what they need
3861
+ - Use direct built-in tools yourself for lightweight reading and status tracking
3862
+ - Use native agent routing when specialist work or deeper execution is the better fit
4635
3863
 
4636
3864
  </Delegation>`;
4637
3865
  }
4638
- function createOrchestratorAgent(model, customPrompt, customAppendPrompt, disabledAgents) {
4639
- const basePrompt = buildOrchestratorPrompt(disabledAgents);
3866
+ function createOrchestratorAgent(model, customPrompt, customAppendPrompt, disabledAgents, workflowClass) {
3867
+ const basePrompt = buildOrchestratorPrompt(disabledAgents, workflowClass);
4640
3868
  const prompt = resolvePrompt(basePrompt, customPrompt, customAppendPrompt);
4641
3869
  const definition = {
4642
3870
  name: "orchestrator",
4643
- description: "AI coding orchestrator that delegates tasks to specialist agents for optimal quality, speed, and cost",
3871
+ description: "AI coding orchestrator that coordinates specialist agents and built-in tools for execution",
4644
3872
  config: {
4645
3873
  temperature: 0.1,
4646
3874
  prompt
@@ -7456,612 +6684,11 @@ function getAgentConfigs(agentModels) {
7456
6684
  return configs;
7457
6685
  }
7458
6686
 
7459
- // src/services/agent-contract-registry.ts
7460
- var CONTRACTS = [
7461
- {
7462
- agent: "orchestrator",
7463
- role: "Coordinate multi-agent execution. Delegates all work — never implements directly.",
7464
- allowedTaskTypes: ["orchestration", "coordination", "delegation", "phase-management"],
7465
- requiredInputs: ["STATE.md", "PLAN.md"],
7466
- expectedOutputFields: ["delegated_steps", "completed_steps", "current_phase"],
7467
- allowedTools: [
7468
- "delegate",
7469
- "run-pipeline",
7470
- "council",
7471
- "planning-state",
7472
- "codebase-state",
7473
- "repo-memory",
7474
- "decision-trace",
7475
- "policy-engine",
7476
- "reflect"
7477
- ],
7478
- forbiddenActions: [
7479
- "write_file",
7480
- "edit_file",
7481
- "create_file",
7482
- "bash",
7483
- "patch",
7484
- "apply_patch",
7485
- "read source files directly"
7486
- ],
7487
- escalationConditions: [
7488
- "delegated agent fails twice",
7489
- "delegation budget exhausted",
7490
- "deadlock detected",
7491
- "all agents blocked on the same step"
7492
- ],
7493
- stopConditions: [
7494
- "all PLAN.md steps completed",
7495
- "user requests stop",
7496
- "budget exceeded with no fallback"
7497
- ],
7498
- successCriteria: [
7499
- "all plan steps delegated and completed",
7500
- "STATE.md phase updated to review",
7501
- "no implementation performed directly by orchestrator"
7502
- ]
7503
- },
7504
- {
7505
- agent: "planner",
7506
- role: "Create detailed implementation plans. Output PLAN.md with numbered steps.",
7507
- allowedTaskTypes: ["planning", "task-breakdown", "step-decomposition"],
7508
- requiredInputs: ["task description or STATE.md"],
7509
- expectedOutputFields: ["steps", "phase"],
7510
- allowedTools: ["read", "glob", "grep", "planning-state"],
7511
- forbiddenActions: [
7512
- "write source files",
7513
- "run bash commands",
7514
- "edit application code",
7515
- "implement features"
7516
- ],
7517
- escalationConditions: [
7518
- "requirements are ambiguous",
7519
- "dependencies between steps unclear",
7520
- "conflicting constraints"
7521
- ],
7522
- stopConditions: ["PLAN.md written and reviewed by plan-checker", "user confirms plan"],
7523
- successCriteria: [
7524
- "PLAN.md contains numbered steps with assigned agents",
7525
- "each step has clear success criteria",
7526
- "no implementation performed"
7527
- ]
7528
- },
7529
- {
7530
- agent: "plan-checker",
7531
- role: "Review PLAN.md quality before execution. Read-only.",
7532
- allowedTaskTypes: ["plan-review", "quality-check"],
7533
- requiredInputs: ["PLAN.md"],
7534
- expectedOutputFields: ["verdict", "issues", "recommendations"],
7535
- allowedTools: ["read", "glob", "grep"],
7536
- forbiddenActions: ["write or edit any files", "modify PLAN.md"],
7537
- escalationConditions: ["plan is fundamentally flawed", "critical gaps found"],
7538
- stopConditions: ["review complete", "verdict issued"],
7539
- successCriteria: ["structured review output", "no file modifications"]
7540
- },
7541
- {
7542
- agent: "design",
7543
- role: "Design UX, wireframes, and visual systems for UI-heavy tasks.",
7544
- allowedTaskTypes: ["ux-design", "wireframe", "visual-system", "design-handoff", "frontend-handoff"],
7545
- requiredInputs: ["task description", "requirements"],
7546
- expectedOutputFields: ["design_stage", "wireframes", "component_structure", "design_tokens"],
7547
- allowedTools: ["read", "write", "glob", "grep", "planning-state"],
7548
- forbiddenActions: [
7549
- "run bash commands",
7550
- "write application logic",
7551
- "implement backend code",
7552
- "implement React components"
7553
- ],
7554
- escalationConditions: [
7555
- "design requirements unclear",
7556
- "conflicting UX requirements",
7557
- "brand guidelines missing"
7558
- ],
7559
- stopConditions: ["design_stage=handoff_complete", "design_approved=true"],
7560
- successCriteria: [
7561
- "design document written",
7562
- "design_stage set to handoff_complete",
7563
- "design_approved set to true",
7564
- "no application code written"
7565
- ]
7566
- },
7567
- {
7568
- agent: "backend-coder",
7569
- role: "Implement backend features: API, services, data layer, business logic.",
7570
- allowedTaskTypes: ["implementation", "backend", "api", "database", "service", "bugfix"],
7571
- requiredInputs: ["PLAN.md step description", "relevant context files"],
7572
- expectedOutputFields: ["files_modified", "summary"],
7573
- allowedTools: ["read", "write", "edit", "bash", "glob", "grep"],
7574
- forbiddenActions: [
7575
- "modify frontend UI component files",
7576
- "change CI/CD config without devops involvement"
7577
- ],
7578
- escalationConditions: [
7579
- "architecture decision needed",
7580
- "security-sensitive change without audit",
7581
- "database migration required"
7582
- ],
7583
- stopConditions: ["step implementation complete", "tests pass", "reviewer approves"],
7584
- successCriteria: [
7585
- "code written per plan step",
7586
- "no regressions introduced",
7587
- "tests exist or updated"
7588
- ]
7589
- },
7590
- {
7591
- agent: "frontend-coder",
7592
- role: "Implement frontend features: UI components, client state, rendering.",
7593
- allowedTaskTypes: ["implementation", "frontend", "ui", "component", "styling", "bugfix"],
7594
- requiredInputs: ["PLAN.md step description", "design handoff for UI-heavy tasks"],
7595
- expectedOutputFields: ["files_modified", "summary"],
7596
- allowedTools: ["read", "write", "edit", "bash", "glob", "grep"],
7597
- forbiddenActions: [
7598
- "modify backend API files",
7599
- "change server configuration",
7600
- "implement without approved design for UI-heavy tasks"
7601
- ],
7602
- escalationConditions: [
7603
- "design handoff missing for UI-heavy task",
7604
- "component library or design system unclear"
7605
- ],
7606
- stopConditions: ["step implementation complete", "tests pass", "reviewer approves"],
7607
- successCriteria: [
7608
- "components implemented per approved design",
7609
- "no regressions introduced",
7610
- "tests exist or updated"
7611
- ]
7612
- },
7613
- {
7614
- agent: "devops",
7615
- role: "Implement DevOps and infrastructure changes: CI/CD, deployment, infra scripts.",
7616
- allowedTaskTypes: ["implementation", "ci-cd", "deployment", "infrastructure", "operations"],
7617
- requiredInputs: ["PLAN.md step description"],
7618
- expectedOutputFields: ["files_modified", "summary"],
7619
- allowedTools: ["read", "write", "edit", "bash", "glob", "grep"],
7620
- forbiddenActions: [
7621
- "modify application source code",
7622
- "deploy to production without approval"
7623
- ],
7624
- escalationConditions: [
7625
- "production deployment requires approval",
7626
- "destructive infra change"
7627
- ],
7628
- stopConditions: ["pipeline or infra change complete", "reviewer approves"],
7629
- successCriteria: ["infrastructure code written per plan", "no prod deployment without approval"]
7630
- },
7631
- {
7632
- agent: "tester",
7633
- role: "Write and run tests following TDD principles. Tests before implementation.",
7634
- allowedTaskTypes: ["testing", "tdd", "regression", "integration-test", "unit-test"],
7635
- requiredInputs: ["feature or step description", "relevant source files"],
7636
- expectedOutputFields: ["test_files_written", "tests_passing", "coverage_summary"],
7637
- allowedTools: ["read", "write", "edit", "bash", "glob", "grep"],
7638
- forbiddenActions: [
7639
- "delete failing tests to make suite pass",
7640
- "implement application features",
7641
- "skip TDD cycle (red → green → refactor)"
7642
- ],
7643
- escalationConditions: [
7644
- "test infrastructure broken",
7645
- "flaky tests blocking all progress"
7646
- ],
7647
- stopConditions: ["all tests pass", "coverage meets threshold"],
7648
- successCriteria: [
7649
- "tests written before implementation",
7650
- "all new tests pass",
7651
- "no test deletions to fix failures"
7652
- ]
7653
- },
7654
- {
7655
- agent: "reviewer",
7656
- role: "Review code quality, security, and convention adherence. Read-only.",
7657
- allowedTaskTypes: ["review", "code-review", "quality-check"],
7658
- requiredInputs: ["files to review", "context of changes"],
7659
- expectedOutputFields: ["verdict", "issues", "recommendations"],
7660
- allowedTools: ["read", "glob", "grep"],
7661
- forbiddenActions: [
7662
- "write or edit any files",
7663
- "make code changes",
7664
- "approve security-sensitive changes without security audit"
7665
- ],
7666
- escalationConditions: [
7667
- "security issues found",
7668
- "critical bugs found",
7669
- "architectural violations"
7670
- ],
7671
- stopConditions: ["review complete", "verdict issued"],
7672
- successCriteria: [
7673
- "structured review output with severity levels",
7674
- "issues categorized",
7675
- "no file modifications"
7676
- ]
7677
- },
7678
- {
7679
- agent: "security-auditor",
7680
- role: "Security audit: OWASP Top 10, injection, auth vulnerabilities. Read-only.",
7681
- allowedTaskTypes: ["security-audit", "vulnerability-scan", "auth-review"],
7682
- requiredInputs: ["files to audit", "change context"],
7683
- expectedOutputFields: ["findings", "severity_breakdown", "recommendations"],
7684
- allowedTools: ["read", "glob", "grep"],
7685
- forbiddenActions: [
7686
- "write or edit files",
7687
- "make changes to fix vulnerabilities directly"
7688
- ],
7689
- escalationConditions: [
7690
- "CRITICAL vulnerability found",
7691
- "auth bypass detected",
7692
- "data exposure found"
7693
- ],
7694
- stopConditions: ["audit complete", "all findings documented"],
7695
- successCriteria: [
7696
- "OWASP checklist evaluated",
7697
- "findings documented with severity levels",
7698
- "no file modifications"
7699
- ]
7700
- },
7701
- {
7702
- agent: "researcher",
7703
- role: "Research documentation, APIs, best practices. Read-only analysis.",
7704
- allowedTaskTypes: ["research", "api-lookup", "documentation", "best-practices"],
7705
- requiredInputs: ["research topic or question"],
7706
- expectedOutputFields: ["findings", "references", "recommendations"],
7707
- allowedTools: ["read", "glob", "grep", "web-search"],
7708
- forbiddenActions: ["write or edit files", "implement solutions"],
7709
- escalationConditions: [
7710
- "critical information unavailable",
7711
- "conflicting official documentation"
7712
- ],
7713
- stopConditions: ["research question answered", "findings documented"],
7714
- successCriteria: [
7715
- "findings clearly summarized",
7716
- "sources cited",
7717
- "no file modifications"
7718
- ]
7719
- },
7720
- {
7721
- agent: "architect",
7722
- role: "Design system architecture, create ADRs, define API contracts.",
7723
- allowedTaskTypes: ["architecture", "adr", "api-design", "system-design"],
7724
- requiredInputs: ["feature or system description", "existing codebase context"],
7725
- expectedOutputFields: ["architecture_document", "adr", "api_contracts"],
7726
- allowedTools: ["read", "write", "glob", "grep", "planning-state"],
7727
- forbiddenActions: ["write application code", "run bash commands"],
7728
- escalationConditions: [
7729
- "major architectural conflict with existing system",
7730
- "breaking API change required"
7731
- ],
7732
- stopConditions: ["ADR written", "architecture reviewed"],
7733
- successCriteria: [
7734
- "architecture documented with tradeoffs",
7735
- "no application code written"
7736
- ]
7737
- },
7738
- {
7739
- agent: "writer",
7740
- role: "Draft project documentation: README, API docs, user guides.",
7741
- allowedTaskTypes: ["documentation", "readme", "api-docs", "user-guide"],
7742
- requiredInputs: ["feature description or codebase context"],
7743
- expectedOutputFields: ["documentation_files"],
7744
- allowedTools: ["read", "write", "edit", "glob", "grep"],
7745
- forbiddenActions: ["modify application code", "run bash commands"],
7746
- escalationConditions: ["documentation scope unclear"],
7747
- stopConditions: ["docs written", "user confirms completeness"],
7748
- successCriteria: [
7749
- "documentation written and accurate",
7750
- "no application code changed"
7751
- ]
7752
- },
7753
- {
7754
- agent: "doc-updater",
7755
- role: "Update existing documentation after code changes.",
7756
- allowedTaskTypes: ["documentation-update", "doc-sync"],
7757
- requiredInputs: ["changed files", "change summary"],
7758
- expectedOutputFields: ["updated_docs"],
7759
- allowedTools: ["read", "write", "edit", "glob", "grep"],
7760
- forbiddenActions: [
7761
- "modify application code",
7762
- "delete documentation without replacement"
7763
- ],
7764
- escalationConditions: ["documentation conflicts with implementation"],
7765
- stopConditions: ["docs updated and synced"],
7766
- successCriteria: ["docs reflect current code", "no application code changed"]
7767
- },
7768
- {
7769
- agent: "supervisor",
7770
- role: "Governance review layer. Inspects existing commands/agents, validates policy, returns structured approve/revise/block/escalate decision. Never creates new commands or workflows.",
7771
- allowedTaskTypes: ["governance-review", "policy-check", "pre-execution-review", "post-stage-review"],
7772
- requiredInputs: ["target name (command or agent)", "task context"],
7773
- expectedOutputFields: ["decision", "targetType", "targetName", "exists", "reasons", "missingRequirements", "riskFlags", "requiredChanges", "approvalStatus", "confidenceScore"],
7774
- allowedTools: ["read", "glob", "grep", "planning-state", "policy-engine"],
7775
- forbiddenActions: [
7776
- "create new commands",
7777
- "create new workflows",
7778
- "invent new agent names",
7779
- "modify command intent",
7780
- "replace orchestrator",
7781
- "become second dispatcher",
7782
- "execute implementation tasks",
7783
- "write or edit source files",
7784
- "run bash commands",
7785
- "modify PLAN.md or STATE.md"
7786
- ],
7787
- escalationConditions: [
7788
- "human approval required and not granted",
7789
- "confidence below threshold",
7790
- "critical policy violation with no safe path forward"
7791
- ],
7792
- stopConditions: ["structured decision issued", "review complete"],
7793
- successCriteria: [
7794
- "structured SupervisorDecision returned",
7795
- "no new commands or workflows created",
7796
- "existing registry not modified",
7797
- "decision is one of: approve, revise, block, escalate"
7798
- ]
7799
- }
7800
- ];
7801
- var REGISTRY = new Map(CONTRACTS.map((c) => [c.agent, c]));
7802
- function getContract(agent) {
7803
- return REGISTRY.get(agent) ?? null;
7804
- }
7805
-
7806
- // src/services/supervisor-binding.ts
7807
- var REGISTERED_COMMANDS = [
7808
- "fd-ask",
7809
- "fd-checkpoint",
7810
- "fd-deploy-check",
7811
- "fd-design",
7812
- "fd-discuss",
7813
- "fd-doctor",
7814
- "fd-execute",
7815
- "fd-fix-bug",
7816
- "fd-map-codebase",
7817
- "fd-multi-repo",
7818
- "fd-new-feature",
7819
- "fd-plan",
7820
- "fd-quick",
7821
- "fd-reflect",
7822
- "fd-resume",
7823
- "fd-status",
7824
- "fd-suggest",
7825
- "fd-translate-intent",
7826
- "fd-verify",
7827
- "fd-write-docs",
7828
- "fd-done"
7829
- ];
7830
- function resolveSupervisorConfig(directory) {
7831
- try {
7832
- const config = loadFlowDeckConfig(directory);
7833
- const sup = config?.governance?.supervisor ?? {};
7834
- return {
7835
- enabled: sup.enabled ?? false,
7836
- mode: sup.mode ?? "advisory",
7837
- reviewedTargets: sup.reviewedTargets ?? [],
7838
- canBlock: sup.canBlock ?? true,
7839
- confidenceThreshold: sup.confidenceThreshold ?? 0.7,
7840
- postExecutionReview: sup.postExecutionReview ?? false
7841
- };
7842
- } catch {
7843
- return {
7844
- enabled: false,
7845
- mode: "advisory",
7846
- reviewedTargets: [],
7847
- canBlock: true,
7848
- confidenceThreshold: 0.7,
7849
- postExecutionReview: false
7850
- };
7851
- }
7852
- }
7853
- function isRegisteredCommand(name) {
7854
- return REGISTERED_COMMANDS.includes(name);
7855
- }
7856
- function isRegisteredAgent(name) {
7857
- return AGENT_NAMES.includes(name);
7858
- }
7859
- function isRegisteredTarget(name) {
7860
- if (isRegisteredCommand(name))
7861
- return { exists: true, type: "command" };
7862
- if (isRegisteredAgent(name))
7863
- return { exists: true, type: "agent" };
7864
- return { exists: false, type: "agent" };
7865
- }
7866
- function checkCommandPolicy(commandName, ctx) {
7867
- const reasons = [];
7868
- const riskFlags = [];
7869
- const missingRequirements = [];
7870
- const requiredChanges = [];
7871
- if (commandName === "fd-new-feature" || commandName === "fd-execute") {
7872
- const taskLower = (ctx.taskDescription ?? "").toLowerCase();
7873
- const isUiHeavy = /landing page|dashboard|admin panel|website|web app|ui|ux|interface|frontend|component/.test(taskLower);
7874
- if (isUiHeavy && ctx.currentPhase === "execute" && ctx.designApprovalPresent === false) {
7875
- missingRequirements.push("design approval (design stage must complete before execute for UI-heavy tasks)");
7876
- riskFlags.push("UI-heavy task entering execute phase without design approval");
7877
- requiredChanges.push("Run /fd-design first and obtain design approval before proceeding to execute");
7878
- }
7879
- }
7880
- if (commandName === "fd-fix-bug") {
7881
- if (ctx.regressionTestPresent === false) {
7882
- missingRequirements.push("regression test (required before bugfix implementation)");
7883
- riskFlags.push("Bugfix command invoked without a regression test");
7884
- requiredChanges.push("Write a failing regression test before implementing the fix");
7885
- }
7886
- }
7887
- if (commandName === "fd-deploy-check") {
7888
- if (ctx.prerequisitesMet === false && ctx.missingInputs && ctx.missingInputs.length > 0) {
7889
- missingRequirements.push(...ctx.missingInputs);
7890
- riskFlags.push("Deploy check attempted with unmet prerequisites");
7891
- }
7892
- }
7893
- if (commandName === "fd-execute" && ctx.currentPhase && ctx.currentPhase !== "execute") {
7894
- riskFlags.push(`fd-execute invoked in phase "${ctx.currentPhase}" instead of "execute"`);
7895
- requiredChanges.push(`Ensure project phase is "execute" before running fd-execute (currently: ${ctx.currentPhase})`);
7896
- }
7897
- if (ctx.approvalRequired && !ctx.approvalGranted) {
7898
- missingRequirements.push("human approval (required for this command)");
7899
- riskFlags.push("Approval gate not satisfied");
7900
- requiredChanges.push("Obtain explicit human approval before proceeding");
7901
- }
7902
- const passed = missingRequirements.length === 0 && riskFlags.length === 0 && requiredChanges.length === 0;
7903
- if (passed) {
7904
- reasons.push(`Command "${commandName}" passed all policy checks`);
7905
- }
7906
- return { passed, reasons, riskFlags, missingRequirements, requiredChanges };
7907
- }
7908
- function checkAgentPolicy(agentName, ctx) {
7909
- const reasons = [];
7910
- const riskFlags = [];
7911
- const missingRequirements = [];
7912
- const requiredChanges = [];
7913
- const contract = getContract(agentName);
7914
- if (!contract) {
7915
- riskFlags.push(`Agent "${agentName}" has no registered capability contract`);
7916
- return { passed: false, reasons, riskFlags, missingRequirements, requiredChanges };
7917
- }
7918
- if (ctx.missingInputs && ctx.missingInputs.length > 0) {
7919
- for (const missing of ctx.missingInputs) {
7920
- const isRequired = contract.requiredInputs.some((r) => r.toLowerCase().includes(missing.toLowerCase()) || missing.toLowerCase().includes(r.toLowerCase()));
7921
- if (isRequired) {
7922
- missingRequirements.push(missing);
7923
- requiredChanges.push(`Provide "${missing}" before delegating to ${agentName}`);
7924
- }
7925
- }
7926
- }
7927
- if (ctx.approvalRequired && !ctx.approvalGranted) {
7928
- const needsApproval = contract.escalationConditions.some((c) => c.toLowerCase().includes("approval") || c.toLowerCase().includes("approve"));
7929
- if (needsApproval) {
7930
- missingRequirements.push("human approval");
7931
- riskFlags.push(`Agent "${agentName}" requires approval via escalation condition`);
7932
- requiredChanges.push("Obtain explicit human approval before proceeding");
7933
- }
7934
- }
7935
- if (agentName === "design" || agentName === "frontend-coder") {
7936
- const taskLower = (ctx.taskDescription ?? "").toLowerCase();
7937
- const isUiHeavy = /landing page|dashboard|admin panel|website|web app|ui|ux|interface|frontend|component/.test(taskLower);
7938
- if (agentName === "frontend-coder" && isUiHeavy && ctx.designApprovalPresent === false) {
7939
- missingRequirements.push("design handoff approval");
7940
- riskFlags.push("frontend-coder invoked for UI-heavy task without approved design handoff");
7941
- requiredChanges.push("Complete design stage and obtain design approval before delegating to frontend-coder");
7942
- }
7943
- }
7944
- const passed = missingRequirements.length === 0 && riskFlags.length === 0;
7945
- if (passed) {
7946
- reasons.push(`Agent "${agentName}" passed all policy checks`);
7947
- }
7948
- return { passed, reasons, riskFlags, missingRequirements, requiredChanges };
7949
- }
7950
- function computeConfidence(exists, policyResult, ctx) {
7951
- if (!exists)
7952
- return 0;
7953
- if (policyResult.riskFlags.length >= 3)
7954
- return 0.2;
7955
- if (policyResult.riskFlags.length === 2)
7956
- return 0.4;
7957
- if (policyResult.riskFlags.length === 1)
7958
- return 0.6;
7959
- if (policyResult.missingRequirements.length > 0)
7960
- return 0.5;
7961
- if (ctx.prerequisitesMet === false)
7962
- return 0.45;
7963
- return 0.95;
7964
- }
7965
- function resolveDecision(exists, policyResult, confidenceScore, threshold, ctx, clarificationQuestion) {
7966
- if (!exists) {
7967
- return { decision: "block", approvalStatus: "denied" };
7968
- }
7969
- if (ctx.approvalRequired && !ctx.approvalGranted) {
7970
- return { decision: "escalate", approvalStatus: "escalated", clarificationQuestion };
7971
- }
7972
- if (!policyResult.passed) {
7973
- if (policyResult.requiredChanges.length > 0) {
7974
- return { decision: "revise", approvalStatus: "pending" };
7975
- }
7976
- return { decision: "block", approvalStatus: "denied" };
7977
- }
7978
- if (confidenceScore < threshold) {
7979
- return { decision: "escalate", approvalStatus: "escalated", clarificationQuestion };
7980
- }
7981
- return { decision: "approve", approvalStatus: "approved" };
7982
- }
7983
- function runSupervisorReview(directory, targetName, ctx = {}, clarificationQuestion) {
7984
- const config = resolveSupervisorConfig(directory);
7985
- const reviewPhase = ctx.reviewPhase ?? "preflight";
7986
- const timestamp2 = new Date().toISOString();
7987
- if (config.reviewedTargets.length > 0 && !config.reviewedTargets.includes(targetName)) {
7988
- return {
7989
- decision: "approve",
7990
- targetType: "agent",
7991
- targetName,
7992
- exists: true,
7993
- reasons: [`Target "${targetName}" is not in the reviewed targets list — auto-approved`],
7994
- missingRequirements: [],
7995
- riskFlags: [],
7996
- requiredChanges: [],
7997
- approvalStatus: "approved",
7998
- confidenceScore: 1,
7999
- reviewPhase,
8000
- timestamp: timestamp2
8001
- };
8002
- }
8003
- const { exists, type: targetType } = isRegisteredTarget(targetName);
8004
- if (!exists) {
8005
- const decision2 = {
8006
- decision: "block",
8007
- targetType,
8008
- targetName,
8009
- exists: false,
8010
- reasons: [
8011
- `Target "${targetName}" is not registered in the FlowDeck command or agent registry.`,
8012
- "The supervisor does not create new commands or workflows.",
8013
- "Only registered targets can be executed."
8014
- ],
8015
- missingRequirements: [],
8016
- riskFlags: [`Unregistered target: "${targetName}"`],
8017
- requiredChanges: [
8018
- `Use one of the registered commands: ${REGISTERED_COMMANDS.join(", ")}`,
8019
- `Or use one of the registered agents: ${AGENT_NAMES.join(", ")}`
8020
- ],
8021
- approvalStatus: "denied",
8022
- confidenceScore: 0,
8023
- reviewPhase,
8024
- timestamp: timestamp2
8025
- };
8026
- return decision2;
8027
- }
8028
- const policyResult = targetType === "command" ? checkCommandPolicy(targetName, ctx) : checkAgentPolicy(targetName, ctx);
8029
- const confidenceScore = computeConfidence(exists, policyResult, ctx);
8030
- const { decision, approvalStatus, clarificationQuestion: escalationQuestion } = resolveDecision(exists, policyResult, confidenceScore, config.confidenceThreshold, ctx, clarificationQuestion);
8031
- const reasons = policyResult.reasons.length > 0 ? policyResult.reasons : decision === "approve" ? [`Target "${targetName}" reviewed and approved for execution`] : [`Target "${targetName}" reviewed — decision: ${decision}`];
8032
- const supervisorDecision = {
8033
- decision,
8034
- targetType,
8035
- targetName,
8036
- exists,
8037
- reasons,
8038
- missingRequirements: policyResult.missingRequirements,
8039
- riskFlags: policyResult.riskFlags,
8040
- requiredChanges: policyResult.requiredChanges,
8041
- approvalStatus,
8042
- confidenceScore,
8043
- reviewPhase,
8044
- timestamp: timestamp2,
8045
- ...escalationQuestion ? { clarificationQuestion: escalationQuestion } : {}
8046
- };
8047
- return supervisorDecision;
8048
- }
8049
- function shouldProceed(decision, mode, canBlock) {
8050
- if (!decision.exists)
8051
- return false;
8052
- if (!canBlock)
8053
- return true;
8054
- if (mode === "strict") {
8055
- return decision.decision === "approve" || decision.decision === "revise";
8056
- }
8057
- return decision.decision !== "block" || decision.confidenceScore > 0.3;
8058
- }
8059
-
8060
6687
  // src/index.ts
8061
6688
  function lazyLoadRulePaths(projectRoot) {
8062
6689
  const __dir = dirname3(fileURLToPath2(import.meta.url));
8063
- const rulesDir = join27(__dir, "..", "src", "rules");
8064
- if (!existsSync29(rulesDir))
6690
+ const rulesDir = join24(__dir, "..", "src", "rules");
6691
+ if (!existsSync25(rulesDir))
8065
6692
  return { paths: [], diagnostics: "[LazyRuleLoader] rules directory not found" };
8066
6693
  const detectedLanguages = detectProjectLanguages(projectRoot);
8067
6694
  const paths = getStartupRulePaths(rulesDir, detectedLanguages);
@@ -8071,16 +6698,16 @@ function lazyLoadRulePaths(projectRoot) {
8071
6698
  }
8072
6699
  function loadCommands() {
8073
6700
  const __dir = dirname3(fileURLToPath2(import.meta.url));
8074
- const commandsDir = join27(__dir, "..", "src", "commands");
8075
- if (!existsSync29(commandsDir))
6701
+ const commandsDir = join24(__dir, "..", "src", "commands");
6702
+ if (!existsSync25(commandsDir))
8076
6703
  return {};
8077
6704
  const commands = {};
8078
6705
  try {
8079
- for (const file of readdirSync4(commandsDir)) {
6706
+ for (const file of readdirSync3(commandsDir)) {
8080
6707
  if (!file.endsWith(".md"))
8081
6708
  continue;
8082
6709
  const name = basename2(file, ".md");
8083
- const raw = readFileSync28(join27(commandsDir, file), "utf-8");
6710
+ const raw = readFileSync24(join24(commandsDir, file), "utf-8");
8084
6711
  let description;
8085
6712
  let template = raw;
8086
6713
  const fmMatch = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/);
@@ -8098,8 +6725,6 @@ function loadCommands() {
8098
6725
  var plugin = async (input, _options) => {
8099
6726
  const { directory, client, worktree } = input;
8100
6727
  const appLog = (msg) => client.app.log({ body: { service: "flowdeck", level: "info", message: msg } }).catch(() => {});
8101
- const runPipelineTool = createRunPipelineTool(client);
8102
- const delegateTool = createDelegateTool(client);
8103
6728
  const councilTool = createCouncilTool(client);
8104
6729
  const fileTracker = new SessionFileTracker;
8105
6730
  const { fileEdited, fileWatcherUpdated } = createFileTrackerHooks(fileTracker);
@@ -8164,8 +6789,8 @@ var plugin = async (input, _options) => {
8164
6789
  }
8165
6790
  }
8166
6791
  }
8167
- const skillsDir = join27(dirname3(fileURLToPath2(import.meta.url)), "..", "src", "skills");
8168
- if (existsSync29(skillsDir)) {
6792
+ const skillsDir = join24(dirname3(fileURLToPath2(import.meta.url)), "..", "src", "skills");
6793
+ if (existsSync25(skillsDir)) {
8169
6794
  const cfgAny = cfg;
8170
6795
  if (!cfgAny.skills || typeof cfgAny.skills !== "object") {
8171
6796
  cfgAny.skills = { paths: [] };
@@ -8194,8 +6819,6 @@ var plugin = async (input, _options) => {
8194
6819
  tool: {
8195
6820
  "planning-state": planningStateTool,
8196
6821
  "codebase-state": codebaseStateTool,
8197
- "run-pipeline": runPipelineTool,
8198
- delegate: delegateTool,
8199
6822
  "repo-memory": repoMemoryTool,
8200
6823
  "failure-replay": failureReplayTool,
8201
6824
  "decision-trace": decisionTraceTool,
@@ -8269,33 +6892,6 @@ var plugin = async (input, _options) => {
8269
6892
  }
8270
6893
  }
8271
6894
  orchestratorGuard.check(toolInput.sessionID ?? "", toolInput.tool ?? toolInput.name ?? "");
8272
- const toolName = toolInput.tool ?? toolInput.name ?? "";
8273
- if (toolName === "delegate" || toolName === "run-pipeline") {
8274
- const supConfig = resolveSupervisorConfig(directory);
8275
- if (supConfig.enabled) {
8276
- const args = toolOutput?.args ?? toolInput?.args ?? {};
8277
- const agentTarget = typeof args.agent === "string" ? args.agent.replace(/^@/, "") : Array.isArray(args.steps) && args.steps[0]?.agent ? String(args.steps[0].agent).replace(/^@/, "") : "";
8278
- if (agentTarget) {
8279
- const decision = runSupervisorReview(directory, agentTarget, {
8280
- taskDescription: typeof args.prompt === "string" ? args.prompt : undefined,
8281
- reviewPhase: "preflight",
8282
- session_id: toolInput.sessionID ?? toolInput.sessionId ?? ""
8283
- });
8284
- const proceed = shouldProceed(decision, supConfig.mode, supConfig.canBlock);
8285
- appLog(`[Supervisor] ${decision.reviewPhase} review of "${decision.targetName}": ` + `decision=${decision.decision} exists=${decision.exists} confidence=${decision.confidenceScore.toFixed(2)} ` + `${decision.riskFlags.length > 0 ? `risks=[${decision.riskFlags.join("; ")}]` : ""}`);
8286
- if (!proceed) {
8287
- const summary = [
8288
- `[Supervisor] Execution blocked for target "${decision.targetName}".`,
8289
- ...decision.reasons,
8290
- ...decision.missingRequirements.length > 0 ? [`Missing: ${decision.missingRequirements.join(", ")}`] : [],
8291
- ...decision.requiredChanges.length > 0 ? [`Required changes: ${decision.requiredChanges.join("; ")}`] : []
8292
- ].join(`
8293
- `);
8294
- throw new Error(summary);
8295
- }
8296
- }
8297
- }
8298
- }
8299
6895
  await approvalHook({ directory }, toolInput, toolOutput);
8300
6896
  await guardRailsHook({ directory }, toolInput, toolOutput);
8301
6897
  await toolGuardHook({ directory }, toolInput, toolOutput);
@@ -8305,30 +6901,6 @@ var plugin = async (input, _options) => {
8305
6901
  },
8306
6902
  "tool.execute.after": async (toolInput, toolOutput) => {
8307
6903
  await eventLog.after({ directory }, toolInput, toolOutput);
8308
- const afterToolName = toolInput.tool ?? toolInput.name ?? "";
8309
- if (afterToolName === "delegate" || afterToolName === "run-pipeline") {
8310
- try {
8311
- const supConfig = resolveSupervisorConfig(directory);
8312
- if (supConfig.enabled && supConfig.postExecutionReview) {
8313
- const args = toolOutput?.args ?? toolInput?.args ?? {};
8314
- const agentTarget = typeof args.agent === "string" ? args.agent.replace(/^@/, "") : Array.isArray(args.steps) && args.steps[0]?.agent ? String(args.steps[0].agent).replace(/^@/, "") : "";
8315
- if (agentTarget) {
8316
- const executionErrored = toolOutput?.error != null || toolOutput?.status === "error" || typeof toolOutput?.output === "string" && toolOutput.output.startsWith("Error:");
8317
- const decision = runSupervisorReview(directory, agentTarget, {
8318
- taskDescription: typeof args.prompt === "string" ? args.prompt : undefined,
8319
- reviewPhase: "post-stage",
8320
- session_id: toolInput.sessionID ?? toolInput.sessionId ?? "",
8321
- prerequisitesMet: !executionErrored
8322
- });
8323
- const logLevel = decision.decision === "block" || decision.decision === "escalate" ? "[Supervisor][WARN]" : "[Supervisor]";
8324
- appLog(`${logLevel} post-stage review of "${decision.targetName}": ` + `decision=${decision.decision} exists=${decision.exists} confidence=${decision.confidenceScore.toFixed(2)} ` + `executionErrored=${executionErrored} ` + `${decision.riskFlags.length > 0 ? `risks=[${decision.riskFlags.join("; ")}]` : ""}`);
8325
- if (supConfig.mode === "strict" && !shouldProceed(decision, "strict", supConfig.canBlock)) {
8326
- appLog(`[Supervisor][STRICT] Post-execution governance violation detected for "${decision.targetName}". ` + `Review the scorecard and telemetry for this run. ` + `Reasons: ${decision.reasons.join("; ")}`);
8327
- }
8328
- }
8329
- }
8330
- } catch {}
8331
- }
8332
6904
  await contextMonitor["tool.execute.after"](toolInput, toolOutput);
8333
6905
  }
8334
6906
  };