@dv.nghiem/flowdeck 0.4.6 → 0.4.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -1
- package/dist/agents/orchestrator.d.ts +2 -5
- package/dist/agents/orchestrator.d.ts.map +1 -1
- package/dist/config/schema.d.ts +1 -15
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/dashboard/server.mjs +14 -1
- package/dist/hooks/orchestrator-guard-hook.d.ts +5 -15
- package/dist/hooks/orchestrator-guard-hook.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +617 -1938
- package/dist/{tools/dispatch-routing.d.ts → lib/task-routing.d.ts} +1 -3
- package/dist/lib/task-routing.d.ts.map +1 -0
- package/dist/services/agent-contract-registry.d.ts.map +1 -1
- package/dist/services/agent-performance.d.ts +1 -1
- package/dist/services/agent-performance.d.ts.map +1 -1
- package/dist/services/cost-estimator.d.ts +0 -88
- package/dist/services/cost-estimator.d.ts.map +1 -1
- package/dist/services/event-logger.d.ts +1 -1
- package/dist/services/event-logger.d.ts.map +1 -1
- package/dist/services/quick-router.d.ts +24 -0
- package/dist/services/quick-router.d.ts.map +1 -1
- package/dist/services/supervisor-binding.d.ts +6 -0
- package/dist/services/supervisor-binding.d.ts.map +1 -1
- package/dist/services/workflow-router.d.ts +57 -0
- package/dist/services/workflow-router.d.ts.map +1 -0
- package/dist/services/workflow-scorecard.d.ts.map +1 -1
- package/dist/tools/planning-state-lib.d.ts +23 -0
- package/dist/tools/planning-state-lib.d.ts.map +1 -1
- package/docs/concepts/workflows.md +48 -0
- package/docs/index.md +15 -2
- package/docs/reference/workflow-router.md +337 -0
- package/package.json +1 -1
- package/src/commands/fd-discuss.md +8 -1
- package/src/commands/fd-execute.md +25 -3
- package/src/commands/fd-new-feature.md +97 -4
- package/src/commands/fd-plan.md +14 -4
- package/src/commands/fd-quick.md +43 -16
- package/src/rules/common/agent-orchestration.md +52 -18
- package/src/skills/agent-harness-construction/SKILL.md +5 -5
- package/dist/services/delegation-budget.d.ts +0 -54
- package/dist/services/delegation-budget.d.ts.map +0 -1
- package/dist/services/prompt-cache.d.ts +0 -61
- package/dist/services/prompt-cache.d.ts.map +0 -1
- package/dist/services/token-metrics.d.ts +0 -97
- package/dist/services/token-metrics.d.ts.map +0 -1
- package/dist/tools/delegate.d.ts +0 -4
- package/dist/tools/delegate.d.ts.map +0 -1
- package/dist/tools/dispatch-routing.d.ts.map +0 -1
- package/dist/tools/run-pipeline.d.ts +0 -4
- 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
|
|
6
|
-
import { join as
|
|
2
|
+
import { readFileSync as readFileSync24, readdirSync as readdirSync3, existsSync as existsSync25 } from "fs";
|
|
3
|
+
import { join as join24, basename as basename2 } from "path";
|
|
7
4
|
import { dirname as dirname3 } from "path";
|
|
8
5
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
9
6
|
|
|
@@ -281,6 +278,14 @@ function parseState(content) {
|
|
|
281
278
|
result[key] = value === "true";
|
|
282
279
|
} else if (key === "requires_design_first" || key === "design_approved" || key === "design_override") {
|
|
283
280
|
result[key] = value === "true";
|
|
281
|
+
} else if (key === "skippedStages") {
|
|
282
|
+
result[key] = value.replace(/[\[\]]/g, "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
283
|
+
} else if (key === "escalationHistory" || key === "routingScores") {
|
|
284
|
+
try {
|
|
285
|
+
result[key] = JSON.parse(value);
|
|
286
|
+
} catch {
|
|
287
|
+
result[key] = undefined;
|
|
288
|
+
}
|
|
284
289
|
} else if (value !== "" && !isNaN(Number(value)) && key !== "plan_file" && key !== "confirmed_at") {
|
|
285
290
|
result[key] = Number(value);
|
|
286
291
|
} else {
|
|
@@ -352,7 +357,12 @@ function readPlanningState(dir) {
|
|
|
352
357
|
lastUpdatedBy: parsed.lastUpdatedBy || "",
|
|
353
358
|
lastUpdatedPhase: parsed.lastUpdatedPhase || 1,
|
|
354
359
|
summaryVersion: parsed.summaryVersion || 0,
|
|
355
|
-
freshnessStatus: parsed.freshnessStatus || "unknown"
|
|
360
|
+
freshnessStatus: parsed.freshnessStatus || "unknown",
|
|
361
|
+
workflowClass: parsed.workflowClass || undefined,
|
|
362
|
+
skippedStages: parsed.skippedStages || undefined,
|
|
363
|
+
escalationHistory: parsed.escalationHistory || undefined,
|
|
364
|
+
routingScores: parsed.routingScores || undefined,
|
|
365
|
+
routingReason: parsed.routingReason || undefined
|
|
356
366
|
};
|
|
357
367
|
}
|
|
358
368
|
function parseTDDState(parsed) {
|
|
@@ -569,879 +579,54 @@ ${entry}`, "utf-8");
|
|
|
569
579
|
}
|
|
570
580
|
});
|
|
571
581
|
|
|
572
|
-
// src/tools/
|
|
582
|
+
// src/tools/repo-memory.ts
|
|
573
583
|
import { tool as tool3 } from "@opencode-ai/plugin";
|
|
574
|
-
|
|
575
|
-
// src/services/agent-performance.ts
|
|
576
|
-
import { existsSync as existsSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync4, mkdirSync as mkdirSync2 } from "fs";
|
|
584
|
+
import { readFileSync as readFileSync5, writeFileSync as writeFileSync4, existsSync as existsSync5, mkdirSync as mkdirSync2 } from "fs";
|
|
577
585
|
import { join as join5 } from "path";
|
|
578
|
-
function perfPath(dir) {
|
|
579
|
-
return join5(codebaseDir(dir), "AGENT_PERF.json");
|
|
580
|
-
}
|
|
581
|
-
function loadStore(dir) {
|
|
582
|
-
const p = perfPath(dir);
|
|
583
|
-
if (!existsSync5(p))
|
|
584
|
-
return { entries: [], updated_at: new Date().toISOString() };
|
|
585
|
-
try {
|
|
586
|
-
return JSON.parse(readFileSync5(p, "utf-8"));
|
|
587
|
-
} catch {
|
|
588
|
-
return { entries: [], updated_at: new Date().toISOString() };
|
|
589
|
-
}
|
|
590
|
-
}
|
|
591
|
-
function saveStore(dir, store) {
|
|
592
|
-
const cd = codebaseDir(dir);
|
|
593
|
-
if (!existsSync5(cd))
|
|
594
|
-
mkdirSync2(cd, { recursive: true });
|
|
595
|
-
writeFileSync4(perfPath(dir), JSON.stringify(store, null, 2), "utf-8");
|
|
596
|
-
}
|
|
597
|
-
function makeKey(agent, model, task_type) {
|
|
598
|
-
return `${agent}::${model}::${task_type}`;
|
|
599
|
-
}
|
|
600
|
-
function recordRun(dir, agent, model, task_type, success, duration_ms, cost = 0) {
|
|
601
|
-
const store = loadStore(dir);
|
|
602
|
-
const key = makeKey(agent, model, task_type);
|
|
603
|
-
const existing = store.entries.find((e) => makeKey(e.agent, e.model, e.task_type) === key);
|
|
604
|
-
if (existing) {
|
|
605
|
-
existing.runs++;
|
|
606
|
-
if (success)
|
|
607
|
-
existing.successes++;
|
|
608
|
-
else
|
|
609
|
-
existing.failures++;
|
|
610
|
-
existing.total_duration_ms += duration_ms;
|
|
611
|
-
existing.total_cost += cost;
|
|
612
|
-
existing.last_run = new Date().toISOString();
|
|
613
|
-
existing.last_status = success ? "success" : "failure";
|
|
614
|
-
} else {
|
|
615
|
-
store.entries.push({
|
|
616
|
-
agent,
|
|
617
|
-
model,
|
|
618
|
-
task_type,
|
|
619
|
-
runs: 1,
|
|
620
|
-
successes: success ? 1 : 0,
|
|
621
|
-
failures: success ? 0 : 1,
|
|
622
|
-
total_duration_ms: duration_ms,
|
|
623
|
-
total_cost: cost,
|
|
624
|
-
last_run: new Date().toISOString(),
|
|
625
|
-
last_status: success ? "success" : "failure"
|
|
626
|
-
});
|
|
627
|
-
}
|
|
628
|
-
store.updated_at = new Date().toISOString();
|
|
629
|
-
saveStore(dir, store);
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
// src/tools/dispatch-routing.ts
|
|
633
|
-
function shouldRetry(promptRes) {
|
|
634
|
-
if (!promptRes)
|
|
635
|
-
return false;
|
|
636
|
-
const detail = promptRes.error?.detail;
|
|
637
|
-
if (isTransientError(detail))
|
|
638
|
-
return true;
|
|
639
|
-
const infoError = promptRes.data?.info?.error;
|
|
640
|
-
const text = typeof infoError === "string" ? infoError : JSON.stringify(infoError ?? "");
|
|
641
|
-
return isTransientError(text);
|
|
642
|
-
}
|
|
643
|
-
function isTransientError(text) {
|
|
644
|
-
if (!text)
|
|
645
|
-
return false;
|
|
646
|
-
const haystack = text.toLowerCase();
|
|
647
|
-
return haystack.includes("overload") || haystack.includes("rate limit") || haystack.includes("timeout") || haystack.includes("temporar") || haystack.includes("econnreset");
|
|
648
|
-
}
|
|
649
|
-
function normalizeTaskType(taskType, agent) {
|
|
650
|
-
const normalized = (taskType ?? "").trim().toLowerCase();
|
|
651
|
-
if (isTaskType(normalized))
|
|
652
|
-
return normalized;
|
|
653
|
-
const a = agent.toLowerCase();
|
|
654
|
-
if (a.includes("design") || a.includes("ui-ux"))
|
|
655
|
-
return "design";
|
|
656
|
-
if (a.includes("review"))
|
|
657
|
-
return "review";
|
|
658
|
-
if (a.includes("test"))
|
|
659
|
-
return "testing";
|
|
660
|
-
if (a.includes("debug"))
|
|
661
|
-
return "debugging";
|
|
662
|
-
if (a.includes("security"))
|
|
663
|
-
return "security";
|
|
664
|
-
if (a.includes("doc"))
|
|
665
|
-
return "documentation";
|
|
666
|
-
if (a.includes("architect") || a.includes("planner"))
|
|
667
|
-
return "planning";
|
|
668
|
-
if (a.includes("orchestrator") || a.includes("coordinator"))
|
|
669
|
-
return "orchestration";
|
|
670
|
-
if (a.includes("analyst") || a.includes("research"))
|
|
671
|
-
return "analysis";
|
|
672
|
-
return "implementation";
|
|
673
|
-
}
|
|
674
|
-
function isTaskType(value) {
|
|
675
|
-
return value === "planning" || value === "design" || value === "implementation" || value === "debugging" || value === "review" || value === "testing" || value === "documentation" || value === "analysis" || value === "security" || value === "orchestration";
|
|
676
|
-
}
|
|
677
|
-
var UI_HEAVY_KEYWORDS = [
|
|
678
|
-
"landing page",
|
|
679
|
-
"marketing site",
|
|
680
|
-
"website",
|
|
681
|
-
"web app",
|
|
682
|
-
"mobile app",
|
|
683
|
-
"app screen",
|
|
684
|
-
"dashboard",
|
|
685
|
-
"admin panel",
|
|
686
|
-
"settings page",
|
|
687
|
-
"onboarding ux",
|
|
688
|
-
"kanban",
|
|
689
|
-
"design system",
|
|
690
|
-
"responsive",
|
|
691
|
-
"ui",
|
|
692
|
-
"ux",
|
|
693
|
-
"cta",
|
|
694
|
-
"conversion flow",
|
|
695
|
-
"saas interface",
|
|
696
|
-
"user-facing"
|
|
697
|
-
];
|
|
698
|
-
var NON_UI_KEYWORDS = [
|
|
699
|
-
"backend",
|
|
700
|
-
"infrastructure",
|
|
701
|
-
"migration",
|
|
702
|
-
"pipeline",
|
|
703
|
-
"api only",
|
|
704
|
-
"database only",
|
|
705
|
-
"cli",
|
|
706
|
-
"worker"
|
|
707
|
-
];
|
|
708
|
-
function isUiHeavyTask(input) {
|
|
709
|
-
const normalized = input.trim().toLowerCase();
|
|
710
|
-
if (!normalized)
|
|
711
|
-
return false;
|
|
712
|
-
const hasUiSignal = UI_HEAVY_KEYWORDS.some((keyword) => normalized.includes(keyword));
|
|
713
|
-
if (!hasUiSignal)
|
|
714
|
-
return false;
|
|
715
|
-
const hasOnlyNonUiSignals = NON_UI_KEYWORDS.some((keyword) => normalized.includes(keyword)) && !normalized.includes("frontend");
|
|
716
|
-
return !hasOnlyNonUiSignals;
|
|
717
|
-
}
|
|
718
|
-
|
|
719
|
-
// src/tools/run-pipeline.ts
|
|
720
|
-
function extractText(parts) {
|
|
721
|
-
return parts.filter((p) => p.type === "text" && typeof p.text === "string").map((p) => p.text).join(`
|
|
722
|
-
`);
|
|
723
|
-
}
|
|
724
|
-
function createRunPipelineTool(client) {
|
|
725
|
-
return tool3({
|
|
726
|
-
description: "Run agents in sequential pipeline. Each step's output is appended to the next step's context. One fresh child session per step. Returns full trace with session ID, input/output/duration per step.",
|
|
727
|
-
args: {
|
|
728
|
-
steps: tool3.schema.array(tool3.schema.object({
|
|
729
|
-
agent: tool3.schema.string(),
|
|
730
|
-
prompt: tool3.schema.string(),
|
|
731
|
-
task_type: tool3.schema.string().optional()
|
|
732
|
-
})),
|
|
733
|
-
initial_context: tool3.schema.string().optional(),
|
|
734
|
-
abort_on_failure: tool3.schema.boolean().optional().default(true),
|
|
735
|
-
retry_attempts: tool3.schema.number().optional().default(1),
|
|
736
|
-
max_carry_chars: tool3.schema.number().optional()
|
|
737
|
-
},
|
|
738
|
-
async execute(args, context) {
|
|
739
|
-
const startTime = Date.now();
|
|
740
|
-
const trace = [];
|
|
741
|
-
let carryContext = args.initial_context ?? "";
|
|
742
|
-
let aborted = false;
|
|
743
|
-
const retryAttempts = typeof args.retry_attempts === "number" ? args.retry_attempts : 1;
|
|
744
|
-
const maxRetries = Math.max(0, Math.floor(retryAttempts));
|
|
745
|
-
const totalSteps = args.steps.length;
|
|
746
|
-
let inflightChildId = null;
|
|
747
|
-
const abortHandler = () => {
|
|
748
|
-
if (inflightChildId) {
|
|
749
|
-
client.session.abort({
|
|
750
|
-
path: { id: inflightChildId },
|
|
751
|
-
query: { directory: context.directory }
|
|
752
|
-
}).catch(() => {});
|
|
753
|
-
}
|
|
754
|
-
};
|
|
755
|
-
context.abort.addEventListener("abort", abortHandler);
|
|
756
|
-
try {
|
|
757
|
-
for (let stepIdx = 0;stepIdx < args.steps.length; stepIdx++) {
|
|
758
|
-
const step = args.steps[stepIdx];
|
|
759
|
-
if (context.abort.aborted) {
|
|
760
|
-
aborted = true;
|
|
761
|
-
break;
|
|
762
|
-
}
|
|
763
|
-
const stepStart = Date.now();
|
|
764
|
-
const taskType = normalizeTaskType(step.task_type, step.agent);
|
|
765
|
-
const stepInput = carryContext ? `${carryContext}
|
|
766
|
-
|
|
767
|
-
---
|
|
768
|
-
|
|
769
|
-
${step.prompt}` : step.prompt;
|
|
770
|
-
const createRes = await client.session.create({
|
|
771
|
-
body: { parentID: context.sessionID, title: `${step.agent}-pipeline` },
|
|
772
|
-
query: { directory: context.directory }
|
|
773
|
-
});
|
|
774
|
-
if (createRes.error || !createRes.data?.id) {
|
|
775
|
-
const errMsg = `Failed to create session: ${createRes.error?.detail ?? "unknown"}`;
|
|
776
|
-
trace.push({ agent: step.agent, task_type: taskType, model: "", input: stepInput, output: errMsg, duration_ms: Date.now() - stepStart, success: false });
|
|
777
|
-
aborted = true;
|
|
778
|
-
break;
|
|
779
|
-
}
|
|
780
|
-
inflightChildId = createRes.data.id;
|
|
781
|
-
let promptRes = null;
|
|
782
|
-
let retriesUsed = 0;
|
|
783
|
-
for (let attempt = 0;attempt <= maxRetries; attempt++) {
|
|
784
|
-
promptRes = await client.session.prompt({
|
|
785
|
-
path: { id: inflightChildId },
|
|
786
|
-
body: {
|
|
787
|
-
agent: step.agent,
|
|
788
|
-
parts: [{ type: "text", text: stepInput }],
|
|
789
|
-
tools: { question: false }
|
|
790
|
-
},
|
|
791
|
-
query: { directory: context.directory }
|
|
792
|
-
});
|
|
793
|
-
if (!shouldRetry(promptRes) || attempt === maxRetries)
|
|
794
|
-
break;
|
|
795
|
-
retriesUsed++;
|
|
796
|
-
}
|
|
797
|
-
inflightChildId = null;
|
|
798
|
-
if (context.abort.aborted) {
|
|
799
|
-
aborted = true;
|
|
800
|
-
break;
|
|
801
|
-
}
|
|
802
|
-
if (!promptRes || promptRes.error) {
|
|
803
|
-
const errMsg = `Prompt failed: ${promptRes?.error?.detail ?? "unknown"}`;
|
|
804
|
-
trace.push({ agent: step.agent, session_id: createRes.data.id, task_type: taskType, model: "", input: stepInput, output: `${errMsg}${retriesUsed > 0 ? ` (retries: ${retriesUsed})` : ""}`, duration_ms: Date.now() - stepStart, success: false });
|
|
805
|
-
recordRun(context.directory, step.agent, "", taskType, false, Date.now() - stepStart);
|
|
806
|
-
if (args.abort_on_failure) {
|
|
807
|
-
aborted = true;
|
|
808
|
-
break;
|
|
809
|
-
}
|
|
810
|
-
continue;
|
|
811
|
-
}
|
|
812
|
-
const info = promptRes.data?.info;
|
|
813
|
-
if (info?.error) {
|
|
814
|
-
const errMsg = `Agent error: ${JSON.stringify(info.error)}`;
|
|
815
|
-
trace.push({ agent: step.agent, session_id: createRes.data.id, task_type: taskType, model: "", input: stepInput, output: `${errMsg}${retriesUsed > 0 ? ` (retries: ${retriesUsed})` : ""}`, duration_ms: Date.now() - stepStart, success: false });
|
|
816
|
-
recordRun(context.directory, step.agent, "", taskType, false, Date.now() - stepStart);
|
|
817
|
-
if (args.abort_on_failure) {
|
|
818
|
-
aborted = true;
|
|
819
|
-
break;
|
|
820
|
-
}
|
|
821
|
-
continue;
|
|
822
|
-
}
|
|
823
|
-
const output = extractText(promptRes.data?.parts ?? []);
|
|
824
|
-
trace.push({ agent: step.agent, session_id: createRes.data.id, task_type: taskType, model: "", input: stepInput, output: output || "(no text output)", duration_ms: Date.now() - stepStart, success: true, context_chars: carryContext.length });
|
|
825
|
-
recordRun(context.directory, step.agent, "", taskType, true, Date.now() - stepStart);
|
|
826
|
-
const rawOutput = output || "";
|
|
827
|
-
carryContext = typeof args.max_carry_chars === "number" && rawOutput.length > args.max_carry_chars ? rawOutput.slice(rawOutput.length - args.max_carry_chars) : rawOutput;
|
|
828
|
-
}
|
|
829
|
-
} finally {
|
|
830
|
-
context.abort.removeEventListener("abort", abortHandler);
|
|
831
|
-
}
|
|
832
|
-
const totalDuration = Date.now() - startTime;
|
|
833
|
-
return JSON.stringify({
|
|
834
|
-
steps: trace,
|
|
835
|
-
total_duration_ms: totalDuration,
|
|
836
|
-
aborted
|
|
837
|
-
});
|
|
838
|
-
}
|
|
839
|
-
});
|
|
840
|
-
}
|
|
841
|
-
|
|
842
|
-
// src/tools/delegate.ts
|
|
843
|
-
import { tool as tool4 } from "@opencode-ai/plugin";
|
|
844
|
-
import { existsSync as existsSync10, readFileSync as readFileSync10 } from "fs";
|
|
845
|
-
|
|
846
|
-
// src/services/prompt-cache.ts
|
|
847
|
-
import { createHash } from "crypto";
|
|
848
|
-
import { existsSync as existsSync6, readFileSync as readFileSync6, writeFileSync as writeFileSync5, readdirSync as readdirSync3, statSync, mkdirSync as mkdirSync3 } from "fs";
|
|
849
|
-
import { join as join6 } from "path";
|
|
850
|
-
var CACHEABLE_AGENTS = new Set([
|
|
851
|
-
"researcher",
|
|
852
|
-
"code-explorer",
|
|
853
|
-
"reviewer",
|
|
854
|
-
"plan-checker",
|
|
855
|
-
"security-auditor",
|
|
856
|
-
"question-guard",
|
|
857
|
-
"quick-router"
|
|
858
|
-
]);
|
|
859
|
-
var CACHE_DIR_NAME = "prompt-cache";
|
|
860
|
-
var MAX_CACHE_ENTRIES = 200;
|
|
861
|
-
var DEFAULT_TTL_MS = 30 * 60 * 1000;
|
|
862
|
-
function cacheDir(dir) {
|
|
863
|
-
return join6(codebaseDir(dir), CACHE_DIR_NAME);
|
|
864
|
-
}
|
|
865
|
-
function entryPath(dir, key) {
|
|
866
|
-
return join6(cacheDir(dir), `${key}.json`);
|
|
867
|
-
}
|
|
868
|
-
function readEntry(dir, key, stateVersion, indexVersion) {
|
|
869
|
-
const path = entryPath(dir, key);
|
|
870
|
-
if (!existsSync6(path))
|
|
871
|
-
return null;
|
|
872
|
-
try {
|
|
873
|
-
const entry = JSON.parse(readFileSync6(path, "utf-8"));
|
|
874
|
-
const age = Date.now() - new Date(entry.created_at).getTime();
|
|
875
|
-
if (age > entry.ttl_ms)
|
|
876
|
-
return null;
|
|
877
|
-
if (entry.state_version !== stateVersion || entry.index_version !== indexVersion)
|
|
878
|
-
return null;
|
|
879
|
-
return entry.response;
|
|
880
|
-
} catch {
|
|
881
|
-
return null;
|
|
882
|
-
}
|
|
883
|
-
}
|
|
884
|
-
function hashKey(agent, prompt, context, stateVersion, indexVersion) {
|
|
885
|
-
const raw = JSON.stringify({ agent, prompt: prompt.trim(), context: context.trim(), stateVersion, indexVersion });
|
|
886
|
-
return createHash("sha256").update(raw).digest("hex").slice(0, 32);
|
|
887
|
-
}
|
|
888
|
-
function normalizeForCache(text) {
|
|
889
|
-
return text.replace(/\s+/g, " ").trim();
|
|
890
|
-
}
|
|
891
|
-
function hashKeyNormalized(agent, prompt, context, stateVersion, indexVersion) {
|
|
892
|
-
const normalized = JSON.stringify({
|
|
893
|
-
agent,
|
|
894
|
-
prompt: normalizeForCache(prompt),
|
|
895
|
-
context: normalizeForCache(context),
|
|
896
|
-
stateVersion,
|
|
897
|
-
indexVersion
|
|
898
|
-
});
|
|
899
|
-
return createHash("sha256").update(normalized).digest("hex").slice(0, 32);
|
|
900
|
-
}
|
|
901
|
-
function getCached(dir, agent, prompt, context, stateVersion, indexVersion, safe_to_cache = false) {
|
|
902
|
-
if (!safe_to_cache)
|
|
903
|
-
return null;
|
|
904
|
-
if (!CACHEABLE_AGENTS.has(agent))
|
|
905
|
-
return null;
|
|
906
|
-
const exactKey = hashKey(agent, prompt, context, stateVersion, indexVersion);
|
|
907
|
-
const exactResult = readEntry(dir, exactKey, stateVersion, indexVersion);
|
|
908
|
-
if (exactResult !== null)
|
|
909
|
-
return exactResult;
|
|
910
|
-
const normKey = hashKeyNormalized(agent, prompt, context, stateVersion, indexVersion);
|
|
911
|
-
if (normKey === exactKey)
|
|
912
|
-
return null;
|
|
913
|
-
return readEntry(dir, normKey, stateVersion, indexVersion);
|
|
914
|
-
}
|
|
915
|
-
function setCached(dir, agent, prompt, context, stateVersion, indexVersion, response, safe_to_cache = false, ttl_ms = DEFAULT_TTL_MS) {
|
|
916
|
-
if (!safe_to_cache)
|
|
917
|
-
return;
|
|
918
|
-
if (!CACHEABLE_AGENTS.has(agent))
|
|
919
|
-
return;
|
|
920
|
-
const cd = cacheDir(dir);
|
|
921
|
-
if (!existsSync6(cd))
|
|
922
|
-
mkdirSync3(cd, { recursive: true });
|
|
923
|
-
const key = hashKey(agent, prompt, context, stateVersion, indexVersion);
|
|
924
|
-
const entry = {
|
|
925
|
-
key,
|
|
926
|
-
agent,
|
|
927
|
-
state_version: stateVersion,
|
|
928
|
-
index_version: indexVersion,
|
|
929
|
-
created_at: new Date().toISOString(),
|
|
930
|
-
ttl_ms,
|
|
931
|
-
response
|
|
932
|
-
};
|
|
933
|
-
writeFileSync5(entryPath(dir, key), JSON.stringify(entry, null, 2), "utf-8");
|
|
934
|
-
pruneExpired(dir);
|
|
935
|
-
}
|
|
936
|
-
function pruneExpired(dir) {
|
|
937
|
-
const cd = cacheDir(dir);
|
|
938
|
-
if (!existsSync6(cd))
|
|
939
|
-
return;
|
|
940
|
-
try {
|
|
941
|
-
const files = readdirSync3(cd).filter((f) => f.endsWith(".json"));
|
|
942
|
-
const now = Date.now();
|
|
943
|
-
const entries = [];
|
|
944
|
-
for (const f of files) {
|
|
945
|
-
const p = join6(cd, f);
|
|
946
|
-
try {
|
|
947
|
-
const entry = JSON.parse(readFileSync6(p, "utf-8"));
|
|
948
|
-
const age = now - new Date(entry.created_at).getTime();
|
|
949
|
-
entries.push({ path: p, created_at: new Date(entry.created_at).getTime(), expired: age > entry.ttl_ms });
|
|
950
|
-
} catch {
|
|
951
|
-
entries.push({ path: p, created_at: 0, expired: true });
|
|
952
|
-
}
|
|
953
|
-
}
|
|
954
|
-
let deleted = 0;
|
|
955
|
-
for (const e of entries) {
|
|
956
|
-
if (e.expired) {
|
|
957
|
-
try {
|
|
958
|
-
__require("fs").unlinkSync(e.path);
|
|
959
|
-
} catch {}
|
|
960
|
-
deleted++;
|
|
961
|
-
}
|
|
962
|
-
}
|
|
963
|
-
const valid = entries.filter((e) => !e.expired).sort((a, b) => a.created_at - b.created_at);
|
|
964
|
-
const excess = valid.length - MAX_CACHE_ENTRIES;
|
|
965
|
-
for (let i = 0;i < excess; i++) {
|
|
966
|
-
try {
|
|
967
|
-
__require("fs").unlinkSync(valid[i].path);
|
|
968
|
-
} catch {}
|
|
969
|
-
}
|
|
970
|
-
} catch {}
|
|
971
|
-
}
|
|
972
|
-
|
|
973
|
-
// src/tools/codebase-index.ts
|
|
974
|
-
import { readFileSync as readFileSync7, writeFileSync as writeFileSync6, existsSync as existsSync7, mkdirSync as mkdirSync4 } from "fs";
|
|
975
|
-
import { join as join7 } from "path";
|
|
976
|
-
var CODEBASE_INDEX_FILE = "CODEBASE_INDEX.md";
|
|
977
|
-
function indexPath(dir) {
|
|
978
|
-
return join7(planningDir(dir), CODEBASE_INDEX_FILE);
|
|
979
|
-
}
|
|
980
|
-
function readCodebaseIndex(dir) {
|
|
981
|
-
const path = indexPath(dir);
|
|
982
|
-
if (!existsSync7(path)) {
|
|
983
|
-
return {
|
|
984
|
-
exists: false,
|
|
985
|
-
lastUpdatedAt: "",
|
|
986
|
-
lastUpdatedBy: "",
|
|
987
|
-
sourceStage: "",
|
|
988
|
-
changedFiles: [],
|
|
989
|
-
fileSnapshots: {},
|
|
990
|
-
explorationHistory: [],
|
|
991
|
-
summaryVersion: 0,
|
|
992
|
-
freshnessStatus: "unknown"
|
|
993
|
-
};
|
|
994
|
-
}
|
|
995
|
-
try {
|
|
996
|
-
const content = readFileSync7(path, "utf-8");
|
|
997
|
-
return parseCodebaseIndexContent(content);
|
|
998
|
-
} catch {
|
|
999
|
-
return {
|
|
1000
|
-
exists: false,
|
|
1001
|
-
lastUpdatedAt: "",
|
|
1002
|
-
lastUpdatedBy: "",
|
|
1003
|
-
sourceStage: "",
|
|
1004
|
-
changedFiles: [],
|
|
1005
|
-
fileSnapshots: {},
|
|
1006
|
-
explorationHistory: [],
|
|
1007
|
-
summaryVersion: 0,
|
|
1008
|
-
freshnessStatus: "unknown"
|
|
1009
|
-
};
|
|
1010
|
-
}
|
|
1011
|
-
}
|
|
1012
|
-
function parseCodebaseIndexContent(content) {
|
|
1013
|
-
const result = { exists: true };
|
|
1014
|
-
for (const line of content.split(`
|
|
1015
|
-
`)) {
|
|
1016
|
-
if (line.startsWith("#") || line.trim() === "")
|
|
1017
|
-
continue;
|
|
1018
|
-
const strippedLine = line.replace(/\*\*/g, "").replace(/^([a-zA-Z_][a-zA-Z0-9_]*):\s*(.*)/, "$1: $2");
|
|
1019
|
-
const kvMatch = strippedLine.match(/^([a-zA-Z_][a-zA-Z0-9_]*):\s*(.*)/);
|
|
1020
|
-
if (!kvMatch)
|
|
1021
|
-
continue;
|
|
1022
|
-
const key = kvMatch[1].trim();
|
|
1023
|
-
const value = kvMatch[2].trim();
|
|
1024
|
-
if (key === "changedFiles") {
|
|
1025
|
-
result.changedFiles = value.replace(/[\[\]]/g, "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
1026
|
-
} else if (key === "summaryVersion") {
|
|
1027
|
-
result.summaryVersion = parseInt(value, 10) || 0;
|
|
1028
|
-
} else if (key === "freshnessStatus") {
|
|
1029
|
-
result.freshnessStatus = value;
|
|
1030
|
-
} else if (key === "lastUpdatedAt" || key === "lastUpdatedBy" || key === "sourceStage") {
|
|
1031
|
-
result[key] = value.replace(/^["']|["']$/g, "");
|
|
1032
|
-
}
|
|
1033
|
-
}
|
|
1034
|
-
let blockCount = 0;
|
|
1035
|
-
for (const jsonMatch of content.matchAll(/```json\n([\s\S]*?)\n```/g)) {
|
|
1036
|
-
if (blockCount >= 2)
|
|
1037
|
-
break;
|
|
1038
|
-
blockCount++;
|
|
1039
|
-
try {
|
|
1040
|
-
const parsed = JSON.parse(jsonMatch[1]);
|
|
1041
|
-
if (parsed.fileSnapshots)
|
|
1042
|
-
result.fileSnapshots = parsed.fileSnapshots;
|
|
1043
|
-
if (parsed.explorationHistory)
|
|
1044
|
-
result.explorationHistory = parsed.explorationHistory;
|
|
1045
|
-
if (!parsed.fileSnapshots && !parsed.explorationHistory) {
|
|
1046
|
-
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
1047
|
-
if (!result.fileSnapshots)
|
|
1048
|
-
result.fileSnapshots = {};
|
|
1049
|
-
Object.assign(result.fileSnapshots, parsed);
|
|
1050
|
-
} else if (Array.isArray(parsed)) {
|
|
1051
|
-
result.explorationHistory = parsed;
|
|
1052
|
-
}
|
|
1053
|
-
}
|
|
1054
|
-
} catch {
|
|
1055
|
-
result.freshnessStatus = "unknown";
|
|
1056
|
-
}
|
|
1057
|
-
}
|
|
1058
|
-
return {
|
|
1059
|
-
exists: true,
|
|
1060
|
-
lastUpdatedAt: result.lastUpdatedAt || "",
|
|
1061
|
-
lastUpdatedBy: result.lastUpdatedBy || "",
|
|
1062
|
-
sourceStage: result.sourceStage || "",
|
|
1063
|
-
changedFiles: result.changedFiles || [],
|
|
1064
|
-
fileSnapshots: result.fileSnapshots || {},
|
|
1065
|
-
explorationHistory: result.explorationHistory || [],
|
|
1066
|
-
summaryVersion: result.summaryVersion || 0,
|
|
1067
|
-
freshnessStatus: result.freshnessStatus || "unknown"
|
|
1068
|
-
};
|
|
1069
|
-
}
|
|
1070
|
-
|
|
1071
|
-
// src/services/token-metrics.ts
|
|
1072
|
-
import { existsSync as existsSync8, readFileSync as readFileSync8, appendFileSync, mkdirSync as mkdirSync5 } from "fs";
|
|
1073
|
-
import { join as join8 } from "path";
|
|
1074
|
-
function estimateTokens(text) {
|
|
1075
|
-
return Math.ceil(text.length / 4);
|
|
1076
|
-
}
|
|
1077
|
-
function metricsPath(dir) {
|
|
1078
|
-
return join8(codebaseDir(dir), "TOKEN_METRICS.jsonl");
|
|
1079
|
-
}
|
|
1080
|
-
function appendEvent(dir, event) {
|
|
1081
|
-
const cd = codebaseDir(dir);
|
|
1082
|
-
if (!existsSync8(cd))
|
|
1083
|
-
mkdirSync5(cd, { recursive: true });
|
|
1084
|
-
appendFileSync(metricsPath(dir), JSON.stringify(event) + `
|
|
1085
|
-
`, "utf-8");
|
|
1086
|
-
}
|
|
1087
|
-
function recordModelCall(dir, workflow_id, stage, inputText, outputText, agent, duration_ms, model, est_cost_usd) {
|
|
1088
|
-
const est_input_tokens = estimateTokens(inputText);
|
|
1089
|
-
const est_output_tokens = estimateTokens(outputText);
|
|
1090
|
-
appendEvent(dir, {
|
|
1091
|
-
ts: new Date().toISOString(),
|
|
1092
|
-
workflow_id,
|
|
1093
|
-
stage,
|
|
1094
|
-
event: "model_call",
|
|
1095
|
-
agent,
|
|
1096
|
-
model,
|
|
1097
|
-
est_input_tokens,
|
|
1098
|
-
est_output_tokens,
|
|
1099
|
-
input_chars: inputText.length,
|
|
1100
|
-
output_chars: outputText.length,
|
|
1101
|
-
duration_ms,
|
|
1102
|
-
est_cost_usd
|
|
1103
|
-
});
|
|
1104
|
-
}
|
|
1105
|
-
function recordCacheHit(dir, workflow_id, stage, inputText, agent, model) {
|
|
1106
|
-
appendEvent(dir, {
|
|
1107
|
-
ts: new Date().toISOString(),
|
|
1108
|
-
workflow_id,
|
|
1109
|
-
stage,
|
|
1110
|
-
event: "cache_hit",
|
|
1111
|
-
agent,
|
|
1112
|
-
model,
|
|
1113
|
-
est_input_tokens: estimateTokens(inputText),
|
|
1114
|
-
est_output_tokens: 0,
|
|
1115
|
-
input_chars: inputText.length,
|
|
1116
|
-
output_chars: 0
|
|
1117
|
-
});
|
|
1118
|
-
}
|
|
1119
|
-
function recordRetryCall(dir, workflow_id, stage, inputText, outputText, agent, duration_ms, model, est_cost_usd) {
|
|
1120
|
-
const est_input_tokens = estimateTokens(inputText);
|
|
1121
|
-
const est_output_tokens = estimateTokens(outputText);
|
|
1122
|
-
appendEvent(dir, {
|
|
1123
|
-
ts: new Date().toISOString(),
|
|
1124
|
-
workflow_id,
|
|
1125
|
-
stage,
|
|
1126
|
-
event: "retry",
|
|
1127
|
-
agent,
|
|
1128
|
-
model,
|
|
1129
|
-
est_input_tokens,
|
|
1130
|
-
est_output_tokens,
|
|
1131
|
-
input_chars: inputText.length,
|
|
1132
|
-
output_chars: outputText.length,
|
|
1133
|
-
duration_ms,
|
|
1134
|
-
est_cost_usd
|
|
1135
|
-
});
|
|
1136
|
-
}
|
|
1137
|
-
var _workflowTimers = new Map;
|
|
1138
|
-
|
|
1139
|
-
// src/config/loader.ts
|
|
1140
|
-
import { existsSync as existsSync9, readFileSync as readFileSync9 } from "fs";
|
|
1141
|
-
import { join as join9 } from "path";
|
|
1142
|
-
import { homedir } from "os";
|
|
1143
|
-
var CONFIG_FILENAME = "flowdeck.json";
|
|
1144
|
-
function getGlobalConfigDir() {
|
|
1145
|
-
return process.env.OPENCODE_CONFIG_DIR || (process.env.XDG_CONFIG_HOME ? join9(process.env.XDG_CONFIG_HOME, "opencode") : join9(homedir(), ".config", "opencode"));
|
|
1146
|
-
}
|
|
1147
|
-
function loadFlowDeckConfig(directory) {
|
|
1148
|
-
const candidates = [];
|
|
1149
|
-
if (directory) {
|
|
1150
|
-
candidates.push(join9(directory, ".opencode", CONFIG_FILENAME));
|
|
1151
|
-
}
|
|
1152
|
-
candidates.push(join9(getGlobalConfigDir(), CONFIG_FILENAME));
|
|
1153
|
-
for (const configPath of candidates) {
|
|
1154
|
-
if (existsSync9(configPath)) {
|
|
1155
|
-
try {
|
|
1156
|
-
const content = readFileSync9(configPath, "utf-8");
|
|
1157
|
-
return JSON.parse(content);
|
|
1158
|
-
} catch {}
|
|
1159
|
-
}
|
|
1160
|
-
}
|
|
1161
|
-
return {};
|
|
1162
|
-
}
|
|
1163
|
-
function resolveDesignFirstConfig(config) {
|
|
1164
|
-
return {
|
|
1165
|
-
enabled: config.designFirst?.enabled ?? true,
|
|
1166
|
-
enforcement: config.designFirst?.enforcement ?? "strict",
|
|
1167
|
-
requireApprovalBeforeImplementation: config.designFirst?.requireApprovalBeforeImplementation ?? true,
|
|
1168
|
-
modelOverrides: config.designFirst?.modelOverrides ?? {},
|
|
1169
|
-
defaultSkillsByTaskType: config.designFirst?.defaultSkillsByTaskType ?? {
|
|
1170
|
-
"landing-page": ["landing-page-design", "wireframe-planning", "design-system-definition", "frontend-handoff"],
|
|
1171
|
-
dashboard: ["dashboard-design", "ui-ux-planning", "wireframe-planning", "responsive-review"],
|
|
1172
|
-
"admin-panel": ["ui-ux-planning", "wireframe-planning", "design-system-definition", "frontend-handoff"],
|
|
1173
|
-
"app-screen": ["app-shell-design", "ui-ux-planning", "wireframe-planning", "responsive-review"],
|
|
1174
|
-
"general-ui": ["ui-ux-planning", "wireframe-planning", "design-system-definition", "frontend-handoff"]
|
|
1175
|
-
}
|
|
1176
|
-
};
|
|
1177
|
-
}
|
|
1178
|
-
// src/services/cost-estimator.ts
|
|
1179
|
-
var PRICING_TABLE = [
|
|
1180
|
-
{ prefix: "claude-opus-4", pricing: { input: 15, output: 75 } },
|
|
1181
|
-
{ prefix: "claude-opus", pricing: { input: 15, output: 75 } },
|
|
1182
|
-
{ prefix: "claude-sonnet-4", pricing: { input: 3, output: 15 } },
|
|
1183
|
-
{ prefix: "claude-sonnet-3-5", pricing: { input: 3, output: 15 } },
|
|
1184
|
-
{ prefix: "claude-sonnet-3", pricing: { input: 3, output: 15 } },
|
|
1185
|
-
{ prefix: "claude-sonnet", pricing: { input: 3, output: 15 } },
|
|
1186
|
-
{ prefix: "claude-haiku-4", pricing: { input: 0.8, output: 4 } },
|
|
1187
|
-
{ prefix: "claude-haiku-3-5", pricing: { input: 0.8, output: 4 } },
|
|
1188
|
-
{ prefix: "claude-haiku", pricing: { input: 0.25, output: 1.25 } },
|
|
1189
|
-
{ prefix: "claude-3-opus", pricing: { input: 15, output: 75 } },
|
|
1190
|
-
{ prefix: "claude-3-5-sonnet", pricing: { input: 3, output: 15 } },
|
|
1191
|
-
{ prefix: "claude-3-sonnet", pricing: { input: 3, output: 15 } },
|
|
1192
|
-
{ prefix: "claude-3-haiku", pricing: { input: 0.25, output: 1.25 } },
|
|
1193
|
-
{ prefix: "claude", pricing: { input: 3, output: 15 } },
|
|
1194
|
-
{ prefix: "gpt-5.4-mini", pricing: { input: 0.15, output: 0.6 } },
|
|
1195
|
-
{ prefix: "gpt-5-mini", pricing: { input: 0.15, output: 0.6 } },
|
|
1196
|
-
{ prefix: "gpt-4.1", pricing: { input: 2, output: 8 } },
|
|
1197
|
-
{ prefix: "gpt-4o-mini", pricing: { input: 0.15, output: 0.6 } },
|
|
1198
|
-
{ prefix: "gpt-4o", pricing: { input: 2.5, output: 10 } },
|
|
1199
|
-
{ prefix: "gpt-4-turbo", pricing: { input: 10, output: 30 } },
|
|
1200
|
-
{ prefix: "gpt-4", pricing: { input: 30, output: 60 } },
|
|
1201
|
-
{ prefix: "gpt-3.5", pricing: { input: 0.5, output: 1.5 } },
|
|
1202
|
-
{ prefix: "gpt-5", pricing: { input: 10, output: 30 } },
|
|
1203
|
-
{ prefix: "o3-mini", pricing: { input: 1.1, output: 4.4 } },
|
|
1204
|
-
{ prefix: "o3", pricing: { input: 10, output: 40 } },
|
|
1205
|
-
{ prefix: "o1-mini", pricing: { input: 1.1, output: 4.4 } },
|
|
1206
|
-
{ prefix: "o1", pricing: { input: 15, output: 60 } },
|
|
1207
|
-
{ prefix: "gemini-2.0-flash", pricing: { input: 0.1, output: 0.4 } },
|
|
1208
|
-
{ prefix: "gemini-2.5-flash", pricing: { input: 0.15, output: 0.6 } },
|
|
1209
|
-
{ prefix: "gemini-2.5-pro", pricing: { input: 1.25, output: 5 } },
|
|
1210
|
-
{ prefix: "gemini-1.5-flash", pricing: { input: 0.075, output: 0.3 } },
|
|
1211
|
-
{ prefix: "gemini-1.5-pro", pricing: { input: 1.25, output: 5 } },
|
|
1212
|
-
{ prefix: "gemini", pricing: { input: 0.1, output: 0.4 } },
|
|
1213
|
-
{ prefix: "github-copilot/sonnet", pricing: { input: 3, output: 15 } },
|
|
1214
|
-
{ prefix: "github-copilot/haiku", pricing: { input: 0.25, output: 1.25 } },
|
|
1215
|
-
{ prefix: "github-copilot/gpt-4", pricing: { input: 2.5, output: 10 } },
|
|
1216
|
-
{ prefix: "github-copilot", pricing: { input: 3, output: 15 } }
|
|
1217
|
-
];
|
|
1218
|
-
var FALLBACK_PRICING = { input: 3, output: 15 };
|
|
1219
|
-
function getModelPricing(model) {
|
|
1220
|
-
if (!model)
|
|
1221
|
-
return FALLBACK_PRICING;
|
|
1222
|
-
const lower = model.toLowerCase();
|
|
1223
|
-
for (const entry of PRICING_TABLE) {
|
|
1224
|
-
if (lower.startsWith(entry.prefix.toLowerCase()))
|
|
1225
|
-
return entry.pricing;
|
|
1226
|
-
}
|
|
1227
|
-
return FALLBACK_PRICING;
|
|
1228
|
-
}
|
|
1229
|
-
function estimateCostUSD(model, inputTokens, outputTokens) {
|
|
1230
|
-
const pricing = getModelPricing(model);
|
|
1231
|
-
return inputTokens / 1e6 * pricing.input + outputTokens / 1e6 * pricing.output;
|
|
1232
|
-
}
|
|
1233
|
-
|
|
1234
|
-
// src/tools/delegate.ts
|
|
1235
|
-
function extractText2(parts) {
|
|
1236
|
-
return parts.filter((p) => p.type === "text" && typeof p.text === "string").map((p) => p.text).join(`
|
|
1237
|
-
`);
|
|
1238
|
-
}
|
|
1239
|
-
function createDelegateTool(client) {
|
|
1240
|
-
return tool4({
|
|
1241
|
-
description: "Delegate a task to a single agent via a child session. Returns the agent's output.",
|
|
1242
|
-
args: {
|
|
1243
|
-
agent: tool4.schema.string(),
|
|
1244
|
-
prompt: tool4.schema.string(),
|
|
1245
|
-
context: tool4.schema.string().optional(),
|
|
1246
|
-
task_type: tool4.schema.string().optional(),
|
|
1247
|
-
retry_attempts: tool4.schema.number().optional().default(1),
|
|
1248
|
-
safe_to_cache: tool4.schema.boolean().optional().default(false),
|
|
1249
|
-
cache_ttl_ms: tool4.schema.number().optional(),
|
|
1250
|
-
workflow_id: tool4.schema.string().optional(),
|
|
1251
|
-
stage: tool4.schema.string().optional()
|
|
1252
|
-
},
|
|
1253
|
-
async execute(args, context) {
|
|
1254
|
-
const startTime = Date.now();
|
|
1255
|
-
const taskType = normalizeTaskType(args.task_type, args.agent);
|
|
1256
|
-
const retryAttempts = typeof args.retry_attempts === "number" ? args.retry_attempts : 1;
|
|
1257
|
-
const maxRetries = Math.max(0, Math.floor(retryAttempts));
|
|
1258
|
-
let agentModel = "";
|
|
1259
|
-
try {
|
|
1260
|
-
const cfg = loadFlowDeckConfig(context.directory);
|
|
1261
|
-
agentModel = cfg.agents?.[args.agent]?.model ?? "";
|
|
1262
|
-
} catch {}
|
|
1263
|
-
const metricsWorkflowId = args.workflow_id ?? "";
|
|
1264
|
-
const metricsStage = args.stage ?? "delegate";
|
|
1265
|
-
const fullPrompt = args.context ? `${args.context}
|
|
1266
|
-
|
|
1267
|
-
---
|
|
1268
|
-
|
|
1269
|
-
${args.prompt}` : args.prompt;
|
|
1270
|
-
const safe_to_cache = args.safe_to_cache === true && CACHEABLE_AGENTS.has(args.agent);
|
|
1271
|
-
let stateVersion = 0;
|
|
1272
|
-
let indexVersion = 0;
|
|
1273
|
-
if (safe_to_cache) {
|
|
1274
|
-
const index = readCodebaseIndex(context.directory);
|
|
1275
|
-
const sp = statePath(context.directory);
|
|
1276
|
-
const rawState = existsSync10(sp) ? readFileSync10(sp, "utf-8") : "";
|
|
1277
|
-
const state = rawState ? parseState(rawState) : {};
|
|
1278
|
-
stateVersion = typeof state.summaryVersion === "number" ? state.summaryVersion : 0;
|
|
1279
|
-
indexVersion = typeof index.summaryVersion === "number" ? index.summaryVersion : 0;
|
|
1280
|
-
const cached = getCached(context.directory, args.agent, fullPrompt, args.context ?? "", stateVersion, indexVersion, true);
|
|
1281
|
-
if (cached !== null) {
|
|
1282
|
-
if (metricsWorkflowId) {
|
|
1283
|
-
recordCacheHit(context.directory, metricsWorkflowId, metricsStage, fullPrompt, args.agent, agentModel);
|
|
1284
|
-
}
|
|
1285
|
-
return JSON.stringify({
|
|
1286
|
-
agent: args.agent,
|
|
1287
|
-
success: true,
|
|
1288
|
-
output: cached,
|
|
1289
|
-
task_type: taskType,
|
|
1290
|
-
model: "",
|
|
1291
|
-
retries_used: 0,
|
|
1292
|
-
duration_ms: Date.now() - startTime,
|
|
1293
|
-
cached: true
|
|
1294
|
-
});
|
|
1295
|
-
}
|
|
1296
|
-
}
|
|
1297
|
-
const createRes = await client.session.create({
|
|
1298
|
-
body: { parentID: context.sessionID, title: `${args.agent}-delegate` },
|
|
1299
|
-
query: { directory: context.directory }
|
|
1300
|
-
});
|
|
1301
|
-
if (createRes.error || !createRes.data?.id) {
|
|
1302
|
-
return JSON.stringify({
|
|
1303
|
-
agent: args.agent,
|
|
1304
|
-
success: false,
|
|
1305
|
-
error: `Failed to create session: ${createRes.error?.detail ?? "unknown"}`,
|
|
1306
|
-
duration_ms: Date.now() - startTime
|
|
1307
|
-
});
|
|
1308
|
-
}
|
|
1309
|
-
const childId = createRes.data.id;
|
|
1310
|
-
context.abort.addEventListener("abort", () => {
|
|
1311
|
-
client.session.abort({
|
|
1312
|
-
path: { id: childId },
|
|
1313
|
-
query: { directory: context.directory }
|
|
1314
|
-
}).catch(() => {});
|
|
1315
|
-
});
|
|
1316
|
-
const fullPromptForSession = args.context ? `${args.context}
|
|
1317
|
-
|
|
1318
|
-
---
|
|
1319
|
-
|
|
1320
|
-
${args.prompt}` : args.prompt;
|
|
1321
|
-
let promptRes = null;
|
|
1322
|
-
let retriesUsed = 0;
|
|
1323
|
-
for (let attempt = 0;attempt <= maxRetries; attempt++) {
|
|
1324
|
-
const attemptStart = Date.now();
|
|
1325
|
-
promptRes = await client.session.prompt({
|
|
1326
|
-
path: { id: childId },
|
|
1327
|
-
body: {
|
|
1328
|
-
agent: args.agent,
|
|
1329
|
-
parts: [{ type: "text", text: fullPromptForSession }],
|
|
1330
|
-
tools: { question: false }
|
|
1331
|
-
},
|
|
1332
|
-
query: { directory: context.directory }
|
|
1333
|
-
});
|
|
1334
|
-
if (!shouldRetry(promptRes) || attempt === maxRetries)
|
|
1335
|
-
break;
|
|
1336
|
-
if (metricsWorkflowId) {
|
|
1337
|
-
const retryInputTokens = estimateTokens(fullPromptForSession);
|
|
1338
|
-
const retryCostUsd = agentModel ? estimateCostUSD(agentModel, retryInputTokens, 0) : undefined;
|
|
1339
|
-
recordRetryCall(context.directory, metricsWorkflowId, metricsStage, fullPromptForSession, "", args.agent, Date.now() - attemptStart, agentModel, retryCostUsd);
|
|
1340
|
-
}
|
|
1341
|
-
retriesUsed++;
|
|
1342
|
-
}
|
|
1343
|
-
if (!promptRes || promptRes.error) {
|
|
1344
|
-
const errMsg = `Prompt failed: ${promptRes?.error?.detail ?? "unknown"}`;
|
|
1345
|
-
recordRun(context.directory, args.agent, "", taskType, false, Date.now() - startTime);
|
|
1346
|
-
return JSON.stringify({
|
|
1347
|
-
agent: args.agent,
|
|
1348
|
-
session_id: childId,
|
|
1349
|
-
success: false,
|
|
1350
|
-
error: errMsg,
|
|
1351
|
-
task_type: taskType,
|
|
1352
|
-
model: "",
|
|
1353
|
-
retries_used: retriesUsed,
|
|
1354
|
-
duration_ms: Date.now() - startTime
|
|
1355
|
-
});
|
|
1356
|
-
}
|
|
1357
|
-
const info = promptRes.data?.info;
|
|
1358
|
-
if (info?.error) {
|
|
1359
|
-
const errMsg = `Agent error: ${JSON.stringify(info.error)}`;
|
|
1360
|
-
recordRun(context.directory, args.agent, "", taskType, false, Date.now() - startTime);
|
|
1361
|
-
return JSON.stringify({
|
|
1362
|
-
agent: args.agent,
|
|
1363
|
-
session_id: childId,
|
|
1364
|
-
success: false,
|
|
1365
|
-
error: errMsg,
|
|
1366
|
-
task_type: taskType,
|
|
1367
|
-
model: "",
|
|
1368
|
-
retries_used: retriesUsed,
|
|
1369
|
-
duration_ms: Date.now() - startTime
|
|
1370
|
-
});
|
|
1371
|
-
}
|
|
1372
|
-
const output = extractText2(promptRes.data?.parts ?? []);
|
|
1373
|
-
recordRun(context.directory, args.agent, "", taskType, true, Date.now() - startTime);
|
|
1374
|
-
if (metricsWorkflowId) {
|
|
1375
|
-
const inputTokens = estimateTokens(fullPromptForSession);
|
|
1376
|
-
const outputTokens = estimateTokens(output);
|
|
1377
|
-
const costUsd = agentModel ? estimateCostUSD(agentModel, inputTokens, outputTokens) : undefined;
|
|
1378
|
-
recordModelCall(context.directory, metricsWorkflowId, metricsStage, fullPromptForSession, output, args.agent, Date.now() - startTime, agentModel, costUsd);
|
|
1379
|
-
}
|
|
1380
|
-
if (safe_to_cache && output) {
|
|
1381
|
-
setCached(context.directory, args.agent, fullPromptForSession, args.context ?? "", stateVersion, indexVersion, output, true, args.cache_ttl_ms);
|
|
1382
|
-
}
|
|
1383
|
-
return JSON.stringify({
|
|
1384
|
-
agent: args.agent,
|
|
1385
|
-
session_id: childId,
|
|
1386
|
-
success: true,
|
|
1387
|
-
output: output || "(no text output)",
|
|
1388
|
-
task_type: taskType,
|
|
1389
|
-
model: "",
|
|
1390
|
-
retries_used: retriesUsed,
|
|
1391
|
-
duration_ms: Date.now() - startTime
|
|
1392
|
-
});
|
|
1393
|
-
}
|
|
1394
|
-
});
|
|
1395
|
-
}
|
|
1396
|
-
|
|
1397
|
-
// src/tools/repo-memory.ts
|
|
1398
|
-
import { tool as tool5 } from "@opencode-ai/plugin";
|
|
1399
|
-
import { readFileSync as readFileSync11, writeFileSync as writeFileSync7, existsSync as existsSync11, mkdirSync as mkdirSync6 } from "fs";
|
|
1400
|
-
import { join as join10 } from "path";
|
|
1401
586
|
var MEMORY_FILE = "MEMORY.json";
|
|
1402
587
|
function memoryPath(directory) {
|
|
1403
|
-
return
|
|
588
|
+
return join5(codebaseDir(directory), MEMORY_FILE);
|
|
1404
589
|
}
|
|
1405
590
|
function emptyMemory() {
|
|
1406
591
|
return { version: "1.0", last_updated: new Date().toISOString(), nodes: {} };
|
|
1407
592
|
}
|
|
1408
593
|
function readMemory(directory) {
|
|
1409
594
|
const p = memoryPath(directory);
|
|
1410
|
-
if (!
|
|
595
|
+
if (!existsSync5(p))
|
|
1411
596
|
return emptyMemory();
|
|
1412
597
|
try {
|
|
1413
|
-
return JSON.parse(
|
|
598
|
+
return JSON.parse(readFileSync5(p, "utf-8"));
|
|
1414
599
|
} catch {
|
|
1415
600
|
return emptyMemory();
|
|
1416
601
|
}
|
|
1417
602
|
}
|
|
1418
603
|
function writeMemory(directory, memory) {
|
|
1419
604
|
const base = codebaseDir(directory);
|
|
1420
|
-
if (!
|
|
1421
|
-
|
|
605
|
+
if (!existsSync5(base))
|
|
606
|
+
mkdirSync2(base, { recursive: true });
|
|
1422
607
|
memory.last_updated = new Date().toISOString();
|
|
1423
|
-
|
|
608
|
+
writeFileSync4(memoryPath(directory), JSON.stringify(memory, null, 2), "utf-8");
|
|
1424
609
|
}
|
|
1425
|
-
var repoMemoryTool =
|
|
610
|
+
var repoMemoryTool = tool3({
|
|
1426
611
|
description: "Repo Memory Graph: read/write/query persistent architecture graph in .codebase/MEMORY.json (modules, dependencies, ownership, bug history, conventions)",
|
|
1427
612
|
args: {
|
|
1428
|
-
action:
|
|
1429
|
-
node_id:
|
|
1430
|
-
node:
|
|
1431
|
-
type:
|
|
1432
|
-
path:
|
|
1433
|
-
owner:
|
|
1434
|
-
tags:
|
|
1435
|
-
dependencies:
|
|
1436
|
-
dependents:
|
|
1437
|
-
bug_history:
|
|
1438
|
-
conventions:
|
|
613
|
+
action: tool3.schema.enum(["read", "write_node", "query", "delete_node"]),
|
|
614
|
+
node_id: tool3.schema.string().optional(),
|
|
615
|
+
node: tool3.schema.object({
|
|
616
|
+
type: tool3.schema.enum(["module", "service", "api", "schema", "config"]),
|
|
617
|
+
path: tool3.schema.string(),
|
|
618
|
+
owner: tool3.schema.string().optional(),
|
|
619
|
+
tags: tool3.schema.array(tool3.schema.string()),
|
|
620
|
+
dependencies: tool3.schema.array(tool3.schema.string()),
|
|
621
|
+
dependents: tool3.schema.array(tool3.schema.string()),
|
|
622
|
+
bug_history: tool3.schema.array(tool3.schema.string()),
|
|
623
|
+
conventions: tool3.schema.array(tool3.schema.string())
|
|
1439
624
|
}).optional(),
|
|
1440
|
-
query:
|
|
1441
|
-
type:
|
|
1442
|
-
owner:
|
|
1443
|
-
tag:
|
|
1444
|
-
path_prefix:
|
|
625
|
+
query: tool3.schema.object({
|
|
626
|
+
type: tool3.schema.enum(["module", "service", "api", "schema", "config"]).optional(),
|
|
627
|
+
owner: tool3.schema.string().optional(),
|
|
628
|
+
tag: tool3.schema.string().optional(),
|
|
629
|
+
path_prefix: tool3.schema.string().optional()
|
|
1445
630
|
}).optional()
|
|
1446
631
|
},
|
|
1447
632
|
async execute(args, context) {
|
|
@@ -1496,50 +681,50 @@ var repoMemoryTool = tool5({
|
|
|
1496
681
|
});
|
|
1497
682
|
|
|
1498
683
|
// src/tools/failure-replay.ts
|
|
1499
|
-
import { tool as
|
|
1500
|
-
import { readFileSync as
|
|
1501
|
-
import { join as
|
|
684
|
+
import { tool as tool4 } from "@opencode-ai/plugin";
|
|
685
|
+
import { readFileSync as readFileSync6, writeFileSync as writeFileSync5, existsSync as existsSync6, mkdirSync as mkdirSync3 } from "fs";
|
|
686
|
+
import { join as join6 } from "path";
|
|
1502
687
|
var FAILURES_FILE = "FAILURES.json";
|
|
1503
688
|
function failuresPath(directory) {
|
|
1504
|
-
return
|
|
689
|
+
return join6(codebaseDir(directory), FAILURES_FILE);
|
|
1505
690
|
}
|
|
1506
691
|
function readStore(directory) {
|
|
1507
692
|
const p = failuresPath(directory);
|
|
1508
|
-
if (!
|
|
693
|
+
if (!existsSync6(p))
|
|
1509
694
|
return { version: "1.0", last_updated: new Date().toISOString(), entries: [] };
|
|
1510
695
|
try {
|
|
1511
|
-
return JSON.parse(
|
|
696
|
+
return JSON.parse(readFileSync6(p, "utf-8"));
|
|
1512
697
|
} catch {
|
|
1513
698
|
return { version: "1.0", last_updated: new Date().toISOString(), entries: [] };
|
|
1514
699
|
}
|
|
1515
700
|
}
|
|
1516
701
|
function writeStore(directory, store) {
|
|
1517
702
|
const base = codebaseDir(directory);
|
|
1518
|
-
if (!
|
|
1519
|
-
|
|
703
|
+
if (!existsSync6(base))
|
|
704
|
+
mkdirSync3(base, { recursive: true });
|
|
1520
705
|
store.last_updated = new Date().toISOString();
|
|
1521
|
-
|
|
706
|
+
writeFileSync5(failuresPath(directory), JSON.stringify(store, null, 2), "utf-8");
|
|
1522
707
|
}
|
|
1523
|
-
var failureReplayTool =
|
|
708
|
+
var failureReplayTool = tool4({
|
|
1524
709
|
description: "Failure Replay Engine: record and query past failures (reverted commits, failed deployments, flaky tests, bug fixes) in .codebase/FAILURES.json so the agent avoids repeating mistakes",
|
|
1525
710
|
args: {
|
|
1526
|
-
action:
|
|
1527
|
-
entry:
|
|
1528
|
-
id:
|
|
1529
|
-
type:
|
|
1530
|
-
description:
|
|
1531
|
-
affected_paths:
|
|
1532
|
-
root_cause:
|
|
1533
|
-
fix_applied:
|
|
1534
|
-
tags:
|
|
711
|
+
action: tool4.schema.enum(["record", "query", "list", "mark_resolved"]),
|
|
712
|
+
entry: tool4.schema.object({
|
|
713
|
+
id: tool4.schema.string(),
|
|
714
|
+
type: tool4.schema.enum(["reverted_commit", "failed_deployment", "flaky_test", "bug_fix", "build_failure"]),
|
|
715
|
+
description: tool4.schema.string(),
|
|
716
|
+
affected_paths: tool4.schema.array(tool4.schema.string()),
|
|
717
|
+
root_cause: tool4.schema.string().optional(),
|
|
718
|
+
fix_applied: tool4.schema.string().optional(),
|
|
719
|
+
tags: tool4.schema.array(tool4.schema.string())
|
|
1535
720
|
}).optional(),
|
|
1536
|
-
query:
|
|
1537
|
-
type:
|
|
1538
|
-
path_prefix:
|
|
1539
|
-
tag:
|
|
1540
|
-
limit:
|
|
721
|
+
query: tool4.schema.object({
|
|
722
|
+
type: tool4.schema.enum(["reverted_commit", "failed_deployment", "flaky_test", "bug_fix", "build_failure"]).optional(),
|
|
723
|
+
path_prefix: tool4.schema.string().optional(),
|
|
724
|
+
tag: tool4.schema.string().optional(),
|
|
725
|
+
limit: tool4.schema.number().optional()
|
|
1541
726
|
}).optional(),
|
|
1542
|
-
entry_id:
|
|
727
|
+
entry_id: tool4.schema.string().optional()
|
|
1543
728
|
},
|
|
1544
729
|
async execute(args, context) {
|
|
1545
730
|
const dir = context.directory ?? process.cwd();
|
|
@@ -1601,18 +786,18 @@ var failureReplayTool = tool6({
|
|
|
1601
786
|
});
|
|
1602
787
|
|
|
1603
788
|
// src/tools/decision-trace.ts
|
|
1604
|
-
import { tool as
|
|
1605
|
-
import { readFileSync as
|
|
1606
|
-
import { join as
|
|
789
|
+
import { tool as tool5 } from "@opencode-ai/plugin";
|
|
790
|
+
import { readFileSync as readFileSync7, existsSync as existsSync7, mkdirSync as mkdirSync4, appendFileSync } from "fs";
|
|
791
|
+
import { join as join7 } from "path";
|
|
1607
792
|
var DECISIONS_FILE = "DECISIONS.jsonl";
|
|
1608
793
|
function decisionsPath(directory) {
|
|
1609
|
-
return
|
|
794
|
+
return join7(codebaseDir(directory), DECISIONS_FILE);
|
|
1610
795
|
}
|
|
1611
796
|
function readDecisions(directory) {
|
|
1612
797
|
const p = decisionsPath(directory);
|
|
1613
|
-
if (!
|
|
798
|
+
if (!existsSync7(p))
|
|
1614
799
|
return [];
|
|
1615
|
-
return
|
|
800
|
+
return readFileSync7(p, "utf-8").split(`
|
|
1616
801
|
`).filter((l) => l.trim()).map((l) => {
|
|
1617
802
|
try {
|
|
1618
803
|
return JSON.parse(l);
|
|
@@ -1621,29 +806,29 @@ function readDecisions(directory) {
|
|
|
1621
806
|
}
|
|
1622
807
|
}).filter(Boolean);
|
|
1623
808
|
}
|
|
1624
|
-
var decisionTraceTool =
|
|
809
|
+
var decisionTraceTool = tool5({
|
|
1625
810
|
description: "Decision Trace: record why the agent changed something, what evidence was used, and assumptions made. Stored in .codebase/DECISIONS.jsonl for fast review.",
|
|
1626
811
|
args: {
|
|
1627
|
-
action:
|
|
1628
|
-
entry:
|
|
1629
|
-
id:
|
|
1630
|
-
file_path:
|
|
1631
|
-
change_type:
|
|
1632
|
-
rationale:
|
|
1633
|
-
evidence:
|
|
1634
|
-
assumptions:
|
|
1635
|
-
alternatives_considered:
|
|
1636
|
-
risk_level:
|
|
1637
|
-
agent:
|
|
1638
|
-
session_id:
|
|
812
|
+
action: tool5.schema.enum(["record", "query", "get_for_file"]),
|
|
813
|
+
entry: tool5.schema.object({
|
|
814
|
+
id: tool5.schema.string(),
|
|
815
|
+
file_path: tool5.schema.string(),
|
|
816
|
+
change_type: tool5.schema.enum(["create", "edit", "delete", "refactor"]),
|
|
817
|
+
rationale: tool5.schema.string(),
|
|
818
|
+
evidence: tool5.schema.array(tool5.schema.string()),
|
|
819
|
+
assumptions: tool5.schema.array(tool5.schema.string()),
|
|
820
|
+
alternatives_considered: tool5.schema.array(tool5.schema.string()),
|
|
821
|
+
risk_level: tool5.schema.enum(["low", "medium", "high"]),
|
|
822
|
+
agent: tool5.schema.string().optional(),
|
|
823
|
+
session_id: tool5.schema.string().optional()
|
|
1639
824
|
}).optional(),
|
|
1640
|
-
query:
|
|
1641
|
-
file_path:
|
|
1642
|
-
change_type:
|
|
1643
|
-
risk_level:
|
|
1644
|
-
limit:
|
|
825
|
+
query: tool5.schema.object({
|
|
826
|
+
file_path: tool5.schema.string().optional(),
|
|
827
|
+
change_type: tool5.schema.enum(["create", "edit", "delete", "refactor"]).optional(),
|
|
828
|
+
risk_level: tool5.schema.enum(["low", "medium", "high"]).optional(),
|
|
829
|
+
limit: tool5.schema.number().optional()
|
|
1645
830
|
}).optional(),
|
|
1646
|
-
file_path:
|
|
831
|
+
file_path: tool5.schema.string().optional()
|
|
1647
832
|
},
|
|
1648
833
|
async execute(args, context) {
|
|
1649
834
|
const dir = context.directory ?? process.cwd();
|
|
@@ -1652,10 +837,10 @@ var decisionTraceTool = tool7({
|
|
|
1652
837
|
case "record": {
|
|
1653
838
|
if (!args.entry)
|
|
1654
839
|
return JSON.stringify({ error: "entry required" });
|
|
1655
|
-
if (!
|
|
1656
|
-
|
|
840
|
+
if (!existsSync7(base))
|
|
841
|
+
mkdirSync4(base, { recursive: true });
|
|
1657
842
|
const entry = { ...args.entry, timestamp: new Date().toISOString() };
|
|
1658
|
-
|
|
843
|
+
appendFileSync(decisionsPath(dir), JSON.stringify(entry) + `
|
|
1659
844
|
`, "utf-8");
|
|
1660
845
|
return JSON.stringify({ success: true, id: args.entry.id });
|
|
1661
846
|
}
|
|
@@ -1686,48 +871,48 @@ var decisionTraceTool = tool7({
|
|
|
1686
871
|
});
|
|
1687
872
|
|
|
1688
873
|
// src/tools/policy-engine.ts
|
|
1689
|
-
import { tool as
|
|
1690
|
-
import { readFileSync as
|
|
1691
|
-
import { join as
|
|
874
|
+
import { tool as tool6 } from "@opencode-ai/plugin";
|
|
875
|
+
import { readFileSync as readFileSync8, writeFileSync as writeFileSync7, existsSync as existsSync8, mkdirSync as mkdirSync5 } from "fs";
|
|
876
|
+
import { join as join8 } from "path";
|
|
1692
877
|
var POLICIES_FILE = "POLICIES.json";
|
|
1693
878
|
function policiesPath(directory) {
|
|
1694
|
-
return
|
|
879
|
+
return join8(codebaseDir(directory), POLICIES_FILE);
|
|
1695
880
|
}
|
|
1696
881
|
function readStore2(directory) {
|
|
1697
882
|
const p = policiesPath(directory);
|
|
1698
|
-
if (!
|
|
883
|
+
if (!existsSync8(p))
|
|
1699
884
|
return { version: "1.0", last_updated: new Date().toISOString(), policies: [] };
|
|
1700
885
|
try {
|
|
1701
|
-
return JSON.parse(
|
|
886
|
+
return JSON.parse(readFileSync8(p, "utf-8"));
|
|
1702
887
|
} catch {
|
|
1703
888
|
return { version: "1.0", last_updated: new Date().toISOString(), policies: [] };
|
|
1704
889
|
}
|
|
1705
890
|
}
|
|
1706
891
|
function writeStore2(directory, store) {
|
|
1707
892
|
const base = codebaseDir(directory);
|
|
1708
|
-
if (!
|
|
1709
|
-
|
|
893
|
+
if (!existsSync8(base))
|
|
894
|
+
mkdirSync5(base, { recursive: true });
|
|
1710
895
|
store.last_updated = new Date().toISOString();
|
|
1711
|
-
|
|
896
|
+
writeFileSync7(policiesPath(directory), JSON.stringify(store, null, 2), "utf-8");
|
|
1712
897
|
}
|
|
1713
|
-
var policyEngineTool =
|
|
898
|
+
var policyEngineTool = tool6({
|
|
1714
899
|
description: "Self-Healing Policy Engine: manage .codebase/POLICIES.json — add, list, query, toggle, and record violations of editing policies learned from past failures",
|
|
1715
900
|
args: {
|
|
1716
|
-
action:
|
|
1717
|
-
policy:
|
|
1718
|
-
id:
|
|
1719
|
-
name:
|
|
1720
|
-
trigger:
|
|
1721
|
-
rule:
|
|
1722
|
-
source:
|
|
1723
|
-
failure_count:
|
|
901
|
+
action: tool6.schema.enum(["list", "add", "record_violation", "toggle", "query"]),
|
|
902
|
+
policy: tool6.schema.object({
|
|
903
|
+
id: tool6.schema.string(),
|
|
904
|
+
name: tool6.schema.string(),
|
|
905
|
+
trigger: tool6.schema.string(),
|
|
906
|
+
rule: tool6.schema.string(),
|
|
907
|
+
source: tool6.schema.enum(["manual", "learned"]),
|
|
908
|
+
failure_count: tool6.schema.number()
|
|
1724
909
|
}).optional(),
|
|
1725
|
-
policy_id:
|
|
1726
|
-
active:
|
|
1727
|
-
query:
|
|
1728
|
-
source:
|
|
1729
|
-
active_only:
|
|
1730
|
-
trigger_contains:
|
|
910
|
+
policy_id: tool6.schema.string().optional(),
|
|
911
|
+
active: tool6.schema.boolean().optional(),
|
|
912
|
+
query: tool6.schema.object({
|
|
913
|
+
source: tool6.schema.enum(["manual", "learned"]).optional(),
|
|
914
|
+
active_only: tool6.schema.boolean().optional(),
|
|
915
|
+
trigger_contains: tool6.schema.string().optional()
|
|
1731
916
|
}).optional()
|
|
1732
917
|
},
|
|
1733
918
|
async execute(args, context) {
|
|
@@ -1786,54 +971,154 @@ var policyEngineTool = tool8({
|
|
|
1786
971
|
}
|
|
1787
972
|
}
|
|
1788
973
|
}
|
|
1789
|
-
});
|
|
1790
|
-
|
|
1791
|
-
// src/tools/hash-edit.ts
|
|
1792
|
-
import { tool as
|
|
1793
|
-
import { readFileSync as
|
|
1794
|
-
import { createHash
|
|
1795
|
-
var hashEditTool =
|
|
1796
|
-
description: "Reliable file editing with content verification. Takes a target content, its expected MD5 hash, and replacement content. Only applies if the hash matches, preventing edits on stale file versions.",
|
|
1797
|
-
args: {
|
|
1798
|
-
filePath:
|
|
1799
|
-
targetContent:
|
|
1800
|
-
expectedHash:
|
|
1801
|
-
replacementContent:
|
|
1802
|
-
},
|
|
1803
|
-
async execute(args, context) {
|
|
1804
|
-
const fullPath = args.filePath.startsWith("/") ? args.filePath : `${context.directory}/${args.filePath}`;
|
|
1805
|
-
let content;
|
|
974
|
+
});
|
|
975
|
+
|
|
976
|
+
// src/tools/hash-edit.ts
|
|
977
|
+
import { tool as tool7 } from "@opencode-ai/plugin";
|
|
978
|
+
import { readFileSync as readFileSync9, writeFileSync as writeFileSync8 } from "fs";
|
|
979
|
+
import { createHash } from "crypto";
|
|
980
|
+
var hashEditTool = tool7({
|
|
981
|
+
description: "Reliable file editing with content verification. Takes a target content, its expected MD5 hash, and replacement content. Only applies if the hash matches, preventing edits on stale file versions.",
|
|
982
|
+
args: {
|
|
983
|
+
filePath: tool7.schema.string(),
|
|
984
|
+
targetContent: tool7.schema.string(),
|
|
985
|
+
expectedHash: tool7.schema.string().optional(),
|
|
986
|
+
replacementContent: tool7.schema.string()
|
|
987
|
+
},
|
|
988
|
+
async execute(args, context) {
|
|
989
|
+
const fullPath = args.filePath.startsWith("/") ? args.filePath : `${context.directory}/${args.filePath}`;
|
|
990
|
+
let content;
|
|
991
|
+
try {
|
|
992
|
+
content = readFileSync9(fullPath, "utf-8");
|
|
993
|
+
} catch (e) {
|
|
994
|
+
return `Error: Could not read file ${args.filePath}`;
|
|
995
|
+
}
|
|
996
|
+
if (!content.includes(args.targetContent)) {
|
|
997
|
+
return `Error: Target content not found in ${args.filePath}. It may have been modified by another agent.`;
|
|
998
|
+
}
|
|
999
|
+
if (args.expectedHash) {
|
|
1000
|
+
const actualHash = createHash("md5").update(args.targetContent).digest("hex");
|
|
1001
|
+
if (actualHash !== args.expectedHash) {
|
|
1002
|
+
return `Error: Hash mismatch for target content. Expected ${args.expectedHash}, got ${actualHash}. Refusing to edit stale content.`;
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
const newContent = content.replace(args.targetContent, args.replacementContent);
|
|
1006
|
+
writeFileSync8(fullPath, newContent, "utf-8");
|
|
1007
|
+
return `Successfully updated ${args.filePath} using hash-anchored edit.`;
|
|
1008
|
+
}
|
|
1009
|
+
});
|
|
1010
|
+
|
|
1011
|
+
// src/tools/council.ts
|
|
1012
|
+
import { tool as tool8 } from "@opencode-ai/plugin";
|
|
1013
|
+
import { appendFileSync as appendFileSync2, existsSync as existsSync10, mkdirSync as mkdirSync7 } from "fs";
|
|
1014
|
+
import { join as join10 } from "path";
|
|
1015
|
+
import { createHash as createHash2 } from "crypto";
|
|
1016
|
+
|
|
1017
|
+
// src/tools/codebase-index.ts
|
|
1018
|
+
import { readFileSync as readFileSync10, writeFileSync as writeFileSync9, existsSync as existsSync9, mkdirSync as mkdirSync6 } from "fs";
|
|
1019
|
+
import { join as join9 } from "path";
|
|
1020
|
+
var CODEBASE_INDEX_FILE = "CODEBASE_INDEX.md";
|
|
1021
|
+
function indexPath(dir) {
|
|
1022
|
+
return join9(planningDir(dir), CODEBASE_INDEX_FILE);
|
|
1023
|
+
}
|
|
1024
|
+
function readCodebaseIndex(dir) {
|
|
1025
|
+
const path = indexPath(dir);
|
|
1026
|
+
if (!existsSync9(path)) {
|
|
1027
|
+
return {
|
|
1028
|
+
exists: false,
|
|
1029
|
+
lastUpdatedAt: "",
|
|
1030
|
+
lastUpdatedBy: "",
|
|
1031
|
+
sourceStage: "",
|
|
1032
|
+
changedFiles: [],
|
|
1033
|
+
fileSnapshots: {},
|
|
1034
|
+
explorationHistory: [],
|
|
1035
|
+
summaryVersion: 0,
|
|
1036
|
+
freshnessStatus: "unknown"
|
|
1037
|
+
};
|
|
1038
|
+
}
|
|
1039
|
+
try {
|
|
1040
|
+
const content = readFileSync10(path, "utf-8");
|
|
1041
|
+
return parseCodebaseIndexContent(content);
|
|
1042
|
+
} catch {
|
|
1043
|
+
return {
|
|
1044
|
+
exists: false,
|
|
1045
|
+
lastUpdatedAt: "",
|
|
1046
|
+
lastUpdatedBy: "",
|
|
1047
|
+
sourceStage: "",
|
|
1048
|
+
changedFiles: [],
|
|
1049
|
+
fileSnapshots: {},
|
|
1050
|
+
explorationHistory: [],
|
|
1051
|
+
summaryVersion: 0,
|
|
1052
|
+
freshnessStatus: "unknown"
|
|
1053
|
+
};
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
function parseCodebaseIndexContent(content) {
|
|
1057
|
+
const result = { exists: true };
|
|
1058
|
+
for (const line of content.split(`
|
|
1059
|
+
`)) {
|
|
1060
|
+
if (line.startsWith("#") || line.trim() === "")
|
|
1061
|
+
continue;
|
|
1062
|
+
const strippedLine = line.replace(/\*\*/g, "").replace(/^([a-zA-Z_][a-zA-Z0-9_]*):\s*(.*)/, "$1: $2");
|
|
1063
|
+
const kvMatch = strippedLine.match(/^([a-zA-Z_][a-zA-Z0-9_]*):\s*(.*)/);
|
|
1064
|
+
if (!kvMatch)
|
|
1065
|
+
continue;
|
|
1066
|
+
const key = kvMatch[1].trim();
|
|
1067
|
+
const value = kvMatch[2].trim();
|
|
1068
|
+
if (key === "changedFiles") {
|
|
1069
|
+
result.changedFiles = value.replace(/[\[\]]/g, "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
1070
|
+
} else if (key === "summaryVersion") {
|
|
1071
|
+
result.summaryVersion = parseInt(value, 10) || 0;
|
|
1072
|
+
} else if (key === "freshnessStatus") {
|
|
1073
|
+
result.freshnessStatus = value;
|
|
1074
|
+
} else if (key === "lastUpdatedAt" || key === "lastUpdatedBy" || key === "sourceStage") {
|
|
1075
|
+
result[key] = value.replace(/^["']|["']$/g, "");
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
let blockCount = 0;
|
|
1079
|
+
for (const jsonMatch of content.matchAll(/```json\n([\s\S]*?)\n```/g)) {
|
|
1080
|
+
if (blockCount >= 2)
|
|
1081
|
+
break;
|
|
1082
|
+
blockCount++;
|
|
1806
1083
|
try {
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1084
|
+
const parsed = JSON.parse(jsonMatch[1]);
|
|
1085
|
+
if (parsed.fileSnapshots)
|
|
1086
|
+
result.fileSnapshots = parsed.fileSnapshots;
|
|
1087
|
+
if (parsed.explorationHistory)
|
|
1088
|
+
result.explorationHistory = parsed.explorationHistory;
|
|
1089
|
+
if (!parsed.fileSnapshots && !parsed.explorationHistory) {
|
|
1090
|
+
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
1091
|
+
if (!result.fileSnapshots)
|
|
1092
|
+
result.fileSnapshots = {};
|
|
1093
|
+
Object.assign(result.fileSnapshots, parsed);
|
|
1094
|
+
} else if (Array.isArray(parsed)) {
|
|
1095
|
+
result.explorationHistory = parsed;
|
|
1096
|
+
}
|
|
1818
1097
|
}
|
|
1098
|
+
} catch {
|
|
1099
|
+
result.freshnessStatus = "unknown";
|
|
1819
1100
|
}
|
|
1820
|
-
const newContent = content.replace(args.targetContent, args.replacementContent);
|
|
1821
|
-
writeFileSync11(fullPath, newContent, "utf-8");
|
|
1822
|
-
return `Successfully updated ${args.filePath} using hash-anchored edit.`;
|
|
1823
1101
|
}
|
|
1824
|
-
|
|
1102
|
+
return {
|
|
1103
|
+
exists: true,
|
|
1104
|
+
lastUpdatedAt: result.lastUpdatedAt || "",
|
|
1105
|
+
lastUpdatedBy: result.lastUpdatedBy || "",
|
|
1106
|
+
sourceStage: result.sourceStage || "",
|
|
1107
|
+
changedFiles: result.changedFiles || [],
|
|
1108
|
+
fileSnapshots: result.fileSnapshots || {},
|
|
1109
|
+
explorationHistory: result.explorationHistory || [],
|
|
1110
|
+
summaryVersion: result.summaryVersion || 0,
|
|
1111
|
+
freshnessStatus: result.freshnessStatus || "unknown"
|
|
1112
|
+
};
|
|
1113
|
+
}
|
|
1825
1114
|
|
|
1826
1115
|
// src/tools/council.ts
|
|
1827
|
-
import {
|
|
1828
|
-
import { appendFileSync as appendFileSync3, existsSync as existsSync15, mkdirSync as mkdirSync10 } from "fs";
|
|
1829
|
-
import { join as join14 } from "path";
|
|
1830
|
-
import { createHash as createHash3 } from "crypto";
|
|
1831
|
-
import { readFileSync as readFileSync16 } from "fs";
|
|
1116
|
+
import { readFileSync as readFileSync11 } from "fs";
|
|
1832
1117
|
var _councilCache = new Map;
|
|
1833
1118
|
var COUNCIL_CACHE_TTL_MS = 20 * 60 * 1000;
|
|
1834
1119
|
function councilCacheKey(task, agents, stateVersion, indexVersion) {
|
|
1835
1120
|
const sorted = [...agents].sort();
|
|
1836
|
-
return
|
|
1121
|
+
return createHash2("sha256").update(JSON.stringify({ task: task.trim(), agents: sorted, sv: stateVersion, iv: indexVersion })).digest("hex").slice(0, 32);
|
|
1837
1122
|
}
|
|
1838
1123
|
async function runWithConcurrencyLimit(tasks, limit) {
|
|
1839
1124
|
const results = new Array(tasks.length);
|
|
@@ -1849,20 +1134,20 @@ async function runWithConcurrencyLimit(tasks, limit) {
|
|
|
1849
1134
|
return results;
|
|
1850
1135
|
}
|
|
1851
1136
|
function createCouncilTool(client) {
|
|
1852
|
-
return
|
|
1137
|
+
return tool8({
|
|
1853
1138
|
description: "Run an ensemble of agents (Council) on the same task to reach consensus or compare approaches. Runs specialized agents in parallel (bounded concurrency) and returns their synthesized outputs.",
|
|
1854
1139
|
args: {
|
|
1855
|
-
task:
|
|
1856
|
-
agents:
|
|
1857
|
-
force_fresh:
|
|
1858
|
-
max_concurrency:
|
|
1140
|
+
task: tool8.schema.string(),
|
|
1141
|
+
agents: tool8.schema.array(tool8.schema.string()).optional(),
|
|
1142
|
+
force_fresh: tool8.schema.boolean().optional().default(false),
|
|
1143
|
+
max_concurrency: tool8.schema.number().optional().default(3)
|
|
1859
1144
|
},
|
|
1860
1145
|
async execute(args, context) {
|
|
1861
1146
|
const agents = args.agents || ["architect", "reviewer", "backend-coder"];
|
|
1862
1147
|
const concurrencyLimit = Math.max(1, Math.min(5, typeof args.max_concurrency === "number" ? args.max_concurrency : 3));
|
|
1863
1148
|
const index = readCodebaseIndex(context.directory);
|
|
1864
1149
|
const sp = statePath(context.directory);
|
|
1865
|
-
const rawState =
|
|
1150
|
+
const rawState = existsSync10(sp) ? readFileSync11(sp, "utf-8") : "";
|
|
1866
1151
|
const state = rawState ? parseState(rawState) : {};
|
|
1867
1152
|
const stateVersion = typeof state.summaryVersion === "number" ? state.summaryVersion : 0;
|
|
1868
1153
|
const indexVersion = typeof index.summaryVersion === "number" ? index.summaryVersion : 0;
|
|
@@ -1938,18 +1223,18 @@ Please synthesize these results. Identify areas of agreement, resolve conflicts,
|
|
|
1938
1223
|
function persistCouncilResult(directory, payload) {
|
|
1939
1224
|
try {
|
|
1940
1225
|
const base = codebaseDir(directory);
|
|
1941
|
-
if (!
|
|
1942
|
-
|
|
1943
|
-
const path =
|
|
1944
|
-
|
|
1226
|
+
if (!existsSync10(base))
|
|
1227
|
+
mkdirSync7(base, { recursive: true });
|
|
1228
|
+
const path = join10(base, "COUNCILS.jsonl");
|
|
1229
|
+
appendFileSync2(path, JSON.stringify(payload) + `
|
|
1945
1230
|
`, "utf-8");
|
|
1946
1231
|
} catch {}
|
|
1947
1232
|
}
|
|
1948
1233
|
|
|
1949
1234
|
// src/tools/reflect.ts
|
|
1950
|
-
import { tool as
|
|
1951
|
-
import { existsSync as
|
|
1952
|
-
import { join as
|
|
1235
|
+
import { tool as tool9 } from "@opencode-ai/plugin";
|
|
1236
|
+
import { existsSync as existsSync11, readFileSync as readFileSync12 } from "fs";
|
|
1237
|
+
import { join as join11 } from "path";
|
|
1953
1238
|
var MAX_ARTIFACT_BYTES = 4000;
|
|
1954
1239
|
function tail(text, maxBytes) {
|
|
1955
1240
|
if (text.length <= maxBytes)
|
|
@@ -1957,10 +1242,10 @@ function tail(text, maxBytes) {
|
|
|
1957
1242
|
return `... (truncated) ...
|
|
1958
1243
|
` + text.slice(-maxBytes);
|
|
1959
1244
|
}
|
|
1960
|
-
var reflectTool =
|
|
1245
|
+
var reflectTool = tool9({
|
|
1961
1246
|
description: "Gather session artifacts (decisions, telemetry, failures, policies) and return a structured " + "reflection context that the agent can reason over to produce self-improvement proposals.",
|
|
1962
1247
|
args: {
|
|
1963
|
-
scope:
|
|
1248
|
+
scope: tool9.schema.enum(["session", "project"]).optional().describe("'session' (default) uses only recent artifacts; 'project' includes all historical data")
|
|
1964
1249
|
},
|
|
1965
1250
|
async execute(args, context) {
|
|
1966
1251
|
const root = context.directory;
|
|
@@ -1978,11 +1263,11 @@ var reflectTool = tool11({
|
|
|
1978
1263
|
];
|
|
1979
1264
|
let found = 0;
|
|
1980
1265
|
for (const [rel, label] of ARTIFACT_PATHS) {
|
|
1981
|
-
const full =
|
|
1982
|
-
if (!
|
|
1266
|
+
const full = join11(root, rel);
|
|
1267
|
+
if (!existsSync11(full))
|
|
1983
1268
|
continue;
|
|
1984
1269
|
try {
|
|
1985
|
-
const raw =
|
|
1270
|
+
const raw = readFileSync12(full, "utf-8").trim();
|
|
1986
1271
|
if (!raw)
|
|
1987
1272
|
continue;
|
|
1988
1273
|
const count = raw.split(`
|
|
@@ -2002,16 +1287,16 @@ var reflectTool = tool11({
|
|
|
2002
1287
|
});
|
|
2003
1288
|
|
|
2004
1289
|
// src/tools/codegraph-tool.ts
|
|
2005
|
-
import { tool as
|
|
1290
|
+
import { tool as tool10 } from "@opencode-ai/plugin";
|
|
2006
1291
|
|
|
2007
1292
|
// src/services/codegraph.ts
|
|
2008
1293
|
import { spawnSync } from "child_process";
|
|
2009
|
-
import { existsSync as
|
|
2010
|
-
import { join as
|
|
1294
|
+
import { existsSync as existsSync12, readFileSync as readFileSync13, writeFileSync as writeFileSync10, mkdirSync as mkdirSync8 } from "fs";
|
|
1295
|
+
import { join as join12 } from "path";
|
|
2011
1296
|
var CODEGRAPH_META_FILE = "CODEGRAPH.md";
|
|
2012
1297
|
var MAX_FRESHNESS_MS = 30 * 60 * 1000;
|
|
2013
1298
|
function metaPath(dir) {
|
|
2014
|
-
return
|
|
1299
|
+
return join12(codebaseDir(dir), CODEGRAPH_META_FILE);
|
|
2015
1300
|
}
|
|
2016
1301
|
function isCodegraphInstalled() {
|
|
2017
1302
|
try {
|
|
@@ -2026,11 +1311,11 @@ function isCodegraphInstalled() {
|
|
|
2026
1311
|
}
|
|
2027
1312
|
}
|
|
2028
1313
|
function isCodegraphIndexed(dir) {
|
|
2029
|
-
return
|
|
1314
|
+
return existsSync12(join12(dir, ".codegraph", "codegraph.db"));
|
|
2030
1315
|
}
|
|
2031
1316
|
function readCodegraphMeta(dir) {
|
|
2032
1317
|
const path = metaPath(dir);
|
|
2033
|
-
if (!
|
|
1318
|
+
if (!existsSync12(path)) {
|
|
2034
1319
|
return {
|
|
2035
1320
|
installed: false,
|
|
2036
1321
|
indexed: false,
|
|
@@ -2043,7 +1328,7 @@ function readCodegraphMeta(dir) {
|
|
|
2043
1328
|
};
|
|
2044
1329
|
}
|
|
2045
1330
|
try {
|
|
2046
|
-
const content =
|
|
1331
|
+
const content = readFileSync13(path, "utf-8");
|
|
2047
1332
|
return parseCodegraphMeta(content);
|
|
2048
1333
|
} catch {
|
|
2049
1334
|
return {
|
|
@@ -2110,8 +1395,8 @@ function parseCodegraphMeta(content) {
|
|
|
2110
1395
|
}
|
|
2111
1396
|
function writeCodegraphMeta(dir, meta) {
|
|
2112
1397
|
const base = codebaseDir(dir);
|
|
2113
|
-
if (!
|
|
2114
|
-
|
|
1398
|
+
if (!existsSync12(base))
|
|
1399
|
+
mkdirSync8(base, { recursive: true });
|
|
2115
1400
|
const lines = [
|
|
2116
1401
|
"# Codegraph Metadata",
|
|
2117
1402
|
"",
|
|
@@ -2124,7 +1409,7 @@ function writeCodegraphMeta(dir, meta) {
|
|
|
2124
1409
|
`**installLog:** ${meta.installLog}`,
|
|
2125
1410
|
`**indexLog:** ${meta.indexLog}`
|
|
2126
1411
|
];
|
|
2127
|
-
|
|
1412
|
+
writeFileSync10(metaPath(dir), lines.join(`
|
|
2128
1413
|
`), "utf-8");
|
|
2129
1414
|
}
|
|
2130
1415
|
function isCodegraphFresh(dir, maxAgeMs = MAX_FRESHNESS_MS) {
|
|
@@ -2335,11 +1620,11 @@ function markCodegraphStale(dir) {
|
|
|
2335
1620
|
}
|
|
2336
1621
|
|
|
2337
1622
|
// src/tools/codegraph-tool.ts
|
|
2338
|
-
var codegraphTool =
|
|
1623
|
+
var codegraphTool = tool10({
|
|
2339
1624
|
description: "Manage codegraph lifecycle only: check installation, install, init/rebuild the index, refresh (incremental sync), " + "query status, or mark-stale. Valid actions: check | install | init | refresh | status | mark-stale. " + "Do NOT use this tool for code intelligence queries (files, search, callers, callees, etc.) — " + "those are available as codegraph MCP tools (codegraph_files, codegraph_search, codegraph_context, " + "codegraph_explore, codegraph_callers, codegraph_callees, codegraph_impact, codegraph_trace) " + "when the index is ready.",
|
|
2340
1625
|
args: {
|
|
2341
|
-
action:
|
|
2342
|
-
agent:
|
|
1626
|
+
action: tool10.schema.enum(["check", "install", "init", "refresh", "status", "mark-stale"]),
|
|
1627
|
+
agent: tool10.schema.string().optional()
|
|
2343
1628
|
},
|
|
2344
1629
|
async execute(args, context) {
|
|
2345
1630
|
const dir = context.directory ?? process.cwd();
|
|
@@ -2428,21 +1713,21 @@ var codegraphTool = tool12({
|
|
|
2428
1713
|
});
|
|
2429
1714
|
|
|
2430
1715
|
// src/tools/load-rules.ts
|
|
2431
|
-
import { tool as
|
|
2432
|
-
import { existsSync as
|
|
2433
|
-
import { join as
|
|
1716
|
+
import { tool as tool11 } from "@opencode-ai/plugin";
|
|
1717
|
+
import { existsSync as existsSync13, readFileSync as readFileSync14 } from "fs";
|
|
1718
|
+
import { join as join13, dirname as dirname2 } from "path";
|
|
2434
1719
|
import { fileURLToPath } from "url";
|
|
2435
|
-
var RULES_DIR =
|
|
1720
|
+
var RULES_DIR = join13(dirname2(fileURLToPath(import.meta.url)), "..", "rules");
|
|
2436
1721
|
var _loadedPaths = new Set;
|
|
2437
|
-
var loadRulesTool =
|
|
1722
|
+
var loadRulesTool = tool11({
|
|
2438
1723
|
description: "Load additional rule modules on demand for the current workflow stage. " + "Use this at the start of a new stage (execute, verify, fix-bug) to load " + "coding-style, security, testing, and language-specific rules that were not " + "injected at startup. Returns the full text of selected rules. " + "Already-loaded rules are not returned again (suppressed to avoid duplication).",
|
|
2439
1724
|
args: {
|
|
2440
|
-
stage:
|
|
2441
|
-
languages:
|
|
2442
|
-
force_reload:
|
|
1725
|
+
stage: tool11.schema.string().optional().describe("Current workflow stage: discuss | plan | execute | verify | fix-bug | write-docs"),
|
|
1726
|
+
languages: tool11.schema.array(tool11.schema.string()).optional().describe("Project languages to load rules for, e.g. ['typescript']. " + "Omit to use all languages (returns all matching stage rules)."),
|
|
1727
|
+
force_reload: tool11.schema.boolean().optional().default(false).describe("When true, return rules even if they were already loaded in this session. " + "Use only when stage context has changed and you need a fresh load.")
|
|
2443
1728
|
},
|
|
2444
1729
|
async execute(args) {
|
|
2445
|
-
const rulesDir =
|
|
1730
|
+
const rulesDir = existsSync13(RULES_DIR) ? RULES_DIR : null;
|
|
2446
1731
|
if (!rulesDir) {
|
|
2447
1732
|
return JSON.stringify({
|
|
2448
1733
|
loaded: [],
|
|
@@ -2468,7 +1753,7 @@ var loadRulesTool = tool13({
|
|
|
2468
1753
|
continue;
|
|
2469
1754
|
}
|
|
2470
1755
|
try {
|
|
2471
|
-
const text =
|
|
1756
|
+
const text = readFileSync14(rule.path, "utf-8");
|
|
2472
1757
|
contents.push(`## ${name}
|
|
2473
1758
|
|
|
2474
1759
|
${text}`);
|
|
@@ -2499,11 +1784,11 @@ ${text}`);
|
|
|
2499
1784
|
function ruleShortName(rule) {
|
|
2500
1785
|
return rule.path.replace(RULES_DIR + "/", "").replace(/\.md$/, "");
|
|
2501
1786
|
}
|
|
2502
|
-
var listRulesTool =
|
|
1787
|
+
var listRulesTool = tool11({
|
|
2503
1788
|
description: "List all available FlowDeck rule modules with their metadata (description, always_on, " + "stages, languages). Use this before calling load-rules to see what is available. " + "Does NOT load rule content — only returns metadata for discovery.",
|
|
2504
1789
|
args: {},
|
|
2505
1790
|
async execute() {
|
|
2506
|
-
const rulesDir =
|
|
1791
|
+
const rulesDir = existsSync13(RULES_DIR) ? RULES_DIR : null;
|
|
2507
1792
|
if (!rulesDir) {
|
|
2508
1793
|
return JSON.stringify({ rules: [], error: `Rules directory not found at ${RULES_DIR}` });
|
|
2509
1794
|
}
|
|
@@ -2523,13 +1808,13 @@ var listRulesTool = tool13({
|
|
|
2523
1808
|
});
|
|
2524
1809
|
|
|
2525
1810
|
// src/tools/rtk-setup.ts
|
|
2526
|
-
import { tool as
|
|
1811
|
+
import { tool as tool12 } from "@opencode-ai/plugin";
|
|
2527
1812
|
|
|
2528
1813
|
// src/services/rtk-manager.ts
|
|
2529
1814
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
2530
|
-
import { existsSync as
|
|
2531
|
-
import { homedir
|
|
2532
|
-
import { join as
|
|
1815
|
+
import { existsSync as existsSync14 } from "fs";
|
|
1816
|
+
import { homedir } from "os";
|
|
1817
|
+
import { join as join14 } from "path";
|
|
2533
1818
|
|
|
2534
1819
|
// src/services/rtk-policy.ts
|
|
2535
1820
|
var SUPPORTED_COMMANDS = new Set([
|
|
@@ -2575,7 +1860,7 @@ var INSTALL_INSTRUCTIONS = [
|
|
|
2575
1860
|
"After installation, call rtk-setup again to verify detection."
|
|
2576
1861
|
].join(`
|
|
2577
1862
|
`);
|
|
2578
|
-
var CANDIDATE_PATHS = [
|
|
1863
|
+
var CANDIDATE_PATHS = [join14(homedir(), ".local", "bin", "rtk"), "/usr/local/bin/rtk", "/usr/bin/rtk"];
|
|
2579
1864
|
function detectRtk() {
|
|
2580
1865
|
const fromPath = spawnSync2("rtk", ["--version"], { encoding: "utf-8", timeout: 5000 });
|
|
2581
1866
|
if (fromPath.status === 0) {
|
|
@@ -2584,7 +1869,7 @@ function detectRtk() {
|
|
|
2584
1869
|
return { installed: true, binPath: "rtk", version };
|
|
2585
1870
|
}
|
|
2586
1871
|
for (const candidate of CANDIDATE_PATHS) {
|
|
2587
|
-
if (!
|
|
1872
|
+
if (!existsSync14(candidate))
|
|
2588
1873
|
continue;
|
|
2589
1874
|
const result = spawnSync2(candidate, ["--version"], { encoding: "utf-8", timeout: 5000 });
|
|
2590
1875
|
if (result.status === 0) {
|
|
@@ -2662,7 +1947,7 @@ function getRtkStatus(opts) {
|
|
|
2662
1947
|
}
|
|
2663
1948
|
|
|
2664
1949
|
// src/tools/rtk-setup.ts
|
|
2665
|
-
var rtkSetupTool =
|
|
1950
|
+
var rtkSetupTool = tool12({
|
|
2666
1951
|
description: [
|
|
2667
1952
|
"Detect, initialize, and report status of rtk (output compression proxy for CLI commands).",
|
|
2668
1953
|
"rtk reduces noisy CLI output (git, npm, test runners, linters, docker) by 60-90%.",
|
|
@@ -2670,7 +1955,7 @@ var rtkSetupTool = tool14({
|
|
|
2670
1955
|
"When RTK_INSTALLED=true in the environment, use `$RTK_BIN git status` for compressed output."
|
|
2671
1956
|
].join(" "),
|
|
2672
1957
|
args: {
|
|
2673
|
-
action:
|
|
1958
|
+
action: tool12.schema.enum(["status", "init"]).optional().describe("'status' — detect and report rtk state (default). " + "'init' — detect, then run `rtk init -g` to install the bash hook. " + "Use 'init' only once per environment setup.")
|
|
2674
1959
|
},
|
|
2675
1960
|
async execute(args) {
|
|
2676
1961
|
const action = args.action ?? "status";
|
|
@@ -2710,15 +1995,99 @@ var rtkSetupTool = tool14({
|
|
|
2710
1995
|
});
|
|
2711
1996
|
|
|
2712
1997
|
// src/hooks/guard-rails.ts
|
|
2713
|
-
import { existsSync as
|
|
2714
|
-
import { join as
|
|
1998
|
+
import { existsSync as existsSync16, readFileSync as readFileSync16 } from "fs";
|
|
1999
|
+
import { join as join16 } from "path";
|
|
2000
|
+
|
|
2001
|
+
// src/lib/task-routing.ts
|
|
2002
|
+
var UI_HEAVY_KEYWORDS = [
|
|
2003
|
+
"landing page",
|
|
2004
|
+
"marketing site",
|
|
2005
|
+
"website",
|
|
2006
|
+
"web app",
|
|
2007
|
+
"mobile app",
|
|
2008
|
+
"app screen",
|
|
2009
|
+
"dashboard",
|
|
2010
|
+
"admin panel",
|
|
2011
|
+
"settings page",
|
|
2012
|
+
"onboarding ux",
|
|
2013
|
+
"kanban",
|
|
2014
|
+
"design system",
|
|
2015
|
+
"responsive",
|
|
2016
|
+
"ui",
|
|
2017
|
+
"ux",
|
|
2018
|
+
"cta",
|
|
2019
|
+
"conversion flow",
|
|
2020
|
+
"saas interface",
|
|
2021
|
+
"user-facing"
|
|
2022
|
+
];
|
|
2023
|
+
var NON_UI_KEYWORDS = [
|
|
2024
|
+
"backend",
|
|
2025
|
+
"infrastructure",
|
|
2026
|
+
"migration",
|
|
2027
|
+
"pipeline",
|
|
2028
|
+
"api only",
|
|
2029
|
+
"database only",
|
|
2030
|
+
"cli",
|
|
2031
|
+
"worker"
|
|
2032
|
+
];
|
|
2033
|
+
function isUiHeavyTask(input) {
|
|
2034
|
+
const normalized = input.trim().toLowerCase();
|
|
2035
|
+
if (!normalized)
|
|
2036
|
+
return false;
|
|
2037
|
+
const hasUiSignal = UI_HEAVY_KEYWORDS.some((keyword) => normalized.includes(keyword));
|
|
2038
|
+
if (!hasUiSignal)
|
|
2039
|
+
return false;
|
|
2040
|
+
const hasOnlyNonUiSignals = NON_UI_KEYWORDS.some((keyword) => normalized.includes(keyword)) && !normalized.includes("frontend");
|
|
2041
|
+
return !hasOnlyNonUiSignals;
|
|
2042
|
+
}
|
|
2043
|
+
|
|
2044
|
+
// src/config/loader.ts
|
|
2045
|
+
import { existsSync as existsSync15, readFileSync as readFileSync15 } from "fs";
|
|
2046
|
+
import { join as join15 } from "path";
|
|
2047
|
+
import { homedir as homedir2 } from "os";
|
|
2048
|
+
var CONFIG_FILENAME = "flowdeck.json";
|
|
2049
|
+
function getGlobalConfigDir() {
|
|
2050
|
+
return process.env.OPENCODE_CONFIG_DIR || (process.env.XDG_CONFIG_HOME ? join15(process.env.XDG_CONFIG_HOME, "opencode") : join15(homedir2(), ".config", "opencode"));
|
|
2051
|
+
}
|
|
2052
|
+
function loadFlowDeckConfig(directory) {
|
|
2053
|
+
const candidates = [];
|
|
2054
|
+
if (directory) {
|
|
2055
|
+
candidates.push(join15(directory, ".opencode", CONFIG_FILENAME));
|
|
2056
|
+
}
|
|
2057
|
+
candidates.push(join15(getGlobalConfigDir(), CONFIG_FILENAME));
|
|
2058
|
+
for (const configPath of candidates) {
|
|
2059
|
+
if (existsSync15(configPath)) {
|
|
2060
|
+
try {
|
|
2061
|
+
const content = readFileSync15(configPath, "utf-8");
|
|
2062
|
+
return JSON.parse(content);
|
|
2063
|
+
} catch {}
|
|
2064
|
+
}
|
|
2065
|
+
}
|
|
2066
|
+
return {};
|
|
2067
|
+
}
|
|
2068
|
+
function resolveDesignFirstConfig(config) {
|
|
2069
|
+
return {
|
|
2070
|
+
enabled: config.designFirst?.enabled ?? true,
|
|
2071
|
+
enforcement: config.designFirst?.enforcement ?? "strict",
|
|
2072
|
+
requireApprovalBeforeImplementation: config.designFirst?.requireApprovalBeforeImplementation ?? true,
|
|
2073
|
+
modelOverrides: config.designFirst?.modelOverrides ?? {},
|
|
2074
|
+
defaultSkillsByTaskType: config.designFirst?.defaultSkillsByTaskType ?? {
|
|
2075
|
+
"landing-page": ["landing-page-design", "wireframe-planning", "design-system-definition", "frontend-handoff"],
|
|
2076
|
+
dashboard: ["dashboard-design", "ui-ux-planning", "wireframe-planning", "responsive-review"],
|
|
2077
|
+
"admin-panel": ["ui-ux-planning", "wireframe-planning", "design-system-definition", "frontend-handoff"],
|
|
2078
|
+
"app-screen": ["app-shell-design", "ui-ux-planning", "wireframe-planning", "responsive-review"],
|
|
2079
|
+
"general-ui": ["ui-ux-planning", "wireframe-planning", "design-system-definition", "frontend-handoff"]
|
|
2080
|
+
}
|
|
2081
|
+
};
|
|
2082
|
+
}
|
|
2083
|
+
// src/hooks/guard-rails.ts
|
|
2715
2084
|
var PLANNING_DIR2 = ".planning";
|
|
2716
2085
|
var CONFIG_FILE = "config.json";
|
|
2717
2086
|
var STATE_FILE2 = "STATE.md";
|
|
2718
2087
|
function resolveExecutionMode(configPath, trustScore, volatility) {
|
|
2719
|
-
if (
|
|
2088
|
+
if (existsSync16(configPath)) {
|
|
2720
2089
|
try {
|
|
2721
|
-
const config = JSON.parse(
|
|
2090
|
+
const config = JSON.parse(readFileSync16(configPath, "utf-8"));
|
|
2722
2091
|
if (config.execution_mode === "review-only")
|
|
2723
2092
|
return "review-only";
|
|
2724
2093
|
if (config.execution_mode === "guarded")
|
|
@@ -2772,22 +2141,22 @@ async function guardRailsHook(ctx, input, _output) {
|
|
|
2772
2141
|
if (!ENABLED)
|
|
2773
2142
|
return;
|
|
2774
2143
|
const dir = ctx.directory;
|
|
2775
|
-
const planningDirPath =
|
|
2144
|
+
const planningDirPath = join16(dir, PLANNING_DIR2);
|
|
2776
2145
|
const codebaseDirectory = codebaseDir(dir);
|
|
2777
|
-
const configPath =
|
|
2778
|
-
const statePath2 =
|
|
2146
|
+
const configPath = join16(planningDirPath, CONFIG_FILE);
|
|
2147
|
+
const statePath2 = join16(planningDirPath, STATE_FILE2);
|
|
2779
2148
|
const workspaceRoot = findWorkspaceRoot(dir);
|
|
2780
2149
|
if (workspaceRoot && dir !== workspaceRoot) {
|
|
2781
2150
|
const config = getWorkspaceConfig(dir);
|
|
2782
|
-
if (config && config.workspace_mode === "shared" && !
|
|
2151
|
+
if (config && config.workspace_mode === "shared" && !existsSync16(planningDirPath)) {
|
|
2783
2152
|
const msg = `No .planning/ in this sub-repo. Switch to workspace root: cd ${workspaceRoot}`;
|
|
2784
2153
|
throw new Error(`[flowdeck] BLOCK: ${msg}`);
|
|
2785
2154
|
}
|
|
2786
2155
|
}
|
|
2787
2156
|
if (input.tool === "write" || input.tool === "edit") {
|
|
2788
|
-
if (!
|
|
2157
|
+
if (!existsSync16(planningDirPath))
|
|
2789
2158
|
return;
|
|
2790
|
-
if (!
|
|
2159
|
+
if (!existsSync16(codebaseDirectory)) {
|
|
2791
2160
|
throw new Error(`[flowdeck] WARNING: .codebase/ not found. Run /fd-map-codebase to map the codebase.`);
|
|
2792
2161
|
}
|
|
2793
2162
|
const execMode = resolveExecutionMode(configPath, null);
|
|
@@ -2843,15 +2212,15 @@ function getDesignGateMessage(dir) {
|
|
|
2843
2212
|
}
|
|
2844
2213
|
function planSuggestsUiHeavy(dir, phase) {
|
|
2845
2214
|
const planPath = phasePlanPath(dir, phase);
|
|
2846
|
-
if (!
|
|
2215
|
+
if (!existsSync16(planPath))
|
|
2847
2216
|
return false;
|
|
2848
|
-
const planContent =
|
|
2217
|
+
const planContent = readFileSync16(planPath, "utf-8");
|
|
2849
2218
|
return isUiHeavyTask(planContent);
|
|
2850
2219
|
}
|
|
2851
2220
|
function effectiveSeverity(configPath, statePath2) {
|
|
2852
|
-
if (
|
|
2221
|
+
if (existsSync16(configPath)) {
|
|
2853
2222
|
try {
|
|
2854
|
-
const configContent =
|
|
2223
|
+
const configContent = readFileSync16(configPath, "utf-8");
|
|
2855
2224
|
const config = JSON.parse(configContent);
|
|
2856
2225
|
if (config.guard_enforcement === "warn")
|
|
2857
2226
|
return "warn";
|
|
@@ -2867,10 +2236,10 @@ function getEffectiveSeverity(configPath, statePath2) {
|
|
|
2867
2236
|
return effectiveSeverity(configPath, statePath2);
|
|
2868
2237
|
}
|
|
2869
2238
|
function getPlanConfirmed(statePath2) {
|
|
2870
|
-
if (!
|
|
2239
|
+
if (!existsSync16(statePath2))
|
|
2871
2240
|
return false;
|
|
2872
2241
|
try {
|
|
2873
|
-
const content =
|
|
2242
|
+
const content = readFileSync16(statePath2, "utf-8");
|
|
2874
2243
|
const match = content.match(/plan_confirmed:\s*(true|false)/i);
|
|
2875
2244
|
return match ? match[1].toLowerCase() === "true" : false;
|
|
2876
2245
|
} catch {
|
|
@@ -2878,32 +2247,32 @@ function getPlanConfirmed(statePath2) {
|
|
|
2878
2247
|
}
|
|
2879
2248
|
}
|
|
2880
2249
|
function getWarningMessage(planningDir2) {
|
|
2881
|
-
if (!
|
|
2250
|
+
if (!existsSync16(join16(planningDir2, STATE_FILE2))) {
|
|
2882
2251
|
return "No STATE.md found. Run /fd-map-codebase then /fd-new-feature to start a feature.";
|
|
2883
2252
|
}
|
|
2884
2253
|
return "Plan not confirmed. Run /fd-plan and confirm to enable execution.";
|
|
2885
2254
|
}
|
|
2886
2255
|
function getBlockMessage(planningDir2) {
|
|
2887
|
-
if (!
|
|
2256
|
+
if (!existsSync16(join16(planningDir2, STATE_FILE2))) {
|
|
2888
2257
|
return "No STATE.md found. Run /fd-map-codebase then /fd-new-feature to start a feature.";
|
|
2889
2258
|
}
|
|
2890
2259
|
return "Plan not confirmed. Run /fd-plan and confirm to enable execution.";
|
|
2891
2260
|
}
|
|
2892
2261
|
|
|
2893
2262
|
// src/hooks/tool-guard.ts
|
|
2894
|
-
import { existsSync as
|
|
2895
|
-
import { join as
|
|
2263
|
+
import { existsSync as existsSync17, readFileSync as readFileSync17 } from "fs";
|
|
2264
|
+
import { join as join17 } from "path";
|
|
2896
2265
|
var IS_ENABLED = () => process.env.FLOWDECK_TOOL_GUARD_ENABLED === "on";
|
|
2897
2266
|
var BLOCKED_PATTERNS = {
|
|
2898
2267
|
read: [".env", ".pem", ".key", ".secret"],
|
|
2899
2268
|
write: ["node_modules"],
|
|
2900
2269
|
bash: ["rm -rf"]
|
|
2901
2270
|
};
|
|
2902
|
-
function isBlocked(
|
|
2903
|
-
const patterns = BLOCKED_PATTERNS[
|
|
2271
|
+
function isBlocked(tool13, args) {
|
|
2272
|
+
const patterns = BLOCKED_PATTERNS[tool13];
|
|
2904
2273
|
if (!patterns)
|
|
2905
2274
|
return null;
|
|
2906
|
-
if (
|
|
2275
|
+
if (tool13 === "bash") {
|
|
2907
2276
|
const cmd = args.command;
|
|
2908
2277
|
if (!cmd)
|
|
2909
2278
|
return null;
|
|
@@ -2914,7 +2283,7 @@ function isBlocked(tool15, args) {
|
|
|
2914
2283
|
}
|
|
2915
2284
|
return null;
|
|
2916
2285
|
}
|
|
2917
|
-
if (
|
|
2286
|
+
if (tool13 === "read") {
|
|
2918
2287
|
const filePath = args.filePath;
|
|
2919
2288
|
if (!filePath)
|
|
2920
2289
|
return null;
|
|
@@ -2925,7 +2294,7 @@ function isBlocked(tool15, args) {
|
|
|
2925
2294
|
}
|
|
2926
2295
|
return null;
|
|
2927
2296
|
}
|
|
2928
|
-
if (
|
|
2297
|
+
if (tool13 === "write") {
|
|
2929
2298
|
const filePath = args.filePath;
|
|
2930
2299
|
if (!filePath)
|
|
2931
2300
|
return null;
|
|
@@ -2939,11 +2308,11 @@ function isBlocked(tool15, args) {
|
|
|
2939
2308
|
return null;
|
|
2940
2309
|
}
|
|
2941
2310
|
function checkArchConstraint(directory, filePath) {
|
|
2942
|
-
const constraintsPath =
|
|
2943
|
-
if (!
|
|
2311
|
+
const constraintsPath = join17(codebaseDir(directory), "CONSTRAINTS.md");
|
|
2312
|
+
if (!existsSync17(constraintsPath))
|
|
2944
2313
|
return null;
|
|
2945
2314
|
try {
|
|
2946
|
-
const content =
|
|
2315
|
+
const content = readFileSync17(constraintsPath, "utf-8");
|
|
2947
2316
|
const match = content.match(/## Forbidden Paths\n([\s\S]*?)(?:\n##|$)/);
|
|
2948
2317
|
if (!match)
|
|
2949
2318
|
return null;
|
|
@@ -2984,9 +2353,9 @@ function isUiDesignApprovalRequired(directory) {
|
|
|
2984
2353
|
return !(state.design_stage === "handoff_complete" && state.design_approved);
|
|
2985
2354
|
}
|
|
2986
2355
|
const planPath = phasePlanPath(directory, state.phase || 1);
|
|
2987
|
-
if (!
|
|
2356
|
+
if (!existsSync17(planPath))
|
|
2988
2357
|
return false;
|
|
2989
|
-
const planContent =
|
|
2358
|
+
const planContent = readFileSync17(planPath, "utf-8");
|
|
2990
2359
|
if (!isUiHeavyTask(planContent))
|
|
2991
2360
|
return false;
|
|
2992
2361
|
return !(state.design_stage === "handoff_complete" && state.design_approved);
|
|
@@ -3015,18 +2384,18 @@ async function toolGuardHook(ctx, input, output) {
|
|
|
3015
2384
|
}
|
|
3016
2385
|
|
|
3017
2386
|
// src/hooks/session-start.ts
|
|
3018
|
-
import { existsSync as
|
|
2387
|
+
import { existsSync as existsSync18, readFileSync as readFileSync18 } from "fs";
|
|
3019
2388
|
async function sessionStartHook(ctx) {
|
|
3020
2389
|
const planningDir2 = ctx.directory + "/.planning";
|
|
3021
2390
|
const codebaseDirectory = codebaseDir(ctx.directory);
|
|
3022
2391
|
const workspaceRoot = findWorkspaceRoot(ctx.directory);
|
|
3023
2392
|
const config = workspaceRoot ? getWorkspaceConfig(ctx.directory) : null;
|
|
3024
|
-
if (!
|
|
2393
|
+
if (!existsSync18(planningDir2)) {
|
|
3025
2394
|
return {
|
|
3026
2395
|
flowdeck_phase: null,
|
|
3027
2396
|
flowdeck_status: "no_plan",
|
|
3028
2397
|
flowdeck_warning: "Run /fd-map-codebase to index the codebase, then /fd-new-feature to start a feature.",
|
|
3029
|
-
flowdeck_has_codebase:
|
|
2398
|
+
flowdeck_has_codebase: existsSync18(codebaseDirectory),
|
|
3030
2399
|
...workspaceRoot && config?.sub_repos ? {
|
|
3031
2400
|
flowdeck_workspace_root: workspaceRoot,
|
|
3032
2401
|
flowdeck_sub_repos: config.sub_repos,
|
|
@@ -3037,7 +2406,7 @@ async function sessionStartHook(ctx) {
|
|
|
3037
2406
|
}
|
|
3038
2407
|
try {
|
|
3039
2408
|
const stateFilePath = statePath(ctx.directory);
|
|
3040
|
-
const content =
|
|
2409
|
+
const content = readFileSync18(stateFilePath, "utf-8");
|
|
3041
2410
|
const state = parseState(content);
|
|
3042
2411
|
const currentPhase = state["current_phase"] || {};
|
|
3043
2412
|
const result = {
|
|
@@ -3045,7 +2414,7 @@ async function sessionStartHook(ctx) {
|
|
|
3045
2414
|
flowdeck_status: currentPhase["status"] ?? null,
|
|
3046
2415
|
flowdeck_steps_pending: currentPhase["steps_pending"] ?? null,
|
|
3047
2416
|
flowdeck_last_action: currentPhase["last_action"] ?? null,
|
|
3048
|
-
flowdeck_has_codebase:
|
|
2417
|
+
flowdeck_has_codebase: existsSync18(codebaseDirectory)
|
|
3049
2418
|
};
|
|
3050
2419
|
if (workspaceRoot && config?.sub_repos && config.sub_repos.length > 0) {
|
|
3051
2420
|
result.flowdeck_workspace_root = workspaceRoot;
|
|
@@ -3059,7 +2428,7 @@ async function sessionStartHook(ctx) {
|
|
|
3059
2428
|
flowdeck_phase: null,
|
|
3060
2429
|
flowdeck_status: "error",
|
|
3061
2430
|
flowdeck_warning: "State file unreadable. Continuing without flowdeck context.",
|
|
3062
|
-
flowdeck_has_codebase:
|
|
2431
|
+
flowdeck_has_codebase: existsSync18(codebaseDirectory)
|
|
3063
2432
|
};
|
|
3064
2433
|
if (workspaceRoot && config?.sub_repos && config.sub_repos.length > 0) {
|
|
3065
2434
|
result.flowdeck_workspace_root = workspaceRoot;
|
|
@@ -3191,13 +2560,13 @@ class NotificationController {
|
|
|
3191
2560
|
return this.lastNotifiedKey;
|
|
3192
2561
|
}
|
|
3193
2562
|
}
|
|
3194
|
-
function notifyPermissionNeeded(
|
|
3195
|
-
notify("FlowDeck Permission Required", `Agent needs approval to use tool: ${
|
|
2563
|
+
function notifyPermissionNeeded(tool13) {
|
|
2564
|
+
notify("FlowDeck Permission Required", `Agent needs approval to use tool: ${tool13}`, "critical");
|
|
3196
2565
|
}
|
|
3197
2566
|
|
|
3198
2567
|
// src/hooks/patch-trust.ts
|
|
3199
|
-
import { existsSync as
|
|
3200
|
-
import { join as
|
|
2568
|
+
import { existsSync as existsSync19, readFileSync as readFileSync19 } from "fs";
|
|
2569
|
+
import { join as join18 } from "path";
|
|
3201
2570
|
var HIGH_RISK_KEYWORDS = [
|
|
3202
2571
|
"password",
|
|
3203
2572
|
"secret",
|
|
@@ -3219,11 +2588,11 @@ var HIGH_RISK_KEYWORDS = [
|
|
|
3219
2588
|
"privilege"
|
|
3220
2589
|
];
|
|
3221
2590
|
function loadFailedPaths(directory) {
|
|
3222
|
-
const p =
|
|
3223
|
-
if (!
|
|
2591
|
+
const p = join18(codebaseDir(directory), "FAILURES.json");
|
|
2592
|
+
if (!existsSync19(p))
|
|
3224
2593
|
return [];
|
|
3225
2594
|
try {
|
|
3226
|
-
const data = JSON.parse(
|
|
2595
|
+
const data = JSON.parse(readFileSync19(p, "utf-8"));
|
|
3227
2596
|
return (data.entries ?? []).flatMap((e) => e.affected_paths ?? []);
|
|
3228
2597
|
} catch {
|
|
3229
2598
|
return [];
|
|
@@ -3276,8 +2645,8 @@ async function patchTrustHook(ctx, input, output) {
|
|
|
3276
2645
|
}
|
|
3277
2646
|
|
|
3278
2647
|
// src/hooks/decision-trace-hook.ts
|
|
3279
|
-
import { existsSync as
|
|
3280
|
-
import { join as
|
|
2648
|
+
import { existsSync as existsSync20, mkdirSync as mkdirSync9, appendFileSync as appendFileSync3 } from "fs";
|
|
2649
|
+
import { join as join19 } from "path";
|
|
3281
2650
|
async function decisionTraceHook(ctx, input, output) {
|
|
3282
2651
|
if (input.tool !== "write" && input.tool !== "edit")
|
|
3283
2652
|
return;
|
|
@@ -3286,8 +2655,8 @@ async function decisionTraceHook(ctx, input, output) {
|
|
|
3286
2655
|
return;
|
|
3287
2656
|
const base = codebaseDir(ctx.directory);
|
|
3288
2657
|
try {
|
|
3289
|
-
if (!
|
|
3290
|
-
|
|
2658
|
+
if (!existsSync20(base))
|
|
2659
|
+
mkdirSync9(base, { recursive: true });
|
|
3291
2660
|
const entry = {
|
|
3292
2661
|
timestamp: new Date().toISOString(),
|
|
3293
2662
|
file_path: filePath,
|
|
@@ -3299,14 +2668,14 @@ async function decisionTraceHook(ctx, input, output) {
|
|
|
3299
2668
|
risk_level: "unknown",
|
|
3300
2669
|
auto_recorded: true
|
|
3301
2670
|
};
|
|
3302
|
-
|
|
2671
|
+
appendFileSync3(join19(base, "DECISIONS.jsonl"), JSON.stringify(entry) + `
|
|
3303
2672
|
`, "utf-8");
|
|
3304
2673
|
} catch {}
|
|
3305
2674
|
}
|
|
3306
2675
|
|
|
3307
2676
|
// src/services/approval-manager.ts
|
|
3308
|
-
import { existsSync as
|
|
3309
|
-
import { join as
|
|
2677
|
+
import { existsSync as existsSync21, readFileSync as readFileSync20, writeFileSync as writeFileSync11, mkdirSync as mkdirSync10 } from "fs";
|
|
2678
|
+
import { join as join20 } from "path";
|
|
3310
2679
|
var APPROVAL_TTL_MS = 30 * 60 * 1000;
|
|
3311
2680
|
var SENSITIVE_PATTERNS = [
|
|
3312
2681
|
/auth/i,
|
|
@@ -3343,20 +2712,20 @@ function isSensitivePath(filePath) {
|
|
|
3343
2712
|
return SENSITIVE_PATTERNS.some((p) => p.test(filePath));
|
|
3344
2713
|
}
|
|
3345
2714
|
function approvalsPath(dir) {
|
|
3346
|
-
return
|
|
2715
|
+
return join20(codebaseDir(dir), "APPROVALS.json");
|
|
3347
2716
|
}
|
|
3348
|
-
function
|
|
2717
|
+
function loadStore(dir) {
|
|
3349
2718
|
const p = approvalsPath(dir);
|
|
3350
|
-
if (!
|
|
2719
|
+
if (!existsSync21(p))
|
|
3351
2720
|
return { requests: [] };
|
|
3352
2721
|
try {
|
|
3353
|
-
return JSON.parse(
|
|
2722
|
+
return JSON.parse(readFileSync20(p, "utf-8"));
|
|
3354
2723
|
} catch {
|
|
3355
2724
|
return { requests: [] };
|
|
3356
2725
|
}
|
|
3357
2726
|
}
|
|
3358
2727
|
function checkApproval(dir, file_path, command) {
|
|
3359
|
-
const store =
|
|
2728
|
+
const store = loadStore(dir);
|
|
3360
2729
|
const now = Date.now();
|
|
3361
2730
|
return store.requests.filter((r) => r.status === "approved" && r.resolved_at && (r.file_path === file_path || r.trigger === command) && now - new Date(r.resolved_at).getTime() < APPROVAL_TTL_MS).sort((a, b) => b.resolved_at.localeCompare(a.resolved_at)).at(0) ?? null;
|
|
3362
2731
|
}
|
|
@@ -3368,8 +2737,8 @@ async function approvalHook(context, toolInput, output) {
|
|
|
3368
2737
|
if (!ENABLED2)
|
|
3369
2738
|
return;
|
|
3370
2739
|
const dir = context.directory ?? process.cwd();
|
|
3371
|
-
const
|
|
3372
|
-
if (!WRITE_TOOLS.has(
|
|
2740
|
+
const tool13 = toolInput.name ?? toolInput.tool ?? "";
|
|
2741
|
+
if (!WRITE_TOOLS.has(tool13))
|
|
3373
2742
|
return;
|
|
3374
2743
|
const args = output.args ?? {};
|
|
3375
2744
|
const filePath = String(args.path ?? args.file_path ?? args.filename ?? "");
|
|
@@ -3386,8 +2755,8 @@ async function approvalHook(context, toolInput, output) {
|
|
|
3386
2755
|
}
|
|
3387
2756
|
|
|
3388
2757
|
// src/services/event-logger.ts
|
|
3389
|
-
import { existsSync as
|
|
3390
|
-
import { join as
|
|
2758
|
+
import { existsSync as existsSync22, mkdirSync as mkdirSync11, appendFileSync as appendFileSync4, readFileSync as readFileSync21, writeFileSync as writeFileSync12, renameSync, unlinkSync, statSync } from "fs";
|
|
2759
|
+
import { join as join21, resolve as resolve2, sep } from "path";
|
|
3391
2760
|
var SENSITIVE_KEYS = [
|
|
3392
2761
|
"password",
|
|
3393
2762
|
"token",
|
|
@@ -3441,7 +2810,7 @@ function isValidDirectory(directory) {
|
|
|
3441
2810
|
return false;
|
|
3442
2811
|
}
|
|
3443
2812
|
try {
|
|
3444
|
-
const stats =
|
|
2813
|
+
const stats = statSync(directory);
|
|
3445
2814
|
return stats.isDirectory();
|
|
3446
2815
|
} catch {
|
|
3447
2816
|
return false;
|
|
@@ -3452,13 +2821,13 @@ function logEvent(directory, event, log) {
|
|
|
3452
2821
|
return;
|
|
3453
2822
|
if (!isValidDirectory(directory))
|
|
3454
2823
|
return;
|
|
3455
|
-
const logDir =
|
|
3456
|
-
const logPath =
|
|
2824
|
+
const logDir = join21(directory, ".opencode");
|
|
2825
|
+
const logPath = join21(logDir, "flowdeck-events.jsonl");
|
|
3457
2826
|
try {
|
|
3458
|
-
if (!
|
|
3459
|
-
|
|
2827
|
+
if (!existsSync22(logDir)) {
|
|
2828
|
+
mkdirSync11(logDir, { recursive: true });
|
|
3460
2829
|
}
|
|
3461
|
-
|
|
2830
|
+
appendFileSync4(logPath, JSON.stringify(event) + `
|
|
3462
2831
|
`, "utf-8");
|
|
3463
2832
|
rotateLogFile(logPath);
|
|
3464
2833
|
if (log) {
|
|
@@ -3468,17 +2837,17 @@ function logEvent(directory, event, log) {
|
|
|
3468
2837
|
}
|
|
3469
2838
|
function rotateLogFile(logPath) {
|
|
3470
2839
|
try {
|
|
3471
|
-
const stats =
|
|
2840
|
+
const stats = statSync(logPath);
|
|
3472
2841
|
if (stats.size < 5000)
|
|
3473
2842
|
return;
|
|
3474
|
-
const content =
|
|
2843
|
+
const content = readFileSync21(logPath, "utf-8");
|
|
3475
2844
|
const lines = content.split(`
|
|
3476
2845
|
`).filter((l) => l.trim());
|
|
3477
2846
|
if (lines.length > 1000) {
|
|
3478
2847
|
const backupPath = logPath + ".backup";
|
|
3479
2848
|
renameSync(logPath, backupPath);
|
|
3480
2849
|
const keep = lines.slice(-1000);
|
|
3481
|
-
|
|
2850
|
+
writeFileSync12(logPath, keep.join(`
|
|
3482
2851
|
`) + `
|
|
3483
2852
|
`, "utf-8");
|
|
3484
2853
|
try {
|
|
@@ -3502,8 +2871,6 @@ function formatEventForStderr(event) {
|
|
|
3502
2871
|
icon = "\uD83D\uDD0D";
|
|
3503
2872
|
else if (event.tool === "bash" || event.tool === "shell")
|
|
3504
2873
|
icon = "\uD83C\uDFC3";
|
|
3505
|
-
else if (event.tool === "delegate")
|
|
3506
|
-
icon = "\uD83E\uDD16";
|
|
3507
2874
|
else
|
|
3508
2875
|
icon = "\uD83D\uDD27";
|
|
3509
2876
|
const argStr = formatArgs(event.args);
|
|
@@ -3531,10 +2898,6 @@ function formatEventForStderr(event) {
|
|
|
3531
2898
|
const error = event.error ? ` error: ${event.error}` : "";
|
|
3532
2899
|
return `${dim}[${time}]${reset} ${icon} ${cyan}${agent}${reset} → ${event.tool}(${argStr})${statusColor}${duration}${error}${reset}`;
|
|
3533
2900
|
}
|
|
3534
|
-
case "agent.delegated": {
|
|
3535
|
-
const thinking = event.thinking ? ` "${event.thinking}"` : "";
|
|
3536
|
-
return `${dim}[${time}]${reset} \uD83E\uDD16 ${cyan}${agent}${reset} → delegate(${thinking})`;
|
|
3537
|
-
}
|
|
3538
2901
|
case "session.created":
|
|
3539
2902
|
return `${dim}[${time}]${reset} \uD83D\uDCC2 session created${event.session_id ? ` (${event.session_id})` : ""}`;
|
|
3540
2903
|
case "session.idle":
|
|
@@ -3673,10 +3036,6 @@ function extractAgentFromEvent(props) {
|
|
|
3673
3036
|
return props.agent;
|
|
3674
3037
|
if (typeof props.name === "string")
|
|
3675
3038
|
return props.name;
|
|
3676
|
-
const title = typeof props.title === "string" ? props.title : "";
|
|
3677
|
-
const match = title.match(/^(.+)-delegate$/);
|
|
3678
|
-
if (match)
|
|
3679
|
-
return match[1];
|
|
3680
3039
|
return "unknown";
|
|
3681
3040
|
}
|
|
3682
3041
|
|
|
@@ -3731,15 +3090,15 @@ function createContextWindowMonitorHook() {
|
|
|
3731
3090
|
}
|
|
3732
3091
|
|
|
3733
3092
|
// src/hooks/shell-env-hook.ts
|
|
3734
|
-
import { existsSync as
|
|
3735
|
-
import { join as
|
|
3736
|
-
import { createRequire
|
|
3093
|
+
import { existsSync as existsSync23, readFileSync as readFileSync22 } from "fs";
|
|
3094
|
+
import { join as join22 } from "path";
|
|
3095
|
+
import { createRequire } from "module";
|
|
3737
3096
|
var _version;
|
|
3738
3097
|
function getVersion() {
|
|
3739
3098
|
if (_version)
|
|
3740
3099
|
return _version;
|
|
3741
3100
|
try {
|
|
3742
|
-
const require2 =
|
|
3101
|
+
const require2 = createRequire(import.meta.url);
|
|
3743
3102
|
const pkg = require2("../../package.json");
|
|
3744
3103
|
_version = pkg.version ?? "0.0.0";
|
|
3745
3104
|
} catch {
|
|
@@ -3768,7 +3127,7 @@ var MARKER_TO_LANG = {
|
|
|
3768
3127
|
};
|
|
3769
3128
|
function detectPackageManager(root) {
|
|
3770
3129
|
for (const [lockfile, pm] of Object.entries(LOCKFILE_TO_PM)) {
|
|
3771
|
-
if (
|
|
3130
|
+
if (existsSync23(join22(root, lockfile)))
|
|
3772
3131
|
return pm;
|
|
3773
3132
|
}
|
|
3774
3133
|
return;
|
|
@@ -3777,7 +3136,7 @@ function detectLanguages(root) {
|
|
|
3777
3136
|
const langs = [];
|
|
3778
3137
|
const seen = new Set;
|
|
3779
3138
|
for (const [marker, lang] of Object.entries(MARKER_TO_LANG)) {
|
|
3780
|
-
if (!seen.has(lang) &&
|
|
3139
|
+
if (!seen.has(lang) && existsSync23(join22(root, marker))) {
|
|
3781
3140
|
langs.push(lang);
|
|
3782
3141
|
seen.add(lang);
|
|
3783
3142
|
}
|
|
@@ -3785,11 +3144,11 @@ function detectLanguages(root) {
|
|
|
3785
3144
|
return langs;
|
|
3786
3145
|
}
|
|
3787
3146
|
function readCurrentPhase(root) {
|
|
3788
|
-
const statePath2 =
|
|
3789
|
-
if (!
|
|
3147
|
+
const statePath2 = join22(root, ".planning", "STATE.md");
|
|
3148
|
+
if (!existsSync23(statePath2))
|
|
3790
3149
|
return;
|
|
3791
3150
|
try {
|
|
3792
|
-
const content =
|
|
3151
|
+
const content = readFileSync22(statePath2, "utf-8");
|
|
3793
3152
|
const match = content.match(/phase:\s*(\S+)/i);
|
|
3794
3153
|
return match?.[1];
|
|
3795
3154
|
} catch {
|
|
@@ -3914,8 +3273,8 @@ function createSessionIdleHook(client, tracker) {
|
|
|
3914
3273
|
}
|
|
3915
3274
|
|
|
3916
3275
|
// src/hooks/compaction-hook.ts
|
|
3917
|
-
import { existsSync as
|
|
3918
|
-
import { join as
|
|
3276
|
+
import { existsSync as existsSync24, readFileSync as readFileSync23 } from "fs";
|
|
3277
|
+
import { join as join23 } from "path";
|
|
3919
3278
|
var STRUCTURED_SUMMARY_PROMPT = `
|
|
3920
3279
|
When summarizing this session, you MUST include the following sections:
|
|
3921
3280
|
|
|
@@ -3956,10 +3315,10 @@ For each: agent name, status, description, session_id.
|
|
|
3956
3315
|
var _lastInjected = new Map;
|
|
3957
3316
|
function readPlanningState2(directory) {
|
|
3958
3317
|
const sp = statePath(directory);
|
|
3959
|
-
if (!
|
|
3318
|
+
if (!existsSync24(sp))
|
|
3960
3319
|
return null;
|
|
3961
3320
|
try {
|
|
3962
|
-
const content =
|
|
3321
|
+
const content = readFileSync23(sp, "utf-8");
|
|
3963
3322
|
const parsed = parseState(content);
|
|
3964
3323
|
const version = typeof parsed.summaryVersion === "number" ? parsed.summaryVersion : 0;
|
|
3965
3324
|
return { content: content.slice(0, 1500), version };
|
|
@@ -3988,15 +3347,15 @@ function createCompactionHook(ctx, tracker) {
|
|
|
3988
3347
|
sections.push(`_State unchanged since last compaction. summaryVersion=${currentStateVersion}_`);
|
|
3989
3348
|
sections.push("");
|
|
3990
3349
|
}
|
|
3991
|
-
const indexPath2 =
|
|
3992
|
-
if (indexChanged &&
|
|
3350
|
+
const indexPath2 = join23(ctx.directory, ".planning", "CODEBASE_INDEX.md");
|
|
3351
|
+
if (indexChanged && existsSync24(indexPath2)) {
|
|
3993
3352
|
try {
|
|
3994
|
-
const indexContent =
|
|
3353
|
+
const indexContent = readFileSync23(indexPath2, "utf-8");
|
|
3995
3354
|
const indexSummary = "\n## Codebase Index\n```\n" + indexContent.slice(0, 800) + "\n```\n";
|
|
3996
3355
|
sections.push(indexSummary);
|
|
3997
3356
|
sections.push("");
|
|
3998
3357
|
} catch {}
|
|
3999
|
-
} else if (
|
|
3358
|
+
} else if (existsSync24(indexPath2)) {
|
|
4000
3359
|
sections.push(`## Codebase Index (unchanged, v${currentIndexVersion})`);
|
|
4001
3360
|
sections.push(`_Index unchanged since last compaction. summaryVersion=${currentIndexVersion}_`);
|
|
4002
3361
|
sections.push("");
|
|
@@ -4042,9 +3401,6 @@ var BLOCKED_TOOLS = new Set([
|
|
|
4042
3401
|
"shell"
|
|
4043
3402
|
]);
|
|
4044
3403
|
var ALWAYS_ALLOWED = new Set([
|
|
4045
|
-
"delegate",
|
|
4046
|
-
"run-pipeline",
|
|
4047
|
-
"council",
|
|
4048
3404
|
"planning-state",
|
|
4049
3405
|
"codebase-state",
|
|
4050
3406
|
"repo-memory",
|
|
@@ -4052,9 +3408,6 @@ var ALWAYS_ALLOWED = new Set([
|
|
|
4052
3408
|
"policy-engine",
|
|
4053
3409
|
"reflect"
|
|
4054
3410
|
]);
|
|
4055
|
-
function isDelegationTool(name) {
|
|
4056
|
-
return ALWAYS_ALLOWED.has(name);
|
|
4057
|
-
}
|
|
4058
3411
|
function isBlocked2(name) {
|
|
4059
3412
|
const norm = name.toLowerCase().replace(/[-_]/g, "");
|
|
4060
3413
|
for (const b of BLOCKED_TOOLS) {
|
|
@@ -4066,15 +3419,15 @@ function isBlocked2(name) {
|
|
|
4066
3419
|
function blockMessage(toolName) {
|
|
4067
3420
|
return `[Orchestrator Guard] The orchestrator cannot use \`${toolName}\` directly.
|
|
4068
3421
|
|
|
4069
|
-
` + `
|
|
3422
|
+
` + `Use built-in read/search tools for lightweight inspection, then route execution with OpenCode's native @agent invocation.
|
|
4070
3423
|
|
|
4071
|
-
` + `
|
|
4072
|
-
` + `
|
|
4073
|
-
` + `
|
|
4074
|
-
` + `
|
|
4075
|
-
` + `
|
|
4076
|
-
` + `
|
|
4077
|
-
` + `
|
|
3424
|
+
` + `Recommended handoffs:
|
|
3425
|
+
` + ` @backend-coder — backend code writing and editing
|
|
3426
|
+
` + ` @frontend-coder — frontend code writing and editing
|
|
3427
|
+
` + ` @devops — CI/CD, deploy, and infrastructure changes
|
|
3428
|
+
` + ` @mapper — codebase mapping
|
|
3429
|
+
` + ` @researcher — focused research and file analysis
|
|
3430
|
+
` + ` @tester — tests, builds, and shell-heavy verification
|
|
4078
3431
|
|
|
4079
3432
|
` + `To enable this guard: set FLOWDECK_ORCHESTRATOR_GUARD=on`;
|
|
4080
3433
|
}
|
|
@@ -4108,7 +3461,7 @@ class OrchestratorGuard {
|
|
|
4108
3461
|
return;
|
|
4109
3462
|
if (sessionId !== this.primarySessionId)
|
|
4110
3463
|
return;
|
|
4111
|
-
if (
|
|
3464
|
+
if (ALWAYS_ALLOWED.has(toolName))
|
|
4112
3465
|
return;
|
|
4113
3466
|
if (isBlocked2(toolName)) {
|
|
4114
3467
|
throw new Error(blockMessage(toolName));
|
|
@@ -4244,74 +3597,57 @@ ${customAppendPrompt}`;
|
|
|
4244
3597
|
return base;
|
|
4245
3598
|
}
|
|
4246
3599
|
// src/agents/orchestrator.ts
|
|
4247
|
-
var ORCHESTRATOR_PROMPT = `You coordinate multi-agent execution.
|
|
4248
|
-
|
|
4249
|
-
## HARD RULES — Non-Negotiable
|
|
4250
|
-
|
|
4251
|
-
**You are a coordinator. You NEVER do implementation work yourself.**
|
|
4252
|
-
|
|
4253
|
-
1. **Never read source files directly.** You may read STATE.md, PLAN.md, and .codebase/ summary files — nothing else. For all other file reading, delegate to @code-explorer or @researcher.
|
|
4254
|
-
2. **Never write or edit any file.** All file creation, editing, and patching is done by specialist agents. Use \`delegate\` to hand it off.
|
|
4255
|
-
3. **Never run shell commands, tests, or builds.** Delegate to @tester or @build-error-resolver.
|
|
4256
|
-
4. **Every step in PLAN.md is executed by a delegated agent**, never by you directly.
|
|
3600
|
+
var ORCHESTRATOR_PROMPT = `You coordinate multi-agent execution. Read planning state, inspect the codebase with built-in tools when needed, and route specialized work to the right agent using OpenCode's native agent invocation.
|
|
4257
3601
|
|
|
4258
|
-
|
|
3602
|
+
## Operating Model
|
|
4259
3603
|
|
|
4260
|
-
|
|
3604
|
+
- Start by reading STATE.md and the active PLAN.md.
|
|
3605
|
+
- Use built-in read/search tools directly for lightweight inspection and progress tracking.
|
|
3606
|
+
- Use native agent routing for implementation, testing, deep research, reviews, and other specialist work.
|
|
3607
|
+
- Do not rely on the removed FlowDeck-specific delegation tools.
|
|
4261
3608
|
|
|
4262
3609
|
## Startup Behavior
|
|
4263
3610
|
|
|
4264
|
-
|
|
4265
|
-
1. Read
|
|
4266
|
-
2. Read the active
|
|
4267
|
-
3.
|
|
4268
|
-
4. Begin execution from the first incomplete step
|
|
3611
|
+
At session start:
|
|
3612
|
+
1. Read STATE.md to identify the current phase and active plan.
|
|
3613
|
+
2. Read the active PLAN.md to identify complete and incomplete steps.
|
|
3614
|
+
3. Resume from the first incomplete step.
|
|
4269
3615
|
|
|
4270
|
-
If STATE.md does not exist, tell the user:
|
|
3616
|
+
If STATE.md does not exist, tell the user: No STATE.md found. Run /fd-map-codebase then /fd-new-feature to start a feature.
|
|
4271
3617
|
|
|
4272
3618
|
## Phase Gating
|
|
4273
3619
|
|
|
4274
|
-
|
|
3620
|
+
Read STATE.md to determine the current phase and workflow class.
|
|
4275
3621
|
|
|
4276
|
-
|
|
4277
|
-
-
|
|
4278
|
-
-
|
|
4279
|
-
-
|
|
3622
|
+
The orchestrator may run in any phase, but should respect the workflow class:
|
|
3623
|
+
- For \`quick\` workflows: run directly in execute phase, skip discuss/plan.
|
|
3624
|
+
- For \`standard\` workflows: plan → execute → verify.
|
|
3625
|
+
- For \`explore\` workflows: discuss → plan → execute → verify.
|
|
3626
|
+
- For \`ui-heavy\` workflows: discuss → design → plan → execute → verify.
|
|
3627
|
+
- For \`bugfix\` workflows: discuss → fix-bug → verify.
|
|
3628
|
+
- For \`docs-only\` workflows: write-docs → verify.
|
|
3629
|
+
- For \`verify-heavy\` workflows: plan → execute → verify (with enhanced checks).
|
|
3630
|
+
|
|
3631
|
+
If the project is in a different phase than expected:
|
|
3632
|
+
- Suggest the correct next command but allow override for adaptive workflows.
|
|
3633
|
+
- Log any phase skips with reasons.
|
|
4280
3634
|
|
|
4281
3635
|
## State-First Read Strategy
|
|
4282
3636
|
|
|
4283
|
-
Before
|
|
4284
|
-
1. Read
|
|
4285
|
-
2. Read
|
|
4286
|
-
3.
|
|
4287
|
-
|
|
4288
|
-
→ Log: "[StateManager] Skipped codebase exploration — state is fresh"
|
|
4289
|
-
4. If state is missing, stale, or insufficient:
|
|
4290
|
-
→ Delegate to @code-explorer with specific question
|
|
4291
|
-
→ After exploration completes, file-tracker auto-publishes to CODEBASE_INDEX.md
|
|
4292
|
-
→ Log: "[StateManager] Triggered re-exploration — state was stale"
|
|
4293
|
-
|
|
4294
|
-
State becomes **stale** when:
|
|
4295
|
-
- \`lastUpdatedAt\` > 5 minutes ago
|
|
4296
|
-
- Phase transitions
|
|
4297
|
-
- New plan confirmed
|
|
4298
|
-
- User runs /fd-checkpoint or /fd-resume
|
|
4299
|
-
|
|
4300
|
-
State becomes **fresh** when:
|
|
4301
|
-
- Any agent writes to CODEBASE_INDEX.md
|
|
4302
|
-
- updatePlanningState() is called
|
|
4303
|
-
- file-tracker hook fires after a file edit
|
|
3637
|
+
Before invoking an agent that needs codebase context:
|
|
3638
|
+
1. Read STATE.md and check freshnessStatus and lastUpdatedAt.
|
|
3639
|
+
2. Read .planning/CODEBASE_INDEX.md when available.
|
|
3640
|
+
3. Reuse fresh state when it already answers the question.
|
|
3641
|
+
4. When state is stale or missing, inspect the relevant files directly or route focused exploration to @code-explorer or @researcher.
|
|
4304
3642
|
|
|
4305
3643
|
## Step Execution
|
|
4306
3644
|
|
|
4307
3645
|
For each incomplete step in PLAN.md:
|
|
4308
|
-
|
|
4309
|
-
|
|
4310
|
-
|
|
4311
|
-
|
|
4312
|
-
|
|
4313
|
-
5. Re-read STATE.md to confirm state
|
|
4314
|
-
6. Move to the next incomplete step
|
|
3646
|
+
1. Identify the step requirements and the best agent for the work.
|
|
3647
|
+
2. Gather only the context needed to brief that agent.
|
|
3648
|
+
3. Invoke the specialist directly with native agent routing.
|
|
3649
|
+
4. Wait for completion, then update and re-read STATE.md.
|
|
3650
|
+
5. Move to the next incomplete step.
|
|
4315
3651
|
|
|
4316
3652
|
## Implementation Routing
|
|
4317
3653
|
|
|
@@ -4319,84 +3655,78 @@ When a plan step requires implementation, route to a role-specific agent:
|
|
|
4319
3655
|
- Use @backend-coder for server, API, business logic, database, and non-UI application code.
|
|
4320
3656
|
- Use @frontend-coder for UI components, client state, styling, and interaction behavior.
|
|
4321
3657
|
- Use @devops for CI/CD workflows, deployment, infrastructure, runtime config, and operations scripts.
|
|
4322
|
-
-
|
|
3658
|
+
- Split mixed-domain steps into smaller specialist handoffs when that reduces risk.
|
|
4323
3659
|
|
|
4324
3660
|
## Agent Team
|
|
4325
3661
|
|
|
4326
|
-
|
|
4327
|
-
|
|
4328
|
-
|
|
4329
|
-
|
|
4330
|
-
|
|
4331
|
-
|
|
4332
|
-
|
|
4333
|
-
|
|
4334
|
-
|
|
4335
|
-
|
|
4336
|
-
|
|
4337
|
-
|
|
4338
|
-
|
|
4339
|
-
|
|
4340
|
-
|
|
4341
|
-
|
|
4342
|
-
|
|
4343
|
-
|
|
4344
|
-
|
|
4345
|
-
|
|
4346
|
-
|
|
4347
|
-
|
|
4348
|
-
|
|
4349
|
-
|
|
4350
|
-
|
|
4351
|
-
|
|
4352
|
-
|
|
4353
|
-
|
|
4354
|
-
|
|
4355
|
-
|
|
4356
|
-
|
|
4357
|
-
-
|
|
4358
|
-
|
|
4359
|
-
-
|
|
4360
|
-
|
|
4361
|
-
|
|
3662
|
+
- @design: discovery, UX planning, wireframes, visual system, implementation handoff, design fidelity review
|
|
3663
|
+
- @backend-coder: backend code implementation
|
|
3664
|
+
- @frontend-coder: frontend code implementation
|
|
3665
|
+
- @devops: CI/CD and infrastructure implementation
|
|
3666
|
+
- @researcher: API docs and library usage
|
|
3667
|
+
- @tester: writing and running tests
|
|
3668
|
+
- @reviewer: code quality review
|
|
3669
|
+
- @writer: documentation
|
|
3670
|
+
- @mapper: codebase mapping to .codebase/
|
|
3671
|
+
- @architect: system design and ADRs
|
|
3672
|
+
- @security-auditor: security review
|
|
3673
|
+
- @code-explorer: reading unfamiliar code
|
|
3674
|
+
- @debug-specialist: root cause analysis
|
|
3675
|
+
- @build-error-resolver: build and compile failures
|
|
3676
|
+
- @doc-updater: updating existing docs
|
|
3677
|
+
- @task-splitter: decomposing complex tasks
|
|
3678
|
+
- @discusser: requirements extraction
|
|
3679
|
+
- @plan-checker: plan quality review
|
|
3680
|
+
- @planner: feature planning
|
|
3681
|
+
- @performance-optimizer: performance analysis
|
|
3682
|
+
- @refactor-guide: safe refactoring
|
|
3683
|
+
|
|
3684
|
+
## Adaptive Workflow Routing
|
|
3685
|
+
|
|
3686
|
+
The orchestrator reads the workflow class from STATE.md and adapts its behavior:
|
|
3687
|
+
|
|
3688
|
+
| Workflow Class | Stages | When Used |
|
|
3689
|
+
|----------------|--------|-----------|
|
|
3690
|
+
| \`quick\` | execute → verify | Simple, low-risk tasks (< 5 files, no ambiguity) |
|
|
3691
|
+
| \`standard\` | plan → execute → verify | Normal implementation tasks |
|
|
3692
|
+
| \`explore\` | discuss → plan → execute → verify | Ambiguous or unfamiliar tasks |
|
|
3693
|
+
| \`ui-heavy\` | discuss → design → plan → execute → verify | UI/UX-heavy tasks |
|
|
3694
|
+
| \`bugfix\` | discuss → fix-bug → verify | Bug fixes |
|
|
3695
|
+
| \`docs-only\` | write-docs → verify | Documentation-only changes |
|
|
3696
|
+
| \`verify-heavy\` | plan → execute → verify | High blast radius or sensitive paths |
|
|
3697
|
+
|
|
3698
|
+
- discuss: requirements extraction with @discusser (only for explore/bugfix/ui-heavy)
|
|
3699
|
+
- plan: plan creation with @planner, review with @plan-checker (skip for quick/docs-only)
|
|
3700
|
+
- design: UX structure with @design (only for ui-heavy)
|
|
3701
|
+
- execute: implementation with appropriate specialists
|
|
3702
|
+
- verify: review with @reviewer and @security-auditor (always run for edited code)
|
|
3703
|
+
|
|
3704
|
+
The workflow class is chosen by scoring task complexity, confidence, risk, and codebase familiarity. Prefer the lightest workflow that is sufficient. Escalate to a richer workflow only when evidence shows the current path is insufficient.
|
|
4362
3705
|
|
|
4363
3706
|
## Tracking
|
|
4364
3707
|
|
|
4365
3708
|
After each step completes:
|
|
4366
|
-
- Call
|
|
3709
|
+
- Call mark_step_complete with the step ID
|
|
4367
3710
|
- Re-read STATE.md to confirm the update
|
|
4368
|
-
- Update STATE.md
|
|
3711
|
+
- Update STATE.md current_step to the next step
|
|
4369
3712
|
|
|
4370
3713
|
On all steps complete:
|
|
4371
|
-
- Update STATE.md
|
|
3714
|
+
- Update STATE.md phase to review
|
|
4372
3715
|
- Summarize what was delivered
|
|
4373
3716
|
|
|
4374
3717
|
## Error Recovery
|
|
4375
3718
|
|
|
4376
|
-
If a
|
|
4377
|
-
1. Log the failure with the error message
|
|
4378
|
-
2. Retry once with
|
|
4379
|
-
3. If still
|
|
4380
|
-
|
|
4381
|
-
\`\`\`
|
|
4382
|
-
BLOCKED: implementation agent failed on step 3 (add payment endpoint).
|
|
4383
|
-
Error: [exact error message]
|
|
4384
|
-
Retried once with clarification. Still failing.
|
|
4385
|
-
|
|
4386
|
-
Options:
|
|
4387
|
-
1. Skip this step and continue
|
|
4388
|
-
2. Replan step 3 with smaller scope
|
|
4389
|
-
3. Stop and debug manually
|
|
4390
|
-
|
|
4391
|
-
Please advise.
|
|
4392
|
-
\`\`\`
|
|
3719
|
+
If a specialist fails:
|
|
3720
|
+
1. Log the failure with the exact error message.
|
|
3721
|
+
2. Retry once with clearer context if the issue is recoverable.
|
|
3722
|
+
3. If it still fails, surface a blocked summary with next options.
|
|
4393
3723
|
|
|
4394
3724
|
## Self-Learning
|
|
4395
3725
|
|
|
4396
3726
|
When a task required unusual human guidance, a novel solution strategy, or exposed a knowledge gap:
|
|
4397
|
-
1. After the task completes successfully, write a new skill markdown file under
|
|
4398
|
-
2. Use a descriptive kebab-case name for the directory, a one-sentence description in the frontmatter, and structured Markdown content
|
|
4399
|
-
3. Include: When to Activate, Steps, Examples, and Pitfalls sections
|
|
3727
|
+
1. After the task completes successfully, write a new skill markdown file under src/skills/<name>/SKILL.md to capture the pattern.
|
|
3728
|
+
2. Use a descriptive kebab-case name for the directory, a one-sentence description in the frontmatter, and structured Markdown content.
|
|
3729
|
+
3. Include: When to Activate, Steps, Examples, and Pitfalls sections.
|
|
4400
3730
|
|
|
4401
3731
|
Do NOT create a skill for routine tasks. Only capture genuinely novel or reusable patterns.`;
|
|
4402
3732
|
var AGENT_DESCRIPTIONS = {
|
|
@@ -4404,114 +3734,118 @@ var AGENT_DESCRIPTIONS = {
|
|
|
4404
3734
|
- Role: Runs design-first workflow for user-facing tasks
|
|
4405
3735
|
- Permissions: Read/write files
|
|
4406
3736
|
- Best for: UX structure, wireframes, visual direction, tokens, and frontend handoff
|
|
4407
|
-
-
|
|
3737
|
+
- Use when: Task includes website/app/dashboard/admin/user-facing UI work`,
|
|
4408
3738
|
"backend-coder": `@backend-coder
|
|
4409
3739
|
- Role: Implements backend features and fixes based on confirmed plans
|
|
4410
3740
|
- Permissions: Read/write files
|
|
4411
3741
|
- Best for: API, services, data layer, and business logic
|
|
4412
|
-
-
|
|
3742
|
+
- Use when: Backend or server-side implementation work`,
|
|
4413
3743
|
"frontend-coder": `@frontend-coder
|
|
4414
3744
|
- Role: Implements frontend features and fixes based on confirmed plans
|
|
4415
3745
|
- Permissions: Read/write files
|
|
4416
3746
|
- Best for: UI components, client state, rendering, and interaction behavior
|
|
4417
|
-
-
|
|
3747
|
+
- Use when: Frontend implementation work`,
|
|
4418
3748
|
devops: `@devops
|
|
4419
3749
|
- Role: Implements DevOps and infrastructure changes based on confirmed plans
|
|
4420
3750
|
- Permissions: Read/write files
|
|
4421
3751
|
- Best for: CI/CD, deployment config, infra scripts, and runtime operations
|
|
4422
|
-
-
|
|
3752
|
+
- Use when: Infrastructure, pipeline, or operations implementation work`,
|
|
4423
3753
|
researcher: `@researcher
|
|
4424
3754
|
- Role: Researches documentation, APIs, and best practices
|
|
4425
3755
|
- Permissions: Read files
|
|
4426
3756
|
- Stats: 10x better finding up-to-date library docs
|
|
4427
|
-
-
|
|
4428
|
-
-
|
|
3757
|
+
- Use when: Need API docs, library usage, or best practices
|
|
3758
|
+
- Skip when: Standard usage you're already confident about`,
|
|
4429
3759
|
tester: `@tester
|
|
4430
3760
|
- Role: Writes and runs tests following TDD principles
|
|
4431
3761
|
- Permissions: Read/write files
|
|
4432
3762
|
- Best for: Writing tests before code (TDD), running test suites
|
|
4433
|
-
-
|
|
3763
|
+
- Use when: Implementing new features, fixing bugs, or increasing coverage`,
|
|
4434
3764
|
reviewer: `@reviewer
|
|
4435
3765
|
- Role: Reviews code for quality, security, and adherence to conventions
|
|
4436
3766
|
- Permissions: Read files
|
|
4437
3767
|
- Best for: Code review before PRs
|
|
4438
|
-
-
|
|
3768
|
+
- Use when: After writing or modifying code, before opening PRs`,
|
|
4439
3769
|
architect: `@architect
|
|
4440
3770
|
- Role: Designs system architecture, creates ADRs, defines API contracts
|
|
4441
3771
|
- Permissions: Read files
|
|
4442
3772
|
- Best for: New modules, API changes, database schema changes, cross-cutting concerns
|
|
4443
|
-
-
|
|
3773
|
+
- Use when: Planning new features that need architectural decisions`,
|
|
4444
3774
|
"security-auditor": `@security-auditor
|
|
4445
3775
|
- Role: Deep security audit of code changes
|
|
4446
3776
|
- Permissions: Read files
|
|
4447
3777
|
- Best for: OWASP Top 10, injection vulnerabilities, auth issues
|
|
4448
|
-
-
|
|
3778
|
+
- Use when: Before merging security-sensitive code`,
|
|
4449
3779
|
"code-explorer": `@code-explorer
|
|
4450
3780
|
- Role: Explores and maps unfamiliar codebases
|
|
4451
3781
|
- Permissions: Read files
|
|
4452
3782
|
- Best for: Tracing call paths, building structural models
|
|
4453
|
-
-
|
|
3783
|
+
- Use when: Before making changes to unfamiliar code`,
|
|
4454
3784
|
"debug-specialist": `@debug-specialist
|
|
4455
3785
|
- Role: Diagnoses bugs through systematic root cause analysis
|
|
4456
3786
|
- Permissions: Read files
|
|
4457
3787
|
- Best for: Deep investigation before fixing
|
|
4458
|
-
-
|
|
3788
|
+
- Use when: A bug needs investigation, not just a quick fix`,
|
|
4459
3789
|
"build-error-resolver": `@build-error-resolver
|
|
4460
3790
|
- Role: Fixes build errors, compilation failures, dependency issues
|
|
4461
3791
|
- Permissions: Read/write files
|
|
4462
3792
|
- Best for: Build failures, type errors, broken dependencies
|
|
4463
|
-
-
|
|
3793
|
+
- Use when: Build fails, types error out, or dependencies break`,
|
|
4464
3794
|
"doc-updater": `@doc-updater
|
|
4465
3795
|
- Role: Updates documentation after code changes
|
|
4466
3796
|
- Permissions: Read/write files
|
|
4467
3797
|
- Best for: API references, README, inline comments
|
|
4468
|
-
-
|
|
3798
|
+
- Use when: Implementation completes and docs need syncing`,
|
|
4469
3799
|
writer: `@writer
|
|
4470
3800
|
- Role: Drafts project documentation
|
|
4471
3801
|
- Permissions: Read/write files
|
|
4472
3802
|
- Best for: README, API docs, user guides
|
|
4473
|
-
-
|
|
3803
|
+
- Use when: Creating new documentation from scratch`,
|
|
4474
3804
|
mapper: `@mapper
|
|
4475
3805
|
- Role: Maps codebase to structured documentation files
|
|
4476
3806
|
- Permissions: Read/write files
|
|
4477
3807
|
- Best for: .codebase/ directory documentation
|
|
4478
|
-
-
|
|
3808
|
+
- Use when: Need to document existing codebase structure`,
|
|
4479
3809
|
"plan-checker": `@plan-checker
|
|
4480
3810
|
- Role: Reviews PLAN.md for quality before execution
|
|
4481
3811
|
- Permissions: Read files
|
|
4482
3812
|
- Best for: Plan verification before execution
|
|
4483
|
-
-
|
|
3813
|
+
- Use when: PLAN.md needs review before execution`,
|
|
4484
3814
|
"task-splitter": `@task-splitter
|
|
4485
3815
|
- Role: Decomposes complex tasks into parallel workstreams
|
|
4486
3816
|
- Permissions: Read files
|
|
4487
3817
|
- Best for: Multi-track work organization
|
|
4488
|
-
-
|
|
3818
|
+
- Use when: Complex work needs parallelization`,
|
|
4489
3819
|
discusser: `@discusser
|
|
4490
3820
|
- Role: Extracts requirements via structured Q&A
|
|
4491
3821
|
- Permissions: Read/write files
|
|
4492
3822
|
- Best for: Requirements extraction
|
|
4493
|
-
-
|
|
3823
|
+
- Use when: Starting a new feature or project phase`,
|
|
4494
3824
|
planner: `@planner
|
|
4495
3825
|
- Role: Creates detailed implementation plans
|
|
4496
3826
|
- Permissions: Read files
|
|
4497
3827
|
- Best for: Feature planning, step breakdown
|
|
4498
|
-
-
|
|
3828
|
+
- Use when: Need an implementation plan for a feature`,
|
|
4499
3829
|
"performance-optimizer": `@performance-optimizer
|
|
4500
3830
|
- Role: Analyzes and optimizes performance
|
|
4501
3831
|
- Permissions: Read files
|
|
4502
3832
|
- Best for: Performance analysis
|
|
4503
|
-
-
|
|
3833
|
+
- Use when: Need to optimize slow code`,
|
|
4504
3834
|
"refactor-guide": `@refactor-guide
|
|
4505
3835
|
- Role: Guides safe refactoring
|
|
4506
3836
|
- Permissions: Read files
|
|
4507
3837
|
- Best for: Code restructuring
|
|
4508
|
-
-
|
|
3838
|
+
- Use when: Need to refactor existing code safely`
|
|
4509
3839
|
};
|
|
4510
|
-
function buildOrchestratorPrompt(disabledAgents) {
|
|
3840
|
+
function buildOrchestratorPrompt(disabledAgents, workflowClass) {
|
|
4511
3841
|
const enabledAgents = Object.entries(AGENT_DESCRIPTIONS).filter(([name]) => !disabledAgents?.has(name)).map(([, desc]) => desc).join(`
|
|
4512
3842
|
|
|
4513
3843
|
`);
|
|
4514
|
-
|
|
3844
|
+
const workflowSection = workflowClass ? `
|
|
3845
|
+
## Current Workflow
|
|
3846
|
+
|
|
3847
|
+
Active workflow class: ${workflowClass}` : "";
|
|
3848
|
+
return `${ORCHESTRATOR_PROMPT}${workflowSection}
|
|
4515
3849
|
|
|
4516
3850
|
<Delegation>
|
|
4517
3851
|
|
|
@@ -4519,21 +3853,22 @@ function buildOrchestratorPrompt(disabledAgents) {
|
|
|
4519
3853
|
|
|
4520
3854
|
${enabledAgents}
|
|
4521
3855
|
|
|
4522
|
-
##
|
|
3856
|
+
## Routing Guidelines
|
|
4523
3857
|
|
|
4524
3858
|
- Review available agents before acting
|
|
4525
|
-
- Reference paths
|
|
4526
|
-
- Provide context summaries, let specialists
|
|
4527
|
-
-
|
|
3859
|
+
- Reference paths and line numbers instead of pasting full files
|
|
3860
|
+
- Provide context summaries, then let specialists inspect what they need
|
|
3861
|
+
- Use direct built-in tools yourself for lightweight reading and status tracking
|
|
3862
|
+
- Use native agent routing when specialist work or deeper execution is the better fit
|
|
4528
3863
|
|
|
4529
3864
|
</Delegation>`;
|
|
4530
3865
|
}
|
|
4531
|
-
function createOrchestratorAgent(model, customPrompt, customAppendPrompt, disabledAgents) {
|
|
4532
|
-
const basePrompt = buildOrchestratorPrompt(disabledAgents);
|
|
3866
|
+
function createOrchestratorAgent(model, customPrompt, customAppendPrompt, disabledAgents, workflowClass) {
|
|
3867
|
+
const basePrompt = buildOrchestratorPrompt(disabledAgents, workflowClass);
|
|
4533
3868
|
const prompt = resolvePrompt(basePrompt, customPrompt, customAppendPrompt);
|
|
4534
3869
|
const definition = {
|
|
4535
3870
|
name: "orchestrator",
|
|
4536
|
-
description: "AI coding orchestrator that
|
|
3871
|
+
description: "AI coding orchestrator that coordinates specialist agents and built-in tools for execution",
|
|
4537
3872
|
config: {
|
|
4538
3873
|
temperature: 0.1,
|
|
4539
3874
|
prompt
|
|
@@ -7349,612 +6684,11 @@ function getAgentConfigs(agentModels) {
|
|
|
7349
6684
|
return configs;
|
|
7350
6685
|
}
|
|
7351
6686
|
|
|
7352
|
-
// src/services/agent-contract-registry.ts
|
|
7353
|
-
var CONTRACTS = [
|
|
7354
|
-
{
|
|
7355
|
-
agent: "orchestrator",
|
|
7356
|
-
role: "Coordinate multi-agent execution. Delegates all work — never implements directly.",
|
|
7357
|
-
allowedTaskTypes: ["orchestration", "coordination", "delegation", "phase-management"],
|
|
7358
|
-
requiredInputs: ["STATE.md", "PLAN.md"],
|
|
7359
|
-
expectedOutputFields: ["delegated_steps", "completed_steps", "current_phase"],
|
|
7360
|
-
allowedTools: [
|
|
7361
|
-
"delegate",
|
|
7362
|
-
"run-pipeline",
|
|
7363
|
-
"council",
|
|
7364
|
-
"planning-state",
|
|
7365
|
-
"codebase-state",
|
|
7366
|
-
"repo-memory",
|
|
7367
|
-
"decision-trace",
|
|
7368
|
-
"policy-engine",
|
|
7369
|
-
"reflect"
|
|
7370
|
-
],
|
|
7371
|
-
forbiddenActions: [
|
|
7372
|
-
"write_file",
|
|
7373
|
-
"edit_file",
|
|
7374
|
-
"create_file",
|
|
7375
|
-
"bash",
|
|
7376
|
-
"patch",
|
|
7377
|
-
"apply_patch",
|
|
7378
|
-
"read source files directly"
|
|
7379
|
-
],
|
|
7380
|
-
escalationConditions: [
|
|
7381
|
-
"delegated agent fails twice",
|
|
7382
|
-
"delegation budget exhausted",
|
|
7383
|
-
"deadlock detected",
|
|
7384
|
-
"all agents blocked on the same step"
|
|
7385
|
-
],
|
|
7386
|
-
stopConditions: [
|
|
7387
|
-
"all PLAN.md steps completed",
|
|
7388
|
-
"user requests stop",
|
|
7389
|
-
"budget exceeded with no fallback"
|
|
7390
|
-
],
|
|
7391
|
-
successCriteria: [
|
|
7392
|
-
"all plan steps delegated and completed",
|
|
7393
|
-
"STATE.md phase updated to review",
|
|
7394
|
-
"no implementation performed directly by orchestrator"
|
|
7395
|
-
]
|
|
7396
|
-
},
|
|
7397
|
-
{
|
|
7398
|
-
agent: "planner",
|
|
7399
|
-
role: "Create detailed implementation plans. Output PLAN.md with numbered steps.",
|
|
7400
|
-
allowedTaskTypes: ["planning", "task-breakdown", "step-decomposition"],
|
|
7401
|
-
requiredInputs: ["task description or STATE.md"],
|
|
7402
|
-
expectedOutputFields: ["steps", "phase"],
|
|
7403
|
-
allowedTools: ["read", "glob", "grep", "planning-state"],
|
|
7404
|
-
forbiddenActions: [
|
|
7405
|
-
"write source files",
|
|
7406
|
-
"run bash commands",
|
|
7407
|
-
"edit application code",
|
|
7408
|
-
"implement features"
|
|
7409
|
-
],
|
|
7410
|
-
escalationConditions: [
|
|
7411
|
-
"requirements are ambiguous",
|
|
7412
|
-
"dependencies between steps unclear",
|
|
7413
|
-
"conflicting constraints"
|
|
7414
|
-
],
|
|
7415
|
-
stopConditions: ["PLAN.md written and reviewed by plan-checker", "user confirms plan"],
|
|
7416
|
-
successCriteria: [
|
|
7417
|
-
"PLAN.md contains numbered steps with assigned agents",
|
|
7418
|
-
"each step has clear success criteria",
|
|
7419
|
-
"no implementation performed"
|
|
7420
|
-
]
|
|
7421
|
-
},
|
|
7422
|
-
{
|
|
7423
|
-
agent: "plan-checker",
|
|
7424
|
-
role: "Review PLAN.md quality before execution. Read-only.",
|
|
7425
|
-
allowedTaskTypes: ["plan-review", "quality-check"],
|
|
7426
|
-
requiredInputs: ["PLAN.md"],
|
|
7427
|
-
expectedOutputFields: ["verdict", "issues", "recommendations"],
|
|
7428
|
-
allowedTools: ["read", "glob", "grep"],
|
|
7429
|
-
forbiddenActions: ["write or edit any files", "modify PLAN.md"],
|
|
7430
|
-
escalationConditions: ["plan is fundamentally flawed", "critical gaps found"],
|
|
7431
|
-
stopConditions: ["review complete", "verdict issued"],
|
|
7432
|
-
successCriteria: ["structured review output", "no file modifications"]
|
|
7433
|
-
},
|
|
7434
|
-
{
|
|
7435
|
-
agent: "design",
|
|
7436
|
-
role: "Design UX, wireframes, and visual systems for UI-heavy tasks.",
|
|
7437
|
-
allowedTaskTypes: ["ux-design", "wireframe", "visual-system", "design-handoff", "frontend-handoff"],
|
|
7438
|
-
requiredInputs: ["task description", "requirements"],
|
|
7439
|
-
expectedOutputFields: ["design_stage", "wireframes", "component_structure", "design_tokens"],
|
|
7440
|
-
allowedTools: ["read", "write", "glob", "grep", "planning-state"],
|
|
7441
|
-
forbiddenActions: [
|
|
7442
|
-
"run bash commands",
|
|
7443
|
-
"write application logic",
|
|
7444
|
-
"implement backend code",
|
|
7445
|
-
"implement React components"
|
|
7446
|
-
],
|
|
7447
|
-
escalationConditions: [
|
|
7448
|
-
"design requirements unclear",
|
|
7449
|
-
"conflicting UX requirements",
|
|
7450
|
-
"brand guidelines missing"
|
|
7451
|
-
],
|
|
7452
|
-
stopConditions: ["design_stage=handoff_complete", "design_approved=true"],
|
|
7453
|
-
successCriteria: [
|
|
7454
|
-
"design document written",
|
|
7455
|
-
"design_stage set to handoff_complete",
|
|
7456
|
-
"design_approved set to true",
|
|
7457
|
-
"no application code written"
|
|
7458
|
-
]
|
|
7459
|
-
},
|
|
7460
|
-
{
|
|
7461
|
-
agent: "backend-coder",
|
|
7462
|
-
role: "Implement backend features: API, services, data layer, business logic.",
|
|
7463
|
-
allowedTaskTypes: ["implementation", "backend", "api", "database", "service", "bugfix"],
|
|
7464
|
-
requiredInputs: ["PLAN.md step description", "relevant context files"],
|
|
7465
|
-
expectedOutputFields: ["files_modified", "summary"],
|
|
7466
|
-
allowedTools: ["read", "write", "edit", "bash", "glob", "grep"],
|
|
7467
|
-
forbiddenActions: [
|
|
7468
|
-
"modify frontend UI component files",
|
|
7469
|
-
"change CI/CD config without devops involvement"
|
|
7470
|
-
],
|
|
7471
|
-
escalationConditions: [
|
|
7472
|
-
"architecture decision needed",
|
|
7473
|
-
"security-sensitive change without audit",
|
|
7474
|
-
"database migration required"
|
|
7475
|
-
],
|
|
7476
|
-
stopConditions: ["step implementation complete", "tests pass", "reviewer approves"],
|
|
7477
|
-
successCriteria: [
|
|
7478
|
-
"code written per plan step",
|
|
7479
|
-
"no regressions introduced",
|
|
7480
|
-
"tests exist or updated"
|
|
7481
|
-
]
|
|
7482
|
-
},
|
|
7483
|
-
{
|
|
7484
|
-
agent: "frontend-coder",
|
|
7485
|
-
role: "Implement frontend features: UI components, client state, rendering.",
|
|
7486
|
-
allowedTaskTypes: ["implementation", "frontend", "ui", "component", "styling", "bugfix"],
|
|
7487
|
-
requiredInputs: ["PLAN.md step description", "design handoff for UI-heavy tasks"],
|
|
7488
|
-
expectedOutputFields: ["files_modified", "summary"],
|
|
7489
|
-
allowedTools: ["read", "write", "edit", "bash", "glob", "grep"],
|
|
7490
|
-
forbiddenActions: [
|
|
7491
|
-
"modify backend API files",
|
|
7492
|
-
"change server configuration",
|
|
7493
|
-
"implement without approved design for UI-heavy tasks"
|
|
7494
|
-
],
|
|
7495
|
-
escalationConditions: [
|
|
7496
|
-
"design handoff missing for UI-heavy task",
|
|
7497
|
-
"component library or design system unclear"
|
|
7498
|
-
],
|
|
7499
|
-
stopConditions: ["step implementation complete", "tests pass", "reviewer approves"],
|
|
7500
|
-
successCriteria: [
|
|
7501
|
-
"components implemented per approved design",
|
|
7502
|
-
"no regressions introduced",
|
|
7503
|
-
"tests exist or updated"
|
|
7504
|
-
]
|
|
7505
|
-
},
|
|
7506
|
-
{
|
|
7507
|
-
agent: "devops",
|
|
7508
|
-
role: "Implement DevOps and infrastructure changes: CI/CD, deployment, infra scripts.",
|
|
7509
|
-
allowedTaskTypes: ["implementation", "ci-cd", "deployment", "infrastructure", "operations"],
|
|
7510
|
-
requiredInputs: ["PLAN.md step description"],
|
|
7511
|
-
expectedOutputFields: ["files_modified", "summary"],
|
|
7512
|
-
allowedTools: ["read", "write", "edit", "bash", "glob", "grep"],
|
|
7513
|
-
forbiddenActions: [
|
|
7514
|
-
"modify application source code",
|
|
7515
|
-
"deploy to production without approval"
|
|
7516
|
-
],
|
|
7517
|
-
escalationConditions: [
|
|
7518
|
-
"production deployment requires approval",
|
|
7519
|
-
"destructive infra change"
|
|
7520
|
-
],
|
|
7521
|
-
stopConditions: ["pipeline or infra change complete", "reviewer approves"],
|
|
7522
|
-
successCriteria: ["infrastructure code written per plan", "no prod deployment without approval"]
|
|
7523
|
-
},
|
|
7524
|
-
{
|
|
7525
|
-
agent: "tester",
|
|
7526
|
-
role: "Write and run tests following TDD principles. Tests before implementation.",
|
|
7527
|
-
allowedTaskTypes: ["testing", "tdd", "regression", "integration-test", "unit-test"],
|
|
7528
|
-
requiredInputs: ["feature or step description", "relevant source files"],
|
|
7529
|
-
expectedOutputFields: ["test_files_written", "tests_passing", "coverage_summary"],
|
|
7530
|
-
allowedTools: ["read", "write", "edit", "bash", "glob", "grep"],
|
|
7531
|
-
forbiddenActions: [
|
|
7532
|
-
"delete failing tests to make suite pass",
|
|
7533
|
-
"implement application features",
|
|
7534
|
-
"skip TDD cycle (red → green → refactor)"
|
|
7535
|
-
],
|
|
7536
|
-
escalationConditions: [
|
|
7537
|
-
"test infrastructure broken",
|
|
7538
|
-
"flaky tests blocking all progress"
|
|
7539
|
-
],
|
|
7540
|
-
stopConditions: ["all tests pass", "coverage meets threshold"],
|
|
7541
|
-
successCriteria: [
|
|
7542
|
-
"tests written before implementation",
|
|
7543
|
-
"all new tests pass",
|
|
7544
|
-
"no test deletions to fix failures"
|
|
7545
|
-
]
|
|
7546
|
-
},
|
|
7547
|
-
{
|
|
7548
|
-
agent: "reviewer",
|
|
7549
|
-
role: "Review code quality, security, and convention adherence. Read-only.",
|
|
7550
|
-
allowedTaskTypes: ["review", "code-review", "quality-check"],
|
|
7551
|
-
requiredInputs: ["files to review", "context of changes"],
|
|
7552
|
-
expectedOutputFields: ["verdict", "issues", "recommendations"],
|
|
7553
|
-
allowedTools: ["read", "glob", "grep"],
|
|
7554
|
-
forbiddenActions: [
|
|
7555
|
-
"write or edit any files",
|
|
7556
|
-
"make code changes",
|
|
7557
|
-
"approve security-sensitive changes without security audit"
|
|
7558
|
-
],
|
|
7559
|
-
escalationConditions: [
|
|
7560
|
-
"security issues found",
|
|
7561
|
-
"critical bugs found",
|
|
7562
|
-
"architectural violations"
|
|
7563
|
-
],
|
|
7564
|
-
stopConditions: ["review complete", "verdict issued"],
|
|
7565
|
-
successCriteria: [
|
|
7566
|
-
"structured review output with severity levels",
|
|
7567
|
-
"issues categorized",
|
|
7568
|
-
"no file modifications"
|
|
7569
|
-
]
|
|
7570
|
-
},
|
|
7571
|
-
{
|
|
7572
|
-
agent: "security-auditor",
|
|
7573
|
-
role: "Security audit: OWASP Top 10, injection, auth vulnerabilities. Read-only.",
|
|
7574
|
-
allowedTaskTypes: ["security-audit", "vulnerability-scan", "auth-review"],
|
|
7575
|
-
requiredInputs: ["files to audit", "change context"],
|
|
7576
|
-
expectedOutputFields: ["findings", "severity_breakdown", "recommendations"],
|
|
7577
|
-
allowedTools: ["read", "glob", "grep"],
|
|
7578
|
-
forbiddenActions: [
|
|
7579
|
-
"write or edit files",
|
|
7580
|
-
"make changes to fix vulnerabilities directly"
|
|
7581
|
-
],
|
|
7582
|
-
escalationConditions: [
|
|
7583
|
-
"CRITICAL vulnerability found",
|
|
7584
|
-
"auth bypass detected",
|
|
7585
|
-
"data exposure found"
|
|
7586
|
-
],
|
|
7587
|
-
stopConditions: ["audit complete", "all findings documented"],
|
|
7588
|
-
successCriteria: [
|
|
7589
|
-
"OWASP checklist evaluated",
|
|
7590
|
-
"findings documented with severity levels",
|
|
7591
|
-
"no file modifications"
|
|
7592
|
-
]
|
|
7593
|
-
},
|
|
7594
|
-
{
|
|
7595
|
-
agent: "researcher",
|
|
7596
|
-
role: "Research documentation, APIs, best practices. Read-only analysis.",
|
|
7597
|
-
allowedTaskTypes: ["research", "api-lookup", "documentation", "best-practices"],
|
|
7598
|
-
requiredInputs: ["research topic or question"],
|
|
7599
|
-
expectedOutputFields: ["findings", "references", "recommendations"],
|
|
7600
|
-
allowedTools: ["read", "glob", "grep", "web-search"],
|
|
7601
|
-
forbiddenActions: ["write or edit files", "implement solutions"],
|
|
7602
|
-
escalationConditions: [
|
|
7603
|
-
"critical information unavailable",
|
|
7604
|
-
"conflicting official documentation"
|
|
7605
|
-
],
|
|
7606
|
-
stopConditions: ["research question answered", "findings documented"],
|
|
7607
|
-
successCriteria: [
|
|
7608
|
-
"findings clearly summarized",
|
|
7609
|
-
"sources cited",
|
|
7610
|
-
"no file modifications"
|
|
7611
|
-
]
|
|
7612
|
-
},
|
|
7613
|
-
{
|
|
7614
|
-
agent: "architect",
|
|
7615
|
-
role: "Design system architecture, create ADRs, define API contracts.",
|
|
7616
|
-
allowedTaskTypes: ["architecture", "adr", "api-design", "system-design"],
|
|
7617
|
-
requiredInputs: ["feature or system description", "existing codebase context"],
|
|
7618
|
-
expectedOutputFields: ["architecture_document", "adr", "api_contracts"],
|
|
7619
|
-
allowedTools: ["read", "write", "glob", "grep", "planning-state"],
|
|
7620
|
-
forbiddenActions: ["write application code", "run bash commands"],
|
|
7621
|
-
escalationConditions: [
|
|
7622
|
-
"major architectural conflict with existing system",
|
|
7623
|
-
"breaking API change required"
|
|
7624
|
-
],
|
|
7625
|
-
stopConditions: ["ADR written", "architecture reviewed"],
|
|
7626
|
-
successCriteria: [
|
|
7627
|
-
"architecture documented with tradeoffs",
|
|
7628
|
-
"no application code written"
|
|
7629
|
-
]
|
|
7630
|
-
},
|
|
7631
|
-
{
|
|
7632
|
-
agent: "writer",
|
|
7633
|
-
role: "Draft project documentation: README, API docs, user guides.",
|
|
7634
|
-
allowedTaskTypes: ["documentation", "readme", "api-docs", "user-guide"],
|
|
7635
|
-
requiredInputs: ["feature description or codebase context"],
|
|
7636
|
-
expectedOutputFields: ["documentation_files"],
|
|
7637
|
-
allowedTools: ["read", "write", "edit", "glob", "grep"],
|
|
7638
|
-
forbiddenActions: ["modify application code", "run bash commands"],
|
|
7639
|
-
escalationConditions: ["documentation scope unclear"],
|
|
7640
|
-
stopConditions: ["docs written", "user confirms completeness"],
|
|
7641
|
-
successCriteria: [
|
|
7642
|
-
"documentation written and accurate",
|
|
7643
|
-
"no application code changed"
|
|
7644
|
-
]
|
|
7645
|
-
},
|
|
7646
|
-
{
|
|
7647
|
-
agent: "doc-updater",
|
|
7648
|
-
role: "Update existing documentation after code changes.",
|
|
7649
|
-
allowedTaskTypes: ["documentation-update", "doc-sync"],
|
|
7650
|
-
requiredInputs: ["changed files", "change summary"],
|
|
7651
|
-
expectedOutputFields: ["updated_docs"],
|
|
7652
|
-
allowedTools: ["read", "write", "edit", "glob", "grep"],
|
|
7653
|
-
forbiddenActions: [
|
|
7654
|
-
"modify application code",
|
|
7655
|
-
"delete documentation without replacement"
|
|
7656
|
-
],
|
|
7657
|
-
escalationConditions: ["documentation conflicts with implementation"],
|
|
7658
|
-
stopConditions: ["docs updated and synced"],
|
|
7659
|
-
successCriteria: ["docs reflect current code", "no application code changed"]
|
|
7660
|
-
},
|
|
7661
|
-
{
|
|
7662
|
-
agent: "supervisor",
|
|
7663
|
-
role: "Governance review layer. Inspects existing commands/agents, validates policy, returns structured approve/revise/block/escalate decision. Never creates new commands or workflows.",
|
|
7664
|
-
allowedTaskTypes: ["governance-review", "policy-check", "pre-execution-review", "post-stage-review"],
|
|
7665
|
-
requiredInputs: ["target name (command or agent)", "task context"],
|
|
7666
|
-
expectedOutputFields: ["decision", "targetType", "targetName", "exists", "reasons", "missingRequirements", "riskFlags", "requiredChanges", "approvalStatus", "confidenceScore"],
|
|
7667
|
-
allowedTools: ["read", "glob", "grep", "planning-state", "policy-engine"],
|
|
7668
|
-
forbiddenActions: [
|
|
7669
|
-
"create new commands",
|
|
7670
|
-
"create new workflows",
|
|
7671
|
-
"invent new agent names",
|
|
7672
|
-
"modify command intent",
|
|
7673
|
-
"replace orchestrator",
|
|
7674
|
-
"become second dispatcher",
|
|
7675
|
-
"execute implementation tasks",
|
|
7676
|
-
"write or edit source files",
|
|
7677
|
-
"run bash commands",
|
|
7678
|
-
"modify PLAN.md or STATE.md"
|
|
7679
|
-
],
|
|
7680
|
-
escalationConditions: [
|
|
7681
|
-
"human approval required and not granted",
|
|
7682
|
-
"confidence below threshold",
|
|
7683
|
-
"critical policy violation with no safe path forward"
|
|
7684
|
-
],
|
|
7685
|
-
stopConditions: ["structured decision issued", "review complete"],
|
|
7686
|
-
successCriteria: [
|
|
7687
|
-
"structured SupervisorDecision returned",
|
|
7688
|
-
"no new commands or workflows created",
|
|
7689
|
-
"existing registry not modified",
|
|
7690
|
-
"decision is one of: approve, revise, block, escalate"
|
|
7691
|
-
]
|
|
7692
|
-
}
|
|
7693
|
-
];
|
|
7694
|
-
var REGISTRY = new Map(CONTRACTS.map((c) => [c.agent, c]));
|
|
7695
|
-
function getContract(agent) {
|
|
7696
|
-
return REGISTRY.get(agent) ?? null;
|
|
7697
|
-
}
|
|
7698
|
-
|
|
7699
|
-
// src/services/supervisor-binding.ts
|
|
7700
|
-
var REGISTERED_COMMANDS = [
|
|
7701
|
-
"fd-ask",
|
|
7702
|
-
"fd-checkpoint",
|
|
7703
|
-
"fd-deploy-check",
|
|
7704
|
-
"fd-design",
|
|
7705
|
-
"fd-discuss",
|
|
7706
|
-
"fd-doctor",
|
|
7707
|
-
"fd-execute",
|
|
7708
|
-
"fd-fix-bug",
|
|
7709
|
-
"fd-map-codebase",
|
|
7710
|
-
"fd-multi-repo",
|
|
7711
|
-
"fd-new-feature",
|
|
7712
|
-
"fd-plan",
|
|
7713
|
-
"fd-quick",
|
|
7714
|
-
"fd-reflect",
|
|
7715
|
-
"fd-resume",
|
|
7716
|
-
"fd-status",
|
|
7717
|
-
"fd-suggest",
|
|
7718
|
-
"fd-translate-intent",
|
|
7719
|
-
"fd-verify",
|
|
7720
|
-
"fd-write-docs",
|
|
7721
|
-
"fd-done"
|
|
7722
|
-
];
|
|
7723
|
-
function resolveSupervisorConfig(directory) {
|
|
7724
|
-
try {
|
|
7725
|
-
const config = loadFlowDeckConfig(directory);
|
|
7726
|
-
const sup = config?.governance?.supervisor ?? {};
|
|
7727
|
-
return {
|
|
7728
|
-
enabled: sup.enabled ?? false,
|
|
7729
|
-
mode: sup.mode ?? "advisory",
|
|
7730
|
-
reviewedTargets: sup.reviewedTargets ?? [],
|
|
7731
|
-
canBlock: sup.canBlock ?? true,
|
|
7732
|
-
confidenceThreshold: sup.confidenceThreshold ?? 0.7,
|
|
7733
|
-
postExecutionReview: sup.postExecutionReview ?? false
|
|
7734
|
-
};
|
|
7735
|
-
} catch {
|
|
7736
|
-
return {
|
|
7737
|
-
enabled: false,
|
|
7738
|
-
mode: "advisory",
|
|
7739
|
-
reviewedTargets: [],
|
|
7740
|
-
canBlock: true,
|
|
7741
|
-
confidenceThreshold: 0.7,
|
|
7742
|
-
postExecutionReview: false
|
|
7743
|
-
};
|
|
7744
|
-
}
|
|
7745
|
-
}
|
|
7746
|
-
function isRegisteredCommand(name) {
|
|
7747
|
-
return REGISTERED_COMMANDS.includes(name);
|
|
7748
|
-
}
|
|
7749
|
-
function isRegisteredAgent(name) {
|
|
7750
|
-
return AGENT_NAMES.includes(name);
|
|
7751
|
-
}
|
|
7752
|
-
function isRegisteredTarget(name) {
|
|
7753
|
-
if (isRegisteredCommand(name))
|
|
7754
|
-
return { exists: true, type: "command" };
|
|
7755
|
-
if (isRegisteredAgent(name))
|
|
7756
|
-
return { exists: true, type: "agent" };
|
|
7757
|
-
return { exists: false, type: "agent" };
|
|
7758
|
-
}
|
|
7759
|
-
function checkCommandPolicy(commandName, ctx) {
|
|
7760
|
-
const reasons = [];
|
|
7761
|
-
const riskFlags = [];
|
|
7762
|
-
const missingRequirements = [];
|
|
7763
|
-
const requiredChanges = [];
|
|
7764
|
-
if (commandName === "fd-new-feature" || commandName === "fd-execute") {
|
|
7765
|
-
const taskLower = (ctx.taskDescription ?? "").toLowerCase();
|
|
7766
|
-
const isUiHeavy = /landing page|dashboard|admin panel|website|web app|ui|ux|interface|frontend|component/.test(taskLower);
|
|
7767
|
-
if (isUiHeavy && ctx.currentPhase === "execute" && ctx.designApprovalPresent === false) {
|
|
7768
|
-
missingRequirements.push("design approval (design stage must complete before execute for UI-heavy tasks)");
|
|
7769
|
-
riskFlags.push("UI-heavy task entering execute phase without design approval");
|
|
7770
|
-
requiredChanges.push("Run /fd-design first and obtain design approval before proceeding to execute");
|
|
7771
|
-
}
|
|
7772
|
-
}
|
|
7773
|
-
if (commandName === "fd-fix-bug") {
|
|
7774
|
-
if (ctx.regressionTestPresent === false) {
|
|
7775
|
-
missingRequirements.push("regression test (required before bugfix implementation)");
|
|
7776
|
-
riskFlags.push("Bugfix command invoked without a regression test");
|
|
7777
|
-
requiredChanges.push("Write a failing regression test before implementing the fix");
|
|
7778
|
-
}
|
|
7779
|
-
}
|
|
7780
|
-
if (commandName === "fd-deploy-check") {
|
|
7781
|
-
if (ctx.prerequisitesMet === false && ctx.missingInputs && ctx.missingInputs.length > 0) {
|
|
7782
|
-
missingRequirements.push(...ctx.missingInputs);
|
|
7783
|
-
riskFlags.push("Deploy check attempted with unmet prerequisites");
|
|
7784
|
-
}
|
|
7785
|
-
}
|
|
7786
|
-
if (commandName === "fd-execute" && ctx.currentPhase && ctx.currentPhase !== "execute") {
|
|
7787
|
-
riskFlags.push(`fd-execute invoked in phase "${ctx.currentPhase}" instead of "execute"`);
|
|
7788
|
-
requiredChanges.push(`Ensure project phase is "execute" before running fd-execute (currently: ${ctx.currentPhase})`);
|
|
7789
|
-
}
|
|
7790
|
-
if (ctx.approvalRequired && !ctx.approvalGranted) {
|
|
7791
|
-
missingRequirements.push("human approval (required for this command)");
|
|
7792
|
-
riskFlags.push("Approval gate not satisfied");
|
|
7793
|
-
requiredChanges.push("Obtain explicit human approval before proceeding");
|
|
7794
|
-
}
|
|
7795
|
-
const passed = missingRequirements.length === 0 && riskFlags.length === 0 && requiredChanges.length === 0;
|
|
7796
|
-
if (passed) {
|
|
7797
|
-
reasons.push(`Command "${commandName}" passed all policy checks`);
|
|
7798
|
-
}
|
|
7799
|
-
return { passed, reasons, riskFlags, missingRequirements, requiredChanges };
|
|
7800
|
-
}
|
|
7801
|
-
function checkAgentPolicy(agentName, ctx) {
|
|
7802
|
-
const reasons = [];
|
|
7803
|
-
const riskFlags = [];
|
|
7804
|
-
const missingRequirements = [];
|
|
7805
|
-
const requiredChanges = [];
|
|
7806
|
-
const contract = getContract(agentName);
|
|
7807
|
-
if (!contract) {
|
|
7808
|
-
riskFlags.push(`Agent "${agentName}" has no registered capability contract`);
|
|
7809
|
-
return { passed: false, reasons, riskFlags, missingRequirements, requiredChanges };
|
|
7810
|
-
}
|
|
7811
|
-
if (ctx.missingInputs && ctx.missingInputs.length > 0) {
|
|
7812
|
-
for (const missing of ctx.missingInputs) {
|
|
7813
|
-
const isRequired = contract.requiredInputs.some((r) => r.toLowerCase().includes(missing.toLowerCase()) || missing.toLowerCase().includes(r.toLowerCase()));
|
|
7814
|
-
if (isRequired) {
|
|
7815
|
-
missingRequirements.push(missing);
|
|
7816
|
-
requiredChanges.push(`Provide "${missing}" before delegating to ${agentName}`);
|
|
7817
|
-
}
|
|
7818
|
-
}
|
|
7819
|
-
}
|
|
7820
|
-
if (ctx.approvalRequired && !ctx.approvalGranted) {
|
|
7821
|
-
const needsApproval = contract.escalationConditions.some((c) => c.toLowerCase().includes("approval") || c.toLowerCase().includes("approve"));
|
|
7822
|
-
if (needsApproval) {
|
|
7823
|
-
missingRequirements.push("human approval");
|
|
7824
|
-
riskFlags.push(`Agent "${agentName}" requires approval via escalation condition`);
|
|
7825
|
-
requiredChanges.push("Obtain explicit human approval before proceeding");
|
|
7826
|
-
}
|
|
7827
|
-
}
|
|
7828
|
-
if (agentName === "design" || agentName === "frontend-coder") {
|
|
7829
|
-
const taskLower = (ctx.taskDescription ?? "").toLowerCase();
|
|
7830
|
-
const isUiHeavy = /landing page|dashboard|admin panel|website|web app|ui|ux|interface|frontend|component/.test(taskLower);
|
|
7831
|
-
if (agentName === "frontend-coder" && isUiHeavy && ctx.designApprovalPresent === false) {
|
|
7832
|
-
missingRequirements.push("design handoff approval");
|
|
7833
|
-
riskFlags.push("frontend-coder invoked for UI-heavy task without approved design handoff");
|
|
7834
|
-
requiredChanges.push("Complete design stage and obtain design approval before delegating to frontend-coder");
|
|
7835
|
-
}
|
|
7836
|
-
}
|
|
7837
|
-
const passed = missingRequirements.length === 0 && riskFlags.length === 0;
|
|
7838
|
-
if (passed) {
|
|
7839
|
-
reasons.push(`Agent "${agentName}" passed all policy checks`);
|
|
7840
|
-
}
|
|
7841
|
-
return { passed, reasons, riskFlags, missingRequirements, requiredChanges };
|
|
7842
|
-
}
|
|
7843
|
-
function computeConfidence(exists, policyResult, ctx) {
|
|
7844
|
-
if (!exists)
|
|
7845
|
-
return 0;
|
|
7846
|
-
if (policyResult.riskFlags.length >= 3)
|
|
7847
|
-
return 0.2;
|
|
7848
|
-
if (policyResult.riskFlags.length === 2)
|
|
7849
|
-
return 0.4;
|
|
7850
|
-
if (policyResult.riskFlags.length === 1)
|
|
7851
|
-
return 0.6;
|
|
7852
|
-
if (policyResult.missingRequirements.length > 0)
|
|
7853
|
-
return 0.5;
|
|
7854
|
-
if (ctx.prerequisitesMet === false)
|
|
7855
|
-
return 0.45;
|
|
7856
|
-
return 0.95;
|
|
7857
|
-
}
|
|
7858
|
-
function resolveDecision(exists, policyResult, confidenceScore, threshold, ctx, clarificationQuestion) {
|
|
7859
|
-
if (!exists) {
|
|
7860
|
-
return { decision: "block", approvalStatus: "denied" };
|
|
7861
|
-
}
|
|
7862
|
-
if (ctx.approvalRequired && !ctx.approvalGranted) {
|
|
7863
|
-
return { decision: "escalate", approvalStatus: "escalated", clarificationQuestion };
|
|
7864
|
-
}
|
|
7865
|
-
if (!policyResult.passed) {
|
|
7866
|
-
if (policyResult.requiredChanges.length > 0) {
|
|
7867
|
-
return { decision: "revise", approvalStatus: "pending" };
|
|
7868
|
-
}
|
|
7869
|
-
return { decision: "block", approvalStatus: "denied" };
|
|
7870
|
-
}
|
|
7871
|
-
if (confidenceScore < threshold) {
|
|
7872
|
-
return { decision: "escalate", approvalStatus: "escalated", clarificationQuestion };
|
|
7873
|
-
}
|
|
7874
|
-
return { decision: "approve", approvalStatus: "approved" };
|
|
7875
|
-
}
|
|
7876
|
-
function runSupervisorReview(directory, targetName, ctx = {}, clarificationQuestion) {
|
|
7877
|
-
const config = resolveSupervisorConfig(directory);
|
|
7878
|
-
const reviewPhase = ctx.reviewPhase ?? "preflight";
|
|
7879
|
-
const timestamp2 = new Date().toISOString();
|
|
7880
|
-
if (config.reviewedTargets.length > 0 && !config.reviewedTargets.includes(targetName)) {
|
|
7881
|
-
return {
|
|
7882
|
-
decision: "approve",
|
|
7883
|
-
targetType: "agent",
|
|
7884
|
-
targetName,
|
|
7885
|
-
exists: true,
|
|
7886
|
-
reasons: [`Target "${targetName}" is not in the reviewed targets list — auto-approved`],
|
|
7887
|
-
missingRequirements: [],
|
|
7888
|
-
riskFlags: [],
|
|
7889
|
-
requiredChanges: [],
|
|
7890
|
-
approvalStatus: "approved",
|
|
7891
|
-
confidenceScore: 1,
|
|
7892
|
-
reviewPhase,
|
|
7893
|
-
timestamp: timestamp2
|
|
7894
|
-
};
|
|
7895
|
-
}
|
|
7896
|
-
const { exists, type: targetType } = isRegisteredTarget(targetName);
|
|
7897
|
-
if (!exists) {
|
|
7898
|
-
const decision2 = {
|
|
7899
|
-
decision: "block",
|
|
7900
|
-
targetType,
|
|
7901
|
-
targetName,
|
|
7902
|
-
exists: false,
|
|
7903
|
-
reasons: [
|
|
7904
|
-
`Target "${targetName}" is not registered in the FlowDeck command or agent registry.`,
|
|
7905
|
-
"The supervisor does not create new commands or workflows.",
|
|
7906
|
-
"Only registered targets can be executed."
|
|
7907
|
-
],
|
|
7908
|
-
missingRequirements: [],
|
|
7909
|
-
riskFlags: [`Unregistered target: "${targetName}"`],
|
|
7910
|
-
requiredChanges: [
|
|
7911
|
-
`Use one of the registered commands: ${REGISTERED_COMMANDS.join(", ")}`,
|
|
7912
|
-
`Or use one of the registered agents: ${AGENT_NAMES.join(", ")}`
|
|
7913
|
-
],
|
|
7914
|
-
approvalStatus: "denied",
|
|
7915
|
-
confidenceScore: 0,
|
|
7916
|
-
reviewPhase,
|
|
7917
|
-
timestamp: timestamp2
|
|
7918
|
-
};
|
|
7919
|
-
return decision2;
|
|
7920
|
-
}
|
|
7921
|
-
const policyResult = targetType === "command" ? checkCommandPolicy(targetName, ctx) : checkAgentPolicy(targetName, ctx);
|
|
7922
|
-
const confidenceScore = computeConfidence(exists, policyResult, ctx);
|
|
7923
|
-
const { decision, approvalStatus, clarificationQuestion: escalationQuestion } = resolveDecision(exists, policyResult, confidenceScore, config.confidenceThreshold, ctx, clarificationQuestion);
|
|
7924
|
-
const reasons = policyResult.reasons.length > 0 ? policyResult.reasons : decision === "approve" ? [`Target "${targetName}" reviewed and approved for execution`] : [`Target "${targetName}" reviewed — decision: ${decision}`];
|
|
7925
|
-
const supervisorDecision = {
|
|
7926
|
-
decision,
|
|
7927
|
-
targetType,
|
|
7928
|
-
targetName,
|
|
7929
|
-
exists,
|
|
7930
|
-
reasons,
|
|
7931
|
-
missingRequirements: policyResult.missingRequirements,
|
|
7932
|
-
riskFlags: policyResult.riskFlags,
|
|
7933
|
-
requiredChanges: policyResult.requiredChanges,
|
|
7934
|
-
approvalStatus,
|
|
7935
|
-
confidenceScore,
|
|
7936
|
-
reviewPhase,
|
|
7937
|
-
timestamp: timestamp2,
|
|
7938
|
-
...escalationQuestion ? { clarificationQuestion: escalationQuestion } : {}
|
|
7939
|
-
};
|
|
7940
|
-
return supervisorDecision;
|
|
7941
|
-
}
|
|
7942
|
-
function shouldProceed(decision, mode, canBlock) {
|
|
7943
|
-
if (!decision.exists)
|
|
7944
|
-
return false;
|
|
7945
|
-
if (!canBlock)
|
|
7946
|
-
return true;
|
|
7947
|
-
if (mode === "strict") {
|
|
7948
|
-
return decision.decision === "approve" || decision.decision === "revise";
|
|
7949
|
-
}
|
|
7950
|
-
return decision.decision !== "block" || decision.confidenceScore > 0.3;
|
|
7951
|
-
}
|
|
7952
|
-
|
|
7953
6687
|
// src/index.ts
|
|
7954
6688
|
function lazyLoadRulePaths(projectRoot) {
|
|
7955
6689
|
const __dir = dirname3(fileURLToPath2(import.meta.url));
|
|
7956
|
-
const rulesDir =
|
|
7957
|
-
if (!
|
|
6690
|
+
const rulesDir = join24(__dir, "..", "src", "rules");
|
|
6691
|
+
if (!existsSync25(rulesDir))
|
|
7958
6692
|
return { paths: [], diagnostics: "[LazyRuleLoader] rules directory not found" };
|
|
7959
6693
|
const detectedLanguages = detectProjectLanguages(projectRoot);
|
|
7960
6694
|
const paths = getStartupRulePaths(rulesDir, detectedLanguages);
|
|
@@ -7964,16 +6698,16 @@ function lazyLoadRulePaths(projectRoot) {
|
|
|
7964
6698
|
}
|
|
7965
6699
|
function loadCommands() {
|
|
7966
6700
|
const __dir = dirname3(fileURLToPath2(import.meta.url));
|
|
7967
|
-
const commandsDir =
|
|
7968
|
-
if (!
|
|
6701
|
+
const commandsDir = join24(__dir, "..", "src", "commands");
|
|
6702
|
+
if (!existsSync25(commandsDir))
|
|
7969
6703
|
return {};
|
|
7970
6704
|
const commands = {};
|
|
7971
6705
|
try {
|
|
7972
|
-
for (const file of
|
|
6706
|
+
for (const file of readdirSync3(commandsDir)) {
|
|
7973
6707
|
if (!file.endsWith(".md"))
|
|
7974
6708
|
continue;
|
|
7975
6709
|
const name = basename2(file, ".md");
|
|
7976
|
-
const raw =
|
|
6710
|
+
const raw = readFileSync24(join24(commandsDir, file), "utf-8");
|
|
7977
6711
|
let description;
|
|
7978
6712
|
let template = raw;
|
|
7979
6713
|
const fmMatch = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/);
|
|
@@ -7991,8 +6725,6 @@ function loadCommands() {
|
|
|
7991
6725
|
var plugin = async (input, _options) => {
|
|
7992
6726
|
const { directory, client, worktree } = input;
|
|
7993
6727
|
const appLog = (msg) => client.app.log({ body: { service: "flowdeck", level: "info", message: msg } }).catch(() => {});
|
|
7994
|
-
const runPipelineTool = createRunPipelineTool(client);
|
|
7995
|
-
const delegateTool = createDelegateTool(client);
|
|
7996
6728
|
const councilTool = createCouncilTool(client);
|
|
7997
6729
|
const fileTracker = new SessionFileTracker;
|
|
7998
6730
|
const { fileEdited, fileWatcherUpdated } = createFileTrackerHooks(fileTracker);
|
|
@@ -8057,8 +6789,8 @@ var plugin = async (input, _options) => {
|
|
|
8057
6789
|
}
|
|
8058
6790
|
}
|
|
8059
6791
|
}
|
|
8060
|
-
const skillsDir =
|
|
8061
|
-
if (
|
|
6792
|
+
const skillsDir = join24(dirname3(fileURLToPath2(import.meta.url)), "..", "src", "skills");
|
|
6793
|
+
if (existsSync25(skillsDir)) {
|
|
8062
6794
|
const cfgAny = cfg;
|
|
8063
6795
|
if (!cfgAny.skills || typeof cfgAny.skills !== "object") {
|
|
8064
6796
|
cfgAny.skills = { paths: [] };
|
|
@@ -8087,8 +6819,6 @@ var plugin = async (input, _options) => {
|
|
|
8087
6819
|
tool: {
|
|
8088
6820
|
"planning-state": planningStateTool,
|
|
8089
6821
|
"codebase-state": codebaseStateTool,
|
|
8090
|
-
"run-pipeline": runPipelineTool,
|
|
8091
|
-
delegate: delegateTool,
|
|
8092
6822
|
"repo-memory": repoMemoryTool,
|
|
8093
6823
|
"failure-replay": failureReplayTool,
|
|
8094
6824
|
"decision-trace": decisionTraceTool,
|
|
@@ -8162,33 +6892,6 @@ var plugin = async (input, _options) => {
|
|
|
8162
6892
|
}
|
|
8163
6893
|
}
|
|
8164
6894
|
orchestratorGuard.check(toolInput.sessionID ?? "", toolInput.tool ?? toolInput.name ?? "");
|
|
8165
|
-
const toolName = toolInput.tool ?? toolInput.name ?? "";
|
|
8166
|
-
if (toolName === "delegate" || toolName === "run-pipeline") {
|
|
8167
|
-
const supConfig = resolveSupervisorConfig(directory);
|
|
8168
|
-
if (supConfig.enabled) {
|
|
8169
|
-
const args = toolOutput?.args ?? toolInput?.args ?? {};
|
|
8170
|
-
const agentTarget = typeof args.agent === "string" ? args.agent.replace(/^@/, "") : Array.isArray(args.steps) && args.steps[0]?.agent ? String(args.steps[0].agent).replace(/^@/, "") : "";
|
|
8171
|
-
if (agentTarget) {
|
|
8172
|
-
const decision = runSupervisorReview(directory, agentTarget, {
|
|
8173
|
-
taskDescription: typeof args.prompt === "string" ? args.prompt : undefined,
|
|
8174
|
-
reviewPhase: "preflight",
|
|
8175
|
-
session_id: toolInput.sessionID ?? toolInput.sessionId ?? ""
|
|
8176
|
-
});
|
|
8177
|
-
const proceed = shouldProceed(decision, supConfig.mode, supConfig.canBlock);
|
|
8178
|
-
appLog(`[Supervisor] ${decision.reviewPhase} review of "${decision.targetName}": ` + `decision=${decision.decision} exists=${decision.exists} confidence=${decision.confidenceScore.toFixed(2)} ` + `${decision.riskFlags.length > 0 ? `risks=[${decision.riskFlags.join("; ")}]` : ""}`);
|
|
8179
|
-
if (!proceed) {
|
|
8180
|
-
const summary = [
|
|
8181
|
-
`[Supervisor] Execution blocked for target "${decision.targetName}".`,
|
|
8182
|
-
...decision.reasons,
|
|
8183
|
-
...decision.missingRequirements.length > 0 ? [`Missing: ${decision.missingRequirements.join(", ")}`] : [],
|
|
8184
|
-
...decision.requiredChanges.length > 0 ? [`Required changes: ${decision.requiredChanges.join("; ")}`] : []
|
|
8185
|
-
].join(`
|
|
8186
|
-
`);
|
|
8187
|
-
throw new Error(summary);
|
|
8188
|
-
}
|
|
8189
|
-
}
|
|
8190
|
-
}
|
|
8191
|
-
}
|
|
8192
6895
|
await approvalHook({ directory }, toolInput, toolOutput);
|
|
8193
6896
|
await guardRailsHook({ directory }, toolInput, toolOutput);
|
|
8194
6897
|
await toolGuardHook({ directory }, toolInput, toolOutput);
|
|
@@ -8198,30 +6901,6 @@ var plugin = async (input, _options) => {
|
|
|
8198
6901
|
},
|
|
8199
6902
|
"tool.execute.after": async (toolInput, toolOutput) => {
|
|
8200
6903
|
await eventLog.after({ directory }, toolInput, toolOutput);
|
|
8201
|
-
const afterToolName = toolInput.tool ?? toolInput.name ?? "";
|
|
8202
|
-
if (afterToolName === "delegate" || afterToolName === "run-pipeline") {
|
|
8203
|
-
try {
|
|
8204
|
-
const supConfig = resolveSupervisorConfig(directory);
|
|
8205
|
-
if (supConfig.enabled && supConfig.postExecutionReview) {
|
|
8206
|
-
const args = toolOutput?.args ?? toolInput?.args ?? {};
|
|
8207
|
-
const agentTarget = typeof args.agent === "string" ? args.agent.replace(/^@/, "") : Array.isArray(args.steps) && args.steps[0]?.agent ? String(args.steps[0].agent).replace(/^@/, "") : "";
|
|
8208
|
-
if (agentTarget) {
|
|
8209
|
-
const executionErrored = toolOutput?.error != null || toolOutput?.status === "error" || typeof toolOutput?.output === "string" && toolOutput.output.startsWith("Error:");
|
|
8210
|
-
const decision = runSupervisorReview(directory, agentTarget, {
|
|
8211
|
-
taskDescription: typeof args.prompt === "string" ? args.prompt : undefined,
|
|
8212
|
-
reviewPhase: "post-stage",
|
|
8213
|
-
session_id: toolInput.sessionID ?? toolInput.sessionId ?? "",
|
|
8214
|
-
prerequisitesMet: !executionErrored
|
|
8215
|
-
});
|
|
8216
|
-
const logLevel = decision.decision === "block" || decision.decision === "escalate" ? "[Supervisor][WARN]" : "[Supervisor]";
|
|
8217
|
-
appLog(`${logLevel} post-stage review of "${decision.targetName}": ` + `decision=${decision.decision} exists=${decision.exists} confidence=${decision.confidenceScore.toFixed(2)} ` + `executionErrored=${executionErrored} ` + `${decision.riskFlags.length > 0 ? `risks=[${decision.riskFlags.join("; ")}]` : ""}`);
|
|
8218
|
-
if (supConfig.mode === "strict" && !shouldProceed(decision, "strict", supConfig.canBlock)) {
|
|
8219
|
-
appLog(`[Supervisor][STRICT] Post-execution governance violation detected for "${decision.targetName}". ` + `Review the scorecard and telemetry for this run. ` + `Reasons: ${decision.reasons.join("; ")}`);
|
|
8220
|
-
}
|
|
8221
|
-
}
|
|
8222
|
-
}
|
|
8223
|
-
} catch {}
|
|
8224
|
-
}
|
|
8225
6904
|
await contextMonitor["tool.execute.after"](toolInput, toolOutput);
|
|
8226
6905
|
}
|
|
8227
6906
|
};
|