@dv.nghiem/flowdeck 0.3.3 → 0.3.4
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 +7 -6
- package/dist/hooks/orchestrator-guard-hook.d.ts +4 -1
- package/dist/hooks/orchestrator-guard-hook.d.ts.map +1 -1
- package/dist/hooks/session-idle-hook.d.ts.map +1 -1
- package/dist/hooks/telemetry-hook.d.ts +14 -1
- package/dist/hooks/telemetry-hook.d.ts.map +1 -1
- package/dist/hooks/telemetry-hook.test.d.ts +2 -0
- package/dist/hooks/telemetry-hook.test.d.ts.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +466 -202
- package/dist/tools/council.d.ts.map +1 -1
- package/dist/tools/delegate.d.ts.map +1 -1
- package/dist/tools/dispatch-routing.d.ts +6 -0
- package/dist/tools/dispatch-routing.d.ts.map +1 -0
- package/dist/tools/run-pipeline.d.ts.map +1 -1
- package/docs/installation.md +6 -17
- package/docs/intelligence.md +18 -33
- package/docs/optimization-baseline.md +21 -0
- package/docs/rules.md +9 -36
- package/docs/workflows.md +8 -9
- package/package.json +4 -2
- package/src/rules/README.md +10 -0
- package/src/rules/common/coding-style.md +2 -2
- package/src/rules/typescript/patterns.md +1 -1
- package/src/skills/backend-patterns/SKILL.md +6 -0
- package/src/skills/clean-architecture/SKILL.md +6 -0
- package/src/skills/cqrs/SKILL.md +6 -0
- package/src/skills/ddd-architecture/SKILL.md +6 -0
- package/src/skills/event-driven-architecture/SKILL.md +6 -0
- package/src/skills/hexagonal-architecture/SKILL.md +6 -0
- package/src/skills/layered-architecture/SKILL.md +6 -0
- package/src/skills/postgres-patterns/SKILL.md +6 -0
- package/src/skills/saga-architecture/SKILL.md +6 -0
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 readFileSync24, existsSync as existsSync28 } from "fs";
|
|
3
|
+
import { join as join27, basename } from "path";
|
|
4
4
|
import { dirname as dirname4 } from "path";
|
|
5
5
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
6
6
|
|
|
@@ -509,6 +509,154 @@ var workspaceStateTool = tool3({
|
|
|
509
509
|
|
|
510
510
|
// src/tools/run-pipeline.ts
|
|
511
511
|
import { tool as tool4 } from "@opencode-ai/plugin";
|
|
512
|
+
|
|
513
|
+
// src/services/agent-performance.ts
|
|
514
|
+
import { existsSync as existsSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync5, mkdirSync as mkdirSync2 } from "fs";
|
|
515
|
+
import { join as join5 } from "path";
|
|
516
|
+
function perfPath(dir) {
|
|
517
|
+
return join5(codebaseDir(dir), "AGENT_PERF.json");
|
|
518
|
+
}
|
|
519
|
+
function loadStore(dir) {
|
|
520
|
+
const p = perfPath(dir);
|
|
521
|
+
if (!existsSync5(p))
|
|
522
|
+
return { entries: [], updated_at: new Date().toISOString() };
|
|
523
|
+
try {
|
|
524
|
+
return JSON.parse(readFileSync5(p, "utf-8"));
|
|
525
|
+
} catch {
|
|
526
|
+
return { entries: [], updated_at: new Date().toISOString() };
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
function saveStore(dir, store) {
|
|
530
|
+
const cd = codebaseDir(dir);
|
|
531
|
+
if (!existsSync5(cd))
|
|
532
|
+
mkdirSync2(cd, { recursive: true });
|
|
533
|
+
writeFileSync5(perfPath(dir), JSON.stringify(store, null, 2), "utf-8");
|
|
534
|
+
}
|
|
535
|
+
function makeKey(agent, model, task_type) {
|
|
536
|
+
return `${agent}::${model}::${task_type}`;
|
|
537
|
+
}
|
|
538
|
+
function recordRun(dir, agent, model, task_type, success, duration_ms, cost = 0) {
|
|
539
|
+
const store = loadStore(dir);
|
|
540
|
+
const key = makeKey(agent, model, task_type);
|
|
541
|
+
const existing = store.entries.find((e) => makeKey(e.agent, e.model, e.task_type) === key);
|
|
542
|
+
if (existing) {
|
|
543
|
+
existing.runs++;
|
|
544
|
+
if (success)
|
|
545
|
+
existing.successes++;
|
|
546
|
+
else
|
|
547
|
+
existing.failures++;
|
|
548
|
+
existing.total_duration_ms += duration_ms;
|
|
549
|
+
existing.total_cost += cost;
|
|
550
|
+
existing.last_run = new Date().toISOString();
|
|
551
|
+
existing.last_status = success ? "success" : "failure";
|
|
552
|
+
} else {
|
|
553
|
+
store.entries.push({
|
|
554
|
+
agent,
|
|
555
|
+
model,
|
|
556
|
+
task_type,
|
|
557
|
+
runs: 1,
|
|
558
|
+
successes: success ? 1 : 0,
|
|
559
|
+
failures: success ? 0 : 1,
|
|
560
|
+
total_duration_ms: duration_ms,
|
|
561
|
+
total_cost: cost,
|
|
562
|
+
last_run: new Date().toISOString(),
|
|
563
|
+
last_status: success ? "success" : "failure"
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
store.updated_at = new Date().toISOString();
|
|
567
|
+
saveStore(dir, store);
|
|
568
|
+
}
|
|
569
|
+
|
|
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
|
+
// src/tools/dispatch-routing.ts
|
|
616
|
+
function shouldRetry(promptRes) {
|
|
617
|
+
if (!promptRes)
|
|
618
|
+
return false;
|
|
619
|
+
const detail = promptRes.error?.detail;
|
|
620
|
+
if (isTransientError(detail))
|
|
621
|
+
return true;
|
|
622
|
+
const infoError = promptRes.data?.info?.error;
|
|
623
|
+
const text = typeof infoError === "string" ? infoError : JSON.stringify(infoError ?? "");
|
|
624
|
+
return isTransientError(text);
|
|
625
|
+
}
|
|
626
|
+
function isTransientError(text) {
|
|
627
|
+
if (!text)
|
|
628
|
+
return false;
|
|
629
|
+
const haystack = text.toLowerCase();
|
|
630
|
+
return haystack.includes("overload") || haystack.includes("rate limit") || haystack.includes("timeout") || haystack.includes("temporar") || haystack.includes("econnreset");
|
|
631
|
+
}
|
|
632
|
+
function normalizeTaskType(taskType, agent) {
|
|
633
|
+
const normalized = (taskType ?? "").trim().toLowerCase();
|
|
634
|
+
if (isTaskType(normalized))
|
|
635
|
+
return normalized;
|
|
636
|
+
const a = agent.toLowerCase();
|
|
637
|
+
if (a.includes("review"))
|
|
638
|
+
return "review";
|
|
639
|
+
if (a.includes("test"))
|
|
640
|
+
return "testing";
|
|
641
|
+
if (a.includes("debug"))
|
|
642
|
+
return "debugging";
|
|
643
|
+
if (a.includes("security"))
|
|
644
|
+
return "security";
|
|
645
|
+
if (a.includes("doc"))
|
|
646
|
+
return "documentation";
|
|
647
|
+
if (a.includes("architect") || a.includes("planner"))
|
|
648
|
+
return "planning";
|
|
649
|
+
if (a.includes("orchestrator") || a.includes("coordinator"))
|
|
650
|
+
return "orchestration";
|
|
651
|
+
if (a.includes("analyst") || a.includes("research"))
|
|
652
|
+
return "analysis";
|
|
653
|
+
return "implementation";
|
|
654
|
+
}
|
|
655
|
+
function isTaskType(value) {
|
|
656
|
+
return value === "planning" || value === "implementation" || value === "debugging" || value === "review" || value === "testing" || value === "documentation" || value === "analysis" || value === "security" || value === "orchestration";
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// src/tools/run-pipeline.ts
|
|
512
660
|
function extractText(parts) {
|
|
513
661
|
return parts.filter((p) => p.type === "text" && typeof p.text === "string").map((p) => p.text).join(`
|
|
514
662
|
`);
|
|
@@ -519,16 +667,20 @@ function createRunPipelineTool(client) {
|
|
|
519
667
|
args: {
|
|
520
668
|
steps: tool4.schema.array(tool4.schema.object({
|
|
521
669
|
agent: tool4.schema.string(),
|
|
522
|
-
prompt: tool4.schema.string()
|
|
670
|
+
prompt: tool4.schema.string(),
|
|
671
|
+
task_type: tool4.schema.string().optional()
|
|
523
672
|
})),
|
|
524
673
|
initial_context: tool4.schema.string().optional(),
|
|
525
|
-
abort_on_failure: tool4.schema.boolean().optional().default(true)
|
|
674
|
+
abort_on_failure: tool4.schema.boolean().optional().default(true),
|
|
675
|
+
retry_attempts: tool4.schema.number().optional().default(1)
|
|
526
676
|
},
|
|
527
677
|
async execute(args, context) {
|
|
528
678
|
const startTime = Date.now();
|
|
529
679
|
const trace = [];
|
|
530
680
|
let carryContext = args.initial_context ?? "";
|
|
531
681
|
let aborted = false;
|
|
682
|
+
const retryAttempts = typeof args.retry_attempts === "number" ? args.retry_attempts : 1;
|
|
683
|
+
const maxRetries = Math.max(0, Math.floor(retryAttempts));
|
|
532
684
|
let inflightChildId = null;
|
|
533
685
|
const abortHandler = () => {
|
|
534
686
|
if (inflightChildId) {
|
|
@@ -546,6 +698,8 @@ function createRunPipelineTool(client) {
|
|
|
546
698
|
break;
|
|
547
699
|
}
|
|
548
700
|
const stepStart = Date.now();
|
|
701
|
+
const taskType = normalizeTaskType(step.task_type, step.agent);
|
|
702
|
+
const routing = routeModel(context.directory, taskType);
|
|
549
703
|
const stepInput = carryContext ? `${carryContext}
|
|
550
704
|
|
|
551
705
|
---
|
|
@@ -557,28 +711,37 @@ ${step.prompt}` : step.prompt;
|
|
|
557
711
|
});
|
|
558
712
|
if (createRes.error || !createRes.data?.id) {
|
|
559
713
|
const errMsg = `Failed to create session: ${createRes.error?.detail ?? "unknown"}`;
|
|
560
|
-
trace.push({ agent: step.agent, input: stepInput, output: errMsg, duration_ms: Date.now() - stepStart, success: false });
|
|
714
|
+
trace.push({ agent: step.agent, task_type: taskType, model: routing.model, input: stepInput, output: errMsg, duration_ms: Date.now() - stepStart, success: false });
|
|
561
715
|
aborted = true;
|
|
562
716
|
break;
|
|
563
717
|
}
|
|
564
718
|
inflightChildId = createRes.data.id;
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
719
|
+
let promptRes = null;
|
|
720
|
+
let retriesUsed = 0;
|
|
721
|
+
for (let attempt = 0;attempt <= maxRetries; attempt++) {
|
|
722
|
+
promptRes = await client.session.prompt({
|
|
723
|
+
path: { id: inflightChildId },
|
|
724
|
+
body: {
|
|
725
|
+
agent: step.agent,
|
|
726
|
+
model: routing.model,
|
|
727
|
+
parts: [{ type: "text", text: stepInput }],
|
|
728
|
+
tools: { question: false }
|
|
729
|
+
},
|
|
730
|
+
query: { directory: context.directory }
|
|
731
|
+
});
|
|
732
|
+
if (!shouldRetry(promptRes) || attempt === maxRetries)
|
|
733
|
+
break;
|
|
734
|
+
retriesUsed++;
|
|
735
|
+
}
|
|
574
736
|
inflightChildId = null;
|
|
575
737
|
if (context.abort.aborted) {
|
|
576
738
|
aborted = true;
|
|
577
739
|
break;
|
|
578
740
|
}
|
|
579
|
-
if (promptRes.error) {
|
|
580
|
-
const errMsg = `Prompt failed: ${promptRes
|
|
581
|
-
trace.push({ agent: step.agent, session_id: createRes.data.id, input: stepInput, output: errMsg
|
|
741
|
+
if (!promptRes || promptRes.error) {
|
|
742
|
+
const errMsg = `Prompt failed: ${promptRes?.error?.detail ?? "unknown"}`;
|
|
743
|
+
trace.push({ agent: step.agent, session_id: createRes.data.id, task_type: taskType, model: routing.model, input: stepInput, output: `${errMsg}${retriesUsed > 0 ? ` (retries: ${retriesUsed})` : ""}`, duration_ms: Date.now() - stepStart, success: false });
|
|
744
|
+
recordRun(context.directory, step.agent, routing.model, taskType, false, Date.now() - stepStart);
|
|
582
745
|
if (args.abort_on_failure) {
|
|
583
746
|
aborted = true;
|
|
584
747
|
break;
|
|
@@ -588,7 +751,8 @@ ${step.prompt}` : step.prompt;
|
|
|
588
751
|
const info = promptRes.data?.info;
|
|
589
752
|
if (info?.error) {
|
|
590
753
|
const errMsg = `Agent error: ${JSON.stringify(info.error)}`;
|
|
591
|
-
trace.push({ agent: step.agent, session_id: createRes.data.id, input: stepInput, output: errMsg
|
|
754
|
+
trace.push({ agent: step.agent, session_id: createRes.data.id, task_type: taskType, model: routing.model, input: stepInput, output: `${errMsg}${retriesUsed > 0 ? ` (retries: ${retriesUsed})` : ""}`, duration_ms: Date.now() - stepStart, success: false });
|
|
755
|
+
recordRun(context.directory, step.agent, routing.model, taskType, false, Date.now() - stepStart);
|
|
592
756
|
if (args.abort_on_failure) {
|
|
593
757
|
aborted = true;
|
|
594
758
|
break;
|
|
@@ -596,7 +760,8 @@ ${step.prompt}` : step.prompt;
|
|
|
596
760
|
continue;
|
|
597
761
|
}
|
|
598
762
|
const output = extractText(promptRes.data?.parts ?? []);
|
|
599
|
-
trace.push({ agent: step.agent, session_id: createRes.data.id, input: stepInput, output: output || "(no text output)", duration_ms: Date.now() - stepStart, success: true });
|
|
763
|
+
trace.push({ agent: step.agent, session_id: createRes.data.id, task_type: taskType, model: routing.model, input: stepInput, output: output || "(no text output)", duration_ms: Date.now() - stepStart, success: true });
|
|
764
|
+
recordRun(context.directory, step.agent, routing.model, taskType, true, Date.now() - stepStart);
|
|
600
765
|
carryContext = output;
|
|
601
766
|
}
|
|
602
767
|
} finally {
|
|
@@ -623,10 +788,16 @@ function createDelegateTool(client) {
|
|
|
623
788
|
args: {
|
|
624
789
|
agent: tool5.schema.string(),
|
|
625
790
|
prompt: tool5.schema.string(),
|
|
626
|
-
context: tool5.schema.string().optional()
|
|
791
|
+
context: tool5.schema.string().optional(),
|
|
792
|
+
task_type: tool5.schema.string().optional(),
|
|
793
|
+
retry_attempts: tool5.schema.number().optional().default(1)
|
|
627
794
|
},
|
|
628
795
|
async execute(args, context) {
|
|
629
796
|
const startTime = Date.now();
|
|
797
|
+
const taskType = normalizeTaskType(args.task_type, args.agent);
|
|
798
|
+
const routing = routeModel(context.directory, taskType);
|
|
799
|
+
const retryAttempts = typeof args.retry_attempts === "number" ? args.retry_attempts : 1;
|
|
800
|
+
const maxRetries = Math.max(0, Math.floor(retryAttempts));
|
|
630
801
|
const createRes = await client.session.create({
|
|
631
802
|
body: { parentID: context.sessionID, title: `${args.agent}-delegate` },
|
|
632
803
|
query: { directory: context.directory }
|
|
@@ -651,40 +822,60 @@ function createDelegateTool(client) {
|
|
|
651
822
|
---
|
|
652
823
|
|
|
653
824
|
${args.prompt}` : args.prompt;
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
825
|
+
let promptRes = null;
|
|
826
|
+
let retriesUsed = 0;
|
|
827
|
+
for (let attempt = 0;attempt <= maxRetries; attempt++) {
|
|
828
|
+
promptRes = await client.session.prompt({
|
|
829
|
+
path: { id: childId },
|
|
830
|
+
body: {
|
|
831
|
+
agent: args.agent,
|
|
832
|
+
model: routing.model,
|
|
833
|
+
parts: [{ type: "text", text: fullPrompt }],
|
|
834
|
+
tools: { question: false }
|
|
835
|
+
},
|
|
836
|
+
query: { directory: context.directory }
|
|
837
|
+
});
|
|
838
|
+
if (!shouldRetry(promptRes) || attempt === maxRetries)
|
|
839
|
+
break;
|
|
840
|
+
retriesUsed++;
|
|
841
|
+
}
|
|
842
|
+
if (!promptRes || promptRes.error) {
|
|
843
|
+
recordRun(context.directory, args.agent, routing.model, taskType, false, Date.now() - startTime);
|
|
664
844
|
return JSON.stringify({
|
|
665
845
|
agent: args.agent,
|
|
666
846
|
session_id: childId,
|
|
667
847
|
success: false,
|
|
668
|
-
error: `Prompt failed: ${promptRes
|
|
848
|
+
error: `Prompt failed: ${promptRes?.error?.detail ?? "unknown"}`,
|
|
849
|
+
task_type: taskType,
|
|
850
|
+
model: routing.model,
|
|
851
|
+
retries_used: retriesUsed,
|
|
669
852
|
duration_ms: Date.now() - startTime
|
|
670
853
|
});
|
|
671
854
|
}
|
|
672
855
|
const info = promptRes.data?.info;
|
|
673
856
|
if (info?.error) {
|
|
857
|
+
recordRun(context.directory, args.agent, routing.model, taskType, false, Date.now() - startTime);
|
|
674
858
|
return JSON.stringify({
|
|
675
859
|
agent: args.agent,
|
|
676
860
|
session_id: childId,
|
|
677
861
|
success: false,
|
|
678
862
|
error: `Agent error: ${JSON.stringify(info.error)}`,
|
|
863
|
+
task_type: taskType,
|
|
864
|
+
model: routing.model,
|
|
865
|
+
retries_used: retriesUsed,
|
|
679
866
|
duration_ms: Date.now() - startTime
|
|
680
867
|
});
|
|
681
868
|
}
|
|
682
869
|
const output = extractText2(promptRes.data?.parts ?? []);
|
|
870
|
+
recordRun(context.directory, args.agent, routing.model, taskType, true, Date.now() - startTime);
|
|
683
871
|
return JSON.stringify({
|
|
684
872
|
agent: args.agent,
|
|
685
873
|
session_id: childId,
|
|
686
874
|
success: true,
|
|
687
875
|
output: output || "(no text output)",
|
|
876
|
+
task_type: taskType,
|
|
877
|
+
model: routing.model,
|
|
878
|
+
retries_used: retriesUsed,
|
|
688
879
|
duration_ms: Date.now() - startTime
|
|
689
880
|
});
|
|
690
881
|
}
|
|
@@ -693,31 +884,31 @@ ${args.prompt}` : args.prompt;
|
|
|
693
884
|
|
|
694
885
|
// src/tools/repo-memory.ts
|
|
695
886
|
import { tool as tool6 } from "@opencode-ai/plugin";
|
|
696
|
-
import { readFileSync as
|
|
697
|
-
import { join as
|
|
887
|
+
import { readFileSync as readFileSync7, writeFileSync as writeFileSync6, existsSync as existsSync7, mkdirSync as mkdirSync3 } from "fs";
|
|
888
|
+
import { join as join7 } from "path";
|
|
698
889
|
var MEMORY_FILE = "MEMORY.json";
|
|
699
890
|
function memoryPath(directory) {
|
|
700
|
-
return
|
|
891
|
+
return join7(codebaseDir(directory), MEMORY_FILE);
|
|
701
892
|
}
|
|
702
893
|
function emptyMemory() {
|
|
703
894
|
return { version: "1.0", last_updated: new Date().toISOString(), nodes: {} };
|
|
704
895
|
}
|
|
705
896
|
function readMemory(directory) {
|
|
706
897
|
const p = memoryPath(directory);
|
|
707
|
-
if (!
|
|
898
|
+
if (!existsSync7(p))
|
|
708
899
|
return emptyMemory();
|
|
709
900
|
try {
|
|
710
|
-
return JSON.parse(
|
|
901
|
+
return JSON.parse(readFileSync7(p, "utf-8"));
|
|
711
902
|
} catch {
|
|
712
903
|
return emptyMemory();
|
|
713
904
|
}
|
|
714
905
|
}
|
|
715
906
|
function writeMemory(directory, memory) {
|
|
716
907
|
const base = codebaseDir(directory);
|
|
717
|
-
if (!
|
|
718
|
-
|
|
908
|
+
if (!existsSync7(base))
|
|
909
|
+
mkdirSync3(base, { recursive: true });
|
|
719
910
|
memory.last_updated = new Date().toISOString();
|
|
720
|
-
|
|
911
|
+
writeFileSync6(memoryPath(directory), JSON.stringify(memory, null, 2), "utf-8");
|
|
721
912
|
}
|
|
722
913
|
var repoMemoryTool = tool6({
|
|
723
914
|
description: "Repo Memory Graph: read/write/query persistent architecture graph in .codebase/MEMORY.json (modules, dependencies, ownership, bug history, conventions)",
|
|
@@ -794,28 +985,28 @@ var repoMemoryTool = tool6({
|
|
|
794
985
|
|
|
795
986
|
// src/tools/failure-replay.ts
|
|
796
987
|
import { tool as tool7 } from "@opencode-ai/plugin";
|
|
797
|
-
import { readFileSync as
|
|
798
|
-
import { join as
|
|
988
|
+
import { readFileSync as readFileSync8, writeFileSync as writeFileSync7, existsSync as existsSync8, mkdirSync as mkdirSync4 } from "fs";
|
|
989
|
+
import { join as join8 } from "path";
|
|
799
990
|
var FAILURES_FILE = "FAILURES.json";
|
|
800
991
|
function failuresPath(directory) {
|
|
801
|
-
return
|
|
992
|
+
return join8(codebaseDir(directory), FAILURES_FILE);
|
|
802
993
|
}
|
|
803
994
|
function readStore(directory) {
|
|
804
995
|
const p = failuresPath(directory);
|
|
805
|
-
if (!
|
|
996
|
+
if (!existsSync8(p))
|
|
806
997
|
return { version: "1.0", last_updated: new Date().toISOString(), entries: [] };
|
|
807
998
|
try {
|
|
808
|
-
return JSON.parse(
|
|
999
|
+
return JSON.parse(readFileSync8(p, "utf-8"));
|
|
809
1000
|
} catch {
|
|
810
1001
|
return { version: "1.0", last_updated: new Date().toISOString(), entries: [] };
|
|
811
1002
|
}
|
|
812
1003
|
}
|
|
813
1004
|
function writeStore(directory, store) {
|
|
814
1005
|
const base = codebaseDir(directory);
|
|
815
|
-
if (!
|
|
816
|
-
|
|
1006
|
+
if (!existsSync8(base))
|
|
1007
|
+
mkdirSync4(base, { recursive: true });
|
|
817
1008
|
store.last_updated = new Date().toISOString();
|
|
818
|
-
|
|
1009
|
+
writeFileSync7(failuresPath(directory), JSON.stringify(store, null, 2), "utf-8");
|
|
819
1010
|
}
|
|
820
1011
|
var failureReplayTool = tool7({
|
|
821
1012
|
description: "Failure Replay Engine: record and query past failures (reverted commits, failed deployments, flaky tests, bug fixes) in .codebase/FAILURES.json so the agent avoids repeating mistakes",
|
|
@@ -899,17 +1090,17 @@ var failureReplayTool = tool7({
|
|
|
899
1090
|
|
|
900
1091
|
// src/tools/decision-trace.ts
|
|
901
1092
|
import { tool as tool8 } from "@opencode-ai/plugin";
|
|
902
|
-
import { readFileSync as
|
|
903
|
-
import { join as
|
|
1093
|
+
import { readFileSync as readFileSync9, existsSync as existsSync9, mkdirSync as mkdirSync5, appendFileSync } from "fs";
|
|
1094
|
+
import { join as join9 } from "path";
|
|
904
1095
|
var DECISIONS_FILE = "DECISIONS.jsonl";
|
|
905
1096
|
function decisionsPath(directory) {
|
|
906
|
-
return
|
|
1097
|
+
return join9(codebaseDir(directory), DECISIONS_FILE);
|
|
907
1098
|
}
|
|
908
1099
|
function readDecisions(directory) {
|
|
909
1100
|
const p = decisionsPath(directory);
|
|
910
|
-
if (!
|
|
1101
|
+
if (!existsSync9(p))
|
|
911
1102
|
return [];
|
|
912
|
-
return
|
|
1103
|
+
return readFileSync9(p, "utf-8").split(`
|
|
913
1104
|
`).filter((l) => l.trim()).map((l) => {
|
|
914
1105
|
try {
|
|
915
1106
|
return JSON.parse(l);
|
|
@@ -949,8 +1140,8 @@ var decisionTraceTool = tool8({
|
|
|
949
1140
|
case "record": {
|
|
950
1141
|
if (!args.entry)
|
|
951
1142
|
return JSON.stringify({ error: "entry required" });
|
|
952
|
-
if (!
|
|
953
|
-
|
|
1143
|
+
if (!existsSync9(base))
|
|
1144
|
+
mkdirSync5(base, { recursive: true });
|
|
954
1145
|
const entry = { ...args.entry, timestamp: new Date().toISOString() };
|
|
955
1146
|
appendFileSync(decisionsPath(dir), JSON.stringify(entry) + `
|
|
956
1147
|
`, "utf-8");
|
|
@@ -984,28 +1175,28 @@ var decisionTraceTool = tool8({
|
|
|
984
1175
|
|
|
985
1176
|
// src/tools/volatility-map.ts
|
|
986
1177
|
import { tool as tool9 } from "@opencode-ai/plugin";
|
|
987
|
-
import { readFileSync as
|
|
988
|
-
import { join as
|
|
1178
|
+
import { readFileSync as readFileSync10, writeFileSync as writeFileSync9, existsSync as existsSync10, mkdirSync as mkdirSync6 } from "fs";
|
|
1179
|
+
import { join as join10 } from "path";
|
|
989
1180
|
var VOLATILITY_FILE = "VOLATILITY.json";
|
|
990
1181
|
function volatilityPath(directory) {
|
|
991
|
-
return
|
|
1182
|
+
return join10(codebaseDir(directory), VOLATILITY_FILE);
|
|
992
1183
|
}
|
|
993
1184
|
function readStore2(directory) {
|
|
994
1185
|
const p = volatilityPath(directory);
|
|
995
|
-
if (!
|
|
1186
|
+
if (!existsSync10(p))
|
|
996
1187
|
return { version: "1.0", last_updated: new Date().toISOString(), generated_at: new Date().toISOString(), entries: [] };
|
|
997
1188
|
try {
|
|
998
|
-
return JSON.parse(
|
|
1189
|
+
return JSON.parse(readFileSync10(p, "utf-8"));
|
|
999
1190
|
} catch {
|
|
1000
1191
|
return { version: "1.0", last_updated: new Date().toISOString(), generated_at: new Date().toISOString(), entries: [] };
|
|
1001
1192
|
}
|
|
1002
1193
|
}
|
|
1003
1194
|
function writeStore2(directory, store) {
|
|
1004
1195
|
const base = codebaseDir(directory);
|
|
1005
|
-
if (!
|
|
1006
|
-
|
|
1196
|
+
if (!existsSync10(base))
|
|
1197
|
+
mkdirSync6(base, { recursive: true });
|
|
1007
1198
|
store.last_updated = new Date().toISOString();
|
|
1008
|
-
|
|
1199
|
+
writeFileSync9(volatilityPath(directory), JSON.stringify(store, null, 2), "utf-8");
|
|
1009
1200
|
}
|
|
1010
1201
|
function stabilityLabel(churn, hotfixes, todos) {
|
|
1011
1202
|
const score = churn + hotfixes * 10 + todos * 2;
|
|
@@ -1092,28 +1283,28 @@ var volatilityMapTool = tool9({
|
|
|
1092
1283
|
|
|
1093
1284
|
// src/tools/policy-engine.ts
|
|
1094
1285
|
import { tool as tool10 } from "@opencode-ai/plugin";
|
|
1095
|
-
import { readFileSync as
|
|
1096
|
-
import { join as
|
|
1286
|
+
import { readFileSync as readFileSync11, writeFileSync as writeFileSync10, existsSync as existsSync11, mkdirSync as mkdirSync7 } from "fs";
|
|
1287
|
+
import { join as join11 } from "path";
|
|
1097
1288
|
var POLICIES_FILE = "POLICIES.json";
|
|
1098
1289
|
function policiesPath(directory) {
|
|
1099
|
-
return
|
|
1290
|
+
return join11(codebaseDir(directory), POLICIES_FILE);
|
|
1100
1291
|
}
|
|
1101
1292
|
function readStore3(directory) {
|
|
1102
1293
|
const p = policiesPath(directory);
|
|
1103
|
-
if (!
|
|
1294
|
+
if (!existsSync11(p))
|
|
1104
1295
|
return { version: "1.0", last_updated: new Date().toISOString(), policies: [] };
|
|
1105
1296
|
try {
|
|
1106
|
-
return JSON.parse(
|
|
1297
|
+
return JSON.parse(readFileSync11(p, "utf-8"));
|
|
1107
1298
|
} catch {
|
|
1108
1299
|
return { version: "1.0", last_updated: new Date().toISOString(), policies: [] };
|
|
1109
1300
|
}
|
|
1110
1301
|
}
|
|
1111
1302
|
function writeStore3(directory, store) {
|
|
1112
1303
|
const base = codebaseDir(directory);
|
|
1113
|
-
if (!
|
|
1114
|
-
|
|
1304
|
+
if (!existsSync11(base))
|
|
1305
|
+
mkdirSync7(base, { recursive: true });
|
|
1115
1306
|
store.last_updated = new Date().toISOString();
|
|
1116
|
-
|
|
1307
|
+
writeFileSync10(policiesPath(directory), JSON.stringify(store, null, 2), "utf-8");
|
|
1117
1308
|
}
|
|
1118
1309
|
var policyEngineTool = tool10({
|
|
1119
1310
|
description: "Self-Healing Policy Engine: manage .codebase/POLICIES.json — add, list, query, toggle, and record violations of editing policies learned from past failures",
|
|
@@ -1195,7 +1386,7 @@ var policyEngineTool = tool10({
|
|
|
1195
1386
|
|
|
1196
1387
|
// src/tools/hash-edit.ts
|
|
1197
1388
|
import { tool as tool11 } from "@opencode-ai/plugin";
|
|
1198
|
-
import { readFileSync as
|
|
1389
|
+
import { readFileSync as readFileSync12, writeFileSync as writeFileSync11 } from "fs";
|
|
1199
1390
|
import { createHash } from "crypto";
|
|
1200
1391
|
var hashEditTool = tool11({
|
|
1201
1392
|
description: "Reliable file editing with content verification. Takes a target content, its expected MD5 hash, and replacement content. Only applies if the hash matches, preventing edits on stale file versions.",
|
|
@@ -1209,7 +1400,7 @@ var hashEditTool = tool11({
|
|
|
1209
1400
|
const fullPath = args.filePath.startsWith("/") ? args.filePath : `${context.directory}/${args.filePath}`;
|
|
1210
1401
|
let content;
|
|
1211
1402
|
try {
|
|
1212
|
-
content =
|
|
1403
|
+
content = readFileSync12(fullPath, "utf-8");
|
|
1213
1404
|
} catch (e) {
|
|
1214
1405
|
return `Error: Could not read file ${args.filePath}`;
|
|
1215
1406
|
}
|
|
@@ -1223,13 +1414,15 @@ var hashEditTool = tool11({
|
|
|
1223
1414
|
}
|
|
1224
1415
|
}
|
|
1225
1416
|
const newContent = content.replace(args.targetContent, args.replacementContent);
|
|
1226
|
-
|
|
1417
|
+
writeFileSync11(fullPath, newContent, "utf-8");
|
|
1227
1418
|
return `Successfully updated ${args.filePath} using hash-anchored edit.`;
|
|
1228
1419
|
}
|
|
1229
1420
|
});
|
|
1230
1421
|
|
|
1231
1422
|
// src/tools/council.ts
|
|
1232
1423
|
import { tool as tool12 } from "@opencode-ai/plugin";
|
|
1424
|
+
import { appendFileSync as appendFileSync2, existsSync as existsSync12, mkdirSync as mkdirSync8 } from "fs";
|
|
1425
|
+
import { join as join12 } from "path";
|
|
1233
1426
|
function createCouncilTool(client) {
|
|
1234
1427
|
return tool12({
|
|
1235
1428
|
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.",
|
|
@@ -1284,16 +1477,34 @@ Please synthesize these results. Identify areas of agreement, resolve conflicts,
|
|
|
1284
1477
|
},
|
|
1285
1478
|
query: { directory: context.directory }
|
|
1286
1479
|
});
|
|
1287
|
-
|
|
1480
|
+
const synthesis = (finalRes.data?.parts ?? []).filter((p) => p.type === "text").map((p) => p.text).join(`
|
|
1288
1481
|
`);
|
|
1482
|
+
persistCouncilResult(context.directory, {
|
|
1483
|
+
task: args.task,
|
|
1484
|
+
agents,
|
|
1485
|
+
results,
|
|
1486
|
+
synthesis,
|
|
1487
|
+
created_at: new Date().toISOString()
|
|
1488
|
+
});
|
|
1489
|
+
return synthesis;
|
|
1289
1490
|
}
|
|
1290
1491
|
});
|
|
1291
1492
|
}
|
|
1493
|
+
function persistCouncilResult(directory, payload) {
|
|
1494
|
+
try {
|
|
1495
|
+
const base = codebaseDir(directory);
|
|
1496
|
+
if (!existsSync12(base))
|
|
1497
|
+
mkdirSync8(base, { recursive: true });
|
|
1498
|
+
const path = join12(base, "COUNCILS.jsonl");
|
|
1499
|
+
appendFileSync2(path, JSON.stringify(payload) + `
|
|
1500
|
+
`, "utf-8");
|
|
1501
|
+
} catch {}
|
|
1502
|
+
}
|
|
1292
1503
|
|
|
1293
1504
|
// src/tools/context-generator.ts
|
|
1294
1505
|
import { tool as tool13 } from "@opencode-ai/plugin";
|
|
1295
|
-
import { writeFileSync as
|
|
1296
|
-
import { join as
|
|
1506
|
+
import { writeFileSync as writeFileSync12, existsSync as existsSync13, readFileSync as readFileSync13, readdirSync as readdirSync2, statSync } from "fs";
|
|
1507
|
+
import { join as join13 } from "path";
|
|
1297
1508
|
var contextGeneratorTool = tool13({
|
|
1298
1509
|
description: "Auto-generate or update hierarchical context files (AGENTS.md, CLAUDE.md) throughout the project. These files provide critical grounding for AI agents.",
|
|
1299
1510
|
args: {
|
|
@@ -1302,20 +1513,20 @@ var contextGeneratorTool = tool13({
|
|
|
1302
1513
|
},
|
|
1303
1514
|
async execute(args, context) {
|
|
1304
1515
|
const root = context.directory;
|
|
1305
|
-
const target = args.targetDir ?
|
|
1306
|
-
if (!
|
|
1516
|
+
const target = args.targetDir ? join13(root, args.targetDir) : root;
|
|
1517
|
+
if (!existsSync13(target)) {
|
|
1307
1518
|
return `Error: Directory ${target} does not exist.`;
|
|
1308
1519
|
}
|
|
1309
|
-
const agentsMdPath =
|
|
1310
|
-
if (
|
|
1520
|
+
const agentsMdPath = join13(target, "AGENTS.md");
|
|
1521
|
+
if (existsSync13(agentsMdPath) && !args.force) {
|
|
1311
1522
|
return `AGENTS.md already exists in ${target}. Use force: true to overwrite.`;
|
|
1312
1523
|
}
|
|
1313
|
-
const pkgPath =
|
|
1524
|
+
const pkgPath = join13(root, "package.json");
|
|
1314
1525
|
let projectName = "Project";
|
|
1315
1526
|
let techStack = "Unknown";
|
|
1316
|
-
if (
|
|
1527
|
+
if (existsSync13(pkgPath)) {
|
|
1317
1528
|
try {
|
|
1318
|
-
const pkg = JSON.parse(
|
|
1529
|
+
const pkg = JSON.parse(readFileSync13(pkgPath, "utf-8"));
|
|
1319
1530
|
projectName = pkg.name || projectName;
|
|
1320
1531
|
techStack = Object.keys(pkg.dependencies || {}).slice(0, 5).join(", ");
|
|
1321
1532
|
} catch {}
|
|
@@ -1333,7 +1544,7 @@ var contextGeneratorTool = tool13({
|
|
|
1333
1544
|
|
|
1334
1545
|
## Directory Map
|
|
1335
1546
|
${readdirSync2(target).slice(0, 10).map((f) => {
|
|
1336
|
-
const s = statSync(
|
|
1547
|
+
const s = statSync(join13(target, f));
|
|
1337
1548
|
return `- \`${f}\`${s.isDirectory() ? "/" : ""} : [Description]`;
|
|
1338
1549
|
}).join(`
|
|
1339
1550
|
`)}
|
|
@@ -1341,17 +1552,17 @@ ${readdirSync2(target).slice(0, 10).map((f) => {
|
|
|
1341
1552
|
---
|
|
1342
1553
|
Generated by FlowDeck Context Generator.
|
|
1343
1554
|
`;
|
|
1344
|
-
|
|
1555
|
+
writeFileSync12(agentsMdPath, content, "utf-8");
|
|
1345
1556
|
return `Successfully generated AGENTS.md in ${target}.`;
|
|
1346
1557
|
}
|
|
1347
1558
|
});
|
|
1348
1559
|
|
|
1349
1560
|
// src/tools/create-skill.ts
|
|
1350
1561
|
import { tool as tool14 } from "@opencode-ai/plugin";
|
|
1351
|
-
import { mkdirSync as
|
|
1352
|
-
import { join as
|
|
1562
|
+
import { mkdirSync as mkdirSync9, writeFileSync as writeFileSync13, existsSync as existsSync14 } from "fs";
|
|
1563
|
+
import { join as join14, dirname as dirname3 } from "path";
|
|
1353
1564
|
import { fileURLToPath } from "url";
|
|
1354
|
-
var SKILLS_DIR =
|
|
1565
|
+
var SKILLS_DIR = join14(dirname3(fileURLToPath(import.meta.url)), "..", "skills");
|
|
1355
1566
|
var createSkillTool = tool14({
|
|
1356
1567
|
description: "Create a new reusable skill in the FlowDeck skill library (src/skills/). " + "Use this when you discover a repeatable pattern, solve a novel problem with human guidance, " + "or want to capture domain knowledge for future sessions.",
|
|
1357
1568
|
args: {
|
|
@@ -1361,9 +1572,9 @@ var createSkillTool = tool14({
|
|
|
1361
1572
|
tags: tool14.schema.array(tool14.schema.string()).optional().describe("Optional tags for categorisation, e.g. ['performance', 'typescript']")
|
|
1362
1573
|
},
|
|
1363
1574
|
async execute(args) {
|
|
1364
|
-
const skillDir =
|
|
1365
|
-
const skillFile =
|
|
1366
|
-
if (
|
|
1575
|
+
const skillDir = join14(SKILLS_DIR, args.name);
|
|
1576
|
+
const skillFile = join14(skillDir, "SKILL.md");
|
|
1577
|
+
if (existsSync14(skillFile)) {
|
|
1367
1578
|
return `Skill '${args.name}' already exists at ${skillFile}.
|
|
1368
1579
|
` + `Use a different name or delete the existing skill directory first.`;
|
|
1369
1580
|
}
|
|
@@ -1378,8 +1589,8 @@ origin: FlowDeck (self-learned)${tagLine}
|
|
|
1378
1589
|
`;
|
|
1379
1590
|
const fullContent = frontmatter + args.content.trimStart();
|
|
1380
1591
|
try {
|
|
1381
|
-
|
|
1382
|
-
|
|
1592
|
+
mkdirSync9(skillDir, { recursive: true });
|
|
1593
|
+
writeFileSync13(skillFile, fullContent, "utf-8");
|
|
1383
1594
|
return `✓ Skill '${args.name}' created at ${skillFile}
|
|
1384
1595
|
|
|
1385
1596
|
` + `The skill is now part of the FlowDeck library. Restart OpenCode to load it into the active session.`;
|
|
@@ -1391,8 +1602,8 @@ origin: FlowDeck (self-learned)${tagLine}
|
|
|
1391
1602
|
|
|
1392
1603
|
// src/tools/reflect.ts
|
|
1393
1604
|
import { tool as tool15 } from "@opencode-ai/plugin";
|
|
1394
|
-
import { existsSync as
|
|
1395
|
-
import { join as
|
|
1605
|
+
import { existsSync as existsSync15, readFileSync as readFileSync14 } from "fs";
|
|
1606
|
+
import { join as join15 } from "path";
|
|
1396
1607
|
var MAX_ARTIFACT_BYTES = 4000;
|
|
1397
1608
|
function tail(text, maxBytes) {
|
|
1398
1609
|
if (text.length <= maxBytes)
|
|
@@ -1421,11 +1632,11 @@ var reflectTool = tool15({
|
|
|
1421
1632
|
];
|
|
1422
1633
|
let found = 0;
|
|
1423
1634
|
for (const [rel, label] of ARTIFACT_PATHS) {
|
|
1424
|
-
const full =
|
|
1425
|
-
if (!
|
|
1635
|
+
const full = join15(root, rel);
|
|
1636
|
+
if (!existsSync15(full))
|
|
1426
1637
|
continue;
|
|
1427
1638
|
try {
|
|
1428
|
-
const raw =
|
|
1639
|
+
const raw = readFileSync14(full, "utf-8").trim();
|
|
1429
1640
|
if (!raw)
|
|
1430
1641
|
continue;
|
|
1431
1642
|
const count = raw.split(`
|
|
@@ -1449,14 +1660,14 @@ import { tool as tool16 } from "@opencode-ai/plugin";
|
|
|
1449
1660
|
|
|
1450
1661
|
// src/services/memory-store.ts
|
|
1451
1662
|
import { Database } from "bun:sqlite";
|
|
1452
|
-
import { existsSync as
|
|
1453
|
-
import { join as
|
|
1663
|
+
import { existsSync as existsSync16, mkdirSync as mkdirSync10 } from "fs";
|
|
1664
|
+
import { join as join16 } from "path";
|
|
1454
1665
|
import { homedir } from "os";
|
|
1455
|
-
var MEMORY_DIR =
|
|
1456
|
-
var DB_PATH =
|
|
1666
|
+
var MEMORY_DIR = join16(homedir(), ".flowdeck-memory");
|
|
1667
|
+
var DB_PATH = join16(MEMORY_DIR, "memory.db");
|
|
1457
1668
|
function ensureDir() {
|
|
1458
|
-
if (!
|
|
1459
|
-
|
|
1669
|
+
if (!existsSync16(MEMORY_DIR)) {
|
|
1670
|
+
mkdirSync10(MEMORY_DIR, { recursive: true });
|
|
1460
1671
|
}
|
|
1461
1672
|
}
|
|
1462
1673
|
var db = null;
|
|
@@ -1734,16 +1945,16 @@ var memorySearchTool = tool16({
|
|
|
1734
1945
|
// src/tools/memory-status.ts
|
|
1735
1946
|
import { tool as tool17 } from "@opencode-ai/plugin";
|
|
1736
1947
|
import { Database as Database2 } from "bun:sqlite";
|
|
1737
|
-
import { existsSync as
|
|
1738
|
-
import { join as
|
|
1948
|
+
import { existsSync as existsSync17 } from "fs";
|
|
1949
|
+
import { join as join17 } from "path";
|
|
1739
1950
|
import { homedir as homedir2 } from "os";
|
|
1740
|
-
var DB_PATH2 =
|
|
1951
|
+
var DB_PATH2 = join17(homedir2(), ".flowdeck-memory", "memory.db");
|
|
1741
1952
|
var memoryStatusTool = tool17({
|
|
1742
1953
|
description: "Check FlowDeck memory database status, statistics, and recent sessions",
|
|
1743
1954
|
args: {},
|
|
1744
1955
|
async execute(_args, _context) {
|
|
1745
1956
|
try {
|
|
1746
|
-
const exists =
|
|
1957
|
+
const exists = existsSync17(DB_PATH2);
|
|
1747
1958
|
const result = {
|
|
1748
1959
|
database_exists: exists,
|
|
1749
1960
|
path: DB_PATH2,
|
|
@@ -1900,15 +2111,15 @@ var memoryHook = {
|
|
|
1900
2111
|
};
|
|
1901
2112
|
|
|
1902
2113
|
// src/hooks/guard-rails.ts
|
|
1903
|
-
import { existsSync as
|
|
1904
|
-
import { join as
|
|
2114
|
+
import { existsSync as existsSync18, readFileSync as readFileSync15 } from "fs";
|
|
2115
|
+
import { join as join18 } from "path";
|
|
1905
2116
|
var PLANNING_DIR2 = ".planning";
|
|
1906
2117
|
var CONFIG_FILE = "config.json";
|
|
1907
2118
|
var STATE_FILE2 = "STATE.md";
|
|
1908
2119
|
function resolveExecutionMode(configPath, trustScore, volatility) {
|
|
1909
|
-
if (
|
|
2120
|
+
if (existsSync18(configPath)) {
|
|
1910
2121
|
try {
|
|
1911
|
-
const config = JSON.parse(
|
|
2122
|
+
const config = JSON.parse(readFileSync15(configPath, "utf-8"));
|
|
1912
2123
|
if (config.execution_mode === "review-only")
|
|
1913
2124
|
return "review-only";
|
|
1914
2125
|
if (config.execution_mode === "guarded")
|
|
@@ -1962,22 +2173,22 @@ async function guardRailsHook(ctx, input, _output) {
|
|
|
1962
2173
|
if (!ENABLED)
|
|
1963
2174
|
return;
|
|
1964
2175
|
const dir = ctx.directory;
|
|
1965
|
-
const planningDirPath =
|
|
2176
|
+
const planningDirPath = join18(dir, PLANNING_DIR2);
|
|
1966
2177
|
const codebaseDirectory = codebaseDir(dir);
|
|
1967
|
-
const configPath =
|
|
1968
|
-
const statePath2 =
|
|
2178
|
+
const configPath = join18(planningDirPath, CONFIG_FILE);
|
|
2179
|
+
const statePath2 = join18(planningDirPath, STATE_FILE2);
|
|
1969
2180
|
const workspaceRoot = findWorkspaceRoot(dir);
|
|
1970
2181
|
if (workspaceRoot && dir !== workspaceRoot) {
|
|
1971
2182
|
const config = getWorkspaceConfig(dir);
|
|
1972
|
-
if (config && config.workspace_mode === "shared" && !
|
|
2183
|
+
if (config && config.workspace_mode === "shared" && !existsSync18(planningDirPath)) {
|
|
1973
2184
|
const msg = `No .planning/ in this sub-repo. Switch to workspace root: cd ${workspaceRoot}`;
|
|
1974
2185
|
throw new Error(`[flowdeck] BLOCK: ${msg}`);
|
|
1975
2186
|
}
|
|
1976
2187
|
}
|
|
1977
2188
|
if (input.tool === "write" || input.tool === "edit") {
|
|
1978
|
-
if (!
|
|
2189
|
+
if (!existsSync18(planningDirPath))
|
|
1979
2190
|
return;
|
|
1980
|
-
if (!
|
|
2191
|
+
if (!existsSync18(codebaseDirectory)) {
|
|
1981
2192
|
throw new Error(`[flowdeck] WARNING: .codebase/ not found. Run /map-codebase to map the codebase.`);
|
|
1982
2193
|
}
|
|
1983
2194
|
const execMode = resolveExecutionMode(configPath, null);
|
|
@@ -2010,9 +2221,9 @@ async function guardRailsHook(ctx, input, _output) {
|
|
|
2010
2221
|
}
|
|
2011
2222
|
}
|
|
2012
2223
|
function effectiveSeverity(configPath, statePath2) {
|
|
2013
|
-
if (
|
|
2224
|
+
if (existsSync18(configPath)) {
|
|
2014
2225
|
try {
|
|
2015
|
-
const configContent =
|
|
2226
|
+
const configContent = readFileSync15(configPath, "utf-8");
|
|
2016
2227
|
const config = JSON.parse(configContent);
|
|
2017
2228
|
if (config.guard_enforcement === "warn")
|
|
2018
2229
|
return "warn";
|
|
@@ -2028,10 +2239,10 @@ function getEffectiveSeverity(configPath, statePath2) {
|
|
|
2028
2239
|
return effectiveSeverity(configPath, statePath2);
|
|
2029
2240
|
}
|
|
2030
2241
|
function getPlanConfirmed(statePath2) {
|
|
2031
|
-
if (!
|
|
2242
|
+
if (!existsSync18(statePath2))
|
|
2032
2243
|
return false;
|
|
2033
2244
|
try {
|
|
2034
|
-
const content =
|
|
2245
|
+
const content = readFileSync15(statePath2, "utf-8");
|
|
2035
2246
|
const match = content.match(/plan_confirmed:\s*(true|false)/i);
|
|
2036
2247
|
return match ? match[1].toLowerCase() === "true" : false;
|
|
2037
2248
|
} catch {
|
|
@@ -2039,21 +2250,21 @@ function getPlanConfirmed(statePath2) {
|
|
|
2039
2250
|
}
|
|
2040
2251
|
}
|
|
2041
2252
|
function getWarningMessage(planningDir2) {
|
|
2042
|
-
if (!
|
|
2253
|
+
if (!existsSync18(join18(planningDir2, STATE_FILE2))) {
|
|
2043
2254
|
return "No .planning/ found. Run /new-project first.";
|
|
2044
2255
|
}
|
|
2045
2256
|
return "Plan not confirmed. Run /plan and confirm to enable execution.";
|
|
2046
2257
|
}
|
|
2047
2258
|
function getBlockMessage(planningDir2) {
|
|
2048
|
-
if (!
|
|
2259
|
+
if (!existsSync18(join18(planningDir2, STATE_FILE2))) {
|
|
2049
2260
|
return "No .planning/ found. Run /new-project first.";
|
|
2050
2261
|
}
|
|
2051
2262
|
return "Plan not confirmed. Run /plan and confirm to enable execution.";
|
|
2052
2263
|
}
|
|
2053
2264
|
|
|
2054
2265
|
// src/hooks/tool-guard.ts
|
|
2055
|
-
import { existsSync as
|
|
2056
|
-
import { join as
|
|
2266
|
+
import { existsSync as existsSync19, readFileSync as readFileSync16 } from "fs";
|
|
2267
|
+
import { join as join19 } from "path";
|
|
2057
2268
|
var IS_ENABLED = () => process.env.FLOWDECK_TOOL_GUARD_ENABLED === "on";
|
|
2058
2269
|
var BLOCKED_PATTERNS = {
|
|
2059
2270
|
read: [".env", ".pem", ".key", ".secret"],
|
|
@@ -2100,11 +2311,11 @@ function isBlocked(tool18, args) {
|
|
|
2100
2311
|
return null;
|
|
2101
2312
|
}
|
|
2102
2313
|
function checkArchConstraint(directory, filePath) {
|
|
2103
|
-
const constraintsPath =
|
|
2104
|
-
if (!
|
|
2314
|
+
const constraintsPath = join19(codebaseDir(directory), "CONSTRAINTS.md");
|
|
2315
|
+
if (!existsSync19(constraintsPath))
|
|
2105
2316
|
return null;
|
|
2106
2317
|
try {
|
|
2107
|
-
const content =
|
|
2318
|
+
const content = readFileSync16(constraintsPath, "utf-8");
|
|
2108
2319
|
const match = content.match(/## Forbidden Paths\n([\s\S]*?)(?:\n##|$)/);
|
|
2109
2320
|
if (!match)
|
|
2110
2321
|
return null;
|
|
@@ -2151,18 +2362,18 @@ async function toolGuardHook(ctx, input, output) {
|
|
|
2151
2362
|
}
|
|
2152
2363
|
|
|
2153
2364
|
// src/hooks/session-start.ts
|
|
2154
|
-
import { existsSync as
|
|
2365
|
+
import { existsSync as existsSync20, readFileSync as readFileSync17 } from "fs";
|
|
2155
2366
|
async function sessionStartHook(ctx) {
|
|
2156
2367
|
const planningDir2 = ctx.directory + "/.planning";
|
|
2157
2368
|
const codebaseDirectory = codebaseDir(ctx.directory);
|
|
2158
2369
|
const workspaceRoot = findWorkspaceRoot(ctx.directory);
|
|
2159
2370
|
const config = workspaceRoot ? getWorkspaceConfig(ctx.directory) : null;
|
|
2160
|
-
if (!
|
|
2371
|
+
if (!existsSync20(planningDir2)) {
|
|
2161
2372
|
return {
|
|
2162
2373
|
flowdeck_phase: null,
|
|
2163
2374
|
flowdeck_status: "no_plan",
|
|
2164
2375
|
flowdeck_warning: "Run /new-project or /map-codebase to initialize.",
|
|
2165
|
-
flowdeck_has_codebase:
|
|
2376
|
+
flowdeck_has_codebase: existsSync20(codebaseDirectory),
|
|
2166
2377
|
...workspaceRoot && config?.sub_repos ? {
|
|
2167
2378
|
flowdeck_workspace_root: workspaceRoot,
|
|
2168
2379
|
flowdeck_sub_repos: config.sub_repos,
|
|
@@ -2173,7 +2384,7 @@ async function sessionStartHook(ctx) {
|
|
|
2173
2384
|
}
|
|
2174
2385
|
try {
|
|
2175
2386
|
const stateFilePath = statePath(ctx.directory);
|
|
2176
|
-
const content =
|
|
2387
|
+
const content = readFileSync17(stateFilePath, "utf-8");
|
|
2177
2388
|
const state = parseState(content);
|
|
2178
2389
|
const currentPhase = state["current_phase"] || {};
|
|
2179
2390
|
const result = {
|
|
@@ -2181,7 +2392,7 @@ async function sessionStartHook(ctx) {
|
|
|
2181
2392
|
flowdeck_status: currentPhase["status"] ?? null,
|
|
2182
2393
|
flowdeck_steps_pending: currentPhase["steps_pending"] ?? null,
|
|
2183
2394
|
flowdeck_last_action: currentPhase["last_action"] ?? null,
|
|
2184
|
-
flowdeck_has_codebase:
|
|
2395
|
+
flowdeck_has_codebase: existsSync20(codebaseDirectory)
|
|
2185
2396
|
};
|
|
2186
2397
|
if (workspaceRoot && config?.sub_repos && config.sub_repos.length > 0) {
|
|
2187
2398
|
result.flowdeck_workspace_root = workspaceRoot;
|
|
@@ -2196,7 +2407,7 @@ async function sessionStartHook(ctx) {
|
|
|
2196
2407
|
flowdeck_phase: null,
|
|
2197
2408
|
flowdeck_status: "error",
|
|
2198
2409
|
flowdeck_warning: "State file unreadable. Continuing without flowdeck context.",
|
|
2199
|
-
flowdeck_has_codebase:
|
|
2410
|
+
flowdeck_has_codebase: existsSync20(codebaseDirectory)
|
|
2200
2411
|
};
|
|
2201
2412
|
if (workspaceRoot && config?.sub_repos && config.sub_repos.length > 0) {
|
|
2202
2413
|
result.flowdeck_workspace_root = workspaceRoot;
|
|
@@ -2262,8 +2473,8 @@ function notifyPermissionNeeded(tool18) {
|
|
|
2262
2473
|
}
|
|
2263
2474
|
|
|
2264
2475
|
// src/hooks/patch-trust.ts
|
|
2265
|
-
import { existsSync as
|
|
2266
|
-
import { join as
|
|
2476
|
+
import { existsSync as existsSync21, readFileSync as readFileSync18 } from "fs";
|
|
2477
|
+
import { join as join20 } from "path";
|
|
2267
2478
|
var HIGH_RISK_KEYWORDS = [
|
|
2268
2479
|
"password",
|
|
2269
2480
|
"secret",
|
|
@@ -2285,11 +2496,11 @@ var HIGH_RISK_KEYWORDS = [
|
|
|
2285
2496
|
"privilege"
|
|
2286
2497
|
];
|
|
2287
2498
|
function loadVolatility(directory) {
|
|
2288
|
-
const p =
|
|
2289
|
-
if (!
|
|
2499
|
+
const p = join20(codebaseDir(directory), "VOLATILITY.json");
|
|
2500
|
+
if (!existsSync21(p))
|
|
2290
2501
|
return {};
|
|
2291
2502
|
try {
|
|
2292
|
-
const data = JSON.parse(
|
|
2503
|
+
const data = JSON.parse(readFileSync18(p, "utf-8"));
|
|
2293
2504
|
const map = {};
|
|
2294
2505
|
for (const entry of data.entries ?? [])
|
|
2295
2506
|
map[entry.path] = entry.stability;
|
|
@@ -2299,11 +2510,11 @@ function loadVolatility(directory) {
|
|
|
2299
2510
|
}
|
|
2300
2511
|
}
|
|
2301
2512
|
function loadFailedPaths(directory) {
|
|
2302
|
-
const p =
|
|
2303
|
-
if (!
|
|
2513
|
+
const p = join20(codebaseDir(directory), "FAILURES.json");
|
|
2514
|
+
if (!existsSync21(p))
|
|
2304
2515
|
return [];
|
|
2305
2516
|
try {
|
|
2306
|
-
const data = JSON.parse(
|
|
2517
|
+
const data = JSON.parse(readFileSync18(p, "utf-8"));
|
|
2307
2518
|
return (data.entries ?? []).flatMap((e) => e.affected_paths ?? []);
|
|
2308
2519
|
} catch {
|
|
2309
2520
|
return [];
|
|
@@ -2368,8 +2579,8 @@ async function patchTrustHook(ctx, input, output) {
|
|
|
2368
2579
|
}
|
|
2369
2580
|
|
|
2370
2581
|
// src/hooks/decision-trace-hook.ts
|
|
2371
|
-
import { existsSync as
|
|
2372
|
-
import { join as
|
|
2582
|
+
import { existsSync as existsSync22, mkdirSync as mkdirSync11, appendFileSync as appendFileSync3 } from "fs";
|
|
2583
|
+
import { join as join21 } from "path";
|
|
2373
2584
|
async function decisionTraceHook(ctx, input, output) {
|
|
2374
2585
|
if (input.tool !== "write" && input.tool !== "edit")
|
|
2375
2586
|
return;
|
|
@@ -2378,8 +2589,8 @@ async function decisionTraceHook(ctx, input, output) {
|
|
|
2378
2589
|
return;
|
|
2379
2590
|
const base = codebaseDir(ctx.directory);
|
|
2380
2591
|
try {
|
|
2381
|
-
if (!
|
|
2382
|
-
|
|
2592
|
+
if (!existsSync22(base))
|
|
2593
|
+
mkdirSync11(base, { recursive: true });
|
|
2383
2594
|
const entry = {
|
|
2384
2595
|
timestamp: new Date().toISOString(),
|
|
2385
2596
|
file_path: filePath,
|
|
@@ -2391,62 +2602,87 @@ async function decisionTraceHook(ctx, input, output) {
|
|
|
2391
2602
|
risk_level: "unknown",
|
|
2392
2603
|
auto_recorded: true
|
|
2393
2604
|
};
|
|
2394
|
-
|
|
2605
|
+
appendFileSync3(join21(base, "DECISIONS.jsonl"), JSON.stringify(entry) + `
|
|
2395
2606
|
`, "utf-8");
|
|
2396
2607
|
} catch {}
|
|
2397
2608
|
}
|
|
2398
2609
|
|
|
2399
2610
|
// src/services/telemetry.ts
|
|
2400
|
-
import { existsSync as
|
|
2401
|
-
import { join as
|
|
2611
|
+
import { existsSync as existsSync23, readFileSync as readFileSync19, appendFileSync as appendFileSync4, mkdirSync as mkdirSync12 } from "fs";
|
|
2612
|
+
import { join as join22 } from "path";
|
|
2402
2613
|
import { randomUUID } from "crypto";
|
|
2403
2614
|
function telemetryPath(dir) {
|
|
2404
|
-
return
|
|
2615
|
+
return join22(codebaseDir(dir), "TELEMETRY.jsonl");
|
|
2405
2616
|
}
|
|
2406
2617
|
function appendEvent(dir, partial) {
|
|
2407
2618
|
if (process.env.TELEMETRY_ENABLED !== "true")
|
|
2408
2619
|
return null;
|
|
2409
2620
|
const cd = codebaseDir(dir);
|
|
2410
|
-
if (!
|
|
2411
|
-
|
|
2621
|
+
if (!existsSync23(cd))
|
|
2622
|
+
mkdirSync12(cd, { recursive: true });
|
|
2412
2623
|
const event = {
|
|
2413
2624
|
id: randomUUID(),
|
|
2414
2625
|
ts: new Date().toISOString(),
|
|
2415
2626
|
...partial
|
|
2416
2627
|
};
|
|
2417
|
-
|
|
2628
|
+
appendFileSync4(telemetryPath(dir), JSON.stringify(event) + `
|
|
2418
2629
|
`, "utf-8");
|
|
2419
2630
|
return event;
|
|
2420
2631
|
}
|
|
2421
2632
|
|
|
2422
2633
|
// src/hooks/telemetry-hook.ts
|
|
2634
|
+
function resolveIds(toolInput) {
|
|
2635
|
+
const session_id = toolInput.sessionID ?? toolInput.sessionId ?? process.env.OPENCODE_SESSION_ID ?? "session-0";
|
|
2636
|
+
const run_id = toolInput.messageID ?? toolInput.messageId ?? toolInput.runID ?? toolInput.runId ?? process.env.OPENCODE_RUN_ID ?? "run-0";
|
|
2637
|
+
return { session_id, run_id };
|
|
2638
|
+
}
|
|
2639
|
+
function inferStatus(output) {
|
|
2640
|
+
if (output.error)
|
|
2641
|
+
return "error";
|
|
2642
|
+
if (typeof output.output !== "string")
|
|
2643
|
+
return "ok";
|
|
2644
|
+
const text = output.output.trim();
|
|
2645
|
+
if (!text)
|
|
2646
|
+
return "ok";
|
|
2647
|
+
try {
|
|
2648
|
+
const parsed = JSON.parse(text);
|
|
2649
|
+
if (parsed.success === false || parsed.error || parsed.status === "error")
|
|
2650
|
+
return "error";
|
|
2651
|
+
return "ok";
|
|
2652
|
+
} catch {
|
|
2653
|
+
return "ok";
|
|
2654
|
+
}
|
|
2655
|
+
}
|
|
2423
2656
|
async function telemetryHook(context, toolInput, output) {
|
|
2424
2657
|
const dir = context.directory ?? process.cwd();
|
|
2425
2658
|
const tool18 = toolInput.name ?? toolInput.tool ?? "unknown";
|
|
2659
|
+
const ids = resolveIds(toolInput);
|
|
2426
2660
|
appendEvent(dir, {
|
|
2427
|
-
session_id:
|
|
2428
|
-
run_id:
|
|
2661
|
+
session_id: ids.session_id,
|
|
2662
|
+
run_id: ids.run_id,
|
|
2429
2663
|
event: "tool.call",
|
|
2430
2664
|
tool: tool18,
|
|
2431
2665
|
status: "ok",
|
|
2432
2666
|
meta: { parameters: output.args ?? {} }
|
|
2433
2667
|
});
|
|
2434
2668
|
}
|
|
2435
|
-
async function telemetryAfterHook(context, toolInput,
|
|
2669
|
+
async function telemetryAfterHook(context, toolInput, output) {
|
|
2436
2670
|
const dir = context.directory ?? process.cwd();
|
|
2437
2671
|
const tool18 = toolInput.name ?? toolInput.tool ?? "unknown";
|
|
2672
|
+
const ids = resolveIds(toolInput);
|
|
2673
|
+
const status = inferStatus(output);
|
|
2438
2674
|
appendEvent(dir, {
|
|
2439
|
-
session_id:
|
|
2440
|
-
run_id:
|
|
2675
|
+
session_id: ids.session_id,
|
|
2676
|
+
run_id: ids.run_id,
|
|
2441
2677
|
event: "tool.complete",
|
|
2442
2678
|
tool: tool18,
|
|
2443
|
-
status
|
|
2679
|
+
status
|
|
2444
2680
|
});
|
|
2445
2681
|
}
|
|
2446
2682
|
|
|
2447
2683
|
// src/services/approval-manager.ts
|
|
2448
|
-
import { existsSync as
|
|
2449
|
-
import { join as
|
|
2684
|
+
import { existsSync as existsSync24, readFileSync as readFileSync20, writeFileSync as writeFileSync14, mkdirSync as mkdirSync13 } from "fs";
|
|
2685
|
+
import { join as join23 } from "path";
|
|
2450
2686
|
var APPROVAL_TTL_MS = 30 * 60 * 1000;
|
|
2451
2687
|
var SENSITIVE_PATTERNS = [
|
|
2452
2688
|
/auth/i,
|
|
@@ -2483,20 +2719,20 @@ function isSensitivePath(filePath) {
|
|
|
2483
2719
|
return SENSITIVE_PATTERNS.some((p) => p.test(filePath));
|
|
2484
2720
|
}
|
|
2485
2721
|
function approvalsPath(dir) {
|
|
2486
|
-
return
|
|
2722
|
+
return join23(codebaseDir(dir), "APPROVALS.json");
|
|
2487
2723
|
}
|
|
2488
|
-
function
|
|
2724
|
+
function loadStore2(dir) {
|
|
2489
2725
|
const p = approvalsPath(dir);
|
|
2490
|
-
if (!
|
|
2726
|
+
if (!existsSync24(p))
|
|
2491
2727
|
return { requests: [] };
|
|
2492
2728
|
try {
|
|
2493
|
-
return JSON.parse(
|
|
2729
|
+
return JSON.parse(readFileSync20(p, "utf-8"));
|
|
2494
2730
|
} catch {
|
|
2495
2731
|
return { requests: [] };
|
|
2496
2732
|
}
|
|
2497
2733
|
}
|
|
2498
2734
|
function checkApproval(dir, file_path, command) {
|
|
2499
|
-
const store =
|
|
2735
|
+
const store = loadStore2(dir);
|
|
2500
2736
|
const now = Date.now();
|
|
2501
2737
|
return store.requests.filter((r) => r.status === "approved" && r.resolved_at && (r.file_path === file_path || r.trigger === command) && now - new Date(r.resolved_at).getTime() < APPROVAL_TTL_MS).sort((a, b) => b.resolved_at.localeCompare(a.resolved_at)).at(0) ?? null;
|
|
2502
2738
|
}
|
|
@@ -2585,8 +2821,8 @@ function createContextWindowMonitorHook() {
|
|
|
2585
2821
|
}
|
|
2586
2822
|
|
|
2587
2823
|
// src/hooks/shell-env-hook.ts
|
|
2588
|
-
import { existsSync as
|
|
2589
|
-
import { join as
|
|
2824
|
+
import { existsSync as existsSync25, readFileSync as readFileSync21 } from "fs";
|
|
2825
|
+
import { join as join24 } from "path";
|
|
2590
2826
|
import { createRequire } from "module";
|
|
2591
2827
|
var _version;
|
|
2592
2828
|
function getVersion() {
|
|
@@ -2622,7 +2858,7 @@ var MARKER_TO_LANG = {
|
|
|
2622
2858
|
};
|
|
2623
2859
|
function detectPackageManager(root) {
|
|
2624
2860
|
for (const [lockfile, pm] of Object.entries(LOCKFILE_TO_PM)) {
|
|
2625
|
-
if (
|
|
2861
|
+
if (existsSync25(join24(root, lockfile)))
|
|
2626
2862
|
return pm;
|
|
2627
2863
|
}
|
|
2628
2864
|
return;
|
|
@@ -2631,7 +2867,7 @@ function detectLanguages(root) {
|
|
|
2631
2867
|
const langs = [];
|
|
2632
2868
|
const seen = new Set;
|
|
2633
2869
|
for (const [marker, lang] of Object.entries(MARKER_TO_LANG)) {
|
|
2634
|
-
if (!seen.has(lang) &&
|
|
2870
|
+
if (!seen.has(lang) && existsSync25(join24(root, marker))) {
|
|
2635
2871
|
langs.push(lang);
|
|
2636
2872
|
seen.add(lang);
|
|
2637
2873
|
}
|
|
@@ -2639,11 +2875,11 @@ function detectLanguages(root) {
|
|
|
2639
2875
|
return langs;
|
|
2640
2876
|
}
|
|
2641
2877
|
function readCurrentPhase(root) {
|
|
2642
|
-
const statePath2 =
|
|
2643
|
-
if (!
|
|
2878
|
+
const statePath2 = join24(root, ".planning", "STATE.md");
|
|
2879
|
+
if (!existsSync25(statePath2))
|
|
2644
2880
|
return;
|
|
2645
2881
|
try {
|
|
2646
|
-
const content =
|
|
2882
|
+
const content = readFileSync21(statePath2, "utf-8");
|
|
2647
2883
|
const match = content.match(/phase:\s*(\S+)/i);
|
|
2648
2884
|
return match?.[1];
|
|
2649
2885
|
} catch {
|
|
@@ -2737,14 +2973,13 @@ function createSessionIdleHook(client, tracker) {
|
|
|
2737
2973
|
if (edited.length > 10) {
|
|
2738
2974
|
await client.app.log({ body: { service: "flowdeck", level: "info", message: ` … and ${edited.length - 10} more` } }).catch(() => {});
|
|
2739
2975
|
}
|
|
2740
|
-
tracker.clear();
|
|
2741
2976
|
} catch {}
|
|
2742
2977
|
};
|
|
2743
2978
|
}
|
|
2744
2979
|
|
|
2745
2980
|
// src/hooks/compaction-hook.ts
|
|
2746
|
-
import { existsSync as
|
|
2747
|
-
import { join as
|
|
2981
|
+
import { existsSync as existsSync26, readFileSync as readFileSync22 } from "fs";
|
|
2982
|
+
import { join as join25 } from "path";
|
|
2748
2983
|
var STRUCTURED_SUMMARY_PROMPT = `
|
|
2749
2984
|
When summarizing this session, you MUST include the following sections:
|
|
2750
2985
|
|
|
@@ -2783,11 +3018,11 @@ For each: agent name, status, description, session_id.
|
|
|
2783
3018
|
**RESUME, DON'T RESTART.** Use session_id to continue existing sessions.
|
|
2784
3019
|
`;
|
|
2785
3020
|
function readPlanningState2(directory) {
|
|
2786
|
-
const statePath2 =
|
|
2787
|
-
if (!
|
|
3021
|
+
const statePath2 = join25(directory, ".planning", "STATE.md");
|
|
3022
|
+
if (!existsSync26(statePath2))
|
|
2788
3023
|
return null;
|
|
2789
3024
|
try {
|
|
2790
|
-
const content =
|
|
3025
|
+
const content = readFileSync22(statePath2, "utf-8");
|
|
2791
3026
|
return content.slice(0, 1500);
|
|
2792
3027
|
} catch {
|
|
2793
3028
|
return null;
|
|
@@ -2882,13 +3117,24 @@ function blockMessage(toolName) {
|
|
|
2882
3117
|
class OrchestratorGuard {
|
|
2883
3118
|
primarySessionId = null;
|
|
2884
3119
|
onEvent(event) {
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
const
|
|
2888
|
-
if (
|
|
2889
|
-
this.primarySessionId =
|
|
3120
|
+
const eventType = event.type ?? "";
|
|
3121
|
+
if (eventType === "session.deleted") {
|
|
3122
|
+
const deletedId = extractSessionId(event);
|
|
3123
|
+
if (deletedId && deletedId === this.primarySessionId) {
|
|
3124
|
+
this.primarySessionId = null;
|
|
2890
3125
|
}
|
|
3126
|
+
return;
|
|
2891
3127
|
}
|
|
3128
|
+
if (eventType !== "session.created" && eventType !== "session.started")
|
|
3129
|
+
return;
|
|
3130
|
+
if (this.primarySessionId !== null)
|
|
3131
|
+
return;
|
|
3132
|
+
const id = extractSessionId(event);
|
|
3133
|
+
if (!id)
|
|
3134
|
+
return;
|
|
3135
|
+
if (extractParentSessionId(event))
|
|
3136
|
+
return;
|
|
3137
|
+
this.primarySessionId = id;
|
|
2892
3138
|
}
|
|
2893
3139
|
check(sessionId, toolName) {
|
|
2894
3140
|
if (DISABLED)
|
|
@@ -2904,6 +3150,20 @@ class OrchestratorGuard {
|
|
|
2904
3150
|
}
|
|
2905
3151
|
}
|
|
2906
3152
|
}
|
|
3153
|
+
function extractSessionId(event) {
|
|
3154
|
+
const props = event.properties;
|
|
3155
|
+
const inner = event.event;
|
|
3156
|
+
const info = props?.info;
|
|
3157
|
+
const id = event.sessionID ?? event.sessionId ?? inner?.sessionID ?? inner?.sessionId ?? info?.id;
|
|
3158
|
+
return id ?? null;
|
|
3159
|
+
}
|
|
3160
|
+
function extractParentSessionId(event) {
|
|
3161
|
+
const props = event.properties;
|
|
3162
|
+
const inner = event.event;
|
|
3163
|
+
const info = props?.info;
|
|
3164
|
+
const parentId = inner?.parentID ?? inner?.parentId ?? info?.parentID ?? info?.parentId;
|
|
3165
|
+
return parentId ?? null;
|
|
3166
|
+
}
|
|
2907
3167
|
|
|
2908
3168
|
// src/hooks/auto-learn-hook.ts
|
|
2909
3169
|
var MIN_EDITS = 1;
|
|
@@ -5880,23 +6140,23 @@ function getAgentConfigs(agentModels) {
|
|
|
5880
6140
|
}
|
|
5881
6141
|
|
|
5882
6142
|
// src/config/loader.ts
|
|
5883
|
-
import { existsSync as
|
|
5884
|
-
import { join as
|
|
6143
|
+
import { existsSync as existsSync27, readFileSync as readFileSync23 } from "fs";
|
|
6144
|
+
import { join as join26 } from "path";
|
|
5885
6145
|
import { homedir as homedir3 } from "os";
|
|
5886
6146
|
var CONFIG_FILENAME = "flowdeck.json";
|
|
5887
6147
|
function getGlobalConfigDir() {
|
|
5888
|
-
return process.env.OPENCODE_CONFIG_DIR || (process.env.XDG_CONFIG_HOME ?
|
|
6148
|
+
return process.env.OPENCODE_CONFIG_DIR || (process.env.XDG_CONFIG_HOME ? join26(process.env.XDG_CONFIG_HOME, "opencode") : join26(homedir3(), ".config", "opencode"));
|
|
5889
6149
|
}
|
|
5890
6150
|
function loadFlowDeckConfig(directory) {
|
|
5891
6151
|
const candidates = [];
|
|
5892
6152
|
if (directory) {
|
|
5893
|
-
candidates.push(
|
|
6153
|
+
candidates.push(join26(directory, ".opencode", CONFIG_FILENAME));
|
|
5894
6154
|
}
|
|
5895
|
-
candidates.push(
|
|
6155
|
+
candidates.push(join26(getGlobalConfigDir(), CONFIG_FILENAME));
|
|
5896
6156
|
for (const configPath of candidates) {
|
|
5897
|
-
if (
|
|
6157
|
+
if (existsSync27(configPath)) {
|
|
5898
6158
|
try {
|
|
5899
|
-
const content =
|
|
6159
|
+
const content = readFileSync23(configPath, "utf-8");
|
|
5900
6160
|
return JSON.parse(content);
|
|
5901
6161
|
} catch {
|
|
5902
6162
|
console.warn(`[flowdeck] Failed to load config from ${configPath}`);
|
|
@@ -5908,13 +6168,13 @@ function loadFlowDeckConfig(directory) {
|
|
|
5908
6168
|
// src/index.ts
|
|
5909
6169
|
function loadRulePaths() {
|
|
5910
6170
|
const __dir = dirname4(fileURLToPath2(import.meta.url));
|
|
5911
|
-
const rulesDir =
|
|
5912
|
-
if (!
|
|
6171
|
+
const rulesDir = join27(__dir, "..", "src", "rules");
|
|
6172
|
+
if (!existsSync28(rulesDir))
|
|
5913
6173
|
return [];
|
|
5914
6174
|
const paths = [];
|
|
5915
6175
|
function walk(dir) {
|
|
5916
6176
|
for (const entry of readdirSync3(dir, { withFileTypes: true })) {
|
|
5917
|
-
const full =
|
|
6177
|
+
const full = join27(dir, entry.name);
|
|
5918
6178
|
if (entry.isDirectory()) {
|
|
5919
6179
|
walk(full);
|
|
5920
6180
|
} else if (entry.isFile() && entry.name.endsWith(".md") && entry.name !== "README.md") {
|
|
@@ -5927,8 +6187,8 @@ function loadRulePaths() {
|
|
|
5927
6187
|
}
|
|
5928
6188
|
function loadCommands() {
|
|
5929
6189
|
const __dir = dirname4(fileURLToPath2(import.meta.url));
|
|
5930
|
-
const commandsDir =
|
|
5931
|
-
if (!
|
|
6190
|
+
const commandsDir = join27(__dir, "..", "src", "commands");
|
|
6191
|
+
if (!existsSync28(commandsDir))
|
|
5932
6192
|
return {};
|
|
5933
6193
|
const commands = {};
|
|
5934
6194
|
try {
|
|
@@ -5936,7 +6196,7 @@ function loadCommands() {
|
|
|
5936
6196
|
if (!file.endsWith(".md"))
|
|
5937
6197
|
continue;
|
|
5938
6198
|
const name = basename(file, ".md");
|
|
5939
|
-
const raw =
|
|
6199
|
+
const raw = readFileSync24(join27(commandsDir, file), "utf-8");
|
|
5940
6200
|
let description;
|
|
5941
6201
|
let template = raw;
|
|
5942
6202
|
const fmMatch = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/);
|
|
@@ -6013,8 +6273,8 @@ var plugin = async (input, _options) => {
|
|
|
6013
6273
|
}
|
|
6014
6274
|
}
|
|
6015
6275
|
}
|
|
6016
|
-
const skillsDir =
|
|
6017
|
-
if (
|
|
6276
|
+
const skillsDir = join27(dirname4(fileURLToPath2(import.meta.url)), "..", "src", "skills");
|
|
6277
|
+
if (existsSync28(skillsDir)) {
|
|
6018
6278
|
const cfgAny = cfg;
|
|
6019
6279
|
if (!cfgAny.skills || typeof cfgAny.skills !== "object") {
|
|
6020
6280
|
cfgAny.skills = { paths: [] };
|
|
@@ -6100,8 +6360,12 @@ var plugin = async (input, _options) => {
|
|
|
6100
6360
|
await contextMonitor.event({ event });
|
|
6101
6361
|
orchestratorGuard.onEvent(event);
|
|
6102
6362
|
if (type === "session.idle") {
|
|
6103
|
-
|
|
6104
|
-
|
|
6363
|
+
try {
|
|
6364
|
+
await sessionIdleHook();
|
|
6365
|
+
await autoLearnHook();
|
|
6366
|
+
} finally {
|
|
6367
|
+
fileTracker.clear();
|
|
6368
|
+
}
|
|
6105
6369
|
}
|
|
6106
6370
|
},
|
|
6107
6371
|
"tool.execute.before": async (toolInput, toolOutput) => {
|