@dv.nghiem/flowdeck 0.3.2 → 0.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README.md +18 -13
  2. package/dist/hooks/orchestrator-guard-hook.d.ts +4 -1
  3. package/dist/hooks/orchestrator-guard-hook.d.ts.map +1 -1
  4. package/dist/hooks/session-idle-hook.d.ts.map +1 -1
  5. package/dist/hooks/telemetry-hook.d.ts +14 -1
  6. package/dist/hooks/telemetry-hook.d.ts.map +1 -1
  7. package/dist/hooks/telemetry-hook.test.d.ts +2 -0
  8. package/dist/hooks/telemetry-hook.test.d.ts.map +1 -0
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +583 -240
  11. package/dist/tools/council.d.ts.map +1 -1
  12. package/dist/tools/delegate.d.ts.map +1 -1
  13. package/dist/tools/dispatch-routing.d.ts +6 -0
  14. package/dist/tools/dispatch-routing.d.ts.map +1 -0
  15. package/dist/tools/memory-status.d.ts +3 -0
  16. package/dist/tools/memory-status.d.ts.map +1 -0
  17. package/dist/tools/run-pipeline.d.ts.map +1 -1
  18. package/docs/commands.md +102 -9
  19. package/docs/installation.md +6 -17
  20. package/docs/intelligence.md +18 -33
  21. package/docs/optimization-baseline.md +21 -0
  22. package/docs/quick-start.md +44 -23
  23. package/docs/rules.md +9 -36
  24. package/docs/workflows.md +18 -17
  25. package/package.json +4 -2
  26. package/src/commands/fd-execute.md +192 -0
  27. package/src/commands/fd-new-feature.md +44 -157
  28. package/src/commands/fd-new-project.md +1 -2
  29. package/src/commands/fd-plan.md +1 -1
  30. package/src/commands/fd-suggest.md +84 -0
  31. package/src/commands/fd-verify.md +126 -0
  32. package/src/rules/README.md +10 -0
  33. package/src/rules/common/agent-orchestration.md +5 -5
  34. package/src/rules/common/coding-style.md +17 -0
  35. package/src/rules/typescript/patterns.md +1 -1
  36. package/src/skills/backend-patterns/SKILL.md +6 -0
  37. package/src/skills/clean-architecture/SKILL.md +6 -0
  38. package/src/skills/cqrs/SKILL.md +6 -0
  39. package/src/skills/ddd-architecture/SKILL.md +6 -0
  40. package/src/skills/event-driven-architecture/SKILL.md +6 -0
  41. package/src/skills/hexagonal-architecture/SKILL.md +6 -0
  42. package/src/skills/layered-architecture/SKILL.md +6 -0
  43. package/src/skills/postgres-patterns/SKILL.md +6 -0
  44. package/src/skills/saga-architecture/SKILL.md +6 -0
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // src/index.ts
2
- import { readdirSync as readdirSync3, readFileSync as readFileSync22, existsSync as existsSync24 } from "fs";
3
- import { join as join23, basename } from "path";
2
+ import { readdirSync as readdirSync3, readFileSync as readFileSync24, existsSync as existsSync28 } from "fs";
3
+ import { join as join27, basename } from "path";
4
4
  import { dirname as dirname4 } from "path";
5
5
  import { fileURLToPath as fileURLToPath2 } from "url";
6
6
 
