@dv.nghiem/flowdeck 0.3.4 → 0.3.6
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 +154 -3
- 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/memory-hook.d.ts +7 -0
- package/dist/hooks/memory-hook.d.ts.map +1 -1
- package/dist/hooks/orchestrator-guard-hook.d.ts.map +1 -1
- package/dist/hooks/tool-guard.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +749 -503
- 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/memory-store.d.ts +34 -1
- package/dist/services/memory-store.d.ts.map +1 -1
- package/dist/services/memory-store.test.d.ts +2 -0
- package/dist/services/memory-store.test.d.ts.map +1 -0
- 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/delegate.d.ts.map +1 -1
- package/dist/tools/dispatch-routing.d.ts +4 -1
- package/dist/tools/dispatch-routing.d.ts.map +1 -1
- package/dist/tools/dispatch-routing.test.d.ts +2 -0
- package/dist/tools/dispatch-routing.test.d.ts.map +1 -0
- package/dist/tools/memory-search.d.ts.map +1 -1
- package/dist/tools/memory-status.d.ts.map +1 -1
- 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/intelligence.md +92 -1
- package/docs/multi-repo.md +1 -1
- package/docs/rules.md +1 -1
- package/docs/skills.md +24 -15
- package/docs/workflows.md +11 -6
- package/package.json +1 -1
- 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/common/agent-orchestration.md +6 -6
- package/src/skills/app-shell-design/SKILL.md +31 -0
- package/src/skills/dashboard-design/SKILL.md +32 -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/frontend-handoff/SKILL.md +31 -0
- package/src/skills/landing-page-design/SKILL.md +32 -0
- package/src/skills/multi-repo/SKILL.md +3 -3
- package/src/skills/plan-task/SKILL.md +2 -2
- package/src/skills/responsive-review/SKILL.md +31 -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
|
}
|
|
@@ -567,51 +619,6 @@ function recordRun(dir, agent, model, task_type, success, duration_ms, cost = 0)
|
|
|
567
619
|
saveStore(dir, store);
|
|
568
620
|
}
|
|
569
621
|
|
|
570
|
-
// src/services/model-router.ts
|
|
571
|
-
import { existsSync as existsSync6, readFileSync as readFileSync6 } from "fs";
|
|
572
|
-
import { join as join6 } from "path";
|
|
573
|
-
var DEFAULT_ROUTING = {
|
|
574
|
-
planning: { primary: "claude-sonnet-4-5", temperature: 0.3, reasoning_effort: "medium" },
|
|
575
|
-
implementation: { primary: "claude-opus-4-5", fallback: "claude-sonnet-4-5", high_risk_override: "claude-opus-4-5", temperature: 0.2, reasoning_effort: "high" },
|
|
576
|
-
debugging: { primary: "claude-sonnet-4-5", high_risk_override: "claude-opus-4-5", temperature: 0.2, reasoning_effort: "high" },
|
|
577
|
-
review: { primary: "gemini-2.5-flash", fallback: "claude-haiku-4-5", temperature: 0.1, reasoning_effort: "medium" },
|
|
578
|
-
testing: { primary: "claude-haiku-4-5", fallback: "gemini-2.5-flash", temperature: 0.1, reasoning_effort: "low" },
|
|
579
|
-
documentation: { primary: "claude-sonnet-4-5", fallback: "gemini-2.5-flash", temperature: 0.3, reasoning_effort: "low" },
|
|
580
|
-
analysis: { primary: "claude-sonnet-4-5", temperature: 0.3, reasoning_effort: "medium" },
|
|
581
|
-
security: { primary: "claude-opus-4-5", high_risk_override: "claude-opus-4-5", temperature: 0.1, reasoning_effort: "high" },
|
|
582
|
-
orchestration: { primary: "claude-sonnet-4-5", temperature: 0.3, reasoning_effort: "medium" }
|
|
583
|
-
};
|
|
584
|
-
function getRouterConfig(dir) {
|
|
585
|
-
const p = join6(codebaseDir(dir), "MODEL_ROUTER.json");
|
|
586
|
-
if (!existsSync6(p))
|
|
587
|
-
return DEFAULT_ROUTING;
|
|
588
|
-
try {
|
|
589
|
-
const overrides = JSON.parse(readFileSync6(p, "utf-8"));
|
|
590
|
-
return { ...DEFAULT_ROUTING, ...overrides };
|
|
591
|
-
} catch {
|
|
592
|
-
return DEFAULT_ROUTING;
|
|
593
|
-
}
|
|
594
|
-
}
|
|
595
|
-
function routeModel(dir, task_type, risk_score = 100) {
|
|
596
|
-
const config = getRouterConfig(dir);
|
|
597
|
-
const route = config[task_type] ?? DEFAULT_ROUTING.implementation;
|
|
598
|
-
const is_high_risk = risk_score < 40;
|
|
599
|
-
let model = route.primary;
|
|
600
|
-
let is_override = false;
|
|
601
|
-
if (is_high_risk && route.high_risk_override) {
|
|
602
|
-
model = route.high_risk_override;
|
|
603
|
-
is_override = true;
|
|
604
|
-
}
|
|
605
|
-
return {
|
|
606
|
-
model,
|
|
607
|
-
temperature: route.temperature ?? 0.3,
|
|
608
|
-
reasoning_effort: route.reasoning_effort,
|
|
609
|
-
task_type,
|
|
610
|
-
is_high_risk,
|
|
611
|
-
is_override
|
|
612
|
-
};
|
|
613
|
-
}
|
|
614
|
-
|
|
615
622
|
// src/tools/dispatch-routing.ts
|
|
616
623
|
function shouldRetry(promptRes) {
|
|
617
624
|
if (!promptRes)
|
|
@@ -634,6 +641,8 @@ function normalizeTaskType(taskType, agent) {
|
|
|
634
641
|
if (isTaskType(normalized))
|
|
635
642
|
return normalized;
|
|
636
643
|
const a = agent.toLowerCase();
|
|
644
|
+
if (a.includes("design") || a.includes("ui-ux"))
|
|
645
|
+
return "design";
|
|
637
646
|
if (a.includes("review"))
|
|
638
647
|
return "review";
|
|
639
648
|
if (a.includes("test"))
|
|
@@ -653,7 +662,48 @@ function normalizeTaskType(taskType, agent) {
|
|
|
653
662
|
return "implementation";
|
|
654
663
|
}
|
|
655
664
|
function isTaskType(value) {
|
|
656
|
-
return value === "planning" || value === "implementation" || value === "debugging" || value === "review" || value === "testing" || value === "documentation" || value === "analysis" || value === "security" || value === "orchestration";
|
|
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;
|
|
657
707
|
}
|
|
658
708
|
|
|
659
709
|
// src/tools/run-pipeline.ts
|
|
@@ -699,7 +749,6 @@ function createRunPipelineTool(client) {
|
|
|
699
749
|
}
|
|
700
750
|
const stepStart = Date.now();
|
|
701
751
|
const taskType = normalizeTaskType(step.task_type, step.agent);
|
|
702
|
-
const routing = routeModel(context.directory, taskType);
|
|
703
752
|
const stepInput = carryContext ? `${carryContext}
|
|
704
753
|
|
|
705
754
|
---
|
|
@@ -711,7 +760,7 @@ ${step.prompt}` : step.prompt;
|
|
|
711
760
|
});
|
|
712
761
|
if (createRes.error || !createRes.data?.id) {
|
|
713
762
|
const errMsg = `Failed to create session: ${createRes.error?.detail ?? "unknown"}`;
|
|
714
|
-
trace.push({ agent: step.agent, task_type: taskType, model:
|
|
763
|
+
trace.push({ agent: step.agent, task_type: taskType, model: "", input: stepInput, output: errMsg, duration_ms: Date.now() - stepStart, success: false });
|
|
715
764
|
aborted = true;
|
|
716
765
|
break;
|
|
717
766
|
}
|
|
@@ -723,7 +772,6 @@ ${step.prompt}` : step.prompt;
|
|
|
723
772
|
path: { id: inflightChildId },
|
|
724
773
|
body: {
|
|
725
774
|
agent: step.agent,
|
|
726
|
-
model: routing.model,
|
|
727
775
|
parts: [{ type: "text", text: stepInput }],
|
|
728
776
|
tools: { question: false }
|
|
729
777
|
},
|
|
@@ -740,8 +788,8 @@ ${step.prompt}` : step.prompt;
|
|
|
740
788
|
}
|
|
741
789
|
if (!promptRes || promptRes.error) {
|
|
742
790
|
const errMsg = `Prompt failed: ${promptRes?.error?.detail ?? "unknown"}`;
|
|
743
|
-
trace.push({ agent: step.agent, session_id: createRes.data.id, task_type: taskType, model:
|
|
744
|
-
recordRun(context.directory, step.agent,
|
|
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);
|
|
745
793
|
if (args.abort_on_failure) {
|
|
746
794
|
aborted = true;
|
|
747
795
|
break;
|
|
@@ -751,8 +799,8 @@ ${step.prompt}` : step.prompt;
|
|
|
751
799
|
const info = promptRes.data?.info;
|
|
752
800
|
if (info?.error) {
|
|
753
801
|
const errMsg = `Agent error: ${JSON.stringify(info.error)}`;
|
|
754
|
-
trace.push({ agent: step.agent, session_id: createRes.data.id, task_type: taskType, model:
|
|
755
|
-
recordRun(context.directory, step.agent,
|
|
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);
|
|
756
804
|
if (args.abort_on_failure) {
|
|
757
805
|
aborted = true;
|
|
758
806
|
break;
|
|
@@ -760,8 +808,8 @@ ${step.prompt}` : step.prompt;
|
|
|
760
808
|
continue;
|
|
761
809
|
}
|
|
762
810
|
const output = extractText(promptRes.data?.parts ?? []);
|
|
763
|
-
trace.push({ agent: step.agent, session_id: createRes.data.id, task_type: taskType, model:
|
|
764
|
-
recordRun(context.directory, step.agent,
|
|
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);
|
|
765
813
|
carryContext = output;
|
|
766
814
|
}
|
|
767
815
|
} finally {
|
|
@@ -795,7 +843,6 @@ function createDelegateTool(client) {
|
|
|
795
843
|
async execute(args, context) {
|
|
796
844
|
const startTime = Date.now();
|
|
797
845
|
const taskType = normalizeTaskType(args.task_type, args.agent);
|
|
798
|
-
const routing = routeModel(context.directory, taskType);
|
|
799
846
|
const retryAttempts = typeof args.retry_attempts === "number" ? args.retry_attempts : 1;
|
|
800
847
|
const maxRetries = Math.max(0, Math.floor(retryAttempts));
|
|
801
848
|
const createRes = await client.session.create({
|
|
@@ -829,7 +876,6 @@ ${args.prompt}` : args.prompt;
|
|
|
829
876
|
path: { id: childId },
|
|
830
877
|
body: {
|
|
831
878
|
agent: args.agent,
|
|
832
|
-
model: routing.model,
|
|
833
879
|
parts: [{ type: "text", text: fullPrompt }],
|
|
834
880
|
tools: { question: false }
|
|
835
881
|
},
|
|
@@ -840,41 +886,41 @@ ${args.prompt}` : args.prompt;
|
|
|
840
886
|
retriesUsed++;
|
|
841
887
|
}
|
|
842
888
|
if (!promptRes || promptRes.error) {
|
|
843
|
-
recordRun(context.directory, args.agent,
|
|
889
|
+
recordRun(context.directory, args.agent, "", taskType, false, Date.now() - startTime);
|
|
844
890
|
return JSON.stringify({
|
|
845
891
|
agent: args.agent,
|
|
846
892
|
session_id: childId,
|
|
847
893
|
success: false,
|
|
848
894
|
error: `Prompt failed: ${promptRes?.error?.detail ?? "unknown"}`,
|
|
849
895
|
task_type: taskType,
|
|
850
|
-
model:
|
|
896
|
+
model: "",
|
|
851
897
|
retries_used: retriesUsed,
|
|
852
898
|
duration_ms: Date.now() - startTime
|
|
853
899
|
});
|
|
854
900
|
}
|
|
855
901
|
const info = promptRes.data?.info;
|
|
856
902
|
if (info?.error) {
|
|
857
|
-
recordRun(context.directory, args.agent,
|
|
903
|
+
recordRun(context.directory, args.agent, "", taskType, false, Date.now() - startTime);
|
|
858
904
|
return JSON.stringify({
|
|
859
905
|
agent: args.agent,
|
|
860
906
|
session_id: childId,
|
|
861
907
|
success: false,
|
|
862
908
|
error: `Agent error: ${JSON.stringify(info.error)}`,
|
|
863
909
|
task_type: taskType,
|
|
864
|
-
model:
|
|
910
|
+
model: "",
|
|
865
911
|
retries_used: retriesUsed,
|
|
866
912
|
duration_ms: Date.now() - startTime
|
|
867
913
|
});
|
|
868
914
|
}
|
|
869
915
|
const output = extractText2(promptRes.data?.parts ?? []);
|
|
870
|
-
recordRun(context.directory, args.agent,
|
|
916
|
+
recordRun(context.directory, args.agent, "", taskType, true, Date.now() - startTime);
|
|
871
917
|
return JSON.stringify({
|
|
872
918
|
agent: args.agent,
|
|
873
919
|
session_id: childId,
|
|
874
920
|
success: true,
|
|
875
921
|
output: output || "(no text output)",
|
|
876
922
|
task_type: taskType,
|
|
877
|
-
model:
|
|
923
|
+
model: "",
|
|
878
924
|
retries_used: retriesUsed,
|
|
879
925
|
duration_ms: Date.now() - startTime
|
|
880
926
|
});
|
|
@@ -884,28 +930,28 @@ ${args.prompt}` : args.prompt;
|
|
|
884
930
|
|
|
885
931
|
// src/tools/repo-memory.ts
|
|
886
932
|
import { tool as tool6 } from "@opencode-ai/plugin";
|
|
887
|
-
import { readFileSync as
|
|
888
|
-
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";
|
|
889
935
|
var MEMORY_FILE = "MEMORY.json";
|
|
890
936
|
function memoryPath(directory) {
|
|
891
|
-
return
|
|
937
|
+
return join6(codebaseDir(directory), MEMORY_FILE);
|
|
892
938
|
}
|
|
893
939
|
function emptyMemory() {
|
|
894
940
|
return { version: "1.0", last_updated: new Date().toISOString(), nodes: {} };
|
|
895
941
|
}
|
|
896
942
|
function readMemory(directory) {
|
|
897
943
|
const p = memoryPath(directory);
|
|
898
|
-
if (!
|
|
944
|
+
if (!existsSync6(p))
|
|
899
945
|
return emptyMemory();
|
|
900
946
|
try {
|
|
901
|
-
return JSON.parse(
|
|
947
|
+
return JSON.parse(readFileSync6(p, "utf-8"));
|
|
902
948
|
} catch {
|
|
903
949
|
return emptyMemory();
|
|
904
950
|
}
|
|
905
951
|
}
|
|
906
952
|
function writeMemory(directory, memory) {
|
|
907
953
|
const base = codebaseDir(directory);
|
|
908
|
-
if (!
|
|
954
|
+
if (!existsSync6(base))
|
|
909
955
|
mkdirSync3(base, { recursive: true });
|
|
910
956
|
memory.last_updated = new Date().toISOString();
|
|
911
957
|
writeFileSync6(memoryPath(directory), JSON.stringify(memory, null, 2), "utf-8");
|
|
@@ -985,25 +1031,25 @@ var repoMemoryTool = tool6({
|
|
|
985
1031
|
|
|
986
1032
|
// src/tools/failure-replay.ts
|
|
987
1033
|
import { tool as tool7 } from "@opencode-ai/plugin";
|
|
988
|
-
import { readFileSync as
|
|
989
|
-
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";
|
|
990
1036
|
var FAILURES_FILE = "FAILURES.json";
|
|
991
1037
|
function failuresPath(directory) {
|
|
992
|
-
return
|
|
1038
|
+
return join7(codebaseDir(directory), FAILURES_FILE);
|
|
993
1039
|
}
|
|
994
1040
|
function readStore(directory) {
|
|
995
1041
|
const p = failuresPath(directory);
|
|
996
|
-
if (!
|
|
1042
|
+
if (!existsSync7(p))
|
|
997
1043
|
return { version: "1.0", last_updated: new Date().toISOString(), entries: [] };
|
|
998
1044
|
try {
|
|
999
|
-
return JSON.parse(
|
|
1045
|
+
return JSON.parse(readFileSync7(p, "utf-8"));
|
|
1000
1046
|
} catch {
|
|
1001
1047
|
return { version: "1.0", last_updated: new Date().toISOString(), entries: [] };
|
|
1002
1048
|
}
|
|
1003
1049
|
}
|
|
1004
1050
|
function writeStore(directory, store) {
|
|
1005
1051
|
const base = codebaseDir(directory);
|
|
1006
|
-
if (!
|
|
1052
|
+
if (!existsSync7(base))
|
|
1007
1053
|
mkdirSync4(base, { recursive: true });
|
|
1008
1054
|
store.last_updated = new Date().toISOString();
|
|
1009
1055
|
writeFileSync7(failuresPath(directory), JSON.stringify(store, null, 2), "utf-8");
|
|
@@ -1090,17 +1136,17 @@ var failureReplayTool = tool7({
|
|
|
1090
1136
|
|
|
1091
1137
|
// src/tools/decision-trace.ts
|
|
1092
1138
|
import { tool as tool8 } from "@opencode-ai/plugin";
|
|
1093
|
-
import { readFileSync as
|
|
1094
|
-
import { join as
|
|
1139
|
+
import { readFileSync as readFileSync8, existsSync as existsSync8, mkdirSync as mkdirSync5, appendFileSync } from "fs";
|
|
1140
|
+
import { join as join8 } from "path";
|
|
1095
1141
|
var DECISIONS_FILE = "DECISIONS.jsonl";
|
|
1096
1142
|
function decisionsPath(directory) {
|
|
1097
|
-
return
|
|
1143
|
+
return join8(codebaseDir(directory), DECISIONS_FILE);
|
|
1098
1144
|
}
|
|
1099
1145
|
function readDecisions(directory) {
|
|
1100
1146
|
const p = decisionsPath(directory);
|
|
1101
|
-
if (!
|
|
1147
|
+
if (!existsSync8(p))
|
|
1102
1148
|
return [];
|
|
1103
|
-
return
|
|
1149
|
+
return readFileSync8(p, "utf-8").split(`
|
|
1104
1150
|
`).filter((l) => l.trim()).map((l) => {
|
|
1105
1151
|
try {
|
|
1106
1152
|
return JSON.parse(l);
|
|
@@ -1140,7 +1186,7 @@ var decisionTraceTool = tool8({
|
|
|
1140
1186
|
case "record": {
|
|
1141
1187
|
if (!args.entry)
|
|
1142
1188
|
return JSON.stringify({ error: "entry required" });
|
|
1143
|
-
if (!
|
|
1189
|
+
if (!existsSync8(base))
|
|
1144
1190
|
mkdirSync5(base, { recursive: true });
|
|
1145
1191
|
const entry = { ...args.entry, timestamp: new Date().toISOString() };
|
|
1146
1192
|
appendFileSync(decisionsPath(dir), JSON.stringify(entry) + `
|
|
@@ -1175,25 +1221,25 @@ var decisionTraceTool = tool8({
|
|
|
1175
1221
|
|
|
1176
1222
|
// src/tools/volatility-map.ts
|
|
1177
1223
|
import { tool as tool9 } from "@opencode-ai/plugin";
|
|
1178
|
-
import { readFileSync as
|
|
1179
|
-
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";
|
|
1180
1226
|
var VOLATILITY_FILE = "VOLATILITY.json";
|
|
1181
1227
|
function volatilityPath(directory) {
|
|
1182
|
-
return
|
|
1228
|
+
return join9(codebaseDir(directory), VOLATILITY_FILE);
|
|
1183
1229
|
}
|
|
1184
1230
|
function readStore2(directory) {
|
|
1185
1231
|
const p = volatilityPath(directory);
|
|
1186
|
-
if (!
|
|
1232
|
+
if (!existsSync9(p))
|
|
1187
1233
|
return { version: "1.0", last_updated: new Date().toISOString(), generated_at: new Date().toISOString(), entries: [] };
|
|
1188
1234
|
try {
|
|
1189
|
-
return JSON.parse(
|
|
1235
|
+
return JSON.parse(readFileSync9(p, "utf-8"));
|
|
1190
1236
|
} catch {
|
|
1191
1237
|
return { version: "1.0", last_updated: new Date().toISOString(), generated_at: new Date().toISOString(), entries: [] };
|
|
1192
1238
|
}
|
|
1193
1239
|
}
|
|
1194
1240
|
function writeStore2(directory, store) {
|
|
1195
1241
|
const base = codebaseDir(directory);
|
|
1196
|
-
if (!
|
|
1242
|
+
if (!existsSync9(base))
|
|
1197
1243
|
mkdirSync6(base, { recursive: true });
|
|
1198
1244
|
store.last_updated = new Date().toISOString();
|
|
1199
1245
|
writeFileSync9(volatilityPath(directory), JSON.stringify(store, null, 2), "utf-8");
|
|
@@ -1283,25 +1329,25 @@ var volatilityMapTool = tool9({
|
|
|
1283
1329
|
|
|
1284
1330
|
// src/tools/policy-engine.ts
|
|
1285
1331
|
import { tool as tool10 } from "@opencode-ai/plugin";
|
|
1286
|
-
import { readFileSync as
|
|
1287
|
-
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";
|
|
1288
1334
|
var POLICIES_FILE = "POLICIES.json";
|
|
1289
1335
|
function policiesPath(directory) {
|
|
1290
|
-
return
|
|
1336
|
+
return join10(codebaseDir(directory), POLICIES_FILE);
|
|
1291
1337
|
}
|
|
1292
1338
|
function readStore3(directory) {
|
|
1293
1339
|
const p = policiesPath(directory);
|
|
1294
|
-
if (!
|
|
1340
|
+
if (!existsSync10(p))
|
|
1295
1341
|
return { version: "1.0", last_updated: new Date().toISOString(), policies: [] };
|
|
1296
1342
|
try {
|
|
1297
|
-
return JSON.parse(
|
|
1343
|
+
return JSON.parse(readFileSync10(p, "utf-8"));
|
|
1298
1344
|
} catch {
|
|
1299
1345
|
return { version: "1.0", last_updated: new Date().toISOString(), policies: [] };
|
|
1300
1346
|
}
|
|
1301
1347
|
}
|
|
1302
1348
|
function writeStore3(directory, store) {
|
|
1303
1349
|
const base = codebaseDir(directory);
|
|
1304
|
-
if (!
|
|
1350
|
+
if (!existsSync10(base))
|
|
1305
1351
|
mkdirSync7(base, { recursive: true });
|
|
1306
1352
|
store.last_updated = new Date().toISOString();
|
|
1307
1353
|
writeFileSync10(policiesPath(directory), JSON.stringify(store, null, 2), "utf-8");
|
|
@@ -1386,7 +1432,7 @@ var policyEngineTool = tool10({
|
|
|
1386
1432
|
|
|
1387
1433
|
// src/tools/hash-edit.ts
|
|
1388
1434
|
import { tool as tool11 } from "@opencode-ai/plugin";
|
|
1389
|
-
import { readFileSync as
|
|
1435
|
+
import { readFileSync as readFileSync11, writeFileSync as writeFileSync11 } from "fs";
|
|
1390
1436
|
import { createHash } from "crypto";
|
|
1391
1437
|
var hashEditTool = tool11({
|
|
1392
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.",
|
|
@@ -1400,7 +1446,7 @@ var hashEditTool = tool11({
|
|
|
1400
1446
|
const fullPath = args.filePath.startsWith("/") ? args.filePath : `${context.directory}/${args.filePath}`;
|
|
1401
1447
|
let content;
|
|
1402
1448
|
try {
|
|
1403
|
-
content =
|
|
1449
|
+
content = readFileSync11(fullPath, "utf-8");
|
|
1404
1450
|
} catch (e) {
|
|
1405
1451
|
return `Error: Could not read file ${args.filePath}`;
|
|
1406
1452
|
}
|
|
@@ -1421,8 +1467,8 @@ var hashEditTool = tool11({
|
|
|
1421
1467
|
|
|
1422
1468
|
// src/tools/council.ts
|
|
1423
1469
|
import { tool as tool12 } from "@opencode-ai/plugin";
|
|
1424
|
-
import { appendFileSync as appendFileSync2, existsSync as
|
|
1425
|
-
import { join as
|
|
1470
|
+
import { appendFileSync as appendFileSync2, existsSync as existsSync11, mkdirSync as mkdirSync8 } from "fs";
|
|
1471
|
+
import { join as join11 } from "path";
|
|
1426
1472
|
function createCouncilTool(client) {
|
|
1427
1473
|
return tool12({
|
|
1428
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.",
|
|
@@ -1431,7 +1477,7 @@ function createCouncilTool(client) {
|
|
|
1431
1477
|
agents: tool12.schema.array(tool12.schema.string()).optional()
|
|
1432
1478
|
},
|
|
1433
1479
|
async execute(args, context) {
|
|
1434
|
-
const agents = args.agents || ["architect", "reviewer", "coder"];
|
|
1480
|
+
const agents = args.agents || ["architect", "reviewer", "backend-coder"];
|
|
1435
1481
|
const tasks = agents.map((agent) => ({
|
|
1436
1482
|
agent,
|
|
1437
1483
|
prompt: `TASK: ${args.task}
|
|
@@ -1493,9 +1539,9 @@ Please synthesize these results. Identify areas of agreement, resolve conflicts,
|
|
|
1493
1539
|
function persistCouncilResult(directory, payload) {
|
|
1494
1540
|
try {
|
|
1495
1541
|
const base = codebaseDir(directory);
|
|
1496
|
-
if (!
|
|
1542
|
+
if (!existsSync11(base))
|
|
1497
1543
|
mkdirSync8(base, { recursive: true });
|
|
1498
|
-
const path =
|
|
1544
|
+
const path = join11(base, "COUNCILS.jsonl");
|
|
1499
1545
|
appendFileSync2(path, JSON.stringify(payload) + `
|
|
1500
1546
|
`, "utf-8");
|
|
1501
1547
|
} catch {}
|
|
@@ -1503,8 +1549,8 @@ function persistCouncilResult(directory, payload) {
|
|
|
1503
1549
|
|
|
1504
1550
|
// src/tools/context-generator.ts
|
|
1505
1551
|
import { tool as tool13 } from "@opencode-ai/plugin";
|
|
1506
|
-
import { writeFileSync as writeFileSync12, existsSync as
|
|
1507
|
-
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";
|
|
1508
1554
|
var contextGeneratorTool = tool13({
|
|
1509
1555
|
description: "Auto-generate or update hierarchical context files (AGENTS.md, CLAUDE.md) throughout the project. These files provide critical grounding for AI agents.",
|
|
1510
1556
|
args: {
|
|
@@ -1513,20 +1559,20 @@ var contextGeneratorTool = tool13({
|
|
|
1513
1559
|
},
|
|
1514
1560
|
async execute(args, context) {
|
|
1515
1561
|
const root = context.directory;
|
|
1516
|
-
const target = args.targetDir ?
|
|
1517
|
-
if (!
|
|
1562
|
+
const target = args.targetDir ? join12(root, args.targetDir) : root;
|
|
1563
|
+
if (!existsSync12(target)) {
|
|
1518
1564
|
return `Error: Directory ${target} does not exist.`;
|
|
1519
1565
|
}
|
|
1520
|
-
const agentsMdPath =
|
|
1521
|
-
if (
|
|
1566
|
+
const agentsMdPath = join12(target, "AGENTS.md");
|
|
1567
|
+
if (existsSync12(agentsMdPath) && !args.force) {
|
|
1522
1568
|
return `AGENTS.md already exists in ${target}. Use force: true to overwrite.`;
|
|
1523
1569
|
}
|
|
1524
|
-
const pkgPath =
|
|
1570
|
+
const pkgPath = join12(root, "package.json");
|
|
1525
1571
|
let projectName = "Project";
|
|
1526
1572
|
let techStack = "Unknown";
|
|
1527
|
-
if (
|
|
1573
|
+
if (existsSync12(pkgPath)) {
|
|
1528
1574
|
try {
|
|
1529
|
-
const pkg = JSON.parse(
|
|
1575
|
+
const pkg = JSON.parse(readFileSync12(pkgPath, "utf-8"));
|
|
1530
1576
|
projectName = pkg.name || projectName;
|
|
1531
1577
|
techStack = Object.keys(pkg.dependencies || {}).slice(0, 5).join(", ");
|
|
1532
1578
|
} catch {}
|
|
@@ -1544,7 +1590,7 @@ var contextGeneratorTool = tool13({
|
|
|
1544
1590
|
|
|
1545
1591
|
## Directory Map
|
|
1546
1592
|
${readdirSync2(target).slice(0, 10).map((f) => {
|
|
1547
|
-
const s = statSync(
|
|
1593
|
+
const s = statSync(join12(target, f));
|
|
1548
1594
|
return `- \`${f}\`${s.isDirectory() ? "/" : ""} : [Description]`;
|
|
1549
1595
|
}).join(`
|
|
1550
1596
|
`)}
|
|
@@ -1559,10 +1605,10 @@ Generated by FlowDeck Context Generator.
|
|
|
1559
1605
|
|
|
1560
1606
|
// src/tools/create-skill.ts
|
|
1561
1607
|
import { tool as tool14 } from "@opencode-ai/plugin";
|
|
1562
|
-
import { mkdirSync as mkdirSync9, writeFileSync as writeFileSync13, existsSync as
|
|
1563
|
-
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";
|
|
1564
1610
|
import { fileURLToPath } from "url";
|
|
1565
|
-
var SKILLS_DIR =
|
|
1611
|
+
var SKILLS_DIR = join13(dirname3(fileURLToPath(import.meta.url)), "..", "skills");
|
|
1566
1612
|
var createSkillTool = tool14({
|
|
1567
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.",
|
|
1568
1614
|
args: {
|
|
@@ -1572,9 +1618,9 @@ var createSkillTool = tool14({
|
|
|
1572
1618
|
tags: tool14.schema.array(tool14.schema.string()).optional().describe("Optional tags for categorisation, e.g. ['performance', 'typescript']")
|
|
1573
1619
|
},
|
|
1574
1620
|
async execute(args) {
|
|
1575
|
-
const skillDir =
|
|
1576
|
-
const skillFile =
|
|
1577
|
-
if (
|
|
1621
|
+
const skillDir = join13(SKILLS_DIR, args.name);
|
|
1622
|
+
const skillFile = join13(skillDir, "SKILL.md");
|
|
1623
|
+
if (existsSync13(skillFile)) {
|
|
1578
1624
|
return `Skill '${args.name}' already exists at ${skillFile}.
|
|
1579
1625
|
` + `Use a different name or delete the existing skill directory first.`;
|
|
1580
1626
|
}
|
|
@@ -1602,8 +1648,8 @@ origin: FlowDeck (self-learned)${tagLine}
|
|
|
1602
1648
|
|
|
1603
1649
|
// src/tools/reflect.ts
|
|
1604
1650
|
import { tool as tool15 } from "@opencode-ai/plugin";
|
|
1605
|
-
import { existsSync as
|
|
1606
|
-
import { join as
|
|
1651
|
+
import { existsSync as existsSync14, readFileSync as readFileSync13 } from "fs";
|
|
1652
|
+
import { join as join14 } from "path";
|
|
1607
1653
|
var MAX_ARTIFACT_BYTES = 4000;
|
|
1608
1654
|
function tail(text, maxBytes) {
|
|
1609
1655
|
if (text.length <= maxBytes)
|
|
@@ -1632,11 +1678,11 @@ var reflectTool = tool15({
|
|
|
1632
1678
|
];
|
|
1633
1679
|
let found = 0;
|
|
1634
1680
|
for (const [rel, label] of ARTIFACT_PATHS) {
|
|
1635
|
-
const full =
|
|
1636
|
-
if (!
|
|
1681
|
+
const full = join14(root, rel);
|
|
1682
|
+
if (!existsSync14(full))
|
|
1637
1683
|
continue;
|
|
1638
1684
|
try {
|
|
1639
|
-
const raw =
|
|
1685
|
+
const raw = readFileSync13(full, "utf-8").trim();
|
|
1640
1686
|
if (!raw)
|
|
1641
1687
|
continue;
|
|
1642
1688
|
const count = raw.split(`
|
|
@@ -1660,26 +1706,37 @@ import { tool as tool16 } from "@opencode-ai/plugin";
|
|
|
1660
1706
|
|
|
1661
1707
|
// src/services/memory-store.ts
|
|
1662
1708
|
import { Database } from "bun:sqlite";
|
|
1663
|
-
import { existsSync as
|
|
1664
|
-
import { join as
|
|
1709
|
+
import { existsSync as existsSync15, mkdirSync as mkdirSync10 } from "fs";
|
|
1710
|
+
import { join as join15 } from "path";
|
|
1665
1711
|
import { homedir } from "os";
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
function ensureDir() {
|
|
1669
|
-
if (!existsSync16(MEMORY_DIR)) {
|
|
1670
|
-
mkdirSync10(MEMORY_DIR, { recursive: true });
|
|
1671
|
-
}
|
|
1712
|
+
function resolveMemoryDir() {
|
|
1713
|
+
return process.env.FLOWDECK_MEMORY_DIR ?? join15(homedir(), ".flowdeck-memory");
|
|
1672
1714
|
}
|
|
1715
|
+
var JS_RETRY_COUNT = 3;
|
|
1716
|
+
var JS_RETRY_BASE_MS = 50;
|
|
1673
1717
|
var db = null;
|
|
1718
|
+
function debugLog(msg) {
|
|
1719
|
+
if (process.env.FLOWDECK_MEMORY_DEBUG) {
|
|
1720
|
+
console.error(`[FlowDeck Memory] ${msg}`);
|
|
1721
|
+
}
|
|
1722
|
+
}
|
|
1674
1723
|
function getDb() {
|
|
1675
1724
|
if (!db) {
|
|
1676
|
-
|
|
1677
|
-
|
|
1725
|
+
const dir = resolveMemoryDir();
|
|
1726
|
+
if (!existsSync15(dir))
|
|
1727
|
+
mkdirSync10(dir, { recursive: true });
|
|
1728
|
+
const dbPath = join15(dir, "memory.db");
|
|
1729
|
+
db = new Database(dbPath);
|
|
1730
|
+
debugLog(`DB opened: ${dbPath}`);
|
|
1678
1731
|
initializeSchema(db);
|
|
1679
1732
|
}
|
|
1680
1733
|
return db;
|
|
1681
1734
|
}
|
|
1682
1735
|
function initializeSchema(database) {
|
|
1736
|
+
database.run("PRAGMA journal_mode = WAL");
|
|
1737
|
+
database.run("PRAGMA busy_timeout = 5000");
|
|
1738
|
+
database.run("PRAGMA synchronous = NORMAL");
|
|
1739
|
+
database.run("PRAGMA wal_autocheckpoint = 1000");
|
|
1683
1740
|
const schema = `
|
|
1684
1741
|
CREATE TABLE IF NOT EXISTS sessions (
|
|
1685
1742
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
@@ -1707,6 +1764,7 @@ function initializeSchema(database) {
|
|
|
1707
1764
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1708
1765
|
session_id INTEGER NOT NULL UNIQUE,
|
|
1709
1766
|
content TEXT NOT NULL,
|
|
1767
|
+
metadata TEXT,
|
|
1710
1768
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1711
1769
|
FOREIGN KEY (session_id) REFERENCES sessions(id)
|
|
1712
1770
|
);
|
|
@@ -1718,6 +1776,49 @@ function initializeSchema(database) {
|
|
|
1718
1776
|
CREATE INDEX IF NOT EXISTS idx_sessions_directory ON sessions(directory);
|
|
1719
1777
|
`;
|
|
1720
1778
|
database.run(schema);
|
|
1779
|
+
const summaryColumns = database.prepare("PRAGMA table_info(summaries)").all().map((c) => c.name);
|
|
1780
|
+
if (!summaryColumns.includes("metadata")) {
|
|
1781
|
+
database.run("ALTER TABLE summaries ADD COLUMN metadata TEXT");
|
|
1782
|
+
debugLog("Migrated summaries table: added metadata column");
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1785
|
+
function isBusyError(err) {
|
|
1786
|
+
if (!err || typeof err !== "object")
|
|
1787
|
+
return false;
|
|
1788
|
+
const e = err;
|
|
1789
|
+
return e.code === "SQLITE_BUSY" || (e.message?.includes("database is locked") ?? false);
|
|
1790
|
+
}
|
|
1791
|
+
function sleepSync(ms) {
|
|
1792
|
+
try {
|
|
1793
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
1794
|
+
} catch {
|
|
1795
|
+
const end = Date.now() + ms;
|
|
1796
|
+
while (Date.now() < end) {}
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
function executeWrite(fn, context) {
|
|
1800
|
+
for (let attempt = 0;attempt <= JS_RETRY_COUNT; attempt++) {
|
|
1801
|
+
const start = Date.now();
|
|
1802
|
+
try {
|
|
1803
|
+
const result = fn();
|
|
1804
|
+
const duration = Date.now() - start;
|
|
1805
|
+
if (attempt > 0) {
|
|
1806
|
+
debugLog(`${context}: succeeded after ${attempt} JS retr${attempt === 1 ? "y" : "ies"} (${duration}ms)`);
|
|
1807
|
+
} else {
|
|
1808
|
+
debugLog(`${context}: completed in ${duration}ms`);
|
|
1809
|
+
}
|
|
1810
|
+
return result;
|
|
1811
|
+
} catch (err) {
|
|
1812
|
+
if (isBusyError(err) && attempt < JS_RETRY_COUNT) {
|
|
1813
|
+
const delay = JS_RETRY_BASE_MS * (attempt + 1);
|
|
1814
|
+
debugLog(`${context}: SQLITE_BUSY — JS retry ${attempt + 1}/${JS_RETRY_COUNT} after ${delay}ms`);
|
|
1815
|
+
sleepSync(delay);
|
|
1816
|
+
continue;
|
|
1817
|
+
}
|
|
1818
|
+
throw err;
|
|
1819
|
+
}
|
|
1820
|
+
}
|
|
1821
|
+
throw new Error(`${context}: exhausted all retries`);
|
|
1721
1822
|
}
|
|
1722
1823
|
function serializeToolInput(input) {
|
|
1723
1824
|
if (!input)
|
|
@@ -1745,7 +1846,7 @@ function initSession(contentSessionId, project, directory) {
|
|
|
1745
1846
|
database.prepare("UPDATE sessions SET last_active_at = ?, prompt_count = prompt_count + 1 WHERE id = ?").run(now, existing.id);
|
|
1746
1847
|
return { ...existing, last_active_at: now, prompt_count: (existing.prompt_count || 0) + 1 };
|
|
1747
1848
|
}
|
|
1748
|
-
const result = database.prepare("INSERT INTO sessions (content_session_id, project, directory, created_at, last_active_at) VALUES (?, ?, ?, ?, ?)").run(contentSessionId, project, directory, now, now);
|
|
1849
|
+
const result = database.prepare("INSERT INTO sessions (content_session_id, project, directory, created_at, last_active_at, prompt_count) VALUES (?, ?, ?, ?, ?, ?)").run(contentSessionId, project, directory, now, now, 1);
|
|
1749
1850
|
return {
|
|
1750
1851
|
id: result.lastInsertRowid,
|
|
1751
1852
|
content_session_id: contentSessionId,
|
|
@@ -1759,27 +1860,38 @@ function initSession(contentSessionId, project, directory) {
|
|
|
1759
1860
|
function storeObservation(sessionId, toolName, toolInput, toolResponse, directory) {
|
|
1760
1861
|
const database = getDb();
|
|
1761
1862
|
const now = new Date().toISOString();
|
|
1762
|
-
const
|
|
1763
|
-
|
|
1863
|
+
const serializedInput = serializeToolInput(toolInput);
|
|
1864
|
+
const truncatedResponse = toolResponse ? toolResponse.slice(0, 1e4) : null;
|
|
1865
|
+
const result = executeWrite(database.transaction(() => {
|
|
1866
|
+
const r = database.prepare("INSERT INTO observations (session_id, tool_name, tool_input, tool_response, directory, created_at) VALUES (?, ?, ?, ?, ?, ?)").run(sessionId, toolName, serializedInput, truncatedResponse, directory, now);
|
|
1867
|
+
database.prepare("UPDATE sessions SET last_active_at = ? WHERE id = ?").run(now, sessionId);
|
|
1868
|
+
return r;
|
|
1869
|
+
}), `storeObservation(${toolName})`);
|
|
1764
1870
|
return {
|
|
1765
1871
|
id: result.lastInsertRowid,
|
|
1766
1872
|
session_id: sessionId,
|
|
1767
1873
|
tool_name: toolName,
|
|
1768
|
-
tool_input: parseToolInput(
|
|
1769
|
-
tool_response:
|
|
1874
|
+
tool_input: parseToolInput(serializedInput),
|
|
1875
|
+
tool_response: truncatedResponse,
|
|
1770
1876
|
directory,
|
|
1771
1877
|
created_at: now
|
|
1772
1878
|
};
|
|
1773
1879
|
}
|
|
1774
|
-
function storeSummary(sessionId, content) {
|
|
1880
|
+
function storeSummary(sessionId, content, metadata) {
|
|
1775
1881
|
const database = getDb();
|
|
1776
1882
|
const now = new Date().toISOString();
|
|
1777
|
-
|
|
1778
|
-
|
|
1883
|
+
const serializedMetadata = metadata ? JSON.stringify(metadata) : null;
|
|
1884
|
+
const id = executeWrite(database.transaction(() => {
|
|
1885
|
+
database.prepare("INSERT OR REPLACE INTO summaries (session_id, content, metadata, created_at) VALUES (?, ?, ?, ?)").run(sessionId, content, serializedMetadata, now);
|
|
1886
|
+
database.prepare("UPDATE sessions SET summary = ? WHERE id = ?").run(content.slice(0, 2000), sessionId);
|
|
1887
|
+
return database.prepare("SELECT last_insert_rowid() as id").get().id;
|
|
1888
|
+
}), `storeSummary(session=${sessionId})`);
|
|
1889
|
+
debugLog(`storeSummary: wrote ${content.length} chars${metadata ? ` + ${JSON.stringify(metadata).length}B metadata` : ""} for session ${sessionId}`);
|
|
1779
1890
|
return {
|
|
1780
|
-
id
|
|
1891
|
+
id,
|
|
1781
1892
|
session_id: sessionId,
|
|
1782
1893
|
content,
|
|
1894
|
+
metadata: metadata ?? null,
|
|
1783
1895
|
created_at: now
|
|
1784
1896
|
};
|
|
1785
1897
|
}
|
|
@@ -1798,6 +1910,20 @@ function getObservationsForSession(sessionId) {
|
|
|
1798
1910
|
tool_input: parseToolInput(obs.tool_input)
|
|
1799
1911
|
}));
|
|
1800
1912
|
}
|
|
1913
|
+
function getSessionSummary(sessionId) {
|
|
1914
|
+
const database = getDb();
|
|
1915
|
+
const row = database.prepare("SELECT * FROM summaries WHERE session_id = ?").get(sessionId);
|
|
1916
|
+
if (!row)
|
|
1917
|
+
return null;
|
|
1918
|
+
return {
|
|
1919
|
+
...row,
|
|
1920
|
+
metadata: row.metadata ? JSON.parse(row.metadata) : null
|
|
1921
|
+
};
|
|
1922
|
+
}
|
|
1923
|
+
function getSessionByContentSessionId(contentSessionId) {
|
|
1924
|
+
const database = getDb();
|
|
1925
|
+
return database.prepare("SELECT * FROM sessions WHERE content_session_id = ?").get(contentSessionId) || null;
|
|
1926
|
+
}
|
|
1801
1927
|
function getRecentObservations(directory, limit = 50) {
|
|
1802
1928
|
const database = getDb();
|
|
1803
1929
|
const rows = database.prepare(`SELECT o.*, s.project, s.content_session_id, s.created_at as session_created
|
|
@@ -1859,11 +1985,15 @@ function getContextForDirectory(directory, maxObservations = 20) {
|
|
|
1859
1985
|
lines.push(`Output: ${preview}${observation.tool_response.length > 300 ? "..." : ""}`);
|
|
1860
1986
|
}
|
|
1861
1987
|
}
|
|
1862
|
-
const
|
|
1988
|
+
const summaryRows = getDb().prepare(`SELECT su.* FROM summaries su
|
|
1863
1989
|
JOIN sessions s ON su.session_id = s.id
|
|
1864
1990
|
WHERE s.directory = ?
|
|
1865
1991
|
ORDER BY su.created_at DESC
|
|
1866
1992
|
LIMIT 3`).all(directory);
|
|
1993
|
+
const summaries = summaryRows.map((r) => ({
|
|
1994
|
+
...r,
|
|
1995
|
+
metadata: r.metadata ? JSON.parse(r.metadata) : null
|
|
1996
|
+
}));
|
|
1867
1997
|
if (summaries.length > 0) {
|
|
1868
1998
|
lines.push(`
|
|
1869
1999
|
## Session Summaries`);
|
|
@@ -1871,12 +2001,20 @@ function getContextForDirectory(directory, maxObservations = 20) {
|
|
|
1871
2001
|
const date = sum.created_at ? new Date(sum.created_at).toLocaleDateString() : "unknown";
|
|
1872
2002
|
lines.push(`
|
|
1873
2003
|
### [${date}]`);
|
|
1874
|
-
lines.push(sum.content.slice(0,
|
|
2004
|
+
lines.push(sum.content.slice(0, 2000));
|
|
1875
2005
|
}
|
|
1876
2006
|
}
|
|
1877
2007
|
return lines.join(`
|
|
1878
2008
|
`);
|
|
1879
2009
|
}
|
|
2010
|
+
function getDbSettings() {
|
|
2011
|
+
const database = getDb();
|
|
2012
|
+
const journalMode = database.prepare("PRAGMA journal_mode").get().journal_mode;
|
|
2013
|
+
const busyTimeout = database.prepare("PRAGMA busy_timeout").get().timeout;
|
|
2014
|
+
const synchronous = database.prepare("PRAGMA synchronous").get().synchronous;
|
|
2015
|
+
const walAutocheckpoint = database.prepare("PRAGMA wal_autocheckpoint").get().wal_autocheckpoint;
|
|
2016
|
+
return { journal_mode: journalMode, busy_timeout: busyTimeout, synchronous, wal_autocheckpoint: walAutocheckpoint };
|
|
2017
|
+
}
|
|
1880
2018
|
|
|
1881
2019
|
// src/tools/memory-search.ts
|
|
1882
2020
|
var memorySearchTool = tool16({
|
|
@@ -1896,8 +2034,14 @@ var memorySearchTool = tool16({
|
|
|
1896
2034
|
return JSON.stringify({ error: "Session not found", session_id: args.session_id });
|
|
1897
2035
|
}
|
|
1898
2036
|
const observations = getObservationsForSession(targetSession.id);
|
|
2037
|
+
const summary = getSessionSummary(targetSession.id);
|
|
1899
2038
|
return JSON.stringify({
|
|
1900
2039
|
session: targetSession,
|
|
2040
|
+
summary: summary ? {
|
|
2041
|
+
content: summary.content,
|
|
2042
|
+
metadata: summary.metadata,
|
|
2043
|
+
created_at: summary.created_at
|
|
2044
|
+
} : null,
|
|
1901
2045
|
observations: observations.map((o) => ({
|
|
1902
2046
|
tool_name: o.tool_name,
|
|
1903
2047
|
tool_input: o.tool_input,
|
|
@@ -1944,69 +2088,57 @@ var memorySearchTool = tool16({
|
|
|
1944
2088
|
|
|
1945
2089
|
// src/tools/memory-status.ts
|
|
1946
2090
|
import { tool as tool17 } from "@opencode-ai/plugin";
|
|
1947
|
-
import {
|
|
1948
|
-
import {
|
|
1949
|
-
import { join as join17 } from "path";
|
|
2091
|
+
import { existsSync as existsSync16 } from "fs";
|
|
2092
|
+
import { join as join16 } from "path";
|
|
1950
2093
|
import { homedir as homedir2 } from "os";
|
|
1951
|
-
|
|
2094
|
+
function resolveDbPath() {
|
|
2095
|
+
return join16(process.env.FLOWDECK_MEMORY_DIR ?? join16(homedir2(), ".flowdeck-memory"), "memory.db");
|
|
2096
|
+
}
|
|
1952
2097
|
var memoryStatusTool = tool17({
|
|
1953
2098
|
description: "Check FlowDeck memory database status, statistics, and recent sessions",
|
|
1954
2099
|
args: {},
|
|
1955
|
-
async execute(_args,
|
|
2100
|
+
async execute(_args, context) {
|
|
2101
|
+
const directory = context?.directory ?? process.cwd();
|
|
2102
|
+
const dbPath = resolveDbPath();
|
|
1956
2103
|
try {
|
|
1957
|
-
const exists =
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
if (exists) {
|
|
1965
|
-
try {
|
|
1966
|
-
const db2 = new Database2(DB_PATH2);
|
|
1967
|
-
const sessions = db2.prepare("SELECT COUNT(*) as count FROM sessions").get();
|
|
1968
|
-
const observations = db2.prepare("SELECT COUNT(*) as count FROM observations").get();
|
|
1969
|
-
const summaries = db2.prepare("SELECT COUNT(*) as count FROM summaries").get();
|
|
1970
|
-
const recentSessions = db2.prepare(`
|
|
1971
|
-
SELECT
|
|
1972
|
-
id,
|
|
1973
|
-
content_session_id,
|
|
1974
|
-
project,
|
|
1975
|
-
directory,
|
|
1976
|
-
created_at,
|
|
1977
|
-
last_active_at,
|
|
1978
|
-
prompt_count
|
|
1979
|
-
FROM sessions
|
|
1980
|
-
ORDER BY last_active_at DESC
|
|
1981
|
-
LIMIT 5
|
|
1982
|
-
`).all();
|
|
1983
|
-
result.statistics = {
|
|
1984
|
-
sessions: sessions.count,
|
|
1985
|
-
observations: observations.count,
|
|
1986
|
-
summaries: summaries.count,
|
|
1987
|
-
recent_sessions: recentSessions.map((s) => {
|
|
1988
|
-
const obsCount = db2.prepare("SELECT COUNT(*) as count FROM observations WHERE session_id = ?").get(s.id);
|
|
1989
|
-
return {
|
|
1990
|
-
project: s.project,
|
|
1991
|
-
directory: s.directory,
|
|
1992
|
-
observations_in_session: obsCount.count,
|
|
1993
|
-
last_active: s.last_active_at,
|
|
1994
|
-
prompt_count: s.prompt_count
|
|
1995
|
-
};
|
|
1996
|
-
})
|
|
1997
|
-
};
|
|
1998
|
-
db2.close();
|
|
1999
|
-
} catch (err) {
|
|
2000
|
-
result.status = "ERROR";
|
|
2001
|
-
result.statistics = { error: String(err) };
|
|
2002
|
-
}
|
|
2104
|
+
const exists = existsSync16(dbPath);
|
|
2105
|
+
if (!exists) {
|
|
2106
|
+
return JSON.stringify({
|
|
2107
|
+
database_exists: false,
|
|
2108
|
+
path: dbPath,
|
|
2109
|
+
status: "NOT_INITIALIZED"
|
|
2110
|
+
}, null, 2);
|
|
2003
2111
|
}
|
|
2004
|
-
|
|
2112
|
+
const settings = getDbSettings();
|
|
2113
|
+
const recentSessions = getRecentSessions(directory, 5);
|
|
2114
|
+
const sessionStats = recentSessions.map((s) => {
|
|
2115
|
+
const observations = getObservationsForSession(s.id);
|
|
2116
|
+
const summary = getSessionSummary(s.id);
|
|
2117
|
+
return {
|
|
2118
|
+
project: s.project,
|
|
2119
|
+
directory: s.directory,
|
|
2120
|
+
content_session_id: s.content_session_id,
|
|
2121
|
+
observations_in_session: observations.length,
|
|
2122
|
+
last_active: s.last_active_at,
|
|
2123
|
+
prompt_count: s.prompt_count,
|
|
2124
|
+
has_summary: !!summary,
|
|
2125
|
+
summary_length: summary?.content.length ?? 0,
|
|
2126
|
+
summary_preview: summary?.content.slice(0, 200) ?? null,
|
|
2127
|
+
handoff_metadata: summary?.metadata ?? null
|
|
2128
|
+
};
|
|
2129
|
+
});
|
|
2130
|
+
return JSON.stringify({
|
|
2131
|
+
database_exists: true,
|
|
2132
|
+
path: dbPath,
|
|
2133
|
+
status: "ACTIVE",
|
|
2134
|
+
pragma_settings: settings,
|
|
2135
|
+
recent_sessions_in_directory: sessionStats
|
|
2136
|
+
}, null, 2);
|
|
2005
2137
|
} catch (err) {
|
|
2006
2138
|
return JSON.stringify({
|
|
2007
2139
|
status: "ERROR",
|
|
2008
2140
|
error: String(err),
|
|
2009
|
-
path:
|
|
2141
|
+
path: dbPath
|
|
2010
2142
|
}, null, 2);
|
|
2011
2143
|
}
|
|
2012
2144
|
}
|
|
@@ -2014,7 +2146,7 @@ var memoryStatusTool = tool17({
|
|
|
2014
2146
|
|
|
2015
2147
|
// src/hooks/memory-hook.ts
|
|
2016
2148
|
var MAX_TOOL_RESPONSE = 1e4;
|
|
2017
|
-
var
|
|
2149
|
+
var MAX_SUMMARY_STORAGE = 50000;
|
|
2018
2150
|
var activeSessions = new Map;
|
|
2019
2151
|
function extractProjectFromDirectory(directory) {
|
|
2020
2152
|
const parts = directory.split("/");
|
|
@@ -2025,6 +2157,59 @@ function truncate(str, max) {
|
|
|
2025
2157
|
return str || "";
|
|
2026
2158
|
return str.slice(0, max);
|
|
2027
2159
|
}
|
|
2160
|
+
function buildHandoffMetadata(sessionId, directory, summaryText, observations) {
|
|
2161
|
+
const fileTools = new Set(["edit", "create", "view", "read", "hash-edit", "str-replace-editor"]);
|
|
2162
|
+
const importantFilesSet = new Set;
|
|
2163
|
+
for (const obs of observations) {
|
|
2164
|
+
if (fileTools.has(obs.tool_name) && obs.tool_input) {
|
|
2165
|
+
const path = obs.tool_input.path;
|
|
2166
|
+
if (path)
|
|
2167
|
+
importantFilesSet.add(path);
|
|
2168
|
+
}
|
|
2169
|
+
}
|
|
2170
|
+
const toolNamesUsed = [...new Set(observations.map((o) => o.tool_name).filter((t) => t !== "assistant_message"))];
|
|
2171
|
+
function extractBullets(text) {
|
|
2172
|
+
return text.split(`
|
|
2173
|
+
`).filter((l) => /^\s*[-*]/.test(l)).map((l) => l.replace(/^\s*[-*]\s+/, "").trim()).filter(Boolean);
|
|
2174
|
+
}
|
|
2175
|
+
const sections = {};
|
|
2176
|
+
let currentSection = "";
|
|
2177
|
+
const currentLines = [];
|
|
2178
|
+
for (const line of summaryText.split(`
|
|
2179
|
+
`)) {
|
|
2180
|
+
const header = line.match(/^##\s+\d+\.\s+(.+)/);
|
|
2181
|
+
if (header) {
|
|
2182
|
+
if (currentSection)
|
|
2183
|
+
sections[currentSection] = currentLines.join(`
|
|
2184
|
+
`).trim();
|
|
2185
|
+
currentSection = header[1].trim();
|
|
2186
|
+
currentLines.length = 0;
|
|
2187
|
+
} else {
|
|
2188
|
+
currentLines.push(line);
|
|
2189
|
+
}
|
|
2190
|
+
}
|
|
2191
|
+
if (currentSection)
|
|
2192
|
+
sections[currentSection] = currentLines.join(`
|
|
2193
|
+
`).trim();
|
|
2194
|
+
const completed = extractBullets(sections["Work Completed"] ?? "");
|
|
2195
|
+
const pending = extractBullets(sections["Remaining Tasks"] ?? "");
|
|
2196
|
+
return {
|
|
2197
|
+
workflow_name: extractProjectFromDirectory(directory),
|
|
2198
|
+
current_status: "compacted",
|
|
2199
|
+
current_stage: null,
|
|
2200
|
+
completed_stages: completed,
|
|
2201
|
+
pending_stages: pending,
|
|
2202
|
+
key_decisions: [],
|
|
2203
|
+
blockers: [],
|
|
2204
|
+
important_files: [...importantFilesSet].slice(0, 30),
|
|
2205
|
+
approvals: [],
|
|
2206
|
+
open_questions: [],
|
|
2207
|
+
next_steps: pending.slice(0, 5),
|
|
2208
|
+
tool_names_used: toolNamesUsed.slice(0, 20),
|
|
2209
|
+
observation_count: observations.length,
|
|
2210
|
+
updated_at: new Date().toISOString()
|
|
2211
|
+
};
|
|
2212
|
+
}
|
|
2028
2213
|
function onSessionCreated(directory, contentSessionId, prompt) {
|
|
2029
2214
|
const project = extractProjectFromDirectory(directory);
|
|
2030
2215
|
const session = initSession(contentSessionId, project, directory);
|
|
@@ -2049,7 +2234,11 @@ function onToolExecuted(contentSessionId, toolName, toolInput, toolResponse, dir
|
|
|
2049
2234
|
};
|
|
2050
2235
|
activeSessions.set(contentSessionId, ctx);
|
|
2051
2236
|
}
|
|
2052
|
-
|
|
2237
|
+
try {
|
|
2238
|
+
storeObservation(ctx.sessionId, truncate(toolName, 200), toolInput, toolResponse ? truncate(toolResponse, MAX_TOOL_RESPONSE) : null, directory);
|
|
2239
|
+
} catch (err) {
|
|
2240
|
+
console.warn(`[FlowDeck Memory] Failed to store observation for tool "${toolName}":`, err);
|
|
2241
|
+
}
|
|
2053
2242
|
}
|
|
2054
2243
|
function onMessageUpdated(contentSessionId, role, content, directory) {
|
|
2055
2244
|
if (role !== "assistant")
|
|
@@ -2068,33 +2257,64 @@ function onMessageUpdated(contentSessionId, role, content, directory) {
|
|
|
2068
2257
|
};
|
|
2069
2258
|
activeSessions.set(contentSessionId, ctx);
|
|
2070
2259
|
}
|
|
2071
|
-
|
|
2260
|
+
try {
|
|
2261
|
+
storeObservation(ctx.sessionId, "assistant_message", { role }, truncate(content, MAX_TOOL_RESPONSE), directory);
|
|
2262
|
+
} catch (err) {
|
|
2263
|
+
console.warn("[FlowDeck Memory] Failed to store assistant message observation:", err);
|
|
2264
|
+
}
|
|
2072
2265
|
}
|
|
2073
2266
|
function onSessionCompact(contentSessionId, summary) {
|
|
2074
|
-
|
|
2075
|
-
if (!ctx)
|
|
2076
|
-
|
|
2077
|
-
|
|
2267
|
+
let ctx = activeSessions.get(contentSessionId);
|
|
2268
|
+
if (!ctx) {
|
|
2269
|
+
const dbSession = getSessionByContentSessionId(contentSessionId);
|
|
2270
|
+
if (!dbSession) {
|
|
2271
|
+
console.warn(`[FlowDeck Memory] onSessionCompact: no session found for contentSessionId=${contentSessionId} — summary discarded`);
|
|
2272
|
+
return;
|
|
2273
|
+
}
|
|
2274
|
+
ctx = {
|
|
2275
|
+
sessionId: dbSession.id,
|
|
2276
|
+
contentSessionId,
|
|
2277
|
+
project: dbSession.project,
|
|
2278
|
+
directory: dbSession.directory
|
|
2279
|
+
};
|
|
2280
|
+
activeSessions.set(contentSessionId, ctx);
|
|
2281
|
+
}
|
|
2282
|
+
const storedContent = truncate(summary, MAX_SUMMARY_STORAGE);
|
|
2283
|
+
try {
|
|
2284
|
+
const observations = getObservationsForSession(ctx.sessionId);
|
|
2285
|
+
const metadata = buildHandoffMetadata(ctx.sessionId, ctx.directory, summary, observations);
|
|
2286
|
+
storeSummary(ctx.sessionId, storedContent, metadata);
|
|
2287
|
+
} catch (err) {
|
|
2288
|
+
console.warn(`[FlowDeck Memory] Failed to store compaction summary for session ${ctx.sessionId}:`, err);
|
|
2289
|
+
}
|
|
2078
2290
|
}
|
|
2079
2291
|
function onSessionEnd(contentSessionId, lastMessage) {
|
|
2080
|
-
|
|
2081
|
-
if (!ctx)
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2292
|
+
let ctx = activeSessions.get(contentSessionId);
|
|
2293
|
+
if (!ctx) {
|
|
2294
|
+
const dbSession = getSessionByContentSessionId(contentSessionId);
|
|
2295
|
+
if (dbSession) {
|
|
2296
|
+
ctx = {
|
|
2297
|
+
sessionId: dbSession.id,
|
|
2298
|
+
contentSessionId,
|
|
2299
|
+
project: dbSession.project,
|
|
2300
|
+
directory: dbSession.directory
|
|
2301
|
+
};
|
|
2302
|
+
}
|
|
2303
|
+
}
|
|
2304
|
+
if (ctx && lastMessage && lastMessage.trim()) {
|
|
2305
|
+
try {
|
|
2306
|
+
const observations = getObservationsForSession(ctx.sessionId);
|
|
2307
|
+
const metadata = buildHandoffMetadata(ctx.sessionId, ctx.directory, lastMessage, observations);
|
|
2308
|
+
storeSummary(ctx.sessionId, truncate(lastMessage, MAX_SUMMARY_STORAGE), metadata);
|
|
2309
|
+
} catch (err) {
|
|
2310
|
+
console.warn(`[FlowDeck Memory] Failed to store end-of-session summary for session ${ctx?.sessionId}:`, err);
|
|
2311
|
+
}
|
|
2085
2312
|
}
|
|
2086
2313
|
activeSessions.delete(contentSessionId);
|
|
2087
2314
|
}
|
|
2088
2315
|
function getSessionContext(directory, contentSessionId) {
|
|
2089
2316
|
const context = getContextForDirectory(directory, 30);
|
|
2090
2317
|
const previousSessions = getRecentSessions(directory, 5);
|
|
2091
|
-
if (previousSessions.length > 0 && activeSessions.has(contentSessionId)) {
|
|
2092
|
-
const ctx = activeSessions.get(contentSessionId);
|
|
2093
|
-
for (const prev of previousSessions) {
|
|
2094
|
-
if (prev.content_session_id === contentSessionId)
|
|
2095
|
-
continue;
|
|
2096
|
-
}
|
|
2097
|
-
}
|
|
2098
2318
|
return { context, previousSessions };
|
|
2099
2319
|
}
|
|
2100
2320
|
function clearSession(contentSessionId) {
|
|
@@ -2113,6 +2333,49 @@ var memoryHook = {
|
|
|
2113
2333
|
// src/hooks/guard-rails.ts
|
|
2114
2334
|
import { existsSync as existsSync18, readFileSync as readFileSync15 } from "fs";
|
|
2115
2335
|
import { join as join18 } from "path";
|
|
2336
|
+
|
|
2337
|
+
// src/config/loader.ts
|
|
2338
|
+
import { existsSync as existsSync17, readFileSync as readFileSync14 } from "fs";
|
|
2339
|
+
import { join as join17 } from "path";
|
|
2340
|
+
import { homedir as homedir3 } from "os";
|
|
2341
|
+
var CONFIG_FILENAME = "flowdeck.json";
|
|
2342
|
+
function getGlobalConfigDir() {
|
|
2343
|
+
return process.env.OPENCODE_CONFIG_DIR || (process.env.XDG_CONFIG_HOME ? join17(process.env.XDG_CONFIG_HOME, "opencode") : join17(homedir3(), ".config", "opencode"));
|
|
2344
|
+
}
|
|
2345
|
+
function loadFlowDeckConfig(directory) {
|
|
2346
|
+
const candidates = [];
|
|
2347
|
+
if (directory) {
|
|
2348
|
+
candidates.push(join17(directory, ".opencode", CONFIG_FILENAME));
|
|
2349
|
+
}
|
|
2350
|
+
candidates.push(join17(getGlobalConfigDir(), CONFIG_FILENAME));
|
|
2351
|
+
for (const configPath of candidates) {
|
|
2352
|
+
if (existsSync17(configPath)) {
|
|
2353
|
+
try {
|
|
2354
|
+
const content = readFileSync14(configPath, "utf-8");
|
|
2355
|
+
return JSON.parse(content);
|
|
2356
|
+
} catch {
|
|
2357
|
+
console.warn(`[flowdeck] Failed to load config from ${configPath}`);
|
|
2358
|
+
}
|
|
2359
|
+
}
|
|
2360
|
+
}
|
|
2361
|
+
return {};
|
|
2362
|
+
}
|
|
2363
|
+
function resolveDesignFirstConfig(config) {
|
|
2364
|
+
return {
|
|
2365
|
+
enabled: config.designFirst?.enabled ?? true,
|
|
2366
|
+
enforcement: config.designFirst?.enforcement ?? "strict",
|
|
2367
|
+
requireApprovalBeforeImplementation: config.designFirst?.requireApprovalBeforeImplementation ?? true,
|
|
2368
|
+
modelOverrides: config.designFirst?.modelOverrides ?? {},
|
|
2369
|
+
defaultSkillsByTaskType: config.designFirst?.defaultSkillsByTaskType ?? {
|
|
2370
|
+
"landing-page": ["landing-page-design", "wireframe-planning", "design-system-definition", "frontend-handoff"],
|
|
2371
|
+
dashboard: ["dashboard-design", "ui-ux-planning", "wireframe-planning", "responsive-review"],
|
|
2372
|
+
"admin-panel": ["ui-ux-planning", "wireframe-planning", "design-system-definition", "frontend-handoff"],
|
|
2373
|
+
"app-screen": ["app-shell-design", "ui-ux-planning", "wireframe-planning", "responsive-review"],
|
|
2374
|
+
"general-ui": ["ui-ux-planning", "wireframe-planning", "design-system-definition", "frontend-handoff"]
|
|
2375
|
+
}
|
|
2376
|
+
};
|
|
2377
|
+
}
|
|
2378
|
+
// src/hooks/guard-rails.ts
|
|
2116
2379
|
var PLANNING_DIR2 = ".planning";
|
|
2117
2380
|
var CONFIG_FILE = "config.json";
|
|
2118
2381
|
var STATE_FILE2 = "STATE.md";
|
|
@@ -2198,6 +2461,10 @@ async function guardRailsHook(ctx, input, _output) {
|
|
|
2198
2461
|
if (execMode === "guarded") {
|
|
2199
2462
|
throw new Error(`[flowdeck] GUARDED MODE: edit will proceed but flag for human review.`);
|
|
2200
2463
|
}
|
|
2464
|
+
const designGateMessage = getDesignGateMessage(dir);
|
|
2465
|
+
if (designGateMessage) {
|
|
2466
|
+
throw new Error(designGateMessage);
|
|
2467
|
+
}
|
|
2201
2468
|
const effectiveSeverity = getEffectiveSeverity(configPath, statePath2);
|
|
2202
2469
|
if (effectiveSeverity === null)
|
|
2203
2470
|
return;
|
|
@@ -2220,6 +2487,31 @@ async function guardRailsHook(ctx, input, _output) {
|
|
|
2220
2487
|
}
|
|
2221
2488
|
}
|
|
2222
2489
|
}
|
|
2490
|
+
function getDesignGateMessage(dir) {
|
|
2491
|
+
const designConfig = resolveDesignFirstConfig(loadFlowDeckConfig(dir));
|
|
2492
|
+
if (!designConfig.enabled || !designConfig.requireApprovalBeforeImplementation)
|
|
2493
|
+
return null;
|
|
2494
|
+
const state = readPlanningState(dir);
|
|
2495
|
+
if (state.design_override && state.design_override_reason && state.design_override_reason.trim().length > 0)
|
|
2496
|
+
return null;
|
|
2497
|
+
const designApproved = state.design_stage === "handoff_complete" && state.design_approved;
|
|
2498
|
+
if (state.requires_design_first || state.task_type && isUiHeavyTask(state.task_type) || planSuggestsUiHeavy(dir, state.phase || 1)) {
|
|
2499
|
+
if (designApproved)
|
|
2500
|
+
return null;
|
|
2501
|
+
if (designConfig.enforcement === "advisory") {
|
|
2502
|
+
return "[flowdeck] WARNING: UI-heavy task detected without approved design handoff. Run /fd-design --mode=draft first.";
|
|
2503
|
+
}
|
|
2504
|
+
return "[flowdeck] BLOCK: UI-heavy task requires approved design handoff. Run /fd-design --mode=draft or set explicit design override in STATE.md.";
|
|
2505
|
+
}
|
|
2506
|
+
return null;
|
|
2507
|
+
}
|
|
2508
|
+
function planSuggestsUiHeavy(dir, phase) {
|
|
2509
|
+
const planPath = phasePlanPath(dir, phase);
|
|
2510
|
+
if (!existsSync18(planPath))
|
|
2511
|
+
return false;
|
|
2512
|
+
const planContent = readFileSync15(planPath, "utf-8");
|
|
2513
|
+
return isUiHeavyTask(planContent);
|
|
2514
|
+
}
|
|
2223
2515
|
function effectiveSeverity(configPath, statePath2) {
|
|
2224
2516
|
if (existsSync18(configPath)) {
|
|
2225
2517
|
try {
|
|
@@ -2332,12 +2624,37 @@ function checkArchConstraint(directory, filePath) {
|
|
|
2332
2624
|
function checkPhaseEnforcement(directory) {
|
|
2333
2625
|
try {
|
|
2334
2626
|
const state = readPlanningState(directory);
|
|
2627
|
+
const flowdeckConfig = resolveDesignFirstConfig(loadFlowDeckConfig(directory));
|
|
2335
2628
|
if (state.phase > 0 && state.phase < 3) {
|
|
2336
2629
|
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.`;
|
|
2337
2630
|
}
|
|
2631
|
+
if (flowdeckConfig.enabled && flowdeckConfig.requireApprovalBeforeImplementation && isUiDesignApprovalRequired(directory)) {
|
|
2632
|
+
if (flowdeckConfig.enforcement === "advisory") {
|
|
2633
|
+
return `FLOWDECK [design-gate]: advisory design-first mode detected missing approval. Run /fd-design --mode=draft or set design_override=true in STATE.md.`;
|
|
2634
|
+
}
|
|
2635
|
+
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.`;
|
|
2636
|
+
}
|
|
2338
2637
|
} catch {}
|
|
2339
2638
|
return null;
|
|
2340
2639
|
}
|
|
2640
|
+
function isUiDesignApprovalRequired(directory) {
|
|
2641
|
+
const state = readPlanningState(directory);
|
|
2642
|
+
if (state.design_override && state.design_override_reason && state.design_override_reason.trim().length > 0)
|
|
2643
|
+
return false;
|
|
2644
|
+
if (state.requires_design_first) {
|
|
2645
|
+
return !(state.design_stage === "handoff_complete" && state.design_approved);
|
|
2646
|
+
}
|
|
2647
|
+
if (state.task_type && isUiHeavyTask(state.task_type)) {
|
|
2648
|
+
return !(state.design_stage === "handoff_complete" && state.design_approved);
|
|
2649
|
+
}
|
|
2650
|
+
const planPath = phasePlanPath(directory, state.phase || 1);
|
|
2651
|
+
if (!existsSync19(planPath))
|
|
2652
|
+
return false;
|
|
2653
|
+
const planContent = readFileSync16(planPath, "utf-8");
|
|
2654
|
+
if (!isUiHeavyTask(planContent))
|
|
2655
|
+
return false;
|
|
2656
|
+
return !(state.design_stage === "handoff_complete" && state.design_approved);
|
|
2657
|
+
}
|
|
2341
2658
|
async function toolGuardHook(ctx, input, output) {
|
|
2342
2659
|
if (!IS_ENABLED())
|
|
2343
2660
|
return;
|
|
@@ -3106,7 +3423,9 @@ function blockMessage(toolName) {
|
|
|
3106
3423
|
` + `The orchestrator is a coordinator — it must delegate all implementation work.
|
|
3107
3424
|
|
|
3108
3425
|
` + `Use the \`delegate\` tool to hand this off:
|
|
3109
|
-
` + ` delegate({ agent: "@coder", prompt: "..." }) — code writing / editing
|
|
3426
|
+
` + ` delegate({ agent: "@backend-coder", prompt: "..." }) — backend code writing / editing
|
|
3427
|
+
` + ` delegate({ agent: "@frontend-coder", prompt: "..." }) — frontend code writing / editing
|
|
3428
|
+
` + ` delegate({ agent: "@devops", prompt: "..." }) — CI/CD, deploy, and infra changes
|
|
3110
3429
|
` + ` delegate({ agent: "@mapper", prompt: "..." }) — codebase mapping
|
|
3111
3430
|
` + ` delegate({ agent: "@researcher", prompt: "..." }) — research / file analysis
|
|
3112
3431
|
` + ` delegate({ agent: "@tester", prompt: "..." }) — tests / commands
|
|
@@ -3308,11 +3627,22 @@ For each incomplete step in PLAN.md:
|
|
|
3308
3627
|
5. Re-read STATE.md to confirm state
|
|
3309
3628
|
6. Move to the next incomplete step
|
|
3310
3629
|
|
|
3630
|
+
## Implementation Routing
|
|
3631
|
+
|
|
3632
|
+
When a plan step requires implementation, route to a role-specific agent:
|
|
3633
|
+
- Use @backend-coder for server, API, business logic, database, and non-UI application code.
|
|
3634
|
+
- Use @frontend-coder for UI components, client state, styling, and interaction behavior.
|
|
3635
|
+
- Use @devops for CI/CD workflows, deployment, infrastructure, runtime config, and operations scripts.
|
|
3636
|
+
- If a step mixes multiple domains, split it into multiple delegated tasks by domain.
|
|
3637
|
+
|
|
3311
3638
|
## Agent Team
|
|
3312
3639
|
|
|
3313
3640
|
| Agent | Invoke | Best For |
|
|
3314
3641
|
|-------|--------|----------|
|
|
3315
|
-
|
|
|
3642
|
+
| Design | @design | Discovery, UX planning, wireframes, visual system, implementation handoff, design fidelity review |
|
|
3643
|
+
| Backend Coder | @backend-coder | Backend code implementation |
|
|
3644
|
+
| Frontend Coder | @frontend-coder | Frontend code implementation |
|
|
3645
|
+
| DevOps | @devops | CI/CD and infrastructure implementation |
|
|
3316
3646
|
| Researcher | @researcher | API docs, library usage |
|
|
3317
3647
|
| Tester | @tester | Writing and running tests |
|
|
3318
3648
|
| Reviewer | @reviewer | Code quality review |
|
|
@@ -3323,7 +3653,6 @@ For each incomplete step in PLAN.md:
|
|
|
3323
3653
|
| Code Explorer | @code-explorer | Reading unfamiliar code |
|
|
3324
3654
|
| Debug Specialist | @debug-specialist | Root cause analysis |
|
|
3325
3655
|
| Build Resolver | @build-error-resolver | Build/compile failures |
|
|
3326
|
-
| Parallel Coordinator | @parallel-coordinator | Multi-track parallel work |
|
|
3327
3656
|
| Doc Updater | @doc-updater | Updating existing docs |
|
|
3328
3657
|
| Task Splitter | @task-splitter | Decomposing complex tasks |
|
|
3329
3658
|
| Discusser | @discusser | Requirements extraction |
|
|
@@ -3336,12 +3665,13 @@ For each incomplete step in PLAN.md:
|
|
|
3336
3665
|
## Phase State Machine
|
|
3337
3666
|
|
|
3338
3667
|
\`\`\`
|
|
3339
|
-
discuss → plan → execute → review
|
|
3668
|
+
discuss → plan → design (for UI-heavy tasks) → execute → review
|
|
3340
3669
|
\`\`\`
|
|
3341
3670
|
|
|
3342
3671
|
- **discuss**: Requirements extraction with @discusser
|
|
3343
3672
|
- **plan**: Plan creation with @planner, review with @plan-checker
|
|
3344
|
-
- **
|
|
3673
|
+
- **design**: UX structure, wireframe/layout planning, and visual system definition with @design
|
|
3674
|
+
- **execute**: Implementation with @backend-coder, @frontend-coder, @devops, @tester, and @researcher in parallel where possible, only after approved design handoff for UI-heavy tasks
|
|
3345
3675
|
- **review**: Review with @reviewer, @security-auditor
|
|
3346
3676
|
|
|
3347
3677
|
## Tracking
|
|
@@ -3363,7 +3693,7 @@ If a delegated agent fails:
|
|
|
3363
3693
|
3. If still failing, escalate:
|
|
3364
3694
|
|
|
3365
3695
|
\`\`\`
|
|
3366
|
-
BLOCKED:
|
|
3696
|
+
BLOCKED: implementation agent failed on step 3 (add payment endpoint).
|
|
3367
3697
|
Error: [exact error message]
|
|
3368
3698
|
Retried once with clarification. Still failing.
|
|
3369
3699
|
|
|
@@ -3384,11 +3714,26 @@ When a task required unusual human guidance, a novel solution strategy, or expos
|
|
|
3384
3714
|
|
|
3385
3715
|
Do NOT create a skill for routine tasks. Only capture genuinely novel or reusable patterns.`;
|
|
3386
3716
|
var AGENT_DESCRIPTIONS = {
|
|
3387
|
-
|
|
3388
|
-
- Role:
|
|
3717
|
+
design: `@design
|
|
3718
|
+
- Role: Runs design-first workflow for user-facing tasks
|
|
3389
3719
|
- Permissions: Read/write files
|
|
3390
|
-
- Best for:
|
|
3391
|
-
- **Delegate when:**
|
|
3720
|
+
- Best for: UX structure, wireframes, visual direction, tokens, and frontend handoff
|
|
3721
|
+
- **Delegate when:** Task includes website/app/dashboard/admin/user-facing UI work`,
|
|
3722
|
+
"backend-coder": `@backend-coder
|
|
3723
|
+
- Role: Implements backend features and fixes based on confirmed plans
|
|
3724
|
+
- Permissions: Read/write files
|
|
3725
|
+
- Best for: API, services, data layer, and business logic
|
|
3726
|
+
- **Delegate when:** Backend or server-side implementation work`,
|
|
3727
|
+
"frontend-coder": `@frontend-coder
|
|
3728
|
+
- Role: Implements frontend features and fixes based on confirmed plans
|
|
3729
|
+
- Permissions: Read/write files
|
|
3730
|
+
- Best for: UI components, client state, rendering, and interaction behavior
|
|
3731
|
+
- **Delegate when:** Frontend implementation work`,
|
|
3732
|
+
devops: `@devops
|
|
3733
|
+
- Role: Implements DevOps and infrastructure changes based on confirmed plans
|
|
3734
|
+
- Permissions: Read/write files
|
|
3735
|
+
- Best for: CI/CD, deployment config, infra scripts, and runtime operations
|
|
3736
|
+
- **Delegate when:** Infrastructure, pipeline, or operations implementation work`,
|
|
3392
3737
|
researcher: `@researcher
|
|
3393
3738
|
- Role: Researches documentation, APIs, and best practices
|
|
3394
3739
|
- Permissions: Read files
|
|
@@ -3460,11 +3805,6 @@ var AGENT_DESCRIPTIONS = {
|
|
|
3460
3805
|
- Permissions: Read/write files
|
|
3461
3806
|
- Best for: Requirements extraction
|
|
3462
3807
|
- **Delegate when:** Starting new feature or project phase`,
|
|
3463
|
-
"parallel-coordinator": `@parallel-coordinator
|
|
3464
|
-
- Role: Coordinates multi-wave parallel execution
|
|
3465
|
-
- Permissions: Read files
|
|
3466
|
-
- Best for: Multi-track parallel work
|
|
3467
|
-
- **Delegate when:** Need to execute multiple tasks in parallel`,
|
|
3468
3808
|
planner: `@planner
|
|
3469
3809
|
- Role: Creates detailed implementation plans
|
|
3470
3810
|
- Permissions: Read files
|
|
@@ -3766,7 +4106,7 @@ var createPlanCheckerAgent = (model, customPrompt, customAppendPrompt) => {
|
|
|
3766
4106
|
};
|
|
3767
4107
|
|
|
3768
4108
|
// src/agents/coder.ts
|
|
3769
|
-
var
|
|
4109
|
+
var BASE_IMPLEMENTER_PROMPT = `You implement features and fix bugs. You follow the plan exactly. You do not invent requirements.
|
|
3770
4110
|
|
|
3771
4111
|
## Before Writing Code
|
|
3772
4112
|
|
|
@@ -3876,11 +4216,62 @@ After implementing, report:
|
|
|
3876
4216
|
- Tests added or updated
|
|
3877
4217
|
- Any deviations from the plan and why
|
|
3878
4218
|
- Next step ready to execute`;
|
|
3879
|
-
var
|
|
3880
|
-
|
|
4219
|
+
var BACKEND_CODER_PROMPT = `${BASE_IMPLEMENTER_PROMPT}
|
|
4220
|
+
|
|
4221
|
+
## Domain Focus
|
|
4222
|
+
|
|
4223
|
+
Prioritize backend and platform code:
|
|
4224
|
+
- Server handlers, services, repositories, jobs, and business logic
|
|
4225
|
+
- Database and persistence-layer changes
|
|
4226
|
+
- API contracts and boundary validation
|
|
4227
|
+
`;
|
|
4228
|
+
var FRONTEND_CODER_PROMPT = `${BASE_IMPLEMENTER_PROMPT}
|
|
4229
|
+
|
|
4230
|
+
## Domain Focus
|
|
4231
|
+
|
|
4232
|
+
Prioritize frontend implementation quality:
|
|
4233
|
+
- UI components, client state, accessibility, and interaction behavior
|
|
4234
|
+
- Styling consistency with existing design system/tokens
|
|
4235
|
+
- Browser/runtime safety (no server-only assumptions in client code)
|
|
4236
|
+
`;
|
|
4237
|
+
var DEVOPS_PROMPT = `${BASE_IMPLEMENTER_PROMPT}
|
|
4238
|
+
|
|
4239
|
+
## Domain Focus
|
|
4240
|
+
|
|
4241
|
+
Prioritize infrastructure and delivery tasks:
|
|
4242
|
+
- CI/CD workflows, build pipelines, deployment configuration
|
|
4243
|
+
- Environment/runtime configuration and operational scripts
|
|
4244
|
+
- Reliability and rollback safety for production-facing changes
|
|
4245
|
+
`;
|
|
4246
|
+
var createBackendCoderAgent = (model, customPrompt, customAppendPrompt) => {
|
|
4247
|
+
const prompt = resolvePrompt(BACKEND_CODER_PROMPT, customPrompt, customAppendPrompt);
|
|
3881
4248
|
return {
|
|
3882
|
-
name: "coder",
|
|
3883
|
-
description: "Implements features and fixes based on confirmed plans. Follows existing code patterns and project conventions.
|
|
4249
|
+
name: "backend-coder",
|
|
4250
|
+
description: "Implements backend features and fixes based on confirmed plans. Follows existing code patterns and project conventions.",
|
|
4251
|
+
config: {
|
|
4252
|
+
model,
|
|
4253
|
+
temperature: 0.1,
|
|
4254
|
+
prompt
|
|
4255
|
+
}
|
|
4256
|
+
};
|
|
4257
|
+
};
|
|
4258
|
+
var createFrontendCoderAgent = (model, customPrompt, customAppendPrompt) => {
|
|
4259
|
+
const prompt = resolvePrompt(FRONTEND_CODER_PROMPT, customPrompt, customAppendPrompt);
|
|
4260
|
+
return {
|
|
4261
|
+
name: "frontend-coder",
|
|
4262
|
+
description: "Implements frontend features and fixes based on confirmed plans. Follows existing code patterns and project conventions.",
|
|
4263
|
+
config: {
|
|
4264
|
+
model,
|
|
4265
|
+
temperature: 0.1,
|
|
4266
|
+
prompt
|
|
4267
|
+
}
|
|
4268
|
+
};
|
|
4269
|
+
};
|
|
4270
|
+
var createDevopsAgent = (model, customPrompt, customAppendPrompt) => {
|
|
4271
|
+
const prompt = resolvePrompt(DEVOPS_PROMPT, customPrompt, customAppendPrompt);
|
|
4272
|
+
return {
|
|
4273
|
+
name: "devops",
|
|
4274
|
+
description: "Implements DevOps and infrastructure changes based on confirmed plans. Follows existing repo conventions and operational safety practices.",
|
|
3884
4275
|
config: {
|
|
3885
4276
|
model,
|
|
3886
4277
|
temperature: 0.1,
|
|
@@ -4027,6 +4418,13 @@ var REVIEWER_PROMPT = `You review code for correctness, security, and quality. Y
|
|
|
4027
4418
|
4. Apply the checklist below
|
|
4028
4419
|
5. Report by severity — CRITICAL first, then HIGH, MEDIUM, PASS
|
|
4029
4420
|
|
|
4421
|
+
If the task is UI-heavy and a design artifact is available, include design fidelity checks:
|
|
4422
|
+
- visual hierarchy and spacing consistency
|
|
4423
|
+
- CTA flow quality
|
|
4424
|
+
- responsive behavior
|
|
4425
|
+
- accessibility semantics and states
|
|
4426
|
+
- empty/loading/error/success state coverage
|
|
4427
|
+
|
|
4030
4428
|
## Security Checklist — CRITICAL
|
|
4031
4429
|
|
|
4032
4430
|
**Hardcoded credentials:**
|
|
@@ -4231,7 +4629,7 @@ Never fabricate information to appear more helpful.
|
|
|
4231
4629
|
## Scope Boundaries
|
|
4232
4630
|
|
|
4233
4631
|
- Report facts only. Do not make implementation decisions.
|
|
4234
|
-
- Do not write code unless asked. Return research findings for the
|
|
4632
|
+
- Do not write code unless asked. Return research findings for the implementation agent to act on.
|
|
4235
4633
|
- If you find a better approach than what was requested, mention it as an option — do not substitute it.
|
|
4236
4634
|
|
|
4237
4635
|
## Research Areas
|
|
@@ -4351,7 +4749,7 @@ var createWriterAgent = (model, customPrompt, customAppendPrompt) => {
|
|
|
4351
4749
|
};
|
|
4352
4750
|
|
|
4353
4751
|
// src/agents/security-auditor.ts
|
|
4354
|
-
var SECURITY_AUDITOR_PROMPT = `You audit code for security vulnerabilities. You report findings with severity and specific remediation. You do not fix — that is
|
|
4752
|
+
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).
|
|
4355
4753
|
|
|
4356
4754
|
## Audit Scope
|
|
4357
4755
|
|
|
@@ -4454,7 +4852,7 @@ For high/critical vulnerabilities: report exact package, CVE ID, and whether it'
|
|
|
4454
4852
|
|
|
4455
4853
|
## After Finding Issues
|
|
4456
4854
|
|
|
4457
|
-
Report only. Do not fix. Tag @coder with specific remediations for each finding.`;
|
|
4855
|
+
Report only. Do not fix. Tag the appropriate implementation agent (@backend-coder, @frontend-coder, or @devops) with specific remediations for each finding.`;
|
|
4458
4856
|
var createSecurityAuditorAgent = (model, customPrompt, customAppendPrompt) => {
|
|
4459
4857
|
const prompt = resolvePrompt(SECURITY_AUDITOR_PROMPT, customPrompt, customAppendPrompt);
|
|
4460
4858
|
return {
|
|
@@ -4802,7 +5200,7 @@ request → router → UserController.create() → UserService.create() → ❌
|
|
|
4802
5200
|
|
|
4803
5201
|
## Scope
|
|
4804
5202
|
|
|
4805
|
-
Report only. Do not implement the fix. Tag @coder with the recommended fix.`;
|
|
5203
|
+
Report only. Do not implement the fix. Tag the appropriate implementation agent (@backend-coder, @frontend-coder, or @devops) with the recommended fix.`;
|
|
4806
5204
|
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.
|
|
4807
5205
|
|
|
4808
5206
|
## Diagnostic Commands
|
|
@@ -4905,7 +5303,7 @@ npx tsc --noEmit src/path/to/file.ts
|
|
|
4905
5303
|
|
|
4906
5304
|
- Build fails because of architectural problems → @architect
|
|
4907
5305
|
- A feature is not working correctly → @debug-specialist
|
|
4908
|
-
- Missing functionality needs to be written → @coder`;
|
|
5306
|
+
- Missing functionality needs to be written → @backend-coder/@frontend-coder/@devops`;
|
|
4909
5307
|
var createDebugSpecialistAgent = (model, customPrompt, customAppendPrompt) => {
|
|
4910
5308
|
const prompt = resolvePrompt(DEBUG_SPECIALIST_PROMPT, customPrompt, customAppendPrompt);
|
|
4911
5309
|
return {
|
|
@@ -4932,7 +5330,7 @@ var createBuildErrorResolverAgent = (model, customPrompt, customAppendPrompt) =>
|
|
|
4932
5330
|
};
|
|
4933
5331
|
|
|
4934
5332
|
// src/agents/specialist.ts
|
|
4935
|
-
var TASK_SPLITTER_PROMPT = `You decompose complex tasks into parallel workstreams. You identify dependencies, group independent work into waves, and produce a plan that @
|
|
5333
|
+
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.
|
|
4936
5334
|
|
|
4937
5335
|
## Wave-Structured Output
|
|
4938
5336
|
|
|
@@ -4942,7 +5340,7 @@ var TASK_SPLITTER_PROMPT = `You decompose complex tasks into parallel workstream
|
|
|
4942
5340
|
### Wave 1 (parallel — start simultaneously)
|
|
4943
5341
|
|
|
4944
5342
|
**Track A — [description]**
|
|
4945
|
-
- Agent: @coder
|
|
5343
|
+
- Agent: @backend-coder
|
|
4946
5344
|
- Files: \`src/auth/user.ts\`, \`src/auth/types.ts\`
|
|
4947
5345
|
- Task: [specific implementation task]
|
|
4948
5346
|
- Verify: [how to confirm it's done]
|
|
@@ -4962,7 +5360,7 @@ var TASK_SPLITTER_PROMPT = `You decompose complex tasks into parallel workstream
|
|
|
4962
5360
|
### Wave 2 (after Wave 1 completes)
|
|
4963
5361
|
|
|
4964
5362
|
**Track D — Integration**
|
|
4965
|
-
- Agent: @coder
|
|
5363
|
+
- Agent: @backend-coder
|
|
4966
5364
|
- Depends on: Track A, Track C
|
|
4967
5365
|
- Task: Wire together outputs from Wave 1
|
|
4968
5366
|
|
|
@@ -5002,7 +5400,9 @@ After Wave 2: @reviewer reviews all changes together
|
|
|
5002
5400
|
| Agent | Best For |
|
|
5003
5401
|
|-------|---------|
|
|
5004
5402
|
| @architect | Interface contracts, ADRs |
|
|
5005
|
-
| @coder |
|
|
5403
|
+
| @backend-coder | Backend implementation |
|
|
5404
|
+
| @frontend-coder | Frontend implementation |
|
|
5405
|
+
| @devops | Infrastructure implementation |
|
|
5006
5406
|
| @researcher | API docs, library research |
|
|
5007
5407
|
| @tester | Test writing and coverage |
|
|
5008
5408
|
| @reviewer | Code quality review |
|
|
@@ -5140,200 +5540,6 @@ Discussion is complete when:
|
|
|
5140
5540
|
- No open questions remain
|
|
5141
5541
|
|
|
5142
5542
|
Report: "Requirements gathering complete. N decisions recorded. Ready for /plan."`;
|
|
5143
|
-
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.
|
|
5144
|
-
|
|
5145
|
-
## Your Outputs
|
|
5146
|
-
|
|
5147
|
-
1. **WAVE TABLE** — printed at job start, shows every agent slot and its dependencies
|
|
5148
|
-
2. **Agent briefings** — full context packet per agent (they are stateless — give them everything)
|
|
5149
|
-
3. **Wave reports** — status after each wave closes
|
|
5150
|
-
4. **Merge resolution** — reconcile outputs when two tracks touched the same conceptual area
|
|
5151
|
-
|
|
5152
|
-
## WAVE TABLE Format
|
|
5153
|
-
|
|
5154
|
-
Print this at the start of every job before delegating any agents:
|
|
5155
|
-
|
|
5156
|
-
\`\`\`
|
|
5157
|
-
╔══════════════════════════════════════════════════════════════╗
|
|
5158
|
-
║ WAVE TABLE — [Job Title] ║
|
|
5159
|
-
╠══════════════════════════════════════════════════════════════╣
|
|
5160
|
-
║ Wave 1 (parallel) │ @researcher + @code-explorer ║
|
|
5161
|
-
║ Wave 2 (serial) │ @architect ║
|
|
5162
|
-
║ Wave 3 (parallel) │ @coder + @tester ║
|
|
5163
|
-
║ Wave 4 (parallel) │ @reviewer + @security-auditor ║
|
|
5164
|
-
╠══════════════════════════════════════════════════════════════╣
|
|
5165
|
-
║ Est. sequential: │ 8h ║
|
|
5166
|
-
║ Est. parallel: │ 4.5h ║
|
|
5167
|
-
║ Dependency locks: │ Wave 3 blocked on Wave 2 output ║
|
|
5168
|
-
╚══════════════════════════════════════════════════════════════╝
|
|
5169
|
-
\`\`\`
|
|
5170
|
-
|
|
5171
|
-
Adjust lanes based on actual task content. Remove any wave whose agents have no work.
|
|
5172
|
-
|
|
5173
|
-
## Standard Wave Delegation Syntax
|
|
5174
|
-
|
|
5175
|
-
**Wave 1 — Discovery (parallelize):**
|
|
5176
|
-
\`\`\`
|
|
5177
|
-
@researcher: [exact research task with sources to check]
|
|
5178
|
-
@code-explorer: [exact files/modules to map — list paths]
|
|
5179
|
-
\`\`\`
|
|
5180
|
-
Start both simultaneously. Do not wait for one before sending the other.
|
|
5181
|
-
|
|
5182
|
-
**Wave 2 — Architecture (serial, depends on Wave 1):**
|
|
5183
|
-
\`\`\`
|
|
5184
|
-
@architect: [design task — attach Wave 1 outputs as context]
|
|
5185
|
-
\`\`\`
|
|
5186
|
-
One agent. Must complete before Wave 3 starts.
|
|
5187
|
-
|
|
5188
|
-
**Wave 3 — Implementation (parallelize, depends on Wave 2):**
|
|
5189
|
-
\`\`\`
|
|
5190
|
-
@coder: [implementation task — attach @architect output + relevant Wave 1 findings]
|
|
5191
|
-
@tester: [test task — attach interface contracts from @architect, NOT @coder output]
|
|
5192
|
-
\`\`\`
|
|
5193
|
-
Start both simultaneously once Wave 2 output is in hand. @tester works from contracts, not @coder's code, so they are truly parallel.
|
|
5194
|
-
|
|
5195
|
-
**Wave 4 — Validation (parallelize):**
|
|
5196
|
-
\`\`\`
|
|
5197
|
-
@reviewer: [review scope — list files changed by Wave 3]
|
|
5198
|
-
@security-auditor: [audit scope — list entry points, auth surfaces, data flows]
|
|
5199
|
-
\`\`\`
|
|
5200
|
-
Start both once Wave 3 is complete.
|
|
5201
|
-
|
|
5202
|
-
## Parallelism Rules
|
|
5203
|
-
|
|
5204
|
-
**Safe to parallelize:**
|
|
5205
|
-
- Tasks touching different files with no shared output
|
|
5206
|
-
- Research alongside implementation (research produces inputs, not outputs of implementation)
|
|
5207
|
-
- Test writing from interface contracts alongside implementation
|
|
5208
|
-
- Documentation alongside implementation when writing to different files
|
|
5209
|
-
|
|
5210
|
-
**Must be sequential:**
|
|
5211
|
-
- Task B's design depends on decisions Task A makes
|
|
5212
|
-
- Task B reads a file Task A will write
|
|
5213
|
-
- Both tasks modify the same file
|
|
5214
|
-
|
|
5215
|
-
**Not worth parallelizing:**
|
|
5216
|
-
- Total estimated work is under 20 minutes
|
|
5217
|
-
- File ownership is ambiguous — if unclear who owns a file, serialize it
|
|
5218
|
-
|
|
5219
|
-
## Agent Team
|
|
5220
|
-
|
|
5221
|
-
| Agent | Best For |
|
|
5222
|
-
|-------|---------|
|
|
5223
|
-
| @architect | Interface contracts, ADRs, system design |
|
|
5224
|
-
| @coder | All code implementation |
|
|
5225
|
-
| @researcher | API docs, library usage, best practices |
|
|
5226
|
-
| @tester | Test writing and coverage |
|
|
5227
|
-
| @reviewer | Code quality review |
|
|
5228
|
-
| @security-auditor | Security vulnerability audit |
|
|
5229
|
-
| @writer | New documentation |
|
|
5230
|
-
| @doc-updater | Updating existing documentation |
|
|
5231
|
-
| @code-explorer | Mapping unfamiliar code |
|
|
5232
|
-
| @debug-specialist | Root cause analysis |
|
|
5233
|
-
| @build-error-resolver | Build and compile failures |
|
|
5234
|
-
|
|
5235
|
-
## Merging Parallel Outputs
|
|
5236
|
-
|
|
5237
|
-
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):
|
|
5238
|
-
|
|
5239
|
-
**Step 1 — Detect the overlap.** After each wave, compare the file sets each agent reported touching. Any overlap is a merge candidate.
|
|
5240
|
-
|
|
5241
|
-
**Step 2 — Classify the overlap:**
|
|
5242
|
-
- **Additive** (different functions in the same file): safe to auto-merge, reconcile manually.
|
|
5243
|
-
- **Structural** (same type, same interface, same function signature): do not auto-merge — escalate.
|
|
5244
|
-
- **Contradictory** (one agent added a field, another removed it): escalate.
|
|
5245
|
-
|
|
5246
|
-
**Step 3 — Resolve:**
|
|
5247
|
-
- Additive: apply both changesets, verify no symbol collisions, verify tests pass.
|
|
5248
|
-
- Structural or contradictory: invoke the conflict resolution protocol below.
|
|
5249
|
-
|
|
5250
|
-
## Conflict Resolution Protocol
|
|
5251
|
-
|
|
5252
|
-
Trigger when two tracks produced incompatible changes to the same logical unit.
|
|
5253
|
-
|
|
5254
|
-
\`\`\`
|
|
5255
|
-
CONFLICT DETECTED
|
|
5256
|
-
Track A (@coder): added \`refreshToken: string\` to UserSession in src/types/session.ts
|
|
5257
|
-
Track B (@tester): wrote tests assuming UserSession has no refresh field
|
|
5258
|
-
Classification: Structural — interface mismatch
|
|
5259
|
-
|
|
5260
|
-
RESOLUTION PLAN
|
|
5261
|
-
1. Suspend Track B output (do not apply tests yet)
|
|
5262
|
-
2. Delegate to @coder: reconcile both versions sequentially
|
|
5263
|
-
- Brief: "Track A and Track B produced incompatible changes. [Attach both outputs.]
|
|
5264
|
-
Produce a single unified version that satisfies both intents."
|
|
5265
|
-
3. Once @coder delivers unified version: re-run @tester against it
|
|
5266
|
-
4. Mark original conflict as resolved, continue to Wave 4
|
|
5267
|
-
\`\`\`
|
|
5268
|
-
|
|
5269
|
-
Never silently pick one side. Always surface what was lost in the merge and why.
|
|
5270
|
-
|
|
5271
|
-
## Failure Handling
|
|
5272
|
-
|
|
5273
|
-
**Wave failure does not block independent waves.**
|
|
5274
|
-
|
|
5275
|
-
Before each wave starts, classify each task as:
|
|
5276
|
-
- **Blocking** — downstream waves need its output
|
|
5277
|
-
- **Independent** — downstream waves do not depend on it
|
|
5278
|
-
|
|
5279
|
-
If a blocking task fails:
|
|
5280
|
-
\`\`\`
|
|
5281
|
-
Wave 1 FAILURE — @researcher: could not retrieve bcrypt API docs
|
|
5282
|
-
Impact: Wave 3 @coder task "implement password hashing" is blocked.
|
|
5283
|
-
Action: Pause that specific Wave 3 slot. Continue all other Wave 3 slots.
|
|
5284
|
-
Retry: Re-run @researcher with a fallback source list, then unblock the Wave 3 slot.
|
|
5285
|
-
\`\`\`
|
|
5286
|
-
|
|
5287
|
-
If an independent task fails:
|
|
5288
|
-
\`\`\`
|
|
5289
|
-
Wave 4 FAILURE — @security-auditor: process timed out
|
|
5290
|
-
Impact: None — @reviewer completed independently.
|
|
5291
|
-
Action: Log failure. Do not block Wave 4 close. Re-run @security-auditor as a follow-up.
|
|
5292
|
-
\`\`\`
|
|
5293
|
-
|
|
5294
|
-
Wave gates work per-slot, not per-wave: a wave closes when all blocking slots complete. Independent failures are retried async.
|
|
5295
|
-
|
|
5296
|
-
## Full Execution Report Format
|
|
5297
|
-
|
|
5298
|
-
\`\`\`markdown
|
|
5299
|
-
## Parallel Execution Report — [Job Title]
|
|
5300
|
-
|
|
5301
|
-
### Wave 1 Results (Discovery)
|
|
5302
|
-
| Track | Agent | Status | Output |
|
|
5303
|
-
|-------|-------|--------|--------|
|
|
5304
|
-
| A | @researcher | ✅ | \`.planning/research/bcrypt.md\` |
|
|
5305
|
-
| B | @code-explorer | ✅ | \`.codebase/auth-module-map.md\` |
|
|
5306
|
-
|
|
5307
|
-
### Wave 1 → Wave 2 Gate
|
|
5308
|
-
- All blocking slots complete: ✅
|
|
5309
|
-
- Merge check: no file conflicts
|
|
5310
|
-
|
|
5311
|
-
### Wave 2 Results (Architecture)
|
|
5312
|
-
| Track | Agent | Status | Output |
|
|
5313
|
-
|-------|-------|--------|--------|
|
|
5314
|
-
| A | @architect | ✅ | \`.planning/adr/auth-design.md\`, interface contracts |
|
|
5315
|
-
|
|
5316
|
-
### Wave 3 Results (Implementation)
|
|
5317
|
-
| Track | Agent | Status | Output |
|
|
5318
|
-
|-------|-------|--------|--------|
|
|
5319
|
-
| A | @coder | ✅ | \`src/auth/service.ts\`, \`src/auth/session.ts\` |
|
|
5320
|
-
| B | @tester | ✅ | \`src/auth/service.test.ts\` — 14 tests, 14 passing |
|
|
5321
|
-
|
|
5322
|
-
### Wave 3 Merge Check
|
|
5323
|
-
- File overlap: none
|
|
5324
|
-
- Conceptual overlap: @coder and @tester both reference UserSession — compatible ✅
|
|
5325
|
-
|
|
5326
|
-
### Wave 4 Results (Validation)
|
|
5327
|
-
| Track | Agent | Status | Output |
|
|
5328
|
-
|-------|-------|--------|--------|
|
|
5329
|
-
| A | @reviewer | ✅ | 2 non-blocking suggestions filed |
|
|
5330
|
-
| B | @security-auditor | ⚠️ FAILED | Timeout — retrying async |
|
|
5331
|
-
|
|
5332
|
-
### Final Status
|
|
5333
|
-
- All blocking work complete ✅
|
|
5334
|
-
- @security-auditor re-run scheduled as follow-up
|
|
5335
|
-
- Elapsed: 4h 20m (vs 8h sequential)
|
|
5336
|
-
\`\`\``;
|
|
5337
5543
|
var createTaskSplitterAgent = (model, customPrompt, customAppendPrompt) => {
|
|
5338
5544
|
const prompt = resolvePrompt(TASK_SPLITTER_PROMPT, customPrompt, customAppendPrompt);
|
|
5339
5545
|
return {
|
|
@@ -5358,18 +5564,6 @@ var createDiscusserAgent = (model, customPrompt, customAppendPrompt) => {
|
|
|
5358
5564
|
}
|
|
5359
5565
|
};
|
|
5360
5566
|
};
|
|
5361
|
-
var createParallelCoordinatorAgent = (model, customPrompt, customAppendPrompt) => {
|
|
5362
|
-
const prompt = resolvePrompt(PARALLEL_COORDINATOR_PROMPT, customPrompt, customAppendPrompt);
|
|
5363
|
-
return {
|
|
5364
|
-
name: "parallel-coordinator",
|
|
5365
|
-
description: "Coordinates parallel agent execution for multi-track workstreams. Manages wave execution, handles merge conflicts, and maximizes throughput.",
|
|
5366
|
-
config: {
|
|
5367
|
-
model,
|
|
5368
|
-
temperature: 0.1,
|
|
5369
|
-
prompt
|
|
5370
|
-
}
|
|
5371
|
-
};
|
|
5372
|
-
};
|
|
5373
5567
|
|
|
5374
5568
|
// src/agents/architect.ts
|
|
5375
5569
|
var ARCHITECT_PROMPT = `You design system architecture, create Architecture Decision Records (ADRs), and define API contracts before implementation begins.
|
|
@@ -6016,11 +6210,81 @@ function createAutoLearnerAgent(model) {
|
|
|
6016
6210
|
return definition;
|
|
6017
6211
|
}
|
|
6018
6212
|
|
|
6213
|
+
// src/agents/design.ts
|
|
6214
|
+
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.
|
|
6215
|
+
|
|
6216
|
+
## Scope
|
|
6217
|
+
|
|
6218
|
+
Use this workflow for website, web app, mobile app, dashboard, admin panel, landing page, SaaS interface, onboarding UX, and other user-facing surfaces.
|
|
6219
|
+
|
|
6220
|
+
## Required Execution Stages
|
|
6221
|
+
|
|
6222
|
+
For UI-heavy tasks, produce all stages in order:
|
|
6223
|
+
1. discovery
|
|
6224
|
+
2. ux_planning
|
|
6225
|
+
3. wireframe_layout
|
|
6226
|
+
4. visual_system_definition
|
|
6227
|
+
5. design_approval
|
|
6228
|
+
6. implementation_handoff
|
|
6229
|
+
|
|
6230
|
+
Do not skip or merge stages.
|
|
6231
|
+
|
|
6232
|
+
## Structured Output Contract
|
|
6233
|
+
|
|
6234
|
+
Always return machine-readable markdown sections with these keys:
|
|
6235
|
+
- task_type
|
|
6236
|
+
- user_goals
|
|
6237
|
+
- target_audience
|
|
6238
|
+
- core_user_flows
|
|
6239
|
+
- page_map_or_screen_map
|
|
6240
|
+
- section_structure
|
|
6241
|
+
- layout_plan
|
|
6242
|
+
- component_list
|
|
6243
|
+
- state_list (loading, empty, error, success)
|
|
6244
|
+
- responsive_behavior_notes
|
|
6245
|
+
- visual_direction
|
|
6246
|
+
- design_tokens_guidance
|
|
6247
|
+
- accessibility_notes
|
|
6248
|
+
- implementation_handoff_checklist
|
|
6249
|
+
- approval_status
|
|
6250
|
+
|
|
6251
|
+
## Design Review Mode
|
|
6252
|
+
|
|
6253
|
+
When asked to review an implemented UI, compare against approved design artifacts and report:
|
|
6254
|
+
- design mismatches
|
|
6255
|
+
- hierarchy issues
|
|
6256
|
+
- spacing inconsistency
|
|
6257
|
+
- weak call-to-action flow
|
|
6258
|
+
- responsiveness issues
|
|
6259
|
+
- accessibility concerns
|
|
6260
|
+
- component inconsistency
|
|
6261
|
+
- missing empty/error states
|
|
6262
|
+
|
|
6263
|
+
## Constraints
|
|
6264
|
+
|
|
6265
|
+
- Do not write implementation code in design mode.
|
|
6266
|
+
- Do not claim approval without explicit pass/fail rationale.
|
|
6267
|
+
- Keep output concise but complete enough for frontend handoff.`;
|
|
6268
|
+
var createDesignAgent = (model, customPrompt, customAppendPrompt) => {
|
|
6269
|
+
const prompt = resolvePrompt(DESIGN_PROMPT, customPrompt, customAppendPrompt);
|
|
6270
|
+
return {
|
|
6271
|
+
name: "design",
|
|
6272
|
+
description: "Design-first specialist for UX structure, wireframe planning, visual system definition, and frontend handoff before implementation.",
|
|
6273
|
+
config: {
|
|
6274
|
+
model,
|
|
6275
|
+
temperature: 0.1,
|
|
6276
|
+
prompt
|
|
6277
|
+
}
|
|
6278
|
+
};
|
|
6279
|
+
};
|
|
6280
|
+
|
|
6019
6281
|
// src/agents/index.ts
|
|
6020
6282
|
var AGENT_NAMES = [
|
|
6021
6283
|
"orchestrator",
|
|
6022
6284
|
"planner",
|
|
6023
|
-
"coder",
|
|
6285
|
+
"backend-coder",
|
|
6286
|
+
"frontend-coder",
|
|
6287
|
+
"devops",
|
|
6024
6288
|
"plan-checker",
|
|
6025
6289
|
"tester",
|
|
6026
6290
|
"reviewer",
|
|
@@ -6034,13 +6298,13 @@ var AGENT_NAMES = [
|
|
|
6034
6298
|
"build-error-resolver",
|
|
6035
6299
|
"task-splitter",
|
|
6036
6300
|
"discusser",
|
|
6037
|
-
"parallel-coordinator",
|
|
6038
6301
|
"architect",
|
|
6039
6302
|
"risk-analyst",
|
|
6040
6303
|
"policy-enforcer",
|
|
6041
6304
|
"performance-optimizer",
|
|
6042
6305
|
"refactor-guide",
|
|
6043
|
-
"auto-learner"
|
|
6306
|
+
"auto-learner",
|
|
6307
|
+
"design"
|
|
6044
6308
|
];
|
|
6045
6309
|
var PRIMARY_AGENTS = new Set(["orchestrator"]);
|
|
6046
6310
|
var ALL_MODES_AGENTS = new Set;
|
|
@@ -6060,8 +6324,12 @@ function createAgent(name, model, customPrompt, customAppendPrompt) {
|
|
|
6060
6324
|
return createOrchestratorAgent(model, customPrompt, customAppendPrompt);
|
|
6061
6325
|
case "planner":
|
|
6062
6326
|
return createPlannerAgent(model, customPrompt, customAppendPrompt);
|
|
6063
|
-
case "coder":
|
|
6064
|
-
return
|
|
6327
|
+
case "backend-coder":
|
|
6328
|
+
return createBackendCoderAgent(model, customPrompt, customAppendPrompt);
|
|
6329
|
+
case "frontend-coder":
|
|
6330
|
+
return createFrontendCoderAgent(model, customPrompt, customAppendPrompt);
|
|
6331
|
+
case "devops":
|
|
6332
|
+
return createDevopsAgent(model, customPrompt, customAppendPrompt);
|
|
6065
6333
|
case "plan-checker":
|
|
6066
6334
|
return createPlanCheckerAgent(model, customPrompt, customAppendPrompt);
|
|
6067
6335
|
case "tester":
|
|
@@ -6088,8 +6356,6 @@ function createAgent(name, model, customPrompt, customAppendPrompt) {
|
|
|
6088
6356
|
return createTaskSplitterAgent(model, customPrompt, customAppendPrompt);
|
|
6089
6357
|
case "discusser":
|
|
6090
6358
|
return createDiscusserAgent(model, customPrompt, customAppendPrompt);
|
|
6091
|
-
case "parallel-coordinator":
|
|
6092
|
-
return createParallelCoordinatorAgent(model, customPrompt, customAppendPrompt);
|
|
6093
6359
|
case "architect":
|
|
6094
6360
|
return createArchitectAgent(model, customPrompt, customAppendPrompt);
|
|
6095
6361
|
case "risk-analyst":
|
|
@@ -6102,6 +6368,8 @@ function createAgent(name, model, customPrompt, customAppendPrompt) {
|
|
|
6102
6368
|
return createRefactorGuideAgent(model, customPrompt, customAppendPrompt);
|
|
6103
6369
|
case "auto-learner":
|
|
6104
6370
|
return createAutoLearnerAgent(model);
|
|
6371
|
+
case "design":
|
|
6372
|
+
return createDesignAgent(model, customPrompt, customAppendPrompt);
|
|
6105
6373
|
default:
|
|
6106
6374
|
console.warn(`[flowdeck] Unknown agent: ${name}`);
|
|
6107
6375
|
return;
|
|
@@ -6139,42 +6407,16 @@ function getAgentConfigs(agentModels) {
|
|
|
6139
6407
|
return configs;
|
|
6140
6408
|
}
|
|
6141
6409
|
|
|
6142
|
-
// src/config/loader.ts
|
|
6143
|
-
import { existsSync as existsSync27, readFileSync as readFileSync23 } from "fs";
|
|
6144
|
-
import { join as join26 } from "path";
|
|
6145
|
-
import { homedir as homedir3 } from "os";
|
|
6146
|
-
var CONFIG_FILENAME = "flowdeck.json";
|
|
6147
|
-
function getGlobalConfigDir() {
|
|
6148
|
-
return process.env.OPENCODE_CONFIG_DIR || (process.env.XDG_CONFIG_HOME ? join26(process.env.XDG_CONFIG_HOME, "opencode") : join26(homedir3(), ".config", "opencode"));
|
|
6149
|
-
}
|
|
6150
|
-
function loadFlowDeckConfig(directory) {
|
|
6151
|
-
const candidates = [];
|
|
6152
|
-
if (directory) {
|
|
6153
|
-
candidates.push(join26(directory, ".opencode", CONFIG_FILENAME));
|
|
6154
|
-
}
|
|
6155
|
-
candidates.push(join26(getGlobalConfigDir(), CONFIG_FILENAME));
|
|
6156
|
-
for (const configPath of candidates) {
|
|
6157
|
-
if (existsSync27(configPath)) {
|
|
6158
|
-
try {
|
|
6159
|
-
const content = readFileSync23(configPath, "utf-8");
|
|
6160
|
-
return JSON.parse(content);
|
|
6161
|
-
} catch {
|
|
6162
|
-
console.warn(`[flowdeck] Failed to load config from ${configPath}`);
|
|
6163
|
-
}
|
|
6164
|
-
}
|
|
6165
|
-
}
|
|
6166
|
-
return {};
|
|
6167
|
-
}
|
|
6168
6410
|
// src/index.ts
|
|
6169
6411
|
function loadRulePaths() {
|
|
6170
6412
|
const __dir = dirname4(fileURLToPath2(import.meta.url));
|
|
6171
|
-
const rulesDir =
|
|
6172
|
-
if (!
|
|
6413
|
+
const rulesDir = join26(__dir, "..", "src", "rules");
|
|
6414
|
+
if (!existsSync27(rulesDir))
|
|
6173
6415
|
return [];
|
|
6174
6416
|
const paths = [];
|
|
6175
6417
|
function walk(dir) {
|
|
6176
6418
|
for (const entry of readdirSync3(dir, { withFileTypes: true })) {
|
|
6177
|
-
const full =
|
|
6419
|
+
const full = join26(dir, entry.name);
|
|
6178
6420
|
if (entry.isDirectory()) {
|
|
6179
6421
|
walk(full);
|
|
6180
6422
|
} else if (entry.isFile() && entry.name.endsWith(".md") && entry.name !== "README.md") {
|
|
@@ -6187,8 +6429,8 @@ function loadRulePaths() {
|
|
|
6187
6429
|
}
|
|
6188
6430
|
function loadCommands() {
|
|
6189
6431
|
const __dir = dirname4(fileURLToPath2(import.meta.url));
|
|
6190
|
-
const commandsDir =
|
|
6191
|
-
if (!
|
|
6432
|
+
const commandsDir = join26(__dir, "..", "src", "commands");
|
|
6433
|
+
if (!existsSync27(commandsDir))
|
|
6192
6434
|
return {};
|
|
6193
6435
|
const commands = {};
|
|
6194
6436
|
try {
|
|
@@ -6196,7 +6438,7 @@ function loadCommands() {
|
|
|
6196
6438
|
if (!file.endsWith(".md"))
|
|
6197
6439
|
continue;
|
|
6198
6440
|
const name = basename(file, ".md");
|
|
6199
|
-
const raw =
|
|
6441
|
+
const raw = readFileSync23(join26(commandsDir, file), "utf-8");
|
|
6200
6442
|
let description;
|
|
6201
6443
|
let template = raw;
|
|
6202
6444
|
const fmMatch = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/);
|
|
@@ -6237,12 +6479,16 @@ var plugin = async (input, _options) => {
|
|
|
6237
6479
|
cfg.default_agent = "orchestrator";
|
|
6238
6480
|
}
|
|
6239
6481
|
const flowdeckConfig = loadFlowDeckConfig(directory);
|
|
6482
|
+
const designFirstConfig = resolveDesignFirstConfig(flowdeckConfig);
|
|
6240
6483
|
const agentModels = {};
|
|
6241
6484
|
for (const [name, agentCfg] of Object.entries(flowdeckConfig.agents ?? {})) {
|
|
6242
6485
|
if (agentCfg.model) {
|
|
6243
6486
|
agentModels[name] = agentCfg.model;
|
|
6244
6487
|
}
|
|
6245
6488
|
}
|
|
6489
|
+
if (designFirstConfig.modelOverrides.design) {
|
|
6490
|
+
agentModels.design = designFirstConfig.modelOverrides.design;
|
|
6491
|
+
}
|
|
6246
6492
|
const resolvedAgentConfigs = getAgentConfigs(agentModels);
|
|
6247
6493
|
if (!cfg.agent) {
|
|
6248
6494
|
cfg.agent = { ...resolvedAgentConfigs };
|
|
@@ -6273,8 +6519,8 @@ var plugin = async (input, _options) => {
|
|
|
6273
6519
|
}
|
|
6274
6520
|
}
|
|
6275
6521
|
}
|
|
6276
|
-
const skillsDir =
|
|
6277
|
-
if (
|
|
6522
|
+
const skillsDir = join26(dirname4(fileURLToPath2(import.meta.url)), "..", "src", "skills");
|
|
6523
|
+
if (existsSync27(skillsDir)) {
|
|
6278
6524
|
const cfgAny = cfg;
|
|
6279
6525
|
if (!cfgAny.skills || typeof cfgAny.skills !== "object") {
|
|
6280
6526
|
cfgAny.skills = { paths: [] };
|
|
@@ -6351,7 +6597,7 @@ var plugin = async (input, _options) => {
|
|
|
6351
6597
|
const delEvent = event?.event ?? event;
|
|
6352
6598
|
const sessionId = delEvent?.sessionID ?? delEvent?.sessionId ?? "";
|
|
6353
6599
|
if (sessionId) {
|
|
6354
|
-
memoryHook.
|
|
6600
|
+
memoryHook.onSessionEnd(sessionId);
|
|
6355
6601
|
}
|
|
6356
6602
|
}
|
|
6357
6603
|
} catch (err) {
|