@dv.nghiem/flowdeck 0.4.6 → 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 -1938
  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,879 +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
- function createDelegateTool(client) {
1240
- return tool4({
1241
- description: "Delegate a task to a single agent via a child session. Returns the agent's output.",
1242
- args: {
1243
- agent: tool4.schema.string(),
1244
- prompt: tool4.schema.string(),
1245
- context: tool4.schema.string().optional(),
1246
- task_type: tool4.schema.string().optional(),
1247
- retry_attempts: tool4.schema.number().optional().default(1),
1248
- safe_to_cache: tool4.schema.boolean().optional().default(false),
1249
- cache_ttl_ms: tool4.schema.number().optional(),
1250
- workflow_id: tool4.schema.string().optional(),
1251
- stage: tool4.schema.string().optional()
1252
- },
1253
- async execute(args, context) {
1254
- const startTime = Date.now();
1255
- const taskType = normalizeTaskType(args.task_type, args.agent);
1256
- const retryAttempts = typeof args.retry_attempts === "number" ? args.retry_attempts : 1;
1257
- const maxRetries = Math.max(0, Math.floor(retryAttempts));
1258
- let agentModel = "";
1259
- try {
1260
- const cfg = loadFlowDeckConfig(context.directory);
1261
- agentModel = cfg.agents?.[args.agent]?.model ?? "";
1262
- } catch {}
1263
- const metricsWorkflowId = args.workflow_id ?? "";
1264
- const metricsStage = args.stage ?? "delegate";
1265
- const fullPrompt = args.context ? `${args.context}
1266
-
1267
- ---
1268
-
1269
- ${args.prompt}` : args.prompt;
1270
- const safe_to_cache = args.safe_to_cache === true && CACHEABLE_AGENTS.has(args.agent);
1271
- let stateVersion = 0;
1272
- let indexVersion = 0;
1273
- if (safe_to_cache) {
1274
- const index = readCodebaseIndex(context.directory);
1275
- const sp = statePath(context.directory);
1276
- const rawState = existsSync10(sp) ? readFileSync10(sp, "utf-8") : "";
1277
- const state = rawState ? parseState(rawState) : {};
1278
- stateVersion = typeof state.summaryVersion === "number" ? state.summaryVersion : 0;
1279
- indexVersion = typeof index.summaryVersion === "number" ? index.summaryVersion : 0;
1280
- const cached = getCached(context.directory, args.agent, fullPrompt, args.context ?? "", stateVersion, indexVersion, true);
1281
- if (cached !== null) {
1282
- if (metricsWorkflowId) {
1283
- recordCacheHit(context.directory, metricsWorkflowId, metricsStage, fullPrompt, args.agent, agentModel);
1284
- }
1285
- return JSON.stringify({
1286
- agent: args.agent,
1287
- success: true,
1288
- output: cached,
1289
- task_type: taskType,
1290
- model: "",
1291
- retries_used: 0,
1292
- duration_ms: Date.now() - startTime,
1293
- cached: true
1294
- });
1295
- }
1296
- }
1297
- const createRes = await client.session.create({
1298
- body: { parentID: context.sessionID, title: `${args.agent}-delegate` },
1299
- query: { directory: context.directory }
1300
- });
1301
- if (createRes.error || !createRes.data?.id) {
1302
- return JSON.stringify({
1303
- agent: args.agent,
1304
- success: false,
1305
- error: `Failed to create session: ${createRes.error?.detail ?? "unknown"}`,
1306
- duration_ms: Date.now() - startTime
1307
- });
1308
- }
1309
- const childId = createRes.data.id;
1310
- context.abort.addEventListener("abort", () => {
1311
- client.session.abort({
1312
- path: { id: childId },
1313
- query: { directory: context.directory }
1314
- }).catch(() => {});
1315
- });
1316
- const fullPromptForSession = args.context ? `${args.context}
1317
-
1318
- ---
1319
-
1320
- ${args.prompt}` : args.prompt;
1321
- let promptRes = null;
1322
- let retriesUsed = 0;
1323
- for (let attempt = 0;attempt <= maxRetries; attempt++) {
1324
- const attemptStart = Date.now();
1325
- promptRes = await client.session.prompt({
1326
- path: { id: childId },
1327
- body: {
1328
- agent: args.agent,
1329
- parts: [{ type: "text", text: fullPromptForSession }],
1330
- tools: { question: false }
1331
- },
1332
- query: { directory: context.directory }
1333
- });
1334
- if (!shouldRetry(promptRes) || attempt === maxRetries)
1335
- break;
1336
- if (metricsWorkflowId) {
1337
- const retryInputTokens = estimateTokens(fullPromptForSession);
1338
- const retryCostUsd = agentModel ? estimateCostUSD(agentModel, retryInputTokens, 0) : undefined;
1339
- recordRetryCall(context.directory, metricsWorkflowId, metricsStage, fullPromptForSession, "", args.agent, Date.now() - attemptStart, agentModel, retryCostUsd);
1340
- }
1341
- retriesUsed++;
1342
- }
1343
- if (!promptRes || promptRes.error) {
1344
- const errMsg = `Prompt failed: ${promptRes?.error?.detail ?? "unknown"}`;
1345
- recordRun(context.directory, args.agent, "", taskType, false, Date.now() - startTime);
1346
- return JSON.stringify({
1347
- agent: args.agent,
1348
- session_id: childId,
1349
- success: false,
1350
- error: errMsg,
1351
- task_type: taskType,
1352
- model: "",
1353
- retries_used: retriesUsed,
1354
- duration_ms: Date.now() - startTime
1355
- });
1356
- }
1357
- const info = promptRes.data?.info;
1358
- if (info?.error) {
1359
- const errMsg = `Agent error: ${JSON.stringify(info.error)}`;
1360
- recordRun(context.directory, args.agent, "", taskType, false, Date.now() - startTime);
1361
- return JSON.stringify({
1362
- agent: args.agent,
1363
- session_id: childId,
1364
- success: false,
1365
- error: errMsg,
1366
- task_type: taskType,
1367
- model: "",
1368
- retries_used: retriesUsed,
1369
- duration_ms: Date.now() - startTime
1370
- });
1371
- }
1372
- const output = extractText2(promptRes.data?.parts ?? []);
1373
- recordRun(context.directory, args.agent, "", taskType, true, Date.now() - startTime);
1374
- if (metricsWorkflowId) {
1375
- const inputTokens = estimateTokens(fullPromptForSession);
1376
- const outputTokens = estimateTokens(output);
1377
- const costUsd = agentModel ? estimateCostUSD(agentModel, inputTokens, outputTokens) : undefined;
1378
- recordModelCall(context.directory, metricsWorkflowId, metricsStage, fullPromptForSession, output, args.agent, Date.now() - startTime, agentModel, costUsd);
1379
- }
1380
- if (safe_to_cache && output) {
1381
- setCached(context.directory, args.agent, fullPromptForSession, args.context ?? "", stateVersion, indexVersion, output, true, args.cache_ttl_ms);
1382
- }
1383
- return JSON.stringify({
1384
- agent: args.agent,
1385
- session_id: childId,
1386
- success: true,
1387
- output: output || "(no text output)",
1388
- task_type: taskType,
1389
- model: "",
1390
- retries_used: retriesUsed,
1391
- duration_ms: Date.now() - startTime
1392
- });
1393
- }
1394
- });
1395
- }
1396
-
1397
- // src/tools/repo-memory.ts
1398
- import { tool as tool5 } from "@opencode-ai/plugin";
1399
- import { readFileSync as readFileSync11, writeFileSync as writeFileSync7, existsSync as existsSync11, mkdirSync as mkdirSync6 } from "fs";
1400
- import { join as join10 } from "path";
1401
586
  var MEMORY_FILE = "MEMORY.json";
1402
587
  function memoryPath(directory) {
1403
- return join10(codebaseDir(directory), MEMORY_FILE);
588
+ return join5(codebaseDir(directory), MEMORY_FILE);
1404
589
  }
1405
590
  function emptyMemory() {
1406
591
  return { version: "1.0", last_updated: new Date().toISOString(), nodes: {} };
1407
592
  }
1408
593
  function readMemory(directory) {
1409
594
  const p = memoryPath(directory);
1410
- if (!existsSync11(p))
595
+ if (!existsSync5(p))
1411
596
  return emptyMemory();
1412
597
  try {
1413
- return JSON.parse(readFileSync11(p, "utf-8"));
598
+ return JSON.parse(readFileSync5(p, "utf-8"));
1414
599
  } catch {
1415
600
  return emptyMemory();
1416
601
  }
1417
602
  }
1418
603
  function writeMemory(directory, memory) {
1419
604
  const base = codebaseDir(directory);
1420
- if (!existsSync11(base))
1421
- mkdirSync6(base, { recursive: true });
605
+ if (!existsSync5(base))
606
+ mkdirSync2(base, { recursive: true });
1422
607
  memory.last_updated = new Date().toISOString();
1423
- writeFileSync7(memoryPath(directory), JSON.stringify(memory, null, 2), "utf-8");
608
+ writeFileSync4(memoryPath(directory), JSON.stringify(memory, null, 2), "utf-8");
1424
609
  }
