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