@dv.nghiem/flowdeck 0.4.7 → 0.4.9

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