@dv.nghiem/flowdeck 0.3.3 → 0.3.5
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 +160 -8
- package/dist/agents/coder.d.ts +3 -1
- package/dist/agents/coder.d.ts.map +1 -1
- package/dist/agents/design.d.ts +3 -0
- package/dist/agents/design.d.ts.map +1 -0
- package/dist/agents/index.d.ts +4 -3
- package/dist/agents/index.d.ts.map +1 -1
- package/dist/agents/orchestrator.d.ts.map +1 -1
- package/dist/agents/reviewer.d.ts.map +1 -1
- package/dist/agents/specialist.d.ts +0 -1
- package/dist/agents/specialist.d.ts.map +1 -1
- package/dist/config/index.d.ts +1 -1
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/loader.d.ts +8 -0
- package/dist/config/loader.d.ts.map +1 -1
- package/dist/config/schema.d.ts +55 -2
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/dashboard/server.mjs +24 -1
- package/dist/dashboard/types.d.ts +72 -0
- package/dist/dashboard/types.d.ts.map +1 -1
- package/dist/hooks/guard-rails.d.ts.map +1 -1
- package/dist/hooks/orchestrator-guard-hook.d.ts +4 -1
- package/dist/hooks/orchestrator-guard-hook.d.ts.map +1 -1
- package/dist/hooks/session-idle-hook.d.ts.map +1 -1
- package/dist/hooks/telemetry-hook.d.ts +14 -1
- package/dist/hooks/telemetry-hook.d.ts.map +1 -1
- package/dist/hooks/telemetry-hook.test.d.ts +2 -0
- package/dist/hooks/telemetry-hook.test.d.ts.map +1 -0
- package/dist/hooks/tool-guard.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +810 -474
- package/dist/services/agent-contract-registry.d.ts +32 -0
- package/dist/services/agent-contract-registry.d.ts.map +1 -0
- package/dist/services/agent-performance.d.ts +1 -1
- package/dist/services/agent-performance.d.ts.map +1 -1
- package/dist/services/agent-trace-graph.d.ts +94 -0
- package/dist/services/agent-trace-graph.d.ts.map +1 -0
- package/dist/services/agent-validator.d.ts +56 -0
- package/dist/services/agent-validator.d.ts.map +1 -0
- package/dist/services/deadlock-detector.d.ts +34 -0
- package/dist/services/deadlock-detector.d.ts.map +1 -0
- package/dist/services/delegation-budget.d.ts +54 -0
- package/dist/services/delegation-budget.d.ts.map +1 -0
- package/dist/services/governance.test.d.ts +11 -0
- package/dist/services/governance.test.d.ts.map +1 -0
- package/dist/services/index.d.ts +6 -1
- package/dist/services/index.d.ts.map +1 -1
- package/dist/services/telemetry.d.ts +1 -1
- package/dist/services/telemetry.d.ts.map +1 -1
- package/dist/services/workflow-scorecard.d.ts +76 -0
- package/dist/services/workflow-scorecard.d.ts.map +1 -0
- package/dist/tools/council.d.ts.map +1 -1
- package/dist/tools/delegate.d.ts.map +1 -1
- package/dist/tools/dispatch-routing.d.ts +9 -0
- package/dist/tools/dispatch-routing.d.ts.map +1 -0
- package/dist/tools/dispatch-routing.test.d.ts +2 -0
- package/dist/tools/dispatch-routing.test.d.ts.map +1 -0
- package/dist/tools/planning-state-lib.d.ts +8 -0
- package/dist/tools/planning-state-lib.d.ts.map +1 -1
- package/dist/tools/planning-state.d.ts.map +1 -1
- package/dist/tools/run-pipeline.d.ts.map +1 -1
- package/docs/agents.md +104 -74
- package/docs/best-practices.md +1 -1
- package/docs/commands/fd-ask.md +2 -2
- package/docs/commands/fd-fix-bug.md +2 -2
- package/docs/commands/fd-new-feature.md +2 -2
- package/docs/commands/fd-quick.md +3 -1
- package/docs/commands.md +37 -7
- package/docs/configuration.md +76 -46
- package/docs/design-first-workflow.md +94 -0
- package/docs/feature-integration-architecture.md +3 -31
- package/docs/index.md +5 -2
- package/docs/installation.md +6 -17
- package/docs/intelligence.md +110 -34
- package/docs/multi-repo.md +1 -1
- package/docs/optimization-baseline.md +21 -0
- package/docs/rules.md +10 -37
- package/docs/skills.md +24 -15
- package/docs/workflows.md +18 -14
- package/package.json +4 -2
- package/src/commands/fd-ask.md +1 -0
- package/src/commands/fd-design.md +64 -0
- package/src/commands/fd-discuss.md +2 -0
- package/src/commands/fd-execute.md +7 -3
- package/src/commands/fd-fix-bug.md +2 -2
- package/src/commands/fd-multi-repo.md +3 -3
- package/src/commands/fd-plan.md +2 -0
- package/src/commands/fd-quick.md +4 -1
- package/src/commands/fd-verify.md +6 -0
- package/src/rules/README.md +10 -0
- package/src/rules/common/agent-orchestration.md +6 -6
- package/src/rules/common/coding-style.md +2 -2
- package/src/rules/typescript/patterns.md +1 -1
- package/src/skills/app-shell-design/SKILL.md +31 -0
- package/src/skills/backend-patterns/SKILL.md +6 -0
- package/src/skills/clean-architecture/SKILL.md +6 -0
- package/src/skills/cqrs/SKILL.md +6 -0
- package/src/skills/dashboard-design/SKILL.md +32 -0
- package/src/skills/ddd-architecture/SKILL.md +6 -0
- package/src/skills/decision-trace/SKILL.md +1 -1
- package/src/skills/design-audit/SKILL.md +37 -0
- package/src/skills/design-system-definition/SKILL.md +33 -0
- package/src/skills/event-driven-architecture/SKILL.md +6 -0
- package/src/skills/frontend-handoff/SKILL.md +31 -0
- package/src/skills/hexagonal-architecture/SKILL.md +6 -0
- package/src/skills/landing-page-design/SKILL.md +32 -0
- package/src/skills/layered-architecture/SKILL.md +6 -0
- package/src/skills/multi-repo/SKILL.md +3 -3
- package/src/skills/plan-task/SKILL.md +2 -2
- package/src/skills/postgres-patterns/SKILL.md +6 -0
- package/src/skills/responsive-review/SKILL.md +31 -0
- package/src/skills/saga-architecture/SKILL.md +6 -0
- package/src/skills/ui-ux-planning/SKILL.md +32 -0
- package/src/skills/wireframe-planning/SKILL.md +30 -0
- package/dist/services/model-router.d.ts +0 -35
- package/dist/services/model-router.d.ts.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
|
-
import { readdirSync as readdirSync3, readFileSync as
|
|
3
|
-
import { join as
|
|
2
|
+
import { readdirSync as readdirSync3, readFileSync as readFileSync23, existsSync as existsSync27 } from "fs";
|
|
3
|
+
import { join as join26, basename } from "path";
|
|
4
4
|
import { dirname as dirname4 } from "path";
|
|
5
5
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
6
6
|
|
|
@@ -128,6 +128,8 @@ function parseState(content) {
|
|
|
128
128
|
result[key] = value.replace(/[\[\]]/g, "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
129
129
|
} else if (key === "plan_confirmed") {
|
|
130
130
|
result[key] = value === "true";
|
|
131
|
+
} else if (key === "requires_design_first" || key === "design_approved" || key === "design_override") {
|
|
132
|
+
result[key] = value === "true";
|
|
131
133
|
} else if (value !== "" && !isNaN(Number(value)) && key !== "plan_file" && key !== "confirmed_at") {
|
|
132
134
|
result[key] = Number(value);
|
|
133
135
|
} else {
|
|
@@ -155,7 +157,21 @@ ${entry}
|
|
|
155
157
|
function readPlanningState(dir) {
|
|
156
158
|
const sp = statePath(dir);
|
|
157
159
|
if (!existsSync2(sp)) {
|
|
158
|
-
return {
|
|
160
|
+
return {
|
|
161
|
+
phase: 0,
|
|
162
|
+
status: "",
|
|
163
|
+
plan_confirmed: false,
|
|
164
|
+
requires_design_first: false,
|
|
165
|
+
design_stage: "pending",
|
|
166
|
+
design_approved: false,
|
|
167
|
+
design_override: false,
|
|
168
|
+
steps_complete: [],
|
|
169
|
+
steps_pending: [],
|
|
170
|
+
last_action: "",
|
|
171
|
+
next_action: "",
|
|
172
|
+
blockers: [],
|
|
173
|
+
tdd: undefined
|
|
174
|
+
};
|
|
159
175
|
}
|
|
160
176
|
const content = readFileSync2(sp, "utf-8");
|
|
161
177
|
const parsed = parseState(content);
|
|
@@ -163,6 +179,13 @@ function readPlanningState(dir) {
|
|
|
163
179
|
phase: parsed.phase || 1,
|
|
164
180
|
status: parsed.status || "",
|
|
165
181
|
plan_confirmed: Boolean(parsed.plan_confirmed),
|
|
182
|
+
task_type: parsed.task_type || undefined,
|
|
183
|
+
requires_design_first: Boolean(parsed.requires_design_first),
|
|
184
|
+
design_stage: parsed.design_stage || "pending",
|
|
185
|
+
design_approved: Boolean(parsed.design_approved),
|
|
186
|
+
design_override: Boolean(parsed.design_override),
|
|
187
|
+
design_override_reason: parsed.design_override_reason || undefined,
|
|
188
|
+
design_artifact: parsed.design_artifact || undefined,
|
|
166
189
|
steps_complete: parsed.steps_complete || [],
|
|
167
190
|
steps_pending: parsed.steps_pending || [],
|
|
168
191
|
last_action: parsed.last_action || "",
|
|
@@ -251,6 +274,13 @@ var planningStateTool = tool2({
|
|
|
251
274
|
plan_file: tool2.schema.string().optional(),
|
|
252
275
|
plan_confirmed: tool2.schema.boolean().optional(),
|
|
253
276
|
confirmed_at: tool2.schema.string().optional(),
|
|
277
|
+
task_type: tool2.schema.string().optional(),
|
|
278
|
+
requires_design_first: tool2.schema.boolean().optional(),
|
|
279
|
+
design_stage: tool2.schema.enum(["pending", "discovery", "ux_planning", "wireframe_layout", "visual_system_definition", "design_approval", "handoff_complete"]).optional(),
|
|
280
|
+
design_approved: tool2.schema.boolean().optional(),
|
|
281
|
+
design_override: tool2.schema.boolean().optional(),
|
|
282
|
+
design_override_reason: tool2.schema.string().optional(),
|
|
283
|
+
design_artifact: tool2.schema.string().optional(),
|
|
254
284
|
steps_complete: tool2.schema.array(tool2.schema.number()).optional(),
|
|
255
285
|
steps_pending: tool2.schema.array(tool2.schema.number()).optional()
|
|
256
286
|
}).optional(),
|
|
@@ -273,16 +303,24 @@ var planningStateTool = tool2({
|
|
|
273
303
|
if (!u)
|
|
274
304
|
return JSON.stringify({ error: "No updates provided" });
|
|
275
305
|
let content = readFileSync3(sp, "utf-8");
|
|
306
|
+
const upsertLine = (current, key, value) => {
|
|
307
|
+
const pattern = new RegExp(`^${key}:\\s*.*$`, "m");
|
|
308
|
+
if (pattern.test(current))
|
|
309
|
+
return current.replace(pattern, `${key}: ${value}`);
|
|
310
|
+
return `${current.trimEnd()}
|
|
311
|
+
${key}: ${value}
|
|
312
|
+
`;
|
|
313
|
+
};
|
|
276
314
|
if (u.phase !== undefined)
|
|
277
|
-
content = content
|
|
315
|
+
content = upsertLine(content, "phase", `${u.phase}`);
|
|
278
316
|
if (u.status !== undefined)
|
|
279
|
-
content = content
|
|
317
|
+
content = upsertLine(content, "status", `${u.status}`);
|
|
280
318
|
if (u.last_action !== undefined) {
|
|
281
|
-
content = content
|
|
319
|
+
content = upsertLine(content, "last_action", `"${u.last_action}"`);
|
|
282
320
|
content = appendHistory(content, u.last_action);
|
|
283
321
|
}
|
|
284
322
|
if (u.next_action !== undefined)
|
|
285
|
-
content = content
|
|
323
|
+
content = upsertLine(content, "next_action", `"${u.next_action}"`);
|
|
286
324
|
if (u.blockers !== undefined) {
|
|
287
325
|
const blockersMd = u.blockers.map((b) => `- ${b}`).join(`
|
|
288
326
|
`);
|
|
@@ -291,15 +329,29 @@ ${blockersMd}
|
|
|
291
329
|
`);
|
|
292
330
|
}
|
|
293
331
|
if (u.plan_file !== undefined)
|
|
294
|
-
content = content
|
|
332
|
+
content = upsertLine(content, "plan_file", `${u.plan_file}`);
|
|
295
333
|
if (u.plan_confirmed !== undefined)
|
|
296
|
-
content = content
|
|
334
|
+
content = upsertLine(content, "plan_confirmed", `${u.plan_confirmed}`);
|
|
297
335
|
if (u.confirmed_at !== undefined)
|
|
298
|
-
content = content
|
|
336
|
+
content = upsertLine(content, "confirmed_at", `${u.confirmed_at}`);
|
|
337
|
+
if (u.task_type !== undefined)
|
|
338
|
+
content = upsertLine(content, "task_type", `"${u.task_type}"`);
|
|
339
|
+
if (u.requires_design_first !== undefined)
|
|
340
|
+
content = upsertLine(content, "requires_design_first", `${u.requires_design_first}`);
|
|
341
|
+
if (u.design_stage !== undefined)
|
|
342
|
+
content = upsertLine(content, "design_stage", `"${u.design_stage}"`);
|
|
343
|
+
if (u.design_approved !== undefined)
|
|
344
|
+
content = upsertLine(content, "design_approved", `${u.design_approved}`);
|
|
345
|
+
if (u.design_override !== undefined)
|
|
346
|
+
content = upsertLine(content, "design_override", `${u.design_override}`);
|
|
347
|
+
if (u.design_override_reason !== undefined)
|
|
348
|
+
content = upsertLine(content, "design_override_reason", `"${u.design_override_reason}"`);
|
|
349
|
+
if (u.design_artifact !== undefined)
|
|
350
|
+
content = upsertLine(content, "design_artifact", `'${u.design_artifact.replace(/'/g, "''")}'`);
|
|
299
351
|
if (u.steps_complete !== undefined)
|
|
300
|
-
content = content
|
|
352
|
+
content = upsertLine(content, "steps_complete", `[${u.steps_complete.join(", ")}]`);
|
|
301
353
|
if (u.steps_pending !== undefined)
|
|
302
|
-
content = content
|
|
354
|
+
content = upsertLine(content, "steps_pending", `[${u.steps_pending.join(", ")}]`);
|
|
303
355
|
writeFileSync3(sp, content, "utf-8");
|
|
304
356
|
return JSON.stringify({ success: true, updated_at: timestamp() });
|
|
305
357
|
}
|
|
@@ -509,6 +561,152 @@ var workspaceStateTool = tool3({
|
|
|
509
561
|
|
|
510
562
|
// src/tools/run-pipeline.ts
|
|
511
563
|
import { tool as tool4 } from "@opencode-ai/plugin";
|
|
564
|
+
|
|
565
|
+
// src/services/agent-performance.ts
|
|
566
|
+
import { existsSync as existsSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync5, mkdirSync as mkdirSync2 } from "fs";
|
|
567
|
+
import { join as join5 } from "path";
|
|
568
|
+
function perfPath(dir) {
|
|
569
|
+
return join5(codebaseDir(dir), "AGENT_PERF.json");
|
|
570
|
+
}
|
|
571
|
+
function loadStore(dir) {
|
|
572
|
+
const p = perfPath(dir);
|
|
573
|
+
if (!existsSync5(p))
|
|
574
|
+
return { entries: [], updated_at: new Date().toISOString() };
|
|
575
|
+
try {
|
|
576
|
+
return JSON.parse(readFileSync5(p, "utf-8"));
|
|
577
|
+
} catch {
|
|
578
|
+
return { entries: [], updated_at: new Date().toISOString() };
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
function saveStore(dir, store) {
|
|
582
|
+
const cd = codebaseDir(dir);
|
|
583
|
+
if (!existsSync5(cd))
|
|
584
|
+
mkdirSync2(cd, { recursive: true });
|
|
585
|
+
writeFileSync5(perfPath(dir), JSON.stringify(store, null, 2), "utf-8");
|
|
586
|
+
}
|
|
587
|
+
function makeKey(agent, model, task_type) {
|
|
588
|
+
return `${agent}::${model}::${task_type}`;
|
|
589
|
+
}
|
|
590
|
+
function recordRun(dir, agent, model, task_type, success, duration_ms, cost = 0) {
|
|
591
|
+
const store = loadStore(dir);
|
|
592
|
+
const key = makeKey(agent, model, task_type);
|
|
593
|
+
const existing = store.entries.find((e) => makeKey(e.agent, e.model, e.task_type) === key);
|
|
594
|
+
if (existing) {
|
|
595
|
+
existing.runs++;
|
|
596
|
+
if (success)
|
|
597
|
+
existing.successes++;
|
|
598
|
+
else
|
|
599
|
+
existing.failures++;
|
|
600
|
+
existing.total_duration_ms += duration_ms;
|
|
601
|
+
existing.total_cost += cost;
|
|
602
|
+
existing.last_run = new Date().toISOString();
|
|
603
|
+
existing.last_status = success ? "success" : "failure";
|
|
604
|
+
} else {
|
|
605
|
+
store.entries.push({
|
|
606
|
+
agent,
|
|
607
|
+
model,
|
|
608
|
+
task_type,
|
|
609
|
+
runs: 1,
|
|
610
|
+
successes: success ? 1 : 0,
|
|
611
|
+
failures: success ? 0 : 1,
|
|
612
|
+
total_duration_ms: duration_ms,
|
|
613
|
+
total_cost: cost,
|
|
614
|
+
last_run: new Date().toISOString(),
|
|
615
|
+
last_status: success ? "success" : "failure"
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
store.updated_at = new Date().toISOString();
|
|
619
|
+
saveStore(dir, store);
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// src/tools/dispatch-routing.ts
|
|
623
|
+
function shouldRetry(promptRes) {
|
|
624
|
+
if (!promptRes)
|
|
625
|
+
return false;
|
|
626
|
+
const detail = promptRes.error?.detail;
|
|
627
|
+
if (isTransientError(detail))
|
|
628
|
+
return true;
|
|
629
|
+
const infoError = promptRes.data?.info?.error;
|
|
630
|
+
const text = typeof infoError === "string" ? infoError : JSON.stringify(infoError ?? "");
|
|
631
|
+
return isTransientError(text);
|
|
632
|
+
}
|
|
633
|
+
function isTransientError(text) {
|
|
634
|
+
if (!text)
|
|
635
|
+
return false;
|
|
636
|
+
const haystack = text.toLowerCase();
|
|
637
|
+
return haystack.includes("overload") || haystack.includes("rate limit") || haystack.includes("timeout") || haystack.includes("temporar") || haystack.includes("econnreset");
|
|
638
|
+
}
|
|
639
|
+
function normalizeTaskType(taskType, agent) {
|
|
640
|
+
const normalized = (taskType ?? "").trim().toLowerCase();
|
|
641
|
+
if (isTaskType(normalized))
|
|
642
|
+
return normalized;
|
|
643
|
+
const a = agent.toLowerCase();
|
|
644
|
+
if (a.includes("design") || a.includes("ui-ux"))
|
|
645
|
+
return "design";
|
|
646
|
+
if (a.includes("review"))
|
|
647
|
+
return "review";
|
|
648
|
+
if (a.includes("test"))
|
|
649
|
+
return "testing";
|
|
650
|
+
if (a.includes("debug"))
|
|
651
|
+
return "debugging";
|
|
652
|
+
if (a.includes("security"))
|
|
653
|
+
return "security";
|
|
654
|
+
if (a.includes("doc"))
|
|
655
|
+
return "documentation";
|
|
656
|
+
if (a.includes("architect") || a.includes("planner"))
|
|
657
|
+
return "planning";
|
|
658
|
+
if (a.includes("orchestrator") || a.includes("coordinator"))
|
|
659
|
+
return "orchestration";
|
|
660
|
+
if (a.includes("analyst") || a.includes("research"))
|
|
661
|
+
return "analysis";
|
|
662
|
+
return "implementation";
|
|
663
|
+
}
|
|
664
|
+
function isTaskType(value) {
|
|
665
|
+
return value === "planning" || value === "design" || value === "implementation" || value === "debugging" || value === "review" || value === "testing" || value === "documentation" || value === "analysis" || value === "security" || value === "orchestration";
|
|
666
|
+
}
|
|
667
|
+
var UI_HEAVY_KEYWORDS = [
|
|
668
|
+
"landing page",
|
|
669
|
+
"marketing site",
|
|
670
|
+
"website",
|
|
671
|
+
"web app",
|
|
672
|
+
"mobile app",
|
|
673
|
+
"app screen",
|
|
674
|
+
"dashboard",
|
|
675
|
+
"admin panel",
|
|
676
|
+
"settings page",
|
|
677
|
+
"onboarding ux",
|
|
678
|
+
"kanban",
|
|
679
|
+
"design system",
|
|
680
|
+
"responsive",
|
|
681
|
+
"ui",
|
|
682
|
+
"ux",
|
|
683
|
+
"cta",
|
|
684
|
+
"conversion flow",
|
|
685
|
+
"saas interface",
|
|
686
|
+
"user-facing"
|
|
687
|
+
];
|
|
688
|
+
var NON_UI_KEYWORDS = [
|
|
689
|
+
"backend",
|
|
690
|
+
"infrastructure",
|
|
691
|
+
"migration",
|
|
692
|
+
"pipeline",
|
|
693
|
+
"api only",
|
|
694
|
+
"database only",
|
|
695
|
+
"cli",
|
|
696
|
+
"worker"
|
|
697
|
+
];
|
|
698
|
+
function isUiHeavyTask(input) {
|
|
699
|
+
const normalized = input.trim().toLowerCase();
|
|
700
|
+
if (!normalized)
|
|
701
|
+
return false;
|
|
702
|
+
const hasUiSignal = UI_HEAVY_KEYWORDS.some((keyword) => normalized.includes(keyword));
|
|
703
|
+
if (!hasUiSignal)
|
|
704
|
+
return false;
|
|
705
|
+
const hasOnlyNonUiSignals = NON_UI_KEYWORDS.some((keyword) => normalized.includes(keyword)) && !normalized.includes("frontend");
|
|
706
|
+
return !hasOnlyNonUiSignals;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
// src/tools/run-pipeline.ts
|
|
512
710
|
function extractText(parts) {
|
|
513
711
|
return parts.filter((p) => p.type === "text" && typeof p.text === "string").map((p) => p.text).join(`
|
|
514
712
|
`);
|
|
@@ -519,16 +717,20 @@ function createRunPipelineTool(client) {
|
|
|
519
717
|
args: {
|
|
520
718
|
steps: tool4.schema.array(tool4.schema.object({
|
|
521
719
|
agent: tool4.schema.string(),
|
|
522
|
-
prompt: tool4.schema.string()
|
|
720
|
+
prompt: tool4.schema.string(),
|
|
721
|
+
task_type: tool4.schema.string().optional()
|
|
523
722
|
})),
|
|
524
723
|
initial_context: tool4.schema.string().optional(),
|
|
525
|
-
abort_on_failure: tool4.schema.boolean().optional().default(true)
|
|
724
|
+
abort_on_failure: tool4.schema.boolean().optional().default(true),
|
|
725
|
+
retry_attempts: tool4.schema.number().optional().default(1)
|
|
526
726
|
},
|
|
527
727
|
async execute(args, context) {
|
|
528
728
|
const startTime = Date.now();
|
|
529
729
|
const trace = [];
|
|
530
730
|
let carryContext = args.initial_context ?? "";
|
|
531
731
|
let aborted = false;
|
|
732
|
+
const retryAttempts = typeof args.retry_attempts === "number" ? args.retry_attempts : 1;
|
|
733
|
+
const maxRetries = Math.max(0, Math.floor(retryAttempts));
|
|
532
734
|
let inflightChildId = null;
|
|
533
735
|
const abortHandler = () => {
|
|
534
736
|
if (inflightChildId) {
|
|
@@ -546,6 +748,7 @@ function createRunPipelineTool(client) {
|
|
|
546
748
|
break;
|
|
547
749
|
}
|
|
548
750
|
const stepStart = Date.now();
|
|
751
|
+
const taskType = normalizeTaskType(step.task_type, step.agent);
|
|
549
752
|
const stepInput = carryContext ? `${carryContext}
|
|
550
753
|
|
|
551
754
|
---
|
|
@@ -557,28 +760,36 @@ ${step.prompt}` : step.prompt;
|
|
|
557
760
|
});
|
|
558
761
|
if (createRes.error || !createRes.data?.id) {
|
|
559
762
|
const errMsg = `Failed to create session: ${createRes.error?.detail ?? "unknown"}`;
|
|
560
|
-
trace.push({ agent: step.agent, input: stepInput, output: errMsg, duration_ms: Date.now() - stepStart, success: false });
|
|
763
|
+
trace.push({ agent: step.agent, task_type: taskType, model: "", input: stepInput, output: errMsg, duration_ms: Date.now() - stepStart, success: false });
|
|
561
764
|
aborted = true;
|
|
562
765
|
break;
|
|
563
766
|
}
|
|
564
767
|
inflightChildId = createRes.data.id;
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
768
|
+
let promptRes = null;
|
|
769
|
+
let retriesUsed = 0;
|
|
770
|
+
for (let attempt = 0;attempt <= maxRetries; attempt++) {
|
|
771
|
+
promptRes = await client.session.prompt({
|
|
772
|
+
path: { id: inflightChildId },
|
|
773
|
+
body: {
|
|
774
|
+
agent: step.agent,
|
|
775
|
+
parts: [{ type: "text", text: stepInput }],
|
|
776
|
+
tools: { question: false }
|
|
777
|
+
},
|
|
778
|
+
query: { directory: context.directory }
|
|
779
|
+
});
|
|
780
|
+
if (!shouldRetry(promptRes) || attempt === maxRetries)
|
|
781
|
+
break;
|
|
782
|
+
retriesUsed++;
|
|
783
|
+
}
|
|
574
784
|
inflightChildId = null;
|
|
575
785
|
if (context.abort.aborted) {
|
|
576
786
|
aborted = true;
|
|
577
787
|
break;
|
|
578
788
|
}
|
|
579
|
-
if (promptRes.error) {
|
|
580
|
-
const errMsg = `Prompt failed: ${promptRes
|
|
581
|
-
trace.push({ agent: step.agent, session_id: createRes.data.id, input: stepInput, output: errMsg
|
|
789
|
+
if (!promptRes || promptRes.error) {
|
|
790
|
+
const errMsg = `Prompt failed: ${promptRes?.error?.detail ?? "unknown"}`;
|
|
791
|
+
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 });
|
|
792
|
+
recordRun(context.directory, step.agent, "", taskType, false, Date.now() - stepStart);
|
|
582
793
|
if (args.abort_on_failure) {
|
|
583
794
|
aborted = true;
|
|
584
795
|
break;
|
|
@@ -588,7 +799,8 @@ ${step.prompt}` : step.prompt;
|
|
|
588
799
|
const info = promptRes.data?.info;
|
|
589
800
|
if (info?.error) {
|
|
590
801
|
const errMsg = `Agent error: ${JSON.stringify(info.error)}`;
|
|
591
|
-
trace.push({ agent: step.agent, session_id: createRes.data.id, input: stepInput, output: errMsg
|
|
802
|
+
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 });
|
|
803
|
+
recordRun(context.directory, step.agent, "", taskType, false, Date.now() - stepStart);
|
|
592
804
|
if (args.abort_on_failure) {
|
|
593
805
|
aborted = true;
|
|
594
806
|
break;
|
|
@@ -596,7 +808,8 @@ ${step.prompt}` : step.prompt;
|
|
|
596
808
|
continue;
|
|
597
809
|
}
|
|
598
810
|
const output = extractText(promptRes.data?.parts ?? []);
|
|
599
|
-
trace.push({ agent: step.agent, session_id: createRes.data.id, input: stepInput, output: output || "(no text output)", duration_ms: Date.now() - stepStart, success: true });
|
|
811
|
+
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 });
|
|
812
|
+
recordRun(context.directory, step.agent, "", taskType, true, Date.now() - stepStart);
|
|
600
813
|
carryContext = output;
|
|
601
814
|
}
|
|
602
815
|
} finally {
|
|
@@ -623,10 +836,15 @@ function createDelegateTool(client) {
|
|
|
623
836
|
args: {
|
|
624
837
|
agent: tool5.schema.string(),
|
|
625
838
|
prompt: tool5.schema.string(),
|
|
626
|
-
context: tool5.schema.string().optional()
|
|
839
|
+
context: tool5.schema.string().optional(),
|
|
840
|
+
task_type: tool5.schema.string().optional(),
|
|
841
|
+
retry_attempts: tool5.schema.number().optional().default(1)
|
|
627
842
|
},
|
|
628
843
|
async execute(args, context) {
|
|
629
844
|
const startTime = Date.now();
|
|
845
|
+
const taskType = normalizeTaskType(args.task_type, args.agent);
|
|
846
|
+
const retryAttempts = typeof args.retry_attempts === "number" ? args.retry_attempts : 1;
|
|
847
|
+
const maxRetries = Math.max(0, Math.floor(retryAttempts));
|
|
630
848
|
const createRes = await client.session.create({
|
|
631
849
|
body: { parentID: context.sessionID, title: `${args.agent}-delegate` },
|
|
632
850
|
query: { directory: context.directory }
|
|
@@ -651,40 +869,59 @@ function createDelegateTool(client) {
|
|
|
651
869
|
---
|
|
652
870
|
|
|
653
871
|
${args.prompt}` : args.prompt;
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
872
|
+
let promptRes = null;
|
|
873
|
+
let retriesUsed = 0;
|
|
874
|
+
for (let attempt = 0;attempt <= maxRetries; attempt++) {
|
|
875
|
+
promptRes = await client.session.prompt({
|
|
876
|
+
path: { id: childId },
|
|
877
|
+
body: {
|
|
878
|
+
agent: args.agent,
|
|
879
|
+
parts: [{ type: "text", text: fullPrompt }],
|
|
880
|
+
tools: { question: false }
|
|
881
|
+
},
|
|
882
|
+
query: { directory: context.directory }
|
|
883
|
+
});
|
|
884
|
+
if (!shouldRetry(promptRes) || attempt === maxRetries)
|
|
885
|
+
break;
|
|
886
|
+
retriesUsed++;
|
|
887
|
+
}
|
|
888
|
+
if (!promptRes || promptRes.error) {
|
|
889
|
+
recordRun(context.directory, args.agent, "", taskType, false, Date.now() - startTime);
|
|
664
890
|
return JSON.stringify({
|
|
665
891
|
agent: args.agent,
|
|
666
892
|
session_id: childId,
|
|
667
893
|
success: false,
|
|
668
|
-
error: `Prompt failed: ${promptRes
|
|
894
|
+
error: `Prompt failed: ${promptRes?.error?.detail ?? "unknown"}`,
|
|
895
|
+
task_type: taskType,
|
|
896
|
+
model: "",
|
|
897
|
+
retries_used: retriesUsed,
|
|
669
898
|
duration_ms: Date.now() - startTime
|
|
670
899
|
});
|
|
671
900
|
}
|
|
672
901
|
const info = promptRes.data?.info;
|
|
673
902
|
if (info?.error) {
|
|
903
|
+
recordRun(context.directory, args.agent, "", taskType, false, Date.now() - startTime);
|
|
674
904
|
return JSON.stringify({
|
|
675
905
|
agent: args.agent,
|
|
676
906
|
session_id: childId,
|
|
677
907
|
success: false,
|
|
678
908
|
error: `Agent error: ${JSON.stringify(info.error)}`,
|
|
909
|
+
task_type: taskType,
|
|
910
|
+
model: "",
|
|
911
|
+
retries_used: retriesUsed,
|
|
679
912
|
duration_ms: Date.now() - startTime
|
|
680
913
|
});
|
|
681
914
|
}
|
|
682
915
|
const output = extractText2(promptRes.data?.parts ?? []);
|
|
916
|
+
recordRun(context.directory, args.agent, "", taskType, true, Date.now() - startTime);
|
|
683
917
|
return JSON.stringify({
|
|
684
918
|
agent: args.agent,
|
|
685
919
|
session_id: childId,
|
|
686
920
|
success: true,
|
|
687
921
|
output: output || "(no text output)",
|
|
922
|
+
task_type: taskType,
|
|
923
|
+
model: "",
|
|
924
|
+
retries_used: retriesUsed,
|
|
688
925
|
duration_ms: Date.now() - startTime
|
|
689
926
|
});
|
|
690
927
|
}
|
|
@@ -693,31 +930,31 @@ ${args.prompt}` : args.prompt;
|
|
|
693
930
|
|
|
694
931
|
// src/tools/repo-memory.ts
|
|
695
932
|
import { tool as tool6 } from "@opencode-ai/plugin";
|
|
696
|
-
import { readFileSync as
|
|
697
|
-
import { join as
|
|
933
|
+
import { readFileSync as readFileSync6, writeFileSync as writeFileSync6, existsSync as existsSync6, mkdirSync as mkdirSync3 } from "fs";
|
|
934
|
+
import { join as join6 } from "path";
|
|
698
935
|
var MEMORY_FILE = "MEMORY.json";
|
|
699
936
|
function memoryPath(directory) {
|
|
700
|
-
return
|
|
937
|
+
return join6(codebaseDir(directory), MEMORY_FILE);
|
|
701
938
|
}
|
|
702
939
|
function emptyMemory() {
|
|
703
940
|
return { version: "1.0", last_updated: new Date().toISOString(), nodes: {} };
|
|
704
941
|
}
|
|
705
942
|
function readMemory(directory) {
|
|
706
943
|
const p = memoryPath(directory);
|
|
707
|
-
if (!
|
|
944
|
+
if (!existsSync6(p))
|
|
708
945
|
return emptyMemory();
|
|
709
946
|
try {
|
|
710
|
-
return JSON.parse(
|
|
947
|
+
return JSON.parse(readFileSync6(p, "utf-8"));
|
|
711
948
|
} catch {
|
|
712
949
|
return emptyMemory();
|
|
713
950
|
}
|
|
714
951
|
}
|
|
715
952
|
function writeMemory(directory, memory) {
|
|
716
953
|
const base = codebaseDir(directory);
|
|
717
|
-
if (!
|
|
718
|
-
|
|
954
|
+
if (!existsSync6(base))
|
|
955
|
+
mkdirSync3(base, { recursive: true });
|
|
719
956
|
memory.last_updated = new Date().toISOString();
|
|
720
|
-
|
|
957
|
+
writeFileSync6(memoryPath(directory), JSON.stringify(memory, null, 2), "utf-8");
|
|
721
958
|
}
|
|
722
959
|
var repoMemoryTool = tool6({
|
|
723
960
|
description: "Repo Memory Graph: read/write/query persistent architecture graph in .codebase/MEMORY.json (modules, dependencies, ownership, bug history, conventions)",
|
|
@@ -794,28 +1031,28 @@ var repoMemoryTool = tool6({
|
|
|
794
1031
|
|
|
795
1032
|
// src/tools/failure-replay.ts
|
|
796
1033
|
import { tool as tool7 } from "@opencode-ai/plugin";
|
|
797
|
-
import { readFileSync as
|
|
798
|
-
import { join as
|
|
1034
|
+
import { readFileSync as readFileSync7, writeFileSync as writeFileSync7, existsSync as existsSync7, mkdirSync as mkdirSync4 } from "fs";
|
|
1035
|
+
import { join as join7 } from "path";
|
|
799
1036
|
var FAILURES_FILE = "FAILURES.json";
|
|
800
1037
|
function failuresPath(directory) {
|
|
801
|
-
return
|
|
1038
|
+
return join7(codebaseDir(directory), FAILURES_FILE);
|
|
802
1039
|
}
|
|
803
1040
|
function readStore(directory) {
|
|
804
1041
|
const p = failuresPath(directory);
|
|
805
|
-
if (!
|
|
1042
|
+
if (!existsSync7(p))
|
|
806
1043
|
return { version: "1.0", last_updated: new Date().toISOString(), entries: [] };
|
|
807
1044
|
try {
|
|
808
|
-
return JSON.parse(
|
|
1045
|
+
return JSON.parse(readFileSync7(p, "utf-8"));
|
|
809
1046
|
} catch {
|
|
810
1047
|
return { version: "1.0", last_updated: new Date().toISOString(), entries: [] };
|
|
811
1048
|
}
|
|
812
1049
|
}
|
|
813
1050
|
function writeStore(directory, store) {
|
|
814
1051
|
const base = codebaseDir(directory);
|
|
815
|
-
if (!
|
|
816
|
-
|
|
1052
|
+
if (!existsSync7(base))
|
|
1053
|
+
mkdirSync4(base, { recursive: true });
|
|
817
1054
|
store.last_updated = new Date().toISOString();
|
|
818
|
-
|
|
1055
|
+
writeFileSync7(failuresPath(directory), JSON.stringify(store, null, 2), "utf-8");
|
|
819
1056
|
}
|
|
820
1057
|
var failureReplayTool = tool7({
|
|
821
1058
|
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",
|
|
@@ -899,17 +1136,17 @@ var failureReplayTool = tool7({
|
|
|
899
1136
|
|
|
900
1137
|
// src/tools/decision-trace.ts
|
|
901
1138
|
import { tool as tool8 } from "@opencode-ai/plugin";
|
|
902
|
-
import { readFileSync as
|
|
903
|
-
import { join as
|
|
1139
|
+
import { readFileSync as readFileSync8, existsSync as existsSync8, mkdirSync as mkdirSync5, appendFileSync } from "fs";
|
|
1140
|
+
import { join as join8 } from "path";
|
|
904
1141
|
var DECISIONS_FILE = "DECISIONS.jsonl";
|
|
905
1142
|
function decisionsPath(directory) {
|
|
906
|
-
return
|
|
1143
|
+
return join8(codebaseDir(directory), DECISIONS_FILE);
|
|
907
1144
|
}
|
|
908
1145
|
function readDecisions(directory) {
|
|
909
1146
|
const p = decisionsPath(directory);
|
|
910
|
-
if (!
|
|
1147
|
+
if (!existsSync8(p))
|
|
911
1148
|
return [];
|
|
912
|
-
return
|
|
1149
|
+
return readFileSync8(p, "utf-8").split(`
|
|
913
1150
|
`).filter((l) => l.trim()).map((l) => {
|
|
914
1151
|
try {
|
|
915
1152
|
return JSON.parse(l);
|
|
@@ -949,8 +1186,8 @@ var decisionTraceTool = tool8({
|
|
|
949
1186
|
case "record": {
|
|
950
1187
|
if (!args.entry)
|
|
951
1188
|
return JSON.stringify({ error: "entry required" });
|
|
952
|
-
if (!
|
|
953
|
-
|
|
1189
|
+
if (!existsSync8(base))
|
|
1190
|
+
mkdirSync5(base, { recursive: true });
|
|
954
1191
|
const entry = { ...args.entry, timestamp: new Date().toISOString() };
|
|
955
1192
|
appendFileSync(decisionsPath(dir), JSON.stringify(entry) + `
|
|
956
1193
|
`, "utf-8");
|
|
@@ -984,28 +1221,28 @@ var decisionTraceTool = tool8({
|
|
|
984
1221
|
|
|
985
1222
|
// src/tools/volatility-map.ts
|
|
986
1223
|
import { tool as tool9 } from "@opencode-ai/plugin";
|
|
987
|
-
import { readFileSync as
|
|
988
|
-
import { join as
|
|
1224
|
+
import { readFileSync as readFileSync9, writeFileSync as writeFileSync9, existsSync as existsSync9, mkdirSync as mkdirSync6 } from "fs";
|
|
1225
|
+
import { join as join9 } from "path";
|
|
989
1226
|
var VOLATILITY_FILE = "VOLATILITY.json";
|
|
990
1227
|
function volatilityPath(directory) {
|
|
991
|
-
return
|
|
1228
|
+
return join9(codebaseDir(directory), VOLATILITY_FILE);
|
|
992
1229
|
}
|
|
993
1230
|
function readStore2(directory) {
|
|
994
1231
|
const p = volatilityPath(directory);
|
|
995
|
-
if (!
|
|
1232
|
+
if (!existsSync9(p))
|
|
996
1233
|
return { version: "1.0", last_updated: new Date().toISOString(), generated_at: new Date().toISOString(), entries: [] };
|
|
997
1234
|
try {
|
|
998
|
-
return JSON.parse(
|
|
1235
|
+
return JSON.parse(readFileSync9(p, "utf-8"));
|
|
999
1236
|
} catch {
|
|
1000
1237
|
return { version: "1.0", last_updated: new Date().toISOString(), generated_at: new Date().toISOString(), entries: [] };
|
|
1001
1238
|
}
|
|
1002
1239
|
}
|
|
1003
1240
|
function writeStore2(directory, store) {
|
|
1004
1241
|
const base = codebaseDir(directory);
|
|
1005
|
-
if (!
|
|
1006
|
-
|
|
1242
|
+
if (!existsSync9(base))
|
|
1243
|
+
mkdirSync6(base, { recursive: true });
|
|
1007
1244
|
store.last_updated = new Date().toISOString();
|
|
1008
|
-
|
|
1245
|
+
writeFileSync9(volatilityPath(directory), JSON.stringify(store, null, 2), "utf-8");
|
|
1009
1246
|
}
|
|
1010
1247
|
function stabilityLabel(churn, hotfixes, todos) {
|
|
1011
1248
|
const score = churn + hotfixes * 10 + todos * 2;
|
|
@@ -1092,28 +1329,28 @@ var volatilityMapTool = tool9({
|
|
|
1092
1329
|
|
|
1093
1330
|
// src/tools/policy-engine.ts
|
|
1094
1331
|
import { tool as tool10 } from "@opencode-ai/plugin";
|
|
1095
|
-
import { readFileSync as
|
|
1096
|
-
import { join as
|
|
1332
|
+
import { readFileSync as readFileSync10, writeFileSync as writeFileSync10, existsSync as existsSync10, mkdirSync as mkdirSync7 } from "fs";
|
|
1333
|
+
import { join as join10 } from "path";
|
|
1097
1334
|
var POLICIES_FILE = "POLICIES.json";
|
|
1098
1335
|
function policiesPath(directory) {
|
|
1099
|
-
return
|
|
1336
|
+
return join10(codebaseDir(directory), POLICIES_FILE);
|
|
1100
1337
|
}
|
|
1101
1338
|
function readStore3(directory) {
|
|
1102
1339
|
const p = policiesPath(directory);
|
|
1103
|
-
if (!
|
|
1340
|
+
if (!existsSync10(p))
|
|
1104
1341
|
return { version: "1.0", last_updated: new Date().toISOString(), policies: [] };
|
|
1105
1342
|
try {
|
|
1106
|
-
return JSON.parse(
|
|
1343
|
+
return JSON.parse(readFileSync10(p, "utf-8"));
|
|
1107
1344
|
} catch {
|
|
1108
1345
|
return { version: "1.0", last_updated: new Date().toISOString(), policies: [] };
|
|
1109
1346
|
}
|
|
1110
1347
|
}
|
|
1111
1348
|
function writeStore3(directory, store) {
|
|
1112
1349
|
const base = codebaseDir(directory);
|
|
1113
|
-
if (!
|
|
1114
|
-
|
|
1350
|
+
if (!existsSync10(base))
|
|
1351
|
+
mkdirSync7(base, { recursive: true });
|
|
1115
1352
|
store.last_updated = new Date().toISOString();
|
|
1116
|
-
|
|
1353
|
+
writeFileSync10(policiesPath(directory), JSON.stringify(store, null, 2), "utf-8");
|
|
1117
1354
|
}
|
|
1118
1355
|
var policyEngineTool = tool10({
|
|
1119
1356
|
description: "Self-Healing Policy Engine: manage .codebase/POLICIES.json — add, list, query, toggle, and record violations of editing policies learned from past failures",
|
|
@@ -1195,7 +1432,7 @@ var policyEngineTool = tool10({
|
|
|
1195
1432
|
|
|
1196
1433
|
// src/tools/hash-edit.ts
|
|
1197
1434
|
import { tool as tool11 } from "@opencode-ai/plugin";
|
|
1198
|
-
import { readFileSync as
|
|
1435
|
+
import { readFileSync as readFileSync11, writeFileSync as writeFileSync11 } from "fs";
|
|
1199
1436
|
import { createHash } from "crypto";
|
|
1200
1437
|
var hashEditTool = tool11({
|
|
1201
1438
|
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.",
|
|
@@ -1209,7 +1446,7 @@ var hashEditTool = tool11({
|
|
|
1209
1446
|
const fullPath = args.filePath.startsWith("/") ? args.filePath : `${context.directory}/${args.filePath}`;
|
|
1210
1447
|
let content;
|
|
1211
1448
|
try {
|
|
1212
|
-
content =
|
|
1449
|
+
content = readFileSync11(fullPath, "utf-8");
|
|
1213
1450
|
} catch (e) {
|
|
1214
1451
|
return `Error: Could not read file ${args.filePath}`;
|
|
1215
1452
|
}
|
|
@@ -1223,13 +1460,15 @@ var hashEditTool = tool11({
|
|
|
1223
1460
|
}
|
|
1224
1461
|
}
|
|
1225
1462
|
const newContent = content.replace(args.targetContent, args.replacementContent);
|
|
1226
|
-
|
|
1463
|
+
writeFileSync11(fullPath, newContent, "utf-8");
|
|
1227
1464
|
return `Successfully updated ${args.filePath} using hash-anchored edit.`;
|
|
1228
1465
|
}
|
|
1229
1466
|
});
|
|
1230
1467
|
|
|
1231
1468
|
// src/tools/council.ts
|
|
1232
1469
|
import { tool as tool12 } from "@opencode-ai/plugin";
|
|
1470
|
+
import { appendFileSync as appendFileSync2, existsSync as existsSync11, mkdirSync as mkdirSync8 } from "fs";
|
|
1471
|
+
import { join as join11 } from "path";
|
|
1233
1472
|
function createCouncilTool(client) {
|
|
1234
1473
|
return tool12({
|
|
1235
1474
|
description: "Run an ensemble of agents (Council) on the same task to reach consensus or compare approaches. Runs 3 specialized agents in parallel and returns their synthesized outputs.",
|
|
@@ -1238,7 +1477,7 @@ function createCouncilTool(client) {
|
|
|
1238
1477
|
agents: tool12.schema.array(tool12.schema.string()).optional()
|
|
1239
1478
|
},
|
|
1240
1479
|
async execute(args, context) {
|
|
1241
|
-
const agents = args.agents || ["architect", "reviewer", "coder"];
|
|
1480
|
+
const agents = args.agents || ["architect", "reviewer", "backend-coder"];
|
|
1242
1481
|
const tasks = agents.map((agent) => ({
|
|
1243
1482
|
agent,
|
|
1244
1483
|
prompt: `TASK: ${args.task}
|
|
@@ -1284,16 +1523,34 @@ Please synthesize these results. Identify areas of agreement, resolve conflicts,
|
|
|
1284
1523
|
},
|
|
1285
1524
|
query: { directory: context.directory }
|
|
1286
1525
|
});
|
|
1287
|
-
|
|
1526
|
+
const synthesis = (finalRes.data?.parts ?? []).filter((p) => p.type === "text").map((p) => p.text).join(`
|
|
1288
1527
|
`);
|
|
1528
|
+
persistCouncilResult(context.directory, {
|
|
1529
|
+
task: args.task,
|
|
1530
|
+
agents,
|
|
1531
|
+
results,
|
|
1532
|
+
synthesis,
|
|
1533
|
+
created_at: new Date().toISOString()
|
|
1534
|
+
});
|
|
1535
|
+
return synthesis;
|
|
1289
1536
|
}
|
|
1290
1537
|
});
|
|
1291
1538
|
}
|
|
1539
|
+
function persistCouncilResult(directory, payload) {
|
|
1540
|
+
try {
|
|
1541
|
+
const base = codebaseDir(directory);
|
|
1542
|
+
if (!existsSync11(base))
|
|
1543
|
+
mkdirSync8(base, { recursive: true });
|
|
1544
|
+
const path = join11(base, "COUNCILS.jsonl");
|
|
1545
|
+
appendFileSync2(path, JSON.stringify(payload) + `
|
|
1546
|
+
`, "utf-8");
|
|
1547
|
+
} catch {}
|
|
1548
|
+
}
|
|
1292
1549
|
|
|
1293
1550
|
// src/tools/context-generator.ts
|
|
1294
1551
|
import { tool as tool13 } from "@opencode-ai/plugin";
|
|
1295
|
-
import { writeFileSync as
|
|
1296
|
-
import { join as
|
|
1552
|
+
import { writeFileSync as writeFileSync12, existsSync as existsSync12, readFileSync as readFileSync12, readdirSync as readdirSync2, statSync } from "fs";
|
|
1553
|
+
import { join as join12 } from "path";
|
|
1297
1554
|
var contextGeneratorTool = tool13({
|
|
1298
1555
|
description: "Auto-generate or update hierarchical context files (AGENTS.md, CLAUDE.md) throughout the project. These files provide critical grounding for AI agents.",
|
|
1299
1556
|
args: {
|
|
@@ -1302,20 +1559,20 @@ var contextGeneratorTool = tool13({
|
|
|
1302
1559
|
},
|
|
1303
1560
|
async execute(args, context) {
|
|
1304
1561
|
const root = context.directory;
|
|
1305
|
-
const target = args.targetDir ?
|
|
1306
|
-
if (!
|
|
1562
|
+
const target = args.targetDir ? join12(root, args.targetDir) : root;
|
|
1563
|
+
if (!existsSync12(target)) {
|
|
1307
1564
|
return `Error: Directory ${target} does not exist.`;
|
|
1308
1565
|
}
|
|
1309
|
-
const agentsMdPath =
|
|
1310
|
-
if (
|
|
1566
|
+
const agentsMdPath = join12(target, "AGENTS.md");
|
|
1567
|
+
if (existsSync12(agentsMdPath) && !args.force) {
|
|
1311
1568
|
return `AGENTS.md already exists in ${target}. Use force: true to overwrite.`;
|
|
1312
1569
|
}
|
|
1313
|
-
const pkgPath =
|
|
1570
|
+
const pkgPath = join12(root, "package.json");
|
|
1314
1571
|
let projectName = "Project";
|
|
1315
1572
|
let techStack = "Unknown";
|
|
1316
|
-
if (
|
|
1573
|
+
if (existsSync12(pkgPath)) {
|
|
1317
1574
|
try {
|
|
1318
|
-
const pkg = JSON.parse(
|
|
1575
|
+
const pkg = JSON.parse(readFileSync12(pkgPath, "utf-8"));
|
|
1319
1576
|
projectName = pkg.name || projectName;
|
|
1320
1577
|
techStack = Object.keys(pkg.dependencies || {}).slice(0, 5).join(", ");
|
|
1321
1578
|
} catch {}
|
|
@@ -1333,7 +1590,7 @@ var contextGeneratorTool = tool13({
|
|
|
1333
1590
|
|
|
1334
1591
|
## Directory Map
|
|
1335
1592
|
${readdirSync2(target).slice(0, 10).map((f) => {
|
|
1336
|
-
const s = statSync(
|
|
1593
|
+
const s = statSync(join12(target, f));
|
|
1337
1594
|
return `- \`${f}\`${s.isDirectory() ? "/" : ""} : [Description]`;
|
|
1338
1595
|
}).join(`
|
|
1339
1596
|
`)}
|
|
@@ -1341,17 +1598,17 @@ ${readdirSync2(target).slice(0, 10).map((f) => {
|
|
|
1341
1598
|
---
|
|
1342
1599
|
Generated by FlowDeck Context Generator.
|
|
1343
1600
|
`;
|
|
1344
|
-
|
|
1601
|
+
writeFileSync12(agentsMdPath, content, "utf-8");
|
|
1345
1602
|
return `Successfully generated AGENTS.md in ${target}.`;
|
|
1346
1603
|
}
|
|
1347
1604
|
});
|
|
1348
1605
|
|
|
1349
1606
|
// src/tools/create-skill.ts
|
|
1350
1607
|
import { tool as tool14 } from "@opencode-ai/plugin";
|
|
1351
|
-
import { mkdirSync as
|
|
1352
|
-
import { join as
|
|
1608
|
+
import { mkdirSync as mkdirSync9, writeFileSync as writeFileSync13, existsSync as existsSync13 } from "fs";
|
|
1609
|
+
import { join as join13, dirname as dirname3 } from "path";
|
|
1353
1610
|
import { fileURLToPath } from "url";
|
|
1354
|
-
var SKILLS_DIR =
|
|
1611
|
+
var SKILLS_DIR = join13(dirname3(fileURLToPath(import.meta.url)), "..", "skills");
|
|
1355
1612
|
var createSkillTool = tool14({
|
|
1356
1613
|
description: "Create a new reusable skill in the FlowDeck skill library (src/skills/). " + "Use this when you discover a repeatable pattern, solve a novel problem with human guidance, " + "or want to capture domain knowledge for future sessions.",
|
|
1357
1614
|
args: {
|
|
@@ -1361,9 +1618,9 @@ var createSkillTool = tool14({
|
|
|
1361
1618
|
tags: tool14.schema.array(tool14.schema.string()).optional().describe("Optional tags for categorisation, e.g. ['performance', 'typescript']")
|
|
1362
1619
|
},
|
|
1363
1620
|
async execute(args) {
|
|
1364
|
-
const skillDir =
|
|
1365
|
-
const skillFile =
|
|
1366
|
-
if (
|
|
1621
|
+
const skillDir = join13(SKILLS_DIR, args.name);
|
|
1622
|
+
const skillFile = join13(skillDir, "SKILL.md");
|
|
1623
|
+
if (existsSync13(skillFile)) {
|
|
1367
1624
|
return `Skill '${args.name}' already exists at ${skillFile}.
|
|
1368
1625
|
` + `Use a different name or delete the existing skill directory first.`;
|
|
1369
1626
|
}
|
|
@@ -1378,8 +1635,8 @@ origin: FlowDeck (self-learned)${tagLine}
|
|
|
1378
1635
|
`;
|
|
1379
1636
|
const fullContent = frontmatter + args.content.trimStart();
|
|
1380
1637
|
try {
|
|
1381
|
-
|
|
1382
|
-
|
|
1638
|
+
mkdirSync9(skillDir, { recursive: true });
|
|
1639
|
+
writeFileSync13(skillFile, fullContent, "utf-8");
|
|
1383
1640
|
return `✓ Skill '${args.name}' created at ${skillFile}
|
|
1384
1641
|
|
|
1385
1642
|
` + `The skill is now part of the FlowDeck library. Restart OpenCode to load it into the active session.`;
|
|
@@ -1391,8 +1648,8 @@ origin: FlowDeck (self-learned)${tagLine}
|
|
|
1391
1648
|
|
|
1392
1649
|
// src/tools/reflect.ts
|
|
1393
1650
|
import { tool as tool15 } from "@opencode-ai/plugin";
|
|
1394
|
-
import { existsSync as
|
|
1395
|
-
import { join as
|
|
1651
|
+
import { existsSync as existsSync14, readFileSync as readFileSync13 } from "fs";
|
|
1652
|
+
import { join as join14 } from "path";
|
|
1396
1653
|
var MAX_ARTIFACT_BYTES = 4000;
|
|
1397
1654
|
function tail(text, maxBytes) {
|
|
1398
1655
|
if (text.length <= maxBytes)
|
|
@@ -1421,11 +1678,11 @@ var reflectTool = tool15({
|
|
|
1421
1678
|
];
|
|
1422
1679
|
let found = 0;
|
|
1423
1680
|
for (const [rel, label] of ARTIFACT_PATHS) {
|
|
1424
|
-
const full =
|
|
1425
|
-
if (!
|
|
1681
|
+
const full = join14(root, rel);
|
|
1682
|
+
if (!existsSync14(full))
|
|
1426
1683
|
continue;
|
|
1427
1684
|
try {
|
|
1428
|
-
const raw =
|
|
1685
|
+
const raw = readFileSync13(full, "utf-8").trim();
|
|
1429
1686
|
if (!raw)
|
|
1430
1687
|
continue;
|
|
1431
1688
|
const count = raw.split(`
|
|
@@ -1449,14 +1706,14 @@ import { tool as tool16 } from "@opencode-ai/plugin";
|
|
|
1449
1706
|
|
|
1450
1707
|
// src/services/memory-store.ts
|
|
1451
1708
|
import { Database } from "bun:sqlite";
|
|
1452
|
-
import { existsSync as
|
|
1453
|
-
import { join as
|
|
1709
|
+
import { existsSync as existsSync15, mkdirSync as mkdirSync10 } from "fs";
|
|
1710
|
+
import { join as join15 } from "path";
|
|
1454
1711
|
import { homedir } from "os";
|
|
1455
|
-
var MEMORY_DIR =
|
|
1456
|
-
var DB_PATH =
|
|
1712
|
+
var MEMORY_DIR = join15(homedir(), ".flowdeck-memory");
|
|
1713
|
+
var DB_PATH = join15(MEMORY_DIR, "memory.db");
|
|
1457
1714
|
function ensureDir() {
|
|
1458
|
-
if (!
|
|
1459
|
-
|
|
1715
|
+
if (!existsSync15(MEMORY_DIR)) {
|
|
1716
|
+
mkdirSync10(MEMORY_DIR, { recursive: true });
|
|
1460
1717
|
}
|
|
1461
1718
|
}
|
|
1462
1719
|
var db = null;
|
|
@@ -1734,16 +1991,16 @@ var memorySearchTool = tool16({
|
|
|
1734
1991
|
// src/tools/memory-status.ts
|
|
1735
1992
|
import { tool as tool17 } from "@opencode-ai/plugin";
|
|
1736
1993
|
import { Database as Database2 } from "bun:sqlite";
|
|
1737
|
-
import { existsSync as
|
|
1738
|
-
import { join as
|
|
1994
|
+
import { existsSync as existsSync16 } from "fs";
|
|
1995
|
+
import { join as join16 } from "path";
|
|
1739
1996
|
import { homedir as homedir2 } from "os";
|
|
1740
|
-
var DB_PATH2 =
|
|
1997
|
+
var DB_PATH2 = join16(homedir2(), ".flowdeck-memory", "memory.db");
|
|
1741
1998
|
var memoryStatusTool = tool17({
|
|
1742
1999
|
description: "Check FlowDeck memory database status, statistics, and recent sessions",
|
|
1743
2000
|
args: {},
|
|
1744
2001
|
async execute(_args, _context) {
|
|
1745
2002
|
try {
|
|
1746
|
-
const exists =
|
|
2003
|
+
const exists = existsSync16(DB_PATH2);
|
|
1747
2004
|
const result = {
|
|
1748
2005
|
database_exists: exists,
|
|
1749
2006
|
path: DB_PATH2,
|
|
@@ -1900,15 +2157,58 @@ var memoryHook = {
|
|
|
1900
2157
|
};
|
|
1901
2158
|
|
|
1902
2159
|
// src/hooks/guard-rails.ts
|
|
1903
|
-
import { existsSync as
|
|
1904
|
-
import { join as
|
|
2160
|
+
import { existsSync as existsSync18, readFileSync as readFileSync15 } from "fs";
|
|
2161
|
+
import { join as join18 } from "path";
|
|
2162
|
+
|
|
2163
|
+
// src/config/loader.ts
|
|
2164
|
+
import { existsSync as existsSync17, readFileSync as readFileSync14 } from "fs";
|
|
2165
|
+
import { join as join17 } from "path";
|
|
2166
|
+
import { homedir as homedir3 } from "os";
|
|
2167
|
+
var CONFIG_FILENAME = "flowdeck.json";
|
|
2168
|
+
function getGlobalConfigDir() {
|
|
2169
|
+
return process.env.OPENCODE_CONFIG_DIR || (process.env.XDG_CONFIG_HOME ? join17(process.env.XDG_CONFIG_HOME, "opencode") : join17(homedir3(), ".config", "opencode"));
|
|
2170
|
+
}
|
|
2171
|
+
function loadFlowDeckConfig(directory) {
|
|
2172
|
+
const candidates = [];
|
|
2173
|
+
if (directory) {
|
|
2174
|
+
candidates.push(join17(directory, ".opencode", CONFIG_FILENAME));
|
|
2175
|
+
}
|
|
2176
|
+
candidates.push(join17(getGlobalConfigDir(), CONFIG_FILENAME));
|
|
2177
|
+
for (const configPath of candidates) {
|
|
2178
|
+
if (existsSync17(configPath)) {
|
|
2179
|
+
try {
|
|
2180
|
+
const content = readFileSync14(configPath, "utf-8");
|
|
2181
|
+
return JSON.parse(content);
|
|
2182
|
+
} catch {
|
|
2183
|
+
console.warn(`[flowdeck] Failed to load config from ${configPath}`);
|
|
2184
|
+
}
|
|
2185
|
+
}
|
|
2186
|
+
}
|
|
2187
|
+
return {};
|
|
2188
|
+
}
|
|
2189
|
+
function resolveDesignFirstConfig(config) {
|
|
2190
|
+
return {
|
|
2191
|
+
enabled: config.designFirst?.enabled ?? true,
|
|
2192
|
+
enforcement: config.designFirst?.enforcement ?? "strict",
|
|
2193
|
+
requireApprovalBeforeImplementation: config.designFirst?.requireApprovalBeforeImplementation ?? true,
|
|
2194
|
+
modelOverrides: config.designFirst?.modelOverrides ?? {},
|
|
2195
|
+
defaultSkillsByTaskType: config.designFirst?.defaultSkillsByTaskType ?? {
|
|
2196
|
+
"landing-page": ["landing-page-design", "wireframe-planning", "design-system-definition", "frontend-handoff"],
|
|
2197
|
+
dashboard: ["dashboard-design", "ui-ux-planning", "wireframe-planning", "responsive-review"],
|
|
2198
|
+
"admin-panel": ["ui-ux-planning", "wireframe-planning", "design-system-definition", "frontend-handoff"],
|
|
2199
|
+
"app-screen": ["app-shell-design", "ui-ux-planning", "wireframe-planning", "responsive-review"],
|
|
2200
|
+
"general-ui": ["ui-ux-planning", "wireframe-planning", "design-system-definition", "frontend-handoff"]
|
|
2201
|
+
}
|
|
2202
|
+
};
|
|
2203
|
+
}
|
|
2204
|
+
// src/hooks/guard-rails.ts
|
|
1905
2205
|
var PLANNING_DIR2 = ".planning";
|
|
1906
2206
|
var CONFIG_FILE = "config.json";
|
|
1907
2207
|
var STATE_FILE2 = "STATE.md";
|
|
1908
2208
|
function resolveExecutionMode(configPath, trustScore, volatility) {
|
|
1909
|
-
if (
|
|
2209
|
+
if (existsSync18(configPath)) {
|
|
1910
2210
|
try {
|
|
1911
|
-
const config = JSON.parse(
|
|
2211
|
+
const config = JSON.parse(readFileSync15(configPath, "utf-8"));
|
|
1912
2212
|
if (config.execution_mode === "review-only")
|
|
1913
2213
|
return "review-only";
|
|
1914
2214
|
if (config.execution_mode === "guarded")
|
|
@@ -1962,22 +2262,22 @@ async function guardRailsHook(ctx, input, _output) {
|
|
|
1962
2262
|
if (!ENABLED)
|
|
1963
2263
|
return;
|
|
1964
2264
|
const dir = ctx.directory;
|
|
1965
|
-
const planningDirPath =
|
|
2265
|
+
const planningDirPath = join18(dir, PLANNING_DIR2);
|
|
1966
2266
|
const codebaseDirectory = codebaseDir(dir);
|
|
1967
|
-
const configPath =
|
|
1968
|
-
const statePath2 =
|
|
2267
|
+
const configPath = join18(planningDirPath, CONFIG_FILE);
|
|
2268
|
+
const statePath2 = join18(planningDirPath, STATE_FILE2);
|
|
1969
2269
|
const workspaceRoot = findWorkspaceRoot(dir);
|
|
1970
2270
|
if (workspaceRoot && dir !== workspaceRoot) {
|
|
1971
2271
|
const config = getWorkspaceConfig(dir);
|
|
1972
|
-
if (config && config.workspace_mode === "shared" && !
|
|
2272
|
+
if (config && config.workspace_mode === "shared" && !existsSync18(planningDirPath)) {
|
|
1973
2273
|
const msg = `No .planning/ in this sub-repo. Switch to workspace root: cd ${workspaceRoot}`;
|
|
1974
2274
|
throw new Error(`[flowdeck] BLOCK: ${msg}`);
|
|
1975
2275
|
}
|
|
1976
2276
|
}
|
|
1977
2277
|
if (input.tool === "write" || input.tool === "edit") {
|
|
1978
|
-
if (!
|
|
2278
|
+
if (!existsSync18(planningDirPath))
|
|
1979
2279
|
return;
|
|
1980
|
-
if (!
|
|
2280
|
+
if (!existsSync18(codebaseDirectory)) {
|
|
1981
2281
|
throw new Error(`[flowdeck] WARNING: .codebase/ not found. Run /map-codebase to map the codebase.`);
|
|
1982
2282
|
}
|
|
1983
2283
|
const execMode = resolveExecutionMode(configPath, null);
|
|
@@ -1987,6 +2287,10 @@ async function guardRailsHook(ctx, input, _output) {
|
|
|
1987
2287
|
if (execMode === "guarded") {
|
|
1988
2288
|
throw new Error(`[flowdeck] GUARDED MODE: edit will proceed but flag for human review.`);
|
|
1989
2289
|
}
|
|
2290
|
+
const designGateMessage = getDesignGateMessage(dir);
|
|
2291
|
+
if (designGateMessage) {
|
|
2292
|
+
throw new Error(designGateMessage);
|
|
2293
|
+
}
|
|
1990
2294
|
const effectiveSeverity = getEffectiveSeverity(configPath, statePath2);
|
|
1991
2295
|
if (effectiveSeverity === null)
|
|
1992
2296
|
return;
|
|
@@ -2009,10 +2313,35 @@ async function guardRailsHook(ctx, input, _output) {
|
|
|
2009
2313
|
}
|
|
2010
2314
|
}
|
|
2011
2315
|
}
|
|
2316
|
+
function getDesignGateMessage(dir) {
|
|
2317
|
+
const designConfig = resolveDesignFirstConfig(loadFlowDeckConfig(dir));
|
|
2318
|
+
if (!designConfig.enabled || !designConfig.requireApprovalBeforeImplementation)
|
|
2319
|
+
return null;
|
|
2320
|
+
const state = readPlanningState(dir);
|
|
2321
|
+
if (state.design_override && state.design_override_reason && state.design_override_reason.trim().length > 0)
|
|
2322
|
+
return null;
|
|
2323
|
+
const designApproved = state.design_stage === "handoff_complete" && state.design_approved;
|
|
2324
|
+
if (state.requires_design_first || state.task_type && isUiHeavyTask(state.task_type) || planSuggestsUiHeavy(dir, state.phase || 1)) {
|
|
2325
|
+
if (designApproved)
|
|
2326
|
+
return null;
|
|
2327
|
+
if (designConfig.enforcement === "advisory") {
|
|
2328
|
+
return "[flowdeck] WARNING: UI-heavy task detected without approved design handoff. Run /fd-design --mode=draft first.";
|
|
2329
|
+
}
|
|
2330
|
+
return "[flowdeck] BLOCK: UI-heavy task requires approved design handoff. Run /fd-design --mode=draft or set explicit design override in STATE.md.";
|
|
2331
|
+
}
|
|
2332
|
+
return null;
|
|
2333
|
+
}
|
|
2334
|
+
function planSuggestsUiHeavy(dir, phase) {
|
|
2335
|
+
const planPath = phasePlanPath(dir, phase);
|
|
2336
|
+
if (!existsSync18(planPath))
|
|
2337
|
+
return false;
|
|
2338
|
+
const planContent = readFileSync15(planPath, "utf-8");
|
|
2339
|
+
return isUiHeavyTask(planContent);
|
|
2340
|
+
}
|
|
2012
2341
|
function effectiveSeverity(configPath, statePath2) {
|
|
2013
|
-
if (
|
|
2342
|
+
if (existsSync18(configPath)) {
|
|
2014
2343
|
try {
|
|
2015
|
-
const configContent =
|
|
2344
|
+
const configContent = readFileSync15(configPath, "utf-8");
|
|
2016
2345
|
const config = JSON.parse(configContent);
|
|
2017
2346
|
if (config.guard_enforcement === "warn")
|
|
2018
2347
|
return "warn";
|
|
@@ -2028,10 +2357,10 @@ function getEffectiveSeverity(configPath, statePath2) {
|
|
|
2028
2357
|
return effectiveSeverity(configPath, statePath2);
|
|
2029
2358
|
}
|
|
2030
2359
|
function getPlanConfirmed(statePath2) {
|
|
2031
|
-
if (!
|
|
2360
|
+
if (!existsSync18(statePath2))
|
|
2032
2361
|
return false;
|
|
2033
2362
|
try {
|
|
2034
|
-
const content =
|
|
2363
|
+
const content = readFileSync15(statePath2, "utf-8");
|
|
2035
2364
|
const match = content.match(/plan_confirmed:\s*(true|false)/i);
|
|
2036
2365
|
return match ? match[1].toLowerCase() === "true" : false;
|
|
2037
2366
|
} catch {
|
|
@@ -2039,21 +2368,21 @@ function getPlanConfirmed(statePath2) {
|
|
|
2039
2368
|
}
|
|
2040
2369
|
}
|
|
2041
2370
|
function getWarningMessage(planningDir2) {
|
|
2042
|
-
if (!
|
|
2371
|
+
if (!existsSync18(join18(planningDir2, STATE_FILE2))) {
|
|
2043
2372
|
return "No .planning/ found. Run /new-project first.";
|
|
2044
2373
|
}
|
|
2045
2374
|
return "Plan not confirmed. Run /plan and confirm to enable execution.";
|
|
2046
2375
|
}
|
|
2047
2376
|
function getBlockMessage(planningDir2) {
|
|
2048
|
-
if (!
|
|
2377
|
+
if (!existsSync18(join18(planningDir2, STATE_FILE2))) {
|
|
2049
2378
|
return "No .planning/ found. Run /new-project first.";
|
|
2050
2379
|
}
|
|
2051
2380
|
return "Plan not confirmed. Run /plan and confirm to enable execution.";
|
|
2052
2381
|
}
|
|
2053
2382
|
|
|
2054
2383
|
// src/hooks/tool-guard.ts
|
|
2055
|
-
import { existsSync as
|
|
2056
|
-
import { join as
|
|
2384
|
+
import { existsSync as existsSync19, readFileSync as readFileSync16 } from "fs";
|
|
2385
|
+
import { join as join19 } from "path";
|
|
2057
2386
|
var IS_ENABLED = () => process.env.FLOWDECK_TOOL_GUARD_ENABLED === "on";
|
|
2058
2387
|
var BLOCKED_PATTERNS = {
|
|
2059
2388
|
read: [".env", ".pem", ".key", ".secret"],
|
|
@@ -2100,11 +2429,11 @@ function isBlocked(tool18, args) {
|
|
|
2100
2429
|
return null;
|
|
2101
2430
|
}
|
|
2102
2431
|
function checkArchConstraint(directory, filePath) {
|
|
2103
|
-
const constraintsPath =
|
|
2104
|
-
if (!
|
|
2432
|
+
const constraintsPath = join19(codebaseDir(directory), "CONSTRAINTS.md");
|
|
2433
|
+
if (!existsSync19(constraintsPath))
|
|
2105
2434
|
return null;
|
|
2106
2435
|
try {
|
|
2107
|
-
const content =
|
|
2436
|
+
const content = readFileSync16(constraintsPath, "utf-8");
|
|
2108
2437
|
const match = content.match(/## Forbidden Paths\n([\s\S]*?)(?:\n##|$)/);
|
|
2109
2438
|
if (!match)
|
|
2110
2439
|
return null;
|
|
@@ -2121,12 +2450,37 @@ function checkArchConstraint(directory, filePath) {
|
|
|
2121
2450
|
function checkPhaseEnforcement(directory) {
|
|
2122
2451
|
try {
|
|
2123
2452
|
const state = readPlanningState(directory);
|
|
2453
|
+
const flowdeckConfig = resolveDesignFirstConfig(loadFlowDeckConfig(directory));
|
|
2124
2454
|
if (state.phase > 0 && state.phase < 3) {
|
|
2125
2455
|
return `FLOWDECK [phase-gate]: writing to codebase is blocked in phase ${state.phase} (${state.phase === 1 ? "discuss" : "plan"}). Run /fd-plan --confirm to enter execute phase.`;
|
|
2126
2456
|
}
|
|
2457
|
+
if (flowdeckConfig.enabled && flowdeckConfig.requireApprovalBeforeImplementation && isUiDesignApprovalRequired(directory)) {
|
|
2458
|
+
if (flowdeckConfig.enforcement === "advisory") {
|
|
2459
|
+
return `FLOWDECK [design-gate]: advisory design-first mode detected missing approval. Run /fd-design --mode=draft or set design_override=true in STATE.md.`;
|
|
2460
|
+
}
|
|
2461
|
+
return `FLOWDECK [design-gate]: UI-heavy task requires approved design handoff before implementation. Run /fd-design --mode=draft and ensure design_stage=handoff_complete + design_approved=true, or set explicit design_override with reason.`;
|
|
2462
|
+
}
|
|
2127
2463
|
} catch {}
|
|
2128
2464
|
return null;
|
|
2129
2465
|
}
|
|
2466
|
+
function isUiDesignApprovalRequired(directory) {
|
|
2467
|
+
const state = readPlanningState(directory);
|
|
2468
|
+
if (state.design_override && state.design_override_reason && state.design_override_reason.trim().length > 0)
|
|
2469
|
+
return false;
|
|
2470
|
+
if (state.requires_design_first) {
|
|
2471
|
+
return !(state.design_stage === "handoff_complete" && state.design_approved);
|
|
2472
|
+
}
|
|
2473
|
+
if (state.task_type && isUiHeavyTask(state.task_type)) {
|
|
2474
|
+
return !(state.design_stage === "handoff_complete" && state.design_approved);
|
|
2475
|
+
}
|
|
2476
|
+
const planPath = phasePlanPath(directory, state.phase || 1);
|
|
2477
|
+
if (!existsSync19(planPath))
|
|
2478
|
+
return false;
|
|
2479
|
+
const planContent = readFileSync16(planPath, "utf-8");
|
|
2480
|
+
if (!isUiHeavyTask(planContent))
|
|
2481
|
+
return false;
|
|
2482
|
+
return !(state.design_stage === "handoff_complete" && state.design_approved);
|
|
2483
|
+
}
|
|
2130
2484
|
async function toolGuardHook(ctx, input, output) {
|
|
2131
2485
|
if (!IS_ENABLED())
|
|
2132
2486
|
return;
|
|
@@ -2151,18 +2505,18 @@ async function toolGuardHook(ctx, input, output) {
|
|
|
2151
2505
|
}
|
|
2152
2506
|
|
|
2153
2507
|
// src/hooks/session-start.ts
|
|
2154
|
-
import { existsSync as
|
|
2508
|
+
import { existsSync as existsSync20, readFileSync as readFileSync17 } from "fs";
|
|
2155
2509
|
async function sessionStartHook(ctx) {
|
|
2156
2510
|
const planningDir2 = ctx.directory + "/.planning";
|
|
2157
2511
|
const codebaseDirectory = codebaseDir(ctx.directory);
|
|
2158
2512
|
const workspaceRoot = findWorkspaceRoot(ctx.directory);
|
|
2159
2513
|
const config = workspaceRoot ? getWorkspaceConfig(ctx.directory) : null;
|
|
2160
|
-
if (!
|
|
2514
|
+
if (!existsSync20(planningDir2)) {
|
|
2161
2515
|
return {
|
|
2162
2516
|
flowdeck_phase: null,
|
|
2163
2517
|
flowdeck_status: "no_plan",
|
|
2164
2518
|
flowdeck_warning: "Run /new-project or /map-codebase to initialize.",
|
|
2165
|
-
flowdeck_has_codebase:
|
|
2519
|
+
flowdeck_has_codebase: existsSync20(codebaseDirectory),
|
|
2166
2520
|
...workspaceRoot && config?.sub_repos ? {
|
|
2167
2521
|
flowdeck_workspace_root: workspaceRoot,
|
|
2168
2522
|
flowdeck_sub_repos: config.sub_repos,
|
|
@@ -2173,7 +2527,7 @@ async function sessionStartHook(ctx) {
|
|
|
2173
2527
|
}
|
|
2174
2528
|
try {
|
|
2175
2529
|
const stateFilePath = statePath(ctx.directory);
|
|
2176
|
-
const content =
|
|
2530
|
+
const content = readFileSync17(stateFilePath, "utf-8");
|
|
2177
2531
|
const state = parseState(content);
|
|
2178
2532
|
const currentPhase = state["current_phase"] || {};
|
|
2179
2533
|
const result = {
|
|
@@ -2181,7 +2535,7 @@ async function sessionStartHook(ctx) {
|
|
|
2181
2535
|
flowdeck_status: currentPhase["status"] ?? null,
|
|
2182
2536
|
flowdeck_steps_pending: currentPhase["steps_pending"] ?? null,
|
|
2183
2537
|
flowdeck_last_action: currentPhase["last_action"] ?? null,
|
|
2184
|
-
flowdeck_has_codebase:
|
|
2538
|
+
flowdeck_has_codebase: existsSync20(codebaseDirectory)
|
|
2185
2539
|
};
|
|
2186
2540
|
if (workspaceRoot && config?.sub_repos && config.sub_repos.length > 0) {
|
|
2187
2541
|
result.flowdeck_workspace_root = workspaceRoot;
|
|
@@ -2196,7 +2550,7 @@ async function sessionStartHook(ctx) {
|
|
|
2196
2550
|
flowdeck_phase: null,
|
|
2197
2551
|
flowdeck_status: "error",
|
|
2198
2552
|
flowdeck_warning: "State file unreadable. Continuing without flowdeck context.",
|
|
2199
|
-
flowdeck_has_codebase:
|
|
2553
|
+
flowdeck_has_codebase: existsSync20(codebaseDirectory)
|
|
2200
2554
|
};
|
|
2201
2555
|
if (workspaceRoot && config?.sub_repos && config.sub_repos.length > 0) {
|
|
2202
2556
|
result.flowdeck_workspace_root = workspaceRoot;
|
|
@@ -2262,8 +2616,8 @@ function notifyPermissionNeeded(tool18) {
|
|
|
2262
2616
|
}
|
|
2263
2617
|
|
|
2264
2618
|
// src/hooks/patch-trust.ts
|
|
2265
|
-
import { existsSync as
|
|
2266
|
-
import { join as
|
|
2619
|
+
import { existsSync as existsSync21, readFileSync as readFileSync18 } from "fs";
|
|
2620
|
+
import { join as join20 } from "path";
|
|
2267
2621
|
var HIGH_RISK_KEYWORDS = [
|
|
2268
2622
|
"password",
|
|
2269
2623
|
"secret",
|
|
@@ -2285,11 +2639,11 @@ var HIGH_RISK_KEYWORDS = [
|
|
|
2285
2639
|
"privilege"
|
|
2286
2640
|
];
|
|
2287
2641
|
function loadVolatility(directory) {
|
|
2288
|
-
const p =
|
|
2289
|
-
if (!
|
|
2642
|
+
const p = join20(codebaseDir(directory), "VOLATILITY.json");
|
|
2643
|
+
if (!existsSync21(p))
|
|
2290
2644
|
return {};
|
|
2291
2645
|
try {
|
|
2292
|
-
const data = JSON.parse(
|
|
2646
|
+
const data = JSON.parse(readFileSync18(p, "utf-8"));
|
|
2293
2647
|
const map = {};
|
|
2294
2648
|
for (const entry of data.entries ?? [])
|
|
2295
2649
|
map[entry.path] = entry.stability;
|
|
@@ -2299,11 +2653,11 @@ function loadVolatility(directory) {
|
|
|
2299
2653
|
}
|
|
2300
2654
|
}
|
|
2301
2655
|
function loadFailedPaths(directory) {
|
|
2302
|
-
const p =
|
|
2303
|
-
if (!
|
|
2656
|
+
const p = join20(codebaseDir(directory), "FAILURES.json");
|
|
2657
|
+
if (!existsSync21(p))
|
|
2304
2658
|
return [];
|
|
2305
2659
|
try {
|
|
2306
|
-
const data = JSON.parse(
|
|
2660
|
+
const data = JSON.parse(readFileSync18(p, "utf-8"));
|
|
2307
2661
|
return (data.entries ?? []).flatMap((e) => e.affected_paths ?? []);
|
|
2308
2662
|
} catch {
|
|
2309
2663
|
return [];
|
|
@@ -2368,8 +2722,8 @@ async function patchTrustHook(ctx, input, output) {
|
|
|
2368
2722
|
}
|
|
2369
2723
|
|
|
2370
2724
|
// src/hooks/decision-trace-hook.ts
|
|
2371
|
-
import { existsSync as
|
|
2372
|
-
import { join as
|
|
2725
|
+
import { existsSync as existsSync22, mkdirSync as mkdirSync11, appendFileSync as appendFileSync3 } from "fs";
|
|
2726
|
+
import { join as join21 } from "path";
|
|
2373
2727
|
async function decisionTraceHook(ctx, input, output) {
|
|
2374
2728
|
if (input.tool !== "write" && input.tool !== "edit")
|
|
2375
2729
|
return;
|
|
@@ -2378,8 +2732,8 @@ async function decisionTraceHook(ctx, input, output) {
|
|
|
2378
2732
|
return;
|
|
2379
2733
|
const base = codebaseDir(ctx.directory);
|
|
2380
2734
|
try {
|
|
2381
|
-
if (!
|
|
2382
|
-
|
|
2735
|
+
if (!existsSync22(base))
|
|
2736
|
+
mkdirSync11(base, { recursive: true });
|
|
2383
2737
|
const entry = {
|
|
2384
2738
|
timestamp: new Date().toISOString(),
|
|
2385
2739
|
file_path: filePath,
|
|
@@ -2391,62 +2745,87 @@ async function decisionTraceHook(ctx, input, output) {
|
|
|
2391
2745
|
risk_level: "unknown",
|
|
2392
2746
|
auto_recorded: true
|
|
2393
2747
|
};
|
|
2394
|
-
|
|
2748
|
+
appendFileSync3(join21(base, "DECISIONS.jsonl"), JSON.stringify(entry) + `
|
|
2395
2749
|
`, "utf-8");
|
|
2396
2750
|
} catch {}
|
|
2397
2751
|
}
|
|
2398
2752
|
|
|
2399
2753
|
// src/services/telemetry.ts
|
|
2400
|
-
import { existsSync as
|
|
2401
|
-
import { join as
|
|
2754
|
+
import { existsSync as existsSync23, readFileSync as readFileSync19, appendFileSync as appendFileSync4, mkdirSync as mkdirSync12 } from "fs";
|
|
2755
|
+
import { join as join22 } from "path";
|
|
2402
2756
|
import { randomUUID } from "crypto";
|
|
2403
2757
|
function telemetryPath(dir) {
|
|
2404
|
-
return
|
|
2758
|
+
return join22(codebaseDir(dir), "TELEMETRY.jsonl");
|
|
2405
2759
|
}
|
|
2406
2760
|
function appendEvent(dir, partial) {
|
|
2407
2761
|
if (process.env.TELEMETRY_ENABLED !== "true")
|
|
2408
2762
|
return null;
|
|
2409
2763
|
const cd = codebaseDir(dir);
|
|
2410
|
-
if (!
|
|
2411
|
-
|
|
2764
|
+
if (!existsSync23(cd))
|
|
2765
|
+
mkdirSync12(cd, { recursive: true });
|
|
2412
2766
|
const event = {
|
|
2413
2767
|
id: randomUUID(),
|
|
2414
2768
|
ts: new Date().toISOString(),
|
|
2415
2769
|
...partial
|
|
2416
2770
|
};
|
|
2417
|
-
|
|
2771
|
+
appendFileSync4(telemetryPath(dir), JSON.stringify(event) + `
|
|
2418
2772
|
`, "utf-8");
|
|
2419
2773
|
return event;
|
|
2420
2774
|
}
|
|
2421
2775
|
|
|
2422
2776
|
// src/hooks/telemetry-hook.ts
|
|
2777
|
+
function resolveIds(toolInput) {
|
|
2778
|
+
const session_id = toolInput.sessionID ?? toolInput.sessionId ?? process.env.OPENCODE_SESSION_ID ?? "session-0";
|
|
2779
|
+
const run_id = toolInput.messageID ?? toolInput.messageId ?? toolInput.runID ?? toolInput.runId ?? process.env.OPENCODE_RUN_ID ?? "run-0";
|
|
2780
|
+
return { session_id, run_id };
|
|
2781
|
+
}
|
|
2782
|
+
function inferStatus(output) {
|
|
2783
|
+
if (output.error)
|
|
2784
|
+
return "error";
|
|
2785
|
+
if (typeof output.output !== "string")
|
|
2786
|
+
return "ok";
|
|
2787
|
+
const text = output.output.trim();
|
|
2788
|
+
if (!text)
|
|
2789
|
+
return "ok";
|
|
2790
|
+
try {
|
|
2791
|
+
const parsed = JSON.parse(text);
|
|
2792
|
+
if (parsed.success === false || parsed.error || parsed.status === "error")
|
|
2793
|
+
return "error";
|
|
2794
|
+
return "ok";
|
|
2795
|
+
} catch {
|
|
2796
|
+
return "ok";
|
|
2797
|
+
}
|
|
2798
|
+
}
|
|
2423
2799
|
async function telemetryHook(context, toolInput, output) {
|
|
2424
2800
|
const dir = context.directory ?? process.cwd();
|
|
2425
2801
|
const tool18 = toolInput.name ?? toolInput.tool ?? "unknown";
|
|
2802
|
+
const ids = resolveIds(toolInput);
|
|
2426
2803
|
appendEvent(dir, {
|
|
2427
|
-
session_id:
|
|
2428
|
-
run_id:
|
|
2804
|
+
session_id: ids.session_id,
|
|
2805
|
+
run_id: ids.run_id,
|
|
2429
2806
|
event: "tool.call",
|
|
2430
2807
|
tool: tool18,
|
|
2431
2808
|
status: "ok",
|
|
2432
2809
|
meta: { parameters: output.args ?? {} }
|
|
2433
2810
|
});
|
|
2434
2811
|
}
|
|
2435
|
-
async function telemetryAfterHook(context, toolInput,
|
|
2812
|
+
async function telemetryAfterHook(context, toolInput, output) {
|
|
2436
2813
|
const dir = context.directory ?? process.cwd();
|
|
2437
2814
|
const tool18 = toolInput.name ?? toolInput.tool ?? "unknown";
|
|
2815
|
+
const ids = resolveIds(toolInput);
|
|
2816
|
+
const status = inferStatus(output);
|
|
2438
2817
|
appendEvent(dir, {
|
|
2439
|
-
session_id:
|
|
2440
|
-
run_id:
|
|
2818
|
+
session_id: ids.session_id,
|
|
2819
|
+
run_id: ids.run_id,
|
|
2441
2820
|
event: "tool.complete",
|
|
2442
2821
|
tool: tool18,
|
|
2443
|
-
status
|
|
2822
|
+
status
|
|
2444
2823
|
});
|
|
2445
2824
|
}
|
|
2446
2825
|
|
|
2447
2826
|
// src/services/approval-manager.ts
|
|
2448
|
-
import { existsSync as
|
|
2449
|
-
import { join as
|
|
2827
|
+
import { existsSync as existsSync24, readFileSync as readFileSync20, writeFileSync as writeFileSync14, mkdirSync as mkdirSync13 } from "fs";
|
|
2828
|
+
import { join as join23 } from "path";
|
|
2450
2829
|
var APPROVAL_TTL_MS = 30 * 60 * 1000;
|
|
2451
2830
|
var SENSITIVE_PATTERNS = [
|
|
2452
2831
|
/auth/i,
|
|
@@ -2483,20 +2862,20 @@ function isSensitivePath(filePath) {
|
|
|
2483
2862
|
return SENSITIVE_PATTERNS.some((p) => p.test(filePath));
|
|
2484
2863
|
}
|
|
2485
2864
|
function approvalsPath(dir) {
|
|
2486
|
-
return
|
|
2865
|
+
return join23(codebaseDir(dir), "APPROVALS.json");
|
|
2487
2866
|
}
|
|
2488
|
-
function
|
|
2867
|
+
function loadStore2(dir) {
|
|
2489
2868
|
const p = approvalsPath(dir);
|
|
2490
|
-
if (!
|
|
2869
|
+
if (!existsSync24(p))
|
|
2491
2870
|
return { requests: [] };
|
|
2492
2871
|
try {
|
|
2493
|
-
return JSON.parse(
|
|
2872
|
+
return JSON.parse(readFileSync20(p, "utf-8"));
|
|
2494
2873
|
} catch {
|
|
2495
2874
|
return { requests: [] };
|
|
2496
2875
|
}
|
|
2497
2876
|
}
|
|
2498
2877
|
function checkApproval(dir, file_path, command) {
|
|
2499
|
-
const store =
|
|
2878
|
+
const store = loadStore2(dir);
|
|
2500
2879
|
const now = Date.now();
|
|
2501
2880
|
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;
|
|
2502
2881
|
}
|
|
@@ -2585,8 +2964,8 @@ function createContextWindowMonitorHook() {
|
|
|
2585
2964
|
}
|
|
2586
2965
|
|
|
2587
2966
|
// src/hooks/shell-env-hook.ts
|
|
2588
|
-
import { existsSync as
|
|
2589
|
-
import { join as
|
|
2967
|
+
import { existsSync as existsSync25, readFileSync as readFileSync21 } from "fs";
|
|
2968
|
+
import { join as join24 } from "path";
|
|
2590
2969
|
import { createRequire } from "module";
|
|
2591
2970
|
var _version;
|
|
2592
2971
|
function getVersion() {
|
|
@@ -2622,7 +3001,7 @@ var MARKER_TO_LANG = {
|
|
|
2622
3001
|
};
|
|
2623
3002
|
function detectPackageManager(root) {
|
|
2624
3003
|
for (const [lockfile, pm] of Object.entries(LOCKFILE_TO_PM)) {
|
|
2625
|
-
if (
|
|
3004
|
+
if (existsSync25(join24(root, lockfile)))
|
|
2626
3005
|
return pm;
|
|
2627
3006
|
}
|
|
2628
3007
|
return;
|
|
@@ -2631,7 +3010,7 @@ function detectLanguages(root) {
|
|
|
2631
3010
|
const langs = [];
|
|
2632
3011
|
const seen = new Set;
|
|
2633
3012
|
for (const [marker, lang] of Object.entries(MARKER_TO_LANG)) {
|
|
2634
|
-
if (!seen.has(lang) &&
|
|
3013
|
+
if (!seen.has(lang) && existsSync25(join24(root, marker))) {
|
|
2635
3014
|
langs.push(lang);
|
|
2636
3015
|
seen.add(lang);
|
|
2637
3016
|
}
|
|
@@ -2639,11 +3018,11 @@ function detectLanguages(root) {
|
|
|
2639
3018
|
return langs;
|
|
2640
3019
|
}
|
|
2641
3020
|
function readCurrentPhase(root) {
|
|
2642
|
-
const statePath2 =
|
|
2643
|
-
if (!
|
|
3021
|
+
const statePath2 = join24(root, ".planning", "STATE.md");
|
|
3022
|
+
if (!existsSync25(statePath2))
|
|
2644
3023
|
return;
|
|
2645
3024
|
try {
|
|
2646
|
-
const content =
|
|
3025
|
+
const content = readFileSync21(statePath2, "utf-8");
|
|
2647
3026
|
const match = content.match(/phase:\s*(\S+)/i);
|
|
2648
3027
|
return match?.[1];
|
|
2649
3028
|
} catch {
|
|
@@ -2737,14 +3116,13 @@ function createSessionIdleHook(client, tracker) {
|
|
|
2737
3116
|
if (edited.length > 10) {
|
|
2738
3117
|
await client.app.log({ body: { service: "flowdeck", level: "info", message: ` … and ${edited.length - 10} more` } }).catch(() => {});
|
|
2739
3118
|
}
|
|
2740
|
-
tracker.clear();
|
|
2741
3119
|
} catch {}
|
|
2742
3120
|
};
|
|
2743
3121
|
}
|
|
2744
3122
|
|
|
2745
3123
|
// src/hooks/compaction-hook.ts
|
|
2746
|
-
import { existsSync as
|
|
2747
|
-
import { join as
|
|
3124
|
+
import { existsSync as existsSync26, readFileSync as readFileSync22 } from "fs";
|
|
3125
|
+
import { join as join25 } from "path";
|
|
2748
3126
|
var STRUCTURED_SUMMARY_PROMPT = `
|
|
2749
3127
|
When summarizing this session, you MUST include the following sections:
|
|
2750
3128
|
|
|
@@ -2783,11 +3161,11 @@ For each: agent name, status, description, session_id.
|
|
|
2783
3161
|
**RESUME, DON'T RESTART.** Use session_id to continue existing sessions.
|
|
2784
3162
|
`;
|
|
2785
3163
|
function readPlanningState2(directory) {
|
|
2786
|
-
const statePath2 =
|
|
2787
|
-
if (!
|
|
3164
|
+
const statePath2 = join25(directory, ".planning", "STATE.md");
|
|
3165
|
+
if (!existsSync26(statePath2))
|
|
2788
3166
|
return null;
|
|
2789
3167
|
try {
|
|
2790
|
-
const content =
|
|
3168
|
+
const content = readFileSync22(statePath2, "utf-8");
|
|
2791
3169
|
return content.slice(0, 1500);
|
|
2792
3170
|
} catch {
|
|
2793
3171
|
return null;
|
|
@@ -2871,7 +3249,9 @@ function blockMessage(toolName) {
|
|
|
2871
3249
|
` + `The orchestrator is a coordinator — it must delegate all implementation work.
|
|
2872
3250
|
|
|
2873
3251
|
` + `Use the \`delegate\` tool to hand this off:
|
|
2874
|
-
` + ` delegate({ agent: "@coder", prompt: "..." }) — code writing / editing
|
|
3252
|
+
` + ` delegate({ agent: "@backend-coder", prompt: "..." }) — backend code writing / editing
|
|
3253
|
+
` + ` delegate({ agent: "@frontend-coder", prompt: "..." }) — frontend code writing / editing
|
|
3254
|
+
` + ` delegate({ agent: "@devops", prompt: "..." }) — CI/CD, deploy, and infra changes
|
|
2875
3255
|
` + ` delegate({ agent: "@mapper", prompt: "..." }) — codebase mapping
|
|
2876
3256
|
` + ` delegate({ agent: "@researcher", prompt: "..." }) — research / file analysis
|
|
2877
3257
|
` + ` delegate({ agent: "@tester", prompt: "..." }) — tests / commands
|
|
@@ -2882,13 +3262,24 @@ function blockMessage(toolName) {
|
|
|
2882
3262
|
class OrchestratorGuard {
|
|
2883
3263
|
primarySessionId = null;
|
|
2884
3264
|
onEvent(event) {
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
const
|
|
2888
|
-
if (
|
|
2889
|
-
this.primarySessionId =
|
|
3265
|
+
const eventType = event.type ?? "";
|
|
3266
|
+
if (eventType === "session.deleted") {
|
|
3267
|
+
const deletedId = extractSessionId(event);
|
|
3268
|
+
if (deletedId && deletedId === this.primarySessionId) {
|
|
3269
|
+
this.primarySessionId = null;
|
|
2890
3270
|
}
|
|
3271
|
+
return;
|
|
2891
3272
|
}
|
|
3273
|
+
if (eventType !== "session.created" && eventType !== "session.started")
|
|
3274
|
+
return;
|
|
3275
|
+
if (this.primarySessionId !== null)
|
|
3276
|
+
return;
|
|
3277
|
+
const id = extractSessionId(event);
|
|
3278
|
+
if (!id)
|
|
3279
|
+
return;
|
|
3280
|
+
if (extractParentSessionId(event))
|
|
3281
|
+
return;
|
|
3282
|
+
this.primarySessionId = id;
|
|
2892
3283
|
}
|
|
2893
3284
|
check(sessionId, toolName) {
|
|
2894
3285
|
if (DISABLED)
|
|
@@ -2904,6 +3295,20 @@ class OrchestratorGuard {
|
|
|
2904
3295
|
}
|
|
2905
3296
|
}
|
|
2906
3297
|
}
|
|
3298
|
+
function extractSessionId(event) {
|
|
3299
|
+
const props = event.properties;
|
|
3300
|
+
const inner = event.event;
|
|
3301
|
+
const info = props?.info;
|
|
3302
|
+
const id = event.sessionID ?? event.sessionId ?? inner?.sessionID ?? inner?.sessionId ?? info?.id;
|
|
3303
|
+
return id ?? null;
|
|
3304
|
+
}
|
|
3305
|
+
function extractParentSessionId(event) {
|
|
3306
|
+
const props = event.properties;
|
|
3307
|
+
const inner = event.event;
|
|
3308
|
+
const info = props?.info;
|
|
3309
|
+
const parentId = inner?.parentID ?? inner?.parentId ?? info?.parentID ?? info?.parentId;
|
|
3310
|
+
return parentId ?? null;
|
|
3311
|
+
}
|
|
2907
3312
|
|
|
2908
3313
|
// src/hooks/auto-learn-hook.ts
|
|
2909
3314
|
var MIN_EDITS = 1;
|
|
@@ -3048,11 +3453,22 @@ For each incomplete step in PLAN.md:
|
|
|
3048
3453
|
5. Re-read STATE.md to confirm state
|
|
3049
3454
|
6. Move to the next incomplete step
|
|
3050
3455
|
|
|
3456
|
+
## Implementation Routing
|
|
3457
|
+
|
|
3458
|
+
When a plan step requires implementation, route to a role-specific agent:
|
|
3459
|
+
- Use @backend-coder for server, API, business logic, database, and non-UI application code.
|
|
3460
|
+
- Use @frontend-coder for UI components, client state, styling, and interaction behavior.
|
|
3461
|
+
- Use @devops for CI/CD workflows, deployment, infrastructure, runtime config, and operations scripts.
|
|
3462
|
+
- If a step mixes multiple domains, split it into multiple delegated tasks by domain.
|
|
3463
|
+
|
|
3051
3464
|
## Agent Team
|
|
3052
3465
|
|
|
3053
3466
|
| Agent | Invoke | Best For |
|
|
3054
3467
|
|-------|--------|----------|
|
|
3055
|
-
|
|
|
3468
|
+
| Design | @design | Discovery, UX planning, wireframes, visual system, implementation handoff, design fidelity review |
|
|
3469
|
+
| Backend Coder | @backend-coder | Backend code implementation |
|
|
3470
|
+
| Frontend Coder | @frontend-coder | Frontend code implementation |
|
|
3471
|
+
| DevOps | @devops | CI/CD and infrastructure implementation |
|
|
3056
3472
|
| Researcher | @researcher | API docs, library usage |
|
|
3057
3473
|
| Tester | @tester | Writing and running tests |
|
|
3058
3474
|
| Reviewer | @reviewer | Code quality review |
|
|
@@ -3063,7 +3479,6 @@ For each incomplete step in PLAN.md:
|
|
|
3063
3479
|
| Code Explorer | @code-explorer | Reading unfamiliar code |
|
|
3064
3480
|
| Debug Specialist | @debug-specialist | Root cause analysis |
|
|
3065
3481
|
| Build Resolver | @build-error-resolver | Build/compile failures |
|
|
3066
|
-
| Parallel Coordinator | @parallel-coordinator | Multi-track parallel work |
|
|
3067
3482
|
| Doc Updater | @doc-updater | Updating existing docs |
|
|
3068
3483
|
| Task Splitter | @task-splitter | Decomposing complex tasks |
|
|
3069
3484
|
| Discusser | @discusser | Requirements extraction |
|
|
@@ -3076,12 +3491,13 @@ For each incomplete step in PLAN.md:
|
|
|
3076
3491
|
## Phase State Machine
|
|
3077
3492
|
|
|
3078
3493
|
\`\`\`
|
|
3079
|
-
discuss → plan → execute → review
|
|
3494
|
+
discuss → plan → design (for UI-heavy tasks) → execute → review
|
|
3080
3495
|
\`\`\`
|
|
3081
3496
|
|
|
3082
3497
|
- **discuss**: Requirements extraction with @discusser
|
|
3083
3498
|
- **plan**: Plan creation with @planner, review with @plan-checker
|
|
3084
|
-
- **
|
|
3499
|
+
- **design**: UX structure, wireframe/layout planning, and visual system definition with @design
|
|
3500
|
+
- **execute**: Implementation with @backend-coder, @frontend-coder, @devops, @tester, and @researcher in parallel where possible, only after approved design handoff for UI-heavy tasks
|
|
3085
3501
|
- **review**: Review with @reviewer, @security-auditor
|
|
3086
3502
|
|
|
3087
3503
|
## Tracking
|
|
@@ -3103,7 +3519,7 @@ If a delegated agent fails:
|
|
|
3103
3519
|
3. If still failing, escalate:
|
|
3104
3520
|
|
|
3105
3521
|
\`\`\`
|
|
3106
|
-
BLOCKED:
|
|
3522
|
+
BLOCKED: implementation agent failed on step 3 (add payment endpoint).
|
|
3107
3523
|
Error: [exact error message]
|
|
3108
3524
|
Retried once with clarification. Still failing.
|
|
3109
3525
|
|
|
@@ -3124,11 +3540,26 @@ When a task required unusual human guidance, a novel solution strategy, or expos
|
|
|
3124
3540
|
|
|
3125
3541
|
Do NOT create a skill for routine tasks. Only capture genuinely novel or reusable patterns.`;
|
|
3126
3542
|
var AGENT_DESCRIPTIONS = {
|
|
3127
|
-
|
|
3128
|
-
- Role:
|
|
3543
|
+
design: `@design
|
|
3544
|
+
- Role: Runs design-first workflow for user-facing tasks
|
|
3545
|
+
- Permissions: Read/write files
|
|
3546
|
+
- Best for: UX structure, wireframes, visual direction, tokens, and frontend handoff
|
|
3547
|
+
- **Delegate when:** Task includes website/app/dashboard/admin/user-facing UI work`,
|
|
3548
|
+
"backend-coder": `@backend-coder
|
|
3549
|
+
- Role: Implements backend features and fixes based on confirmed plans
|
|
3550
|
+
- Permissions: Read/write files
|
|
3551
|
+
- Best for: API, services, data layer, and business logic
|
|
3552
|
+
- **Delegate when:** Backend or server-side implementation work`,
|
|
3553
|
+
"frontend-coder": `@frontend-coder
|
|
3554
|
+
- Role: Implements frontend features and fixes based on confirmed plans
|
|
3555
|
+
- Permissions: Read/write files
|
|
3556
|
+
- Best for: UI components, client state, rendering, and interaction behavior
|
|
3557
|
+
- **Delegate when:** Frontend implementation work`,
|
|
3558
|
+
devops: `@devops
|
|
3559
|
+
- Role: Implements DevOps and infrastructure changes based on confirmed plans
|
|
3129
3560
|
- Permissions: Read/write files
|
|
3130
|
-
- Best for:
|
|
3131
|
-
- **Delegate when:**
|
|
3561
|
+
- Best for: CI/CD, deployment config, infra scripts, and runtime operations
|
|
3562
|
+
- **Delegate when:** Infrastructure, pipeline, or operations implementation work`,
|
|
3132
3563
|
researcher: `@researcher
|
|
3133
3564
|
- Role: Researches documentation, APIs, and best practices
|
|
3134
3565
|
- Permissions: Read files
|
|
@@ -3200,11 +3631,6 @@ var AGENT_DESCRIPTIONS = {
|
|
|
3200
3631
|
- Permissions: Read/write files
|
|
3201
3632
|
- Best for: Requirements extraction
|
|
3202
3633
|
- **Delegate when:** Starting new feature or project phase`,
|
|
3203
|
-
"parallel-coordinator": `@parallel-coordinator
|
|
3204
|
-
- Role: Coordinates multi-wave parallel execution
|
|
3205
|
-
- Permissions: Read files
|
|
3206
|
-
- Best for: Multi-track parallel work
|
|
3207
|
-
- **Delegate when:** Need to execute multiple tasks in parallel`,
|
|
3208
3634
|
planner: `@planner
|
|
3209
3635
|
- Role: Creates detailed implementation plans
|
|
3210
3636
|
- Permissions: Read files
|
|
@@ -3506,7 +3932,7 @@ var createPlanCheckerAgent = (model, customPrompt, customAppendPrompt) => {
|
|
|
3506
3932
|
};
|
|
3507
3933
|
|
|
3508
3934
|
// src/agents/coder.ts
|
|
3509
|
-
var
|
|
3935
|
+
var BASE_IMPLEMENTER_PROMPT = `You implement features and fix bugs. You follow the plan exactly. You do not invent requirements.
|
|
3510
3936
|
|
|
3511
3937
|
## Before Writing Code
|
|
3512
3938
|
|
|
@@ -3616,11 +4042,62 @@ After implementing, report:
|
|
|
3616
4042
|
- Tests added or updated
|
|
3617
4043
|
- Any deviations from the plan and why
|
|
3618
4044
|
- Next step ready to execute`;
|
|
3619
|
-
var
|
|
3620
|
-
|
|
4045
|
+
var BACKEND_CODER_PROMPT = `${BASE_IMPLEMENTER_PROMPT}
|
|
4046
|
+
|
|
4047
|
+
## Domain Focus
|
|
4048
|
+
|
|
4049
|
+
Prioritize backend and platform code:
|
|
4050
|
+
- Server handlers, services, repositories, jobs, and business logic
|
|
4051
|
+
- Database and persistence-layer changes
|
|
4052
|
+
- API contracts and boundary validation
|
|
4053
|
+
`;
|
|
4054
|
+
var FRONTEND_CODER_PROMPT = `${BASE_IMPLEMENTER_PROMPT}
|
|
4055
|
+
|
|
4056
|
+
## Domain Focus
|
|
4057
|
+
|
|
4058
|
+
Prioritize frontend implementation quality:
|
|
4059
|
+
- UI components, client state, accessibility, and interaction behavior
|
|
4060
|
+
- Styling consistency with existing design system/tokens
|
|
4061
|
+
- Browser/runtime safety (no server-only assumptions in client code)
|
|
4062
|
+
`;
|
|
4063
|
+
var DEVOPS_PROMPT = `${BASE_IMPLEMENTER_PROMPT}
|
|
4064
|
+
|
|
4065
|
+
## Domain Focus
|
|
4066
|
+
|
|
4067
|
+
Prioritize infrastructure and delivery tasks:
|
|
4068
|
+
- CI/CD workflows, build pipelines, deployment configuration
|
|
4069
|
+
- Environment/runtime configuration and operational scripts
|
|
4070
|
+
- Reliability and rollback safety for production-facing changes
|
|
4071
|
+
`;
|
|
4072
|
+
var createBackendCoderAgent = (model, customPrompt, customAppendPrompt) => {
|
|
4073
|
+
const prompt = resolvePrompt(BACKEND_CODER_PROMPT, customPrompt, customAppendPrompt);
|
|
3621
4074
|
return {
|
|
3622
|
-
name: "coder",
|
|
3623
|
-
description: "Implements features and fixes based on confirmed plans. Follows existing code patterns and project conventions.
|
|
4075
|
+
name: "backend-coder",
|
|
4076
|
+
description: "Implements backend features and fixes based on confirmed plans. Follows existing code patterns and project conventions.",
|
|
4077
|
+
config: {
|
|
4078
|
+
model,
|
|
4079
|
+
temperature: 0.1,
|
|
4080
|
+
prompt
|
|
4081
|
+
}
|
|
4082
|
+
};
|
|
4083
|
+
};
|
|
4084
|
+
var createFrontendCoderAgent = (model, customPrompt, customAppendPrompt) => {
|
|
4085
|
+
const prompt = resolvePrompt(FRONTEND_CODER_PROMPT, customPrompt, customAppendPrompt);
|
|
4086
|
+
return {
|
|
4087
|
+
name: "frontend-coder",
|
|
4088
|
+
description: "Implements frontend features and fixes based on confirmed plans. Follows existing code patterns and project conventions.",
|
|
4089
|
+
config: {
|
|
4090
|
+
model,
|
|
4091
|
+
temperature: 0.1,
|
|
4092
|
+
prompt
|
|
4093
|
+
}
|
|
4094
|
+
};
|
|
4095
|
+
};
|
|
4096
|
+
var createDevopsAgent = (model, customPrompt, customAppendPrompt) => {
|
|
4097
|
+
const prompt = resolvePrompt(DEVOPS_PROMPT, customPrompt, customAppendPrompt);
|
|
4098
|
+
return {
|
|
4099
|
+
name: "devops",
|
|
4100
|
+
description: "Implements DevOps and infrastructure changes based on confirmed plans. Follows existing repo conventions and operational safety practices.",
|
|
3624
4101
|
config: {
|
|
3625
4102
|
model,
|
|
3626
4103
|
temperature: 0.1,
|
|
@@ -3767,6 +4244,13 @@ var REVIEWER_PROMPT = `You review code for correctness, security, and quality. Y
|
|
|
3767
4244
|
4. Apply the checklist below
|
|
3768
4245
|
5. Report by severity — CRITICAL first, then HIGH, MEDIUM, PASS
|
|
3769
4246
|
|
|
4247
|
+
If the task is UI-heavy and a design artifact is available, include design fidelity checks:
|
|
4248
|
+
- visual hierarchy and spacing consistency
|
|
4249
|
+
- CTA flow quality
|
|
4250
|
+
- responsive behavior
|
|
4251
|
+
- accessibility semantics and states
|
|
4252
|
+
- empty/loading/error/success state coverage
|
|
4253
|
+
|
|
3770
4254
|
## Security Checklist — CRITICAL
|
|
3771
4255
|
|
|
3772
4256
|
**Hardcoded credentials:**
|
|
@@ -3971,7 +4455,7 @@ Never fabricate information to appear more helpful.
|
|
|
3971
4455
|
## Scope Boundaries
|
|
3972
4456
|
|
|
3973
4457
|
- Report facts only. Do not make implementation decisions.
|
|
3974
|
-
- Do not write code unless asked. Return research findings for the
|
|
4458
|
+
- Do not write code unless asked. Return research findings for the implementation agent to act on.
|
|
3975
4459
|
- If you find a better approach than what was requested, mention it as an option — do not substitute it.
|
|
3976
4460
|
|
|
3977
4461
|
## Research Areas
|
|
@@ -4091,7 +4575,7 @@ var createWriterAgent = (model, customPrompt, customAppendPrompt) => {
|
|
|
4091
4575
|
};
|
|
4092
4576
|
|
|
4093
4577
|
// src/agents/security-auditor.ts
|
|
4094
|
-
var SECURITY_AUDITOR_PROMPT = `You audit code for security vulnerabilities. You report findings with severity and specific remediation. You do not fix — that is
|
|
4578
|
+
var SECURITY_AUDITOR_PROMPT = `You audit code for security vulnerabilities. You report findings with severity and specific remediation. You do not fix — that is the implementation agent's job (@backend-coder, @frontend-coder, or @devops).
|
|
4095
4579
|
|
|
4096
4580
|
## Audit Scope
|
|
4097
4581
|
|
|
@@ -4194,7 +4678,7 @@ For high/critical vulnerabilities: report exact package, CVE ID, and whether it'
|
|
|
4194
4678
|
|
|
4195
4679
|
## After Finding Issues
|
|
4196
4680
|
|
|
4197
|
-
Report only. Do not fix. Tag @coder with specific remediations for each finding.`;
|
|
4681
|
+
Report only. Do not fix. Tag the appropriate implementation agent (@backend-coder, @frontend-coder, or @devops) with specific remediations for each finding.`;
|
|
4198
4682
|
var createSecurityAuditorAgent = (model, customPrompt, customAppendPrompt) => {
|
|
4199
4683
|
const prompt = resolvePrompt(SECURITY_AUDITOR_PROMPT, customPrompt, customAppendPrompt);
|
|
4200
4684
|
return {
|
|
@@ -4542,7 +5026,7 @@ request → router → UserController.create() → UserService.create() → ❌
|
|
|
4542
5026
|
|
|
4543
5027
|
## Scope
|
|
4544
5028
|
|
|
4545
|
-
Report only. Do not implement the fix. Tag @coder with the recommended fix.`;
|
|
5029
|
+
Report only. Do not implement the fix. Tag the appropriate implementation agent (@backend-coder, @frontend-coder, or @devops) with the recommended fix.`;
|
|
4546
5030
|
var BUILD_ERROR_RESOLVER_PROMPT = `You fix build failures. You read the full error output, find the root cause, and apply the minimum fix to get the build green.
|
|
4547
5031
|
|
|
4548
5032
|
## Diagnostic Commands
|
|
@@ -4645,7 +5129,7 @@ npx tsc --noEmit src/path/to/file.ts
|
|
|
4645
5129
|
|
|
4646
5130
|
- Build fails because of architectural problems → @architect
|
|
4647
5131
|
- A feature is not working correctly → @debug-specialist
|
|
4648
|
-
- Missing functionality needs to be written → @coder`;
|
|
5132
|
+
- Missing functionality needs to be written → @backend-coder/@frontend-coder/@devops`;
|
|
4649
5133
|
var createDebugSpecialistAgent = (model, customPrompt, customAppendPrompt) => {
|
|
4650
5134
|
const prompt = resolvePrompt(DEBUG_SPECIALIST_PROMPT, customPrompt, customAppendPrompt);
|
|
4651
5135
|
return {
|
|
@@ -4672,7 +5156,7 @@ var createBuildErrorResolverAgent = (model, customPrompt, customAppendPrompt) =>
|
|
|
4672
5156
|
};
|
|
4673
5157
|
|
|
4674
5158
|
// src/agents/specialist.ts
|
|
4675
|
-
var TASK_SPLITTER_PROMPT = `You decompose complex tasks into parallel workstreams. You identify dependencies, group independent work into waves, and produce a plan that @
|
|
5159
|
+
var TASK_SPLITTER_PROMPT = `You decompose complex tasks into parallel workstreams. You identify dependencies, group independent work into waves, and produce a plan that @orchestrator can execute.
|
|
4676
5160
|
|
|
4677
5161
|
## Wave-Structured Output
|
|
4678
5162
|
|
|
@@ -4682,7 +5166,7 @@ var TASK_SPLITTER_PROMPT = `You decompose complex tasks into parallel workstream
|
|
|
4682
5166
|
### Wave 1 (parallel — start simultaneously)
|
|
4683
5167
|
|
|
4684
5168
|
**Track A — [description]**
|
|
4685
|
-
- Agent: @coder
|
|
5169
|
+
- Agent: @backend-coder
|
|
4686
5170
|
- Files: \`src/auth/user.ts\`, \`src/auth/types.ts\`
|
|
4687
5171
|
- Task: [specific implementation task]
|
|
4688
5172
|
- Verify: [how to confirm it's done]
|
|
@@ -4702,7 +5186,7 @@ var TASK_SPLITTER_PROMPT = `You decompose complex tasks into parallel workstream
|
|
|
4702
5186
|
### Wave 2 (after Wave 1 completes)
|
|
4703
5187
|
|
|
4704
5188
|
**Track D — Integration**
|
|
4705
|
-
- Agent: @coder
|
|
5189
|
+
- Agent: @backend-coder
|
|
4706
5190
|
- Depends on: Track A, Track C
|
|
4707
5191
|
- Task: Wire together outputs from Wave 1
|
|
4708
5192
|
|
|
@@ -4742,7 +5226,9 @@ After Wave 2: @reviewer reviews all changes together
|
|
|
4742
5226
|
| Agent | Best For |
|
|
4743
5227
|
|-------|---------|
|
|
4744
5228
|
| @architect | Interface contracts, ADRs |
|
|
4745
|
-
| @coder |
|
|
5229
|
+
| @backend-coder | Backend implementation |
|
|
5230
|
+
| @frontend-coder | Frontend implementation |
|
|
5231
|
+
| @devops | Infrastructure implementation |
|
|
4746
5232
|
| @researcher | API docs, library research |
|
|
4747
5233
|
| @tester | Test writing and coverage |
|
|
4748
5234
|
| @reviewer | Code quality review |
|
|
@@ -4880,200 +5366,6 @@ Discussion is complete when:
|
|
|
4880
5366
|
- No open questions remain
|
|
4881
5367
|
|
|
4882
5368
|
Report: "Requirements gathering complete. N decisions recorded. Ready for /plan."`;
|
|
4883
|
-
var PARALLEL_COORDINATOR_PROMPT = `You orchestrate multi-wave parallel execution. At the start of every job you emit a WAVE TABLE, then delegate agents by wave, wait for wave completion before advancing, and merge outputs when parallel tracks converge.
|
|
4884
|
-
|
|
4885
|
-
## Your Outputs
|
|
4886
|
-
|
|
4887
|
-
1. **WAVE TABLE** — printed at job start, shows every agent slot and its dependencies
|
|
4888
|
-
2. **Agent briefings** — full context packet per agent (they are stateless — give them everything)
|
|
4889
|
-
3. **Wave reports** — status after each wave closes
|
|
4890
|
-
4. **Merge resolution** — reconcile outputs when two tracks touched the same conceptual area
|
|
4891
|
-
|
|
4892
|
-
## WAVE TABLE Format
|
|
4893
|
-
|
|
4894
|
-
Print this at the start of every job before delegating any agents:
|
|
4895
|
-
|
|
4896
|
-
\`\`\`
|
|
4897
|
-
╔══════════════════════════════════════════════════════════════╗
|
|
4898
|
-
║ WAVE TABLE — [Job Title] ║
|
|
4899
|
-
╠══════════════════════════════════════════════════════════════╣
|
|
4900
|
-
║ Wave 1 (parallel) │ @researcher + @code-explorer ║
|
|
4901
|
-
║ Wave 2 (serial) │ @architect ║
|
|
4902
|
-
║ Wave 3 (parallel) │ @coder + @tester ║
|
|
4903
|
-
║ Wave 4 (parallel) │ @reviewer + @security-auditor ║
|
|
4904
|
-
╠══════════════════════════════════════════════════════════════╣
|
|
4905
|
-
║ Est. sequential: │ 8h ║
|
|
4906
|
-
║ Est. parallel: │ 4.5h ║
|
|
4907
|
-
║ Dependency locks: │ Wave 3 blocked on Wave 2 output ║
|
|
4908
|
-
╚══════════════════════════════════════════════════════════════╝
|
|
4909
|
-
\`\`\`
|
|
4910
|
-
|
|
4911
|
-
Adjust lanes based on actual task content. Remove any wave whose agents have no work.
|
|
4912
|
-
|
|
4913
|
-
## Standard Wave Delegation Syntax
|
|
4914
|
-
|
|
4915
|
-
**Wave 1 — Discovery (parallelize):**
|
|
4916
|
-
\`\`\`
|
|
4917
|
-
@researcher: [exact research task with sources to check]
|
|
4918
|
-
@code-explorer: [exact files/modules to map — list paths]
|
|
4919
|
-
\`\`\`
|
|
4920
|
-
Start both simultaneously. Do not wait for one before sending the other.
|
|
4921
|
-
|
|
4922
|
-
**Wave 2 — Architecture (serial, depends on Wave 1):**
|
|
4923
|
-
\`\`\`
|
|
4924
|
-
@architect: [design task — attach Wave 1 outputs as context]
|
|
4925
|
-
\`\`\`
|
|
4926
|
-
One agent. Must complete before Wave 3 starts.
|
|
4927
|
-
|
|
4928
|
-
**Wave 3 — Implementation (parallelize, depends on Wave 2):**
|
|
4929
|
-
\`\`\`
|
|
4930
|
-
@coder: [implementation task — attach @architect output + relevant Wave 1 findings]
|
|
4931
|
-
@tester: [test task — attach interface contracts from @architect, NOT @coder output]
|
|
4932
|
-
\`\`\`
|
|
4933
|
-
Start both simultaneously once Wave 2 output is in hand. @tester works from contracts, not @coder's code, so they are truly parallel.
|
|
4934
|
-
|
|
4935
|
-
**Wave 4 — Validation (parallelize):**
|
|
4936
|
-
\`\`\`
|
|
4937
|
-
@reviewer: [review scope — list files changed by Wave 3]
|
|
4938
|
-
@security-auditor: [audit scope — list entry points, auth surfaces, data flows]
|
|
4939
|
-
\`\`\`
|
|
4940
|
-
Start both once Wave 3 is complete.
|
|
4941
|
-
|
|
4942
|
-
## Parallelism Rules
|
|
4943
|
-
|
|
4944
|
-
**Safe to parallelize:**
|
|
4945
|
-
- Tasks touching different files with no shared output
|
|
4946
|
-
- Research alongside implementation (research produces inputs, not outputs of implementation)
|
|
4947
|
-
- Test writing from interface contracts alongside implementation
|
|
4948
|
-
- Documentation alongside implementation when writing to different files
|
|
4949
|
-
|
|
4950
|
-
**Must be sequential:**
|
|
4951
|
-
- Task B's design depends on decisions Task A makes
|
|
4952
|
-
- Task B reads a file Task A will write
|
|
4953
|
-
- Both tasks modify the same file
|
|
4954
|
-
|
|
4955
|
-
**Not worth parallelizing:**
|
|
4956
|
-
- Total estimated work is under 20 minutes
|
|
4957
|
-
- File ownership is ambiguous — if unclear who owns a file, serialize it
|
|
4958
|
-
|
|
4959
|
-
## Agent Team
|
|
4960
|
-
|
|
4961
|
-
| Agent | Best For |
|
|
4962
|
-
|-------|---------|
|
|
4963
|
-
| @architect | Interface contracts, ADRs, system design |
|
|
4964
|
-
| @coder | All code implementation |
|
|
4965
|
-
| @researcher | API docs, library usage, best practices |
|
|
4966
|
-
| @tester | Test writing and coverage |
|
|
4967
|
-
| @reviewer | Code quality review |
|
|
4968
|
-
| @security-auditor | Security vulnerability audit |
|
|
4969
|
-
| @writer | New documentation |
|
|
4970
|
-
| @doc-updater | Updating existing documentation |
|
|
4971
|
-
| @code-explorer | Mapping unfamiliar code |
|
|
4972
|
-
| @debug-specialist | Root cause analysis |
|
|
4973
|
-
| @build-error-resolver | Build and compile failures |
|
|
4974
|
-
|
|
4975
|
-
## Merging Parallel Outputs
|
|
4976
|
-
|
|
4977
|
-
When two Wave 3+ agents both worked on the same conceptual area (e.g., both touched auth logic, both proposed an interface for the same type):
|
|
4978
|
-
|
|
4979
|
-
**Step 1 — Detect the overlap.** After each wave, compare the file sets each agent reported touching. Any overlap is a merge candidate.
|
|
4980
|
-
|
|
4981
|
-
**Step 2 — Classify the overlap:**
|
|
4982
|
-
- **Additive** (different functions in the same file): safe to auto-merge, reconcile manually.
|
|
4983
|
-
- **Structural** (same type, same interface, same function signature): do not auto-merge — escalate.
|
|
4984
|
-
- **Contradictory** (one agent added a field, another removed it): escalate.
|
|
4985
|
-
|
|
4986
|
-
**Step 3 — Resolve:**
|
|
4987
|
-
- Additive: apply both changesets, verify no symbol collisions, verify tests pass.
|
|
4988
|
-
- Structural or contradictory: invoke the conflict resolution protocol below.
|
|
4989
|
-
|
|
4990
|
-
## Conflict Resolution Protocol
|
|
4991
|
-
|
|
4992
|
-
Trigger when two tracks produced incompatible changes to the same logical unit.
|
|
4993
|
-
|
|
4994
|
-
\`\`\`
|
|
4995
|
-
CONFLICT DETECTED
|
|
4996
|
-
Track A (@coder): added \`refreshToken: string\` to UserSession in src/types/session.ts
|
|
4997
|
-
Track B (@tester): wrote tests assuming UserSession has no refresh field
|
|
4998
|
-
Classification: Structural — interface mismatch
|
|
4999
|
-
|
|
5000
|
-
RESOLUTION PLAN
|
|
5001
|
-
1. Suspend Track B output (do not apply tests yet)
|
|
5002
|
-
2. Delegate to @coder: reconcile both versions sequentially
|
|
5003
|
-
- Brief: "Track A and Track B produced incompatible changes. [Attach both outputs.]
|
|
5004
|
-
Produce a single unified version that satisfies both intents."
|
|
5005
|
-
3. Once @coder delivers unified version: re-run @tester against it
|
|
5006
|
-
4. Mark original conflict as resolved, continue to Wave 4
|
|
5007
|
-
\`\`\`
|
|
5008
|
-
|
|
5009
|
-
Never silently pick one side. Always surface what was lost in the merge and why.
|
|
5010
|
-
|
|
5011
|
-
## Failure Handling
|
|
5012
|
-
|
|
5013
|
-
**Wave failure does not block independent waves.**
|
|
5014
|
-
|
|
5015
|
-
Before each wave starts, classify each task as:
|
|
5016
|
-
- **Blocking** — downstream waves need its output
|
|
5017
|
-
- **Independent** — downstream waves do not depend on it
|
|
5018
|
-
|
|
5019
|
-
If a blocking task fails:
|
|
5020
|
-
\`\`\`
|
|
5021
|
-
Wave 1 FAILURE — @researcher: could not retrieve bcrypt API docs
|
|
5022
|
-
Impact: Wave 3 @coder task "implement password hashing" is blocked.
|
|
5023
|
-
Action: Pause that specific Wave 3 slot. Continue all other Wave 3 slots.
|
|
5024
|
-
Retry: Re-run @researcher with a fallback source list, then unblock the Wave 3 slot.
|
|
5025
|
-
\`\`\`
|
|
5026
|
-
|
|
5027
|
-
If an independent task fails:
|
|
5028
|
-
\`\`\`
|
|
5029
|
-
Wave 4 FAILURE — @security-auditor: process timed out
|
|
5030
|
-
Impact: None — @reviewer completed independently.
|
|
5031
|
-
Action: Log failure. Do not block Wave 4 close. Re-run @security-auditor as a follow-up.
|
|
5032
|
-
\`\`\`
|
|
5033
|
-
|
|
5034
|
-
Wave gates work per-slot, not per-wave: a wave closes when all blocking slots complete. Independent failures are retried async.
|
|
5035
|
-
|
|
5036
|
-
## Full Execution Report Format
|
|
5037
|
-
|
|
5038
|
-
\`\`\`markdown
|
|
5039
|
-
## Parallel Execution Report — [Job Title]
|
|
5040
|
-
|
|
5041
|
-
### Wave 1 Results (Discovery)
|
|
5042
|
-
| Track | Agent | Status | Output |
|
|
5043
|
-
|-------|-------|--------|--------|
|
|
5044
|
-
| A | @researcher | ✅ | \`.planning/research/bcrypt.md\` |
|
|
5045
|
-
| B | @code-explorer | ✅ | \`.codebase/auth-module-map.md\` |
|
|
5046
|
-
|
|
5047
|
-
### Wave 1 → Wave 2 Gate
|
|
5048
|
-
- All blocking slots complete: ✅
|
|
5049
|
-
- Merge check: no file conflicts
|
|
5050
|
-
|
|
5051
|
-
### Wave 2 Results (Architecture)
|
|
5052
|
-
| Track | Agent | Status | Output |
|
|
5053
|
-
|-------|-------|--------|--------|
|
|
5054
|
-
| A | @architect | ✅ | \`.planning/adr/auth-design.md\`, interface contracts |
|
|
5055
|
-
|
|
5056
|
-
### Wave 3 Results (Implementation)
|
|
5057
|
-
| Track | Agent | Status | Output |
|
|
5058
|
-
|-------|-------|--------|--------|
|
|
5059
|
-
| A | @coder | ✅ | \`src/auth/service.ts\`, \`src/auth/session.ts\` |
|
|
5060
|
-
| B | @tester | ✅ | \`src/auth/service.test.ts\` — 14 tests, 14 passing |
|
|
5061
|
-
|
|
5062
|
-
### Wave 3 Merge Check
|
|
5063
|
-
- File overlap: none
|
|
5064
|
-
- Conceptual overlap: @coder and @tester both reference UserSession — compatible ✅
|
|
5065
|
-
|
|
5066
|
-
### Wave 4 Results (Validation)
|
|
5067
|
-
| Track | Agent | Status | Output |
|
|
5068
|
-
|-------|-------|--------|--------|
|
|
5069
|
-
| A | @reviewer | ✅ | 2 non-blocking suggestions filed |
|
|
5070
|
-
| B | @security-auditor | ⚠️ FAILED | Timeout — retrying async |
|
|
5071
|
-
|
|
5072
|
-
### Final Status
|
|
5073
|
-
- All blocking work complete ✅
|
|
5074
|
-
- @security-auditor re-run scheduled as follow-up
|
|
5075
|
-
- Elapsed: 4h 20m (vs 8h sequential)
|
|
5076
|
-
\`\`\``;
|
|
5077
5369
|
var createTaskSplitterAgent = (model, customPrompt, customAppendPrompt) => {
|
|
5078
5370
|
const prompt = resolvePrompt(TASK_SPLITTER_PROMPT, customPrompt, customAppendPrompt);
|
|
5079
5371
|
return {
|
|
@@ -5098,18 +5390,6 @@ var createDiscusserAgent = (model, customPrompt, customAppendPrompt) => {
|
|
|
5098
5390
|
}
|
|
5099
5391
|
};
|
|
5100
5392
|
};
|
|
5101
|
-
var createParallelCoordinatorAgent = (model, customPrompt, customAppendPrompt) => {
|
|
5102
|
-
const prompt = resolvePrompt(PARALLEL_COORDINATOR_PROMPT, customPrompt, customAppendPrompt);
|
|
5103
|
-
return {
|
|
5104
|
-
name: "parallel-coordinator",
|
|
5105
|
-
description: "Coordinates parallel agent execution for multi-track workstreams. Manages wave execution, handles merge conflicts, and maximizes throughput.",
|
|
5106
|
-
config: {
|
|
5107
|
-
model,
|
|
5108
|
-
temperature: 0.1,
|
|
5109
|
-
prompt
|
|
5110
|
-
}
|
|
5111
|
-
};
|
|
5112
|
-
};
|
|
5113
5393
|
|
|
5114
5394
|
// src/agents/architect.ts
|
|
5115
5395
|
var ARCHITECT_PROMPT = `You design system architecture, create Architecture Decision Records (ADRs), and define API contracts before implementation begins.
|
|
@@ -5756,11 +6036,81 @@ function createAutoLearnerAgent(model) {
|
|
|
5756
6036
|
return definition;
|
|
5757
6037
|
}
|
|
5758
6038
|
|
|
6039
|
+
// src/agents/design.ts
|
|
6040
|
+
var DESIGN_PROMPT = `You are the dedicated design architect for user-facing products. Your work is mandatory before coding for UI-heavy tasks unless an explicit override is recorded.
|
|
6041
|
+
|
|
6042
|
+
## Scope
|
|
6043
|
+
|
|
6044
|
+
Use this workflow for website, web app, mobile app, dashboard, admin panel, landing page, SaaS interface, onboarding UX, and other user-facing surfaces.
|
|
6045
|
+
|
|
6046
|
+
## Required Execution Stages
|
|
6047
|
+
|
|
6048
|
+
For UI-heavy tasks, produce all stages in order:
|
|
6049
|
+
1. discovery
|
|
6050
|
+
2. ux_planning
|
|
6051
|
+
3. wireframe_layout
|
|
6052
|
+
4. visual_system_definition
|
|
6053
|
+
5. design_approval
|
|
6054
|
+
6. implementation_handoff
|
|
6055
|
+
|
|
6056
|
+
Do not skip or merge stages.
|
|
6057
|
+
|
|
6058
|
+
## Structured Output Contract
|
|
6059
|
+
|
|
6060
|
+
Always return machine-readable markdown sections with these keys:
|
|
6061
|
+
- task_type
|
|
6062
|
+
- user_goals
|
|
6063
|
+
- target_audience
|
|
6064
|
+
- core_user_flows
|
|
6065
|
+
- page_map_or_screen_map
|
|
6066
|
+
- section_structure
|
|
6067
|
+
- layout_plan
|
|
6068
|
+
- component_list
|
|
6069
|
+
- state_list (loading, empty, error, success)
|
|
6070
|
+
- responsive_behavior_notes
|
|
6071
|
+
- visual_direction
|
|
6072
|
+
- design_tokens_guidance
|
|
6073
|
+
- accessibility_notes
|
|
6074
|
+
- implementation_handoff_checklist
|
|
6075
|
+
- approval_status
|
|
6076
|
+
|
|
6077
|
+
## Design Review Mode
|
|
6078
|
+
|
|
6079
|
+
When asked to review an implemented UI, compare against approved design artifacts and report:
|
|
6080
|
+
- design mismatches
|
|
6081
|
+
- hierarchy issues
|
|
6082
|
+
- spacing inconsistency
|
|
6083
|
+
- weak call-to-action flow
|
|
6084
|
+
- responsiveness issues
|
|
6085
|
+
- accessibility concerns
|
|
6086
|
+
- component inconsistency
|
|
6087
|
+
- missing empty/error states
|
|
6088
|
+
|
|
6089
|
+
## Constraints
|
|
6090
|
+
|
|
6091
|
+
- Do not write implementation code in design mode.
|
|
6092
|
+
- Do not claim approval without explicit pass/fail rationale.
|
|
6093
|
+
- Keep output concise but complete enough for frontend handoff.`;
|
|
6094
|
+
var createDesignAgent = (model, customPrompt, customAppendPrompt) => {
|
|
6095
|
+
const prompt = resolvePrompt(DESIGN_PROMPT, customPrompt, customAppendPrompt);
|
|
6096
|
+
return {
|
|
6097
|
+
name: "design",
|
|
6098
|
+
description: "Design-first specialist for UX structure, wireframe planning, visual system definition, and frontend handoff before implementation.",
|
|
6099
|
+
config: {
|
|
6100
|
+
model,
|
|
6101
|
+
temperature: 0.1,
|
|
6102
|
+
prompt
|
|
6103
|
+
}
|
|
6104
|
+
};
|
|
6105
|
+
};
|
|
6106
|
+
|
|
5759
6107
|
// src/agents/index.ts
|
|
5760
6108
|
var AGENT_NAMES = [
|
|
5761
6109
|
"orchestrator",
|
|
5762
6110
|
"planner",
|
|
5763
|
-
"coder",
|
|
6111
|
+
"backend-coder",
|
|
6112
|
+
"frontend-coder",
|
|
6113
|
+
"devops",
|
|
5764
6114
|
"plan-checker",
|
|
5765
6115
|
"tester",
|
|
5766
6116
|
"reviewer",
|
|
@@ -5774,13 +6124,13 @@ var AGENT_NAMES = [
|
|
|
5774
6124
|
"build-error-resolver",
|
|
5775
6125
|
"task-splitter",
|
|
5776
6126
|
"discusser",
|
|
5777
|
-
"parallel-coordinator",
|
|
5778
6127
|
"architect",
|
|
5779
6128
|
"risk-analyst",
|
|
5780
6129
|
"policy-enforcer",
|
|
5781
6130
|
"performance-optimizer",
|
|
5782
6131
|
"refactor-guide",
|
|
5783
|
-
"auto-learner"
|
|
6132
|
+
"auto-learner",
|
|
6133
|
+
"design"
|
|
5784
6134
|
];
|
|
5785
6135
|
var PRIMARY_AGENTS = new Set(["orchestrator"]);
|
|
5786
6136
|
var ALL_MODES_AGENTS = new Set;
|
|
@@ -5800,8 +6150,12 @@ function createAgent(name, model, customPrompt, customAppendPrompt) {
|
|
|
5800
6150
|
return createOrchestratorAgent(model, customPrompt, customAppendPrompt);
|
|
5801
6151
|
case "planner":
|
|
5802
6152
|
return createPlannerAgent(model, customPrompt, customAppendPrompt);
|
|
5803
|
-
case "coder":
|
|
5804
|
-
return
|
|
6153
|
+
case "backend-coder":
|
|
6154
|
+
return createBackendCoderAgent(model, customPrompt, customAppendPrompt);
|
|
6155
|
+
case "frontend-coder":
|
|
6156
|
+
return createFrontendCoderAgent(model, customPrompt, customAppendPrompt);
|
|
6157
|
+
case "devops":
|
|
6158
|
+
return createDevopsAgent(model, customPrompt, customAppendPrompt);
|
|
5805
6159
|
case "plan-checker":
|
|
5806
6160
|
return createPlanCheckerAgent(model, customPrompt, customAppendPrompt);
|
|
5807
6161
|
case "tester":
|
|
@@ -5828,8 +6182,6 @@ function createAgent(name, model, customPrompt, customAppendPrompt) {
|
|
|
5828
6182
|
return createTaskSplitterAgent(model, customPrompt, customAppendPrompt);
|
|
5829
6183
|
case "discusser":
|
|
5830
6184
|
return createDiscusserAgent(model, customPrompt, customAppendPrompt);
|
|
5831
|
-
case "parallel-coordinator":
|
|
5832
|
-
return createParallelCoordinatorAgent(model, customPrompt, customAppendPrompt);
|
|
5833
6185
|
case "architect":
|
|
5834
6186
|
return createArchitectAgent(model, customPrompt, customAppendPrompt);
|
|
5835
6187
|
case "risk-analyst":
|
|
@@ -5842,6 +6194,8 @@ function createAgent(name, model, customPrompt, customAppendPrompt) {
|
|
|
5842
6194
|
return createRefactorGuideAgent(model, customPrompt, customAppendPrompt);
|
|
5843
6195
|
case "auto-learner":
|
|
5844
6196
|
return createAutoLearnerAgent(model);
|
|
6197
|
+
case "design":
|
|
6198
|
+
return createDesignAgent(model, customPrompt, customAppendPrompt);
|
|
5845
6199
|
default:
|
|
5846
6200
|
console.warn(`[flowdeck] Unknown agent: ${name}`);
|
|
5847
6201
|
return;
|
|
@@ -5879,42 +6233,16 @@ function getAgentConfigs(agentModels) {
|
|
|
5879
6233
|
return configs;
|
|
5880
6234
|
}
|
|
5881
6235
|
|
|
5882
|
-
// src/config/loader.ts
|
|
5883
|
-
import { existsSync as existsSync24, readFileSync as readFileSync21 } from "fs";
|
|
5884
|
-
import { join as join23 } from "path";
|
|
5885
|
-
import { homedir as homedir3 } from "os";
|
|
5886
|
-
var CONFIG_FILENAME = "flowdeck.json";
|
|
5887
|
-
function getGlobalConfigDir() {
|
|
5888
|
-
return process.env.OPENCODE_CONFIG_DIR || (process.env.XDG_CONFIG_HOME ? join23(process.env.XDG_CONFIG_HOME, "opencode") : join23(homedir3(), ".config", "opencode"));
|
|
5889
|
-
}
|
|
5890
|
-
function loadFlowDeckConfig(directory) {
|
|
5891
|
-
const candidates = [];
|
|
5892
|
-
if (directory) {
|
|
5893
|
-
candidates.push(join23(directory, ".opencode", CONFIG_FILENAME));
|
|
5894
|
-
}
|
|
5895
|
-
candidates.push(join23(getGlobalConfigDir(), CONFIG_FILENAME));
|
|
5896
|
-
for (const configPath of candidates) {
|
|
5897
|
-
if (existsSync24(configPath)) {
|
|
5898
|
-
try {
|
|
5899
|
-
const content = readFileSync21(configPath, "utf-8");
|
|
5900
|
-
return JSON.parse(content);
|
|
5901
|
-
} catch {
|
|
5902
|
-
console.warn(`[flowdeck] Failed to load config from ${configPath}`);
|
|
5903
|
-
}
|
|
5904
|
-
}
|
|
5905
|
-
}
|
|
5906
|
-
return {};
|
|
5907
|
-
}
|
|
5908
6236
|
// src/index.ts
|
|
5909
6237
|
function loadRulePaths() {
|
|
5910
6238
|
const __dir = dirname4(fileURLToPath2(import.meta.url));
|
|
5911
|
-
const rulesDir =
|
|
5912
|
-
if (!
|
|
6239
|
+
const rulesDir = join26(__dir, "..", "src", "rules");
|
|
6240
|
+
if (!existsSync27(rulesDir))
|
|
5913
6241
|
return [];
|
|
5914
6242
|
const paths = [];
|
|
5915
6243
|
function walk(dir) {
|
|
5916
6244
|
for (const entry of readdirSync3(dir, { withFileTypes: true })) {
|
|
5917
|
-
const full =
|
|
6245
|
+
const full = join26(dir, entry.name);
|
|
5918
6246
|
if (entry.isDirectory()) {
|
|
5919
6247
|
walk(full);
|
|
5920
6248
|
} else if (entry.isFile() && entry.name.endsWith(".md") && entry.name !== "README.md") {
|
|
@@ -5927,8 +6255,8 @@ function loadRulePaths() {
|
|
|
5927
6255
|
}
|
|
5928
6256
|
function loadCommands() {
|
|
5929
6257
|
const __dir = dirname4(fileURLToPath2(import.meta.url));
|
|
5930
|
-
const commandsDir =
|
|
5931
|
-
if (!
|
|
6258
|
+
const commandsDir = join26(__dir, "..", "src", "commands");
|
|
6259
|
+
if (!existsSync27(commandsDir))
|
|
5932
6260
|
return {};
|
|
5933
6261
|
const commands = {};
|
|
5934
6262
|
try {
|
|
@@ -5936,7 +6264,7 @@ function loadCommands() {
|
|
|
5936
6264
|
if (!file.endsWith(".md"))
|
|
5937
6265
|
continue;
|
|
5938
6266
|
const name = basename(file, ".md");
|
|
5939
|
-
const raw =
|
|
6267
|
+
const raw = readFileSync23(join26(commandsDir, file), "utf-8");
|
|
5940
6268
|
let description;
|
|
5941
6269
|
let template = raw;
|
|
5942
6270
|
const fmMatch = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/);
|
|
@@ -5977,12 +6305,16 @@ var plugin = async (input, _options) => {
|
|
|
5977
6305
|
cfg.default_agent = "orchestrator";
|
|
5978
6306
|
}
|
|
5979
6307
|
const flowdeckConfig = loadFlowDeckConfig(directory);
|
|
6308
|
+
const designFirstConfig = resolveDesignFirstConfig(flowdeckConfig);
|
|
5980
6309
|
const agentModels = {};
|
|
5981
6310
|
for (const [name, agentCfg] of Object.entries(flowdeckConfig.agents ?? {})) {
|
|
5982
6311
|
if (agentCfg.model) {
|
|
5983
6312
|
agentModels[name] = agentCfg.model;
|
|
5984
6313
|
}
|
|
5985
6314
|
}
|
|
6315
|
+
if (designFirstConfig.modelOverrides.design) {
|
|
6316
|
+
agentModels.design = designFirstConfig.modelOverrides.design;
|
|
6317
|
+
}
|
|
5986
6318
|
const resolvedAgentConfigs = getAgentConfigs(agentModels);
|
|
5987
6319
|
if (!cfg.agent) {
|
|
5988
6320
|
cfg.agent = { ...resolvedAgentConfigs };
|
|
@@ -6013,8 +6345,8 @@ var plugin = async (input, _options) => {
|
|
|
6013
6345
|
}
|
|
6014
6346
|
}
|
|
6015
6347
|
}
|
|
6016
|
-
const skillsDir =
|
|
6017
|
-
if (
|
|
6348
|
+
const skillsDir = join26(dirname4(fileURLToPath2(import.meta.url)), "..", "src", "skills");
|
|
6349
|
+
if (existsSync27(skillsDir)) {
|
|
6018
6350
|
const cfgAny = cfg;
|
|
6019
6351
|
if (!cfgAny.skills || typeof cfgAny.skills !== "object") {
|
|
6020
6352
|
cfgAny.skills = { paths: [] };
|
|
@@ -6100,8 +6432,12 @@ var plugin = async (input, _options) => {
|
|
|
6100
6432
|
await contextMonitor.event({ event });
|
|
6101
6433
|
orchestratorGuard.onEvent(event);
|
|
6102
6434
|
if (type === "session.idle") {
|
|
6103
|
-
|
|
6104
|
-
|
|
6435
|
+
try {
|
|
6436
|
+
await sessionIdleHook();
|
|
6437
|
+
await autoLearnHook();
|
|
6438
|
+
} finally {
|
|
6439
|
+
fileTracker.clear();
|
|
6440
|
+
}
|
|
6105
6441
|
}
|
|
6106
6442
|
},
|
|
6107
6443
|
"tool.execute.before": async (toolInput, toolOutput) => {
|