@@ -509,6 +509,154 @@ var workspaceStateTool = tool3({
509
509
 
510
510
  // src/tools/run-pipeline.ts
511
511
  import { tool as tool4 } from "@opencode-ai/plugin";
512
+
513
+ // src/services/agent-performance.ts
514
+ import { existsSync as existsSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync5, mkdirSync as mkdirSync2 } from "fs";
515
+ import { join as join5 } from "path";
516
+ function perfPath(dir) {
517
+ return join5(codebaseDir(dir), "AGENT_PERF.json");
518
+ }
519
+ function loadStore(dir) {
520
+ const p = perfPath(dir);
521
+ if (!existsSync5(p))
522
+ return { entries: [], updated_at: new Date().toISOString() };
523
+ try {
524
+ return JSON.parse(readFileSync5(p, "utf-8"));
525
+ } catch {
526
+ return { entries: [], updated_at: new Date().toISOString() };
527
+ }
528
+ }
529
+ function saveStore(dir, store) {
530
+ const cd = codebaseDir(dir);
531
+ if (!existsSync5(cd))
532
+ mkdirSync2(cd, { recursive: true });
533
+ writeFileSync5(perfPath(dir), JSON.stringify(store, null, 2), "utf-8");
534
+ }
535
+ function makeKey(agent, model, task_type) {
536
+ return `${agent}::${model}::${task_type}`;
537
+ }
538
+ function recordRun(dir, agent, model, task_type, success, duration_ms, cost = 0) {
539
+ const store = loadStore(dir);
540
+ const key = makeKey(agent, model, task_type);
541
+ const existing = store.entries.find((e) => makeKey(e.agent, e.model, e.task_type) === key);
542
+ if (existing) {
543
+ existing.runs++;
544
+ if (success)
545
+ existing.successes++;
546
+ else
547
+ existing.failures++;
548
+ existing.total_duration_ms += duration_ms;
549
+ existing.total_cost += cost;
550
+ existing.last_run = new Date().toISOString();
551
+ existing.last_status = success ? "success" : "failure";
552
+ } else {
553
+ store.entries.push({
554
+ agent,
555
+ model,
556
+ task_type,
557
+ runs: 1,
558
+ successes: success ? 1 : 0,
559
+ failures: success ? 0 : 1,
560
+ total_duration_ms: duration_ms,
561
+ total_cost: cost,
562
+ last_run: new Date().toISOString(),
563
+ last_status: success ? "success" : "failure"
564
+ });
565
+ }
566
+ store.updated_at = new Date().toISOString();
567
+ saveStore(dir, store);
568
+ }
569
+
570
+ // src/services/model-router.ts
571
+ import { existsSync as existsSync6, readFileSync as readFileSync6 } from "fs";
572
+ import { join as join6 } from "path";
573
+ var DEFAULT_ROUTING = {
574
+ planning: { primary: "claude-sonnet-4-5", temperature: 0.3, reasoning_effort: "medium" },
575
+ implementation: { primary: "claude-opus-4-5", fallback: "claude-sonnet-4-5", high_risk_override: "claude-opus-4-5", temperature: 0.2, reasoning_effort: "high" },
576
+ debugging: { primary: "claude-sonnet-4-5", high_risk_override: "claude-opus-4-5", temperature: 0.2, reasoning_effort: "high" },
577
+ review: { primary: "gemini-2.5-flash", fallback: "claude-haiku-4-5", temperature: 0.1, reasoning_effort: "medium" },
578
+ testing: { primary: "claude-haiku-4-5", fallback: "gemini-2.5-flash", temperature: 0.1, reasoning_effort: "low" },
579
+ documentation: { primary: "claude-sonnet-4-5", fallback: "gemini-2.5-flash", temperature: 0.3, reasoning_effort: "low" },
580
+ analysis: { primary: "claude-sonnet-4-5", temperature: 0.3, reasoning_effort: "medium" },
581
+ security: { primary: "claude-opus-4-5", high_risk_override: "claude-opus-4-5", temperature: 0.1, reasoning_effort: "high" },
582
+ orchestration: { primary: "claude-sonnet-4-5", temperature: 0.3, reasoning_effort: "medium" }
583
+ };
584
+ function getRouterConfig(dir) {
585
+ const p = join6(codebaseDir(dir), "MODEL_ROUTER.json");
586
+ if (!existsSync6(p))
587
+ return DEFAULT_ROUTING;
588
+ try {
589
+ const overrides = JSON.parse(readFileSync6(p, "utf-8"));
590
+ return { ...DEFAULT_ROUTING, ...overrides };
591
+ } catch {
592
+ return DEFAULT_ROUTING;
593
+ }
594
+ }
595
+ function routeModel(dir, task_type, risk_score = 100) {
596
+ const config = getRouterConfig(dir);
597
+ const route = config[task_type] ?? DEFAULT_ROUTING.implementation;
598
+ const is_high_risk = risk_score < 40;
599
+ let model = route.primary;
600
+ let is_override = false;
601
+ if (is_high_risk && route.high_risk_override) {
602
+ model = route.high_risk_override;
603
+ is_override = true;
604
+ }
605
+ return {
606
+ model,
607
+ temperature: route.temperature ?? 0.3,
608
+ reasoning_effort: route.reasoning_effort,
609
+ task_type,
610
+ is_high_risk,
611
+ is_override
612
+ };
613
+ }
614
+
615
+ // src/tools/dispatch-routing.ts
616
+ function shouldRetry(promptRes) {
617
+ if (!promptRes)
618
+ return false;
619
+ const detail = promptRes.error?.detail;
620
+ if (isTransientError(detail))
621
+ return true;
622
+ const infoError = promptRes.data?.info?.error;
623
+ const text = typeof infoError === "string" ? infoError : JSON.stringify(infoError ?? "");
624
+ return isTransientError(text);
625
+ }
626
+ function isTransientError(text) {
627
+ if (!text)
628
+ return false;
629
+ const haystack = text.toLowerCase();
630
+ return haystack.includes("overload") || haystack.includes("rate limit") || haystack.includes("timeout") || haystack.includes("temporar") || haystack.includes("econnreset");
631
+ }
632
+ function normalizeTaskType(taskType, agent) {
633
+ const normalized = (taskType ?? "").trim().toLowerCase();
634
+ if (isTaskType(normalized))
635
+ return normalized;
636
+ const a = agent.toLowerCase();
637
+ if (a.includes("review"))
638
+ return "review";
639
+ if (a.includes("test"))
640
+ return "testing";
641
+ if (a.includes("debug"))
642
+ return "debugging";
643
+ if (a.includes("security"))
644
+ return "security";
645
+ if (a.includes("doc"))
646
+ return "documentation";
647
+ if (a.includes("architect") || a.includes("planner"))
648
+ return "planning";
649
+ if (a.includes("orchestrator") || a.includes("coordinator"))
650
+ return "orchestration";
651
+ if (a.includes("analyst") || a.includes("research"))
652
+ return "analysis";
653
+ return "implementation";
654
+ }
655
+ function isTaskType(value) {
656
+ return value === "planning" || value === "implementation" || value === "debugging" || value === "review" || value === "testing" || value === "documentation" || value === "analysis" || value === "security" || value === "orchestration";
657
+ }
658
+
659
+ // src/tools/run-pipeline.ts
512
660
  function extractText(parts) {
513
661
  return parts.filter((p) => p.type === "text" && typeof p.text === "string").map((p) => p.text).join(`
514
662
  `);
@@ -519,16 +667,20 @@ function createRunPipelineTool(client) {
519
667
  args: {
520
668
  steps: tool4.schema.array(tool4.schema.object({
521
669
  agent: tool4.schema.string(),
522
- prompt: tool4.schema.string()
670
+ prompt: tool4.schema.string(),
671
+ task_type: tool4.schema.string().optional()
523
672
  })),
524
673
  initial_context: tool4.schema.string().optional(),
525
- abort_on_failure: tool4.schema.boolean().optional().default(true)
674
+ abort_on_failure: tool4.schema.boolean().optional().default(true),
675
+ retry_attempts: tool4.schema.number().optional().default(1)
526
676
  },
527
677
  async execute(args, context) {
528
678
  const startTime = Date.now();
529
679
  const trace = [];
530
680
  let carryContext = args.initial_context ?? "";
531
681
  let aborted = false;
682
+ const retryAttempts = typeof args.retry_attempts === "number" ? args.retry_attempts : 1;
683
+ const maxRetries = Math.max(0, Math.floor(retryAttempts));
532
684
  let inflightChildId = null;
533
685
  const abortHandler = () => {
534
686
  if (inflightChildId) {
@@ -546,6 +698,8 @@ function createRunPipelineTool(client) {
546
698
  break;
547
699
  }
548
700
  const stepStart = Date.now();
701
+ const taskType = normalizeTaskType(step.task_type, step.agent);
702
+ const routing = routeModel(context.directory, taskType);
549
703
  const stepInput = carryContext ? `${carryContext}
550
704
 
551
705
  ---
@@ -557,28 +711,37 @@ ${step.prompt}` : step.prompt;
557
711
  });
558
712
  if (createRes.error || !createRes.data?.id) {
559
713
  const errMsg = `Failed to create session: ${createRes.error?.detail ?? "unknown"}`;
560
- trace.push({ agent: step.agent, input: stepInput, output: errMsg, duration_ms: Date.now() - stepStart, success: false });
714
+ trace.push({ agent: step.agent, task_type: taskType, model: routing.model, input: stepInput, output: errMsg, duration_ms: Date.now() - stepStart, success: false });
561
715
  aborted = true;
562
716
  break;
563
717
  }
564
718
  inflightChildId = createRes.data.id;
565
- const promptRes = await client.session.prompt({
566
- path: { id: inflightChildId },
567
- body: {
568
- agent: step.agent,
569
- parts: [{ type: "text", text: stepInput }],
570
- tools: { question: false }
571
- },
572
- query: { directory: context.directory }
573
- });
719
+ let promptRes = null;
720
+ let retriesUsed = 0;
721
+ for (let attempt = 0;attempt <= maxRetries; attempt++) {
722
+ promptRes = await client.session.prompt({
723
+ path: { id: inflightChildId },
724
+ body: {
725
+ agent: step.agent,
726
+ model: routing.model,
727
+ parts: [{ type: "text", text: stepInput }],
728
+ tools: { question: false }
729
+ },
730
+ query: { directory: context.directory }
731
+ });
732
+ if (!shouldRetry(promptRes) || attempt === maxRetries)
733
+ break;
734
+ retriesUsed++;
735
+ }
574
736
  inflightChildId = null;
575
737
  if (context.abort.aborted) {
576
738
  aborted = true;
577
739
  break;
578
740
  }
579
- if (promptRes.error) {
580
- const errMsg = `Prompt failed: ${promptRes.error?.detail ?? "unknown"}`;
581
- trace.push({ agent: step.agent, session_id: createRes.data.id, input: stepInput, output: errMsg, duration_ms: Date.now() - stepStart, success: false });
741
+ if (!promptRes || promptRes.error) {
742
+ const errMsg = `Prompt failed: ${promptRes?.error?.detail ?? "unknown"}`;
743
+ trace.push({ agent: step.agent, session_id: createRes.data.id, task_type: taskType, model: routing.model, input: stepInput, output: `${errMsg}${retriesUsed > 0 ? ` (retries: ${retriesUsed})` : ""}`, duration_ms: Date.now() - stepStart, success: false });
744
+ recordRun(context.directory, step.agent, routing.model, taskType, false, Date.now() - stepStart);
582
745
  if (args.abort_on_failure) {
583
746
  aborted = true;
584
747
  break;
@@ -588,7 +751,8 @@ ${step.prompt}` : step.prompt;
588
751
  const info = promptRes.data?.info;
589
752
  if (info?.error) {
590
753
  const errMsg = `Agent error: ${JSON.stringify(info.error)}`;
591
- trace.push({ agent: step.agent, session_id: createRes.data.id, input: stepInput, output: errMsg, duration_ms: Date.now() - stepStart, success: false });
754
+ trace.push({ agent: step.agent, session_id: createRes.data.id, task_type: taskType, model: routing.model, input: stepInput, output: `${errMsg}${retriesUsed > 0 ? ` (retries: ${retriesUsed})` : ""}`, duration_ms: Date.now() - stepStart, success: false });
755
+ recordRun(context.directory, step.agent, routing.model, taskType, false, Date.now() - stepStart);
592
756
  if (args.abort_on_failure) {
593
757
  aborted = true;
594
758
  break;
@@ -596,7 +760,8 @@ ${step.prompt}` : step.prompt;
596
760
  continue;
597
761
  }
598
762
  const output = extractText(promptRes.data?.parts ?? []);
599
- trace.push({ agent: step.agent, session_id: createRes.data.id, input: stepInput, output: output || "(no text output)", duration_ms: Date.now() - stepStart, success: true });
763
+ trace.push({ agent: step.agent, session_id: createRes.data.id, task_type: taskType, model: routing.model, input: stepInput, output: output || "(no text output)", duration_ms: Date.now() - stepStart, success: true });
764
+ recordRun(context.directory, step.agent, routing.model, taskType, true, Date.now() - stepStart);
600
765
  carryContext = output;
601
766
  }
602
767
  } finally {
@@ -623,10 +788,16 @@ function createDelegateTool(client) {
623
788
  args: {
624
789
  agent: tool5.schema.string(),
625
790
  prompt: tool5.schema.string(),
626
- context: tool5.schema.string().optional()
791
+ context: tool5.schema.string().optional(),
792
+ task_type: tool5.schema.string().optional(),
793
+ retry_attempts: tool5.schema.number().optional().default(1)
627
794
  },
628
795
  async execute(args, context) {
629
796
  const startTime = Date.now();
797
+ const taskType = normalizeTaskType(args.task_type, args.agent);
798
+ const routing = routeModel(context.directory, taskType);
799
+ const retryAttempts = typeof args.retry_attempts === "number" ? args.retry_attempts : 1;
800
+ const maxRetries = Math.max(0, Math.floor(retryAttempts));
630
801
  const createRes = await client.session.create({
631
802
  body: { parentID: context.sessionID, title: `${args.agent}-delegate` },
632
803
  query: { directory: context.directory }
@@ -651,40 +822,60 @@ function createDelegateTool(client) {
651
822
  ---
652
823
 
653
824
  ${args.prompt}` : args.prompt;
654
- const promptRes = await client.session.prompt({
655
- path: { id: childId },
656
- body: {
657
- agent: args.agent,
658
- parts: [{ type: "text", text: fullPrompt }],
659
- tools: { question: false }
660
- },
661
- query: { directory: context.directory }
662
- });
663
- if (promptRes.error) {
825
+ let promptRes = null;
826
+ let retriesUsed = 0;
827
+ for (let attempt = 0;attempt <= maxRetries; attempt++) {
828
+ promptRes = await client.session.prompt({
829
+ path: { id: childId },
830
+ body: {
831
+ agent: args.agent,
832
+ model: routing.model,
833
+ parts: [{ type: "text", text: fullPrompt }],
834
+ tools: { question: false }
835
+ },
836
+ query: { directory: context.directory }
837
+ });
838
+ if (!shouldRetry(promptRes) || attempt === maxRetries)
839
+ break;
840
+ retriesUsed++;
841
+ }
842
+ if (!promptRes || promptRes.error) {
843
+ recordRun(context.directory, args.agent, routing.model, taskType, false, Date.now() - startTime);
664
844
  return JSON.stringify({
665
845
  agent: args.agent,
666
846
  session_id: childId,
667
847
  success: false,
668
- error: `Prompt failed: ${promptRes.error?.detail ?? "unknown"}`,
848
+ error: `Prompt failed: ${promptRes?.error?.detail ?? "unknown"}`,
849
+ task_type: taskType,
850
+ model: routing.model,
851
+ retries_used: retriesUsed,
669
852
  duration_ms: Date.now() - startTime
670
853
  });
671
854
  }
672
855
  const info = promptRes.data?.info;
673
856
  if (info?.error) {
857
+ recordRun(context.directory, args.agent, routing.model, taskType, false, Date.now() - startTime);
674
858
  return JSON.stringify({
675
859
  agent: args.agent,
676
860
  session_id: childId,
677
861
  success: false,
678
862
  error: `Agent error: ${JSON.stringify(info.error)}`,
863
+ task_type: taskType,
864
+ model: routing.model,
865
+ retries_used: retriesUsed,
679
866
  duration_ms: Date.now() - startTime
680
867
  });
681
868
  }
682
869
  const output = extractText2(promptRes.data?.parts ?? []);
870
+ recordRun(context.directory, args.agent, routing.model, taskType, true, Date.now() - startTime);
683
871
  return JSON.stringify({
684
872
  agent: args.agent,
685
873
  session_id: childId,
686
874
  success: true,
687
875
  output: output || "(no text output)",
876
+ task_type: taskType,
877
+ model: routing.model,
878
+ retries_used: retriesUsed,
688
879
  duration_ms: Date.now() - startTime
689
880
  });
690
881
  }
@@ -693,31 +884,31 @@ ${args.prompt}` : args.prompt;
693
884
 
694
885
  // src/tools/repo-memory.ts
695
886
  import { tool as tool6 } from "@opencode-ai/plugin";
696
- import { readFileSync as readFileSync5, writeFileSync as writeFileSync5, existsSync as existsSync5, mkdirSync as mkdirSync2 } from "fs";
697
- import { join as join5 } from "path";
887
+ import { readFileSync as readFileSync7, writeFileSync as writeFileSync6, existsSync as existsSync7, mkdirSync as mkdirSync3 } from "fs";
888
+ import { join as join7 } from "path";
698
889
  var MEMORY_FILE = "MEMORY.json";
699
890
  function memoryPath(directory) {
700
- return join5(codebaseDir(directory), MEMORY_FILE);
891
+ return join7(codebaseDir(directory), MEMORY_FILE);
701
892
  }
702
893
  function emptyMemory() {
703
894
  return { version: "1.0", last_updated: new Date().toISOString(), nodes: {} };
704
895
  }
705
896
  function readMemory(directory) {
706
897
  const p = memoryPath(directory);
707
- if (!existsSync5(p))
898
+ if (!existsSync7(p))
708
899
  return emptyMemory();
709
900
  try {
710
- return JSON.parse(readFileSync5(p, "utf-8"));
901
+ return JSON.parse(readFileSync7(p, "utf-8"));
711
902
  } catch {
712
903
  return emptyMemory();
713
904
  }
714
905
  }
715
906
  function writeMemory(directory, memory) {
716
907
  const base = codebaseDir(directory);
717
- if (!existsSync5(base))
718
- mkdirSync2(base, { recursive: true });
908
+ if (!existsSync7(base))
909
+ mkdirSync3(base, { recursive: true });
719
910
  memory.last_updated = new Date().toISOString();
720
- writeFileSync5(memoryPath(directory), JSON.stringify(memory, null, 2), "utf-8");
911
+ writeFileSync6(memoryPath(directory), JSON.stringify(memory, null, 2), "utf-8");
721
912
  }
722
913
  var repoMemoryTool = tool6({
723
914
  description: "Repo Memory Graph: read/write/query persistent architecture graph in .codebase/MEMORY.json (modules, dependencies, ownership, bug history, conventions)",
@@ -794,28 +985,28 @@ var repoMemoryTool = tool6({
794
985
 
795
986
  // src/tools/failure-replay.ts
796
987
  import { tool as tool7 } from "@opencode-ai/plugin";
797
- import { readFileSync as readFileSync6, writeFileSync as writeFileSync6, existsSync as existsSync6, mkdirSync as mkdirSync3 } from "fs";
798
- import { join as join6 } from "path";
988
+ import { readFileSync as readFileSync8, writeFileSync as writeFileSync7, existsSync as existsSync8, mkdirSync as mkdirSync4 } from "fs";
989
+ import { join as join8 } from "path";
799
990
  var FAILURES_FILE = "FAILURES.json";
800
991
  function failuresPath(directory) {
801
- return join6(codebaseDir(directory), FAILURES_FILE);
992
+ return join8(codebaseDir(directory), FAILURES_FILE);
802
993
  }
803
994
  function readStore(directory) {
804
995
  const p = failuresPath(directory);
805
- if (!existsSync6(p))
996
+ if (!existsSync8(p))
806
997
  return { version: "1.0", last_updated: new Date().toISOString(), entries: [] };
807
998
  try {
808
- return JSON.parse(readFileSync6(p, "utf-8"));
999
+ return JSON.parse(readFileSync8(p, "utf-8"));
809
1000
  } catch {
810
1001
  return { version: "1.0", last_updated: new Date().toISOString(), entries: [] };
811
1002
  }
812
1003
  }
813
1004
  function writeStore(directory, store) {
814
1005
  const base = codebaseDir(directory);
815
- if (!existsSync6(base))
816
- mkdirSync3(base, { recursive: true });
1006
+ if (!existsSync8(base))
1007
+ mkdirSync4(base, { recursive: true });
817
1008
  store.last_updated = new Date().toISOString();
818
- writeFileSync6(failuresPath(directory), JSON.stringify(store, null, 2), "utf-8");
1009
+ writeFileSync7(failuresPath(directory), JSON.stringify(store, null, 2), "utf-8");
819
1010
  }
820
1011
  var failureReplayTool = tool7({
821
1012
  description: "Failure Replay Engine: record and query past failures (reverted commits, failed deployments, flaky tests, bug fixes) in .codebase/FAILURES.json so the agent avoids repeating mistakes",
@@ -899,17 +1090,17 @@ var failureReplayTool = tool7({
899
1090
 
900
1091
  // src/tools/decision-trace.ts
901
1092
  import { tool as tool8 } from "@opencode-ai/plugin";
902
- import { readFileSync as readFileSync7, existsSync as existsSync7, mkdirSync as mkdirSync4, appendFileSync } from "fs";
903
- import { join as join7 } from "path";
1093
+ import { readFileSync as readFileSync9, existsSync as existsSync9, mkdirSync as mkdirSync5, appendFileSync } from "fs";
1094
+ import { join as join9 } from "path";
904
1095
  var DECISIONS_FILE = "DECISIONS.jsonl";
905
1096
  function decisionsPath(directory) {
906
- return join7(codebaseDir(directory), DECISIONS_FILE);
1097
+ return join9(codebaseDir(directory), DECISIONS_FILE);
907
1098
  }
908
1099
  function readDecisions(directory) {
909
1100
  const p = decisionsPath(directory);
910
- if (!existsSync7(p))
1101
+ if (!existsSync9(p))
911
1102
  return [];
912
- return readFileSync7(p, "utf-8").split(`
1103
+ return readFileSync9(p, "utf-8").split(`
913
1104
  `).filter((l) => l.trim()).map((l) => {
914
1105
  try {
915
1106
  return JSON.parse(l);
@@ -949,8 +1140,8 @@ var decisionTraceTool = tool8({
949
1140
  case "record": {
950
1141
  if (!args.entry)
951
1142
  return JSON.stringify({ error: "entry required" });
952
- if (!existsSync7(base))
953
- mkdirSync4(base, { recursive: true });
1143
+ if (!existsSync9(base))
1144
+ mkdirSync5(base, { recursive: true });
954
1145
  const entry = { ...args.entry, timestamp: new Date().toISOString() };
955
1146
  appendFileSync(decisionsPath(dir), JSON.stringify(entry) + `
956
1147
  `, "utf-8");
@@ -984,28 +1175,28 @@ var decisionTraceTool = tool8({
984
1175
 
985
1176
  // src/tools/volatility-map.ts
986
1177
  import { tool as tool9 } from "@opencode-ai/plugin";
987
- import { readFileSync as readFileSync8, writeFileSync as writeFileSync8, existsSync as existsSync8, mkdirSync as mkdirSync5 } from "fs";
988
- import { join as join8 } from "path";
1178
+ import { readFileSync as readFileSync10, writeFileSync as writeFileSync9, existsSync as existsSync10, mkdirSync as mkdirSync6 } from "fs";
1179
+ import { join as join10 } from "path";
989
1180
  var VOLATILITY_FILE = "VOLATILITY.json";
990
1181
  function volatilityPath(directory) {
991
- return join8(codebaseDir(directory), VOLATILITY_FILE);
1182
+ return join10(codebaseDir(directory), VOLATILITY_FILE);
992
1183
  }
993
1184
  function readStore2(directory) {
994
1185
  const p = volatilityPath(directory);
995
- if (!existsSync8(p))
1186
+ if (!existsSync10(p))
996
1187
  return { version: "1.0", last_updated: new Date().toISOString(), generated_at: new Date().toISOString(), entries: [] };
997
1188
  try {
998
- return JSON.parse(readFileSync8(p, "utf-8"));
1189
+ return JSON.parse(readFileSync10(p, "utf-8"));
999
1190
  } catch {
1000
1191
  return { version: "1.0", last_updated: new Date().toISOString(), generated_at: new Date().toISOString(), entries: [] };
1001
1192
  }
1002
1193
  }
1003
1194
  function writeStore2(directory, store) {
1004
1195
  const base = codebaseDir(directory);
1005
- if (!existsSync8(base))
1006
- mkdirSync5(base, { recursive: true });
1196
+ if (!existsSync10(base))
1197
+ mkdirSync6(base, { recursive: true });
1007
1198
  store.last_updated = new Date().toISOString();
1008
- writeFileSync8(volatilityPath(directory), JSON.stringify(store, null, 2), "utf-8");
1199
+ writeFileSync9(volatilityPath(directory), JSON.stringify(store, null, 2), "utf-8");
1009
1200
  }
1010
1201
  function stabilityLabel(churn, hotfixes, todos) {
1011
1202
  const score = churn + hotfixes * 10 + todos * 2;
@@ -1092,28 +1283,28 @@ var volatilityMapTool = tool9({
1092
1283
 
1093
1284
  // src/tools/policy-engine.ts
1094
1285
  import { tool as tool10 } from "@opencode-ai/plugin";
1095
- import { readFileSync as readFileSync9, writeFileSync as writeFileSync9, existsSync as existsSync9, mkdirSync as mkdirSync6 } from "fs";
1096
- import { join as join9 } from "path";
1286
+ import { readFileSync as readFileSync11, writeFileSync as writeFileSync10, existsSync as existsSync11, mkdirSync as mkdirSync7 } from "fs";
1287
+ import { join as join11 } from "path";
1097
1288
  var POLICIES_FILE = "POLICIES.json";
1098
1289
  function policiesPath(directory) {
1099
- return join9(codebaseDir(directory), POLICIES_FILE);
1290
+ return join11(codebaseDir(directory), POLICIES_FILE);
1100
1291
  }
1101
1292
  function readStore3(directory) {
1102
1293
  const p = policiesPath(directory);
1103
- if (!existsSync9(p))
1294
+ if (!existsSync11(p))
1104
1295
  return { version: "1.0", last_updated: new Date().toISOString(), policies: [] };
1105
1296
  try {
1106
- return JSON.parse(readFileSync9(p, "utf-8"));
1297
+ return JSON.parse(readFileSync11(p, "utf-8"));
1107
1298
  } catch {
1108
1299
  return { version: "1.0", last_updated: new Date().toISOString(), policies: [] };
1109
1300
  }
1110
1301
  }
1111
1302
  function writeStore3(directory, store) {
1112
1303
  const base = codebaseDir(directory);
1113
- if (!existsSync9(base))
1114
- mkdirSync6(base, { recursive: true });
1304
+ if (!existsSync11(base))
1305
+ mkdirSync7(base, { recursive: true });
1115
1306
  store.last_updated = new Date().toISOString();
1116
- writeFileSync9(policiesPath(directory), JSON.stringify(store, null, 2), "utf-8");
1307
+ writeFileSync10(policiesPath(directory), JSON.stringify(store, null, 2), "utf-8");
1117
1308
  }
1118
1309
  var policyEngineTool = tool10({
1119
1310
  description: "Self-Healing Policy Engine: manage .codebase/POLICIES.json — add, list, query, toggle, and record violations of editing policies learned from past failures",
@@ -1195,7 +1386,7 @@ var policyEngineTool = tool10({
1195
1386
 
1196
1387
  // src/tools/hash-edit.ts
1197
1388
  import { tool as tool11 } from "@opencode-ai/plugin";
1198
- import { readFileSync as readFileSync10, writeFileSync as writeFileSync10 } from "fs";
1389
+ import { readFileSync as readFileSync12, writeFileSync as writeFileSync11 } from "fs";
1199
1390
  import { createHash } from "crypto";
1200
1391
  var hashEditTool = tool11({
1201
1392
  description: "Reliable file editing with content verification. Takes a target content, its expected MD5 hash, and replacement content. Only applies if the hash matches, preventing edits on stale file versions.",
@@ -1209,7 +1400,7 @@ var hashEditTool = tool11({
1209
1400
  const fullPath = args.filePath.startsWith("/") ? args.filePath : `${context.directory}/${args.filePath}`;
1210
1401
  let content;
1211
1402
  try {
1212
- content = readFileSync10(fullPath, "utf-8");
1403
+ content = readFileSync12(fullPath, "utf-8");
1213
1404
  } catch (e) {
1214
1405
  return `Error: Could not read file ${args.filePath}`;
1215
1406
  }
@@ -1223,13 +1414,15 @@ var hashEditTool = tool11({
1223
1414
  }
1224
1415
  }
1225
1416
  const newContent = content.replace(args.targetContent, args.replacementContent);
1226
- writeFileSync10(fullPath, newContent, "utf-8");
1417
+ writeFileSync11(fullPath, newContent, "utf-8");
1227
1418
  return `Successfully updated ${args.filePath} using hash-anchored edit.`;
1228
1419
  }
1229
1420
  });
1230
1421
 
1231
1422
  // src/tools/council.ts
1232
1423
  import { tool as tool12 } from "@opencode-ai/plugin";
1424
+ import { appendFileSync as appendFileSync2, existsSync as existsSync12, mkdirSync as mkdirSync8 } from "fs";
1425
+ import { join as join12 } from "path";
1233
1426
  function createCouncilTool(client) {
1234
1427
  return tool12({
1235
1428
  description: "Run an ensemble of agents (Council) on the same task to reach consensus or compare approaches. Runs 3 specialized agents in parallel and returns their synthesized outputs.",
@@ -1284,16 +1477,34 @@ Please synthesize these results. Identify areas of agreement, resolve conflicts,
1284
1477
  },
1285
1478
  query: { directory: context.directory }
1286
1479
  });
1287
- return (finalRes.data?.parts ?? []).filter((p) => p.type === "text").map((p) => p.text).join(`
1480
+ const synthesis = (finalRes.data?.parts ?? []).filter((p) => p.type === "text").map((p) => p.text).join(`
1288
1481
  `);
1482
+ persistCouncilResult(context.directory, {
1483
+ task: args.task,
1484
+ agents,
1485
+ results,
1486
+ synthesis,
1487
+ created_at: new Date().toISOString()
1488
+ });
1489
+ return synthesis;
1289
1490
  }
1290
1491
  });
1291
1492
  }
1493
+ function persistCouncilResult(directory, payload) {
1494
+ try {
1495
+ const base = codebaseDir(directory);
1496
+ if (!existsSync12(base))
1497
+ mkdirSync8(base, { recursive: true });
1498
+ const path = join12(base, "COUNCILS.jsonl");
1499
+ appendFileSync2(path, JSON.stringify(payload) + `
1500
+ `, "utf-8");
1501
+ } catch {}
1502
+ }
1292
1503
 
1293
1504
  // src/tools/context-generator.ts
1294
1505
  import { tool as tool13 } from "@opencode-ai/plugin";
1295
- import { writeFileSync as writeFileSync11, existsSync as existsSync10, readFileSync as readFileSync11, readdirSync as readdirSync2, statSync } from "fs";
1296
- import { join as join10 } from "path";
1506
+ import { writeFileSync as writeFileSync12, existsSync as existsSync13, readFileSync as readFileSync13, readdirSync as readdirSync2, statSync } from "fs";
1507
+ import { join as join13 } from "path";
1297
1508
  var contextGeneratorTool = tool13({
1298
1509
  description: "Auto-generate or update hierarchical context files (AGENTS.md, CLAUDE.md) throughout the project. These files provide critical grounding for AI agents.",
1299
1510
  args: {
@@ -1302,20 +1513,20 @@ var contextGeneratorTool = tool13({
1302
1513
  },
1303
1514
  async execute(args, context) {
1304
1515
  const root = context.directory;
1305
- const target = args.targetDir ? join10(root, args.targetDir) : root;
1306
- if (!existsSync10(target)) {
1516
+ const target = args.targetDir ? join13(root, args.targetDir) : root;
1517
+ if (!existsSync13(target)) {
1307
1518
  return `Error: Directory ${target} does not exist.`;
1308
1519
  }
1309
- const agentsMdPath = join10(target, "AGENTS.md");
1310
- if (existsSync10(agentsMdPath) && !args.force) {
1520
+ const agentsMdPath = join13(target, "AGENTS.md");
1521
+ if (existsSync13(agentsMdPath) && !args.force) {
1311
1522
  return `AGENTS.md already exists in ${target}. Use force: true to overwrite.`;
1312
1523
  }
1313
- const pkgPath = join10(root, "package.json");
1524
+ const pkgPath = join13(root, "package.json");
1314
1525
  let projectName = "Project";
1315
1526
  let techStack = "Unknown";
1316
- if (existsSync10(pkgPath)) {
1527
+ if (existsSync13(pkgPath)) {
1317
1528
  try {
1318
- const pkg = JSON.parse(readFileSync11(pkgPath, "utf-8"));
1529
+ const pkg = JSON.parse(readFileSync13(pkgPath, "utf-8"));
1319
1530
  projectName = pkg.name || projectName;
1320
1531
  techStack = Object.keys(pkg.dependencies || {}).slice(0, 5).join(", ");
1321
1532
  } catch {}
@@ -1333,7 +1544,7 @@ var contextGeneratorTool = tool13({
1333
1544
 
1334
1545
  ## Directory Map
1335
1546
  ${readdirSync2(target).slice(0, 10).map((f) => {
1336
- const s = statSync(join10(target, f));
1547
+ const s = statSync(join13(target, f));
1337
1548
  return `- \`${f}\`${s.isDirectory() ? "/" : ""} : [Description]`;
1338
1549
  }).join(`
1339
1550
  `)}
@@ -1341,17 +1552,17 @@ ${readdirSync2(target).slice(0, 10).map((f) => {
1341
1552
  ---
1342
1553
  Generated by FlowDeck Context Generator.
1343
1554
  `;
1344
- writeFileSync11(agentsMdPath, content, "utf-8");
1555
+ writeFileSync12(agentsMdPath, content, "utf-8");
1345
1556
  return `Successfully generated AGENTS.md in ${target}.`;
1346
1557
  }
1347
1558
  });
1348
1559
 
1349
1560
  // src/tools/create-skill.ts
1350
1561
  import { tool as tool14 } from "@opencode-ai/plugin";
1351
- import { mkdirSync as mkdirSync7, writeFileSync as writeFileSync12, existsSync as existsSync11 } from "fs";
1352
- import { join as join11, dirname as dirname3 } from "path";
1562
+ import { mkdirSync as mkdirSync9, writeFileSync as writeFileSync13, existsSync as existsSync14 } from "fs";
1563
+ import { join as join14, dirname as dirname3 } from "path";
1353
1564
  import { fileURLToPath } from "url";
1354
- var SKILLS_DIR = join11(dirname3(fileURLToPath(import.meta.url)), "..", "skills");
1565
+ var SKILLS_DIR = join14(dirname3(fileURLToPath(import.meta.url)), "..", "skills");
1355
1566
  var createSkillTool = tool14({
1356
1567
  description: "Create a new reusable skill in the FlowDeck skill library (src/skills/). " + "Use this when you discover a repeatable pattern, solve a novel problem with human guidance, " + "or want to capture domain knowledge for future sessions.",
1357
1568
  args: {
@@ -1361,9 +1572,9 @@ var createSkillTool = tool14({
1361
1572
  tags: tool14.schema.array(tool14.schema.string()).optional().describe("Optional tags for categorisation, e.g. ['performance', 'typescript']")
1362
1573
  },
1363
1574
  async execute(args) {
1364
- const skillDir = join11(SKILLS_DIR, args.name);
1365
- const skillFile = join11(skillDir, "SKILL.md");
1366
- if (existsSync11(skillFile)) {
1575
+ const skillDir = join14(SKILLS_DIR, args.name);
1576
+ const skillFile = join14(skillDir, "SKILL.md");
1577
+ if (existsSync14(skillFile)) {
1367
1578
  return `Skill '${args.name}' already exists at ${skillFile}.
1368
1579
  ` + `Use a different name or delete the existing skill directory first.`;
1369
1580
  }
@@ -1378,8 +1589,8 @@ origin: FlowDeck (self-learned)${tagLine}
1378
1589
  `;
1379
1590
  const fullContent = frontmatter + args.content.trimStart();
1380
1591
  try {
1381
- mkdirSync7(skillDir, { recursive: true });
1382
- writeFileSync12(skillFile, fullContent, "utf-8");
1592
+ mkdirSync9(skillDir, { recursive: true });
1593
+ writeFileSync13(skillFile, fullContent, "utf-8");
1383
1594
  return `✓ Skill '${args.name}' created at ${skillFile}
1384
1595
 
1385
1596
  ` + `The skill is now part of the FlowDeck library. Restart OpenCode to load it into the active session.`;
@@ -1391,8 +1602,8 @@ origin: FlowDeck (self-learned)${tagLine}
1391
1602
 
1392
1603
  // src/tools/reflect.ts
1393
1604
  import { tool as tool15 } from "@opencode-ai/plugin";
1394
- import { existsSync as existsSync12, readFileSync as readFileSync12 } from "fs";
1395
- import { join as join12 } from "path";
1605
+ import { existsSync as existsSync15, readFileSync as readFileSync14 } from "fs";
1606
+ import { join as join15 } from "path";
1396
1607
  var MAX_ARTIFACT_BYTES = 4000;
1397
1608
  function tail(text, maxBytes) {
1398
1609
  if (text.length <= maxBytes)
@@ -1421,11 +1632,11 @@ var reflectTool = tool15({
1421
1632
  ];
1422
1633
  let found = 0;
1423
1634
  for (const [rel, label] of ARTIFACT_PATHS) {
1424
- const full = join12(root, rel);
1425
- if (!existsSync12(full))
1635
+ const full = join15(root, rel);
1636
+ if (!existsSync15(full))
1426
1637
  continue;
1427
1638
  try {
1428
- const raw = readFileSync12(full, "utf-8").trim();
1639
+ const raw = readFileSync14(full, "utf-8").trim();
1429
1640
  if (!raw)
1430
1641
  continue;
1431
1642
  const count = raw.split(`
@@ -1449,14 +1660,14 @@ import { tool as tool16 } from "@opencode-ai/plugin";
1449
1660
 
1450
1661
  // src/services/memory-store.ts
1451
1662
  import { Database } from "bun:sqlite";
1452
- import { existsSync as existsSync13, mkdirSync as mkdirSync8 } from "fs";
1453
- import { join as join13 } from "path";
1663
+ import { existsSync as existsSync16, mkdirSync as mkdirSync10 } from "fs";
1664
+ import { join as join16 } from "path";
1454
1665
  import { homedir } from "os";
1455
- var MEMORY_DIR = join13(homedir(), ".flowdeck-memory");
1456
- var DB_PATH = join13(MEMORY_DIR, "memory.db");
1666
+ var MEMORY_DIR = join16(homedir(), ".flowdeck-memory");
1667
+ var DB_PATH = join16(MEMORY_DIR, "memory.db");
1457
1668
  function ensureDir() {
1458
- if (!existsSync13(MEMORY_DIR)) {
1459
- mkdirSync8(MEMORY_DIR, { recursive: true });
1669
+ if (!existsSync16(MEMORY_DIR)) {
1670
+ mkdirSync10(MEMORY_DIR, { recursive: true });
1460
1671
  }
1461
1672
  }
1462
1673
  var db = null;
@@ -1731,6 +1942,76 @@ var memorySearchTool = tool16({
1731
1942
  }
1732
1943
  });
1733
1944
 
1945
+ // src/tools/memory-status.ts
1946
+ import { tool as tool17 } from "@opencode-ai/plugin";
1947
+ import { Database as Database2 } from "bun:sqlite";
1948
+ import { existsSync as existsSync17 } from "fs";
1949
+ import { join as join17 } from "path";
1950
+ import { homedir as homedir2 } from "os";
1951
+ var DB_PATH2 = join17(homedir2(), ".flowdeck-memory", "memory.db");
1952
+ var memoryStatusTool = tool17({
1953
+ description: "Check FlowDeck memory database status, statistics, and recent sessions",
1954
+ args: {},
1955
+ async execute(_args, _context) {
1956
+ try {
1957
+ const exists = existsSync17(DB_PATH2);
1958
+ const result = {
1959
+ database_exists: exists,
1960
+ path: DB_PATH2,
1961
+ status: exists ? "ACTIVE" : "NOT_INITIALIZED",
1962
+ statistics: null
1963
+ };
1964
+ if (exists) {
1965
+ try {
1966
+ const db2 = new Database2(DB_PATH2);
1967
+ const sessions = db2.prepare("SELECT COUNT(*) as count FROM sessions").get();
1968
+ const observations = db2.prepare("SELECT COUNT(*) as count FROM observations").get();
1969
+ const summaries = db2.prepare("SELECT COUNT(*) as count FROM summaries").get();
1970
+ const recentSessions = db2.prepare(`
1971
+ SELECT
1972
+ id,
1973
+ content_session_id,
1974
+ project,
1975
+ directory,
1976
+ created_at,
1977
+ last_active_at,
1978
+ prompt_count
1979
+ FROM sessions
1980
+ ORDER BY last_active_at DESC
1981
+ LIMIT 5
1982
+ `).all();
1983
+ result.statistics = {
1984
+ sessions: sessions.count,
1985
+ observations: observations.count,
1986
+ summaries: summaries.count,
1987
+ recent_sessions: recentSessions.map((s) => {
1988
+ const obsCount = db2.prepare("SELECT COUNT(*) as count FROM observations WHERE session_id = ?").get(s.id);
1989
+ return {
1990
+ project: s.project,
1991
+ directory: s.directory,
1992
+ observations_in_session: obsCount.count,
1993
+ last_active: s.last_active_at,
1994
+ prompt_count: s.prompt_count
1995
+ };
1996
+ })
1997
+ };
1998
+ db2.close();
1999
+ } catch (err) {
2000
+ result.status = "ERROR";
2001
+ result.statistics = { error: String(err) };
2002
+ }
2003
+ }
2004
+ return JSON.stringify(result, null, 2);
2005
+ } catch (err) {
2006
+ return JSON.stringify({
2007
+ status: "ERROR",
2008
+ error: String(err),
2009
+ path: DB_PATH2
2010
+ }, null, 2);
2011
+ }
2012
+ }
2013
+ });
2014
+
1734
2015
  // src/hooks/memory-hook.ts
1735
2016
  var MAX_TOOL_RESPONSE = 1e4;
1736
2017
  var MAX_PROMPT_LENGTH = 2000;
@@ -1830,15 +2111,15 @@ var memoryHook = {
1830
2111
  };
1831
2112
 
1832
2113
  // src/hooks/guard-rails.ts
1833
- import { existsSync as existsSync14, readFileSync as readFileSync13 } from "fs";
1834
- import { join as join14 } from "path";
2114
+ import { existsSync as existsSync18, readFileSync as readFileSync15 } from "fs";
2115
+ import { join as join18 } from "path";
1835
2116
  var PLANNING_DIR2 = ".planning";
1836
2117
  var CONFIG_FILE = "config.json";
1837
2118
  var STATE_FILE2 = "STATE.md";
1838
2119
  function resolveExecutionMode(configPath, trustScore, volatility) {
1839
- if (existsSync14(configPath)) {
2120
+ if (existsSync18(configPath)) {
1840
2121
  try {
1841
- const config = JSON.parse(readFileSync13(configPath, "utf-8"));
2122
+ const config = JSON.parse(readFileSync15(configPath, "utf-8"));
1842
2123
  if (config.execution_mode === "review-only")
1843
2124
  return "review-only";
1844
2125
  if (config.execution_mode === "guarded")
@@ -1892,22 +2173,22 @@ async function guardRailsHook(ctx, input, _output) {
1892
2173
  if (!ENABLED)
1893
2174
  return;
1894
2175
  const dir = ctx.directory;
1895
- const planningDirPath = join14(dir, PLANNING_DIR2);
2176
+ const planningDirPath = join18(dir, PLANNING_DIR2);
1896
2177
  const codebaseDirectory = codebaseDir(dir);
1897
- const configPath = join14(planningDirPath, CONFIG_FILE);
1898
- const statePath2 = join14(planningDirPath, STATE_FILE2);
2178
+ const configPath = join18(planningDirPath, CONFIG_FILE);
2179
+ const statePath2 = join18(planningDirPath, STATE_FILE2);
1899
2180
  const workspaceRoot = findWorkspaceRoot(dir);
1900
2181
  if (workspaceRoot && dir !== workspaceRoot) {
1901
2182
  const config = getWorkspaceConfig(dir);
1902
- if (config && config.workspace_mode === "shared" && !existsSync14(planningDirPath)) {
2183
+ if (config && config.workspace_mode === "shared" && !existsSync18(planningDirPath)) {
1903
2184
  const msg = `No .planning/ in this sub-repo. Switch to workspace root: cd ${workspaceRoot}`;
1904
2185
  throw new Error(`[flowdeck] BLOCK: ${msg}`);
1905
2186
  }
1906
2187
  }
1907
2188
  if (input.tool === "write" || input.tool === "edit") {
1908
- if (!existsSync14(planningDirPath))
2189
+ if (!existsSync18(planningDirPath))
1909
2190
  return;
1910
- if (!existsSync14(codebaseDirectory)) {
2191
+ if (!existsSync18(codebaseDirectory)) {
1911
2192
  throw new Error(`[flowdeck] WARNING: .codebase/ not found. Run /map-codebase to map the codebase.`);
1912
2193
  }
1913
2194
  const execMode = resolveExecutionMode(configPath, null);
@@ -1940,9 +2221,9 @@ async function guardRailsHook(ctx, input, _output) {
1940
2221
  }
1941
2222
  }
1942
2223
  function effectiveSeverity(configPath, statePath2) {
1943
- if (existsSync14(configPath)) {
2224
+ if (existsSync18(configPath)) {
1944
2225
  try {
1945
- const configContent = readFileSync13(configPath, "utf-8");
2226
+ const configContent = readFileSync15(configPath, "utf-8");
1946
2227
  const config = JSON.parse(configContent);
1947
2228
  if (config.guard_enforcement === "warn")
1948
2229
  return "warn";
@@ -1958,10 +2239,10 @@ function getEffectiveSeverity(configPath, statePath2) {
1958
2239
  return effectiveSeverity(configPath, statePath2);
1959
2240
  }
1960
2241
  function getPlanConfirmed(statePath2) {
1961
- if (!existsSync14(statePath2))
2242
+ if (!existsSync18(statePath2))
1962
2243
  return false;
1963
2244
  try {
1964
- const content = readFileSync13(statePath2, "utf-8");
2245
+ const content = readFileSync15(statePath2, "utf-8");
1965
2246
  const match = content.match(/plan_confirmed:\s*(true|false)/i);
1966
2247
  return match ? match[1].toLowerCase() === "true" : false;
1967
2248
  } catch {
@@ -1969,32 +2250,32 @@ function getPlanConfirmed(statePath2) {
1969
2250
  }
1970
2251
  }
1971
2252
  function getWarningMessage(planningDir2) {
1972
- if (!existsSync14(join14(planningDir2, STATE_FILE2))) {
2253
+ if (!existsSync18(join18(planningDir2, STATE_FILE2))) {
1973
2254
  return "No .planning/ found. Run /new-project first.";
1974
2255
  }
1975
2256
  return "Plan not confirmed. Run /plan and confirm to enable execution.";
1976
2257
  }
1977
2258
  function getBlockMessage(planningDir2) {
1978
- if (!existsSync14(join14(planningDir2, STATE_FILE2))) {
2259
+ if (!existsSync18(join18(planningDir2, STATE_FILE2))) {
1979
2260
  return "No .planning/ found. Run /new-project first.";
1980
2261
  }
1981
2262
  return "Plan not confirmed. Run /plan and confirm to enable execution.";
1982
2263
  }
1983
2264
 
1984
2265
  // src/hooks/tool-guard.ts
1985
- import { existsSync as existsSync15, readFileSync as readFileSync14 } from "fs";
1986
- import { join as join15 } from "path";
2266
+ import { existsSync as existsSync19, readFileSync as readFileSync16 } from "fs";
2267
+ import { join as join19 } from "path";
1987
2268
  var IS_ENABLED = () => process.env.FLOWDECK_TOOL_GUARD_ENABLED === "on";
1988
2269
  var BLOCKED_PATTERNS = {
1989
2270
  read: [".env", ".pem", ".key", ".secret"],
1990
2271
  write: ["node_modules"],
1991
2272
  bash: ["rm -rf"]
1992
2273
  };
1993
- function isBlocked(tool17, args) {
1994
- const patterns = BLOCKED_PATTERNS[tool17];
2274
+ function isBlocked(tool18, args) {
2275
+ const patterns = BLOCKED_PATTERNS[tool18];
1995
2276
  if (!patterns)
1996
2277
  return null;
1997
- if (tool17 === "bash") {
2278
+ if (tool18 === "bash") {
1998
2279
  const cmd = args.command;
1999
2280
  if (!cmd)
2000
2281
  return null;
@@ -2005,7 +2286,7 @@ function isBlocked(tool17, args) {
2005
2286
  }
2006
2287
  return null;
2007
2288
  }
2008
- if (tool17 === "read") {
2289
+ if (tool18 === "read") {
2009
2290
  const filePath = args.filePath;
2010
2291
  if (!filePath)
2011
2292
  return null;
@@ -2016,7 +2297,7 @@ function isBlocked(tool17, args) {
2016
2297
  }
2017
2298
  return null;
2018
2299
  }
2019
- if (tool17 === "write") {
2300
+ if (tool18 === "write") {
2020
2301
  const filePath = args.filePath;
2021
2302
  if (!filePath)
2022
2303
  return null;
@@ -2030,11 +2311,11 @@ function isBlocked(tool17, args) {
2030
2311
  return null;
2031
2312
  }
2032
2313
  function checkArchConstraint(directory, filePath) {
2033
- const constraintsPath = join15(codebaseDir(directory), "CONSTRAINTS.md");
2034
- if (!existsSync15(constraintsPath))
2314
+ const constraintsPath = join19(codebaseDir(directory), "CONSTRAINTS.md");
2315
+ if (!existsSync19(constraintsPath))
2035
2316
  return null;
2036
2317
  try {
2037
- const content = readFileSync14(constraintsPath, "utf-8");
2318
+ const content = readFileSync16(constraintsPath, "utf-8");
2038
2319
  const match = content.match(/## Forbidden Paths\n([\s\S]*?)(?:\n##|$)/);
2039
2320
  if (!match)
2040
2321
  return null;
@@ -2081,18 +2362,18 @@ async function toolGuardHook(ctx, input, output) {
2081
2362
  }
2082
2363
 
2083
2364
  // src/hooks/session-start.ts
2084
- import { existsSync as existsSync16, readFileSync as readFileSync15 } from "fs";
2365
+ import { existsSync as existsSync20, readFileSync as readFileSync17 } from "fs";
2085
2366
  async function sessionStartHook(ctx) {
2086
2367
  const planningDir2 = ctx.directory + "/.planning";
2087
2368
  const codebaseDirectory = codebaseDir(ctx.directory);
2088
2369
  const workspaceRoot = findWorkspaceRoot(ctx.directory);
2089
2370
  const config = workspaceRoot ? getWorkspaceConfig(ctx.directory) : null;
2090
- if (!existsSync16(planningDir2)) {
2371
+ if (!existsSync20(planningDir2)) {
2091
2372
  return {
2092
2373
  flowdeck_phase: null,
2093
2374
  flowdeck_status: "no_plan",
2094
2375
  flowdeck_warning: "Run /new-project or /map-codebase to initialize.",
2095
- flowdeck_has_codebase: existsSync16(codebaseDirectory),
2376
+ flowdeck_has_codebase: existsSync20(codebaseDirectory),
2096
2377
  ...workspaceRoot && config?.sub_repos ? {
2097
2378
  flowdeck_workspace_root: workspaceRoot,
2098
2379
  flowdeck_sub_repos: config.sub_repos,
@@ -2103,7 +2384,7 @@ async function sessionStartHook(ctx) {
2103
2384
  }
2104
2385
  try {
2105
2386
  const stateFilePath = statePath(ctx.directory);
2106
- const content = readFileSync15(stateFilePath, "utf-8");
2387
+ const content = readFileSync17(stateFilePath, "utf-8");
2107
2388
  const state = parseState(content);
2108
2389
  const currentPhase = state["current_phase"] || {};
2109
2390
  const result = {
@@ -2111,7 +2392,7 @@ async function sessionStartHook(ctx) {
2111
2392
  flowdeck_status: currentPhase["status"] ?? null,
2112
2393
  flowdeck_steps_pending: currentPhase["steps_pending"] ?? null,
2113
2394
  flowdeck_last_action: currentPhase["last_action"] ?? null,
2114
- flowdeck_has_codebase: existsSync16(codebaseDirectory)
2395
+ flowdeck_has_codebase: existsSync20(codebaseDirectory)
2115
2396
  };
2116
2397
  if (workspaceRoot && config?.sub_repos && config.sub_repos.length > 0) {
2117
2398
  result.flowdeck_workspace_root = workspaceRoot;
@@ -2126,7 +2407,7 @@ async function sessionStartHook(ctx) {
2126
2407
  flowdeck_phase: null,
2127
2408
  flowdeck_status: "error",
2128
2409
  flowdeck_warning: "State file unreadable. Continuing without flowdeck context.",
2129
- flowdeck_has_codebase: existsSync16(codebaseDirectory)
2410
+ flowdeck_has_codebase: existsSync20(codebaseDirectory)
2130
2411
  };
2131
2412
  if (workspaceRoot && config?.sub_repos && config.sub_repos.length > 0) {
2132
2413
  result.flowdeck_workspace_root = workspaceRoot;
@@ -2187,13 +2468,13 @@ function tryTerminalBell() {
2187
2468
  function notifySessionIdle() {
2188
2469
  notify("FlowDeck Task Completed", "Agent is idle and waiting for your next instruction", "info");
2189
2470
  }
2190
- function notifyPermissionNeeded(tool17) {
2191
- notify("FlowDeck Permission Required", `Agent needs approval to use tool: ${tool17}`, "critical");
2471
+ function notifyPermissionNeeded(tool18) {
2472
+ notify("FlowDeck Permission Required", `Agent needs approval to use tool: ${tool18}`, "critical");
2192
2473
  }
2193
2474
 
2194
2475
  // src/hooks/patch-trust.ts
2195
- import { existsSync as existsSync17, readFileSync as readFileSync16 } from "fs";
2196
- import { join as join16 } from "path";
2476
+ import { existsSync as existsSync21, readFileSync as readFileSync18 } from "fs";
2477
+ import { join as join20 } from "path";
2197
2478
  var HIGH_RISK_KEYWORDS = [
2198
2479
  "password",
2199
2480
  "secret",
@@ -2215,11 +2496,11 @@ var HIGH_RISK_KEYWORDS = [
2215
2496
  "privilege"
2216
2497
  ];
2217
2498
  function loadVolatility(directory) {
2218
- const p = join16(codebaseDir(directory), "VOLATILITY.json");
2219
- if (!existsSync17(p))
2499
+ const p = join20(codebaseDir(directory), "VOLATILITY.json");
2500
+ if (!existsSync21(p))
2220
2501
  return {};
2221
2502
  try {
2222
- const data = JSON.parse(readFileSync16(p, "utf-8"));
2503
+ const data = JSON.parse(readFileSync18(p, "utf-8"));
2223
2504
  const map = {};
2224
2505
  for (const entry of data.entries ?? [])
2225
2506
  map[entry.path] = entry.stability;
@@ -2229,11 +2510,11 @@ function loadVolatility(directory) {
2229
2510
  }
2230
2511
  }
2231
2512
  function loadFailedPaths(directory) {
2232
- const p = join16(codebaseDir(directory), "FAILURES.json");
2233
- if (!existsSync17(p))
2513
+ const p = join20(codebaseDir(directory), "FAILURES.json");
2514
+ if (!existsSync21(p))
2234
2515
  return [];
2235
2516
  try {
2236
- const data = JSON.parse(readFileSync16(p, "utf-8"));
2517
+ const data = JSON.parse(readFileSync18(p, "utf-8"));
2237
2518
  return (data.entries ?? []).flatMap((e) => e.affected_paths ?? []);
2238
2519
  } catch {
2239
2520
  return [];
@@ -2298,8 +2579,8 @@ async function patchTrustHook(ctx, input, output) {
2298
2579
  }
2299
2580
 
2300
2581
  // src/hooks/decision-trace-hook.ts
2301
- import { existsSync as existsSync18, mkdirSync as mkdirSync9, appendFileSync as appendFileSync2 } from "fs";
2302
- import { join as join17 } from "path";
2582
+ import { existsSync as existsSync22, mkdirSync as mkdirSync11, appendFileSync as appendFileSync3 } from "fs";
2583
+ import { join as join21 } from "path";
2303
2584
  async function decisionTraceHook(ctx, input, output) {
2304
2585
  if (input.tool !== "write" && input.tool !== "edit")
2305
2586
  return;
@@ -2308,8 +2589,8 @@ async function decisionTraceHook(ctx, input, output) {
2308
2589
  return;
2309
2590
  const base = codebaseDir(ctx.directory);
2310
2591
  try {
2311
- if (!existsSync18(base))
2312
- mkdirSync9(base, { recursive: true });
2592
+ if (!existsSync22(base))
2593
+ mkdirSync11(base, { recursive: true });
2313
2594
  const entry = {
2314
2595
  timestamp: new Date().toISOString(),
2315
2596
  file_path: filePath,
@@ -2321,62 +2602,87 @@ async function decisionTraceHook(ctx, input, output) {
2321
2602
  risk_level: "unknown",
2322
2603
  auto_recorded: true
2323
2604
  };
2324
- appendFileSync2(join17(base, "DECISIONS.jsonl"), JSON.stringify(entry) + `
2605
+ appendFileSync3(join21(base, "DECISIONS.jsonl"), JSON.stringify(entry) + `
2325
2606
  `, "utf-8");
2326
2607
  } catch {}
2327
2608
  }
2328
2609
 
2329
2610
  // src/services/telemetry.ts
2330
- import { existsSync as existsSync19, readFileSync as readFileSync17, appendFileSync as appendFileSync3, mkdirSync as mkdirSync10 } from "fs";
2331
- import { join as join18 } from "path";
2611
+ import { existsSync as existsSync23, readFileSync as readFileSync19, appendFileSync as appendFileSync4, mkdirSync as mkdirSync12 } from "fs";
2612
+ import { join as join22 } from "path";
2332
2613
  import { randomUUID } from "crypto";
2333
2614
  function telemetryPath(dir) {
2334
- return join18(codebaseDir(dir), "TELEMETRY.jsonl");
2615
+ return join22(codebaseDir(dir), "TELEMETRY.jsonl");
2335
2616
  }
2336
2617
  function appendEvent(dir, partial) {
2337
2618
  if (process.env.TELEMETRY_ENABLED !== "true")
2338
2619
  return null;
2339
2620
  const cd = codebaseDir(dir);
2340
- if (!existsSync19(cd))
2341
- mkdirSync10(cd, { recursive: true });
2621
+ if (!existsSync23(cd))
2622
+ mkdirSync12(cd, { recursive: true });
2342
2623
  const event = {
2343
2624
  id: randomUUID(),
2344
2625
  ts: new Date().toISOString(),
2345
2626
  ...partial
2346
2627
  };
2347
- appendFileSync3(telemetryPath(dir), JSON.stringify(event) + `
2628
+ appendFileSync4(telemetryPath(dir), JSON.stringify(event) + `
2348
2629
  `, "utf-8");
2349
2630
  return event;
2350
2631
  }
2351
2632
 
2352
2633
  // src/hooks/telemetry-hook.ts
2634
+ function resolveIds(toolInput) {
2635
+ const session_id = toolInput.sessionID ?? toolInput.sessionId ?? process.env.OPENCODE_SESSION_ID ?? "session-0";
2636
+ const run_id = toolInput.messageID ?? toolInput.messageId ?? toolInput.runID ?? toolInput.runId ?? process.env.OPENCODE_RUN_ID ?? "run-0";
2637
+ return { session_id, run_id };
2638
+ }
2639
+ function inferStatus(output) {
2640
+ if (output.error)
2641
+ return "error";
2642
+ if (typeof output.output !== "string")
2643
+ return "ok";
2644
+ const text = output.output.trim();
2645
+ if (!text)
2646
+ return "ok";
2647
+ try {
2648
+ const parsed = JSON.parse(text);
2649
+ if (parsed.success === false || parsed.error || parsed.status === "error")
2650
+ return "error";
2651
+ return "ok";
2652
+ } catch {
2653
+ return "ok";
2654
+ }
2655
+ }
2353
2656
  async function telemetryHook(context, toolInput, output) {
2354
2657
  const dir = context.directory ?? process.cwd();
2355
- const tool17 = toolInput.name ?? toolInput.tool ?? "unknown";
2658
+ const tool18 = toolInput.name ?? toolInput.tool ?? "unknown";
2659
+ const ids = resolveIds(toolInput);
2356
2660
  appendEvent(dir, {
2357
- session_id: process.env.OPENCODE_SESSION_ID ?? "session-0",
2358
- run_id: process.env.OPENCODE_RUN_ID ?? "run-0",
2661
+ session_id: ids.session_id,
2662
+ run_id: ids.run_id,
2359
2663
  event: "tool.call",
2360
- tool: tool17,
2664
+ tool: tool18,
2361
2665
  status: "ok",
2362
2666
  meta: { parameters: output.args ?? {} }
2363
2667
  });
2364
2668
  }
2365
- async function telemetryAfterHook(context, toolInput, _output) {
2669
+ async function telemetryAfterHook(context, toolInput, output) {
2366
2670
  const dir = context.directory ?? process.cwd();
2367
- const tool17 = toolInput.name ?? toolInput.tool ?? "unknown";
2671
+ const tool18 = toolInput.name ?? toolInput.tool ?? "unknown";
2672
+ const ids = resolveIds(toolInput);
2673
+ const status = inferStatus(output);
2368
2674
  appendEvent(dir, {
2369
- session_id: process.env.OPENCODE_SESSION_ID ?? "session-0",
2370
- run_id: process.env.OPENCODE_RUN_ID ?? "run-0",
2675
+ session_id: ids.session_id,
2676
+ run_id: ids.run_id,
2371
2677
  event: "tool.complete",
2372
- tool: tool17,
2373
- status: "ok"
2678
+ tool: tool18,
2679
+ status
2374
2680
  });
2375
2681
  }
2376
2682
 
2377
2683
  // src/services/approval-manager.ts
2378
- import { existsSync as existsSync20, readFileSync as readFileSync18, writeFileSync as writeFileSync13, mkdirSync as mkdirSync11 } from "fs";
2379
- import { join as join19 } from "path";
2684
+ import { existsSync as existsSync24, readFileSync as readFileSync20, writeFileSync as writeFileSync14, mkdirSync as mkdirSync13 } from "fs";
2685
+ import { join as join23 } from "path";
2380
2686
  var APPROVAL_TTL_MS = 30 * 60 * 1000;
2381
2687
  var SENSITIVE_PATTERNS = [
2382
2688
  /auth/i,
@@ -2413,20 +2719,20 @@ function isSensitivePath(filePath) {
2413
2719
  return SENSITIVE_PATTERNS.some((p) => p.test(filePath));
2414
2720
  }
2415
2721
  function approvalsPath(dir) {
2416
- return join19(codebaseDir(dir), "APPROVALS.json");
2722
+ return join23(codebaseDir(dir), "APPROVALS.json");
2417
2723
  }
2418
- function loadStore(dir) {
2724
+ function loadStore2(dir) {
2419
2725
  const p = approvalsPath(dir);
2420
- if (!existsSync20(p))
2726
+ if (!existsSync24(p))
2421
2727
  return { requests: [] };
2422
2728
  try {
2423
- return JSON.parse(readFileSync18(p, "utf-8"));
2729
+ return JSON.parse(readFileSync20(p, "utf-8"));
2424
2730
  } catch {
2425
2731
  return { requests: [] };
2426
2732
  }
2427
2733
  }
2428
2734
  function checkApproval(dir, file_path, command) {
2429
- const store = loadStore(dir);
2735
+ const store = loadStore2(dir);
2430
2736
  const now = Date.now();
2431
2737
  return store.requests.filter((r) => r.status === "approved" && r.resolved_at && (r.file_path === file_path || r.trigger === command) && now - new Date(r.resolved_at).getTime() < APPROVAL_TTL_MS).sort((a, b) => b.resolved_at.localeCompare(a.resolved_at)).at(0) ?? null;
2432
2738
  }
@@ -2438,8 +2744,8 @@ async function approvalHook(context, toolInput, output) {
2438
2744
  if (!ENABLED2)
2439
2745
  return;
2440
2746
  const dir = context.directory ?? process.cwd();
2441
- const tool17 = toolInput.name ?? toolInput.tool ?? "";
2442
- if (!WRITE_TOOLS.has(tool17))
2747
+ const tool18 = toolInput.name ?? toolInput.tool ?? "";
2748
+ if (!WRITE_TOOLS.has(tool18))
2443
2749
  return;
2444
2750
  const args = output.args ?? {};
2445
2751
  const filePath = String(args.path ?? args.file_path ?? args.filename ?? "");
@@ -2454,7 +2760,7 @@ async function approvalHook(context, toolInput, output) {
2454
2760
  session_id: process.env.OPENCODE_SESSION_ID ?? "session-0",
2455
2761
  run_id: process.env.OPENCODE_RUN_ID ?? "run-0",
2456
2762
  event: "approval.request",
2457
- tool: tool17,
2763
+ tool: tool18,
2458
2764
  status: "blocked",
2459
2765
  files: [filePath],
2460
2766
  meta: { trigger: "sensitive_file", file: filePath }
@@ -2515,8 +2821,8 @@ function createContextWindowMonitorHook() {
2515
2821
  }
2516
2822
 
2517
2823
  // src/hooks/shell-env-hook.ts
2518
- import { existsSync as existsSync21, readFileSync as readFileSync19 } from "fs";
2519
- import { join as join20 } from "path";
2824
+ import { existsSync as existsSync25, readFileSync as readFileSync21 } from "fs";
2825
+ import { join as join24 } from "path";
2520
2826
  import { createRequire } from "module";
2521
2827
  var _version;
2522
2828
  function getVersion() {
@@ -2552,7 +2858,7 @@ var MARKER_TO_LANG = {
2552
2858
  };
2553
2859
  function detectPackageManager(root) {
2554
2860
  for (const [lockfile, pm] of Object.entries(LOCKFILE_TO_PM)) {
2555
- if (existsSync21(join20(root, lockfile)))
2861
+ if (existsSync25(join24(root, lockfile)))
2556
2862
  return pm;
2557
2863
  }
2558
2864
  return;
@@ -2561,7 +2867,7 @@ function detectLanguages(root) {
2561
2867
  const langs = [];
2562
2868
  const seen = new Set;
2563
2869
  for (const [marker, lang] of Object.entries(MARKER_TO_LANG)) {
2564
- if (!seen.has(lang) && existsSync21(join20(root, marker))) {
2870
+ if (!seen.has(lang) && existsSync25(join24(root, marker))) {
2565
2871
  langs.push(lang);
2566
2872
  seen.add(lang);
2567
2873
  }
@@ -2569,11 +2875,11 @@ function detectLanguages(root) {
2569
2875
  return langs;
2570
2876
  }
2571
2877
  function readCurrentPhase(root) {
2572
- const statePath2 = join20(root, ".planning", "STATE.md");
2573
- if (!existsSync21(statePath2))
2878
+ const statePath2 = join24(root, ".planning", "STATE.md");
2879
+ if (!existsSync25(statePath2))
2574
2880
  return;
2575
2881
  try {
2576
- const content = readFileSync19(statePath2, "utf-8");
2882
+ const content = readFileSync21(statePath2, "utf-8");
2577
2883
  const match = content.match(/phase:\s*(\S+)/i);
2578
2884
  return match?.[1];
2579
2885
  } catch {
@@ -2667,14 +2973,13 @@ function createSessionIdleHook(client, tracker) {
2667
2973
  if (edited.length > 10) {
2668
2974
  await client.app.log({ body: { service: "flowdeck", level: "info", message: ` … and ${edited.length - 10} more` } }).catch(() => {});
2669
2975
  }
2670
- tracker.clear();
2671
2976
  } catch {}
2672
2977
  };
2673
2978
  }
2674
2979
 
2675
2980
  // src/hooks/compaction-hook.ts
2676
- import { existsSync as existsSync22, readFileSync as readFileSync20 } from "fs";
2677
- import { join as join21 } from "path";
2981
+ import { existsSync as existsSync26, readFileSync as readFileSync22 } from "fs";
2982
+ import { join as join25 } from "path";
2678
2983
  var STRUCTURED_SUMMARY_PROMPT = `
2679
2984
  When summarizing this session, you MUST include the following sections:
2680
2985
 
@@ -2713,11 +3018,11 @@ For each: agent name, status, description, session_id.
2713
3018
  **RESUME, DON'T RESTART.** Use session_id to continue existing sessions.
2714
3019
  `;
2715
3020
  function readPlanningState2(directory) {
2716
- const statePath2 = join21(directory, ".planning", "STATE.md");
2717
- if (!existsSync22(statePath2))
3021
+ const statePath2 = join25(directory, ".planning", "STATE.md");
3022
+ if (!existsSync26(statePath2))
2718
3023
  return null;
2719
3024
  try {
2720
- const content = readFileSync20(statePath2, "utf-8");
3025
+ const content = readFileSync22(statePath2, "utf-8");
2721
3026
  return content.slice(0, 1500);
2722
3027
  } catch {
2723
3028
  return null;
@@ -2812,13 +3117,24 @@ function blockMessage(toolName) {
2812
3117
  class OrchestratorGuard {
2813
3118
  primarySessionId = null;
2814
3119
  onEvent(event) {
2815
- if ((event.type === "session.created" || event.type === "session.started") && this.primarySessionId === null) {
2816
- const props = event.properties;
2817
- const id = props?.info?.id;
2818
- if (id) {
2819
- this.primarySessionId = id;
3120
+ const eventType = event.type ?? "";
3121
+ if (eventType === "session.deleted") {
3122
+ const deletedId = extractSessionId(event);
3123
+ if (deletedId && deletedId === this.primarySessionId) {
3124
+ this.primarySessionId = null;
2820
3125
  }
3126
+ return;
2821
3127
  }
3128
+ if (eventType !== "session.created" && eventType !== "session.started")
3129
+ return;
3130
+ if (this.primarySessionId !== null)
3131
+ return;
3132
+ const id = extractSessionId(event);
3133
+ if (!id)
3134
+ return;
3135
+ if (extractParentSessionId(event))
3136
+ return;
3137
+ this.primarySessionId = id;
2822
3138
  }
2823
3139
  check(sessionId, toolName) {
2824
3140
  if (DISABLED)
@@ -2834,6 +3150,20 @@ class OrchestratorGuard {
2834
3150
  }
2835
3151
  }
2836
3152
  }
3153
+ function extractSessionId(event) {
3154
+ const props = event.properties;
3155
+ const inner = event.event;
3156
+ const info = props?.info;
3157
+ const id = event.sessionID ?? event.sessionId ?? inner?.sessionID ?? inner?.sessionId ?? info?.id;
3158
+ return id ?? null;
3159
+ }
3160
+ function extractParentSessionId(event) {
3161
+ const props = event.properties;
3162
+ const inner = event.event;
3163
+ const info = props?.info;
3164
+ const parentId = inner?.parentID ?? inner?.parentId ?? info?.parentID ?? info?.parentId;
3165
+ return parentId ?? null;
3166
+ }
2837
3167
 
2838
3168
  // src/hooks/auto-learn-hook.ts
2839
3169
  var MIN_EDITS = 1;
@@ -5810,23 +6140,23 @@ function getAgentConfigs(agentModels) {
5810
6140
  }
5811
6141
 
5812
6142
  // src/config/loader.ts
5813
- import { existsSync as existsSync23, readFileSync as readFileSync21 } from "fs";
5814
- import { join as join22 } from "path";
5815
- import { homedir as homedir2 } from "os";
6143
+ import { existsSync as existsSync27, readFileSync as readFileSync23 } from "fs";
6144
+ import { join as join26 } from "path";
6145
+ import { homedir as homedir3 } from "os";
5816
6146
  var CONFIG_FILENAME = "flowdeck.json";
5817
6147
  function getGlobalConfigDir() {
5818
- return process.env.OPENCODE_CONFIG_DIR || (process.env.XDG_CONFIG_HOME ? join22(process.env.XDG_CONFIG_HOME, "opencode") : join22(homedir2(), ".config", "opencode"));
6148
+ return process.env.OPENCODE_CONFIG_DIR || (process.env.XDG_CONFIG_HOME ? join26(process.env.XDG_CONFIG_HOME, "opencode") : join26(homedir3(), ".config", "opencode"));
5819
6149
  }
5820
6150
  function loadFlowDeckConfig(directory) {
5821
6151
  const candidates = [];
5822
6152
  if (directory) {
5823
- candidates.push(join22(directory, ".opencode", CONFIG_FILENAME));
6153
+ candidates.push(join26(directory, ".opencode", CONFIG_FILENAME));
5824
6154
  }
5825
- candidates.push(join22(getGlobalConfigDir(), CONFIG_FILENAME));
6155
+ candidates.push(join26(getGlobalConfigDir(), CONFIG_FILENAME));
5826
6156
  for (const configPath of candidates) {
5827
- if (existsSync23(configPath)) {
6157
+ if (existsSync27(configPath)) {
5828
6158
  try {
5829
- const content = readFileSync21(configPath, "utf-8");
6159
+ const content = readFileSync23(configPath, "utf-8");
5830
6160
  return JSON.parse(content);
5831
6161
  } catch {
5832
6162
  console.warn(`[flowdeck] Failed to load config from ${configPath}`);
@@ -5838,13 +6168,13 @@ function loadFlowDeckConfig(directory) {
5838
6168
  // src/index.ts
5839
6169
  function loadRulePaths() {
5840
6170
  const __dir = dirname4(fileURLToPath2(import.meta.url));
5841
- const rulesDir = join23(__dir, "..", "src", "rules");
5842
- if (!existsSync24(rulesDir))
6171
+ const rulesDir = join27(__dir, "..", "src", "rules");
6172
+ if (!existsSync28(rulesDir))
5843
6173
  return [];
5844
6174
  const paths = [];
5845
6175
  function walk(dir) {
5846
6176
  for (const entry of readdirSync3(dir, { withFileTypes: true })) {
5847
- const full = join23(dir, entry.name);
6177
+ const full = join27(dir, entry.name);
5848
6178
  if (entry.isDirectory()) {
5849
6179
  walk(full);
5850
6180
  } else if (entry.isFile() && entry.name.endsWith(".md") && entry.name !== "README.md") {
@@ -5857,8 +6187,8 @@ function loadRulePaths() {
5857
6187
  }
5858
6188
  function loadCommands() {
5859
6189
  const __dir = dirname4(fileURLToPath2(import.meta.url));
5860
- const commandsDir = join23(__dir, "..", "src", "commands");
5861
- if (!existsSync24(commandsDir))
6190
+ const commandsDir = join27(__dir, "..", "src", "commands");
6191
+ if (!existsSync28(commandsDir))
5862
6192
  return {};
5863
6193
  const commands = {};
5864
6194
  try {
@@ -5866,7 +6196,7 @@ function loadCommands() {
5866
6196
  if (!file.endsWith(".md"))
5867
6197
  continue;
5868
6198
  const name = basename(file, ".md");
5869
- const raw = readFileSync22(join23(commandsDir, file), "utf-8");
6199
+ const raw = readFileSync24(join27(commandsDir, file), "utf-8");
5870
6200
  let description;
5871
6201
  let template = raw;
5872
6202
  const fmMatch = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/);
@@ -5943,8 +6273,8 @@ var plugin = async (input, _options) => {
5943
6273
  }
5944
6274
  }
5945
6275
  }
5946
- const skillsDir = join23(dirname4(fileURLToPath2(import.meta.url)), "..", "src", "skills");
5947
- if (existsSync24(skillsDir)) {
6276
+ const skillsDir = join27(dirname4(fileURLToPath2(import.meta.url)), "..", "src", "skills");
6277
+ if (existsSync28(skillsDir)) {
5948
6278
  const cfgAny = cfg;
5949
6279
  if (!cfgAny.skills || typeof cfgAny.skills !== "object") {
5950
6280
  cfgAny.skills = { paths: [] };
@@ -5985,7 +6315,8 @@ var plugin = async (input, _options) => {
5985
6315
  "context-generator": contextGeneratorTool,
5986
6316
  "create-skill": createSkillTool,
5987
6317
  reflect: reflectTool,
5988
- "memory-search": memorySearchTool
6318
+ "memory-search": memorySearchTool,
6319
+ "memory-status": memoryStatusTool
5989
6320
  },
5990
6321
  "shell.env": shellEnvHook,
5991
6322
  "todo.updated": todoHook,
@@ -5997,36 +6328,44 @@ var plugin = async (input, _options) => {
5997
6328
  },
5998
6329
  event: async ({ event }) => {
5999
6330
  const type = event?.type ?? "";
6000
- if (type === "session.created" || type === "session.started") {
6001
- const sessionId = event?.sessionID ?? event?.sessionId ?? "";
6002
- if (sessionId) {
6003
- memoryHook.onSessionCreated(directory, sessionId, event?.prompt);
6004
- }
6005
- await sessionStartHook({ directory });
6006
- } else if (type === "message.updated" && event?.event) {
6007
- const msgEvent = event.event;
6008
- const sessionId = msgEvent?.sessionID ?? msgEvent?.sessionId ?? "";
6009
- if (sessionId) {
6010
- memoryHook.onMessageUpdated(sessionId, msgEvent.role, msgEvent.content, directory);
6011
- }
6012
- } else if (type === "session.compacted" && event?.event) {
6013
- const compactEvent = event.event;
6014
- const sessionId = compactEvent?.sessionID ?? compactEvent?.sessionId ?? "";
6015
- if (sessionId) {
6016
- memoryHook.onSessionCompact(sessionId, compactEvent.summary ?? "");
6017
- }
6018
- } else if (type === "session.deleted" && event?.event) {
6019
- const delEvent = event.event;
6020
- const sessionId = delEvent?.sessionID ?? delEvent?.sessionId ?? "";
6021
- if (sessionId) {
6022
- memoryHook.clearSession(sessionId);
6331
+ try {
6332
+ if (type === "session.created" || type === "session.started") {
6333
+ const sessionId = event?.sessionID ?? event?.sessionId ?? "";
6334
+ if (sessionId) {
6335
+ memoryHook.onSessionCreated(directory, sessionId, event?.prompt);
6336
+ }
6337
+ await sessionStartHook({ directory });
6338
+ } else if (type === "message.updated") {
6339
+ const msgEvent = event?.event ?? event;
6340
+ const sessionId = msgEvent?.sessionID ?? msgEvent?.sessionId ?? "";
6341
+ if (sessionId) {
6342
+ memoryHook.onMessageUpdated(sessionId, msgEvent.role, msgEvent.content, directory);
6343
+ }
6344
+ } else if (type === "session.compacted") {
6345
+ const compactEvent = event?.event ?? event;
6346
+ const sessionId = compactEvent?.sessionID ?? compactEvent?.sessionId ?? "";
6347
+ if (sessionId) {
6348
+ memoryHook.onSessionCompact(sessionId, compactEvent.summary ?? "");
6349
+ }
6350
+ } else if (type === "session.deleted") {
6351
+ const delEvent = event?.event ?? event;
6352
+ const sessionId = delEvent?.sessionID ?? delEvent?.sessionId ?? "";
6353
+ if (sessionId) {
6354
+ memoryHook.clearSession(sessionId);
6355
+ }
6023
6356
  }
6357
+ } catch (err) {
6358
+ console.error("[FlowDeck Memory] Event handler error:", err);
6024
6359
  }
6025
6360
  await contextMonitor.event({ event });
6026
6361
  orchestratorGuard.onEvent(event);
6027
6362
  if (type === "session.idle") {
6028
- await sessionIdleHook();
6029
- await autoLearnHook();
6363
+ try {
6364
+ await sessionIdleHook();
6365
+ await autoLearnHook();
6366
+ } finally {
6367
+ fileTracker.clear();
6368
+ }
6030
6369
  }
6031
6370
  },
6032
6371
  "tool.execute.before": async (toolInput, toolOutput) => {
@@ -6050,9 +6389,13 @@ var plugin = async (input, _options) => {
6050
6389
  },
6051
6390
  "tool.execute.after": async (toolInput, toolOutput) => {
6052
6391
  await telemetryAfterHook({ directory }, toolInput, toolOutput);
6053
- const sessionId = toolInput?.sessionID ?? toolInput?.sessionId ?? "";
6054
- if (sessionId && toolInput?.tool) {
6055
- memoryHook.onToolExecuted(sessionId, toolInput.tool, toolInput, toolOutput?.output ?? null, directory);
6392
+ try {
6393
+ const sessionId = toolInput?.sessionID ?? toolInput?.sessionId ?? "";
6394
+ if (sessionId && toolInput?.tool) {
6395
+ memoryHook.onToolExecuted(sessionId, toolInput.tool, toolInput, toolOutput?.output ?? null, directory);
6396
+ }
6397
+ } catch (err) {
6398
+ console.error("[FlowDeck Memory] Tool execution error:", err);
6056
6399
  }
6057
6400
  await contextMonitor["tool.execute.after"](toolInput, toolOutput);
6058
6401
  }