1425
- var repoMemoryTool = tool5({
610
+ var repoMemoryTool = tool3({
1426
611
  description: "Repo Memory Graph: read/write/query persistent architecture graph in .codebase/MEMORY.json (modules, dependencies, ownership, bug history, conventions)",
1427
612
  args: {
1428
- action: tool5.schema.enum(["read", "write_node", "query", "delete_node"]),
1429
- node_id: tool5.schema.string().optional(),
1430
- node: tool5.schema.object({
1431
- type: tool5.schema.enum(["module", "service", "api", "schema", "config"]),
1432
- path: tool5.schema.string(),
1433
- owner: tool5.schema.string().optional(),
1434
- tags: tool5.schema.array(tool5.schema.string()),
1435
- dependencies: tool5.schema.array(tool5.schema.string()),
1436
- dependents: tool5.schema.array(tool5.schema.string()),
1437
- bug_history: tool5.schema.array(tool5.schema.string()),
1438
- 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())
1439
624
  }).optional(),
1440
- query: tool5.schema.object({
1441
- type: tool5.schema.enum(["module", "service", "api", "schema", "config"]).optional(),
1442
- owner: tool5.schema.string().optional(),
1443
- tag: tool5.schema.string().optional(),
1444
- 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()
1445
630
  }).optional()
1446
631
  },
1447
632
  async execute(args, context) {
@@ -1496,50 +681,50 @@ var repoMemoryTool = tool5({
1496
681
  });
1497
682
 
1498
683
  // src/tools/failure-replay.ts
1499
- import { tool as tool6 } from "@opencode-ai/plugin";
1500
- import { readFileSync as readFileSync12, writeFileSync as writeFileSync8, existsSync as existsSync12, mkdirSync as mkdirSync7 } from "fs";
1501
- 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";
1502
687
  var FAILURES_FILE = "FAILURES.json";
1503
688
  function failuresPath(directory) {
1504
- return join11(codebaseDir(directory), FAILURES_FILE);
689
+ return join6(codebaseDir(directory), FAILURES_FILE);
1505
690
  }
1506
691
  function readStore(directory) {
1507
692
  const p = failuresPath(directory);
1508
- if (!existsSync12(p))
693
+ if (!existsSync6(p))
1509
694
  return { version: "1.0", last_updated: new Date().toISOString(), entries: [] };
1510
695
  try {
1511
- return JSON.parse(readFileSync12(p, "utf-8"));
696
+ return JSON.parse(readFileSync6(p, "utf-8"));
1512
697
  } catch {
1513
698
  return { version: "1.0", last_updated: new Date().toISOString(), entries: [] };
1514
699
  }
1515
700
  }
1516
701
  function writeStore(directory, store) {
1517
702
  const base = codebaseDir(directory);
1518
- if (!existsSync12(base))
1519
- mkdirSync7(base, { recursive: true });
703
+ if (!existsSync6(base))
704
+ mkdirSync3(base, { recursive: true });
1520
705
  store.last_updated = new Date().toISOString();
1521
- writeFileSync8(failuresPath(directory), JSON.stringify(store, null, 2), "utf-8");
706
+ writeFileSync5(failuresPath(directory), JSON.stringify(store, null, 2), "utf-8");
1522
707
  }
1523
- var failureReplayTool = tool6({
708
+ var failureReplayTool = tool4({
1524
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",
1525
710
  args: {
1526
- action: tool6.schema.enum(["record", "query", "list", "mark_resolved"]),
1527
- entry: tool6.schema.object({
1528
- id: tool6.schema.string(),
1529
- type: tool6.schema.enum(["reverted_commit", "failed_deployment", "flaky_test", "bug_fix", "build_failure"]),
1530
- description: tool6.schema.string(),
1531
- affected_paths: tool6.schema.array(tool6.schema.string()),
1532
- root_cause: tool6.schema.string().optional(),
1533
- fix_applied: tool6.schema.string().optional(),
1534
- 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())
1535
720
  }).optional(),
1536
- query: tool6.schema.object({
1537
- type: tool6.schema.enum(["reverted_commit", "failed_deployment", "flaky_test", "bug_fix", "build_failure"]).optional(),
1538
- path_prefix: tool6.schema.string().optional(),
1539
- tag: tool6.schema.string().optional(),
1540
- 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()
1541
726
  }).optional(),
1542
- entry_id: tool6.schema.string().optional()
727
+ entry_id: tool4.schema.string().optional()
1543
728
  },
1544
729
  async execute(args, context) {
1545
730
  const dir = context.directory ?? process.cwd();
@@ -1601,18 +786,18 @@ var failureReplayTool = tool6({
1601
786
  });
1602
787
 
1603
788
  // src/tools/decision-trace.ts
1604
- import { tool as tool7 } from "@opencode-ai/plugin";
1605
- import { readFileSync as readFileSync13, existsSync as existsSync13, mkdirSync as mkdirSync8, appendFileSync as appendFileSync2 } from "fs";
1606
- 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";
1607
792
  var DECISIONS_FILE = "DECISIONS.jsonl";
1608
793
  function decisionsPath(directory) {
1609
- return join12(codebaseDir(directory), DECISIONS_FILE);
794
+ return join7(codebaseDir(directory), DECISIONS_FILE);
1610
795
  }
1611
796
  function readDecisions(directory) {
1612
797
  const p = decisionsPath(directory);
1613
- if (!existsSync13(p))
798
+ if (!existsSync7(p))
1614
799
  return [];
1615
- return readFileSync13(p, "utf-8").split(`
800
+ return readFileSync7(p, "utf-8").split(`
1616
801
  `).filter((l) => l.trim()).map((l) => {
1617
802
  try {
1618
803
  return JSON.parse(l);
@@ -1621,29 +806,29 @@ function readDecisions(directory) {
1621
806
  }
1622
807
  }).filter(Boolean);
1623
808
  }
1624
- var decisionTraceTool = tool7({
809
+ var decisionTraceTool = tool5({
1625
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.",
1626
811
  args: {
1627
- action: tool7.schema.enum(["record", "query", "get_for_file"]),
1628
- entry: tool7.schema.object({
1629
- id: tool7.schema.string(),
1630
- file_path: tool7.schema.string(),
1631
- change_type: tool7.schema.enum(["create", "edit", "delete", "refactor"]),
1632
- rationale: tool7.schema.string(),
1633
- evidence: tool7.schema.array(tool7.schema.string()),
1634
- assumptions: tool7.schema.array(tool7.schema.string()),
1635
- alternatives_considered: tool7.schema.array(tool7.schema.string()),
1636
- risk_level: tool7.schema.enum(["low", "medium", "high"]),
1637
- agent: tool7.schema.string().optional(),
1638
- 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()
1639
824
  }).optional(),
1640
- query: tool7.schema.object({
1641
- file_path: tool7.schema.string().optional(),
1642
- change_type: tool7.schema.enum(["create", "edit", "delete", "refactor"]).optional(),
1643
- risk_level: tool7.schema.enum(["low", "medium", "high"]).optional(),
1644
- 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()
1645
830
  }).optional(),
1646
- file_path: tool7.schema.string().optional()
831
+ file_path: tool5.schema.string().optional()
1647
832
  },
1648
833
  async execute(args, context) {
1649
834
  const dir = context.directory ?? process.cwd();
@@ -1652,10 +837,10 @@ var decisionTraceTool = tool7({
1652
837
  case "record": {
1653
838
  if (!args.entry)
1654
839
  return JSON.stringify({ error: "entry required" });
1655
- if (!existsSync13(base))
1656
- mkdirSync8(base, { recursive: true });
840
+ if (!existsSync7(base))
841
+ mkdirSync4(base, { recursive: true });
1657
842
  const entry = { ...args.entry, timestamp: new Date().toISOString() };
1658
- appendFileSync2(decisionsPath(dir), JSON.stringify(entry) + `
843
+ appendFileSync(decisionsPath(dir), JSON.stringify(entry) + `
1659
844
  `, "utf-8");
1660
845
  return JSON.stringify({ success: true, id: args.entry.id });
1661
846
  }
@@ -1686,48 +871,48 @@ var decisionTraceTool = tool7({
1686
871
  });
1687
872
 
1688
873
  // src/tools/policy-engine.ts
1689
- import { tool as tool8 } from "@opencode-ai/plugin";
1690
- import { readFileSync as readFileSync14, writeFileSync as writeFileSync10, existsSync as existsSync14, mkdirSync as mkdirSync9 } from "fs";
1691
- 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";
1692
877
  var POLICIES_FILE = "POLICIES.json";
1693
878
  function policiesPath(directory) {
1694
- return join13(codebaseDir(directory), POLICIES_FILE);
879
+ return join8(codebaseDir(directory), POLICIES_FILE);
1695
880
  }
1696
881
  function readStore2(directory) {
1697
882
  const p = policiesPath(directory);
1698
- if (!existsSync14(p))
883
+ if (!existsSync8(p))
1699
884
  return { version: "1.0", last_updated: new Date().toISOString(), policies: [] };
1700
885
  try {
1701
- return JSON.parse(readFileSync14(p, "utf-8"));
886
+ return JSON.parse(readFileSync8(p, "utf-8"));
1702
887
  } catch {
1703
888
  return { version: "1.0", last_updated: new Date().toISOString(), policies: [] };
1704
889
  }
1705
890
  }
1706
891
  function writeStore2(directory, store) {
1707
892
  const base = codebaseDir(directory);
1708
- if (!existsSync14(base))
1709
- mkdirSync9(base, { recursive: true });
893
+ if (!existsSync8(base))
894
+ mkdirSync5(base, { recursive: true });
1710
895
  store.last_updated = new Date().toISOString();
1711
- writeFileSync10(policiesPath(directory), JSON.stringify(store, null, 2), "utf-8");
896
+ writeFileSync7(policiesPath(directory), JSON.stringify(store, null, 2), "utf-8");
1712
897
  }
1713
- var policyEngineTool = tool8({
898
+ var policyEngineTool = tool6({
1714
899
  description: "Self-Healing Policy Engine: manage .codebase/POLICIES.json — add, list, query, toggle, and record violations of editing policies learned from past failures",
1715
900
  args: {
1716
- action: tool8.schema.enum(["list", "add", "record_violation", "toggle", "query"]),
1717
- policy: tool8.schema.object({
1718
- id: tool8.schema.string(),
1719
- name: tool8.schema.string(),
1720
- trigger: tool8.schema.string(),
1721
- rule: tool8.schema.string(),
1722
- source: tool8.schema.enum(["manual", "learned"]),
1723
- 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()
1724
909
  }).optional(),
1725
- policy_id: tool8.schema.string().optional(),
1726
- active: tool8.schema.boolean().optional(),
1727
- query: tool8.schema.object({
1728
- source: tool8.schema.enum(["manual", "learned"]).optional(),
1729
- active_only: tool8.schema.boolean().optional(),
1730
- 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()
1731
916
  }).optional()
1732
917
  },
1733
918
  async execute(args, context) {
@@ -1786,54 +971,154 @@ var policyEngineTool = tool8({
1786
971
  }
1787
972
  }
1788
973
  }
1789
- });
1790
-
1791
- // src/tools/hash-edit.ts
1792
- import { tool as tool9 } from "@opencode-ai/plugin";
1793
- import { readFileSync as readFileSync15, writeFileSync as writeFileSync11 } from "fs";
1794
- import { createHash as createHash2 } from "crypto";
1795
- var hashEditTool = tool9({
1796
- 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.",
1797
- args: {
1798
- filePath: tool9.schema.string(),
1799
- targetContent: tool9.schema.string(),
1800
- expectedHash: tool9.schema.string().optional(),
1801
- replacementContent: tool9.schema.string()
1802
- },
1803
- async execute(args, context) {
1804
- const fullPath = args.filePath.startsWith("/") ? args.filePath : `${context.directory}/${args.filePath}`;
1805
- 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++;
1806
1083
  try {
1807
- content = readFileSync15(fullPath, "utf-8");
1808
- } catch (e) {
1809
- return `Error: Could not read file ${args.filePath}`;
1810
- }
1811
- if (!content.includes(args.targetContent)) {
1812
- return `Error: Target content not found in ${args.filePath}. It may have been modified by another agent.`;
1813
- }
1814
- if (args.expectedHash) {
1815
- const actualHash = createHash2("md5").update(args.targetContent).digest("hex");
1816
- if (actualHash !== args.expectedHash) {
1817
- 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
+ }
1818
1097
  }
1098
+ } catch {
1099
+ result.freshnessStatus = "unknown";
1819
1100
  }
1820
- const newContent = content.replace(args.targetContent, args.replacementContent);
1821
- writeFileSync11(fullPath, newContent, "utf-8");
1822
- return `Successfully updated ${args.filePath} using hash-anchored edit.`;
1823
1101
  }
1824
- });
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
+ }
1825
1114
 
1826
1115
  // src/tools/council.ts
1827
- import { tool as tool10 } from "@opencode-ai/plugin";
1828
- import { appendFileSync as appendFileSync3, existsSync as existsSync15, mkdirSync as mkdirSync10 } from "fs";
1829
- import { join as join14 } from "path";
1830
- import { createHash as createHash3 } from "crypto";
1831
- import { readFileSync as readFileSync16 } from "fs";
1116
+ import { readFileSync as readFileSync11 } from "fs";
1832
1117
  var _councilCache = new Map;
1833
1118
  var COUNCIL_CACHE_TTL_MS = 20 * 60 * 1000;
1834
1119
  function councilCacheKey(task, agents, stateVersion, indexVersion) {
1835
1120
  const sorted = [...agents].sort();
1836
- 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);
1837
1122
  }
1838
1123
  async function runWithConcurrencyLimit(tasks, limit) {
1839
1124
  const results = new Array(tasks.length);
@@ -1849,20 +1134,20 @@ async function runWithConcurrencyLimit(tasks, limit) {
1849
1134
  return results;
1850
1135
  }
1851
1136
  function createCouncilTool(client) {
1852
- return tool10({
1137
+ return tool8({
1853
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.",
1854
1139
  args: {
1855
- task: tool10.schema.string(),
1856
- agents: tool10.schema.array(tool10.schema.string()).optional(),
1857
- force_fresh: tool10.schema.boolean().optional().default(false),
1858
- 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)
1859
1144
  },
1860
1145
  async execute(args, context) {
1861
1146
  const agents = args.agents || ["architect", "reviewer", "backend-coder"];
1862
1147
  const concurrencyLimit = Math.max(1, Math.min(5, typeof args.max_concurrency === "number" ? args.max_concurrency : 3));
1863
1148
  const index = readCodebaseIndex(context.directory);
1864
1149
  const sp = statePath(context.directory);
1865
- const rawState = existsSync15(sp) ? readFileSync16(sp, "utf-8") : "";
1150
+ const rawState = existsSync10(sp) ? readFileSync11(sp, "utf-8") : "";
1866
1151
  const state = rawState ? parseState(rawState) : {};
1867
1152
  const stateVersion = typeof state.summaryVersion === "number" ? state.summaryVersion : 0;
1868
1153
  const indexVersion = typeof index.summaryVersion === "number" ? index.summaryVersion : 0;
@@ -1938,18 +1223,18 @@ Please synthesize these results. Identify areas of agreement, resolve conflicts,
1938
1223
  function persistCouncilResult(directory, payload) {
1939
1224
  try {
1940
1225
  const base = codebaseDir(directory);
1941
- if (!existsSync15(base))
1942
- mkdirSync10(base, { recursive: true });
1943
- const path = join14(base, "COUNCILS.jsonl");
1944
- 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) + `
1945
1230
  `, "utf-8");
1946
1231
  } catch {}
1947
1232
  }
1948
1233
 
1949
1234
  // src/tools/reflect.ts
1950
- import { tool as tool11 } from "@opencode-ai/plugin";
1951
- import { existsSync as existsSync16, readFileSync as readFileSync17 } from "fs";
1952
- 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";
1953
1238
  var MAX_ARTIFACT_BYTES = 4000;
1954
1239
  function tail(text, maxBytes) {
1955
1240
  if (text.length <= maxBytes)
@@ -1957,10 +1242,10 @@ function tail(text, maxBytes) {
1957
1242
  return `... (truncated) ...
1958
1243
  ` + text.slice(-maxBytes);
1959
1244
  }
1960
- var reflectTool = tool11({
1245
+ var reflectTool = tool9({
1961
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.",
1962
1247
  args: {
1963
- 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")
1964
1249
  },
1965
1250
  async execute(args, context) {
1966
1251
  const root = context.directory;
@@ -1978,11 +1263,11 @@ var reflectTool = tool11({
1978
1263
  ];
1979
1264
  let found = 0;
1980
1265
  for (const [rel, label] of ARTIFACT_PATHS) {
1981
- const full = join15(root, rel);
1982
- if (!existsSync16(full))
1266
+ const full = join11(root, rel);
1267
+ if (!existsSync11(full))
1983
1268
  continue;
1984
1269
  try {
1985
- const raw = readFileSync17(full, "utf-8").trim();
1270
+ const raw = readFileSync12(full, "utf-8").trim();
1986
1271
  if (!raw)
1987
1272
  continue;
1988
1273
  const count = raw.split(`
@@ -2002,16 +1287,16 @@ var reflectTool = tool11({
2002
1287
  });
2003
1288
 
2004
1289
  // src/tools/codegraph-tool.ts
2005
- import { tool as tool12 } from "@opencode-ai/plugin";
1290
+ import { tool as tool10 } from "@opencode-ai/plugin";
2006
1291
 
2007
1292
  // src/services/codegraph.ts
2008
1293
  import { spawnSync } from "child_process";
2009
- import { existsSync as existsSync17, readFileSync as readFileSync18, writeFileSync as writeFileSync12, mkdirSync as mkdirSync11 } from "fs";
2010
- 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";
2011
1296
  var CODEGRAPH_META_FILE = "CODEGRAPH.md";
2012
1297
  var MAX_FRESHNESS_MS = 30 * 60 * 1000;
2013
1298
  function metaPath(dir) {
2014
- return join16(codebaseDir(dir), CODEGRAPH_META_FILE);
1299
+ return join12(codebaseDir(dir), CODEGRAPH_META_FILE);
2015
1300
  }
2016
1301
  function isCodegraphInstalled() {
2017
1302
  try {
@@ -2026,11 +1311,11 @@ function isCodegraphInstalled() {
2026
1311
  }
2027
1312
  }
2028
1313
  function isCodegraphIndexed(dir) {
2029
- return existsSync17(join16(dir, ".codegraph", "codegraph.db"));
1314
+ return existsSync12(join12(dir, ".codegraph", "codegraph.db"));
2030
1315
  }
2031
1316
  function readCodegraphMeta(dir) {
2032
1317
  const path = metaPath(dir);
2033
- if (!existsSync17(path)) {
1318
+ if (!existsSync12(path)) {
2034
1319
  return {
2035
1320
  installed: false,
2036
1321
  indexed: false,
@@ -2043,7 +1328,7 @@ function readCodegraphMeta(dir) {
2043
1328
  };
2044
1329
  }
2045
1330
  try {
2046
- const content = readFileSync18(path, "utf-8");
1331
+ const content = readFileSync13(path, "utf-8");
2047
1332
  return parseCodegraphMeta(content);
2048
1333
  } catch {
2049
1334
  return {
@@ -2110,8 +1395,8 @@ function parseCodegraphMeta(content) {
2110
1395
  }
2111
1396
  function writeCodegraphMeta(dir, meta) {
2112
1397
  const base = codebaseDir(dir);
2113
- if (!existsSync17(base))
2114
- mkdirSync11(base, { recursive: true });
1398
+ if (!existsSync12(base))
1399
+ mkdirSync8(base, { recursive: true });
2115
1400
  const lines = [
2116
1401
  "# Codegraph Metadata",
2117
1402
  "",
@@ -2124,7 +1409,7 @@ function writeCodegraphMeta(dir, meta) {
2124
1409
  `**installLog:** ${meta.installLog}`,
2125
1410
  `**indexLog:** ${meta.indexLog}`
2126
1411
  ];
2127
- writeFileSync12(metaPath(dir), lines.join(`
1412
+ writeFileSync10(metaPath(dir), lines.join(`
2128
1413
  `), "utf-8");
2129
1414
  }
2130
1415
  function isCodegraphFresh(dir, maxAgeMs = MAX_FRESHNESS_MS) {
@@ -2335,11 +1620,11 @@ function markCodegraphStale(dir) {
2335
1620
  }
2336
1621
 
2337
1622
  // src/tools/codegraph-tool.ts
2338
- var codegraphTool = tool12({
1623
+ var codegraphTool = tool10({
2339
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.",
2340
1625
  args: {
2341
- action: tool12.schema.enum(["check", "install", "init", "refresh", "status", "mark-stale"]),
2342
- agent: tool12.schema.string().optional()
1626
+ action: tool10.schema.enum(["check", "install", "init", "refresh", "status", "mark-stale"]),
1627
+ agent: tool10.schema.string().optional()
2343
1628
  },
2344
1629
  async execute(args, context) {
2345
1630
  const dir = context.directory ?? process.cwd();
@@ -2428,21 +1713,21 @@ var codegraphTool = tool12({
2428
1713
  });
2429
1714
 
2430
1715
  // src/tools/load-rules.ts
2431
- import { tool as tool13 } from "@opencode-ai/plugin";
2432
- import { existsSync as existsSync18, readFileSync as readFileSync19 } from "fs";
2433
- 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";
2434
1719
  import { fileURLToPath } from "url";
2435
- var RULES_DIR = join17(dirname2(fileURLToPath(import.meta.url)), "..", "rules");
1720
+ var RULES_DIR = join13(dirname2(fileURLToPath(import.meta.url)), "..", "rules");
2436
1721
  var _loadedPaths = new Set;
2437
- var loadRulesTool = tool13({
1722
+ var loadRulesTool = tool11({
2438
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).",
2439
1724
  args: {
2440
- stage: tool13.schema.string().optional().describe("Current workflow stage: discuss | plan | execute | verify | fix-bug | write-docs"),
2441
- 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)."),
2442
- 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.")
2443
1728
  },
2444
1729
  async execute(args) {
2445
- const rulesDir = existsSync18(RULES_DIR) ? RULES_DIR : null;
1730
+ const rulesDir = existsSync13(RULES_DIR) ? RULES_DIR : null;
2446
1731
  if (!rulesDir) {
2447
1732
  return JSON.stringify({
2448
1733
  loaded: [],
@@ -2468,7 +1753,7 @@ var loadRulesTool = tool13({
2468
1753
  continue;
2469
1754
  }
2470
1755
  try {
2471
- const text = readFileSync19(rule.path, "utf-8");
1756
+ const text = readFileSync14(rule.path, "utf-8");
2472
1757
  contents.push(`## ${name}
2473
1758
 
2474
1759
  ${text}`);
@@ -2499,11 +1784,11 @@ ${text}`);
2499
1784
  function ruleShortName(rule) {
2500
1785
  return rule.path.replace(RULES_DIR + "/", "").replace(/\.md$/, "");
2501
1786
  }
2502
- var listRulesTool = tool13({
1787
+ var listRulesTool = tool11({
2503
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.",
2504
1789
  args: {},
2505
1790
  async execute() {
2506
- const rulesDir = existsSync18(RULES_DIR) ? RULES_DIR : null;
1791
+ const rulesDir = existsSync13(RULES_DIR) ? RULES_DIR : null;
2507
1792
  if (!rulesDir) {
2508
1793
  return JSON.stringify({ rules: [], error: `Rules directory not found at ${RULES_DIR}` });
2509
1794
  }
@@ -2523,13 +1808,13 @@ var listRulesTool = tool13({
2523
1808
  });
2524
1809
 
2525
1810
  // src/tools/rtk-setup.ts
2526
- import { tool as tool14 } from "@opencode-ai/plugin";
1811
+ import { tool as tool12 } from "@opencode-ai/plugin";
2527
1812
 
2528
1813
  // src/services/rtk-manager.ts
2529
1814
  import { spawnSync as spawnSync2 } from "child_process";
2530
- import { existsSync as existsSync19 } from "fs";
2531
- import { homedir as homedir2 } from "os";
2532
- 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";
2533
1818
 
2534
1819
  // src/services/rtk-policy.ts
2535
1820
  var SUPPORTED_COMMANDS = new Set([
@@ -2575,7 +1860,7 @@ var INSTALL_INSTRUCTIONS = [
2575
1860
  "After installation, call rtk-setup again to verify detection."
2576
1861
  ].join(`
2577
1862
  `);
2578
- 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"];
2579
1864
  function detectRtk() {
2580
1865
  const fromPath = spawnSync2("rtk", ["--version"], { encoding: "utf-8", timeout: 5000 });
2581
1866
  if (fromPath.status === 0) {
@@ -2584,7 +1869,7 @@ function detectRtk() {
2584
1869
  return { installed: true, binPath: "rtk", version };
2585
1870
  }
2586
1871
  for (const candidate of CANDIDATE_PATHS) {
2587
- if (!existsSync19(candidate))
1872
+ if (!existsSync14(candidate))
2588
1873
  continue;
2589
1874
  const result = spawnSync2(candidate, ["--version"], { encoding: "utf-8", timeout: 5000 });
2590
1875
  if (result.status === 0) {
@@ -2662,7 +1947,7 @@ function getRtkStatus(opts) {
2662
1947
  }
2663
1948
 
2664
1949
  // src/tools/rtk-setup.ts
2665
- var rtkSetupTool = tool14({
1950
+ var rtkSetupTool = tool12({
2666
1951
  description: [
2667
1952
  "Detect, initialize, and report status of rtk (output compression proxy for CLI commands).",
2668
1953
  "rtk reduces noisy CLI output (git, npm, test runners, linters, docker) by 60-90%.",
@@ -2670,7 +1955,7 @@ var rtkSetupTool = tool14({
2670
1955
  "When RTK_INSTALLED=true in the environment, use `$RTK_BIN git status` for compressed output."
2671
1956
  ].join(" "),
2672
1957
  args: {
2673
- 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.")
2674
1959
  },
2675
1960
  async execute(args) {
2676
1961
  const action = args.action ?? "status";
@@ -2710,15 +1995,99 @@ var rtkSetupTool = tool14({
2710
1995
  });
2711
1996
 
2712
1997
  // src/hooks/guard-rails.ts
2713
- import { existsSync as existsSync20, readFileSync as readFileSync20 } from "fs";
2714
- 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
2715
2084
  var PLANNING_DIR2 = ".planning";
2716
2085
  var CONFIG_FILE = "config.json";
2717
2086
  var STATE_FILE2 = "STATE.md";
2718
2087
  function resolveExecutionMode(configPath, trustScore, volatility) {
2719
- if (existsSync20(configPath)) {
2088
+ if (existsSync16(configPath)) {
2720
2089
  try {
2721
- const config = JSON.parse(readFileSync20(configPath, "utf-8"));
2090
+ const config = JSON.parse(readFileSync16(configPath, "utf-8"));
2722
2091
  if (config.execution_mode === "review-only")
2723
2092
  return "review-only";
2724
2093
  if (config.execution_mode === "guarded")
@@ -2772,22 +2141,22 @@ async function guardRailsHook(ctx, input, _output) {
2772
2141
  if (!ENABLED)
2773
2142
  return;
2774
2143
  const dir = ctx.directory;
2775
- const planningDirPath = join19(dir, PLANNING_DIR2);
2144
+ const planningDirPath = join16(dir, PLANNING_DIR2);
2776
2145
  const codebaseDirectory = codebaseDir(dir);
2777
- const configPath = join19(planningDirPath, CONFIG_FILE);
2778
- const statePath2 = join19(planningDirPath, STATE_FILE2);
2146
+ const configPath = join16(planningDirPath, CONFIG_FILE);
2147
+ const statePath2 = join16(planningDirPath, STATE_FILE2);
2779
2148
  const workspaceRoot = findWorkspaceRoot(dir);
2780
2149
  if (workspaceRoot && dir !== workspaceRoot) {
2781
2150
  const config = getWorkspaceConfig(dir);
2782
- if (config && config.workspace_mode === "shared" && !existsSync20(planningDirPath)) {
2151
+ if (config && config.workspace_mode === "shared" && !existsSync16(planningDirPath)) {
2783
2152
  const msg = `No .planning/ in this sub-repo. Switch to workspace root: cd ${workspaceRoot}`;
2784
2153
  throw new Error(`[flowdeck] BLOCK: ${msg}`);
2785
2154
  }
2786
2155
  }
2787
2156
  if (input.tool === "write" || input.tool === "edit") {
2788
- if (!existsSync20(planningDirPath))
2157
+ if (!existsSync16(planningDirPath))
2789
2158
  return;
2790
- if (!existsSync20(codebaseDirectory)) {
2159
+ if (!existsSync16(codebaseDirectory)) {
2791
2160
  throw new Error(`[flowdeck] WARNING: .codebase/ not found. Run /fd-map-codebase to map the codebase.`);
2792
2161
  }
2793
2162
  const execMode = resolveExecutionMode(configPath, null);
@@ -2843,15 +2212,15 @@ function getDesignGateMessage(dir) {
2843
2212
  }
2844
2213
  function planSuggestsUiHeavy(dir, phase) {
2845
2214
  const planPath = phasePlanPath(dir, phase);
2846
- if (!existsSync20(planPath))
2215
+ if (!existsSync16(planPath))
2847
2216
  return false;
2848
- const planContent = readFileSync20(planPath, "utf-8");
2217
+ const planContent = readFileSync16(planPath, "utf-8");
2849
2218
  return isUiHeavyTask(planContent);
2850
2219
  }
2851
2220
  function effectiveSeverity(configPath, statePath2) {
2852
- if (existsSync20(configPath)) {
2221
+ if (existsSync16(configPath)) {
2853
2222
  try {
2854
- const configContent = readFileSync20(configPath, "utf-8");
2223
+ const configContent = readFileSync16(configPath, "utf-8");
2855
2224
  const config = JSON.parse(configContent);
2856
2225
  if (config.guard_enforcement === "warn")
2857
2226
  return "warn";
@@ -2867,10 +2236,10 @@ function getEffectiveSeverity(configPath, statePath2) {
2867
2236
  return effectiveSeverity(configPath, statePath2);
2868
2237
  }
2869
2238
  function getPlanConfirmed(statePath2) {
2870
- if (!existsSync20(statePath2))
2239
+ if (!existsSync16(statePath2))
2871
2240
  return false;
2872
2241
  try {
2873
- const content = readFileSync20(statePath2, "utf-8");
2242
+ const content = readFileSync16(statePath2, "utf-8");
2874
2243
  const match = content.match(/plan_confirmed:\s*(true|false)/i);
2875
2244
  return match ? match[1].toLowerCase() === "true" : false;
2876
2245
  } catch {
@@ -2878,32 +2247,32 @@ function getPlanConfirmed(statePath2) {
2878
2247
  }
2879
2248
  }
2880
2249
  function getWarningMessage(planningDir2) {
2881
- if (!existsSync20(join19(planningDir2, STATE_FILE2))) {
2250
+ if (!existsSync16(join16(planningDir2, STATE_FILE2))) {
2882
2251
  return "No STATE.md found. Run /fd-map-codebase then /fd-new-feature to start a feature.";
2883
2252
  }
2884
2253
  return "Plan not confirmed. Run /fd-plan and confirm to enable execution.";
2885
2254
  }
2886
2255
  function getBlockMessage(planningDir2) {
2887
- if (!existsSync20(join19(planningDir2, STATE_FILE2))) {
2256
+ if (!existsSync16(join16(planningDir2, STATE_FILE2))) {
2888
2257
  return "No STATE.md found. Run /fd-map-codebase then /fd-new-feature to start a feature.";
2889
2258
  }
2890
2259
  return "Plan not confirmed. Run /fd-plan and confirm to enable execution.";
2891
2260
  }
2892
2261
 
2893
2262
  // src/hooks/tool-guard.ts
2894
- import { existsSync as existsSync21, readFileSync as readFileSync21 } from "fs";
2895
- import { join as join20 } from "path";
2263
+ import { existsSync as existsSync17, readFileSync as readFileSync17 } from "fs";
2264
+ import { join as join17 } from "path";
2896
2265
  var IS_ENABLED = () => process.env.FLOWDECK_TOOL_GUARD_ENABLED === "on";
2897
2266
  var BLOCKED_PATTERNS = {
2898
2267
  read: [".env", ".pem", ".key", ".secret"],
2899
2268
  write: ["node_modules"],
2900
2269
  bash: ["rm -rf"]
2901
2270
  };
2902
- function isBlocked(tool15, args) {
2903
- const patterns = BLOCKED_PATTERNS[tool15];
2271
+ function isBlocked(tool13, args) {
2272
+ const patterns = BLOCKED_PATTERNS[tool13];
2904
2273
  if (!patterns)
2905
2274
  return null;
2906
- if (tool15 === "bash") {
2275
+ if (tool13 === "bash") {
2907
2276
  const cmd = args.command;
2908
2277
  if (!cmd)
2909
2278
  return null;
@@ -2914,7 +2283,7 @@ function isBlocked(tool15, args) {
2914
2283
  }
2915
2284
  return null;
2916
2285
  }
2917
- if (tool15 === "read") {
2286
+ if (tool13 === "read") {
2918
2287
  const filePath = args.filePath;
2919
2288
  if (!filePath)
2920
2289
  return null;
@@ -2925,7 +2294,7 @@ function isBlocked(tool15, args) {
2925
2294
  }
2926
2295
  return null;
2927
2296
  }
2928
- if (tool15 === "write") {
2297
+ if (tool13 === "write") {
2929
2298
  const filePath = args.filePath;
2930
2299
  if (!filePath)
2931
2300
  return null;
@@ -2939,11 +2308,11 @@ function isBlocked(tool15, args) {
2939
2308
  return null;
2940
2309
  }
2941
2310
  function checkArchConstraint(directory, filePath) {
2942
- const constraintsPath = join20(codebaseDir(directory), "CONSTRAINTS.md");
2943
- if (!existsSync21(constraintsPath))
2311
+ const constraintsPath = join17(codebaseDir(directory), "CONSTRAINTS.md");
2312
+ if (!existsSync17(constraintsPath))
2944
2313
  return null;
2945
2314
  try {
2946
- const content = readFileSync21(constraintsPath, "utf-8");
2315
+ const content = readFileSync17(constraintsPath, "utf-8");
2947
2316
  const match = content.match(/## Forbidden Paths\n([\s\S]*?)(?:\n##|$)/);
2948
2317
  if (!match)
2949
2318
  return null;
@@ -2984,9 +2353,9 @@ function isUiDesignApprovalRequired(directory) {
2984
2353
  return !(state.design_stage === "handoff_complete" && state.design_approved);
2985
2354
  }
2986
2355
  const planPath = phasePlanPath(directory, state.phase || 1);
2987
- if (!existsSync21(planPath))
2356
+ if (!existsSync17(planPath))
2988
2357
  return false;
2989
- const planContent = readFileSync21(planPath, "utf-8");
2358
+ const planContent = readFileSync17(planPath, "utf-8");
2990
2359
  if (!isUiHeavyTask(planContent))
2991
2360
  return false;
2992
2361
  return !(state.design_stage === "handoff_complete" && state.design_approved);
@@ -3015,18 +2384,18 @@ async function toolGuardHook(ctx, input, output) {
3015
2384
  }
3016
2385
 
3017
2386
  // src/hooks/session-start.ts
3018
- import { existsSync as existsSync22, readFileSync as readFileSync22 } from "fs";
2387
+ import { existsSync as existsSync18, readFileSync as readFileSync18 } from "fs";
3019
2388
  async function sessionStartHook(ctx) {
3020
2389
  const planningDir2 = ctx.directory + "/.planning";
3021
2390
  const codebaseDirectory = codebaseDir(ctx.directory);
3022
2391
  const workspaceRoot = findWorkspaceRoot(ctx.directory);
3023
2392
  const config = workspaceRoot ? getWorkspaceConfig(ctx.directory) : null;
3024
- if (!existsSync22(planningDir2)) {
2393
+ if (!existsSync18(planningDir2)) {
3025
2394
  return {
3026
2395
  flowdeck_phase: null,
3027
2396
  flowdeck_status: "no_plan",
3028
2397
  flowdeck_warning: "Run /fd-map-codebase to index the codebase, then /fd-new-feature to start a feature.",
3029
- flowdeck_has_codebase: existsSync22(codebaseDirectory),
2398
+ flowdeck_has_codebase: existsSync18(codebaseDirectory),
3030
2399
  ...workspaceRoot && config?.sub_repos ? {
3031
2400
  flowdeck_workspace_root: workspaceRoot,
3032
2401
  flowdeck_sub_repos: config.sub_repos,
@@ -3037,7 +2406,7 @@ async function sessionStartHook(ctx) {
3037
2406
  }
3038
2407
  try {
3039
2408
  const stateFilePath = statePath(ctx.directory);
3040
- const content = readFileSync22(stateFilePath, "utf-8");
2409
+ const content = readFileSync18(stateFilePath, "utf-8");
3041
2410
  const state = parseState(content);
3042
2411
  const currentPhase = state["current_phase"] || {};
3043
2412
  const result = {
@@ -3045,7 +2414,7 @@ async function sessionStartHook(ctx) {
3045
2414
  flowdeck_status: currentPhase["status"] ?? null,
3046
2415
  flowdeck_steps_pending: currentPhase["steps_pending"] ?? null,
3047
2416
  flowdeck_last_action: currentPhase["last_action"] ?? null,
3048
- flowdeck_has_codebase: existsSync22(codebaseDirectory)
2417
+ flowdeck_has_codebase: existsSync18(codebaseDirectory)
3049
2418
  };
3050
2419
  if (workspaceRoot && config?.sub_repos && config.sub_repos.length > 0) {
3051
2420
  result.flowdeck_workspace_root = workspaceRoot;
@@ -3059,7 +2428,7 @@ async function sessionStartHook(ctx) {
3059
2428
  flowdeck_phase: null,
3060
2429
  flowdeck_status: "error",
3061
2430
  flowdeck_warning: "State file unreadable. Continuing without flowdeck context.",
3062
- flowdeck_has_codebase: existsSync22(codebaseDirectory)
2431
+ flowdeck_has_codebase: existsSync18(codebaseDirectory)
3063
2432
  };
3064
2433
  if (workspaceRoot && config?.sub_repos && config.sub_repos.length > 0) {
3065
2434
  result.flowdeck_workspace_root = workspaceRoot;
@@ -3191,13 +2560,13 @@ class NotificationController {
3191
2560
  return this.lastNotifiedKey;
3192
2561
  }
3193
2562
  }
3194
- function notifyPermissionNeeded(tool15) {
3195
- 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");
3196
2565
  }
3197
2566
 
3198
2567
  // src/hooks/patch-trust.ts
3199
- import { existsSync as existsSync23, readFileSync as readFileSync23 } from "fs";
3200
- import { join as join21 } from "path";
2568
+ import { existsSync as existsSync19, readFileSync as readFileSync19 } from "fs";
2569
+ import { join as join18 } from "path";
3201
2570
  var HIGH_RISK_KEYWORDS = [
3202
2571
  "password",
3203
2572
  "secret",
@@ -3219,11 +2588,11 @@ var HIGH_RISK_KEYWORDS = [
3219
2588
  "privilege"
3220
2589
  ];
3221
2590
  function loadFailedPaths(directory) {
3222
- const p = join21(codebaseDir(directory), "FAILURES.json");
3223
- if (!existsSync23(p))
2591
+ const p = join18(codebaseDir(directory), "FAILURES.json");
2592
+ if (!existsSync19(p))
3224
2593
  return [];
3225
2594
  try {
3226
- const data = JSON.parse(readFileSync23(p, "utf-8"));
2595
+ const data = JSON.parse(readFileSync19(p, "utf-8"));
3227
2596
  return (data.entries ?? []).flatMap((e) => e.affected_paths ?? []);
3228
2597
  } catch {
3229
2598
  return [];
@@ -3276,8 +2645,8 @@ async function patchTrustHook(ctx, input, output) {
3276
2645
  }
3277
2646
 
3278
2647
  // src/hooks/decision-trace-hook.ts
3279
- import { existsSync as existsSync24, mkdirSync as mkdirSync12, appendFileSync as appendFileSync4 } from "fs";
3280
- 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";
3281
2650
  async function decisionTraceHook(ctx, input, output) {
3282
2651
  if (input.tool !== "write" && input.tool !== "edit")
3283
2652
  return;
@@ -3286,8 +2655,8 @@ async function decisionTraceHook(ctx, input, output) {
3286
2655
  return;
3287
2656
  const base = codebaseDir(ctx.directory);
3288
2657
  try {
3289
- if (!existsSync24(base))
3290
- mkdirSync12(base, { recursive: true });
2658
+ if (!existsSync20(base))
2659
+ mkdirSync9(base, { recursive: true });
3291
2660
  const entry = {
3292
2661
  timestamp: new Date().toISOString(),
3293
2662
  file_path: filePath,
@@ -3299,14 +2668,14 @@ async function decisionTraceHook(ctx, input, output) {
3299
2668
  risk_level: "unknown",
3300
2669
  auto_recorded: true
3301
2670
  };
3302
- appendFileSync4(join22(base, "DECISIONS.jsonl"), JSON.stringify(entry) + `
2671
+ appendFileSync3(join19(base, "DECISIONS.jsonl"), JSON.stringify(entry) + `
3303
2672
  `, "utf-8");
3304
2673
  } catch {}
3305
2674
  }
3306
2675
 
3307
2676
  // src/services/approval-manager.ts
3308
- import { existsSync as existsSync25, readFileSync as readFileSync24, writeFileSync as writeFileSync13, mkdirSync as mkdirSync13 } from "fs";
3309
- 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";
3310
2679
  var APPROVAL_TTL_MS = 30 * 60 * 1000;
3311
2680
  var SENSITIVE_PATTERNS = [
3312
2681
  /auth/i,
@@ -3343,20 +2712,20 @@ function isSensitivePath(filePath) {
3343
2712
  return SENSITIVE_PATTERNS.some((p) => p.test(filePath));
3344
2713
  }
3345
2714
  function approvalsPath(dir) {
3346
- return join23(codebaseDir(dir), "APPROVALS.json");
2715
+ return join20(codebaseDir(dir), "APPROVALS.json");
3347
2716
  }
3348
- function loadStore2(dir) {
2717
+ function loadStore(dir) {
3349
2718
  const p = approvalsPath(dir);
3350
- if (!existsSync25(p))
2719
+ if (!existsSync21(p))
3351
2720
  return { requests: [] };
3352
2721
  try {
3353
- return JSON.parse(readFileSync24(p, "utf-8"));
2722
+ return JSON.parse(readFileSync20(p, "utf-8"));
3354
2723
  } catch {
3355
2724
  return { requests: [] };
3356
2725
  }
3357
2726
  }
3358
2727
  function checkApproval(dir, file_path, command) {
3359
- const store = loadStore2(dir);
2728
+ const store = loadStore(dir);
3360
2729
  const now = Date.now();
3361
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;
3362
2731
  }
@@ -3368,8 +2737,8 @@ async function approvalHook(context, toolInput, output) {
3368
2737
  if (!ENABLED2)
3369
2738
  return;
3370
2739
  const dir = context.directory ?? process.cwd();
3371
- const tool15 = toolInput.name ?? toolInput.tool ?? "";
3372
- if (!WRITE_TOOLS.has(tool15))
2740
+ const tool13 = toolInput.name ?? toolInput.tool ?? "";
2741
+ if (!WRITE_TOOLS.has(tool13))
3373
2742
  return;
3374
2743
  const args = output.args ?? {};
3375
2744
  const filePath = String(args.path ?? args.file_path ?? args.filename ?? "");
@@ -3386,8 +2755,8 @@ async function approvalHook(context, toolInput, output) {
3386
2755
  }
3387
2756
 
3388
2757
  // src/services/event-logger.ts
3389
- import { existsSync as existsSync26, mkdirSync as mkdirSync14, appendFileSync as appendFileSync5, readFileSync as readFileSync25, writeFileSync as writeFileSync14, renameSync, unlinkSync, statSync as statSync2 } from "fs";
3390
- 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";
3391
2760
  var SENSITIVE_KEYS = [
3392
2761
  "password",
3393
2762
  "token",
@@ -3441,7 +2810,7 @@ function isValidDirectory(directory) {
3441
2810
  return false;
3442
2811
  }
3443
2812
  try {
3444
- const stats = statSync2(directory);
2813
+ const stats = statSync(directory);
3445
2814
  return stats.isDirectory();
3446
2815
  } catch {
3447
2816
  return false;
@@ -3452,13 +2821,13 @@ function logEvent(directory, event, log) {
3452
2821
  return;
3453
2822
  if (!isValidDirectory(directory))
3454
2823
  return;
3455
- const logDir = join24(directory, ".opencode");
3456
- const logPath = join24(logDir, "flowdeck-events.jsonl");
2824
+ const logDir = join21(directory, ".opencode");
2825
+ const logPath = join21(logDir, "flowdeck-events.jsonl");
3457
2826
  try {
3458
- if (!existsSync26(logDir)) {
3459
- mkdirSync14(logDir, { recursive: true });
2827
+ if (!existsSync22(logDir)) {
2828
+ mkdirSync11(logDir, { recursive: true });
3460
2829
  }
3461
- appendFileSync5(logPath, JSON.stringify(event) + `
2830
+ appendFileSync4(logPath, JSON.stringify(event) + `
3462
2831
  `, "utf-8");
3463
2832
  rotateLogFile(logPath);
3464
2833
  if (log) {
@@ -3468,17 +2837,17 @@ function logEvent(directory, event, log) {
3468
2837
  }
3469
2838
  function rotateLogFile(logPath) {
3470
2839
  try {
3471
- const stats = statSync2(logPath);
2840
+ const stats = statSync(logPath);
3472
2841
  if (stats.size < 5000)
3473
2842
  return;
3474
- const content = readFileSync25(logPath, "utf-8");
2843
+ const content = readFileSync21(logPath, "utf-8");
3475
2844
  const lines = content.split(`
3476
2845
  `).filter((l) => l.trim());
3477
2846
  if (lines.length > 1000) {
3478
2847
  const backupPath = logPath + ".backup";
3479
2848
  renameSync(logPath, backupPath);
3480
2849
  const keep = lines.slice(-1000);
3481
- writeFileSync14(logPath, keep.join(`
2850
+ writeFileSync12(logPath, keep.join(`
3482
2851
  `) + `
3483
2852
  `, "utf-8");
3484
2853
  try {
@@ -3502,8 +2871,6 @@ function formatEventForStderr(event) {
3502
2871
  icon = "\uD83D\uDD0D";
3503
2872
  else if (event.tool === "bash" || event.tool === "shell")
3504
2873
  icon = "\uD83C\uDFC3";
3505
- else if (event.tool === "delegate")
3506
- icon = "\uD83E\uDD16";
3507
2874
  else
3508
2875
  icon = "\uD83D\uDD27";
3509
2876
  const argStr = formatArgs(event.args);
@@ -3531,10 +2898,6 @@ function formatEventForStderr(event) {
3531
2898
  const error = event.error ? ` error: ${event.error}` : "";
3532
2899
  return `${dim}[${time}]${reset} ${icon} ${cyan}${agent}${reset} → ${event.tool}(${argStr})${statusColor}${duration}${error}${reset}`;
3533
2900
  }
3534
- case "agent.delegated": {
3535
- const thinking = event.thinking ? ` "${event.thinking}"` : "";
3536
- return `${dim}[${time}]${reset} \uD83E\uDD16 ${cyan}${agent}${reset} → delegate(${thinking})`;
3537
- }
3538
2901
  case "session.created":
3539
2902
  return `${dim}[${time}]${reset} \uD83D\uDCC2 session created${event.session_id ? ` (${event.session_id})` : ""}`;
3540
2903
  case "session.idle":
@@ -3673,10 +3036,6 @@ function extractAgentFromEvent(props) {
3673
3036
  return props.agent;
3674
3037
  if (typeof props.name === "string")
3675
3038
  return props.name;
3676
- const title = typeof props.title === "string" ? props.title : "";
3677
- const match = title.match(/^(.+)-delegate$/);
3678
- if (match)
3679
- return match[1];
3680
3039
  return "unknown";
3681
3040
  }
3682
3041
 
@@ -3731,15 +3090,15 @@ function createContextWindowMonitorHook() {
3731
3090
  }
3732
3091
 
3733
3092
  // src/hooks/shell-env-hook.ts
3734
- import { existsSync as existsSync27, readFileSync as readFileSync26 } from "fs";
3735
- import { join as join25 } from "path";
3736
- 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";
3737
3096
  var _version;
3738
3097
  function getVersion() {
3739
3098
  if (_version)
3740
3099
  return _version;
3741
3100
  try {
3742
- const require2 = createRequire2(import.meta.url);
3101
+ const require2 = createRequire(import.meta.url);
3743
3102
  const pkg = require2("../../package.json");
3744
3103
  _version = pkg.version ?? "0.0.0";
3745
3104
  } catch {
@@ -3768,7 +3127,7 @@ var MARKER_TO_LANG = {
3768
3127
  };
3769
3128
  function detectPackageManager(root) {
3770
3129
  for (const [lockfile, pm] of Object.entries(LOCKFILE_TO_PM)) {
3771
- if (existsSync27(join25(root, lockfile)))
3130
+ if (existsSync23(join22(root, lockfile)))
3772
3131
  return pm;
3773
3132
  }
3774
3133
  return;
@@ -3777,7 +3136,7 @@ function detectLanguages(root) {
3777
3136
  const langs = [];
3778
3137
  const seen = new Set;
3779
3138
  for (const [marker, lang] of Object.entries(MARKER_TO_LANG)) {
3780
- if (!seen.has(lang) && existsSync27(join25(root, marker))) {
3139
+ if (!seen.has(lang) && existsSync23(join22(root, marker))) {
3781
3140
  langs.push(lang);
3782
3141
  seen.add(lang);
3783
3142
  }
@@ -3785,11 +3144,11 @@ function detectLanguages(root) {
3785
3144
  return langs;
3786
3145
  }
3787
3146
  function readCurrentPhase(root) {
3788
- const statePath2 = join25(root, ".planning", "STATE.md");
3789
- if (!existsSync27(statePath2))
3147
+ const statePath2 = join22(root, ".planning", "STATE.md");
3148
+ if (!existsSync23(statePath2))
3790
3149
  return;
3791
3150
  try {
3792
- const content = readFileSync26(statePath2, "utf-8");
3151
+ const content = readFileSync22(statePath2, "utf-8");
3793
3152
  const match = content.match(/phase:\s*(\S+)/i);
3794
3153
  return match?.[1];
3795
3154
  } catch {
@@ -3914,8 +3273,8 @@ function createSessionIdleHook(client, tracker) {
3914
3273
  }
3915
3274
 
3916
3275
  // src/hooks/compaction-hook.ts
3917
- import { existsSync as existsSync28, readFileSync as readFileSync27 } from "fs";
3918
- import { join as join26 } from "path";
3276
+ import { existsSync as existsSync24, readFileSync as readFileSync23 } from "fs";
3277
+ import { join as join23 } from "path";
3919
3278
  var STRUCTURED_SUMMARY_PROMPT = `
3920
3279
  When summarizing this session, you MUST include the following sections:
3921
3280
 
@@ -3956,10 +3315,10 @@ For each: agent name, status, description, session_id.
3956
3315
  var _lastInjected = new Map;
3957
3316
  function readPlanningState2(directory) {
3958
3317
  const sp = statePath(directory);
3959
- if (!existsSync28(sp))
3318
+ if (!existsSync24(sp))
3960
3319
  return null;
3961
3320
  try {
3962
- const content = readFileSync27(sp, "utf-8");
3321
+ const content = readFileSync23(sp, "utf-8");
3963
3322
  const parsed = parseState(content);
3964
3323
  const version = typeof parsed.summaryVersion === "number" ? parsed.summaryVersion : 0;
3965
3324
  return { content: content.slice(0, 1500), version };
@@ -3988,15 +3347,15 @@ function createCompactionHook(ctx, tracker) {
3988
3347
  sections.push(`_State unchanged since last compaction. summaryVersion=${currentStateVersion}_`);
3989
3348
  sections.push("");
3990
3349
  }
3991
- const indexPath2 = join26(ctx.directory, ".planning", "CODEBASE_INDEX.md");
3992
- if (indexChanged && existsSync28(indexPath2)) {
3350
+ const indexPath2 = join23(ctx.directory, ".planning", "CODEBASE_INDEX.md");
3351
+ if (indexChanged && existsSync24(indexPath2)) {
3993
3352
  try {
3994
- const indexContent = readFileSync27(indexPath2, "utf-8");
3353
+ const indexContent = readFileSync23(indexPath2, "utf-8");
3995
3354
  const indexSummary = "\n## Codebase Index\n```\n" + indexContent.slice(0, 800) + "\n```\n";
3996
3355
  sections.push(indexSummary);
3997
3356
  sections.push("");
3998
3357
  } catch {}
3999
- } else if (existsSync28(indexPath2)) {
3358
+ } else if (existsSync24(indexPath2)) {
4000
3359
  sections.push(`## Codebase Index (unchanged, v${currentIndexVersion})`);
4001
3360
  sections.push(`_Index unchanged since last compaction. summaryVersion=${currentIndexVersion}_`);
4002
3361
  sections.push("");
@@ -4042,9 +3401,6 @@ var BLOCKED_TOOLS = new Set([
4042
3401
  "shell"
4043
3402
  ]);
4044
3403
  var ALWAYS_ALLOWED = new Set([
4045
- "delegate",
4046
- "run-pipeline",
4047
- "council",
4048
3404
  "planning-state",
4049
3405
  "codebase-state",
4050
3406
  "repo-memory",
@@ -4052,9 +3408,6 @@ var ALWAYS_ALLOWED = new Set([
4052
3408
  "policy-engine",
4053
3409
  "reflect"
4054
3410
  ]);
4055
- function isDelegationTool(name) {
4056
- return ALWAYS_ALLOWED.has(name);
4057
- }
4058
3411
  function isBlocked2(name) {
4059
3412
  const norm = name.toLowerCase().replace(/[-_]/g, "");
4060
3413
  for (const b of BLOCKED_TOOLS) {
@@ -4066,15 +3419,15 @@ function isBlocked2(name) {
4066
3419
  function blockMessage(toolName) {
4067
3420
  return `[Orchestrator Guard] The orchestrator cannot use \`${toolName}\` directly.
4068
3421
 
4069
- ` + `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.
4070
3423
 
4071
- ` + `Use the \`delegate\` tool to hand this off:
4072
- ` + ` delegate({ agent: "@backend-coder", prompt: "..." }) — backend code writing / editing
4073
- ` + ` delegate({ agent: "@frontend-coder", prompt: "..." }) — frontend code writing / editing
4074
- ` + ` delegate({ agent: "@devops", prompt: "..." }) — CI/CD, deploy, and infra changes
4075
- ` + ` delegate({ agent: "@mapper", prompt: "..." }) — codebase mapping
4076
- ` + ` delegate({ agent: "@researcher", prompt: "..." }) — research / file analysis
4077
- ` + ` 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
4078
3431
 
4079
3432
  ` + `To enable this guard: set FLOWDECK_ORCHESTRATOR_GUARD=on`;
4080
3433
  }
@@ -4108,7 +3461,7 @@ class OrchestratorGuard {
4108
3461
  return;
4109
3462
  if (sessionId !== this.primarySessionId)
4110
3463
  return;
4111
- if (isDelegationTool(toolName))
3464
+ if (ALWAYS_ALLOWED.has(toolName))
4112
3465
  return;
4113
3466
  if (isBlocked2(toolName)) {
4114
3467
  throw new Error(blockMessage(toolName));
@@ -4244,74 +3597,57 @@ ${customAppendPrompt}`;
4244
3597
  return base;
4245
3598
  }
4246
3599
  // src/agents/orchestrator.ts
4247
- var ORCHESTRATOR_PROMPT = `You coordinate multi-agent execution. You read STATE.md and PLAN.md at startup, delegate work to specialists, and track progress.
4248
-
4249
- ## HARD RULES — Non-Negotiable
4250
-
4251
- **You are a coordinator. You NEVER do implementation work yourself.**
4252
-
4253
- 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.
4254
- 2. **Never write or edit any file.** All file creation, editing, and patching is done by specialist agents. Use \`delegate\` to hand it off.
4255
- 3. **Never run shell commands, tests, or builds.** Delegate to @tester or @build-error-resolver.
4256
- 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.
4257
3601
 
4258
- 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
4259
3603
 
4260
- **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.
4261
3608
 
4262
3609
  ## Startup Behavior
4263
3610
 
4264
- MUST execute at session start:
4265
- 1. Read \`STATE.md\` identify current phase and active plan
4266
- 2. Read the active \`PLAN.md\` identify which steps are complete and which are next
4267
- 3. Check which steps are marked complete
4268
- 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.
4269
3615
 
4270
- 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.
4271
3617
 
4272
3618
  ## Phase Gating
4273
3619
 
4274
- Only orchestrate in the **execute** phase.
3620
+ Read STATE.md to determine the current phase and workflow class.
4275
3621
 
4276
- If the project is in another phase:
4277
- - **discuss** phase: "Run \`/fd-discuss\` to complete requirements gathering first."
4278
- - **plan** phase: "Run \`/fd-plan\` to create the implementation plan first."
4279
- - **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.
4280
3634
 
4281
3635
  ## State-First Read Strategy
4282
3636
 
4283
- Before delegating any agent that needs codebase context:
4284
- 1. Read \`STATE.md\` check \`freshnessStatus\` and \`lastUpdatedAt\`
4285
- 2. Read \`.planning/CODEBASE_INDEX.md\` check \`freshnessStatus\`
4286
- 3. If \`freshnessStatus === "fresh"\` AND needed files exist in \`fileSnapshots\`:
4287
- Use the existing state. Do NOT re-explore the codebase.
4288
- → Log: "[StateManager] Skipped codebase exploration — state is fresh"
4289
- 4. If state is missing, stale, or insufficient:
4290
- → Delegate to @code-explorer with specific question
4291
- → After exploration completes, file-tracker auto-publishes to CODEBASE_INDEX.md
4292
- → Log: "[StateManager] Triggered re-exploration — state was stale"
4293
-
4294
- State becomes **stale** when:
4295
- - \`lastUpdatedAt\` > 5 minutes ago
4296
- - Phase transitions
4297
- - New plan confirmed
4298
- - User runs /fd-checkpoint or /fd-resume
4299
-
4300
- State becomes **fresh** when:
4301
- - Any agent writes to CODEBASE_INDEX.md
4302
- - updatePlanningState() is called
4303
- - 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.
4304
3642
 
4305
3643
  ## Step Execution
4306
3644
 
4307
3645
  For each incomplete step in PLAN.md:
4308
-
4309
- 1. Identify the step's requirements and agent type
4310
- 2. Delegate to the appropriate agent with full context
4311
- 3. Wait for the agent to complete
4312
- 4. Mark the step complete in STATE.md
4313
- 5. Re-read STATE.md to confirm state
4314
- 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.
4315
3651
 
4316
3652
  ## Implementation Routing
4317
3653
 
@@ -4319,84 +3655,78 @@ When a plan step requires implementation, route to a role-specific agent:
4319
3655
  - Use @backend-coder for server, API, business logic, database, and non-UI application code.
4320
3656
  - Use @frontend-coder for UI components, client state, styling, and interaction behavior.
4321
3657
  - Use @devops for CI/CD workflows, deployment, infrastructure, runtime config, and operations scripts.
4322
- - 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.
4323
3659
 
4324
3660
  ## Agent Team
4325
3661
 
4326
- | Agent | Invoke | Best For |
4327
- |-------|--------|----------|
4328
- | Design | @design | Discovery, UX planning, wireframes, visual system, implementation handoff, design fidelity review |
4329
- | Backend Coder | @backend-coder | Backend code implementation |
4330
- | Frontend Coder | @frontend-coder | Frontend code implementation |
4331
- | DevOps | @devops | CI/CD and infrastructure implementation |
4332
- | Researcher | @researcher | API docs, library usage |
4333
- | Tester | @tester | Writing and running tests |
4334
- | Reviewer | @reviewer | Code quality review |
4335
- | Writer | @writer | Documentation |
4336
- | Mapper | @mapper | Codebase mapping to .codebase/ |
4337
- | Architect | @architect | System design, ADRs |
4338
- | Security Auditor | @security-auditor | Security review |
4339
- | Code Explorer | @code-explorer | Reading unfamiliar code |
4340
- | Debug Specialist | @debug-specialist | Root cause analysis |
4341
- | Build Resolver | @build-error-resolver | Build/compile failures |
4342
- | Doc Updater | @doc-updater | Updating existing docs |
4343
- | Task Splitter | @task-splitter | Decomposing complex tasks |
4344
- | Discusser | @discusser | Requirements extraction |
4345
- | Plan Checker | @plan-checker | Plan quality review |
4346
- | Planner | @planner | Feature planning |
4347
- | Build Error Resolver | @build-error-resolver | Build error diagnosis |
4348
- | Performance Optimizer | @performance-optimizer | Performance analysis |
4349
- | Refactor Guide | @refactor-guide | Safe refactoring |
4350
-
4351
- ## Phase State Machine
4352
-
4353
- \`\`\`
4354
- discuss plandesign (for UI-heavy tasks) execute review
4355
- \`\`\`
4356
-
4357
- - **discuss**: Requirements extraction with @discusser
4358
- - **plan**: Plan creation with @planner, review with @plan-checker
4359
- - **design**: UX structure, wireframe/layout planning, and visual system definition with @design
4360
- - **execute**: Implementation with @backend-coder, @frontend-coder, @devops, @tester, and @researcher in parallel where possible, only after approved design handoff for UI-heavy tasks
4361
- - **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.
4362
3705
 
4363
3706
  ## Tracking
4364
3707
 
4365
3708
  After each step completes:
4366
- - Call \`mark_step_complete\` with the step ID
3709
+ - Call mark_step_complete with the step ID
4367
3710
  - Re-read STATE.md to confirm the update
4368
- - Update STATE.md \`current_step\` to the next step
3711
+ - Update STATE.md current_step to the next step
4369
3712
 
4370
3713
  On all steps complete:
4371
- - Update STATE.md \`phase\` to \`review\`
3714
+ - Update STATE.md phase to review
4372
3715
  - Summarize what was delivered
4373
3716
 
4374
3717
  ## Error Recovery
4375
3718
 
4376
- If a delegated agent fails:
4377
- 1. Log the failure with the error message
4378
- 2. Retry once with clarified instructions
4379
- 3. If still failing, escalate:
4380
-
4381
- \`\`\`
4382
- BLOCKED: implementation agent failed on step 3 (add payment endpoint).
4383
- Error: [exact error message]
4384
- Retried once with clarification. Still failing.
4385
-
4386
- Options:
4387
- 1. Skip this step and continue
4388
- 2. Replan step 3 with smaller scope
4389
- 3. Stop and debug manually
4390
-
4391
- Please advise.
4392
- \`\`\`
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.
4393
3723
 
4394
3724
  ## Self-Learning
4395
3725
 
4396
3726
  When a task required unusual human guidance, a novel solution strategy, or exposed a knowledge gap:
4397
- 1. After the task completes successfully, write a new skill markdown file under \`src/skills/<name>/SKILL.md\` to capture the pattern
4398
- 2. Use a descriptive kebab-case name for the directory, a one-sentence description in the frontmatter, and structured Markdown content
4399
- 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.
4400
3730
 
4401
3731
  Do NOT create a skill for routine tasks. Only capture genuinely novel or reusable patterns.`;
4402
3732
  var AGENT_DESCRIPTIONS = {
@@ -4404,114 +3734,118 @@ var AGENT_DESCRIPTIONS = {
4404
3734
  - Role: Runs design-first workflow for user-facing tasks
4405
3735
  - Permissions: Read/write files
4406
3736
  - Best for: UX structure, wireframes, visual direction, tokens, and frontend handoff
4407
- - **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`,
4408
3738
  "backend-coder": `@backend-coder
4409
3739
  - Role: Implements backend features and fixes based on confirmed plans
4410
3740
  - Permissions: Read/write files
4411
3741
  - Best for: API, services, data layer, and business logic
4412
- - **Delegate when:** Backend or server-side implementation work`,
3742
+ - Use when: Backend or server-side implementation work`,
4413
3743
  "frontend-coder": `@frontend-coder
4414
3744
  - Role: Implements frontend features and fixes based on confirmed plans
4415
3745
  - Permissions: Read/write files
4416
3746
  - Best for: UI components, client state, rendering, and interaction behavior
4417
- - **Delegate when:** Frontend implementation work`,
3747
+ - Use when: Frontend implementation work`,
4418
3748
  devops: `@devops
4419
3749
  - Role: Implements DevOps and infrastructure changes based on confirmed plans
4420
3750
  - Permissions: Read/write files
4421
3751
  - Best for: CI/CD, deployment config, infra scripts, and runtime operations
4422
- - **Delegate when:** Infrastructure, pipeline, or operations implementation work`,
3752
+ - Use when: Infrastructure, pipeline, or operations implementation work`,
4423
3753
  researcher: `@researcher
4424
3754
  - Role: Researches documentation, APIs, and best practices
4425
3755
  - Permissions: Read files
4426
3756
  - Stats: 10x better finding up-to-date library docs
4427
- - **Delegate when:** Need API docs, library usage, best practices
4428
- - **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`,
4429
3759
  tester: `@tester
4430
3760
  - Role: Writes and runs tests following TDD principles
4431
3761
  - Permissions: Read/write files
4432
3762
  - Best for: Writing tests before code (TDD), running test suites
4433
- - **Delegate when:** Implementing new features, fixing bugs, test coverage needed`,
3763
+ - Use when: Implementing new features, fixing bugs, or increasing coverage`,
4434
3764
  reviewer: `@reviewer
4435
3765
  - Role: Reviews code for quality, security, and adherence to conventions
4436
3766
  - Permissions: Read files
4437
3767
  - Best for: Code review before PRs
4438
- - **Delegate when:** After writing or modifying code, before opening PRs`,
3768
+ - Use when: After writing or modifying code, before opening PRs`,
4439
3769
  architect: `@architect
4440
3770
  - Role: Designs system architecture, creates ADRs, defines API contracts
4441
3771
  - Permissions: Read files
4442
3772
  - Best for: New modules, API changes, database schema changes, cross-cutting concerns
4443
- - **Delegate when:** Planning new features that need architectural decisions`,
3773
+ - Use when: Planning new features that need architectural decisions`,
4444
3774
  "security-auditor": `@security-auditor
4445
3775
  - Role: Deep security audit of code changes
4446
3776
  - Permissions: Read files
4447
3777
  - Best for: OWASP Top 10, injection vulnerabilities, auth issues
4448
- - **Delegate when:** Before merging security-sensitive code`,
3778
+ - Use when: Before merging security-sensitive code`,
4449
3779
  "code-explorer": `@code-explorer
4450
3780
  - Role: Explores and maps unfamiliar codebases
4451
3781
  - Permissions: Read files
4452
3782
  - Best for: Tracing call paths, building structural models
4453
- - **Delegate when:** Before making changes to unfamiliar code`,
3783
+ - Use when: Before making changes to unfamiliar code`,
4454
3784
  "debug-specialist": `@debug-specialist
4455
3785
  - Role: Diagnoses bugs through systematic root cause analysis
4456
3786
  - Permissions: Read files
4457
3787
  - Best for: Deep investigation before fixing
4458
- - **Delegate when:** Bug needs investigation, not just a quick fix`,
3788
+ - Use when: A bug needs investigation, not just a quick fix`,
4459
3789
  "build-error-resolver": `@build-error-resolver
4460
3790
  - Role: Fixes build errors, compilation failures, dependency issues
4461
3791
  - Permissions: Read/write files
4462
3792
  - Best for: Build failures, type errors, broken dependencies
4463
- - **Delegate when:** Build fails, types error out, dependencies broken`,
3793
+ - Use when: Build fails, types error out, or dependencies break`,
4464
3794
  "doc-updater": `@doc-updater
4465
3795
  - Role: Updates documentation after code changes
4466
3796
  - Permissions: Read/write files
4467
3797
  - Best for: API references, README, inline comments
4468
- - **Delegate when:** Implementation completes and docs need updating`,
3798
+ - Use when: Implementation completes and docs need syncing`,
4469
3799
  writer: `@writer
4470
3800
  - Role: Drafts project documentation
4471
3801
  - Permissions: Read/write files
4472
3802
  - Best for: README, API docs, user guides
4473
- - **Delegate when:** Creating new documentation from scratch`,
3803
+ - Use when: Creating new documentation from scratch`,
4474
3804
  mapper: `@mapper
4475
3805
  - Role: Maps codebase to structured documentation files
4476
3806
  - Permissions: Read/write files
4477
3807
  - Best for: .codebase/ directory documentation
4478
- - **Delegate when:** Need to document existing codebase structure`,
3808
+ - Use when: Need to document existing codebase structure`,
4479
3809
  "plan-checker": `@plan-checker
4480
3810
  - Role: Reviews PLAN.md for quality before execution
4481
3811
  - Permissions: Read files
4482
3812
  - Best for: Plan verification before execution
4483
- - **Delegate when:** PLAN.md needs review before execution`,
3813
+ - Use when: PLAN.md needs review before execution`,
4484
3814
  "task-splitter": `@task-splitter
4485
3815
  - Role: Decomposes complex tasks into parallel workstreams
4486
3816
  - Permissions: Read files
4487
3817
  - Best for: Multi-track work organization
4488
- - **Delegate when:** Complex task needs parallelization`,
3818
+ - Use when: Complex work needs parallelization`,
4489
3819
  discusser: `@discusser
4490
3820
  - Role: Extracts requirements via structured Q&A
4491
3821
  - Permissions: Read/write files
4492
3822
  - Best for: Requirements extraction
4493
- - **Delegate when:** Starting new feature or project phase`,
3823
+ - Use when: Starting a new feature or project phase`,
4494
3824
  planner: `@planner
4495
3825
  - Role: Creates detailed implementation plans
4496
3826
  - Permissions: Read files
4497
3827
  - Best for: Feature planning, step breakdown
4498
- - **Delegate when:** Need implementation plan for feature`,
3828
+ - Use when: Need an implementation plan for a feature`,
4499
3829
  "performance-optimizer": `@performance-optimizer
4500
3830
  - Role: Analyzes and optimizes performance
4501
3831
  - Permissions: Read files
4502
3832
  - Best for: Performance analysis
4503
- - **Delegate when:** Need to optimize slow code`,
3833
+ - Use when: Need to optimize slow code`,
4504
3834
  "refactor-guide": `@refactor-guide
4505
3835
  - Role: Guides safe refactoring
4506
3836
  - Permissions: Read files
4507
3837
  - Best for: Code restructuring
4508
- - **Delegate when:** Need to refactor existing code safely`
3838
+ - Use when: Need to refactor existing code safely`
4509
3839
  };
4510
- function buildOrchestratorPrompt(disabledAgents) {
3840
+ function buildOrchestratorPrompt(disabledAgents, workflowClass) {
4511
3841
  const enabledAgents = Object.entries(AGENT_DESCRIPTIONS).filter(([name]) => !disabledAgents?.has(name)).map(([, desc]) => desc).join(`
4512
3842
 
4513
3843
  `);
4514
- return `${ORCHESTRATOR_PROMPT}
3844
+ const workflowSection = workflowClass ? `
3845
+ ## Current Workflow
3846
+
3847
+ Active workflow class: ${workflowClass}` : "";
3848
+ return `${ORCHESTRATOR_PROMPT}${workflowSection}
4515
3849
 
4516
3850
  <Delegation>
4517
3851
 
@@ -4519,21 +3853,22 @@ function buildOrchestratorPrompt(disabledAgents) {
4519
3853
 
4520
3854
  ${enabledAgents}
4521
3855
 
4522
- ## Delegation Guidelines
3856
+ ## Routing Guidelines
4523
3857
 
4524
3858
  - Review available agents before acting
4525
- - Reference paths/lines, don't paste files (\`src/app.ts:42\`)
4526
- - Provide context summaries, let specialists read what they need
4527
- - 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
4528
3863
 
4529
3864
  </Delegation>`;
4530
3865
  }
4531
- function createOrchestratorAgent(model, customPrompt, customAppendPrompt, disabledAgents) {
4532
- const basePrompt = buildOrchestratorPrompt(disabledAgents);
3866
+ function createOrchestratorAgent(model, customPrompt, customAppendPrompt, disabledAgents, workflowClass) {
3867
+ const basePrompt = buildOrchestratorPrompt(disabledAgents, workflowClass);
4533
3868
  const prompt = resolvePrompt(basePrompt, customPrompt, customAppendPrompt);
4534
3869
  const definition = {
4535
3870
  name: "orchestrator",
4536
- 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",
4537
3872
  config: {
4538
3873
  temperature: 0.1,
4539
3874
  prompt
@@ -7349,612 +6684,11 @@ function getAgentConfigs(agentModels) {
7349
6684
  return configs;
7350
6685
  }
7351
6686
 
7352
- // src/services/agent-contract-registry.ts
7353
- var CONTRACTS = [
7354
- {
7355
- agent: "orchestrator",
7356
- role: "Coordinate multi-agent execution. Delegates all work — never implements directly.",
7357
- allowedTaskTypes: ["orchestration", "coordination", "delegation", "phase-management"],
7358
- requiredInputs: ["STATE.md", "PLAN.md"],
7359
- expectedOutputFields: ["delegated_steps", "completed_steps", "current_phase"],
7360
- allowedTools: [
7361
- "delegate",
7362
- "run-pipeline",
7363
- "council",
7364
- "planning-state",
7365
- "codebase-state",
7366
- "repo-memory",
7367
- "decision-trace",
7368
- "policy-engine",
7369
- "reflect"
7370
- ],
7371
- forbiddenActions: [
7372
- "write_file",
7373
- "edit_file",
7374
- "create_file",
7375
- "bash",
7376
- "patch",
7377
- "apply_patch",
7378
- "read source files directly"
7379
- ],
7380
- escalationConditions: [
7381
- "delegated agent fails twice",
7382
- "delegation budget exhausted",
7383
- "deadlock detected",
7384
- "all agents blocked on the same step"
7385
- ],
7386
- stopConditions: [
7387
- "all PLAN.md steps completed",
7388
- "user requests stop",
7389
- "budget exceeded with no fallback"
7390
- ],
7391
- successCriteria: [
7392
- "all plan steps delegated and completed",
7393
- "STATE.md phase updated to review",
7394
- "no implementation performed directly by orchestrator"
7395
- ]
7396
- },
7397
- {
7398
- agent: "planner",
7399
- role: "Create detailed implementation plans. Output PLAN.md with numbered steps.",
7400
- allowedTaskTypes: ["planning", "task-breakdown", "step-decomposition"],
7401
- requiredInputs: ["task description or STATE.md"],
7402
- expectedOutputFields: ["steps", "phase"],
7403
- allowedTools: ["read", "glob", "grep", "planning-state"],
7404
- forbiddenActions: [
7405
- "write source files",
7406
- "run bash commands",
7407
- "edit application code",
7408
- "implement features"
7409
- ],
7410
- escalationConditions: [
7411
- "requirements are ambiguous",
7412
- "dependencies between steps unclear",
7413
- "conflicting constraints"
7414
- ],
7415
- stopConditions: ["PLAN.md written and reviewed by plan-checker", "user confirms plan"],
7416
- successCriteria: [
7417
- "PLAN.md contains numbered steps with assigned agents",
7418
- "each step has clear success criteria",
7419
- "no implementation performed"
7420
- ]
7421
- },
7422
- {
7423
- agent: "plan-checker",
7424
- role: "Review PLAN.md quality before execution. Read-only.",
7425
- allowedTaskTypes: ["plan-review", "quality-check"],
7426
- requiredInputs: ["PLAN.md"],
7427
- expectedOutputFields: ["verdict", "issues", "recommendations"],
7428
- allowedTools: ["read", "glob", "grep"],
7429
- forbiddenActions: ["write or edit any files", "modify PLAN.md"],
7430
- escalationConditions: ["plan is fundamentally flawed", "critical gaps found"],
7431
- stopConditions: ["review complete", "verdict issued"],
7432
- successCriteria: ["structured review output", "no file modifications"]
7433
- },
7434
- {
7435
- agent: "design",
7436
- role: "Design UX, wireframes, and visual systems for UI-heavy tasks.",
7437
- allowedTaskTypes: ["ux-design", "wireframe", "visual-system", "design-handoff", "frontend-handoff"],
7438
- requiredInputs: ["task description", "requirements"],
7439
- expectedOutputFields: ["design_stage", "wireframes", "component_structure", "design_tokens"],
7440
- allowedTools: ["read", "write", "glob", "grep", "planning-state"],
7441
- forbiddenActions: [
7442
- "run bash commands",
7443
- "write application logic",
7444
- "implement backend code",
7445
- "implement React components"
7446
- ],
7447
- escalationConditions: [
7448
- "design requirements unclear",
7449
- "conflicting UX requirements",
7450
- "brand guidelines missing"
7451
- ],
7452
- stopConditions: ["design_stage=handoff_complete", "design_approved=true"],
7453
- successCriteria: [
7454
- "design document written",
7455
- "design_stage set to handoff_complete",
7456
- "design_approved set to true",
7457
- "no application code written"
7458
- ]
7459
- },
7460
- {
7461
- agent: "backend-coder",
7462
- role: "Implement backend features: API, services, data layer, business logic.",
7463
- allowedTaskTypes: ["implementation", "backend", "api", "database", "service", "bugfix"],
7464
- requiredInputs: ["PLAN.md step description", "relevant context files"],
7465
- expectedOutputFields: ["files_modified", "summary"],
7466
- allowedTools: ["read", "write", "edit", "bash", "glob", "grep"],
7467
- forbiddenActions: [
7468
- "modify frontend UI component files",
7469
- "change CI/CD config without devops involvement"
7470
- ],
7471
- escalationConditions: [
7472
- "architecture decision needed",
7473
- "security-sensitive change without audit",
7474
- "database migration required"
7475
- ],
7476
- stopConditions: ["step implementation complete", "tests pass", "reviewer approves"],
7477
- successCriteria: [
7478
- "code written per plan step",
7479
- "no regressions introduced",
7480
- "tests exist or updated"
7481
- ]
7482
- },
7483
- {
7484
- agent: "frontend-coder",
7485
- role: "Implement frontend features: UI components, client state, rendering.",
7486
- allowedTaskTypes: ["implementation", "frontend", "ui", "component", "styling", "bugfix"],
7487
- requiredInputs: ["PLAN.md step description", "design handoff for UI-heavy tasks"],
7488
- expectedOutputFields: ["files_modified", "summary"],
7489
- allowedTools: ["read", "write", "edit", "bash", "glob", "grep"],
7490
- forbiddenActions: [
7491
- "modify backend API files",
7492
- "change server configuration",
7493
- "implement without approved design for UI-heavy tasks"
7494
- ],
7495
- escalationConditions: [
7496
- "design handoff missing for UI-heavy task",
7497
- "component library or design system unclear"
7498
- ],
7499
- stopConditions: ["step implementation complete", "tests pass", "reviewer approves"],
7500
- successCriteria: [
7501
- "components implemented per approved design",
7502
- "no regressions introduced",
7503
- "tests exist or updated"
7504
- ]
7505
- },
7506
- {
7507
- agent: "devops",
7508
- role: "Implement DevOps and infrastructure changes: CI/CD, deployment, infra scripts.",
7509
- allowedTaskTypes: ["implementation", "ci-cd", "deployment", "infrastructure", "operations"],
7510
- requiredInputs: ["PLAN.md step description"],
7511
- expectedOutputFields: ["files_modified", "summary"],
7512
- allowedTools: ["read", "write", "edit", "bash", "glob", "grep"],
7513
- forbiddenActions: [
7514
- "modify application source code",
7515
- "deploy to production without approval"
7516
- ],
7517
- escalationConditions: [
7518
- "production deployment requires approval",
7519
- "destructive infra change"
7520
- ],
7521
- stopConditions: ["pipeline or infra change complete", "reviewer approves"],
7522
- successCriteria: ["infrastructure code written per plan", "no prod deployment without approval"]
7523
- },
7524
- {
7525
- agent: "tester",
7526
- role: "Write and run tests following TDD principles. Tests before implementation.",
7527
- allowedTaskTypes: ["testing", "tdd", "regression", "integration-test", "unit-test"],
7528
- requiredInputs: ["feature or step description", "relevant source files"],
7529
- expectedOutputFields: ["test_files_written", "tests_passing", "coverage_summary"],
7530
- allowedTools: ["read", "write", "edit", "bash", "glob", "grep"],
7531
- forbiddenActions: [
7532
- "delete failing tests to make suite pass",
7533
- "implement application features",
7534
- "skip TDD cycle (red → green → refactor)"
7535
- ],
7536
- escalationConditions: [
7537
- "test infrastructure broken",
7538
- "flaky tests blocking all progress"
7539
- ],
7540
- stopConditions: ["all tests pass", "coverage meets threshold"],
7541
- successCriteria: [
7542
- "tests written before implementation",
7543
- "all new tests pass",
7544
- "no test deletions to fix failures"
7545
- ]
7546
- },
7547
- {
7548
- agent: "reviewer",
7549
- role: "Review code quality, security, and convention adherence. Read-only.",
7550
- allowedTaskTypes: ["review", "code-review", "quality-check"],
7551
- requiredInputs: ["files to review", "context of changes"],
7552
- expectedOutputFields: ["verdict", "issues", "recommendations"],
7553
- allowedTools: ["read", "glob", "grep"],
7554
- forbiddenActions: [
7555
- "write or edit any files",
7556
- "make code changes",
7557
- "approve security-sensitive changes without security audit"
7558
- ],
7559
- escalationConditions: [
7560
- "security issues found",
7561
- "critical bugs found",
7562
- "architectural violations"
7563
- ],
7564
- stopConditions: ["review complete", "verdict issued"],
7565
- successCriteria: [
7566
- "structured review output with severity levels",
7567
- "issues categorized",
7568
- "no file modifications"
7569
- ]
7570
- },
7571
- {
7572
- agent: "security-auditor",
7573
- role: "Security audit: OWASP Top 10, injection, auth vulnerabilities. Read-only.",
7574
- allowedTaskTypes: ["security-audit", "vulnerability-scan", "auth-review"],
7575
- requiredInputs: ["files to audit", "change context"],
7576
- expectedOutputFields: ["findings", "severity_breakdown", "recommendations"],
7577
- allowedTools: ["read", "glob", "grep"],
7578
- forbiddenActions: [
7579
- "write or edit files",
7580
- "make changes to fix vulnerabilities directly"
7581
- ],
7582
- escalationConditions: [
7583
- "CRITICAL vulnerability found",
7584
- "auth bypass detected",
7585
- "data exposure found"
7586
- ],
7587
- stopConditions: ["audit complete", "all findings documented"],
7588
- successCriteria: [
7589
- "OWASP checklist evaluated",
7590
- "findings documented with severity levels",
7591
- "no file modifications"
7592
- ]
7593
- },
7594
- {
7595
- agent: "researcher",
7596
- role: "Research documentation, APIs, best practices. Read-only analysis.",
7597
- allowedTaskTypes: ["research", "api-lookup", "documentation", "best-practices"],
7598
- requiredInputs: ["research topic or question"],
7599
- expectedOutputFields: ["findings", "references", "recommendations"],
7600
- allowedTools: ["read", "glob", "grep", "web-search"],
7601
- forbiddenActions: ["write or edit files", "implement solutions"],
7602
- escalationConditions: [
7603
- "critical information unavailable",
7604
- "conflicting official documentation"
7605
- ],
7606
- stopConditions: ["research question answered", "findings documented"],
7607
- successCriteria: [
7608
- "findings clearly summarized",
7609
- "sources cited",
7610
- "no file modifications"
7611
- ]
7612
- },
7613
- {
7614
- agent: "architect",
7615
- role: "Design system architecture, create ADRs, define API contracts.",
7616
- allowedTaskTypes: ["architecture", "adr", "api-design", "system-design"],
7617
- requiredInputs: ["feature or system description", "existing codebase context"],
7618
- expectedOutputFields: ["architecture_document", "adr", "api_contracts"],
7619
- allowedTools: ["read", "write", "glob", "grep", "planning-state"],
7620
- forbiddenActions: ["write application code", "run bash commands"],
7621
- escalationConditions: [
7622
- "major architectural conflict with existing system",
7623
- "breaking API change required"
7624
- ],
7625
- stopConditions: ["ADR written", "architecture reviewed"],
7626
- successCriteria: [
7627
- "architecture documented with tradeoffs",
7628
- "no application code written"
7629
- ]
7630
- },
7631
- {
7632
- agent: "writer",
7633
- role: "Draft project documentation: README, API docs, user guides.",
7634
- allowedTaskTypes: ["documentation", "readme", "api-docs", "user-guide"],
7635
- requiredInputs: ["feature description or codebase context"],
7636
- expectedOutputFields: ["documentation_files"],
7637
- allowedTools: ["read", "write", "edit", "glob", "grep"],
7638
- forbiddenActions: ["modify application code", "run bash commands"],
7639
- escalationConditions: ["documentation scope unclear"],
7640
- stopConditions: ["docs written", "user confirms completeness"],
7641
- successCriteria: [
7642
- "documentation written and accurate",
7643
- "no application code changed"
7644
- ]
7645
- },
7646
- {
7647
- agent: "doc-updater",
7648
- role: "Update existing documentation after code changes.",
7649
- allowedTaskTypes: ["documentation-update", "doc-sync"],
7650
- requiredInputs: ["changed files", "change summary"],
7651
- expectedOutputFields: ["updated_docs"],
7652
- allowedTools: ["read", "write", "edit", "glob", "grep"],
7653
- forbiddenActions: [
7654
- "modify application code",
7655
- "delete documentation without replacement"
7656
- ],
7657
- escalationConditions: ["documentation conflicts with implementation"],
7658
- stopConditions: ["docs updated and synced"],
7659
- successCriteria: ["docs reflect current code", "no application code changed"]
7660
- },
7661
- {
7662
- agent: "supervisor",
7663
- role: "Governance review layer. Inspects existing commands/agents, validates policy, returns structured approve/revise/block/escalate decision. Never creates new commands or workflows.",
7664
- allowedTaskTypes: ["governance-review", "policy-check", "pre-execution-review", "post-stage-review"],
7665
- requiredInputs: ["target name (command or agent)", "task context"],
7666
- expectedOutputFields: ["decision", "targetType", "targetName", "exists", "reasons", "missingRequirements", "riskFlags", "requiredChanges", "approvalStatus", "confidenceScore"],
7667
- allowedTools: ["read", "glob", "grep", "planning-state", "policy-engine"],
7668
- forbiddenActions: [
7669
- "create new commands",
7670
- "create new workflows",
7671
- "invent new agent names",
7672
- "modify command intent",
7673
- "replace orchestrator",
7674
- "become second dispatcher",
7675
- "execute implementation tasks",
7676
- "write or edit source files",
7677
- "run bash commands",
7678
- "modify PLAN.md or STATE.md"
7679
- ],
7680
- escalationConditions: [
7681
- "human approval required and not granted",
7682
- "confidence below threshold",
7683
- "critical policy violation with no safe path forward"
7684
- ],
7685
- stopConditions: ["structured decision issued", "review complete"],
7686
- successCriteria: [
7687
- "structured SupervisorDecision returned",
7688
- "no new commands or workflows created",
7689
- "existing registry not modified",
7690
- "decision is one of: approve, revise, block, escalate"
7691
- ]
7692
- }
7693
- ];
7694
- var REGISTRY = new Map(CONTRACTS.map((c) => [c.agent, c]));
7695
- function getContract(agent) {
7696
- return REGISTRY.get(agent) ?? null;
7697
- }
7698
-
7699
- // src/services/supervisor-binding.ts
7700
- var REGISTERED_COMMANDS = [
7701
- "fd-ask",
7702
- "fd-checkpoint",
7703
- "fd-deploy-check",
7704
- "fd-design",
7705
- "fd-discuss",
7706
- "fd-doctor",
7707
- "fd-execute",
7708
- "fd-fix-bug",
7709
- "fd-map-codebase",
7710
- "fd-multi-repo",
7711
- "fd-new-feature",
7712
- "fd-plan",
7713
- "fd-quick",
7714
- "fd-reflect",
7715
- "fd-resume",
7716
- "fd-status",
7717
- "fd-suggest",
7718
- "fd-translate-intent",
7719
- "fd-verify",
7720
- "fd-write-docs",
7721
- "fd-done"
7722
- ];
7723
- function resolveSupervisorConfig(directory) {
7724
- try {
7725
- const config = loadFlowDeckConfig(directory);
7726
- const sup = config?.governance?.supervisor ?? {};
7727
- return {
7728
- enabled: sup.enabled ?? false,
7729
- mode: sup.mode ?? "advisory",
7730
- reviewedTargets: sup.reviewedTargets ?? [],
7731
- canBlock: sup.canBlock ?? true,
7732
- confidenceThreshold: sup.confidenceThreshold ?? 0.7,
7733
- postExecutionReview: sup.postExecutionReview ?? false
7734
- };
7735
- } catch {
7736
- return {
7737
- enabled: false,
7738
- mode: "advisory",
7739
- reviewedTargets: [],
7740
- canBlock: true,
7741
- confidenceThreshold: 0.7,
7742
- postExecutionReview: false
7743
- };
7744
- }
7745
- }
7746
- function isRegisteredCommand(name) {
7747
- return REGISTERED_COMMANDS.includes(name);
7748
- }
7749
- function isRegisteredAgent(name) {
7750
- return AGENT_NAMES.includes(name);
7751
- }
7752
- function isRegisteredTarget(name) {
7753
- if (isRegisteredCommand(name))
7754
- return { exists: true, type: "command" };
7755
- if (isRegisteredAgent(name))
7756
- return { exists: true, type: "agent" };
7757
- return { exists: false, type: "agent" };
7758
- }
7759
- function checkCommandPolicy(commandName, ctx) {
7760
- const reasons = [];
7761
- const riskFlags = [];
7762
- const missingRequirements = [];
7763
- const requiredChanges = [];
7764
- if (commandName === "fd-new-feature" || commandName === "fd-execute") {
7765
- const taskLower = (ctx.taskDescription ?? "").toLowerCase();
7766
- const isUiHeavy = /landing page|dashboard|admin panel|website|web app|ui|ux|interface|frontend|component/.test(taskLower);
7767
- if (isUiHeavy && ctx.currentPhase === "execute" && ctx.designApprovalPresent === false) {
7768
- missingRequirements.push("design approval (design stage must complete before execute for UI-heavy tasks)");
7769
- riskFlags.push("UI-heavy task entering execute phase without design approval");
7770
- requiredChanges.push("Run /fd-design first and obtain design approval before proceeding to execute");
7771
- }
7772
- }
7773
- if (commandName === "fd-fix-bug") {
7774
- if (ctx.regressionTestPresent === false) {
7775
- missingRequirements.push("regression test (required before bugfix implementation)");
7776
- riskFlags.push("Bugfix command invoked without a regression test");
7777
- requiredChanges.push("Write a failing regression test before implementing the fix");
7778
- }
7779
- }
7780
- if (commandName === "fd-deploy-check") {
7781
- if (ctx.prerequisitesMet === false && ctx.missingInputs && ctx.missingInputs.length > 0) {
7782
- missingRequirements.push(...ctx.missingInputs);
7783
- riskFlags.push("Deploy check attempted with unmet prerequisites");
7784
- }
7785
- }
7786
- if (commandName === "fd-execute" && ctx.currentPhase && ctx.currentPhase !== "execute") {
7787
- riskFlags.push(`fd-execute invoked in phase "${ctx.currentPhase}" instead of "execute"`);
7788
- requiredChanges.push(`Ensure project phase is "execute" before running fd-execute (currently: ${ctx.currentPhase})`);
7789
- }
7790
- if (ctx.approvalRequired && !ctx.approvalGranted) {
7791
- missingRequirements.push("human approval (required for this command)");
7792
- riskFlags.push("Approval gate not satisfied");
7793
- requiredChanges.push("Obtain explicit human approval before proceeding");
7794
- }
7795
- const passed = missingRequirements.length === 0 && riskFlags.length === 0 && requiredChanges.length === 0;
7796
- if (passed) {
7797
- reasons.push(`Command "${commandName}" passed all policy checks`);
7798
- }
7799
- return { passed, reasons, riskFlags, missingRequirements, requiredChanges };
7800
- }
7801
- function checkAgentPolicy(agentName, ctx) {
7802
- const reasons = [];
7803
- const riskFlags = [];
7804
- const missingRequirements = [];
7805
- const requiredChanges = [];
7806
- const contract = getContract(agentName);
7807
- if (!contract) {
7808
- riskFlags.push(`Agent "${agentName}" has no registered capability contract`);
7809
- return { passed: false, reasons, riskFlags, missingRequirements, requiredChanges };
7810
- }
7811
- if (ctx.missingInputs && ctx.missingInputs.length > 0) {
7812
- for (const missing of ctx.missingInputs) {
7813
- const isRequired = contract.requiredInputs.some((r) => r.toLowerCase().includes(missing.toLowerCase()) || missing.toLowerCase().includes(r.toLowerCase()));
7814
- if (isRequired) {
7815
- missingRequirements.push(missing);
7816
- requiredChanges.push(`Provide "${missing}" before delegating to ${agentName}`);
7817
- }
7818
- }
7819
- }
7820
- if (ctx.approvalRequired && !ctx.approvalGranted) {
7821
- const needsApproval = contract.escalationConditions.some((c) => c.toLowerCase().includes("approval") || c.toLowerCase().includes("approve"));
7822
- if (needsApproval) {
7823
- missingRequirements.push("human approval");
7824
- riskFlags.push(`Agent "${agentName}" requires approval via escalation condition`);
7825
- requiredChanges.push("Obtain explicit human approval before proceeding");
7826
- }
7827
- }
7828
- if (agentName === "design" || agentName === "frontend-coder") {
7829
- const taskLower = (ctx.taskDescription ?? "").toLowerCase();
7830
- const isUiHeavy = /landing page|dashboard|admin panel|website|web app|ui|ux|interface|frontend|component/.test(taskLower);
7831
- if (agentName === "frontend-coder" && isUiHeavy && ctx.designApprovalPresent === false) {
7832
- missingRequirements.push("design handoff approval");
7833
- riskFlags.push("frontend-coder invoked for UI-heavy task without approved design handoff");
7834
- requiredChanges.push("Complete design stage and obtain design approval before delegating to frontend-coder");
7835
- }
7836
- }
7837
- const passed = missingRequirements.length === 0 && riskFlags.length === 0;
7838
- if (passed) {
7839
- reasons.push(`Agent "${agentName}" passed all policy checks`);
7840
- }
7841
- return { passed, reasons, riskFlags, missingRequirements, requiredChanges };
7842
- }
7843
- function computeConfidence(exists, policyResult, ctx) {
7844
- if (!exists)
7845
- return 0;
7846
- if (policyResult.riskFlags.length >= 3)
7847
- return 0.2;
7848
- if (policyResult.riskFlags.length === 2)
7849
- return 0.4;
7850
- if (policyResult.riskFlags.length === 1)
7851
- return 0.6;
7852
- if (policyResult.missingRequirements.length > 0)
7853
- return 0.5;
7854
- if (ctx.prerequisitesMet === false)
7855
- return 0.45;
7856
- return 0.95;
7857
- }
7858
- function resolveDecision(exists, policyResult, confidenceScore, threshold, ctx, clarificationQuestion) {
7859
- if (!exists) {
7860
- return { decision: "block", approvalStatus: "denied" };
7861
- }
7862
- if (ctx.approvalRequired && !ctx.approvalGranted) {
7863
- return { decision: "escalate", approvalStatus: "escalated", clarificationQuestion };
7864
- }
7865
- if (!policyResult.passed) {
7866
- if (policyResult.requiredChanges.length > 0) {
7867
- return { decision: "revise", approvalStatus: "pending" };
7868
- }
7869
- return { decision: "block", approvalStatus: "denied" };
7870
- }
7871
- if (confidenceScore < threshold) {
7872
- return { decision: "escalate", approvalStatus: "escalated", clarificationQuestion };
7873
- }
7874
- return { decision: "approve", approvalStatus: "approved" };
7875
- }
7876
- function runSupervisorReview(directory, targetName, ctx = {}, clarificationQuestion) {
7877
- const config = resolveSupervisorConfig(directory);
7878
- const reviewPhase = ctx.reviewPhase ?? "preflight";
7879
- const timestamp2 = new Date().toISOString();
7880
- if (config.reviewedTargets.length > 0 && !config.reviewedTargets.includes(targetName)) {
7881
- return {
7882
- decision: "approve",
7883
- targetType: "agent",
7884
- targetName,
7885
- exists: true,
7886
- reasons: [`Target "${targetName}" is not in the reviewed targets list — auto-approved`],
7887
- missingRequirements: [],
7888
- riskFlags: [],
7889
- requiredChanges: [],
7890
- approvalStatus: "approved",
7891
- confidenceScore: 1,
7892
- reviewPhase,
7893
- timestamp: timestamp2
7894
- };
7895
- }
7896
- const { exists, type: targetType } = isRegisteredTarget(targetName);
7897
- if (!exists) {
7898
- const decision2 = {
7899
- decision: "block",
7900
- targetType,
7901
- targetName,
7902
- exists: false,
7903
- reasons: [
7904
- `Target "${targetName}" is not registered in the FlowDeck command or agent registry.`,
7905
- "The supervisor does not create new commands or workflows.",
7906
- "Only registered targets can be executed."
7907
- ],
7908
- missingRequirements: [],
7909
- riskFlags: [`Unregistered target: "${targetName}"`],
7910
- requiredChanges: [
7911
- `Use one of the registered commands: ${REGISTERED_COMMANDS.join(", ")}`,
7912
- `Or use one of the registered agents: ${AGENT_NAMES.join(", ")}`
7913
- ],
7914
- approvalStatus: "denied",
7915
- confidenceScore: 0,
7916
- reviewPhase,
7917
- timestamp: timestamp2
7918
- };
7919
- return decision2;
7920
- }
7921
- const policyResult = targetType === "command" ? checkCommandPolicy(targetName, ctx) : checkAgentPolicy(targetName, ctx);
7922
- const confidenceScore = computeConfidence(exists, policyResult, ctx);
7923
- const { decision, approvalStatus, clarificationQuestion: escalationQuestion } = resolveDecision(exists, policyResult, confidenceScore, config.confidenceThreshold, ctx, clarificationQuestion);
7924
- const reasons = policyResult.reasons.length > 0 ? policyResult.reasons : decision === "approve" ? [`Target "${targetName}" reviewed and approved for execution`] : [`Target "${targetName}" reviewed — decision: ${decision}`];
7925
- const supervisorDecision = {
7926
- decision,
7927
- targetType,
7928
- targetName,
7929
- exists,
7930
- reasons,
7931
- missingRequirements: policyResult.missingRequirements,
7932
- riskFlags: policyResult.riskFlags,
7933
- requiredChanges: policyResult.requiredChanges,
7934
- approvalStatus,
7935
- confidenceScore,
7936
- reviewPhase,
7937
- timestamp: timestamp2,
7938
- ...escalationQuestion ? { clarificationQuestion: escalationQuestion } : {}
7939
- };
7940
- return supervisorDecision;
7941
- }
7942
- function shouldProceed(decision, mode, canBlock) {
7943
- if (!decision.exists)
7944
- return false;
7945
- if (!canBlock)
7946
- return true;
7947
- if (mode === "strict") {
7948
- return decision.decision === "approve" || decision.decision === "revise";
7949
- }
7950
- return decision.decision !== "block" || decision.confidenceScore > 0.3;
7951
- }
7952
-
7953
6687
  // src/index.ts
7954
6688
  function lazyLoadRulePaths(projectRoot) {
7955
6689
  const __dir = dirname3(fileURLToPath2(import.meta.url));
7956
- const rulesDir = join27(__dir, "..", "src", "rules");
7957
- if (!existsSync29(rulesDir))
6690
+ const rulesDir = join24(__dir, "..", "src", "rules");
6691
+ if (!existsSync25(rulesDir))
7958
6692
  return { paths: [], diagnostics: "[LazyRuleLoader] rules directory not found" };
7959
6693
  const detectedLanguages = detectProjectLanguages(projectRoot);
7960
6694
  const paths = getStartupRulePaths(rulesDir, detectedLanguages);
@@ -7964,16 +6698,16 @@ function lazyLoadRulePaths(projectRoot) {
7964
6698
  }
7965
6699
  function loadCommands() {
7966
6700
  const __dir = dirname3(fileURLToPath2(import.meta.url));
7967
- const commandsDir = join27(__dir, "..", "src", "commands");
7968
- if (!existsSync29(commandsDir))
6701
+ const commandsDir = join24(__dir, "..", "src", "commands");
6702
+ if (!existsSync25(commandsDir))
7969
6703
  return {};
7970
6704
  const commands = {};
7971
6705
  try {
7972
- for (const file of readdirSync4(commandsDir)) {
6706
+ for (const file of readdirSync3(commandsDir)) {
7973
6707
  if (!file.endsWith(".md"))
7974
6708
  continue;
7975
6709
  const name = basename2(file, ".md");
7976
- const raw = readFileSync28(join27(commandsDir, file), "utf-8");
6710
+ const raw = readFileSync24(join24(commandsDir, file), "utf-8");
7977
6711
  let description;
7978
6712
  let template = raw;
7979
6713
  const fmMatch = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/);
@@ -7991,8 +6725,6 @@ function loadCommands() {
7991
6725
  var plugin = async (input, _options) => {
7992
6726
  const { directory, client, worktree } = input;
7993
6727
  const appLog = (msg) => client.app.log({ body: { service: "flowdeck", level: "info", message: msg } }).catch(() => {});
7994
- const runPipelineTool = createRunPipelineTool(client);
7995
- const delegateTool = createDelegateTool(client);
7996
6728
  const councilTool = createCouncilTool(client);
7997
6729
  const fileTracker = new SessionFileTracker;
7998
6730
  const { fileEdited, fileWatcherUpdated } = createFileTrackerHooks(fileTracker);
@@ -8057,8 +6789,8 @@ var plugin = async (input, _options) => {
8057
6789
  }
8058
6790
  }
8059
6791
  }
8060
- const skillsDir = join27(dirname3(fileURLToPath2(import.meta.url)), "..", "src", "skills");
8061
- if (existsSync29(skillsDir)) {
6792
+ const skillsDir = join24(dirname3(fileURLToPath2(import.meta.url)), "..", "src", "skills");
6793
+ if (existsSync25(skillsDir)) {
8062
6794
  const cfgAny = cfg;
8063
6795
  if (!cfgAny.skills || typeof cfgAny.skills !== "object") {
8064
6796
  cfgAny.skills = { paths: [] };
@@ -8087,8 +6819,6 @@ var plugin = async (input, _options) => {
8087
6819
  tool: {
8088
6820
  "planning-state": planningStateTool,
8089
6821
  "codebase-state": codebaseStateTool,
8090
- "run-pipeline": runPipelineTool,
8091
- delegate: delegateTool,
8092
6822
  "repo-memory": repoMemoryTool,
8093
6823
  "failure-replay": failureReplayTool,
8094
6824
  "decision-trace": decisionTraceTool,
@@ -8162,33 +6892,6 @@ var plugin = async (input, _options) => {
8162
6892
  }
8163
6893
  }
8164
6894
  orchestratorGuard.check(toolInput.sessionID ?? "", toolInput.tool ?? toolInput.name ?? "");
8165
- const toolName = toolInput.tool ?? toolInput.name ?? "";
8166
- if (toolName === "delegate" || toolName === "run-pipeline") {
8167
- const supConfig = resolveSupervisorConfig(directory);
8168
- if (supConfig.enabled) {
8169
- const args = toolOutput?.args ?? toolInput?.args ?? {};
8170
- const agentTarget = typeof args.agent === "string" ? args.agent.replace(/^@/, "") : Array.isArray(args.steps) && args.steps[0]?.agent ? String(args.steps[0].agent).replace(/^@/, "") : "";
8171
- if (agentTarget) {
8172
- const decision = runSupervisorReview(directory, agentTarget, {
8173
- taskDescription: typeof args.prompt === "string" ? args.prompt : undefined,
8174
- reviewPhase: "preflight",
8175
- session_id: toolInput.sessionID ?? toolInput.sessionId ?? ""
8176
- });
8177
- const proceed = shouldProceed(decision, supConfig.mode, supConfig.canBlock);
8178
- 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("; ")}]` : ""}`);
8179
- if (!proceed) {
8180
- const summary = [
8181
- `[Supervisor] Execution blocked for target "${decision.targetName}".`,
8182
- ...decision.reasons,
8183
- ...decision.missingRequirements.length > 0 ? [`Missing: ${decision.missingRequirements.join(", ")}`] : [],
8184
- ...decision.requiredChanges.length > 0 ? [`Required changes: ${decision.requiredChanges.join("; ")}`] : []
8185
- ].join(`
8186
- `);
8187
- throw new Error(summary);
8188
- }
8189
- }
8190
- }
8191
- }
8192
6895
  await approvalHook({ directory }, toolInput, toolOutput);
8193
6896
  await guardRailsHook({ directory }, toolInput, toolOutput);
8194
6897
  await toolGuardHook({ directory }, toolInput, toolOutput);
@@ -8198,30 +6901,6 @@ var plugin = async (input, _options) => {
8198
6901
  },
8199
6902
  "tool.execute.after": async (toolInput, toolOutput) => {
8200
6903
  await eventLog.after({ directory }, toolInput, toolOutput);
8201
- const afterToolName = toolInput.tool ?? toolInput.name ?? "";
8202
- if (afterToolName === "delegate" || afterToolName === "run-pipeline") {
8203
- try {
8204
- const supConfig = resolveSupervisorConfig(directory);
8205
- if (supConfig.enabled && supConfig.postExecutionReview) {
8206
- const args = toolOutput?.args ?? toolInput?.args ?? {};
8207
- const agentTarget = typeof args.agent === "string" ? args.agent.replace(/^@/, "") : Array.isArray(args.steps) && args.steps[0]?.agent ? String(args.steps[0].agent).replace(/^@/, "") : "";
8208
- if (agentTarget) {
8209
- const executionErrored = toolOutput?.error != null || toolOutput?.status === "error" || typeof toolOutput?.output === "string" && toolOutput.output.startsWith("Error:");
8210
- const decision = runSupervisorReview(directory, agentTarget, {
8211
- taskDescription: typeof args.prompt === "string" ? args.prompt : undefined,
8212
- reviewPhase: "post-stage",
8213
- session_id: toolInput.sessionID ?? toolInput.sessionId ?? "",
8214
- prerequisitesMet: !executionErrored
8215
- });
8216
- const logLevel = decision.decision === "block" || decision.decision === "escalate" ? "[Supervisor][WARN]" : "[Supervisor]";
8217
- 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("; ")}]` : ""}`);
8218
- if (supConfig.mode === "strict" && !shouldProceed(decision, "strict", supConfig.canBlock)) {
8219
- appLog(`[Supervisor][STRICT] Post-execution governance violation detected for "${decision.targetName}". ` + `Review the scorecard and telemetry for this run. ` + `Reasons: ${decision.reasons.join("; ")}`);
8220
- }
8221
- }
8222
- }
8223
- } catch {}
8224
- }
8225
6904
  await contextMonitor["tool.execute.after"](toolInput, toolOutput);
8226
6905
  }
8227
6906
  };