@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.
Files changed (82) hide show
  1. package/README.md +24 -41
  2. package/dist/hooks/approval-hook.d.ts +6 -0
  3. package/dist/hooks/approval-hook.d.ts.map +1 -1
  4. package/dist/hooks/guard-rails.d.ts +0 -8
  5. package/dist/hooks/guard-rails.d.ts.map +1 -1
  6. package/dist/hooks/memory-hook.d.ts +21 -0
  7. package/dist/hooks/memory-hook.d.ts.map +1 -0
  8. package/dist/hooks/orchestrator-guard-hook.d.ts.map +1 -1
  9. package/dist/hooks/patch-trust.d.ts.map +1 -1
  10. package/dist/hooks/todo-hook.d.ts +1 -7
  11. package/dist/hooks/todo-hook.d.ts.map +1 -1
  12. package/dist/hooks/tool-guard.d.ts +1 -0
  13. package/dist/hooks/tool-guard.d.ts.map +1 -1
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +728 -428
  16. package/dist/services/memory-store.d.ts +40 -0
  17. package/dist/services/memory-store.d.ts.map +1 -0
  18. package/dist/services/policy-compiler.d.ts.map +1 -1
  19. package/dist/tools/memory-search.d.ts +3 -0
  20. package/dist/tools/memory-search.d.ts.map +1 -0
  21. package/docs/commands/fd-doctor.md +21 -0
  22. package/docs/commands/fd-quick.md +33 -0
  23. package/docs/commands/fd-reflect.md +23 -0
  24. package/docs/commands/fd-status.md +31 -0
  25. package/docs/commands/fd-translate-intent.md +17 -0
  26. package/docs/commands.md +209 -271
  27. package/docs/configuration.md +5 -2
  28. package/docs/index.md +22 -28
  29. package/docs/memory.md +69 -0
  30. package/docs/quick-start.md +1 -1
  31. package/package.json +1 -1
  32. package/src/commands/fd-deploy-check.md +131 -11
  33. package/src/commands/fd-new-project.md +14 -1
  34. package/src/commands/fd-quick.md +60 -0
  35. package/src/commands/fd-reflect.md +41 -2
  36. package/src/commands/fd-status.md +84 -0
  37. package/src/rules/README.md +8 -7
  38. package/src/skills/agent-harness-construction/SKILL.md +227 -0
  39. package/src/skills/api-design/SKILL.md +5 -0
  40. package/src/skills/backend-patterns/SKILL.md +105 -0
  41. package/src/skills/clean-architecture/SKILL.md +85 -0
  42. package/src/skills/cqrs/SKILL.md +230 -0
  43. package/src/skills/ddd-architecture/SKILL.md +104 -0
  44. package/src/skills/django-patterns/SKILL.md +304 -0
  45. package/src/skills/django-tdd/SKILL.md +297 -0
  46. package/src/skills/event-driven-architecture/SKILL.md +152 -0
  47. package/src/skills/frontend-pattern/SKILL.md +159 -0
  48. package/src/skills/hexagonal-architecture/SKILL.md +80 -0
  49. package/src/skills/layered-architecture/SKILL.md +64 -0
  50. package/src/skills/postgres-patterns/SKILL.md +74 -0
  51. package/src/skills/python-patterns/SKILL.md +5 -0
  52. package/src/skills/saga-architecture/SKILL.md +113 -0
  53. package/dist/tools/run-parallel.d.ts +0 -4
  54. package/dist/tools/run-parallel.d.ts.map +0 -1
  55. package/docs/command-migration.md +0 -175
  56. package/docs/commands/fd-analyze-change.md +0 -107
  57. package/docs/commands/fd-dashboard.md +0 -11
  58. package/docs/commands/fd-evaluate-risk.md +0 -134
  59. package/docs/commands/fd-guarded-edit.md +0 -105
  60. package/docs/commands/fd-progress.md +0 -11
  61. package/docs/commands/fd-review-code.md +0 -29
  62. package/docs/commands/fd-roadmap.md +0 -10
  63. package/docs/commands/fd-settings.md +0 -10
  64. package/docs/parallel-execution.md +0 -255
  65. package/src/commands/fd-analyze-change.md +0 -57
  66. package/src/commands/fd-approve.md +0 -64
  67. package/src/commands/fd-blast-radius.md +0 -49
  68. package/src/commands/fd-dashboard.md +0 -57
  69. package/src/commands/fd-evaluate-risk.md +0 -62
  70. package/src/commands/fd-guarded-edit.md +0 -69
  71. package/src/commands/fd-impact-radar.md +0 -51
  72. package/src/commands/fd-learn.md +0 -36
  73. package/src/commands/fd-progress.md +0 -50
  74. package/src/commands/fd-regression-predict.md +0 -57
  75. package/src/commands/fd-review-code.md +0 -96
  76. package/src/commands/fd-review-route.md +0 -54
  77. package/src/commands/fd-roadmap.md +0 -46
  78. package/src/commands/fd-settings.md +0 -57
  79. package/src/commands/fd-test-gap.md +0 -54
  80. package/src/commands/fd-volatility-map.md +0 -64
  81. package/src/commands/fd-workspace-status.md +0 -34
  82. 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 existsSync23 } from "fs";
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-parallel.ts
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 tool5({
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: tool5.schema.array(tool5.schema.object({
697
- agent: tool5.schema.string(),
698
- prompt: tool5.schema.string()
520
+ steps: tool4.schema.array(tool4.schema.object({
521
+ agent: tool4.schema.string(),
522
+ prompt: tool4.schema.string()
699
523
  })),
700
- initial_context: tool5.schema.string().optional(),
701
- abort_on_failure: tool5.schema.boolean().optional().default(true)
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 = extractText2(promptRes.data?.parts ?? []);
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 tool6 } from "@opencode-ai/plugin";
792
- function extractText3(parts) {
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 tool6({
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: tool6.schema.string(),
801
- prompt: tool6.schema.string(),
802
- context: tool6.schema.string().optional()
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 = extractText3(promptRes.data?.parts ?? []);
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 tool7 } from "@opencode-ai/plugin";
872
- import { readFileSync as readFileSync6, writeFileSync as writeFileSync6, existsSync as existsSync6, mkdirSync as mkdirSync3 } from "fs";
873
- import { join as join7 } from "path";
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 join7(codebaseDir(directory), MEMORY_FILE);
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 (!existsSync6(p))
707
+ if (!existsSync5(p))
884
708
  return emptyMemory();
885
709
  try {
886
- return JSON.parse(readFileSync6(p, "utf-8"));
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 (!existsSync6(base))
894
- mkdirSync3(base, { recursive: true });
717
+ if (!existsSync5(base))
718
+ mkdirSync2(base, { recursive: true });
895
719
  memory.last_updated = new Date().toISOString();
896
- writeFileSync6(memoryPath(directory), JSON.stringify(memory, null, 2), "utf-8");
720
+ writeFileSync5(memoryPath(directory), JSON.stringify(memory, null, 2), "utf-8");
897
721
  }
898
- var repoMemoryTool = tool7({
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: tool7.schema.enum(["read", "write_node", "query", "delete_node"]),
902
- node_id: tool7.schema.string().optional(),
903
- node: tool7.schema.object({
904
- type: tool7.schema.enum(["module", "service", "api", "schema", "config"]),
905
- path: tool7.schema.string(),
906
- owner: tool7.schema.string().optional(),
907
- tags: tool7.schema.array(tool7.schema.string()),
908
- dependencies: tool7.schema.array(tool7.schema.string()),
909
- dependents: tool7.schema.array(tool7.schema.string()),
910
- bug_history: tool7.schema.array(tool7.schema.string()),
911
- conventions: tool7.schema.array(tool7.schema.string())
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: tool7.schema.object({
914
- type: tool7.schema.enum(["module", "service", "api", "schema", "config"]).optional(),
915
- owner: tool7.schema.string().optional(),
916
- tag: tool7.schema.string().optional(),
917
- path_prefix: tool7.schema.string().optional()
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 tool8 } from "@opencode-ai/plugin";
973
- import { readFileSync as readFileSync7, writeFileSync as writeFileSync7, existsSync as existsSync7, mkdirSync as mkdirSync4 } from "fs";
974
- import { join as join8 } from "path";
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 join8(codebaseDir(directory), FAILURES_FILE);
801
+ return join6(codebaseDir(directory), FAILURES_FILE);
978
802
  }
979
803
  function readStore(directory) {
980
804
  const p = failuresPath(directory);
981
- if (!existsSync7(p))
805
+ if (!existsSync6(p))
982
806
  return { version: "1.0", last_updated: new Date().toISOString(), entries: [] };
983
807
  try {
984
- return JSON.parse(readFileSync7(p, "utf-8"));
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 (!existsSync7(base))
992
- mkdirSync4(base, { recursive: true });
815
+ if (!existsSync6(base))
816
+ mkdirSync3(base, { recursive: true });
993
817
  store.last_updated = new Date().toISOString();
994
- writeFileSync7(failuresPath(directory), JSON.stringify(store, null, 2), "utf-8");
818
+ writeFileSync6(failuresPath(directory), JSON.stringify(store, null, 2), "utf-8");
995
819
  }
996
- var failureReplayTool = tool8({
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: tool8.schema.enum(["record", "query", "list", "mark_resolved"]),
1000
- entry: tool8.schema.object({
1001
- id: tool8.schema.string(),
1002
- type: tool8.schema.enum(["reverted_commit", "failed_deployment", "flaky_test", "bug_fix", "build_failure"]),
1003
- description: tool8.schema.string(),
1004
- affected_paths: tool8.schema.array(tool8.schema.string()),
1005
- root_cause: tool8.schema.string().optional(),
1006
- fix_applied: tool8.schema.string().optional(),
1007
- tags: tool8.schema.array(tool8.schema.string())
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: tool8.schema.object({
1010
- type: tool8.schema.enum(["reverted_commit", "failed_deployment", "flaky_test", "bug_fix", "build_failure"]).optional(),
1011
- path_prefix: tool8.schema.string().optional(),
1012
- tag: tool8.schema.string().optional(),
1013
- limit: tool8.schema.number().optional()
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: tool8.schema.string().optional()
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 tool9 } from "@opencode-ai/plugin";
1078
- import { readFileSync as readFileSync8, existsSync as existsSync8, mkdirSync as mkdirSync5, appendFileSync as appendFileSync2 } from "fs";
1079
- import { join as join9 } from "path";
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 join9(codebaseDir(directory), DECISIONS_FILE);
906
+ return join7(codebaseDir(directory), DECISIONS_FILE);
1083
907
  }
1084
908
  function readDecisions(directory) {
1085
909
  const p = decisionsPath(directory);
1086
- if (!existsSync8(p))
910
+ if (!existsSync7(p))
1087
911
  return [];
1088
- return readFileSync8(p, "utf-8").split(`
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 = tool9({
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: tool9.schema.enum(["record", "query", "get_for_file"]),
1101
- entry: tool9.schema.object({
1102
- id: tool9.schema.string(),
1103
- file_path: tool9.schema.string(),
1104
- change_type: tool9.schema.enum(["create", "edit", "delete", "refactor"]),
1105
- rationale: tool9.schema.string(),
1106
- evidence: tool9.schema.array(tool9.schema.string()),
1107
- assumptions: tool9.schema.array(tool9.schema.string()),
1108
- alternatives_considered: tool9.schema.array(tool9.schema.string()),
1109
- risk_level: tool9.schema.enum(["low", "medium", "high"]),
1110
- agent: tool9.schema.string().optional(),
1111
- session_id: tool9.schema.string().optional()
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: tool9.schema.object({
1114
- file_path: tool9.schema.string().optional(),
1115
- change_type: tool9.schema.enum(["create", "edit", "delete", "refactor"]).optional(),
1116
- risk_level: tool9.schema.enum(["low", "medium", "high"]).optional(),
1117
- limit: tool9.schema.number().optional()
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: tool9.schema.string().optional()
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 (!existsSync8(base))
1129
- mkdirSync5(base, { recursive: true });
952
+ if (!existsSync7(base))
953
+ mkdirSync4(base, { recursive: true });
1130
954
  const entry = { ...args.entry, timestamp: new Date().toISOString() };
1131
- appendFileSync2(decisionsPath(dir), JSON.stringify(entry) + `
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 tool10 } from "@opencode-ai/plugin";
1163
- import { readFileSync as readFileSync9, writeFileSync as writeFileSync9, existsSync as existsSync9, mkdirSync as mkdirSync6 } from "fs";
1164
- import { join as join10 } from "path";
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 join10(codebaseDir(directory), VOLATILITY_FILE);
991
+ return join8(codebaseDir(directory), VOLATILITY_FILE);
1168
992
  }
1169
993
  function readStore2(directory) {
1170
994
  const p = volatilityPath(directory);
1171
- if (!existsSync9(p))
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(readFileSync9(p, "utf-8"));
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 (!existsSync9(base))
1182
- mkdirSync6(base, { recursive: true });
1005
+ if (!existsSync8(base))
1006
+ mkdirSync5(base, { recursive: true });
1183
1007
  store.last_updated = new Date().toISOString();
1184
- writeFileSync9(volatilityPath(directory), JSON.stringify(store, null, 2), "utf-8");
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 = tool10({
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: tool10.schema.enum(["read", "write", "query_hotspots", "update_entry"]),
1200
- entries: tool10.schema.array(tool10.schema.object({
1201
- path: tool10.schema.string(),
1202
- churn_score: tool10.schema.number(),
1203
- hotfix_count: tool10.schema.number(),
1204
- todo_count: tool10.schema.number(),
1205
- last_breakage: tool10.schema.string().optional(),
1206
- notes: tool10.schema.array(tool10.schema.string())
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: tool10.schema.object({
1209
- path: tool10.schema.string(),
1210
- churn_score: tool10.schema.number(),
1211
- hotfix_count: tool10.schema.number(),
1212
- todo_count: tool10.schema.number(),
1213
- last_breakage: tool10.schema.string().optional(),
1214
- notes: tool10.schema.array(tool10.schema.string())
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: tool10.schema.enum(["stable", "moderate", "volatile", "critical"]).optional(),
1217
- path_prefix: tool10.schema.string().optional(),
1218
- limit: tool10.schema.number().optional()
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 tool11 } from "@opencode-ai/plugin";
1271
- import { readFileSync as readFileSync10, writeFileSync as writeFileSync10, existsSync as existsSync10, mkdirSync as mkdirSync7 } from "fs";
1272
- import { join as join11 } from "path";
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 join11(codebaseDir(directory), POLICIES_FILE);
1099
+ return join9(codebaseDir(directory), POLICIES_FILE);
1276
1100
  }
1277
1101
  function readStore3(directory) {
1278
1102
  const p = policiesPath(directory);
1279
- if (!existsSync10(p))
1103
+ if (!existsSync9(p))
1280
1104
  return { version: "1.0", last_updated: new Date().toISOString(), policies: [] };
1281
1105
  try {
1282
- return JSON.parse(readFileSync10(p, "utf-8"));
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 (!existsSync10(base))
1290
- mkdirSync7(base, { recursive: true });
1113
+ if (!existsSync9(base))
1114
+ mkdirSync6(base, { recursive: true });
1291
1115
  store.last_updated = new Date().toISOString();
1292
- writeFileSync10(policiesPath(directory), JSON.stringify(store, null, 2), "utf-8");
1116
+ writeFileSync9(policiesPath(directory), JSON.stringify(store, null, 2), "utf-8");
1293
1117
  }
1294
- var policyEngineTool = tool11({
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: tool11.schema.enum(["list", "add", "record_violation", "toggle", "query"]),
1298
- policy: tool11.schema.object({
1299
- id: tool11.schema.string(),
1300
- name: tool11.schema.string(),
1301
- trigger: tool11.schema.string(),
1302
- rule: tool11.schema.string(),
1303
- source: tool11.schema.enum(["manual", "learned"]),
1304
- failure_count: tool11.schema.number()
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: tool11.schema.string().optional(),
1307
- active: tool11.schema.boolean().optional(),
1308
- query: tool11.schema.object({
1309
- source: tool11.schema.enum(["manual", "learned"]).optional(),
1310
- active_only: tool11.schema.boolean().optional(),
1311
- trigger_contains: tool11.schema.string().optional()
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 tool12 } from "@opencode-ai/plugin";
1374
- import { readFileSync as readFileSync11, writeFileSync as writeFileSync11 } from "fs";
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 = tool12({
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: tool12.schema.string(),
1380
- targetContent: tool12.schema.string(),
1381
- expectedHash: tool12.schema.string().optional(),
1382
- replacementContent: tool12.schema.string()
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 = readFileSync11(fullPath, "utf-8");
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
- writeFileSync11(fullPath, newContent, "utf-8");
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 tool13 } from "@opencode-ai/plugin";
1232
+ import { tool as tool12 } from "@opencode-ai/plugin";
1409
1233
  function createCouncilTool(client) {
1410
- return tool13({
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: tool13.schema.string(),
1414
- agents: tool13.schema.array(tool13.schema.string()).optional()
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 tool14 } from "@opencode-ai/plugin";
1471
- import { writeFileSync as writeFileSync12, existsSync as existsSync11, readFileSync as readFileSync12, readdirSync as readdirSync2, statSync } from "fs";
1472
- import { join as join12 } from "path";
1473
- var contextGeneratorTool = tool14({
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: tool14.schema.string().optional(),
1477
- force: tool14.schema.boolean().optional()
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 ? join12(root, args.targetDir) : root;
1482
- if (!existsSync11(target)) {
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 = join12(target, "AGENTS.md");
1486
- if (existsSync11(agentsMdPath) && !args.force) {
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 = join12(root, "package.json");
1313
+ const pkgPath = join10(root, "package.json");
1490
1314
  let projectName = "Project";
1491
1315
  let techStack = "Unknown";
1492
- if (existsSync11(pkgPath)) {
1316
+ if (existsSync10(pkgPath)) {
1493
1317
  try {
1494
- const pkg = JSON.parse(readFileSync12(pkgPath, "utf-8"));
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(join12(target, f));
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
- writeFileSync12(agentsMdPath, content, "utf-8");
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 tool15 } from "@opencode-ai/plugin";
1527
- import { mkdirSync as mkdirSync8, writeFileSync as writeFileSync13, existsSync as existsSync12 } from "fs";
1528
- import { join as join13, dirname as dirname3 } from "path";
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 = join13(dirname3(fileURLToPath(import.meta.url)), "..", "skills");
1531
- var createSkillTool = tool15({
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: tool15.schema.string().describe("Unique kebab-case skill name, e.g. 'api-rate-limiting'"),
1535
- description: tool15.schema.string().describe("One-sentence description of what this skill does"),
1536
- content: tool15.schema.string().describe("Full skill body in Markdown. Must include: ## When to Activate, ## Steps, and ## Examples sections."),
1537
- tags: tool15.schema.array(tool15.schema.string()).optional().describe("Optional tags for categorisation, e.g. ['performance', 'typescript']")
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 = join13(SKILLS_DIR, args.name);
1541
- const skillFile = join13(skillDir, "SKILL.md");
1542
- if (existsSync12(skillFile)) {
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
- mkdirSync8(skillDir, { recursive: true });
1558
- writeFileSync13(skillFile, fullContent, "utf-8");
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 tool16 } from "@opencode-ai/plugin";
1570
- import { existsSync as existsSync13, readFileSync as readFileSync13 } from "fs";
1571
- import { join as join14 } from "path";
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 = tool16({
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: tool16.schema.enum(["session", "project"]).optional().describe("'session' (default) uses only recent artifacts; 'project' includes all historical data")
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 = join14(root, rel);
1601
- if (!existsSync13(full))
1424
+ const full = join12(root, rel);
1425
+ if (!existsSync12(full))
1602
1426
  continue;
1603
1427
  try {
1604
- const raw = readFileSync13(full, "utf-8").trim();
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 readFileSync14 } from "fs";
1625
- import { join as join15 } from "path";
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(readFileSync14(configPath, "utf-8"));
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 = join15(dir, PLANNING_DIR2);
1895
+ const planningDirPath = join14(dir, PLANNING_DIR2);
1684
1896
  const codebaseDirectory = codebaseDir(dir);
1685
- const configPath = join15(planningDirPath, CONFIG_FILE);
1686
- const statePath2 = join15(planningDirPath, STATE_FILE2);
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(statePath2, planningDirPath);
1924
+ const warning = getWarningMessage(planningDirPath);
1713
1925
  throw new Error(`[flowdeck] WARNING: ${warning}`);
1714
- return;
1715
1926
  }
1716
- const blockMessage = getBlockMessage(statePath2, planningDirPath);
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 = readFileSync14(configPath, "utf-8");
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 = readFileSync14(statePath2, "utf-8");
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(statePath2, planningDir3) {
1762
- if (!existsSync14(join15(planningDir3, STATE_FILE2))) {
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(statePath2, planningDir3) {
1768
- if (!existsSync14(join15(planningDir3, STATE_FILE2))) {
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 readFileSync15 } from "fs";
1776
- import { join as join16 } from "path";
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 = join16(codebaseDir(directory), "CONSTRAINTS.md");
2033
+ const constraintsPath = join15(codebaseDir(directory), "CONSTRAINTS.md");
1823
2034
  if (!existsSync15(constraintsPath))
1824
2035
  return null;
1825
2036
  try {
1826
- const content = readFileSync15(constraintsPath, "utf-8");
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 readFileSync16 } from "fs";
2084
+ import { existsSync as existsSync16, readFileSync as readFileSync15 } from "fs";
1872
2085
  async function sessionStartHook(ctx) {
1873
- const planningDir3 = ctx.directory + "/.planning";
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(planningDir3)) {
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 = readFileSync16(stateFilePath, "utf-8");
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 readFileSync17 } from "fs";
1983
- import { join as join17 } from "path";
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 = join17(codebaseDir(directory), "VOLATILITY.json");
2218
+ const p = join16(codebaseDir(directory), "VOLATILITY.json");
2006
2219
  if (!existsSync17(p))
2007
2220
  return {};
2008
2221
  try {
2009
- const data = JSON.parse(readFileSync17(p, "utf-8"));
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 = join17(codebaseDir(directory), "FAILURES.json");
2232
+ const p = join16(codebaseDir(directory), "FAILURES.json");
2020
2233
  if (!existsSync17(p))
2021
2234
  return [];
2022
2235
  try {
2023
- const data = JSON.parse(readFileSync17(p, "utf-8"));
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 appendFileSync3 } from "fs";
2081
- import { join as join18 } from "path";
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
- appendFileSync3(join18(base, "DECISIONS.jsonl"), JSON.stringify(entry) + `
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 existsSync19, readFileSync as readFileSync18, writeFileSync as writeFileSync14, mkdirSync as mkdirSync10 } from "fs";
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 (!existsSync19(p))
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 existsSync20, readFileSync as readFileSync19 } from "fs";
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 (existsSync20(join20(root, lockfile)))
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) && existsSync20(join20(root, marker))) {
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 (!existsSync20(statePath2))
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 todos = event.todos ?? [];
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 existsSync21, readFileSync as readFileSync20 } from "fs";
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 (!existsSync21(statePath2))
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 existsSync22, readFileSync as readFileSync21 } from "fs";
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(homedir(), ".config", "opencode"));
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 (existsSync22(configPath)) {
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 (!existsSync23(commandsDir))
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 (existsSync23(skillsDir)) {
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 === "session.idle") {
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
  };