@dv.nghiem/flowdeck 0.2.4 → 0.3.1
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 +24 -41
- package/dist/hooks/approval-hook.d.ts +6 -0
- package/dist/hooks/approval-hook.d.ts.map +1 -1
- package/dist/hooks/guard-rails.d.ts +0 -8
- package/dist/hooks/guard-rails.d.ts.map +1 -1
- package/dist/hooks/memory-hook.d.ts +21 -0
- package/dist/hooks/memory-hook.d.ts.map +1 -0
- package/dist/hooks/orchestrator-guard-hook.d.ts.map +1 -1
- package/dist/hooks/patch-trust.d.ts.map +1 -1
- package/dist/hooks/todo-hook.d.ts +1 -7
- package/dist/hooks/todo-hook.d.ts.map +1 -1
- package/dist/hooks/tool-guard.d.ts +1 -0
- package/dist/hooks/tool-guard.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +728 -428
- package/dist/services/memory-store.d.ts +40 -0
- package/dist/services/memory-store.d.ts.map +1 -0
- package/dist/services/policy-compiler.d.ts.map +1 -1
- package/dist/tools/memory-search.d.ts +3 -0
- package/dist/tools/memory-search.d.ts.map +1 -0
- package/docs/commands/fd-doctor.md +21 -0
- package/docs/commands/fd-quick.md +33 -0
- package/docs/commands/fd-reflect.md +23 -0
- package/docs/commands/fd-status.md +31 -0
- package/docs/commands/fd-translate-intent.md +17 -0
- package/docs/commands.md +209 -271
- package/docs/configuration.md +5 -2
- package/docs/index.md +22 -28
- package/docs/memory.md +69 -0
- package/docs/quick-start.md +1 -1
- package/package.json +1 -1
- package/src/commands/fd-deploy-check.md +131 -11
- package/src/commands/fd-new-project.md +14 -1
- package/src/commands/fd-quick.md +60 -0
- package/src/commands/fd-reflect.md +41 -2
- package/src/commands/fd-status.md +84 -0
- package/src/rules/README.md +8 -7
- package/src/skills/agent-harness-construction/SKILL.md +227 -0
- package/src/skills/api-design/SKILL.md +5 -0
- package/src/skills/backend-patterns/SKILL.md +105 -0
- package/src/skills/clean-architecture/SKILL.md +85 -0
- package/src/skills/cqrs/SKILL.md +230 -0
- package/src/skills/ddd-architecture/SKILL.md +104 -0
- package/src/skills/django-patterns/SKILL.md +304 -0
- package/src/skills/django-tdd/SKILL.md +297 -0
- package/src/skills/event-driven-architecture/SKILL.md +152 -0
- package/src/skills/frontend-pattern/SKILL.md +159 -0
- package/src/skills/hexagonal-architecture/SKILL.md +80 -0
- package/src/skills/layered-architecture/SKILL.md +64 -0
- package/src/skills/postgres-patterns/SKILL.md +74 -0
- package/src/skills/python-patterns/SKILL.md +5 -0
- package/src/skills/saga-architecture/SKILL.md +113 -0
- package/dist/tools/run-parallel.d.ts +0 -4
- package/dist/tools/run-parallel.d.ts.map +0 -1
- package/docs/command-migration.md +0 -175
- package/docs/commands/fd-analyze-change.md +0 -107
- package/docs/commands/fd-dashboard.md +0 -11
- package/docs/commands/fd-evaluate-risk.md +0 -134
- package/docs/commands/fd-guarded-edit.md +0 -105
- package/docs/commands/fd-progress.md +0 -11
- package/docs/commands/fd-review-code.md +0 -29
- package/docs/commands/fd-roadmap.md +0 -10
- package/docs/commands/fd-settings.md +0 -10
- package/docs/parallel-execution.md +0 -255
- package/src/commands/fd-analyze-change.md +0 -57
- package/src/commands/fd-approve.md +0 -64
- package/src/commands/fd-blast-radius.md +0 -49
- package/src/commands/fd-dashboard.md +0 -57
- package/src/commands/fd-evaluate-risk.md +0 -62
- package/src/commands/fd-guarded-edit.md +0 -69
- package/src/commands/fd-impact-radar.md +0 -51
- package/src/commands/fd-learn.md +0 -36
- package/src/commands/fd-progress.md +0 -50
- package/src/commands/fd-regression-predict.md +0 -57
- package/src/commands/fd-review-code.md +0 -96
- package/src/commands/fd-review-route.md +0 -54
- package/src/commands/fd-roadmap.md +0 -46
- package/src/commands/fd-settings.md +0 -57
- package/src/commands/fd-test-gap.md +0 -54
- package/src/commands/fd-volatility-map.md +0 -64
- package/src/commands/fd-workspace-status.md +0 -34
- package/src/skills/parallel-execute/SKILL.md +0 -92
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
|
-
import { readdirSync as readdirSync3, readFileSync as readFileSync22, existsSync as
|
|
2
|
+
import { readdirSync as readdirSync3, readFileSync as readFileSync22, existsSync as existsSync24 } from "fs";
|
|
3
3
|
import { join as join23, basename } from "path";
|
|
4
4
|
import { dirname as dirname4 } from "path";
|
|
5
5
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
@@ -507,198 +507,22 @@ var workspaceStateTool = tool3({
|
|
|
507
507
|
}
|
|
508
508
|
});
|
|
509
509
|
|
|
510
|
-
// src/tools/run-
|
|
510
|
+
// src/tools/run-pipeline.ts
|
|
511
511
|
import { tool as tool4 } from "@opencode-ai/plugin";
|
|
512
|
-
|
|
513
|
-
// src/services/telemetry.ts
|
|
514
|
-
import { existsSync as existsSync5, readFileSync as readFileSync5, appendFileSync, mkdirSync as mkdirSync2 } from "fs";
|
|
515
|
-
import { join as join5 } from "path";
|
|
516
|
-
import { randomUUID } from "crypto";
|
|
517
|
-
function telemetryPath(dir) {
|
|
518
|
-
return join5(codebaseDir(dir), "TELEMETRY.jsonl");
|
|
519
|
-
}
|
|
520
|
-
function appendEvent(dir, partial) {
|
|
521
|
-
if (process.env.TELEMETRY_ENABLED !== "true")
|
|
522
|
-
return null;
|
|
523
|
-
const cd = codebaseDir(dir);
|
|
524
|
-
if (!existsSync5(cd))
|
|
525
|
-
mkdirSync2(cd, { recursive: true });
|
|
526
|
-
const event = {
|
|
527
|
-
id: randomUUID(),
|
|
528
|
-
ts: new Date().toISOString(),
|
|
529
|
-
...partial
|
|
530
|
-
};
|
|
531
|
-
appendFileSync(telemetryPath(dir), JSON.stringify(event) + `
|
|
532
|
-
`, "utf-8");
|
|
533
|
-
return event;
|
|
534
|
-
}
|
|
535
|
-
|
|
536
|
-
// src/tools/run-parallel.ts
|
|
537
|
-
import { writeFileSync as writeFileSync5 } from "fs";
|
|
538
|
-
import { join as join6 } from "path";
|
|
539
512
|
function extractText(parts) {
|
|
540
513
|
return parts.filter((p) => p.type === "text" && typeof p.text === "string").map((p) => p.text).join(`
|
|
541
514
|
`);
|
|
542
515
|
}
|
|
543
|
-
function createRunParallelTool(client) {
|
|
544
|
-
return tool4({
|
|
545
|
-
description: "Run multiple agents in parallel. All tasks execute simultaneously via child sessions. Returns combined results with per-agent wall time. Partial results returned on failure.",
|
|
546
|
-
args: {
|
|
547
|
-
tasks: tool4.schema.array(tool4.schema.object({
|
|
548
|
-
agent: tool4.schema.string(),
|
|
549
|
-
prompt: tool4.schema.string(),
|
|
550
|
-
context: tool4.schema.string().optional()
|
|
551
|
-
}))
|
|
552
|
-
},
|
|
553
|
-
async execute(args, context) {
|
|
554
|
-
const startTime = Date.now();
|
|
555
|
-
const childSessionIds = [];
|
|
556
|
-
context.abort.addEventListener("abort", () => {
|
|
557
|
-
for (const id of childSessionIds) {
|
|
558
|
-
client.session.abort({
|
|
559
|
-
path: { id },
|
|
560
|
-
query: { directory: context.directory }
|
|
561
|
-
}).catch(() => {});
|
|
562
|
-
}
|
|
563
|
-
});
|
|
564
|
-
const dir = context.directory ?? process.cwd();
|
|
565
|
-
const promises = args.tasks.map(async (task) => {
|
|
566
|
-
const taskStart = Date.now();
|
|
567
|
-
const createRes = await client.session.create({
|
|
568
|
-
body: { parentID: context.sessionID, title: `${task.agent}-subtask` },
|
|
569
|
-
query: { directory: context.directory }
|
|
570
|
-
});
|
|
571
|
-
if (createRes.error || !createRes.data?.id) {
|
|
572
|
-
return {
|
|
573
|
-
agent: task.agent,
|
|
574
|
-
success: false,
|
|
575
|
-
error: `Failed to create session: ${createRes.error?.detail ?? "unknown"}`,
|
|
576
|
-
duration_ms: Date.now() - taskStart
|
|
577
|
-
};
|
|
578
|
-
}
|
|
579
|
-
const childId = createRes.data.id;
|
|
580
|
-
childSessionIds.push(childId);
|
|
581
|
-
appendEvent(dir, {
|
|
582
|
-
session_id: context.sessionID,
|
|
583
|
-
run_id: process.env.OPENCODE_RUN_ID ?? "run-0",
|
|
584
|
-
event: "agent.dispatch",
|
|
585
|
-
agent: task.agent,
|
|
586
|
-
status: "ok",
|
|
587
|
-
meta: { child_session_id: childId, task_index: args.tasks.findIndex((t) => t.agent === task.agent) }
|
|
588
|
-
});
|
|
589
|
-
const fullPrompt = task.context ? `${task.context}
|
|
590
|
-
|
|
591
|
-
---
|
|
592
|
-
|
|
593
|
-
${task.prompt}` : task.prompt;
|
|
594
|
-
const promptRes = await client.session.prompt({
|
|
595
|
-
path: { id: childId },
|
|
596
|
-
body: {
|
|
597
|
-
agent: task.agent,
|
|
598
|
-
parts: [{ type: "text", text: fullPrompt }],
|
|
599
|
-
tools: { question: false }
|
|
600
|
-
},
|
|
601
|
-
query: { directory: context.directory }
|
|
602
|
-
});
|
|
603
|
-
if (promptRes.error) {
|
|
604
|
-
appendEvent(dir, {
|
|
605
|
-
session_id: context.sessionID,
|
|
606
|
-
run_id: process.env.OPENCODE_RUN_ID ?? "run-0",
|
|
607
|
-
event: "agent.complete",
|
|
608
|
-
agent: task.agent,
|
|
609
|
-
status: "error",
|
|
610
|
-
duration_ms: Date.now() - taskStart,
|
|
611
|
-
meta: { child_session_id: childId, error: `Prompt failed: ${promptRes.error?.detail ?? "unknown"}` }
|
|
612
|
-
});
|
|
613
|
-
return {
|
|
614
|
-
agent: task.agent,
|
|
615
|
-
session_id: childId,
|
|
616
|
-
success: false,
|
|
617
|
-
error: `Prompt failed: ${promptRes.error?.detail ?? "unknown"}`,
|
|
618
|
-
duration_ms: Date.now() - taskStart
|
|
619
|
-
};
|
|
620
|
-
}
|
|
621
|
-
const info = promptRes.data?.info;
|
|
622
|
-
if (info?.error) {
|
|
623
|
-
appendEvent(dir, {
|
|
624
|
-
session_id: context.sessionID,
|
|
625
|
-
run_id: process.env.OPENCODE_RUN_ID ?? "run-0",
|
|
626
|
-
event: "agent.complete",
|
|
627
|
-
agent: task.agent,
|
|
628
|
-
status: "error",
|
|
629
|
-
duration_ms: Date.now() - taskStart,
|
|
630
|
-
meta: { child_session_id: childId, error: JSON.stringify(info.error) }
|
|
631
|
-
});
|
|
632
|
-
return {
|
|
633
|
-
agent: task.agent,
|
|
634
|
-
session_id: childId,
|
|
635
|
-
success: false,
|
|
636
|
-
error: `Agent error: ${JSON.stringify(info.error)}`,
|
|
637
|
-
duration_ms: Date.now() - taskStart
|
|
638
|
-
};
|
|
639
|
-
}
|
|
640
|
-
const output = extractText(promptRes.data?.parts ?? []);
|
|
641
|
-
appendEvent(dir, {
|
|
642
|
-
session_id: context.sessionID,
|
|
643
|
-
run_id: process.env.OPENCODE_RUN_ID ?? "run-0",
|
|
644
|
-
event: "agent.complete",
|
|
645
|
-
agent: task.agent,
|
|
646
|
-
status: "ok",
|
|
647
|
-
duration_ms: Date.now() - taskStart,
|
|
648
|
-
meta: { child_session_id: childId, output_length: output?.length ?? 0 }
|
|
649
|
-
});
|
|
650
|
-
return {
|
|
651
|
-
agent: task.agent,
|
|
652
|
-
session_id: childId,
|
|
653
|
-
success: true,
|
|
654
|
-
output: output || "(no text output)",
|
|
655
|
-
duration_ms: Date.now() - taskStart
|
|
656
|
-
};
|
|
657
|
-
});
|
|
658
|
-
const settled = await Promise.allSettled(promises);
|
|
659
|
-
const results = settled.map((result, i) => {
|
|
660
|
-
if (result.status === "fulfilled")
|
|
661
|
-
return result.value;
|
|
662
|
-
return {
|
|
663
|
-
agent: args.tasks[i].agent,
|
|
664
|
-
success: false,
|
|
665
|
-
error: result.reason?.message || String(result.reason),
|
|
666
|
-
duration_ms: Date.now() - startTime
|
|
667
|
-
};
|
|
668
|
-
});
|
|
669
|
-
const progress = {
|
|
670
|
-
total: args.tasks.length,
|
|
671
|
-
completed: results.filter((r) => r.success || r.error).length,
|
|
672
|
-
in_progress: childSessionIds.length,
|
|
673
|
-
results: results.map((r) => ({ agent: r.agent, success: r.success, duration_ms: r.duration_ms })),
|
|
674
|
-
total_duration_ms: Date.now() - startTime
|
|
675
|
-
};
|
|
676
|
-
writeFileSync5(join6(codebaseDir(dir), "parallel-progress.json"), JSON.stringify(progress, null, 2));
|
|
677
|
-
return JSON.stringify({
|
|
678
|
-
results,
|
|
679
|
-
total_duration_ms: Date.now() - startTime,
|
|
680
|
-
failures: results.filter((r) => !r.success).map((r) => r.agent)
|
|
681
|
-
});
|
|
682
|
-
}
|
|
683
|
-
});
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
// src/tools/run-pipeline.ts
|
|
687
|
-
import { tool as tool5 } from "@opencode-ai/plugin";
|
|
688
|
-
function extractText2(parts) {
|
|
689
|
-
return parts.filter((p) => p.type === "text" && typeof p.text === "string").map((p) => p.text).join(`
|
|
690
|
-
`);
|
|
691
|
-
}
|
|
692
516
|
function createRunPipelineTool(client) {
|
|
693
|
-
return
|
|
517
|
+
return tool4({
|
|
694
518
|
description: "Run agents in sequential pipeline. Each step's output is appended to the next step's context. One fresh child session per step. Returns full trace with session ID, input/output/duration per step.",
|
|
695
519
|
args: {
|
|
696
|
-
steps:
|
|
697
|
-
agent:
|
|
698
|
-
prompt:
|
|
520
|
+
steps: tool4.schema.array(tool4.schema.object({
|
|
521
|
+
agent: tool4.schema.string(),
|
|
522
|
+
prompt: tool4.schema.string()
|
|
699
523
|
})),
|
|
700
|
-
initial_context:
|
|
701
|
-
abort_on_failure:
|
|
524
|
+
initial_context: tool4.schema.string().optional(),
|
|
525
|
+
abort_on_failure: tool4.schema.boolean().optional().default(true)
|
|
702
526
|
},
|
|
703
527
|
async execute(args, context) {
|
|
704
528
|
const startTime = Date.now();
|
|
@@ -771,7 +595,7 @@ ${step.prompt}` : step.prompt;
|
|
|
771
595
|
}
|
|
772
596
|
continue;
|
|
773
597
|
}
|
|
774
|
-
const output =
|
|
598
|
+
const output = extractText(promptRes.data?.parts ?? []);
|
|
775
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 });
|
|
776
600
|
carryContext = output;
|
|
777
601
|
}
|
|
@@ -788,18 +612,18 @@ ${step.prompt}` : step.prompt;
|
|
|
788
612
|
}
|
|
789
613
|
|
|
790
614
|
// src/tools/delegate.ts
|
|
791
|
-
import { tool as
|
|
792
|
-
function
|
|
615
|
+
import { tool as tool5 } from "@opencode-ai/plugin";
|
|
616
|
+
function extractText2(parts) {
|
|
793
617
|
return parts.filter((p) => p.type === "text" && typeof p.text === "string").map((p) => p.text).join(`
|
|
794
618
|
`);
|
|
795
619
|
}
|
|
796
620
|
function createDelegateTool(client) {
|
|
797
|
-
return
|
|
621
|
+
return tool5({
|
|
798
622
|
description: "Delegate a task to a single agent via a child session. Returns the agent's output.",
|
|
799
623
|
args: {
|
|
800
|
-
agent:
|
|
801
|
-
prompt:
|
|
802
|
-
context:
|
|
624
|
+
agent: tool5.schema.string(),
|
|
625
|
+
prompt: tool5.schema.string(),
|
|
626
|
+
context: tool5.schema.string().optional()
|
|
803
627
|
},
|
|
804
628
|
async execute(args, context) {
|
|
805
629
|
const startTime = Date.now();
|
|
@@ -855,7 +679,7 @@ ${args.prompt}` : args.prompt;
|
|
|
855
679
|
duration_ms: Date.now() - startTime
|
|
856
680
|
});
|
|
857
681
|
}
|
|
858
|
-
const output =
|
|
682
|
+
const output = extractText2(promptRes.data?.parts ?? []);
|
|
859
683
|
return JSON.stringify({
|
|
860
684
|
agent: args.agent,
|
|
861
685
|
session_id: childId,
|
|
@@ -868,53 +692,53 @@ ${args.prompt}` : args.prompt;
|
|
|
868
692
|
}
|
|
869
693
|
|
|
870
694
|
// src/tools/repo-memory.ts
|
|
871
|
-
import { tool as
|
|
872
|
-
import { readFileSync as
|
|
873
|
-
import { join as
|
|
695
|
+
import { tool as tool6 } from "@opencode-ai/plugin";
|
|
696
|
+
import { readFileSync as readFileSync5, writeFileSync as writeFileSync5, existsSync as existsSync5, mkdirSync as mkdirSync2 } from "fs";
|
|
697
|
+
import { join as join5 } from "path";
|
|
874
698
|
var MEMORY_FILE = "MEMORY.json";
|
|
875
699
|
function memoryPath(directory) {
|
|
876
|
-
return
|
|
700
|
+
return join5(codebaseDir(directory), MEMORY_FILE);
|
|
877
701
|
}
|
|
878
702
|
function emptyMemory() {
|
|
879
703
|
return { version: "1.0", last_updated: new Date().toISOString(), nodes: {} };
|
|
880
704
|
}
|
|
881
705
|
function readMemory(directory) {
|
|
882
706
|
const p = memoryPath(directory);
|
|
883
|
-
if (!
|
|
707
|
+
if (!existsSync5(p))
|
|
884
708
|
return emptyMemory();
|
|
885
709
|
try {
|
|
886
|
-
return JSON.parse(
|
|
710
|
+
return JSON.parse(readFileSync5(p, "utf-8"));
|
|
887
711
|
} catch {
|
|
888
712
|
return emptyMemory();
|
|
889
713
|
}
|
|
890
714
|
}
|
|
891
715
|
function writeMemory(directory, memory) {
|
|
892
716
|
const base = codebaseDir(directory);
|
|
893
|
-
if (!
|
|
894
|
-
|
|
717
|
+
if (!existsSync5(base))
|
|
718
|
+
mkdirSync2(base, { recursive: true });
|
|
895
719
|
memory.last_updated = new Date().toISOString();
|
|
896
|
-
|
|
720
|
+
writeFileSync5(memoryPath(directory), JSON.stringify(memory, null, 2), "utf-8");
|
|
897
721
|
}
|
|
898
|
-
var repoMemoryTool =
|
|
722
|
+
var repoMemoryTool = tool6({
|
|
899
723
|
description: "Repo Memory Graph: read/write/query persistent architecture graph in .codebase/MEMORY.json (modules, dependencies, ownership, bug history, conventions)",
|
|
900
724
|
args: {
|
|
901
|
-
action:
|
|
902
|
-
node_id:
|
|
903
|
-
node:
|
|
904
|
-
type:
|
|
905
|
-
path:
|
|
906
|
-
owner:
|
|
907
|
-
tags:
|
|
908
|
-
dependencies:
|
|
909
|
-
dependents:
|
|
910
|
-
bug_history:
|
|
911
|
-
conventions:
|
|
725
|
+
action: tool6.schema.enum(["read", "write_node", "query", "delete_node"]),
|
|
726
|
+
node_id: tool6.schema.string().optional(),
|
|
727
|
+
node: tool6.schema.object({
|
|
728
|
+
type: tool6.schema.enum(["module", "service", "api", "schema", "config"]),
|
|
729
|
+
path: tool6.schema.string(),
|
|
730
|
+
owner: tool6.schema.string().optional(),
|
|
731
|
+
tags: tool6.schema.array(tool6.schema.string()),
|
|
732
|
+
dependencies: tool6.schema.array(tool6.schema.string()),
|
|
733
|
+
dependents: tool6.schema.array(tool6.schema.string()),
|
|
734
|
+
bug_history: tool6.schema.array(tool6.schema.string()),
|
|
735
|
+
conventions: tool6.schema.array(tool6.schema.string())
|
|
912
736
|
}).optional(),
|
|
913
|
-
query:
|
|
914
|
-
type:
|
|
915
|
-
owner:
|
|
916
|
-
tag:
|
|
917
|
-
path_prefix:
|
|
737
|
+
query: tool6.schema.object({
|
|
738
|
+
type: tool6.schema.enum(["module", "service", "api", "schema", "config"]).optional(),
|
|
739
|
+
owner: tool6.schema.string().optional(),
|
|
740
|
+
tag: tool6.schema.string().optional(),
|
|
741
|
+
path_prefix: tool6.schema.string().optional()
|
|
918
742
|
}).optional()
|
|
919
743
|
},
|
|
920
744
|
async execute(args, context) {
|
|
@@ -969,50 +793,50 @@ var repoMemoryTool = tool7({
|
|
|
969
793
|
});
|
|
970
794
|
|
|
971
795
|
// src/tools/failure-replay.ts
|
|
972
|
-
import { tool as
|
|
973
|
-
import { readFileSync as
|
|
974
|
-
import { join as
|
|
796
|
+
import { tool as tool7 } from "@opencode-ai/plugin";
|
|
797
|
+
import { readFileSync as readFileSync6, writeFileSync as writeFileSync6, existsSync as existsSync6, mkdirSync as mkdirSync3 } from "fs";
|
|
798
|
+
import { join as join6 } from "path";
|
|
975
799
|
var FAILURES_FILE = "FAILURES.json";
|
|
976
800
|
function failuresPath(directory) {
|
|
977
|
-
return
|
|
801
|
+
return join6(codebaseDir(directory), FAILURES_FILE);
|
|
978
802
|
}
|
|
979
803
|
function readStore(directory) {
|
|
980
804
|
const p = failuresPath(directory);
|
|
981
|
-
if (!
|
|
805
|
+
if (!existsSync6(p))
|
|
982
806
|
return { version: "1.0", last_updated: new Date().toISOString(), entries: [] };
|
|
983
807
|
try {
|
|
984
|
-
return JSON.parse(
|
|
808
|
+
return JSON.parse(readFileSync6(p, "utf-8"));
|
|
985
809
|
} catch {
|
|
986
810
|
return { version: "1.0", last_updated: new Date().toISOString(), entries: [] };
|
|
987
811
|
}
|
|
988
812
|
}
|
|
989
813
|
function writeStore(directory, store) {
|
|
990
814
|
const base = codebaseDir(directory);
|
|
991
|
-
if (!
|
|
992
|
-
|
|
815
|
+
if (!existsSync6(base))
|
|
816
|
+
mkdirSync3(base, { recursive: true });
|
|
993
817
|
store.last_updated = new Date().toISOString();
|
|
994
|
-
|
|
818
|
+
writeFileSync6(failuresPath(directory), JSON.stringify(store, null, 2), "utf-8");
|
|
995
819
|
}
|
|
996
|
-
var failureReplayTool =
|
|
820
|
+
var failureReplayTool = tool7({
|
|
997
821
|
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",
|
|
998
822
|
args: {
|
|
999
|
-
action:
|
|
1000
|
-
entry:
|
|
1001
|
-
id:
|
|
1002
|
-
type:
|
|
1003
|
-
description:
|
|
1004
|
-
affected_paths:
|
|
1005
|
-
root_cause:
|
|
1006
|
-
fix_applied:
|
|
1007
|
-
tags:
|
|
823
|
+
action: tool7.schema.enum(["record", "query", "list", "mark_resolved"]),
|
|
824
|
+
entry: tool7.schema.object({
|
|
825
|
+
id: tool7.schema.string(),
|
|
826
|
+
type: tool7.schema.enum(["reverted_commit", "failed_deployment", "flaky_test", "bug_fix", "build_failure"]),
|
|
827
|
+
description: tool7.schema.string(),
|
|
828
|
+
affected_paths: tool7.schema.array(tool7.schema.string()),
|
|
829
|
+
root_cause: tool7.schema.string().optional(),
|
|
830
|
+
fix_applied: tool7.schema.string().optional(),
|
|
831
|
+
tags: tool7.schema.array(tool7.schema.string())
|
|
1008
832
|
}).optional(),
|
|
1009
|
-
query:
|
|
1010
|
-
type:
|
|
1011
|
-
path_prefix:
|
|
1012
|
-
tag:
|
|
1013
|
-
limit:
|
|
833
|
+
query: tool7.schema.object({
|
|
834
|
+
type: tool7.schema.enum(["reverted_commit", "failed_deployment", "flaky_test", "bug_fix", "build_failure"]).optional(),
|
|
835
|
+
path_prefix: tool7.schema.string().optional(),
|
|
836
|
+
tag: tool7.schema.string().optional(),
|
|
837
|
+
limit: tool7.schema.number().optional()
|
|
1014
838
|
}).optional(),
|
|
1015
|
-
entry_id:
|
|
839
|
+
entry_id: tool7.schema.string().optional()
|
|
1016
840
|
},
|
|
1017
841
|
async execute(args, context) {
|
|
1018
842
|
const dir = context.directory ?? process.cwd();
|
|
@@ -1074,18 +898,18 @@ var failureReplayTool = tool8({
|
|
|
1074
898
|
});
|
|
1075
899
|
|
|
1076
900
|
// src/tools/decision-trace.ts
|
|
1077
|
-
import { tool as
|
|
1078
|
-
import { readFileSync as
|
|
1079
|
-
import { join as
|
|
901
|
+
import { tool as tool8 } from "@opencode-ai/plugin";
|
|
902
|
+
import { readFileSync as readFileSync7, existsSync as existsSync7, mkdirSync as mkdirSync4, appendFileSync } from "fs";
|
|
903
|
+
import { join as join7 } from "path";
|
|
1080
904
|
var DECISIONS_FILE = "DECISIONS.jsonl";
|
|
1081
905
|
function decisionsPath(directory) {
|
|
1082
|
-
return
|
|
906
|
+
return join7(codebaseDir(directory), DECISIONS_FILE);
|
|
1083
907
|
}
|
|
1084
908
|
function readDecisions(directory) {
|
|
1085
909
|
const p = decisionsPath(directory);
|
|
1086
|
-
if (!
|
|
910
|
+
if (!existsSync7(p))
|
|
1087
911
|
return [];
|
|
1088
|
-
return
|
|
912
|
+
return readFileSync7(p, "utf-8").split(`
|
|
1089
913
|
`).filter((l) => l.trim()).map((l) => {
|
|
1090
914
|
try {
|
|
1091
915
|
return JSON.parse(l);
|
|
@@ -1094,29 +918,29 @@ function readDecisions(directory) {
|
|
|
1094
918
|
}
|
|
1095
919
|
}).filter(Boolean);
|
|
1096
920
|
}
|
|
1097
|
-
var decisionTraceTool =
|
|
921
|
+
var decisionTraceTool = tool8({
|
|
1098
922
|
description: "Decision Trace: record why the agent changed something, what evidence was used, and assumptions made. Stored in .codebase/DECISIONS.jsonl for fast review.",
|
|
1099
923
|
args: {
|
|
1100
|
-
action:
|
|
1101
|
-
entry:
|
|
1102
|
-
id:
|
|
1103
|
-
file_path:
|
|
1104
|
-
change_type:
|
|
1105
|
-
rationale:
|
|
1106
|
-
evidence:
|
|
1107
|
-
assumptions:
|
|
1108
|
-
alternatives_considered:
|
|
1109
|
-
risk_level:
|
|
1110
|
-
agent:
|
|
1111
|
-
session_id:
|
|
924
|
+
action: tool8.schema.enum(["record", "query", "get_for_file"]),
|
|
925
|
+
entry: tool8.schema.object({
|
|
926
|
+
id: tool8.schema.string(),
|
|
927
|
+
file_path: tool8.schema.string(),
|
|
928
|
+
change_type: tool8.schema.enum(["create", "edit", "delete", "refactor"]),
|
|
929
|
+
rationale: tool8.schema.string(),
|
|
930
|
+
evidence: tool8.schema.array(tool8.schema.string()),
|
|
931
|
+
assumptions: tool8.schema.array(tool8.schema.string()),
|
|
932
|
+
alternatives_considered: tool8.schema.array(tool8.schema.string()),
|
|
933
|
+
risk_level: tool8.schema.enum(["low", "medium", "high"]),
|
|
934
|
+
agent: tool8.schema.string().optional(),
|
|
935
|
+
session_id: tool8.schema.string().optional()
|
|
1112
936
|
}).optional(),
|
|
1113
|
-
query:
|
|
1114
|
-
file_path:
|
|
1115
|
-
change_type:
|
|
1116
|
-
risk_level:
|
|
1117
|
-
limit:
|
|
937
|
+
query: tool8.schema.object({
|
|
938
|
+
file_path: tool8.schema.string().optional(),
|
|
939
|
+
change_type: tool8.schema.enum(["create", "edit", "delete", "refactor"]).optional(),
|
|
940
|
+
risk_level: tool8.schema.enum(["low", "medium", "high"]).optional(),
|
|
941
|
+
limit: tool8.schema.number().optional()
|
|
1118
942
|
}).optional(),
|
|
1119
|
-
file_path:
|
|
943
|
+
file_path: tool8.schema.string().optional()
|
|
1120
944
|
},
|
|
1121
945
|
async execute(args, context) {
|
|
1122
946
|
const dir = context.directory ?? process.cwd();
|
|
@@ -1125,10 +949,10 @@ var decisionTraceTool = tool9({
|
|
|
1125
949
|
case "record": {
|
|
1126
950
|
if (!args.entry)
|
|
1127
951
|
return JSON.stringify({ error: "entry required" });
|
|
1128
|
-
if (!
|
|
1129
|
-
|
|
952
|
+
if (!existsSync7(base))
|
|
953
|
+
mkdirSync4(base, { recursive: true });
|
|
1130
954
|
const entry = { ...args.entry, timestamp: new Date().toISOString() };
|
|
1131
|
-
|
|
955
|
+
appendFileSync(decisionsPath(dir), JSON.stringify(entry) + `
|
|
1132
956
|
`, "utf-8");
|
|
1133
957
|
return JSON.stringify({ success: true, id: args.entry.id });
|
|
1134
958
|
}
|
|
@@ -1159,29 +983,29 @@ var decisionTraceTool = tool9({
|
|
|
1159
983
|
});
|
|
1160
984
|
|
|
1161
985
|
// src/tools/volatility-map.ts
|
|
1162
|
-
import { tool as
|
|
1163
|
-
import { readFileSync as
|
|
1164
|
-
import { join as
|
|
986
|
+
import { tool as tool9 } from "@opencode-ai/plugin";
|
|
987
|
+
import { readFileSync as readFileSync8, writeFileSync as writeFileSync8, existsSync as existsSync8, mkdirSync as mkdirSync5 } from "fs";
|
|
988
|
+
import { join as join8 } from "path";
|
|
1165
989
|
var VOLATILITY_FILE = "VOLATILITY.json";
|
|
1166
990
|
function volatilityPath(directory) {
|
|
1167
|
-
return
|
|
991
|
+
return join8(codebaseDir(directory), VOLATILITY_FILE);
|
|
1168
992
|
}
|
|
1169
993
|
function readStore2(directory) {
|
|
1170
994
|
const p = volatilityPath(directory);
|
|
1171
|
-
if (!
|
|
995
|
+
if (!existsSync8(p))
|
|
1172
996
|
return { version: "1.0", last_updated: new Date().toISOString(), generated_at: new Date().toISOString(), entries: [] };
|
|
1173
997
|
try {
|
|
1174
|
-
return JSON.parse(
|
|
998
|
+
return JSON.parse(readFileSync8(p, "utf-8"));
|
|
1175
999
|
} catch {
|
|
1176
1000
|
return { version: "1.0", last_updated: new Date().toISOString(), generated_at: new Date().toISOString(), entries: [] };
|
|
1177
1001
|
}
|
|
1178
1002
|
}
|
|
1179
1003
|
function writeStore2(directory, store) {
|
|
1180
1004
|
const base = codebaseDir(directory);
|
|
1181
|
-
if (!
|
|
1182
|
-
|
|
1005
|
+
if (!existsSync8(base))
|
|
1006
|
+
mkdirSync5(base, { recursive: true });
|
|
1183
1007
|
store.last_updated = new Date().toISOString();
|
|
1184
|
-
|
|
1008
|
+
writeFileSync8(volatilityPath(directory), JSON.stringify(store, null, 2), "utf-8");
|
|
1185
1009
|
}
|
|
1186
1010
|
function stabilityLabel(churn, hotfixes, todos) {
|
|
1187
1011
|
const score = churn + hotfixes * 10 + todos * 2;
|
|
@@ -1193,29 +1017,29 @@ function stabilityLabel(churn, hotfixes, todos) {
|
|
|
1193
1017
|
return "moderate";
|
|
1194
1018
|
return "stable";
|
|
1195
1019
|
}
|
|
1196
|
-
var volatilityMapTool =
|
|
1020
|
+
var volatilityMapTool = tool9({
|
|
1197
1021
|
description: "Codebase Volatility Map: read/write/query .codebase/VOLATILITY.json — highlights unstable zones based on churn, hotfix frequency, and TODO clusters",
|
|
1198
1022
|
args: {
|
|
1199
|
-
action:
|
|
1200
|
-
entries:
|
|
1201
|
-
path:
|
|
1202
|
-
churn_score:
|
|
1203
|
-
hotfix_count:
|
|
1204
|
-
todo_count:
|
|
1205
|
-
last_breakage:
|
|
1206
|
-
notes:
|
|
1023
|
+
action: tool9.schema.enum(["read", "write", "query_hotspots", "update_entry"]),
|
|
1024
|
+
entries: tool9.schema.array(tool9.schema.object({
|
|
1025
|
+
path: tool9.schema.string(),
|
|
1026
|
+
churn_score: tool9.schema.number(),
|
|
1027
|
+
hotfix_count: tool9.schema.number(),
|
|
1028
|
+
todo_count: tool9.schema.number(),
|
|
1029
|
+
last_breakage: tool9.schema.string().optional(),
|
|
1030
|
+
notes: tool9.schema.array(tool9.schema.string())
|
|
1207
1031
|
})).optional(),
|
|
1208
|
-
entry:
|
|
1209
|
-
path:
|
|
1210
|
-
churn_score:
|
|
1211
|
-
hotfix_count:
|
|
1212
|
-
todo_count:
|
|
1213
|
-
last_breakage:
|
|
1214
|
-
notes:
|
|
1032
|
+
entry: tool9.schema.object({
|
|
1033
|
+
path: tool9.schema.string(),
|
|
1034
|
+
churn_score: tool9.schema.number(),
|
|
1035
|
+
hotfix_count: tool9.schema.number(),
|
|
1036
|
+
todo_count: tool9.schema.number(),
|
|
1037
|
+
last_breakage: tool9.schema.string().optional(),
|
|
1038
|
+
notes: tool9.schema.array(tool9.schema.string())
|
|
1215
1039
|
}).optional(),
|
|
1216
|
-
threshold:
|
|
1217
|
-
path_prefix:
|
|
1218
|
-
limit:
|
|
1040
|
+
threshold: tool9.schema.enum(["stable", "moderate", "volatile", "critical"]).optional(),
|
|
1041
|
+
path_prefix: tool9.schema.string().optional(),
|
|
1042
|
+
limit: tool9.schema.number().optional()
|
|
1219
1043
|
},
|
|
1220
1044
|
async execute(args, context) {
|
|
1221
1045
|
const dir = context.directory ?? process.cwd();
|
|
@@ -1267,48 +1091,48 @@ var volatilityMapTool = tool10({
|
|
|
1267
1091
|
});
|
|
1268
1092
|
|
|
1269
1093
|
// src/tools/policy-engine.ts
|
|
1270
|
-
import { tool as
|
|
1271
|
-
import { readFileSync as
|
|
1272
|
-
import { join as
|
|
1094
|
+
import { tool as tool10 } from "@opencode-ai/plugin";
|
|
1095
|
+
import { readFileSync as readFileSync9, writeFileSync as writeFileSync9, existsSync as existsSync9, mkdirSync as mkdirSync6 } from "fs";
|
|
1096
|
+
import { join as join9 } from "path";
|
|
1273
1097
|
var POLICIES_FILE = "POLICIES.json";
|
|
1274
1098
|
function policiesPath(directory) {
|
|
1275
|
-
return
|
|
1099
|
+
return join9(codebaseDir(directory), POLICIES_FILE);
|
|
1276
1100
|
}
|
|
1277
1101
|
function readStore3(directory) {
|
|
1278
1102
|
const p = policiesPath(directory);
|
|
1279
|
-
if (!
|
|
1103
|
+
if (!existsSync9(p))
|
|
1280
1104
|
return { version: "1.0", last_updated: new Date().toISOString(), policies: [] };
|
|
1281
1105
|
try {
|
|
1282
|
-
return JSON.parse(
|
|
1106
|
+
return JSON.parse(readFileSync9(p, "utf-8"));
|
|
1283
1107
|
} catch {
|
|
1284
1108
|
return { version: "1.0", last_updated: new Date().toISOString(), policies: [] };
|
|
1285
1109
|
}
|
|
1286
1110
|
}
|
|
1287
1111
|
function writeStore3(directory, store) {
|
|
1288
1112
|
const base = codebaseDir(directory);
|
|
1289
|
-
if (!
|
|
1290
|
-
|
|
1113
|
+
if (!existsSync9(base))
|
|
1114
|
+
mkdirSync6(base, { recursive: true });
|
|
1291
1115
|
store.last_updated = new Date().toISOString();
|
|
1292
|
-
|
|
1116
|
+
writeFileSync9(policiesPath(directory), JSON.stringify(store, null, 2), "utf-8");
|
|
1293
1117
|
}
|
|
1294
|
-
var policyEngineTool =
|
|
1118
|
+
var policyEngineTool = tool10({
|
|
1295
1119
|
description: "Self-Healing Policy Engine: manage .codebase/POLICIES.json — add, list, query, toggle, and record violations of editing policies learned from past failures",
|
|
1296
1120
|
args: {
|
|
1297
|
-
action:
|
|
1298
|
-
policy:
|
|
1299
|
-
id:
|
|
1300
|
-
name:
|
|
1301
|
-
trigger:
|
|
1302
|
-
rule:
|
|
1303
|
-
source:
|
|
1304
|
-
failure_count:
|
|
1121
|
+
action: tool10.schema.enum(["list", "add", "record_violation", "toggle", "query"]),
|
|
1122
|
+
policy: tool10.schema.object({
|
|
1123
|
+
id: tool10.schema.string(),
|
|
1124
|
+
name: tool10.schema.string(),
|
|
1125
|
+
trigger: tool10.schema.string(),
|
|
1126
|
+
rule: tool10.schema.string(),
|
|
1127
|
+
source: tool10.schema.enum(["manual", "learned"]),
|
|
1128
|
+
failure_count: tool10.schema.number()
|
|
1305
1129
|
}).optional(),
|
|
1306
|
-
policy_id:
|
|
1307
|
-
active:
|
|
1308
|
-
query:
|
|
1309
|
-
source:
|
|
1310
|
-
active_only:
|
|
1311
|
-
trigger_contains:
|
|
1130
|
+
policy_id: tool10.schema.string().optional(),
|
|
1131
|
+
active: tool10.schema.boolean().optional(),
|
|
1132
|
+
query: tool10.schema.object({
|
|
1133
|
+
source: tool10.schema.enum(["manual", "learned"]).optional(),
|
|
1134
|
+
active_only: tool10.schema.boolean().optional(),
|
|
1135
|
+
trigger_contains: tool10.schema.string().optional()
|
|
1312
1136
|
}).optional()
|
|
1313
1137
|
},
|
|
1314
1138
|
async execute(args, context) {
|
|
@@ -1370,22 +1194,22 @@ var policyEngineTool = tool11({
|
|
|
1370
1194
|
});
|
|
1371
1195
|
|
|
1372
1196
|
// src/tools/hash-edit.ts
|
|
1373
|
-
import { tool as
|
|
1374
|
-
import { readFileSync as
|
|
1197
|
+
import { tool as tool11 } from "@opencode-ai/plugin";
|
|
1198
|
+
import { readFileSync as readFileSync10, writeFileSync as writeFileSync10 } from "fs";
|
|
1375
1199
|
import { createHash } from "crypto";
|
|
1376
|
-
var hashEditTool =
|
|
1200
|
+
var hashEditTool = tool11({
|
|
1377
1201
|
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.",
|
|
1378
1202
|
args: {
|
|
1379
|
-
filePath:
|
|
1380
|
-
targetContent:
|
|
1381
|
-
expectedHash:
|
|
1382
|
-
replacementContent:
|
|
1203
|
+
filePath: tool11.schema.string(),
|
|
1204
|
+
targetContent: tool11.schema.string(),
|
|
1205
|
+
expectedHash: tool11.schema.string().optional(),
|
|
1206
|
+
replacementContent: tool11.schema.string()
|
|
1383
1207
|
},
|
|
1384
1208
|
async execute(args, context) {
|
|
1385
1209
|
const fullPath = args.filePath.startsWith("/") ? args.filePath : `${context.directory}/${args.filePath}`;
|
|
1386
1210
|
let content;
|
|
1387
1211
|
try {
|
|
1388
|
-
content =
|
|
1212
|
+
content = readFileSync10(fullPath, "utf-8");
|
|
1389
1213
|
} catch (e) {
|
|
1390
1214
|
return `Error: Could not read file ${args.filePath}`;
|
|
1391
1215
|
}
|
|
@@ -1399,19 +1223,19 @@ var hashEditTool = tool12({
|
|
|
1399
1223
|
}
|
|
1400
1224
|
}
|
|
1401
1225
|
const newContent = content.replace(args.targetContent, args.replacementContent);
|
|
1402
|
-
|
|
1226
|
+
writeFileSync10(fullPath, newContent, "utf-8");
|
|
1403
1227
|
return `Successfully updated ${args.filePath} using hash-anchored edit.`;
|
|
1404
1228
|
}
|
|
1405
1229
|
});
|
|
1406
1230
|
|
|
1407
1231
|
// src/tools/council.ts
|
|
1408
|
-
import { tool as
|
|
1232
|
+
import { tool as tool12 } from "@opencode-ai/plugin";
|
|
1409
1233
|
function createCouncilTool(client) {
|
|
1410
|
-
return
|
|
1234
|
+
return tool12({
|
|
1411
1235
|
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.",
|
|
1412
1236
|
args: {
|
|
1413
|
-
task:
|
|
1414
|
-
agents:
|
|
1237
|
+
task: tool12.schema.string(),
|
|
1238
|
+
agents: tool12.schema.array(tool12.schema.string()).optional()
|
|
1415
1239
|
},
|
|
1416
1240
|
async execute(args, context) {
|
|
1417
1241
|
const agents = args.agents || ["architect", "reviewer", "coder"];
|
|
@@ -1467,31 +1291,31 @@ Please synthesize these results. Identify areas of agreement, resolve conflicts,
|
|
|
1467
1291
|
}
|
|
1468
1292
|
|
|
1469
1293
|
// src/tools/context-generator.ts
|
|
1470
|
-
import { tool as
|
|
1471
|
-
import { writeFileSync as
|
|
1472
|
-
import { join as
|
|
1473
|
-
var contextGeneratorTool =
|
|
1294
|
+
import { tool as tool13 } from "@opencode-ai/plugin";
|
|
1295
|
+
import { writeFileSync as writeFileSync11, existsSync as existsSync10, readFileSync as readFileSync11, readdirSync as readdirSync2, statSync } from "fs";
|
|
1296
|
+
import { join as join10 } from "path";
|
|
1297
|
+
var contextGeneratorTool = tool13({
|
|
1474
1298
|
description: "Auto-generate or update hierarchical context files (AGENTS.md, CLAUDE.md) throughout the project. These files provide critical grounding for AI agents.",
|
|
1475
1299
|
args: {
|
|
1476
|
-
targetDir:
|
|
1477
|
-
force:
|
|
1300
|
+
targetDir: tool13.schema.string().optional(),
|
|
1301
|
+
force: tool13.schema.boolean().optional()
|
|
1478
1302
|
},
|
|
1479
1303
|
async execute(args, context) {
|
|
1480
1304
|
const root = context.directory;
|
|
1481
|
-
const target = args.targetDir ?
|
|
1482
|
-
if (!
|
|
1305
|
+
const target = args.targetDir ? join10(root, args.targetDir) : root;
|
|
1306
|
+
if (!existsSync10(target)) {
|
|
1483
1307
|
return `Error: Directory ${target} does not exist.`;
|
|
1484
1308
|
}
|
|
1485
|
-
const agentsMdPath =
|
|
1486
|
-
if (
|
|
1309
|
+
const agentsMdPath = join10(target, "AGENTS.md");
|
|
1310
|
+
if (existsSync10(agentsMdPath) && !args.force) {
|
|
1487
1311
|
return `AGENTS.md already exists in ${target}. Use force: true to overwrite.`;
|
|
1488
1312
|
}
|
|
1489
|
-
const pkgPath =
|
|
1313
|
+
const pkgPath = join10(root, "package.json");
|
|
1490
1314
|
let projectName = "Project";
|
|
1491
1315
|
let techStack = "Unknown";
|
|
1492
|
-
if (
|
|
1316
|
+
if (existsSync10(pkgPath)) {
|
|
1493
1317
|
try {
|
|
1494
|
-
const pkg = JSON.parse(
|
|
1318
|
+
const pkg = JSON.parse(readFileSync11(pkgPath, "utf-8"));
|
|
1495
1319
|
projectName = pkg.name || projectName;
|
|
1496
1320
|
techStack = Object.keys(pkg.dependencies || {}).slice(0, 5).join(", ");
|
|
1497
1321
|
} catch {}
|
|
@@ -1509,7 +1333,7 @@ var contextGeneratorTool = tool14({
|
|
|
1509
1333
|
|
|
1510
1334
|
## Directory Map
|
|
1511
1335
|
${readdirSync2(target).slice(0, 10).map((f) => {
|
|
1512
|
-
const s = statSync(
|
|
1336
|
+
const s = statSync(join10(target, f));
|
|
1513
1337
|
return `- \`${f}\`${s.isDirectory() ? "/" : ""} : [Description]`;
|
|
1514
1338
|
}).join(`
|
|
1515
1339
|
`)}
|
|
@@ -1517,29 +1341,29 @@ ${readdirSync2(target).slice(0, 10).map((f) => {
|
|
|
1517
1341
|
---
|
|
1518
1342
|
Generated by FlowDeck Context Generator.
|
|
1519
1343
|
`;
|
|
1520
|
-
|
|
1344
|
+
writeFileSync11(agentsMdPath, content, "utf-8");
|
|
1521
1345
|
return `Successfully generated AGENTS.md in ${target}.`;
|
|
1522
1346
|
}
|
|
1523
1347
|
});
|
|
1524
1348
|
|
|
1525
1349
|
// src/tools/create-skill.ts
|
|
1526
|
-
import { tool as
|
|
1527
|
-
import { mkdirSync as
|
|
1528
|
-
import { join as
|
|
1350
|
+
import { tool as tool14 } from "@opencode-ai/plugin";
|
|
1351
|
+
import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync12, existsSync as existsSync11 } from "fs";
|
|
1352
|
+
import { join as join11, dirname as dirname3 } from "path";
|
|
1529
1353
|
import { fileURLToPath } from "url";
|
|
1530
|
-
var SKILLS_DIR =
|
|
1531
|
-
var createSkillTool =
|
|
1354
|
+
var SKILLS_DIR = join11(dirname3(fileURLToPath(import.meta.url)), "..", "skills");
|
|
1355
|
+
var createSkillTool = tool14({
|
|
1532
1356
|
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.",
|
|
1533
1357
|
args: {
|
|
1534
|
-
name:
|
|
1535
|
-
description:
|
|
1536
|
-
content:
|
|
1537
|
-
tags:
|
|
1358
|
+
name: tool14.schema.string().describe("Unique kebab-case skill name, e.g. 'api-rate-limiting'"),
|
|
1359
|
+
description: tool14.schema.string().describe("One-sentence description of what this skill does"),
|
|
1360
|
+
content: tool14.schema.string().describe("Full skill body in Markdown. Must include: ## When to Activate, ## Steps, and ## Examples sections."),
|
|
1361
|
+
tags: tool14.schema.array(tool14.schema.string()).optional().describe("Optional tags for categorisation, e.g. ['performance', 'typescript']")
|
|
1538
1362
|
},
|
|
1539
1363
|
async execute(args) {
|
|
1540
|
-
const skillDir =
|
|
1541
|
-
const skillFile =
|
|
1542
|
-
if (
|
|
1364
|
+
const skillDir = join11(SKILLS_DIR, args.name);
|
|
1365
|
+
const skillFile = join11(skillDir, "SKILL.md");
|
|
1366
|
+
if (existsSync11(skillFile)) {
|
|
1543
1367
|
return `Skill '${args.name}' already exists at ${skillFile}.
|
|
1544
1368
|
` + `Use a different name or delete the existing skill directory first.`;
|
|
1545
1369
|
}
|
|
@@ -1554,8 +1378,8 @@ origin: FlowDeck (self-learned)${tagLine}
|
|
|
1554
1378
|
`;
|
|
1555
1379
|
const fullContent = frontmatter + args.content.trimStart();
|
|
1556
1380
|
try {
|
|
1557
|
-
|
|
1558
|
-
|
|
1381
|
+
mkdirSync7(skillDir, { recursive: true });
|
|
1382
|
+
writeFileSync12(skillFile, fullContent, "utf-8");
|
|
1559
1383
|
return `✓ Skill '${args.name}' created at ${skillFile}
|
|
1560
1384
|
|
|
1561
1385
|
` + `The skill is now part of the FlowDeck library. Restart OpenCode to load it into the active session.`;
|
|
@@ -1566,9 +1390,9 @@ origin: FlowDeck (self-learned)${tagLine}
|
|
|
1566
1390
|
});
|
|
1567
1391
|
|
|
1568
1392
|
// src/tools/reflect.ts
|
|
1569
|
-
import { tool as
|
|
1570
|
-
import { existsSync as
|
|
1571
|
-
import { join as
|
|
1393
|
+
import { tool as tool15 } from "@opencode-ai/plugin";
|
|
1394
|
+
import { existsSync as existsSync12, readFileSync as readFileSync12 } from "fs";
|
|
1395
|
+
import { join as join12 } from "path";
|
|
1572
1396
|
var MAX_ARTIFACT_BYTES = 4000;
|
|
1573
1397
|
function tail(text, maxBytes) {
|
|
1574
1398
|
if (text.length <= maxBytes)
|
|
@@ -1576,10 +1400,10 @@ function tail(text, maxBytes) {
|
|
|
1576
1400
|
return `... (truncated) ...
|
|
1577
1401
|
` + text.slice(-maxBytes);
|
|
1578
1402
|
}
|
|
1579
|
-
var reflectTool =
|
|
1403
|
+
var reflectTool = tool15({
|
|
1580
1404
|
description: "Gather session artifacts (decisions, telemetry, failures, policies) and return a structured " + "reflection context that the agent can reason over to produce self-improvement proposals.",
|
|
1581
1405
|
args: {
|
|
1582
|
-
scope:
|
|
1406
|
+
scope: tool15.schema.enum(["session", "project"]).optional().describe("'session' (default) uses only recent artifacts; 'project' includes all historical data")
|
|
1583
1407
|
},
|
|
1584
1408
|
async execute(args, context) {
|
|
1585
1409
|
const root = context.directory;
|
|
@@ -1597,11 +1421,11 @@ var reflectTool = tool16({
|
|
|
1597
1421
|
];
|
|
1598
1422
|
let found = 0;
|
|
1599
1423
|
for (const [rel, label] of ARTIFACT_PATHS) {
|
|
1600
|
-
const full =
|
|
1601
|
-
if (!
|
|
1424
|
+
const full = join12(root, rel);
|
|
1425
|
+
if (!existsSync12(full))
|
|
1602
1426
|
continue;
|
|
1603
1427
|
try {
|
|
1604
|
-
const raw =
|
|
1428
|
+
const raw = readFileSync12(full, "utf-8").trim();
|
|
1605
1429
|
if (!raw)
|
|
1606
1430
|
continue;
|
|
1607
1431
|
const count = raw.split(`
|
|
@@ -1620,16 +1444,401 @@ var reflectTool = tool16({
|
|
|
1620
1444
|
}
|
|
1621
1445
|
});
|
|
1622
1446
|
|
|
1447
|
+
// src/tools/memory-search.ts
|
|
1448
|
+
import { tool as tool16 } from "@opencode-ai/plugin";
|
|
1449
|
+
|
|
1450
|
+
// src/services/memory-store.ts
|
|
1451
|
+
import { Database } from "bun:sqlite";
|
|
1452
|
+
import { existsSync as existsSync13, mkdirSync as mkdirSync8 } from "fs";
|
|
1453
|
+
import { join as join13 } from "path";
|
|
1454
|
+
import { homedir } from "os";
|
|
1455
|
+
var MEMORY_DIR = join13(homedir(), ".flowdeck-memory");
|
|
1456
|
+
var DB_PATH = join13(MEMORY_DIR, "memory.db");
|
|
1457
|
+
function ensureDir() {
|
|
1458
|
+
if (!existsSync13(MEMORY_DIR)) {
|
|
1459
|
+
mkdirSync8(MEMORY_DIR, { recursive: true });
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
var db = null;
|
|
1463
|
+
function getDb() {
|
|
1464
|
+
if (!db) {
|
|
1465
|
+
ensureDir();
|
|
1466
|
+
db = new Database(DB_PATH);
|
|
1467
|
+
initializeSchema(db);
|
|
1468
|
+
}
|
|
1469
|
+
return db;
|
|
1470
|
+
}
|
|
1471
|
+
function initializeSchema(database) {
|
|
1472
|
+
const schema = `
|
|
1473
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
1474
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1475
|
+
content_session_id TEXT NOT NULL UNIQUE,
|
|
1476
|
+
project TEXT NOT NULL,
|
|
1477
|
+
directory TEXT NOT NULL,
|
|
1478
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1479
|
+
last_active_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1480
|
+
summary TEXT,
|
|
1481
|
+
prompt_count INTEGER DEFAULT 0
|
|
1482
|
+
);
|
|
1483
|
+
|
|
1484
|
+
CREATE TABLE IF NOT EXISTS observations (
|
|
1485
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1486
|
+
session_id INTEGER NOT NULL,
|
|
1487
|
+
tool_name TEXT NOT NULL,
|
|
1488
|
+
tool_input TEXT,
|
|
1489
|
+
tool_response TEXT,
|
|
1490
|
+
directory TEXT NOT NULL,
|
|
1491
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1492
|
+
FOREIGN KEY (session_id) REFERENCES sessions(id)
|
|
1493
|
+
);
|
|
1494
|
+
|
|
1495
|
+
CREATE TABLE IF NOT EXISTS summaries (
|
|
1496
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1497
|
+
session_id INTEGER NOT NULL UNIQUE,
|
|
1498
|
+
content TEXT NOT NULL,
|
|
1499
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1500
|
+
FOREIGN KEY (session_id) REFERENCES sessions(id)
|
|
1501
|
+
);
|
|
1502
|
+
|
|
1503
|
+
CREATE INDEX IF NOT EXISTS idx_observations_session ON observations(session_id);
|
|
1504
|
+
CREATE INDEX IF NOT EXISTS idx_observations_directory ON observations(directory);
|
|
1505
|
+
CREATE INDEX IF NOT EXISTS idx_observations_tool ON observations(tool_name);
|
|
1506
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project);
|
|
1507
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_directory ON sessions(directory);
|
|
1508
|
+
`;
|
|
1509
|
+
database.run(schema);
|
|
1510
|
+
}
|
|
1511
|
+
function serializeToolInput(input) {
|
|
1512
|
+
if (!input)
|
|
1513
|
+
return null;
|
|
1514
|
+
try {
|
|
1515
|
+
return JSON.stringify(input);
|
|
1516
|
+
} catch {
|
|
1517
|
+
return String(input);
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
function parseToolInput(input) {
|
|
1521
|
+
if (!input)
|
|
1522
|
+
return null;
|
|
1523
|
+
try {
|
|
1524
|
+
return JSON.parse(input);
|
|
1525
|
+
} catch {
|
|
1526
|
+
return null;
|
|
1527
|
+
}
|
|
1528
|
+
}
|
|
1529
|
+
function initSession(contentSessionId, project, directory) {
|
|
1530
|
+
const database = getDb();
|
|
1531
|
+
const now = new Date().toISOString();
|
|
1532
|
+
const existing = database.prepare("SELECT * FROM sessions WHERE content_session_id = ?").get(contentSessionId);
|
|
1533
|
+
if (existing) {
|
|
1534
|
+
database.prepare("UPDATE sessions SET last_active_at = ?, prompt_count = prompt_count + 1 WHERE id = ?").run(now, existing.id);
|
|
1535
|
+
return { ...existing, last_active_at: now, prompt_count: (existing.prompt_count || 0) + 1 };
|
|
1536
|
+
}
|
|
1537
|
+
const result = database.prepare("INSERT INTO sessions (content_session_id, project, directory, created_at, last_active_at) VALUES (?, ?, ?, ?, ?)").run(contentSessionId, project, directory, now, now);
|
|
1538
|
+
return {
|
|
1539
|
+
id: result.lastInsertRowid,
|
|
1540
|
+
content_session_id: contentSessionId,
|
|
1541
|
+
project,
|
|
1542
|
+
directory,
|
|
1543
|
+
created_at: now,
|
|
1544
|
+
last_active_at: now,
|
|
1545
|
+
prompt_count: 1
|
|
1546
|
+
};
|
|
1547
|
+
}
|
|
1548
|
+
function storeObservation(sessionId, toolName, toolInput, toolResponse, directory) {
|
|
1549
|
+
const database = getDb();
|
|
1550
|
+
const now = new Date().toISOString();
|
|
1551
|
+
const result = database.prepare("INSERT INTO observations (session_id, tool_name, tool_input, tool_response, directory, created_at) VALUES (?, ?, ?, ?, ?, ?)").run(sessionId, toolName, serializeToolInput(toolInput), toolResponse ? toolResponse.slice(0, 1e4) : null, directory, now);
|
|
1552
|
+
database.prepare("UPDATE sessions SET last_active_at = ? WHERE id = ?").run(now, sessionId);
|
|
1553
|
+
return {
|
|
1554
|
+
id: result.lastInsertRowid,
|
|
1555
|
+
session_id: sessionId,
|
|
1556
|
+
tool_name: toolName,
|
|
1557
|
+
tool_input: parseToolInput(serializeToolInput(toolInput)),
|
|
1558
|
+
tool_response: toolResponse ? toolResponse.slice(0, 1e4) : null,
|
|
1559
|
+
directory,
|
|
1560
|
+
created_at: now
|
|
1561
|
+
};
|
|
1562
|
+
}
|
|
1563
|
+
function storeSummary(sessionId, content) {
|
|
1564
|
+
const database = getDb();
|
|
1565
|
+
const now = new Date().toISOString();
|
|
1566
|
+
database.prepare("INSERT OR REPLACE INTO summaries (session_id, content, created_at) VALUES (?, ?, ?)").run(sessionId, content, now);
|
|
1567
|
+
database.prepare("UPDATE sessions SET summary = ? WHERE id = ?").run(content, sessionId);
|
|
1568
|
+
return {
|
|
1569
|
+
id: database.prepare("SELECT last_insert_rowid() as id").get().id,
|
|
1570
|
+
session_id: sessionId,
|
|
1571
|
+
content,
|
|
1572
|
+
created_at: now
|
|
1573
|
+
};
|
|
1574
|
+
}
|
|
1575
|
+
function getRecentSessions(directory, limit = 5) {
|
|
1576
|
+
const database = getDb();
|
|
1577
|
+
return database.prepare(`SELECT * FROM sessions
|
|
1578
|
+
WHERE directory = ?
|
|
1579
|
+
ORDER BY last_active_at DESC
|
|
1580
|
+
LIMIT ?`).all(directory, limit);
|
|
1581
|
+
}
|
|
1582
|
+
function getObservationsForSession(sessionId) {
|
|
1583
|
+
const database = getDb();
|
|
1584
|
+
const observations = database.prepare("SELECT * FROM observations WHERE session_id = ? ORDER BY created_at ASC").all(sessionId);
|
|
1585
|
+
return observations.map((obs) => ({
|
|
1586
|
+
...obs,
|
|
1587
|
+
tool_input: parseToolInput(obs.tool_input)
|
|
1588
|
+
}));
|
|
1589
|
+
}
|
|
1590
|
+
function getRecentObservations(directory, limit = 50) {
|
|
1591
|
+
const database = getDb();
|
|
1592
|
+
const rows = database.prepare(`SELECT o.*, s.project, s.content_session_id, s.created_at as session_created
|
|
1593
|
+
FROM observations o
|
|
1594
|
+
JOIN sessions s ON o.session_id = s.id
|
|
1595
|
+
WHERE o.directory = ?
|
|
1596
|
+
ORDER BY o.created_at DESC
|
|
1597
|
+
LIMIT ?`).all(directory, limit);
|
|
1598
|
+
return rows.map((row) => ({
|
|
1599
|
+
observation: {
|
|
1600
|
+
...row,
|
|
1601
|
+
tool_input: parseToolInput(row.tool_input)
|
|
1602
|
+
},
|
|
1603
|
+
session: {
|
|
1604
|
+
content_session_id: row.content_session_id,
|
|
1605
|
+
project: row.project,
|
|
1606
|
+
directory,
|
|
1607
|
+
created_at: row.session_created
|
|
1608
|
+
}
|
|
1609
|
+
}));
|
|
1610
|
+
}
|
|
1611
|
+
function searchObservations(directory, query, limit = 10) {
|
|
1612
|
+
const database = getDb();
|
|
1613
|
+
const pattern = `%${query}%`;
|
|
1614
|
+
const rows = database.prepare(`SELECT o.*, s.project, s.content_session_id, s.created_at as session_created
|
|
1615
|
+
FROM observations o
|
|
1616
|
+
JOIN sessions s ON o.session_id = s.id
|
|
1617
|
+
WHERE o.directory = ? AND (o.tool_name LIKE ? OR o.tool_input LIKE ? OR o.tool_response LIKE ?)
|
|
1618
|
+
ORDER BY o.created_at DESC
|
|
1619
|
+
LIMIT ?`).all(directory, pattern, pattern, pattern, limit);
|
|
1620
|
+
return rows.map((row) => ({
|
|
1621
|
+
observation: {
|
|
1622
|
+
...row,
|
|
1623
|
+
tool_input: parseToolInput(row.tool_input)
|
|
1624
|
+
},
|
|
1625
|
+
session: {
|
|
1626
|
+
content_session_id: row.content_session_id,
|
|
1627
|
+
project: row.project,
|
|
1628
|
+
directory,
|
|
1629
|
+
created_at: row.session_created
|
|
1630
|
+
}
|
|
1631
|
+
}));
|
|
1632
|
+
}
|
|
1633
|
+
function getContextForDirectory(directory, maxObservations = 20) {
|
|
1634
|
+
const recentObs = getRecentObservations(directory, maxObservations);
|
|
1635
|
+
if (recentObs.length === 0)
|
|
1636
|
+
return "";
|
|
1637
|
+
const lines = ["## Recent Context"];
|
|
1638
|
+
for (const { observation, session } of recentObs) {
|
|
1639
|
+
const date = observation.created_at ? new Date(observation.created_at).toLocaleDateString() : "unknown";
|
|
1640
|
+
lines.push(`
|
|
1641
|
+
### [${date}] ${session.project} - ${observation.tool_name}`);
|
|
1642
|
+
if (observation.tool_input && Object.keys(observation.tool_input).length > 0) {
|
|
1643
|
+
const preview = JSON.stringify(observation.tool_input).slice(0, 200);
|
|
1644
|
+
lines.push(`Input: ${preview}${preview.length >= 200 ? "..." : ""}`);
|
|
1645
|
+
}
|
|
1646
|
+
if (observation.tool_response) {
|
|
1647
|
+
const preview = observation.tool_response.slice(0, 300);
|
|
1648
|
+
lines.push(`Output: ${preview}${observation.tool_response.length > 300 ? "..." : ""}`);
|
|
1649
|
+
}
|
|
1650
|
+
}
|
|
1651
|
+
const summaries = getDb().prepare(`SELECT su.* FROM summaries su
|
|
1652
|
+
JOIN sessions s ON su.session_id = s.id
|
|
1653
|
+
WHERE s.directory = ?
|
|
1654
|
+
ORDER BY su.created_at DESC
|
|
1655
|
+
LIMIT 3`).all(directory);
|
|
1656
|
+
if (summaries.length > 0) {
|
|
1657
|
+
lines.push(`
|
|
1658
|
+
## Session Summaries`);
|
|
1659
|
+
for (const sum of summaries) {
|
|
1660
|
+
const date = sum.created_at ? new Date(sum.created_at).toLocaleDateString() : "unknown";
|
|
1661
|
+
lines.push(`
|
|
1662
|
+
### [${date}]`);
|
|
1663
|
+
lines.push(sum.content.slice(0, 500));
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
1666
|
+
return lines.join(`
|
|
1667
|
+
`);
|
|
1668
|
+
}
|
|
1669
|
+
|
|
1670
|
+
// src/tools/memory-search.ts
|
|
1671
|
+
var memorySearchTool = tool16({
|
|
1672
|
+
description: "Search FlowDeck memory for past observations, sessions, and context. Use to recall what was worked on previously.",
|
|
1673
|
+
args: {
|
|
1674
|
+
query: tool16.schema.string().optional().describe("Search query for memory (searches tool names, inputs, and outputs)"),
|
|
1675
|
+
session_id: tool16.schema.string().optional().describe("Specific session ID to retrieve observations from"),
|
|
1676
|
+
limit: tool16.schema.number().optional().describe("Maximum number of results (default: 10)")
|
|
1677
|
+
},
|
|
1678
|
+
async execute(args, context) {
|
|
1679
|
+
const directory = context.directory ?? process.cwd();
|
|
1680
|
+
const limit = args.limit ?? 10;
|
|
1681
|
+
if (args.session_id) {
|
|
1682
|
+
const sessions2 = getRecentSessions(directory, 100);
|
|
1683
|
+
const targetSession = sessions2.find((s) => String(s.id) === args.session_id || s.content_session_id === args.session_id);
|
|
1684
|
+
if (!targetSession) {
|
|
1685
|
+
return JSON.stringify({ error: "Session not found", session_id: args.session_id });
|
|
1686
|
+
}
|
|
1687
|
+
const observations = getObservationsForSession(targetSession.id);
|
|
1688
|
+
return JSON.stringify({
|
|
1689
|
+
session: targetSession,
|
|
1690
|
+
observations: observations.map((o) => ({
|
|
1691
|
+
tool_name: o.tool_name,
|
|
1692
|
+
tool_input: o.tool_input,
|
|
1693
|
+
tool_response: o.tool_response ? o.tool_response.slice(0, 500) + (o.tool_response.length > 500 ? "..." : "") : null,
|
|
1694
|
+
created_at: o.created_at
|
|
1695
|
+
}))
|
|
1696
|
+
});
|
|
1697
|
+
}
|
|
1698
|
+
if (args.query) {
|
|
1699
|
+
const results = searchObservations(directory, args.query, limit);
|
|
1700
|
+
if (results.length === 0) {
|
|
1701
|
+
return JSON.stringify({ message: `No results found for "${args.query}"`, results: [] });
|
|
1702
|
+
}
|
|
1703
|
+
return JSON.stringify({
|
|
1704
|
+
query: args.query,
|
|
1705
|
+
count: results.length,
|
|
1706
|
+
results: results.map(({ observation, session }) => ({
|
|
1707
|
+
tool_name: observation.tool_name,
|
|
1708
|
+
tool_input: observation.tool_input,
|
|
1709
|
+
tool_response: observation.tool_response ? observation.tool_response.slice(0, 300) + (observation.tool_response.length > 300 ? "..." : "") : null,
|
|
1710
|
+
project: session.project,
|
|
1711
|
+
date: observation.created_at
|
|
1712
|
+
}))
|
|
1713
|
+
});
|
|
1714
|
+
}
|
|
1715
|
+
const sessions = getRecentSessions(directory, limit);
|
|
1716
|
+
if (sessions.length === 0) {
|
|
1717
|
+
return JSON.stringify({ message: "No previous sessions found in this directory", sessions: [] });
|
|
1718
|
+
}
|
|
1719
|
+
return JSON.stringify({
|
|
1720
|
+
message: "Recent sessions",
|
|
1721
|
+
count: sessions.length,
|
|
1722
|
+
sessions: sessions.map((s) => ({
|
|
1723
|
+
id: s.id,
|
|
1724
|
+
content_session_id: s.content_session_id,
|
|
1725
|
+
project: s.project,
|
|
1726
|
+
created_at: s.created_at,
|
|
1727
|
+
last_active_at: s.last_active_at,
|
|
1728
|
+
summary: s.summary
|
|
1729
|
+
}))
|
|
1730
|
+
});
|
|
1731
|
+
}
|
|
1732
|
+
});
|
|
1733
|
+
|
|
1734
|
+
// src/hooks/memory-hook.ts
|
|
1735
|
+
var MAX_TOOL_RESPONSE = 1e4;
|
|
1736
|
+
var MAX_PROMPT_LENGTH = 2000;
|
|
1737
|
+
var activeSessions = new Map;
|
|
1738
|
+
function extractProjectFromDirectory(directory) {
|
|
1739
|
+
const parts = directory.split("/");
|
|
1740
|
+
return parts[parts.length - 1] || "unknown";
|
|
1741
|
+
}
|
|
1742
|
+
function truncate(str, max) {
|
|
1743
|
+
if (!str || str.length <= max)
|
|
1744
|
+
return str || "";
|
|
1745
|
+
return str.slice(0, max);
|
|
1746
|
+
}
|
|
1747
|
+
function onSessionCreated(directory, contentSessionId, prompt) {
|
|
1748
|
+
const project = extractProjectFromDirectory(directory);
|
|
1749
|
+
const session = initSession(contentSessionId, project, directory);
|
|
1750
|
+
activeSessions.set(contentSessionId, {
|
|
1751
|
+
sessionId: session.id,
|
|
1752
|
+
contentSessionId,
|
|
1753
|
+
project,
|
|
1754
|
+
directory
|
|
1755
|
+
});
|
|
1756
|
+
return session;
|
|
1757
|
+
}
|
|
1758
|
+
function onToolExecuted(contentSessionId, toolName, toolInput, toolResponse, directory) {
|
|
1759
|
+
let ctx = activeSessions.get(contentSessionId);
|
|
1760
|
+
if (!ctx) {
|
|
1761
|
+
const project = extractProjectFromDirectory(directory);
|
|
1762
|
+
const session = initSession(contentSessionId, project, directory);
|
|
1763
|
+
ctx = {
|
|
1764
|
+
sessionId: session.id,
|
|
1765
|
+
contentSessionId,
|
|
1766
|
+
project,
|
|
1767
|
+
directory
|
|
1768
|
+
};
|
|
1769
|
+
activeSessions.set(contentSessionId, ctx);
|
|
1770
|
+
}
|
|
1771
|
+
storeObservation(ctx.sessionId, truncate(toolName, 200), toolInput, toolResponse ? truncate(toolResponse, MAX_TOOL_RESPONSE) : null, directory);
|
|
1772
|
+
}
|
|
1773
|
+
function onMessageUpdated(contentSessionId, role, content, directory) {
|
|
1774
|
+
if (role !== "assistant")
|
|
1775
|
+
return;
|
|
1776
|
+
if (!content || !content.trim())
|
|
1777
|
+
return;
|
|
1778
|
+
let ctx = activeSessions.get(contentSessionId);
|
|
1779
|
+
if (!ctx) {
|
|
1780
|
+
const project = extractProjectFromDirectory(directory);
|
|
1781
|
+
const session = initSession(contentSessionId, project, directory);
|
|
1782
|
+
ctx = {
|
|
1783
|
+
sessionId: session.id,
|
|
1784
|
+
contentSessionId,
|
|
1785
|
+
project,
|
|
1786
|
+
directory
|
|
1787
|
+
};
|
|
1788
|
+
activeSessions.set(contentSessionId, ctx);
|
|
1789
|
+
}
|
|
1790
|
+
storeObservation(ctx.sessionId, "assistant_message", { role }, truncate(content, MAX_TOOL_RESPONSE), directory);
|
|
1791
|
+
}
|
|
1792
|
+
function onSessionCompact(contentSessionId, summary) {
|
|
1793
|
+
const ctx = activeSessions.get(contentSessionId);
|
|
1794
|
+
if (!ctx)
|
|
1795
|
+
return;
|
|
1796
|
+
storeSummary(ctx.sessionId, truncate(summary, MAX_PROMPT_LENGTH));
|
|
1797
|
+
}
|
|
1798
|
+
function onSessionEnd(contentSessionId, lastMessage) {
|
|
1799
|
+
const ctx = activeSessions.get(contentSessionId);
|
|
1800
|
+
if (!ctx)
|
|
1801
|
+
return;
|
|
1802
|
+
if (lastMessage && lastMessage.trim()) {
|
|
1803
|
+
storeSummary(ctx.sessionId, truncate(lastMessage, MAX_PROMPT_LENGTH));
|
|
1804
|
+
}
|
|
1805
|
+
activeSessions.delete(contentSessionId);
|
|
1806
|
+
}
|
|
1807
|
+
function getSessionContext(directory, contentSessionId) {
|
|
1808
|
+
const context = getContextForDirectory(directory, 30);
|
|
1809
|
+
const previousSessions = getRecentSessions(directory, 5);
|
|
1810
|
+
if (previousSessions.length > 0 && activeSessions.has(contentSessionId)) {
|
|
1811
|
+
const ctx = activeSessions.get(contentSessionId);
|
|
1812
|
+
for (const prev of previousSessions) {
|
|
1813
|
+
if (prev.content_session_id === contentSessionId)
|
|
1814
|
+
continue;
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1817
|
+
return { context, previousSessions };
|
|
1818
|
+
}
|
|
1819
|
+
function clearSession(contentSessionId) {
|
|
1820
|
+
activeSessions.delete(contentSessionId);
|
|
1821
|
+
}
|
|
1822
|
+
var memoryHook = {
|
|
1823
|
+
onSessionCreated,
|
|
1824
|
+
onToolExecuted,
|
|
1825
|
+
onMessageUpdated,
|
|
1826
|
+
onSessionCompact,
|
|
1827
|
+
onSessionEnd,
|
|
1828
|
+
getSessionContext,
|
|
1829
|
+
clearSession
|
|
1830
|
+
};
|
|
1831
|
+
|
|
1623
1832
|
// src/hooks/guard-rails.ts
|
|
1624
|
-
import { existsSync as existsSync14, readFileSync as
|
|
1625
|
-
import { join as
|
|
1833
|
+
import { existsSync as existsSync14, readFileSync as readFileSync13 } from "fs";
|
|
1834
|
+
import { join as join14 } from "path";
|
|
1626
1835
|
var PLANNING_DIR2 = ".planning";
|
|
1627
1836
|
var CONFIG_FILE = "config.json";
|
|
1628
1837
|
var STATE_FILE2 = "STATE.md";
|
|
1629
1838
|
function resolveExecutionMode(configPath, trustScore, volatility) {
|
|
1630
1839
|
if (existsSync14(configPath)) {
|
|
1631
1840
|
try {
|
|
1632
|
-
const config = JSON.parse(
|
|
1841
|
+
const config = JSON.parse(readFileSync13(configPath, "utf-8"));
|
|
1633
1842
|
if (config.execution_mode === "review-only")
|
|
1634
1843
|
return "review-only";
|
|
1635
1844
|
if (config.execution_mode === "guarded")
|
|
@@ -1678,12 +1887,15 @@ var BUILD_DEPLOY_PATTERNS = [
|
|
|
1678
1887
|
"rails deploy",
|
|
1679
1888
|
"rake deploy"
|
|
1680
1889
|
];
|
|
1890
|
+
var ENABLED = process.env.FLOWDECK_GUARD_RAILS_ENABLED === "on";
|
|
1681
1891
|
async function guardRailsHook(ctx, input, _output) {
|
|
1892
|
+
if (!ENABLED)
|
|
1893
|
+
return;
|
|
1682
1894
|
const dir = ctx.directory;
|
|
1683
|
-
const planningDirPath =
|
|
1895
|
+
const planningDirPath = join14(dir, PLANNING_DIR2);
|
|
1684
1896
|
const codebaseDirectory = codebaseDir(dir);
|
|
1685
|
-
const configPath =
|
|
1686
|
-
const statePath2 =
|
|
1897
|
+
const configPath = join14(planningDirPath, CONFIG_FILE);
|
|
1898
|
+
const statePath2 = join14(planningDirPath, STATE_FILE2);
|
|
1687
1899
|
const workspaceRoot = findWorkspaceRoot(dir);
|
|
1688
1900
|
if (workspaceRoot && dir !== workspaceRoot) {
|
|
1689
1901
|
const config = getWorkspaceConfig(dir);
|
|
@@ -1709,11 +1921,10 @@ async function guardRailsHook(ctx, input, _output) {
|
|
|
1709
1921
|
if (effectiveSeverity === null)
|
|
1710
1922
|
return;
|
|
1711
1923
|
if (effectiveSeverity === "warn") {
|
|
1712
|
-
const warning = getWarningMessage(
|
|
1924
|
+
const warning = getWarningMessage(planningDirPath);
|
|
1713
1925
|
throw new Error(`[flowdeck] WARNING: ${warning}`);
|
|
1714
|
-
return;
|
|
1715
1926
|
}
|
|
1716
|
-
const blockMessage = getBlockMessage(
|
|
1927
|
+
const blockMessage = getBlockMessage(planningDirPath);
|
|
1717
1928
|
throw new Error(`[flowdeck] BLOCK: ${blockMessage}`);
|
|
1718
1929
|
}
|
|
1719
1930
|
if (input.tool === "bash") {
|
|
@@ -1721,7 +1932,6 @@ async function guardRailsHook(ctx, input, _output) {
|
|
|
1721
1932
|
for (const pattern of BUILD_DEPLOY_PATTERNS) {
|
|
1722
1933
|
if (cmd.includes(pattern)) {
|
|
1723
1934
|
if (!getPlanConfirmed(statePath2)) {
|
|
1724
|
-
const msg = "Build/deploy command detected but plan is not confirmed. Run /plan first.";
|
|
1725
1935
|
throw new Error(`[flowdeck] WARNING: Build/deploy command detected but plan is not confirmed. Run /plan first.`);
|
|
1726
1936
|
}
|
|
1727
1937
|
break;
|
|
@@ -1732,7 +1942,7 @@ async function guardRailsHook(ctx, input, _output) {
|
|
|
1732
1942
|
function effectiveSeverity(configPath, statePath2) {
|
|
1733
1943
|
if (existsSync14(configPath)) {
|
|
1734
1944
|
try {
|
|
1735
|
-
const configContent =
|
|
1945
|
+
const configContent = readFileSync13(configPath, "utf-8");
|
|
1736
1946
|
const config = JSON.parse(configContent);
|
|
1737
1947
|
if (config.guard_enforcement === "warn")
|
|
1738
1948
|
return "warn";
|
|
@@ -1751,29 +1961,30 @@ function getPlanConfirmed(statePath2) {
|
|
|
1751
1961
|
if (!existsSync14(statePath2))
|
|
1752
1962
|
return false;
|
|
1753
1963
|
try {
|
|
1754
|
-
const content =
|
|
1964
|
+
const content = readFileSync13(statePath2, "utf-8");
|
|
1755
1965
|
const match = content.match(/plan_confirmed:\s*(true|false)/i);
|
|
1756
1966
|
return match ? match[1].toLowerCase() === "true" : false;
|
|
1757
1967
|
} catch {
|
|
1758
1968
|
return false;
|
|
1759
1969
|
}
|
|
1760
1970
|
}
|
|
1761
|
-
function getWarningMessage(
|
|
1762
|
-
if (!existsSync14(
|
|
1971
|
+
function getWarningMessage(planningDir2) {
|
|
1972
|
+
if (!existsSync14(join14(planningDir2, STATE_FILE2))) {
|
|
1763
1973
|
return "No .planning/ found. Run /new-project first.";
|
|
1764
1974
|
}
|
|
1765
1975
|
return "Plan not confirmed. Run /plan and confirm to enable execution.";
|
|
1766
1976
|
}
|
|
1767
|
-
function getBlockMessage(
|
|
1768
|
-
if (!existsSync14(
|
|
1977
|
+
function getBlockMessage(planningDir2) {
|
|
1978
|
+
if (!existsSync14(join14(planningDir2, STATE_FILE2))) {
|
|
1769
1979
|
return "No .planning/ found. Run /new-project first.";
|
|
1770
1980
|
}
|
|
1771
1981
|
return "Plan not confirmed. Run /plan and confirm to enable execution.";
|
|
1772
1982
|
}
|
|
1773
1983
|
|
|
1774
1984
|
// src/hooks/tool-guard.ts
|
|
1775
|
-
import { existsSync as existsSync15, readFileSync as
|
|
1776
|
-
import { join as
|
|
1985
|
+
import { existsSync as existsSync15, readFileSync as readFileSync14 } from "fs";
|
|
1986
|
+
import { join as join15 } from "path";
|
|
1987
|
+
var IS_ENABLED = () => process.env.FLOWDECK_TOOL_GUARD_ENABLED === "on";
|
|
1777
1988
|
var BLOCKED_PATTERNS = {
|
|
1778
1989
|
read: [".env", ".pem", ".key", ".secret"],
|
|
1779
1990
|
write: ["node_modules"],
|
|
@@ -1819,11 +2030,11 @@ function isBlocked(tool17, args) {
|
|
|
1819
2030
|
return null;
|
|
1820
2031
|
}
|
|
1821
2032
|
function checkArchConstraint(directory, filePath) {
|
|
1822
|
-
const constraintsPath =
|
|
2033
|
+
const constraintsPath = join15(codebaseDir(directory), "CONSTRAINTS.md");
|
|
1823
2034
|
if (!existsSync15(constraintsPath))
|
|
1824
2035
|
return null;
|
|
1825
2036
|
try {
|
|
1826
|
-
const content =
|
|
2037
|
+
const content = readFileSync14(constraintsPath, "utf-8");
|
|
1827
2038
|
const match = content.match(/## Forbidden Paths\n([\s\S]*?)(?:\n##|$)/);
|
|
1828
2039
|
if (!match)
|
|
1829
2040
|
return null;
|
|
@@ -1847,6 +2058,8 @@ function checkPhaseEnforcement(directory) {
|
|
|
1847
2058
|
return null;
|
|
1848
2059
|
}
|
|
1849
2060
|
async function toolGuardHook(ctx, input, output) {
|
|
2061
|
+
if (!IS_ENABLED())
|
|
2062
|
+
return;
|
|
1850
2063
|
if (input.tool !== "bash" && input.tool !== "read" && input.tool !== "write" && input.tool !== "edit") {
|
|
1851
2064
|
return;
|
|
1852
2065
|
}
|
|
@@ -1868,13 +2081,13 @@ async function toolGuardHook(ctx, input, output) {
|
|
|
1868
2081
|
}
|
|
1869
2082
|
|
|
1870
2083
|
// src/hooks/session-start.ts
|
|
1871
|
-
import { existsSync as existsSync16, readFileSync as
|
|
2084
|
+
import { existsSync as existsSync16, readFileSync as readFileSync15 } from "fs";
|
|
1872
2085
|
async function sessionStartHook(ctx) {
|
|
1873
|
-
const
|
|
2086
|
+
const planningDir2 = ctx.directory + "/.planning";
|
|
1874
2087
|
const codebaseDirectory = codebaseDir(ctx.directory);
|
|
1875
2088
|
const workspaceRoot = findWorkspaceRoot(ctx.directory);
|
|
1876
2089
|
const config = workspaceRoot ? getWorkspaceConfig(ctx.directory) : null;
|
|
1877
|
-
if (!existsSync16(
|
|
2090
|
+
if (!existsSync16(planningDir2)) {
|
|
1878
2091
|
return {
|
|
1879
2092
|
flowdeck_phase: null,
|
|
1880
2093
|
flowdeck_status: "no_plan",
|
|
@@ -1890,7 +2103,7 @@ async function sessionStartHook(ctx) {
|
|
|
1890
2103
|
}
|
|
1891
2104
|
try {
|
|
1892
2105
|
const stateFilePath = statePath(ctx.directory);
|
|
1893
|
-
const content =
|
|
2106
|
+
const content = readFileSync15(stateFilePath, "utf-8");
|
|
1894
2107
|
const state = parseState(content);
|
|
1895
2108
|
const currentPhase = state["current_phase"] || {};
|
|
1896
2109
|
const result = {
|
|
@@ -1979,8 +2192,8 @@ function notifyPermissionNeeded(tool17) {
|
|
|
1979
2192
|
}
|
|
1980
2193
|
|
|
1981
2194
|
// src/hooks/patch-trust.ts
|
|
1982
|
-
import { existsSync as existsSync17, readFileSync as
|
|
1983
|
-
import { join as
|
|
2195
|
+
import { existsSync as existsSync17, readFileSync as readFileSync16 } from "fs";
|
|
2196
|
+
import { join as join16 } from "path";
|
|
1984
2197
|
var HIGH_RISK_KEYWORDS = [
|
|
1985
2198
|
"password",
|
|
1986
2199
|
"secret",
|
|
@@ -2002,11 +2215,11 @@ var HIGH_RISK_KEYWORDS = [
|
|
|
2002
2215
|
"privilege"
|
|
2003
2216
|
];
|
|
2004
2217
|
function loadVolatility(directory) {
|
|
2005
|
-
const p =
|
|
2218
|
+
const p = join16(codebaseDir(directory), "VOLATILITY.json");
|
|
2006
2219
|
if (!existsSync17(p))
|
|
2007
2220
|
return {};
|
|
2008
2221
|
try {
|
|
2009
|
-
const data = JSON.parse(
|
|
2222
|
+
const data = JSON.parse(readFileSync16(p, "utf-8"));
|
|
2010
2223
|
const map = {};
|
|
2011
2224
|
for (const entry of data.entries ?? [])
|
|
2012
2225
|
map[entry.path] = entry.stability;
|
|
@@ -2016,11 +2229,11 @@ function loadVolatility(directory) {
|
|
|
2016
2229
|
}
|
|
2017
2230
|
}
|
|
2018
2231
|
function loadFailedPaths(directory) {
|
|
2019
|
-
const p =
|
|
2232
|
+
const p = join16(codebaseDir(directory), "FAILURES.json");
|
|
2020
2233
|
if (!existsSync17(p))
|
|
2021
2234
|
return [];
|
|
2022
2235
|
try {
|
|
2023
|
-
const data = JSON.parse(
|
|
2236
|
+
const data = JSON.parse(readFileSync16(p, "utf-8"));
|
|
2024
2237
|
return (data.entries ?? []).flatMap((e) => e.affected_paths ?? []);
|
|
2025
2238
|
} catch {
|
|
2026
2239
|
return [];
|
|
@@ -2067,6 +2280,10 @@ async function patchTrustHook(ctx, input, output) {
|
|
|
2067
2280
|
return;
|
|
2068
2281
|
const trust = scorePatch(ctx.directory, filePath, content);
|
|
2069
2282
|
if (trust.verdict === "high-risk") {
|
|
2283
|
+
const highRiskEnabled = process.env.FLOWDECK_PATCH_TRUST_HIGH_RISK_ENABLED;
|
|
2284
|
+
if (highRiskEnabled !== "true" && highRiskEnabled !== "1") {
|
|
2285
|
+
return;
|
|
2286
|
+
}
|
|
2070
2287
|
throw new Error(`[flowdeck] PATCH-TRUST HIGH-RISK (score=${trust.score}): ${filePath}
|
|
2071
2288
|
Signals: ${trust.signals.join("; ")}
|
|
2072
2289
|
This edit requires explicit human review before applying.`);
|
|
@@ -2077,8 +2294,8 @@ async function patchTrustHook(ctx, input, output) {
|
|
|
2077
2294
|
}
|
|
2078
2295
|
|
|
2079
2296
|
// src/hooks/decision-trace-hook.ts
|
|
2080
|
-
import { existsSync as existsSync18, mkdirSync as mkdirSync9, appendFileSync as
|
|
2081
|
-
import { join as
|
|
2297
|
+
import { existsSync as existsSync18, mkdirSync as mkdirSync9, appendFileSync as appendFileSync2 } from "fs";
|
|
2298
|
+
import { join as join17 } from "path";
|
|
2082
2299
|
async function decisionTraceHook(ctx, input, output) {
|
|
2083
2300
|
if (input.tool !== "write" && input.tool !== "edit")
|
|
2084
2301
|
return;
|
|
@@ -2100,11 +2317,34 @@ async function decisionTraceHook(ctx, input, output) {
|
|
|
2100
2317
|
risk_level: "unknown",
|
|
2101
2318
|
auto_recorded: true
|
|
2102
2319
|
};
|
|
2103
|
-
|
|
2320
|
+
appendFileSync2(join17(base, "DECISIONS.jsonl"), JSON.stringify(entry) + `
|
|
2104
2321
|
`, "utf-8");
|
|
2105
2322
|
} catch {}
|
|
2106
2323
|
}
|
|
2107
2324
|
|
|
2325
|
+
// src/services/telemetry.ts
|
|
2326
|
+
import { existsSync as existsSync19, readFileSync as readFileSync17, appendFileSync as appendFileSync3, mkdirSync as mkdirSync10 } from "fs";
|
|
2327
|
+
import { join as join18 } from "path";
|
|
2328
|
+
import { randomUUID } from "crypto";
|
|
2329
|
+
function telemetryPath(dir) {
|
|
2330
|
+
return join18(codebaseDir(dir), "TELEMETRY.jsonl");
|
|
2331
|
+
}
|
|
2332
|
+
function appendEvent(dir, partial) {
|
|
2333
|
+
if (process.env.TELEMETRY_ENABLED !== "true")
|
|
2334
|
+
return null;
|
|
2335
|
+
const cd = codebaseDir(dir);
|
|
2336
|
+
if (!existsSync19(cd))
|
|
2337
|
+
mkdirSync10(cd, { recursive: true });
|
|
2338
|
+
const event = {
|
|
2339
|
+
id: randomUUID(),
|
|
2340
|
+
ts: new Date().toISOString(),
|
|
2341
|
+
...partial
|
|
2342
|
+
};
|
|
2343
|
+
appendFileSync3(telemetryPath(dir), JSON.stringify(event) + `
|
|
2344
|
+
`, "utf-8");
|
|
2345
|
+
return event;
|
|
2346
|
+
}
|
|
2347
|
+
|
|
2108
2348
|
// src/hooks/telemetry-hook.ts
|
|
2109
2349
|
async function telemetryHook(context, toolInput, output) {
|
|
2110
2350
|
const dir = context.directory ?? process.cwd();
|
|
@@ -2131,7 +2371,7 @@ async function telemetryAfterHook(context, toolInput, _output) {
|
|
|
2131
2371
|
}
|
|
2132
2372
|
|
|
2133
2373
|
// src/services/approval-manager.ts
|
|
2134
|
-
import { existsSync as
|
|
2374
|
+
import { existsSync as existsSync20, readFileSync as readFileSync18, writeFileSync as writeFileSync13, mkdirSync as mkdirSync11 } from "fs";
|
|
2135
2375
|
import { join as join19 } from "path";
|
|
2136
2376
|
var APPROVAL_TTL_MS = 30 * 60 * 1000;
|
|
2137
2377
|
var SENSITIVE_PATTERNS = [
|
|
@@ -2173,7 +2413,7 @@ function approvalsPath(dir) {
|
|
|
2173
2413
|
}
|
|
2174
2414
|
function loadStore(dir) {
|
|
2175
2415
|
const p = approvalsPath(dir);
|
|
2176
|
-
if (!
|
|
2416
|
+
if (!existsSync20(p))
|
|
2177
2417
|
return { requests: [] };
|
|
2178
2418
|
try {
|
|
2179
2419
|
return JSON.parse(readFileSync18(p, "utf-8"));
|
|
@@ -2188,8 +2428,11 @@ function checkApproval(dir, file_path, command) {
|
|
|
2188
2428
|
}
|
|
2189
2429
|
|
|
2190
2430
|
// src/hooks/approval-hook.ts
|
|
2431
|
+
var ENABLED2 = process.env.FLOWDECK_APPROVAL_HOOK_ENABLED === "on";
|
|
2191
2432
|
var WRITE_TOOLS = new Set(["write_file", "edit_file", "create_file", "apply_patch", "str_replace_editor", "write"]);
|
|
2192
2433
|
async function approvalHook(context, toolInput, output) {
|
|
2434
|
+
if (!ENABLED2)
|
|
2435
|
+
return;
|
|
2193
2436
|
const dir = context.directory ?? process.cwd();
|
|
2194
2437
|
const tool17 = toolInput.name ?? toolInput.tool ?? "";
|
|
2195
2438
|
if (!WRITE_TOOLS.has(tool17))
|
|
@@ -2268,7 +2511,7 @@ function createContextWindowMonitorHook() {
|
|
|
2268
2511
|
}
|
|
2269
2512
|
|
|
2270
2513
|
// src/hooks/shell-env-hook.ts
|
|
2271
|
-
import { existsSync as
|
|
2514
|
+
import { existsSync as existsSync21, readFileSync as readFileSync19 } from "fs";
|
|
2272
2515
|
import { join as join20 } from "path";
|
|
2273
2516
|
import { createRequire } from "module";
|
|
2274
2517
|
var _version;
|
|
@@ -2305,7 +2548,7 @@ var MARKER_TO_LANG = {
|
|
|
2305
2548
|
};
|
|
2306
2549
|
function detectPackageManager(root) {
|
|
2307
2550
|
for (const [lockfile, pm] of Object.entries(LOCKFILE_TO_PM)) {
|
|
2308
|
-
if (
|
|
2551
|
+
if (existsSync21(join20(root, lockfile)))
|
|
2309
2552
|
return pm;
|
|
2310
2553
|
}
|
|
2311
2554
|
return;
|
|
@@ -2314,7 +2557,7 @@ function detectLanguages(root) {
|
|
|
2314
2557
|
const langs = [];
|
|
2315
2558
|
const seen = new Set;
|
|
2316
2559
|
for (const [marker, lang] of Object.entries(MARKER_TO_LANG)) {
|
|
2317
|
-
if (!seen.has(lang) &&
|
|
2560
|
+
if (!seen.has(lang) && existsSync21(join20(root, marker))) {
|
|
2318
2561
|
langs.push(lang);
|
|
2319
2562
|
seen.add(lang);
|
|
2320
2563
|
}
|
|
@@ -2323,7 +2566,7 @@ function detectLanguages(root) {
|
|
|
2323
2566
|
}
|
|
2324
2567
|
function readCurrentPhase(root) {
|
|
2325
2568
|
const statePath2 = join20(root, ".planning", "STATE.md");
|
|
2326
|
-
if (!
|
|
2569
|
+
if (!existsSync21(statePath2))
|
|
2327
2570
|
return;
|
|
2328
2571
|
try {
|
|
2329
2572
|
const content = readFileSync19(statePath2, "utf-8");
|
|
@@ -2356,7 +2599,8 @@ function createShellEnvHook(ctx) {
|
|
|
2356
2599
|
// src/hooks/todo-hook.ts
|
|
2357
2600
|
function createTodoHook(client) {
|
|
2358
2601
|
return async (event) => {
|
|
2359
|
-
const
|
|
2602
|
+
const rawTodos = event.todos;
|
|
2603
|
+
const todos = Array.isArray(rawTodos) ? rawTodos : [];
|
|
2360
2604
|
const completed = todos.filter((t) => t.done || t.status === "completed").length;
|
|
2361
2605
|
const total = todos.length;
|
|
2362
2606
|
if (total === 0)
|
|
@@ -2425,7 +2669,7 @@ function createSessionIdleHook(client, tracker) {
|
|
|
2425
2669
|
}
|
|
2426
2670
|
|
|
2427
2671
|
// src/hooks/compaction-hook.ts
|
|
2428
|
-
import { existsSync as
|
|
2672
|
+
import { existsSync as existsSync22, readFileSync as readFileSync20 } from "fs";
|
|
2429
2673
|
import { join as join21 } from "path";
|
|
2430
2674
|
var STRUCTURED_SUMMARY_PROMPT = `
|
|
2431
2675
|
When summarizing this session, you MUST include the following sections:
|
|
@@ -2466,7 +2710,7 @@ For each: agent name, status, description, session_id.
|
|
|
2466
2710
|
`;
|
|
2467
2711
|
function readPlanningState2(directory) {
|
|
2468
2712
|
const statePath2 = join21(directory, ".planning", "STATE.md");
|
|
2469
|
-
if (!
|
|
2713
|
+
if (!existsSync22(statePath2))
|
|
2470
2714
|
return null;
|
|
2471
2715
|
try {
|
|
2472
2716
|
const content = readFileSync20(statePath2, "utf-8");
|
|
@@ -2524,7 +2768,6 @@ var BLOCKED_TOOLS = new Set([
|
|
|
2524
2768
|
]);
|
|
2525
2769
|
var ALWAYS_ALLOWED = new Set([
|
|
2526
2770
|
"delegate",
|
|
2527
|
-
"run-parallel",
|
|
2528
2771
|
"run-pipeline",
|
|
2529
2772
|
"council",
|
|
2530
2773
|
"planning-state",
|
|
@@ -5563,12 +5806,12 @@ function getAgentConfigs(agentModels) {
|
|
|
5563
5806
|
}
|
|
5564
5807
|
|
|
5565
5808
|
// src/config/loader.ts
|
|
5566
|
-
import { existsSync as
|
|
5809
|
+
import { existsSync as existsSync23, readFileSync as readFileSync21 } from "fs";
|
|
5567
5810
|
import { join as join22 } from "path";
|
|
5568
|
-
import { homedir } from "os";
|
|
5811
|
+
import { homedir as homedir2 } from "os";
|
|
5569
5812
|
var CONFIG_FILENAME = "flowdeck.json";
|
|
5570
5813
|
function getGlobalConfigDir() {
|
|
5571
|
-
return process.env.OPENCODE_CONFIG_DIR || (process.env.XDG_CONFIG_HOME ? join22(process.env.XDG_CONFIG_HOME, "opencode") : join22(
|
|
5814
|
+
return process.env.OPENCODE_CONFIG_DIR || (process.env.XDG_CONFIG_HOME ? join22(process.env.XDG_CONFIG_HOME, "opencode") : join22(homedir2(), ".config", "opencode"));
|
|
5572
5815
|
}
|
|
5573
5816
|
function loadFlowDeckConfig(directory) {
|
|
5574
5817
|
const candidates = [];
|
|
@@ -5577,7 +5820,7 @@ function loadFlowDeckConfig(directory) {
|
|
|
5577
5820
|
}
|
|
5578
5821
|
candidates.push(join22(getGlobalConfigDir(), CONFIG_FILENAME));
|
|
5579
5822
|
for (const configPath of candidates) {
|
|
5580
|
-
if (
|
|
5823
|
+
if (existsSync23(configPath)) {
|
|
5581
5824
|
try {
|
|
5582
5825
|
const content = readFileSync21(configPath, "utf-8");
|
|
5583
5826
|
return JSON.parse(content);
|
|
@@ -5589,10 +5832,29 @@ function loadFlowDeckConfig(directory) {
|
|
|
5589
5832
|
return {};
|
|
5590
5833
|
}
|
|
5591
5834
|
// src/index.ts
|
|
5835
|
+
function loadRulePaths() {
|
|
5836
|
+
const __dir = dirname4(fileURLToPath2(import.meta.url));
|
|
5837
|
+
const rulesDir = join23(__dir, "..", "src", "rules");
|
|
5838
|
+
if (!existsSync24(rulesDir))
|
|
5839
|
+
return [];
|
|
5840
|
+
const paths = [];
|
|
5841
|
+
function walk(dir) {
|
|
5842
|
+
for (const entry of readdirSync3(dir, { withFileTypes: true })) {
|
|
5843
|
+
const full = join23(dir, entry.name);
|
|
5844
|
+
if (entry.isDirectory()) {
|
|
5845
|
+
walk(full);
|
|
5846
|
+
} else if (entry.isFile() && entry.name.endsWith(".md") && entry.name !== "README.md") {
|
|
5847
|
+
paths.push(full);
|
|
5848
|
+
}
|
|
5849
|
+
}
|
|
5850
|
+
}
|
|
5851
|
+
walk(rulesDir);
|
|
5852
|
+
return paths;
|
|
5853
|
+
}
|
|
5592
5854
|
function loadCommands() {
|
|
5593
5855
|
const __dir = dirname4(fileURLToPath2(import.meta.url));
|
|
5594
5856
|
const commandsDir = join23(__dir, "..", "src", "commands");
|
|
5595
|
-
if (!
|
|
5857
|
+
if (!existsSync24(commandsDir))
|
|
5596
5858
|
return {};
|
|
5597
5859
|
const commands = {};
|
|
5598
5860
|
try {
|
|
@@ -5617,7 +5879,6 @@ function loadCommands() {
|
|
|
5617
5879
|
}
|
|
5618
5880
|
var plugin = async (input, _options) => {
|
|
5619
5881
|
const { directory, client, worktree } = input;
|
|
5620
|
-
const runParallelTool = createRunParallelTool(client);
|
|
5621
5882
|
const runPipelineTool = createRunPipelineTool(client);
|
|
5622
5883
|
const delegateTool = createDelegateTool(client);
|
|
5623
5884
|
const councilTool = createCouncilTool(client);
|
|
@@ -5679,7 +5940,7 @@ var plugin = async (input, _options) => {
|
|
|
5679
5940
|
}
|
|
5680
5941
|
}
|
|
5681
5942
|
const skillsDir = join23(dirname4(fileURLToPath2(import.meta.url)), "..", "src", "skills");
|
|
5682
|
-
if (
|
|
5943
|
+
if (existsSync24(skillsDir)) {
|
|
5683
5944
|
const cfgAny = cfg;
|
|
5684
5945
|
if (!cfgAny.skills || typeof cfgAny.skills !== "object") {
|
|
5685
5946
|
cfgAny.skills = { paths: [] };
|
|
@@ -5691,12 +5952,23 @@ var plugin = async (input, _options) => {
|
|
|
5691
5952
|
cfgSkills.paths.push(skillsDir);
|
|
5692
5953
|
}
|
|
5693
5954
|
}
|
|
5955
|
+
const rulePaths = loadRulePaths();
|
|
5956
|
+
if (rulePaths.length > 0) {
|
|
5957
|
+
if (!Array.isArray(cfg.instructions)) {
|
|
5958
|
+
cfg.instructions = [];
|
|
5959
|
+
}
|
|
5960
|
+
const existing = new Set(cfg.instructions);
|
|
5961
|
+
for (const p of rulePaths) {
|
|
5962
|
+
if (!existing.has(p)) {
|
|
5963
|
+
cfg.instructions.push(p);
|
|
5964
|
+
}
|
|
5965
|
+
}
|
|
5966
|
+
}
|
|
5694
5967
|
},
|
|
5695
5968
|
tool: {
|
|
5696
5969
|
"planning-state": planningStateTool,
|
|
5697
5970
|
"codebase-state": codebaseStateTool,
|
|
5698
5971
|
"workspace-state": workspaceStateTool,
|
|
5699
|
-
"run-parallel": runParallelTool,
|
|
5700
5972
|
"run-pipeline": runPipelineTool,
|
|
5701
5973
|
delegate: delegateTool,
|
|
5702
5974
|
"repo-memory": repoMemoryTool,
|
|
@@ -5708,7 +5980,8 @@ var plugin = async (input, _options) => {
|
|
|
5708
5980
|
council: councilTool,
|
|
5709
5981
|
"context-generator": contextGeneratorTool,
|
|
5710
5982
|
"create-skill": createSkillTool,
|
|
5711
|
-
reflect: reflectTool
|
|
5983
|
+
reflect: reflectTool,
|
|
5984
|
+
"memory-search": memorySearchTool
|
|
5712
5985
|
},
|
|
5713
5986
|
"shell.env": shellEnvHook,
|
|
5714
5987
|
"todo.updated": todoHook,
|
|
@@ -5720,11 +5993,34 @@ var plugin = async (input, _options) => {
|
|
|
5720
5993
|
},
|
|
5721
5994
|
event: async ({ event }) => {
|
|
5722
5995
|
const type = event?.type ?? "";
|
|
5723
|
-
await contextMonitor.event({ event });
|
|
5724
|
-
orchestratorGuard.onEvent(event);
|
|
5725
5996
|
if (type === "session.created" || type === "session.started") {
|
|
5997
|
+
const sessionId = event?.sessionID ?? event?.sessionId ?? "";
|
|
5998
|
+
if (sessionId) {
|
|
5999
|
+
memoryHook.onSessionCreated(directory, sessionId, event?.prompt);
|
|
6000
|
+
}
|
|
5726
6001
|
await sessionStartHook({ directory });
|
|
5727
|
-
} else if (type === "
|
|
6002
|
+
} else if (type === "message.updated" && event?.event) {
|
|
6003
|
+
const msgEvent = event.event;
|
|
6004
|
+
const sessionId = msgEvent?.sessionID ?? msgEvent?.sessionId ?? "";
|
|
6005
|
+
if (sessionId) {
|
|
6006
|
+
memoryHook.onMessageUpdated(sessionId, msgEvent.role, msgEvent.content, directory);
|
|
6007
|
+
}
|
|
6008
|
+
} else if (type === "session.compacted" && event?.event) {
|
|
6009
|
+
const compactEvent = event.event;
|
|
6010
|
+
const sessionId = compactEvent?.sessionID ?? compactEvent?.sessionId ?? "";
|
|
6011
|
+
if (sessionId) {
|
|
6012
|
+
memoryHook.onSessionCompact(sessionId, compactEvent.summary ?? "");
|
|
6013
|
+
}
|
|
6014
|
+
} else if (type === "session.deleted" && event?.event) {
|
|
6015
|
+
const delEvent = event.event;
|
|
6016
|
+
const sessionId = delEvent?.sessionID ?? delEvent?.sessionId ?? "";
|
|
6017
|
+
if (sessionId) {
|
|
6018
|
+
memoryHook.clearSession(sessionId);
|
|
6019
|
+
}
|
|
6020
|
+
}
|
|
6021
|
+
await contextMonitor.event({ event });
|
|
6022
|
+
orchestratorGuard.onEvent(event);
|
|
6023
|
+
if (type === "session.idle") {
|
|
5728
6024
|
await sessionIdleHook();
|
|
5729
6025
|
await autoLearnHook();
|
|
5730
6026
|
}
|
|
@@ -5740,6 +6036,10 @@ var plugin = async (input, _options) => {
|
|
|
5740
6036
|
},
|
|
5741
6037
|
"tool.execute.after": async (toolInput, toolOutput) => {
|
|
5742
6038
|
await telemetryAfterHook({ directory }, toolInput, toolOutput);
|
|
6039
|
+
const sessionId = toolInput?.sessionID ?? toolInput?.sessionId ?? "";
|
|
6040
|
+
if (sessionId && toolInput?.tool) {
|
|
6041
|
+
memoryHook.onToolExecuted(sessionId, toolInput.tool, toolInput, toolOutput?.output ?? null, directory);
|
|
6042
|
+
}
|
|
5743
6043
|
await contextMonitor["tool.execute.after"](toolInput, toolOutput);
|
|
5744
6044
|
}
|
|
5745
6045
|
};
|