@dv.nghiem/flowdeck 0.2.3 → 0.3.0
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/memory-hook.d.ts +21 -0
- package/dist/hooks/memory-hook.d.ts.map +1 -0
- package/dist/hooks/orchestrator-guard-hook.d.ts +2 -1
- package/dist/hooks/orchestrator-guard-hook.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/index.d.ts.map +1 -1
- package/dist/index.js +649 -310
- package/dist/services/memory-store.d.ts +40 -0
- package/dist/services/memory-store.d.ts.map +1 -0
- package/dist/services/telemetry.d.ts +1 -1
- package/dist/services/telemetry.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 +2 -1
- package/docs/index.md +22 -28
- package/docs/memory.md +69 -0
- package/docs/quick-start.md +1 -1
- package/docs/workflows.md +72 -320
- package/package.json +1 -2
- package/src/commands/fd-deploy-check.md +189 -34
- package/src/commands/fd-discuss.md +44 -6
- package/src/commands/fd-fix-bug.md +47 -20
- package/src/commands/fd-map-codebase.md +66 -18
- package/src/commands/fd-multi-repo.md +130 -6
- package/src/commands/fd-new-feature.md +164 -21
- package/src/commands/fd-new-project.md +14 -1
- package/src/commands/fd-plan.md +66 -44
- 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/commands/fd-write-docs.md +55 -23
- 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 -227
- 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 -62
- 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/src/workflows/debug-flow.md +0 -119
- package/src/workflows/deploy-check-flow.md +0 -98
- package/src/workflows/discuss-flow.md +0 -97
- package/src/workflows/execute-flow.md +0 -233
- package/src/workflows/execute-phase.md +0 -145
- package/src/workflows/fix-bug-flow.md +0 -210
- package/src/workflows/map-codebase-flow.md +0 -92
- package/src/workflows/multi-repo-flow.md +0 -226
- package/src/workflows/parallel-execution-flow.md +0 -236
- package/src/workflows/plan-flow.md +0 -126
- package/src/workflows/plan-phase.md +0 -101
- package/src/workflows/refactor-flow.md +0 -122
- package/src/workflows/review-code-flow.md +0 -105
- package/src/workflows/spec-driven-flow.md +0 -43
- package/src/workflows/write-docs-flow.md +0 -95
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
|
-
import { readdirSync as readdirSync3, readFileSync as readFileSync22, existsSync as
|
|
3
|
-
import { join as
|
|
2
|
+
import { readdirSync as readdirSync3, readFileSync as readFileSync22, existsSync as existsSync24 } from "fs";
|
|
3
|
+
import { join as join23, basename } from "path";
|
|
4
4
|
import { dirname as dirname4 } from "path";
|
|
5
5
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
6
6
|
|
|
@@ -507,127 +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
512
|
function extractText(parts) {
|
|
513
513
|
return parts.filter((p) => p.type === "text" && typeof p.text === "string").map((p) => p.text).join(`
|
|
514
514
|
`);
|
|
515
515
|
}
|
|
516
|
-
function createRunParallelTool(client) {
|
|
517
|
-
return tool4({
|
|
518
|
-
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.",
|
|
519
|
-
args: {
|
|
520
|
-
tasks: tool4.schema.array(tool4.schema.object({
|
|
521
|
-
agent: tool4.schema.string(),
|
|
522
|
-
prompt: tool4.schema.string(),
|
|
523
|
-
context: tool4.schema.string().optional()
|
|
524
|
-
}))
|
|
525
|
-
},
|
|
526
|
-
async execute(args, context) {
|
|
527
|
-
const startTime = Date.now();
|
|
528
|
-
const childSessionIds = [];
|
|
529
|
-
context.abort.addEventListener("abort", () => {
|
|
530
|
-
for (const id of childSessionIds) {
|
|
531
|
-
client.session.abort({
|
|
532
|
-
path: { id },
|
|
533
|
-
query: { directory: context.directory }
|
|
534
|
-
}).catch(() => {});
|
|
535
|
-
}
|
|
536
|
-
});
|
|
537
|
-
const promises = args.tasks.map(async (task) => {
|
|
538
|
-
const taskStart = Date.now();
|
|
539
|
-
const createRes = await client.session.create({
|
|
540
|
-
body: { parentID: context.sessionID, title: `${task.agent}-subtask` },
|
|
541
|
-
query: { directory: context.directory }
|
|
542
|
-
});
|
|
543
|
-
if (createRes.error || !createRes.data?.id) {
|
|
544
|
-
return {
|
|
545
|
-
agent: task.agent,
|
|
546
|
-
success: false,
|
|
547
|
-
error: `Failed to create session: ${createRes.error?.detail ?? "unknown"}`,
|
|
548
|
-
duration_ms: Date.now() - taskStart
|
|
549
|
-
};
|
|
550
|
-
}
|
|
551
|
-
const childId = createRes.data.id;
|
|
552
|
-
childSessionIds.push(childId);
|
|
553
|
-
const fullPrompt = task.context ? `${task.context}
|
|
554
|
-
|
|
555
|
-
---
|
|
556
|
-
|
|
557
|
-
${task.prompt}` : task.prompt;
|
|
558
|
-
const promptRes = await client.session.prompt({
|
|
559
|
-
path: { id: childId },
|
|
560
|
-
body: {
|
|
561
|
-
agent: task.agent,
|
|
562
|
-
parts: [{ type: "text", text: fullPrompt }],
|
|
563
|
-
tools: { question: false }
|
|
564
|
-
},
|
|
565
|
-
query: { directory: context.directory }
|
|
566
|
-
});
|
|
567
|
-
if (promptRes.error) {
|
|
568
|
-
return {
|
|
569
|
-
agent: task.agent,
|
|
570
|
-
session_id: childId,
|
|
571
|
-
success: false,
|
|
572
|
-
error: `Prompt failed: ${promptRes.error?.detail ?? "unknown"}`,
|
|
573
|
-
duration_ms: Date.now() - taskStart
|
|
574
|
-
};
|
|
575
|
-
}
|
|
576
|
-
const info = promptRes.data?.info;
|
|
577
|
-
if (info?.error) {
|
|
578
|
-
return {
|
|
579
|
-
agent: task.agent,
|
|
580
|
-
session_id: childId,
|
|
581
|
-
success: false,
|
|
582
|
-
error: `Agent error: ${JSON.stringify(info.error)}`,
|
|
583
|
-
duration_ms: Date.now() - taskStart
|
|
584
|
-
};
|
|
585
|
-
}
|
|
586
|
-
const output = extractText(promptRes.data?.parts ?? []);
|
|
587
|
-
return {
|
|
588
|
-
agent: task.agent,
|
|
589
|
-
session_id: childId,
|
|
590
|
-
success: true,
|
|
591
|
-
output: output || "(no text output)",
|
|
592
|
-
duration_ms: Date.now() - taskStart
|
|
593
|
-
};
|
|
594
|
-
});
|
|
595
|
-
const settled = await Promise.allSettled(promises);
|
|
596
|
-
const results = settled.map((result, i) => {
|
|
597
|
-
if (result.status === "fulfilled")
|
|
598
|
-
return result.value;
|
|
599
|
-
return {
|
|
600
|
-
agent: args.tasks[i].agent,
|
|
601
|
-
success: false,
|
|
602
|
-
error: result.reason?.message || String(result.reason),
|
|
603
|
-
duration_ms: Date.now() - startTime
|
|
604
|
-
};
|
|
605
|
-
});
|
|
606
|
-
return JSON.stringify({
|
|
607
|
-
results,
|
|
608
|
-
total_duration_ms: Date.now() - startTime,
|
|
609
|
-
failures: results.filter((r) => !r.success).map((r) => r.agent)
|
|
610
|
-
});
|
|
611
|
-
}
|
|
612
|
-
});
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
// src/tools/run-pipeline.ts
|
|
616
|
-
import { tool as tool5 } from "@opencode-ai/plugin";
|
|
617
|
-
function extractText2(parts) {
|
|
618
|
-
return parts.filter((p) => p.type === "text" && typeof p.text === "string").map((p) => p.text).join(`
|
|
619
|
-
`);
|
|
620
|
-
}
|
|
621
516
|
function createRunPipelineTool(client) {
|
|
622
|
-
return
|
|
517
|
+
return tool4({
|
|
623
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.",
|
|
624
519
|
args: {
|
|
625
|
-
steps:
|
|
626
|
-
agent:
|
|
627
|
-
prompt:
|
|
520
|
+
steps: tool4.schema.array(tool4.schema.object({
|
|
521
|
+
agent: tool4.schema.string(),
|
|
522
|
+
prompt: tool4.schema.string()
|
|
628
523
|
})),
|
|
629
|
-
initial_context:
|
|
630
|
-
abort_on_failure:
|
|
524
|
+
initial_context: tool4.schema.string().optional(),
|
|
525
|
+
abort_on_failure: tool4.schema.boolean().optional().default(true)
|
|
631
526
|
},
|
|
632
527
|
async execute(args, context) {
|
|
633
528
|
const startTime = Date.now();
|
|
@@ -700,7 +595,7 @@ ${step.prompt}` : step.prompt;
|
|
|
700
595
|
}
|
|
701
596
|
continue;
|
|
702
597
|
}
|
|
703
|
-
const output =
|
|
598
|
+
const output = extractText(promptRes.data?.parts ?? []);
|
|
704
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 });
|
|
705
600
|
carryContext = output;
|
|
706
601
|
}
|
|
@@ -717,18 +612,18 @@ ${step.prompt}` : step.prompt;
|
|
|
717
612
|
}
|
|
718
613
|
|
|
719
614
|
// src/tools/delegate.ts
|
|
720
|
-
import { tool as
|
|
721
|
-
function
|
|
615
|
+
import { tool as tool5 } from "@opencode-ai/plugin";
|
|
616
|
+
function extractText2(parts) {
|
|
722
617
|
return parts.filter((p) => p.type === "text" && typeof p.text === "string").map((p) => p.text).join(`
|
|
723
618
|
`);
|
|
724
619
|
}
|
|
725
620
|
function createDelegateTool(client) {
|
|
726
|
-
return
|
|
621
|
+
return tool5({
|
|
727
622
|
description: "Delegate a task to a single agent via a child session. Returns the agent's output.",
|
|
728
623
|
args: {
|
|
729
|
-
agent:
|
|
730
|
-
prompt:
|
|
731
|
-
context:
|
|
624
|
+
agent: tool5.schema.string(),
|
|
625
|
+
prompt: tool5.schema.string(),
|
|
626
|
+
context: tool5.schema.string().optional()
|
|
732
627
|
},
|
|
733
628
|
async execute(args, context) {
|
|
734
629
|
const startTime = Date.now();
|
|
@@ -784,7 +679,7 @@ ${args.prompt}` : args.prompt;
|
|
|
784
679
|
duration_ms: Date.now() - startTime
|
|
785
680
|
});
|
|
786
681
|
}
|
|
787
|
-
const output =
|
|
682
|
+
const output = extractText2(promptRes.data?.parts ?? []);
|
|
788
683
|
return JSON.stringify({
|
|
789
684
|
agent: args.agent,
|
|
790
685
|
session_id: childId,
|
|
@@ -797,7 +692,7 @@ ${args.prompt}` : args.prompt;
|
|
|
797
692
|
}
|
|
798
693
|
|
|
799
694
|
// src/tools/repo-memory.ts
|
|
800
|
-
import { tool as
|
|
695
|
+
import { tool as tool6 } from "@opencode-ai/plugin";
|
|
801
696
|
import { readFileSync as readFileSync5, writeFileSync as writeFileSync5, existsSync as existsSync5, mkdirSync as mkdirSync2 } from "fs";
|
|
802
697
|
import { join as join5 } from "path";
|
|
803
698
|
var MEMORY_FILE = "MEMORY.json";
|
|
@@ -824,26 +719,26 @@ function writeMemory(directory, memory) {
|
|
|
824
719
|
memory.last_updated = new Date().toISOString();
|
|
825
720
|
writeFileSync5(memoryPath(directory), JSON.stringify(memory, null, 2), "utf-8");
|
|
826
721
|
}
|
|
827
|
-
var repoMemoryTool =
|
|
722
|
+
var repoMemoryTool = tool6({
|
|
828
723
|
description: "Repo Memory Graph: read/write/query persistent architecture graph in .codebase/MEMORY.json (modules, dependencies, ownership, bug history, conventions)",
|
|
829
724
|
args: {
|
|
830
|
-
action:
|
|
831
|
-
node_id:
|
|
832
|
-
node:
|
|
833
|
-
type:
|
|
834
|
-
path:
|
|
835
|
-
owner:
|
|
836
|
-
tags:
|
|
837
|
-
dependencies:
|
|
838
|
-
dependents:
|
|
839
|
-
bug_history:
|
|
840
|
-
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())
|
|
841
736
|
}).optional(),
|
|
842
|
-
query:
|
|
843
|
-
type:
|
|
844
|
-
owner:
|
|
845
|
-
tag:
|
|
846
|
-
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()
|
|
847
742
|
}).optional()
|
|
848
743
|
},
|
|
849
744
|
async execute(args, context) {
|
|
@@ -898,7 +793,7 @@ var repoMemoryTool = tool7({
|
|
|
898
793
|
});
|
|
899
794
|
|
|
900
795
|
// src/tools/failure-replay.ts
|
|
901
|
-
import { tool as
|
|
796
|
+
import { tool as tool7 } from "@opencode-ai/plugin";
|
|
902
797
|
import { readFileSync as readFileSync6, writeFileSync as writeFileSync6, existsSync as existsSync6, mkdirSync as mkdirSync3 } from "fs";
|
|
903
798
|
import { join as join6 } from "path";
|
|
904
799
|
var FAILURES_FILE = "FAILURES.json";
|
|
@@ -922,26 +817,26 @@ function writeStore(directory, store) {
|
|
|
922
817
|
store.last_updated = new Date().toISOString();
|
|
923
818
|
writeFileSync6(failuresPath(directory), JSON.stringify(store, null, 2), "utf-8");
|
|
924
819
|
}
|
|
925
|
-
var failureReplayTool =
|
|
820
|
+
var failureReplayTool = tool7({
|
|
926
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",
|
|
927
822
|
args: {
|
|
928
|
-
action:
|
|
929
|
-
entry:
|
|
930
|
-
id:
|
|
931
|
-
type:
|
|
932
|
-
description:
|
|
933
|
-
affected_paths:
|
|
934
|
-
root_cause:
|
|
935
|
-
fix_applied:
|
|
936
|
-
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())
|
|
937
832
|
}).optional(),
|
|
938
|
-
query:
|
|
939
|
-
type:
|
|
940
|
-
path_prefix:
|
|
941
|
-
tag:
|
|
942
|
-
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()
|
|
943
838
|
}).optional(),
|
|
944
|
-
entry_id:
|
|
839
|
+
entry_id: tool7.schema.string().optional()
|
|
945
840
|
},
|
|
946
841
|
async execute(args, context) {
|
|
947
842
|
const dir = context.directory ?? process.cwd();
|
|
@@ -1003,7 +898,7 @@ var failureReplayTool = tool8({
|
|
|
1003
898
|
});
|
|
1004
899
|
|
|
1005
900
|
// src/tools/decision-trace.ts
|
|
1006
|
-
import { tool as
|
|
901
|
+
import { tool as tool8 } from "@opencode-ai/plugin";
|
|
1007
902
|
import { readFileSync as readFileSync7, existsSync as existsSync7, mkdirSync as mkdirSync4, appendFileSync } from "fs";
|
|
1008
903
|
import { join as join7 } from "path";
|
|
1009
904
|
var DECISIONS_FILE = "DECISIONS.jsonl";
|
|
@@ -1023,29 +918,29 @@ function readDecisions(directory) {
|
|
|
1023
918
|
}
|
|
1024
919
|
}).filter(Boolean);
|
|
1025
920
|
}
|
|
1026
|
-
var decisionTraceTool =
|
|
921
|
+
var decisionTraceTool = tool8({
|
|
1027
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.",
|
|
1028
923
|
args: {
|
|
1029
|
-
action:
|
|
1030
|
-
entry:
|
|
1031
|
-
id:
|
|
1032
|
-
file_path:
|
|
1033
|
-
change_type:
|
|
1034
|
-
rationale:
|
|
1035
|
-
evidence:
|
|
1036
|
-
assumptions:
|
|
1037
|
-
alternatives_considered:
|
|
1038
|
-
risk_level:
|
|
1039
|
-
agent:
|
|
1040
|
-
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()
|
|
1041
936
|
}).optional(),
|
|
1042
|
-
query:
|
|
1043
|
-
file_path:
|
|
1044
|
-
change_type:
|
|
1045
|
-
risk_level:
|
|
1046
|
-
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()
|
|
1047
942
|
}).optional(),
|
|
1048
|
-
file_path:
|
|
943
|
+
file_path: tool8.schema.string().optional()
|
|
1049
944
|
},
|
|
1050
945
|
async execute(args, context) {
|
|
1051
946
|
const dir = context.directory ?? process.cwd();
|
|
@@ -1088,7 +983,7 @@ var decisionTraceTool = tool9({
|
|
|
1088
983
|
});
|
|
1089
984
|
|
|
1090
985
|
// src/tools/volatility-map.ts
|
|
1091
|
-
import { tool as
|
|
986
|
+
import { tool as tool9 } from "@opencode-ai/plugin";
|
|
1092
987
|
import { readFileSync as readFileSync8, writeFileSync as writeFileSync8, existsSync as existsSync8, mkdirSync as mkdirSync5 } from "fs";
|
|
1093
988
|
import { join as join8 } from "path";
|
|
1094
989
|
var VOLATILITY_FILE = "VOLATILITY.json";
|
|
@@ -1122,29 +1017,29 @@ function stabilityLabel(churn, hotfixes, todos) {
|
|
|
1122
1017
|
return "moderate";
|
|
1123
1018
|
return "stable";
|
|
1124
1019
|
}
|
|
1125
|
-
var volatilityMapTool =
|
|
1020
|
+
var volatilityMapTool = tool9({
|
|
1126
1021
|
description: "Codebase Volatility Map: read/write/query .codebase/VOLATILITY.json — highlights unstable zones based on churn, hotfix frequency, and TODO clusters",
|
|
1127
1022
|
args: {
|
|
1128
|
-
action:
|
|
1129
|
-
entries:
|
|
1130
|
-
path:
|
|
1131
|
-
churn_score:
|
|
1132
|
-
hotfix_count:
|
|
1133
|
-
todo_count:
|
|
1134
|
-
last_breakage:
|
|
1135
|
-
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())
|
|
1136
1031
|
})).optional(),
|
|
1137
|
-
entry:
|
|
1138
|
-
path:
|
|
1139
|
-
churn_score:
|
|
1140
|
-
hotfix_count:
|
|
1141
|
-
todo_count:
|
|
1142
|
-
last_breakage:
|
|
1143
|
-
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())
|
|
1144
1039
|
}).optional(),
|
|
1145
|
-
threshold:
|
|
1146
|
-
path_prefix:
|
|
1147
|
-
limit:
|
|
1040
|
+
threshold: tool9.schema.enum(["stable", "moderate", "volatile", "critical"]).optional(),
|
|
1041
|
+
path_prefix: tool9.schema.string().optional(),
|
|
1042
|
+
limit: tool9.schema.number().optional()
|
|
1148
1043
|
},
|
|
1149
1044
|
async execute(args, context) {
|
|
1150
1045
|
const dir = context.directory ?? process.cwd();
|
|
@@ -1196,7 +1091,7 @@ var volatilityMapTool = tool10({
|
|
|
1196
1091
|
});
|
|
1197
1092
|
|
|
1198
1093
|
// src/tools/policy-engine.ts
|
|
1199
|
-
import { tool as
|
|
1094
|
+
import { tool as tool10 } from "@opencode-ai/plugin";
|
|
1200
1095
|
import { readFileSync as readFileSync9, writeFileSync as writeFileSync9, existsSync as existsSync9, mkdirSync as mkdirSync6 } from "fs";
|
|
1201
1096
|
import { join as join9 } from "path";
|
|
1202
1097
|
var POLICIES_FILE = "POLICIES.json";
|
|
@@ -1220,24 +1115,24 @@ function writeStore3(directory, store) {
|
|
|
1220
1115
|
store.last_updated = new Date().toISOString();
|
|
1221
1116
|
writeFileSync9(policiesPath(directory), JSON.stringify(store, null, 2), "utf-8");
|
|
1222
1117
|
}
|
|
1223
|
-
var policyEngineTool =
|
|
1118
|
+
var policyEngineTool = tool10({
|
|
1224
1119
|
description: "Self-Healing Policy Engine: manage .codebase/POLICIES.json — add, list, query, toggle, and record violations of editing policies learned from past failures",
|
|
1225
1120
|
args: {
|
|
1226
|
-
action:
|
|
1227
|
-
policy:
|
|
1228
|
-
id:
|
|
1229
|
-
name:
|
|
1230
|
-
trigger:
|
|
1231
|
-
rule:
|
|
1232
|
-
source:
|
|
1233
|
-
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()
|
|
1234
1129
|
}).optional(),
|
|
1235
|
-
policy_id:
|
|
1236
|
-
active:
|
|
1237
|
-
query:
|
|
1238
|
-
source:
|
|
1239
|
-
active_only:
|
|
1240
|
-
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()
|
|
1241
1136
|
}).optional()
|
|
1242
1137
|
},
|
|
1243
1138
|
async execute(args, context) {
|
|
@@ -1299,16 +1194,16 @@ var policyEngineTool = tool11({
|
|
|
1299
1194
|
});
|
|
1300
1195
|
|
|
1301
1196
|
// src/tools/hash-edit.ts
|
|
1302
|
-
import { tool as
|
|
1197
|
+
import { tool as tool11 } from "@opencode-ai/plugin";
|
|
1303
1198
|
import { readFileSync as readFileSync10, writeFileSync as writeFileSync10 } from "fs";
|
|
1304
1199
|
import { createHash } from "crypto";
|
|
1305
|
-
var hashEditTool =
|
|
1200
|
+
var hashEditTool = tool11({
|
|
1306
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.",
|
|
1307
1202
|
args: {
|
|
1308
|
-
filePath:
|
|
1309
|
-
targetContent:
|
|
1310
|
-
expectedHash:
|
|
1311
|
-
replacementContent:
|
|
1203
|
+
filePath: tool11.schema.string(),
|
|
1204
|
+
targetContent: tool11.schema.string(),
|
|
1205
|
+
expectedHash: tool11.schema.string().optional(),
|
|
1206
|
+
replacementContent: tool11.schema.string()
|
|
1312
1207
|
},
|
|
1313
1208
|
async execute(args, context) {
|
|
1314
1209
|
const fullPath = args.filePath.startsWith("/") ? args.filePath : `${context.directory}/${args.filePath}`;
|
|
@@ -1334,13 +1229,13 @@ var hashEditTool = tool12({
|
|
|
1334
1229
|
});
|
|
1335
1230
|
|
|
1336
1231
|
// src/tools/council.ts
|
|
1337
|
-
import { tool as
|
|
1232
|
+
import { tool as tool12 } from "@opencode-ai/plugin";
|
|
1338
1233
|
function createCouncilTool(client) {
|
|
1339
|
-
return
|
|
1234
|
+
return tool12({
|
|
1340
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.",
|
|
1341
1236
|
args: {
|
|
1342
|
-
task:
|
|
1343
|
-
agents:
|
|
1237
|
+
task: tool12.schema.string(),
|
|
1238
|
+
agents: tool12.schema.array(tool12.schema.string()).optional()
|
|
1344
1239
|
},
|
|
1345
1240
|
async execute(args, context) {
|
|
1346
1241
|
const agents = args.agents || ["architect", "reviewer", "coder"];
|
|
@@ -1396,14 +1291,14 @@ Please synthesize these results. Identify areas of agreement, resolve conflicts,
|
|
|
1396
1291
|
}
|
|
1397
1292
|
|
|
1398
1293
|
// src/tools/context-generator.ts
|
|
1399
|
-
import { tool as
|
|
1294
|
+
import { tool as tool13 } from "@opencode-ai/plugin";
|
|
1400
1295
|
import { writeFileSync as writeFileSync11, existsSync as existsSync10, readFileSync as readFileSync11, readdirSync as readdirSync2, statSync } from "fs";
|
|
1401
1296
|
import { join as join10 } from "path";
|
|
1402
|
-
var contextGeneratorTool =
|
|
1297
|
+
var contextGeneratorTool = tool13({
|
|
1403
1298
|
description: "Auto-generate or update hierarchical context files (AGENTS.md, CLAUDE.md) throughout the project. These files provide critical grounding for AI agents.",
|
|
1404
1299
|
args: {
|
|
1405
|
-
targetDir:
|
|
1406
|
-
force:
|
|
1300
|
+
targetDir: tool13.schema.string().optional(),
|
|
1301
|
+
force: tool13.schema.boolean().optional()
|
|
1407
1302
|
},
|
|
1408
1303
|
async execute(args, context) {
|
|
1409
1304
|
const root = context.directory;
|
|
@@ -1452,18 +1347,18 @@ Generated by FlowDeck Context Generator.
|
|
|
1452
1347
|
});
|
|
1453
1348
|
|
|
1454
1349
|
// src/tools/create-skill.ts
|
|
1455
|
-
import { tool as
|
|
1350
|
+
import { tool as tool14 } from "@opencode-ai/plugin";
|
|
1456
1351
|
import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync12, existsSync as existsSync11 } from "fs";
|
|
1457
1352
|
import { join as join11, dirname as dirname3 } from "path";
|
|
1458
1353
|
import { fileURLToPath } from "url";
|
|
1459
1354
|
var SKILLS_DIR = join11(dirname3(fileURLToPath(import.meta.url)), "..", "skills");
|
|
1460
|
-
var createSkillTool =
|
|
1355
|
+
var createSkillTool = tool14({
|
|
1461
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.",
|
|
1462
1357
|
args: {
|
|
1463
|
-
name:
|
|
1464
|
-
description:
|
|
1465
|
-
content:
|
|
1466
|
-
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']")
|
|
1467
1362
|
},
|
|
1468
1363
|
async execute(args) {
|
|
1469
1364
|
const skillDir = join11(SKILLS_DIR, args.name);
|
|
@@ -1495,7 +1390,7 @@ origin: FlowDeck (self-learned)${tagLine}
|
|
|
1495
1390
|
});
|
|
1496
1391
|
|
|
1497
1392
|
// src/tools/reflect.ts
|
|
1498
|
-
import { tool as
|
|
1393
|
+
import { tool as tool15 } from "@opencode-ai/plugin";
|
|
1499
1394
|
import { existsSync as existsSync12, readFileSync as readFileSync12 } from "fs";
|
|
1500
1395
|
import { join as join12 } from "path";
|
|
1501
1396
|
var MAX_ARTIFACT_BYTES = 4000;
|
|
@@ -1505,10 +1400,10 @@ function tail(text, maxBytes) {
|
|
|
1505
1400
|
return `... (truncated) ...
|
|
1506
1401
|
` + text.slice(-maxBytes);
|
|
1507
1402
|
}
|
|
1508
|
-
var reflectTool =
|
|
1403
|
+
var reflectTool = tool15({
|
|
1509
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.",
|
|
1510
1405
|
args: {
|
|
1511
|
-
scope:
|
|
1406
|
+
scope: tool15.schema.enum(["session", "project"]).optional().describe("'session' (default) uses only recent artifacts; 'project' includes all historical data")
|
|
1512
1407
|
},
|
|
1513
1408
|
async execute(args, context) {
|
|
1514
1409
|
const root = context.directory;
|
|
@@ -1549,14 +1444,399 @@ var reflectTool = tool16({
|
|
|
1549
1444
|
}
|
|
1550
1445
|
});
|
|
1551
1446
|
|
|
1552
|
-
// src/
|
|
1553
|
-
import {
|
|
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";
|
|
1554
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
|
+
|
|
1832
|
+
// src/hooks/guard-rails.ts
|
|
1833
|
+
import { existsSync as existsSync14, readFileSync as readFileSync13 } from "fs";
|
|
1834
|
+
import { join as join14 } from "path";
|
|
1555
1835
|
var PLANNING_DIR2 = ".planning";
|
|
1556
1836
|
var CONFIG_FILE = "config.json";
|
|
1557
1837
|
var STATE_FILE2 = "STATE.md";
|
|
1558
1838
|
function resolveExecutionMode(configPath, trustScore, volatility) {
|
|
1559
|
-
if (
|
|
1839
|
+
if (existsSync14(configPath)) {
|
|
1560
1840
|
try {
|
|
1561
1841
|
const config = JSON.parse(readFileSync13(configPath, "utf-8"));
|
|
1562
1842
|
if (config.execution_mode === "review-only")
|
|
@@ -1609,22 +1889,22 @@ var BUILD_DEPLOY_PATTERNS = [
|
|
|
1609
1889
|
];
|
|
1610
1890
|
async function guardRailsHook(ctx, input, _output) {
|
|
1611
1891
|
const dir = ctx.directory;
|
|
1612
|
-
const planningDirPath =
|
|
1892
|
+
const planningDirPath = join14(dir, PLANNING_DIR2);
|
|
1613
1893
|
const codebaseDirectory = codebaseDir(dir);
|
|
1614
|
-
const configPath =
|
|
1615
|
-
const statePath2 =
|
|
1894
|
+
const configPath = join14(planningDirPath, CONFIG_FILE);
|
|
1895
|
+
const statePath2 = join14(planningDirPath, STATE_FILE2);
|
|
1616
1896
|
const workspaceRoot = findWorkspaceRoot(dir);
|
|
1617
1897
|
if (workspaceRoot && dir !== workspaceRoot) {
|
|
1618
1898
|
const config = getWorkspaceConfig(dir);
|
|
1619
|
-
if (config && config.workspace_mode === "shared" && !
|
|
1899
|
+
if (config && config.workspace_mode === "shared" && !existsSync14(planningDirPath)) {
|
|
1620
1900
|
const msg = `No .planning/ in this sub-repo. Switch to workspace root: cd ${workspaceRoot}`;
|
|
1621
1901
|
throw new Error(`[flowdeck] BLOCK: ${msg}`);
|
|
1622
1902
|
}
|
|
1623
1903
|
}
|
|
1624
1904
|
if (input.tool === "write" || input.tool === "edit") {
|
|
1625
|
-
if (!
|
|
1905
|
+
if (!existsSync14(planningDirPath))
|
|
1626
1906
|
return;
|
|
1627
|
-
if (!
|
|
1907
|
+
if (!existsSync14(codebaseDirectory)) {
|
|
1628
1908
|
throw new Error(`[flowdeck] WARNING: .codebase/ not found. Run /map-codebase to map the codebase.`);
|
|
1629
1909
|
}
|
|
1630
1910
|
const execMode = resolveExecutionMode(configPath, null);
|
|
@@ -1659,7 +1939,7 @@ async function guardRailsHook(ctx, input, _output) {
|
|
|
1659
1939
|
}
|
|
1660
1940
|
}
|
|
1661
1941
|
function effectiveSeverity(configPath, statePath2) {
|
|
1662
|
-
if (
|
|
1942
|
+
if (existsSync14(configPath)) {
|
|
1663
1943
|
try {
|
|
1664
1944
|
const configContent = readFileSync13(configPath, "utf-8");
|
|
1665
1945
|
const config = JSON.parse(configContent);
|
|
@@ -1677,7 +1957,7 @@ function getEffectiveSeverity(configPath, statePath2) {
|
|
|
1677
1957
|
return effectiveSeverity(configPath, statePath2);
|
|
1678
1958
|
}
|
|
1679
1959
|
function getPlanConfirmed(statePath2) {
|
|
1680
|
-
if (!
|
|
1960
|
+
if (!existsSync14(statePath2))
|
|
1681
1961
|
return false;
|
|
1682
1962
|
try {
|
|
1683
1963
|
const content = readFileSync13(statePath2, "utf-8");
|
|
@@ -1688,21 +1968,21 @@ function getPlanConfirmed(statePath2) {
|
|
|
1688
1968
|
}
|
|
1689
1969
|
}
|
|
1690
1970
|
function getWarningMessage(statePath2, planningDir3) {
|
|
1691
|
-
if (!
|
|
1971
|
+
if (!existsSync14(join14(planningDir3, STATE_FILE2))) {
|
|
1692
1972
|
return "No .planning/ found. Run /new-project first.";
|
|
1693
1973
|
}
|
|
1694
1974
|
return "Plan not confirmed. Run /plan and confirm to enable execution.";
|
|
1695
1975
|
}
|
|
1696
1976
|
function getBlockMessage(statePath2, planningDir3) {
|
|
1697
|
-
if (!
|
|
1977
|
+
if (!existsSync14(join14(planningDir3, STATE_FILE2))) {
|
|
1698
1978
|
return "No .planning/ found. Run /new-project first.";
|
|
1699
1979
|
}
|
|
1700
1980
|
return "Plan not confirmed. Run /plan and confirm to enable execution.";
|
|
1701
1981
|
}
|
|
1702
1982
|
|
|
1703
1983
|
// src/hooks/tool-guard.ts
|
|
1704
|
-
import { existsSync as
|
|
1705
|
-
import { join as
|
|
1984
|
+
import { existsSync as existsSync15, readFileSync as readFileSync14 } from "fs";
|
|
1985
|
+
import { join as join15 } from "path";
|
|
1706
1986
|
var BLOCKED_PATTERNS = {
|
|
1707
1987
|
read: [".env", ".pem", ".key", ".secret"],
|
|
1708
1988
|
write: ["node_modules"],
|
|
@@ -1748,8 +2028,8 @@ function isBlocked(tool17, args) {
|
|
|
1748
2028
|
return null;
|
|
1749
2029
|
}
|
|
1750
2030
|
function checkArchConstraint(directory, filePath) {
|
|
1751
|
-
const constraintsPath =
|
|
1752
|
-
if (!
|
|
2031
|
+
const constraintsPath = join15(codebaseDir(directory), "CONSTRAINTS.md");
|
|
2032
|
+
if (!existsSync15(constraintsPath))
|
|
1753
2033
|
return null;
|
|
1754
2034
|
try {
|
|
1755
2035
|
const content = readFileSync14(constraintsPath, "utf-8");
|
|
@@ -1797,18 +2077,18 @@ async function toolGuardHook(ctx, input, output) {
|
|
|
1797
2077
|
}
|
|
1798
2078
|
|
|
1799
2079
|
// src/hooks/session-start.ts
|
|
1800
|
-
import { existsSync as
|
|
2080
|
+
import { existsSync as existsSync16, readFileSync as readFileSync15 } from "fs";
|
|
1801
2081
|
async function sessionStartHook(ctx) {
|
|
1802
2082
|
const planningDir3 = ctx.directory + "/.planning";
|
|
1803
2083
|
const codebaseDirectory = codebaseDir(ctx.directory);
|
|
1804
2084
|
const workspaceRoot = findWorkspaceRoot(ctx.directory);
|
|
1805
2085
|
const config = workspaceRoot ? getWorkspaceConfig(ctx.directory) : null;
|
|
1806
|
-
if (!
|
|
2086
|
+
if (!existsSync16(planningDir3)) {
|
|
1807
2087
|
return {
|
|
1808
2088
|
flowdeck_phase: null,
|
|
1809
2089
|
flowdeck_status: "no_plan",
|
|
1810
2090
|
flowdeck_warning: "Run /new-project or /map-codebase to initialize.",
|
|
1811
|
-
flowdeck_has_codebase:
|
|
2091
|
+
flowdeck_has_codebase: existsSync16(codebaseDirectory),
|
|
1812
2092
|
...workspaceRoot && config?.sub_repos ? {
|
|
1813
2093
|
flowdeck_workspace_root: workspaceRoot,
|
|
1814
2094
|
flowdeck_sub_repos: config.sub_repos,
|
|
@@ -1827,7 +2107,7 @@ async function sessionStartHook(ctx) {
|
|
|
1827
2107
|
flowdeck_status: currentPhase["status"] ?? null,
|
|
1828
2108
|
flowdeck_steps_pending: currentPhase["steps_pending"] ?? null,
|
|
1829
2109
|
flowdeck_last_action: currentPhase["last_action"] ?? null,
|
|
1830
|
-
flowdeck_has_codebase:
|
|
2110
|
+
flowdeck_has_codebase: existsSync16(codebaseDirectory)
|
|
1831
2111
|
};
|
|
1832
2112
|
if (workspaceRoot && config?.sub_repos && config.sub_repos.length > 0) {
|
|
1833
2113
|
result.flowdeck_workspace_root = workspaceRoot;
|
|
@@ -1842,7 +2122,7 @@ async function sessionStartHook(ctx) {
|
|
|
1842
2122
|
flowdeck_phase: null,
|
|
1843
2123
|
flowdeck_status: "error",
|
|
1844
2124
|
flowdeck_warning: "State file unreadable. Continuing without flowdeck context.",
|
|
1845
|
-
flowdeck_has_codebase:
|
|
2125
|
+
flowdeck_has_codebase: existsSync16(codebaseDirectory)
|
|
1846
2126
|
};
|
|
1847
2127
|
if (workspaceRoot && config?.sub_repos && config.sub_repos.length > 0) {
|
|
1848
2128
|
result.flowdeck_workspace_root = workspaceRoot;
|
|
@@ -1908,8 +2188,8 @@ function notifyPermissionNeeded(tool17) {
|
|
|
1908
2188
|
}
|
|
1909
2189
|
|
|
1910
2190
|
// src/hooks/patch-trust.ts
|
|
1911
|
-
import { existsSync as
|
|
1912
|
-
import { join as
|
|
2191
|
+
import { existsSync as existsSync17, readFileSync as readFileSync16 } from "fs";
|
|
2192
|
+
import { join as join16 } from "path";
|
|
1913
2193
|
var HIGH_RISK_KEYWORDS = [
|
|
1914
2194
|
"password",
|
|
1915
2195
|
"secret",
|
|
@@ -1931,8 +2211,8 @@ var HIGH_RISK_KEYWORDS = [
|
|
|
1931
2211
|
"privilege"
|
|
1932
2212
|
];
|
|
1933
2213
|
function loadVolatility(directory) {
|
|
1934
|
-
const p =
|
|
1935
|
-
if (!
|
|
2214
|
+
const p = join16(codebaseDir(directory), "VOLATILITY.json");
|
|
2215
|
+
if (!existsSync17(p))
|
|
1936
2216
|
return {};
|
|
1937
2217
|
try {
|
|
1938
2218
|
const data = JSON.parse(readFileSync16(p, "utf-8"));
|
|
@@ -1945,8 +2225,8 @@ function loadVolatility(directory) {
|
|
|
1945
2225
|
}
|
|
1946
2226
|
}
|
|
1947
2227
|
function loadFailedPaths(directory) {
|
|
1948
|
-
const p =
|
|
1949
|
-
if (!
|
|
2228
|
+
const p = join16(codebaseDir(directory), "FAILURES.json");
|
|
2229
|
+
if (!existsSync17(p))
|
|
1950
2230
|
return [];
|
|
1951
2231
|
try {
|
|
1952
2232
|
const data = JSON.parse(readFileSync16(p, "utf-8"));
|
|
@@ -2006,8 +2286,8 @@ async function patchTrustHook(ctx, input, output) {
|
|
|
2006
2286
|
}
|
|
2007
2287
|
|
|
2008
2288
|
// src/hooks/decision-trace-hook.ts
|
|
2009
|
-
import { existsSync as
|
|
2010
|
-
import { join as
|
|
2289
|
+
import { existsSync as existsSync18, mkdirSync as mkdirSync9, appendFileSync as appendFileSync2 } from "fs";
|
|
2290
|
+
import { join as join17 } from "path";
|
|
2011
2291
|
async function decisionTraceHook(ctx, input, output) {
|
|
2012
2292
|
if (input.tool !== "write" && input.tool !== "edit")
|
|
2013
2293
|
return;
|
|
@@ -2016,8 +2296,8 @@ async function decisionTraceHook(ctx, input, output) {
|
|
|
2016
2296
|
return;
|
|
2017
2297
|
const base = codebaseDir(ctx.directory);
|
|
2018
2298
|
try {
|
|
2019
|
-
if (!
|
|
2020
|
-
|
|
2299
|
+
if (!existsSync18(base))
|
|
2300
|
+
mkdirSync9(base, { recursive: true });
|
|
2021
2301
|
const entry = {
|
|
2022
2302
|
timestamp: new Date().toISOString(),
|
|
2023
2303
|
file_path: filePath,
|
|
@@ -2029,22 +2309,24 @@ async function decisionTraceHook(ctx, input, output) {
|
|
|
2029
2309
|
risk_level: "unknown",
|
|
2030
2310
|
auto_recorded: true
|
|
2031
2311
|
};
|
|
2032
|
-
appendFileSync2(
|
|
2312
|
+
appendFileSync2(join17(base, "DECISIONS.jsonl"), JSON.stringify(entry) + `
|
|
2033
2313
|
`, "utf-8");
|
|
2034
2314
|
} catch {}
|
|
2035
2315
|
}
|
|
2036
2316
|
|
|
2037
2317
|
// src/services/telemetry.ts
|
|
2038
|
-
import { existsSync as
|
|
2039
|
-
import { join as
|
|
2318
|
+
import { existsSync as existsSync19, readFileSync as readFileSync17, appendFileSync as appendFileSync3, mkdirSync as mkdirSync10 } from "fs";
|
|
2319
|
+
import { join as join18 } from "path";
|
|
2040
2320
|
import { randomUUID } from "crypto";
|
|
2041
2321
|
function telemetryPath(dir) {
|
|
2042
|
-
return
|
|
2322
|
+
return join18(codebaseDir(dir), "TELEMETRY.jsonl");
|
|
2043
2323
|
}
|
|
2044
2324
|
function appendEvent(dir, partial) {
|
|
2325
|
+
if (process.env.TELEMETRY_ENABLED !== "true")
|
|
2326
|
+
return null;
|
|
2045
2327
|
const cd = codebaseDir(dir);
|
|
2046
|
-
if (!
|
|
2047
|
-
|
|
2328
|
+
if (!existsSync19(cd))
|
|
2329
|
+
mkdirSync10(cd, { recursive: true });
|
|
2048
2330
|
const event = {
|
|
2049
2331
|
id: randomUUID(),
|
|
2050
2332
|
ts: new Date().toISOString(),
|
|
@@ -2081,8 +2363,8 @@ async function telemetryAfterHook(context, toolInput, _output) {
|
|
|
2081
2363
|
}
|
|
2082
2364
|
|
|
2083
2365
|
// src/services/approval-manager.ts
|
|
2084
|
-
import { existsSync as
|
|
2085
|
-
import { join as
|
|
2366
|
+
import { existsSync as existsSync20, readFileSync as readFileSync18, writeFileSync as writeFileSync13, mkdirSync as mkdirSync11 } from "fs";
|
|
2367
|
+
import { join as join19 } from "path";
|
|
2086
2368
|
var APPROVAL_TTL_MS = 30 * 60 * 1000;
|
|
2087
2369
|
var SENSITIVE_PATTERNS = [
|
|
2088
2370
|
/auth/i,
|
|
@@ -2119,11 +2401,11 @@ function isSensitivePath(filePath) {
|
|
|
2119
2401
|
return SENSITIVE_PATTERNS.some((p) => p.test(filePath));
|
|
2120
2402
|
}
|
|
2121
2403
|
function approvalsPath(dir) {
|
|
2122
|
-
return
|
|
2404
|
+
return join19(codebaseDir(dir), "APPROVALS.json");
|
|
2123
2405
|
}
|
|
2124
2406
|
function loadStore(dir) {
|
|
2125
2407
|
const p = approvalsPath(dir);
|
|
2126
|
-
if (!
|
|
2408
|
+
if (!existsSync20(p))
|
|
2127
2409
|
return { requests: [] };
|
|
2128
2410
|
try {
|
|
2129
2411
|
return JSON.parse(readFileSync18(p, "utf-8"));
|
|
@@ -2218,8 +2500,8 @@ function createContextWindowMonitorHook() {
|
|
|
2218
2500
|
}
|
|
2219
2501
|
|
|
2220
2502
|
// src/hooks/shell-env-hook.ts
|
|
2221
|
-
import { existsSync as
|
|
2222
|
-
import { join as
|
|
2503
|
+
import { existsSync as existsSync21, readFileSync as readFileSync19 } from "fs";
|
|
2504
|
+
import { join as join20 } from "path";
|
|
2223
2505
|
import { createRequire } from "module";
|
|
2224
2506
|
var _version;
|
|
2225
2507
|
function getVersion() {
|
|
@@ -2255,7 +2537,7 @@ var MARKER_TO_LANG = {
|
|
|
2255
2537
|
};
|
|
2256
2538
|
function detectPackageManager(root) {
|
|
2257
2539
|
for (const [lockfile, pm] of Object.entries(LOCKFILE_TO_PM)) {
|
|
2258
|
-
if (
|
|
2540
|
+
if (existsSync21(join20(root, lockfile)))
|
|
2259
2541
|
return pm;
|
|
2260
2542
|
}
|
|
2261
2543
|
return;
|
|
@@ -2264,7 +2546,7 @@ function detectLanguages(root) {
|
|
|
2264
2546
|
const langs = [];
|
|
2265
2547
|
const seen = new Set;
|
|
2266
2548
|
for (const [marker, lang] of Object.entries(MARKER_TO_LANG)) {
|
|
2267
|
-
if (!seen.has(lang) &&
|
|
2549
|
+
if (!seen.has(lang) && existsSync21(join20(root, marker))) {
|
|
2268
2550
|
langs.push(lang);
|
|
2269
2551
|
seen.add(lang);
|
|
2270
2552
|
}
|
|
@@ -2272,8 +2554,8 @@ function detectLanguages(root) {
|
|
|
2272
2554
|
return langs;
|
|
2273
2555
|
}
|
|
2274
2556
|
function readCurrentPhase(root) {
|
|
2275
|
-
const statePath2 =
|
|
2276
|
-
if (!
|
|
2557
|
+
const statePath2 = join20(root, ".planning", "STATE.md");
|
|
2558
|
+
if (!existsSync21(statePath2))
|
|
2277
2559
|
return;
|
|
2278
2560
|
try {
|
|
2279
2561
|
const content = readFileSync19(statePath2, "utf-8");
|
|
@@ -2306,7 +2588,8 @@ function createShellEnvHook(ctx) {
|
|
|
2306
2588
|
// src/hooks/todo-hook.ts
|
|
2307
2589
|
function createTodoHook(client) {
|
|
2308
2590
|
return async (event) => {
|
|
2309
|
-
const
|
|
2591
|
+
const rawTodos = event.todos;
|
|
2592
|
+
const todos = Array.isArray(rawTodos) ? rawTodos : [];
|
|
2310
2593
|
const completed = todos.filter((t) => t.done || t.status === "completed").length;
|
|
2311
2594
|
const total = todos.length;
|
|
2312
2595
|
if (total === 0)
|
|
@@ -2375,8 +2658,8 @@ function createSessionIdleHook(client, tracker) {
|
|
|
2375
2658
|
}
|
|
2376
2659
|
|
|
2377
2660
|
// src/hooks/compaction-hook.ts
|
|
2378
|
-
import { existsSync as
|
|
2379
|
-
import { join as
|
|
2661
|
+
import { existsSync as existsSync22, readFileSync as readFileSync20 } from "fs";
|
|
2662
|
+
import { join as join21 } from "path";
|
|
2380
2663
|
var STRUCTURED_SUMMARY_PROMPT = `
|
|
2381
2664
|
When summarizing this session, you MUST include the following sections:
|
|
2382
2665
|
|
|
@@ -2415,8 +2698,8 @@ For each: agent name, status, description, session_id.
|
|
|
2415
2698
|
**RESUME, DON'T RESTART.** Use session_id to continue existing sessions.
|
|
2416
2699
|
`;
|
|
2417
2700
|
function readPlanningState2(directory) {
|
|
2418
|
-
const statePath2 =
|
|
2419
|
-
if (!
|
|
2701
|
+
const statePath2 = join21(directory, ".planning", "STATE.md");
|
|
2702
|
+
if (!existsSync22(statePath2))
|
|
2420
2703
|
return null;
|
|
2421
2704
|
try {
|
|
2422
2705
|
const content = readFileSync20(statePath2, "utf-8");
|
|
@@ -2453,7 +2736,7 @@ function createCompactionHook(ctx, tracker) {
|
|
|
2453
2736
|
}
|
|
2454
2737
|
|
|
2455
2738
|
// src/hooks/orchestrator-guard-hook.ts
|
|
2456
|
-
var DISABLED = process.env.FLOWDECK_ORCHESTRATOR_GUARD
|
|
2739
|
+
var DISABLED = process.env.FLOWDECK_ORCHESTRATOR_GUARD !== "on";
|
|
2457
2740
|
var BLOCKED_TOOLS = new Set([
|
|
2458
2741
|
"write_file",
|
|
2459
2742
|
"write",
|
|
@@ -2474,7 +2757,6 @@ var BLOCKED_TOOLS = new Set([
|
|
|
2474
2757
|
]);
|
|
2475
2758
|
var ALWAYS_ALLOWED = new Set([
|
|
2476
2759
|
"delegate",
|
|
2477
|
-
"run-parallel",
|
|
2478
2760
|
"run-pipeline",
|
|
2479
2761
|
"council",
|
|
2480
2762
|
"planning-state",
|
|
@@ -2509,7 +2791,7 @@ function blockMessage(toolName) {
|
|
|
2509
2791
|
` + ` delegate({ agent: "@researcher", prompt: "..." }) — research / file analysis
|
|
2510
2792
|
` + ` delegate({ agent: "@tester", prompt: "..." }) — tests / commands
|
|
2511
2793
|
|
|
2512
|
-
` + `To
|
|
2794
|
+
` + `To enable this guard: set FLOWDECK_ORCHESTRATOR_GUARD=on`;
|
|
2513
2795
|
}
|
|
2514
2796
|
|
|
2515
2797
|
class OrchestratorGuard {
|
|
@@ -5513,21 +5795,21 @@ function getAgentConfigs(agentModels) {
|
|
|
5513
5795
|
}
|
|
5514
5796
|
|
|
5515
5797
|
// src/config/loader.ts
|
|
5516
|
-
import { existsSync as
|
|
5517
|
-
import { join as
|
|
5518
|
-
import { homedir } from "os";
|
|
5798
|
+
import { existsSync as existsSync23, readFileSync as readFileSync21 } from "fs";
|
|
5799
|
+
import { join as join22 } from "path";
|
|
5800
|
+
import { homedir as homedir2 } from "os";
|
|
5519
5801
|
var CONFIG_FILENAME = "flowdeck.json";
|
|
5520
5802
|
function getGlobalConfigDir() {
|
|
5521
|
-
return process.env.OPENCODE_CONFIG_DIR || (process.env.XDG_CONFIG_HOME ?
|
|
5803
|
+
return process.env.OPENCODE_CONFIG_DIR || (process.env.XDG_CONFIG_HOME ? join22(process.env.XDG_CONFIG_HOME, "opencode") : join22(homedir2(), ".config", "opencode"));
|
|
5522
5804
|
}
|
|
5523
5805
|
function loadFlowDeckConfig(directory) {
|
|
5524
5806
|
const candidates = [];
|
|
5525
5807
|
if (directory) {
|
|
5526
|
-
candidates.push(
|
|
5808
|
+
candidates.push(join22(directory, ".opencode", CONFIG_FILENAME));
|
|
5527
5809
|
}
|
|
5528
|
-
candidates.push(
|
|
5810
|
+
candidates.push(join22(getGlobalConfigDir(), CONFIG_FILENAME));
|
|
5529
5811
|
for (const configPath of candidates) {
|
|
5530
|
-
if (
|
|
5812
|
+
if (existsSync23(configPath)) {
|
|
5531
5813
|
try {
|
|
5532
5814
|
const content = readFileSync21(configPath, "utf-8");
|
|
5533
5815
|
return JSON.parse(content);
|
|
@@ -5539,10 +5821,29 @@ function loadFlowDeckConfig(directory) {
|
|
|
5539
5821
|
return {};
|
|
5540
5822
|
}
|
|
5541
5823
|
// src/index.ts
|
|
5824
|
+
function loadRulePaths() {
|
|
5825
|
+
const __dir = dirname4(fileURLToPath2(import.meta.url));
|
|
5826
|
+
const rulesDir = join23(__dir, "..", "src", "rules");
|
|
5827
|
+
if (!existsSync24(rulesDir))
|
|
5828
|
+
return [];
|
|
5829
|
+
const paths = [];
|
|
5830
|
+
function walk(dir) {
|
|
5831
|
+
for (const entry of readdirSync3(dir, { withFileTypes: true })) {
|
|
5832
|
+
const full = join23(dir, entry.name);
|
|
5833
|
+
if (entry.isDirectory()) {
|
|
5834
|
+
walk(full);
|
|
5835
|
+
} else if (entry.isFile() && entry.name.endsWith(".md") && entry.name !== "README.md") {
|
|
5836
|
+
paths.push(full);
|
|
5837
|
+
}
|
|
5838
|
+
}
|
|
5839
|
+
}
|
|
5840
|
+
walk(rulesDir);
|
|
5841
|
+
return paths;
|
|
5842
|
+
}
|
|
5542
5843
|
function loadCommands() {
|
|
5543
5844
|
const __dir = dirname4(fileURLToPath2(import.meta.url));
|
|
5544
|
-
const commandsDir =
|
|
5545
|
-
if (!
|
|
5845
|
+
const commandsDir = join23(__dir, "..", "src", "commands");
|
|
5846
|
+
if (!existsSync24(commandsDir))
|
|
5546
5847
|
return {};
|
|
5547
5848
|
const commands = {};
|
|
5548
5849
|
try {
|
|
@@ -5550,7 +5851,7 @@ function loadCommands() {
|
|
|
5550
5851
|
if (!file.endsWith(".md"))
|
|
5551
5852
|
continue;
|
|
5552
5853
|
const name = basename(file, ".md");
|
|
5553
|
-
const raw = readFileSync22(
|
|
5854
|
+
const raw = readFileSync22(join23(commandsDir, file), "utf-8");
|
|
5554
5855
|
let description;
|
|
5555
5856
|
let template = raw;
|
|
5556
5857
|
const fmMatch = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/);
|
|
@@ -5567,7 +5868,6 @@ function loadCommands() {
|
|
|
5567
5868
|
}
|
|
5568
5869
|
var plugin = async (input, _options) => {
|
|
5569
5870
|
const { directory, client, worktree } = input;
|
|
5570
|
-
const runParallelTool = createRunParallelTool(client);
|
|
5571
5871
|
const runPipelineTool = createRunPipelineTool(client);
|
|
5572
5872
|
const delegateTool = createDelegateTool(client);
|
|
5573
5873
|
const councilTool = createCouncilTool(client);
|
|
@@ -5628,8 +5928,8 @@ var plugin = async (input, _options) => {
|
|
|
5628
5928
|
}
|
|
5629
5929
|
}
|
|
5630
5930
|
}
|
|
5631
|
-
const skillsDir =
|
|
5632
|
-
if (
|
|
5931
|
+
const skillsDir = join23(dirname4(fileURLToPath2(import.meta.url)), "..", "src", "skills");
|
|
5932
|
+
if (existsSync24(skillsDir)) {
|
|
5633
5933
|
const cfgAny = cfg;
|
|
5634
5934
|
if (!cfgAny.skills || typeof cfgAny.skills !== "object") {
|
|
5635
5935
|
cfgAny.skills = { paths: [] };
|
|
@@ -5641,12 +5941,23 @@ var plugin = async (input, _options) => {
|
|
|
5641
5941
|
cfgSkills.paths.push(skillsDir);
|
|
5642
5942
|
}
|
|
5643
5943
|
}
|
|
5944
|
+
const rulePaths = loadRulePaths();
|
|
5945
|
+
if (rulePaths.length > 0) {
|
|
5946
|
+
if (!Array.isArray(cfg.instructions)) {
|
|
5947
|
+
cfg.instructions = [];
|
|
5948
|
+
}
|
|
5949
|
+
const existing = new Set(cfg.instructions);
|
|
5950
|
+
for (const p of rulePaths) {
|
|
5951
|
+
if (!existing.has(p)) {
|
|
5952
|
+
cfg.instructions.push(p);
|
|
5953
|
+
}
|
|
5954
|
+
}
|
|
5955
|
+
}
|
|
5644
5956
|
},
|
|
5645
5957
|
tool: {
|
|
5646
5958
|
"planning-state": planningStateTool,
|
|
5647
5959
|
"codebase-state": codebaseStateTool,
|
|
5648
5960
|
"workspace-state": workspaceStateTool,
|
|
5649
|
-
"run-parallel": runParallelTool,
|
|
5650
5961
|
"run-pipeline": runPipelineTool,
|
|
5651
5962
|
delegate: delegateTool,
|
|
5652
5963
|
"repo-memory": repoMemoryTool,
|
|
@@ -5658,7 +5969,8 @@ var plugin = async (input, _options) => {
|
|
|
5658
5969
|
council: councilTool,
|
|
5659
5970
|
"context-generator": contextGeneratorTool,
|
|
5660
5971
|
"create-skill": createSkillTool,
|
|
5661
|
-
reflect: reflectTool
|
|
5972
|
+
reflect: reflectTool,
|
|
5973
|
+
"memory-search": memorySearchTool
|
|
5662
5974
|
},
|
|
5663
5975
|
"shell.env": shellEnvHook,
|
|
5664
5976
|
"todo.updated": todoHook,
|
|
@@ -5670,11 +5982,34 @@ var plugin = async (input, _options) => {
|
|
|
5670
5982
|
},
|
|
5671
5983
|
event: async ({ event }) => {
|
|
5672
5984
|
const type = event?.type ?? "";
|
|
5673
|
-
await contextMonitor.event({ event });
|
|
5674
|
-
orchestratorGuard.onEvent(event);
|
|
5675
5985
|
if (type === "session.created" || type === "session.started") {
|
|
5986
|
+
const sessionId = event?.sessionID ?? event?.sessionId ?? "";
|
|
5987
|
+
if (sessionId) {
|
|
5988
|
+
memoryHook.onSessionCreated(directory, sessionId, event?.prompt);
|
|
5989
|
+
}
|
|
5676
5990
|
await sessionStartHook({ directory });
|
|
5677
|
-
} else if (type === "
|
|
5991
|
+
} else if (type === "message.updated" && event?.event) {
|
|
5992
|
+
const msgEvent = event.event;
|
|
5993
|
+
const sessionId = msgEvent?.sessionID ?? msgEvent?.sessionId ?? "";
|
|
5994
|
+
if (sessionId) {
|
|
5995
|
+
memoryHook.onMessageUpdated(sessionId, msgEvent.role, msgEvent.content, directory);
|
|
5996
|
+
}
|
|
5997
|
+
} else if (type === "session.compacted" && event?.event) {
|
|
5998
|
+
const compactEvent = event.event;
|
|
5999
|
+
const sessionId = compactEvent?.sessionID ?? compactEvent?.sessionId ?? "";
|
|
6000
|
+
if (sessionId) {
|
|
6001
|
+
memoryHook.onSessionCompact(sessionId, compactEvent.summary ?? "");
|
|
6002
|
+
}
|
|
6003
|
+
} else if (type === "session.deleted" && event?.event) {
|
|
6004
|
+
const delEvent = event.event;
|
|
6005
|
+
const sessionId = delEvent?.sessionID ?? delEvent?.sessionId ?? "";
|
|
6006
|
+
if (sessionId) {
|
|
6007
|
+
memoryHook.clearSession(sessionId);
|
|
6008
|
+
}
|
|
6009
|
+
}
|
|
6010
|
+
await contextMonitor.event({ event });
|
|
6011
|
+
orchestratorGuard.onEvent(event);
|
|
6012
|
+
if (type === "session.idle") {
|
|
5678
6013
|
await sessionIdleHook();
|
|
5679
6014
|
await autoLearnHook();
|
|
5680
6015
|
}
|
|
@@ -5690,6 +6025,10 @@ var plugin = async (input, _options) => {
|
|
|
5690
6025
|
},
|
|
5691
6026
|
"tool.execute.after": async (toolInput, toolOutput) => {
|
|
5692
6027
|
await telemetryAfterHook({ directory }, toolInput, toolOutput);
|
|
6028
|
+
const sessionId = toolInput?.sessionID ?? toolInput?.sessionId ?? "";
|
|
6029
|
+
if (sessionId && toolInput?.tool) {
|
|
6030
|
+
memoryHook.onToolExecuted(sessionId, toolInput.tool, toolInput, toolOutput?.output ?? null, directory);
|
|
6031
|
+
}
|
|
5693
6032
|
await contextMonitor["tool.execute.after"](toolInput, toolOutput);
|
|
5694
6033
|
}
|
|
5695
6034
|
};
|