@dv.nghiem/flowdeck 0.3.9 → 0.4.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 (129) hide show
  1. package/README.md +13 -21
  2. package/dist/agents/code-explorer.d.ts.map +1 -1
  3. package/dist/agents/mapper.d.ts.map +1 -1
  4. package/dist/agents/orchestrator.d.ts.map +1 -1
  5. package/dist/agents/planner.d.ts.map +1 -1
  6. package/dist/agents/specialist.d.ts.map +1 -1
  7. package/dist/dashboard/server.mjs +12 -2
  8. package/dist/hooks/compaction-hook.d.ts +1 -2
  9. package/dist/hooks/compaction-hook.d.ts.map +1 -1
  10. package/dist/hooks/file-tracker.d.ts +6 -0
  11. package/dist/hooks/file-tracker.d.ts.map +1 -1
  12. package/dist/hooks/notifications.d.ts +73 -8
  13. package/dist/hooks/notifications.d.ts.map +1 -1
  14. package/dist/hooks/notifications.test.d.ts +14 -0
  15. package/dist/hooks/notifications.test.d.ts.map +1 -0
  16. package/dist/hooks/session-idle-hook.d.ts +5 -3
  17. package/dist/hooks/session-idle-hook.d.ts.map +1 -1
  18. package/dist/hooks/session-start.d.ts.map +1 -1
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +822 -796
  21. package/dist/lib/completion-validator.d.ts +51 -0
  22. package/dist/lib/completion-validator.d.ts.map +1 -0
  23. package/dist/lib/recommended-question.d.ts +24 -0
  24. package/dist/lib/recommended-question.d.ts.map +1 -0
  25. package/dist/lib/research-gate.d.ts +97 -0
  26. package/dist/lib/research-gate.d.ts.map +1 -0
  27. package/dist/lib/research-gate.test.d.ts +2 -0
  28. package/dist/lib/research-gate.test.d.ts.map +1 -0
  29. package/dist/mcp/index.d.ts +12 -2
  30. package/dist/mcp/index.d.ts.map +1 -1
  31. package/dist/services/codegraph.d.ts +36 -0
  32. package/dist/services/codegraph.d.ts.map +1 -0
  33. package/dist/services/codegraph.test.d.ts +2 -0
  34. package/dist/services/codegraph.test.d.ts.map +1 -0
  35. package/dist/services/question-guard.d.ts +4 -0
  36. package/dist/services/question-guard.d.ts.map +1 -1
  37. package/dist/services/recommended-question.test.d.ts +2 -0
  38. package/dist/services/recommended-question.test.d.ts.map +1 -0
  39. package/dist/services/supervisor-binding.d.ts +3 -1
  40. package/dist/services/supervisor-binding.d.ts.map +1 -1
  41. package/dist/tools/codebase-index.d.ts +30 -0
  42. package/dist/tools/codebase-index.d.ts.map +1 -0
  43. package/dist/tools/codebase-index.test.d.ts +2 -0
  44. package/dist/tools/codebase-index.test.d.ts.map +1 -0
  45. package/dist/tools/codegraph-tool.d.ts +3 -0
  46. package/dist/tools/codegraph-tool.d.ts.map +1 -0
  47. package/dist/tools/planning-state-lib.d.ts +23 -0
  48. package/dist/tools/planning-state-lib.d.ts.map +1 -1
  49. package/docs/agents/index.md +154 -0
  50. package/docs/commands/fd-ask.md +71 -39
  51. package/docs/commands/fd-checkpoint.md +63 -8
  52. package/docs/commands/fd-deploy-check.md +166 -9
  53. package/docs/commands/fd-design.md +101 -0
  54. package/docs/commands/fd-discuss.md +87 -20
  55. package/docs/commands/fd-doctor.md +100 -13
  56. package/docs/commands/fd-done.md +215 -0
  57. package/docs/commands/fd-execute.md +104 -0
  58. package/docs/commands/fd-fix-bug.md +144 -24
  59. package/docs/commands/fd-map-codebase.md +85 -21
  60. package/docs/commands/fd-multi-repo.md +155 -40
  61. package/docs/commands/fd-new-feature.md +63 -19
  62. package/docs/commands/fd-plan.md +80 -27
  63. package/docs/commands/fd-quick.md +143 -29
  64. package/docs/commands/fd-reflect.md +81 -13
  65. package/docs/commands/fd-resume.md +65 -8
  66. package/docs/commands/fd-status.md +80 -12
  67. package/docs/commands/fd-suggest.md +114 -0
  68. package/docs/commands/fd-translate-intent.md +69 -9
  69. package/docs/commands/fd-verify.md +71 -14
  70. package/docs/commands/fd-write-docs.md +121 -8
  71. package/docs/concepts/architecture.md +163 -0
  72. package/docs/concepts/governance.md +242 -0
  73. package/docs/concepts/intelligence.md +145 -0
  74. package/docs/concepts/multi-repo.md +227 -0
  75. package/docs/concepts/workflows.md +205 -0
  76. package/docs/configuration/index.md +208 -0
  77. package/docs/configuration/opencode-settings.md +98 -0
  78. package/docs/getting-started/first-project.md +126 -0
  79. package/docs/getting-started/installation.md +73 -0
  80. package/docs/getting-started/quick-start.md +74 -0
  81. package/docs/index.md +36 -72
  82. package/docs/reference/hooks.md +176 -0
  83. package/docs/reference/rules.md +109 -0
  84. package/docs/skills/code-review.md +47 -0
  85. package/docs/skills/index.md +148 -0
  86. package/docs/skills/planning.md +39 -0
  87. package/package.json +1 -1
  88. package/src/commands/fd-discuss.md +74 -10
  89. package/src/commands/fd-done.md +196 -0
  90. package/src/commands/fd-execute.md +43 -6
  91. package/src/commands/fd-fix-bug.md +43 -6
  92. package/src/commands/fd-map-codebase.md +99 -19
  93. package/src/commands/fd-new-feature.md +14 -5
  94. package/src/commands/fd-plan.md +38 -1
  95. package/src/commands/fd-quick.md +1 -1
  96. package/src/commands/fd-resume.md +1 -1
  97. package/src/commands/fd-status.md +1 -1
  98. package/src/commands/fd-verify.md +16 -2
  99. package/src/commands/fd-write-docs.md +30 -5
  100. package/src/skills/context-load/SKILL.md +1 -1
  101. package/dist/hooks/memory-hook.d.ts +0 -28
  102. package/dist/hooks/memory-hook.d.ts.map +0 -1
  103. package/dist/services/memory-store.d.ts +0 -73
  104. package/dist/services/memory-store.d.ts.map +0 -1
  105. package/dist/services/memory-store.test.d.ts +0 -2
  106. package/dist/services/memory-store.test.d.ts.map +0 -1
  107. package/dist/tools/memory-search.d.ts +0 -3
  108. package/dist/tools/memory-search.d.ts.map +0 -1
  109. package/dist/tools/memory-status.d.ts +0 -3
  110. package/dist/tools/memory-status.d.ts.map +0 -1
  111. package/docs/USER_GUIDE.md +0 -20
  112. package/docs/agents.md +0 -544
  113. package/docs/best-practices.md +0 -47
  114. package/docs/commands/fd-new-project.md +0 -24
  115. package/docs/commands.md +0 -557
  116. package/docs/configuration.md +0 -325
  117. package/docs/design-first-workflow.md +0 -94
  118. package/docs/feature-integration-architecture.md +0 -227
  119. package/docs/installation.md +0 -123
  120. package/docs/intelligence.md +0 -370
  121. package/docs/memory.md +0 -69
  122. package/docs/multi-repo.md +0 -201
  123. package/docs/notifications.md +0 -170
  124. package/docs/optimization-baseline.md +0 -21
  125. package/docs/quick-start.md +0 -197
  126. package/docs/rules.md +0 -432
  127. package/docs/skills.md +0 -417
  128. package/docs/workflows.md +0 -134
  129. package/src/commands/fd-new-project.md +0 -114
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // src/index.ts
2
- import { readdirSync as readdirSync3, readFileSync as readFileSync23, existsSync as existsSync27 } from "fs";
3
- import { join as join26, basename } from "path";
2
+ import { readdirSync as readdirSync3, readFileSync as readFileSync24, existsSync as existsSync26 } from "fs";
3
+ import { join as join25, basename } from "path";
4
4
  import { dirname as dirname4 } from "path";
5
5
  import { fileURLToPath as fileURLToPath2 } from "url";
6
6
 
@@ -170,7 +170,12 @@ function readPlanningState(dir) {
170
170
  last_action: "",
171
171
  next_action: "",
172
172
  blockers: [],
173
- tdd: undefined
173
+ tdd: undefined,
174
+ lastUpdatedAt: "",
175
+ lastUpdatedBy: "",
176
+ lastUpdatedPhase: 1,
177
+ summaryVersion: 0,
178
+ freshnessStatus: "unknown"
174
179
  };
175
180
  }
176
181
  const content = readFileSync2(sp, "utf-8");
@@ -191,7 +196,12 @@ function readPlanningState(dir) {
191
196
  last_action: parsed.last_action || "",
192
197
  next_action: parsed.next_action || "",
193
198
  blockers: parsed.blockers || [],
194
- tdd: parseTDDState(parsed)
199
+ tdd: parseTDDState(parsed),
200
+ lastUpdatedAt: parsed.lastUpdatedAt || "",
201
+ lastUpdatedBy: parsed.lastUpdatedBy || "",
202
+ lastUpdatedPhase: parsed.lastUpdatedPhase || 1,
203
+ summaryVersion: parsed.summaryVersion || 0,
204
+ freshnessStatus: parsed.freshnessStatus || "unknown"
195
205
  };
196
206
  }
197
207
  function parseTDDState(parsed) {
@@ -1701,657 +1711,446 @@ var reflectTool = tool15({
1701
1711
  }
1702
1712
  });
1703
1713
 
1704
- // src/tools/memory-search.ts
1714
+ // src/tools/codegraph-tool.ts
1705
1715
  import { tool as tool16 } from "@opencode-ai/plugin";
1706
1716
 
1707
- // src/services/memory-store.ts
1708
- import { Database } from "bun:sqlite";
1709
- import { existsSync as existsSync15, mkdirSync as mkdirSync10 } from "fs";
1717
+ // src/services/codegraph.ts
1718
+ import { spawnSync } from "child_process";
1719
+ import { existsSync as existsSync15, readFileSync as readFileSync14, writeFileSync as writeFileSync14, mkdirSync as mkdirSync10 } from "fs";
1710
1720
  import { join as join15 } from "path";
1711
- import { homedir } from "os";
1712
- function resolveMemoryDir() {
1713
- return process.env.FLOWDECK_MEMORY_DIR ?? join15(homedir(), ".flowdeck-memory");
1714
- }
1715
- var JS_RETRY_COUNT = 3;
1716
- var JS_RETRY_BASE_MS = 50;
1717
- var db = null;
1718
- function debugLog(msg) {
1719
- if (process.env.FLOWDECK_MEMORY_DEBUG) {
1720
- console.error(`[FlowDeck Memory] ${msg}`);
1721
- }
1722
- }
1723
- function getDb() {
1724
- if (!db) {
1725
- const dir = resolveMemoryDir();
1726
- if (!existsSync15(dir))
1727
- mkdirSync10(dir, { recursive: true });
1728
- const dbPath = join15(dir, "memory.db");
1729
- db = new Database(dbPath);
1730
- debugLog(`DB opened: ${dbPath}`);
1731
- initializeSchema(db);
1732
- }
1733
- return db;
1734
- }
1735
- function initializeSchema(database) {
1736
- database.run("PRAGMA journal_mode = WAL");
1737
- database.run("PRAGMA busy_timeout = 5000");
1738
- database.run("PRAGMA synchronous = NORMAL");
1739
- database.run("PRAGMA wal_autocheckpoint = 1000");
1740
- const schema = `
1741
- CREATE TABLE IF NOT EXISTS sessions (
1742
- id INTEGER PRIMARY KEY AUTOINCREMENT,
1743
- content_session_id TEXT NOT NULL UNIQUE,
1744
- project TEXT NOT NULL,
1745
- directory TEXT NOT NULL,
1746
- created_at TEXT NOT NULL DEFAULT (datetime('now')),
1747
- last_active_at TEXT NOT NULL DEFAULT (datetime('now')),
1748
- summary TEXT,
1749
- prompt_count INTEGER DEFAULT 0
1750
- );
1751
-
1752
- CREATE TABLE IF NOT EXISTS observations (
1753
- id INTEGER PRIMARY KEY AUTOINCREMENT,
1754
- session_id INTEGER NOT NULL,
1755
- tool_name TEXT NOT NULL,
1756
- tool_input TEXT,
1757
- tool_response TEXT,
1758
- directory TEXT NOT NULL,
1759
- created_at TEXT NOT NULL DEFAULT (datetime('now')),
1760
- FOREIGN KEY (session_id) REFERENCES sessions(id)
1761
- );
1762
-
1763
- CREATE TABLE IF NOT EXISTS summaries (
1764
- id INTEGER PRIMARY KEY AUTOINCREMENT,
1765
- session_id INTEGER NOT NULL UNIQUE,
1766
- content TEXT NOT NULL,
1767
- metadata TEXT,
1768
- created_at TEXT NOT NULL DEFAULT (datetime('now')),
1769
- FOREIGN KEY (session_id) REFERENCES sessions(id)
1770
- );
1771
-
1772
- CREATE INDEX IF NOT EXISTS idx_observations_session ON observations(session_id);
1773
- CREATE INDEX IF NOT EXISTS idx_observations_directory ON observations(directory);
1774
- CREATE INDEX IF NOT EXISTS idx_observations_tool ON observations(tool_name);
1775
- CREATE INDEX IF NOT EXISTS idx_sessions_project ON sessions(project);
1776
- CREATE INDEX IF NOT EXISTS idx_sessions_directory ON sessions(directory);
1777
- `;
1778
- database.run(schema);
1779
- const summaryColumns = database.prepare("PRAGMA table_info(summaries)").all().map((c) => c.name);
1780
- if (!summaryColumns.includes("metadata")) {
1781
- database.run("ALTER TABLE summaries ADD COLUMN metadata TEXT");
1782
- debugLog("Migrated summaries table: added metadata column");
1783
- }
1784
- }
1785
- function isBusyError(err) {
1786
- if (!err || typeof err !== "object")
1787
- return false;
1788
- const e = err;
1789
- return e.code === "SQLITE_BUSY" || (e.message?.includes("database is locked") ?? false);
1721
+ var CODEGRAPH_META_FILE = "CODEGRAPH.md";
1722
+ var MAX_FRESHNESS_MS = 30 * 60 * 1000;
1723
+ function metaPath(dir) {
1724
+ return join15(codebaseDir(dir), CODEGRAPH_META_FILE);
1790
1725
  }
1791
- function sleepSync(ms) {
1726
+ function isCodegraphInstalled() {
1792
1727
  try {
1793
- Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
1728
+ const result = spawnSync("codegraph", ["--version"], {
1729
+ encoding: "utf-8",
1730
+ timeout: 5000,
1731
+ stdio: "pipe"
1732
+ });
1733
+ return result.status === 0;
1794
1734
  } catch {
1795
- const end = Date.now() + ms;
1796
- while (Date.now() < end) {}
1735
+ return false;
1797
1736
  }
1798
1737
  }
1799
- function executeWrite(fn, context) {
1800
- for (let attempt = 0;attempt <= JS_RETRY_COUNT; attempt++) {
1801
- const start = Date.now();
1802
- try {
1803
- const result = fn();
1804
- const duration = Date.now() - start;
1805
- if (attempt > 0) {
1806
- debugLog(`${context}: succeeded after ${attempt} JS retr${attempt === 1 ? "y" : "ies"} (${duration}ms)`);
1807
- } else {
1808
- debugLog(`${context}: completed in ${duration}ms`);
1809
- }
1810
- return result;
1811
- } catch (err) {
1812
- if (isBusyError(err) && attempt < JS_RETRY_COUNT) {
1813
- const delay = JS_RETRY_BASE_MS * (attempt + 1);
1814
- debugLog(`${context}: SQLITE_BUSY — JS retry ${attempt + 1}/${JS_RETRY_COUNT} after ${delay}ms`);
1815
- sleepSync(delay);
1816
- continue;
1817
- }
1818
- throw err;
1819
- }
1820
- }
1821
- throw new Error(`${context}: exhausted all retries`);
1738
+ function isCodegraphIndexed(dir) {
1739
+ return existsSync15(join15(dir, ".codegraph", "codegraph.db"));
1822
1740
  }
1823
- function serializeToolInput(input) {
1824
- if (!input)
1825
- return null;
1826
- try {
1827
- return JSON.stringify(input);
1828
- } catch {
1829
- return String(input);
1741
+ function readCodegraphMeta(dir) {
1742
+ const path = metaPath(dir);
1743
+ if (!existsSync15(path)) {
1744
+ return {
1745
+ installed: false,
1746
+ indexed: false,
1747
+ lastIndexedAt: "",
1748
+ lastIndexedRevision: "",
1749
+ lastIndexedBy: "",
1750
+ freshnessStatus: "unknown",
1751
+ installLog: "",
1752
+ indexLog: ""
1753
+ };
1830
1754
  }
1831
- }
1832
- function parseToolInput(input) {
1833
- if (!input)
1834
- return null;
1835
1755
  try {
1836
- return JSON.parse(input);
1756
+ const content = readFileSync14(path, "utf-8");
1757
+ return parseCodegraphMeta(content);
1837
1758
  } catch {
1838
- return null;
1759
+ return {
1760
+ installed: false,
1761
+ indexed: false,
1762
+ lastIndexedAt: "",
1763
+ lastIndexedRevision: "",
1764
+ lastIndexedBy: "",
1765
+ freshnessStatus: "unknown",
1766
+ installLog: "",
1767
+ indexLog: ""
1768
+ };
1839
1769
  }
1840
1770
  }
1841
- function initSession(contentSessionId, project, directory) {
1842
- const database = getDb();
1843
- const now = new Date().toISOString();
1844
- const existing = database.prepare("SELECT * FROM sessions WHERE content_session_id = ?").get(contentSessionId);
1845
- if (existing) {
1846
- database.prepare("UPDATE sessions SET last_active_at = ?, prompt_count = prompt_count + 1 WHERE id = ?").run(now, existing.id);
1847
- return { ...existing, last_active_at: now, prompt_count: (existing.prompt_count || 0) + 1 };
1771
+ function parseCodegraphMeta(content) {
1772
+ const result = {};
1773
+ for (const line of content.split(`
1774
+ `)) {
1775
+ if (line.startsWith("#") || !line.trim())
1776
+ continue;
1777
+ const stripped = line.replace(/\*\*/g, "");
1778
+ const m = stripped.match(/^([a-zA-Z_][a-zA-Z0-9_]*):\s*(.*)/);
1779
+ if (!m)
1780
+ continue;
1781
+ const key = m[1];
1782
+ const value = m[2].trim().replace(/^["']|["']$/g, "");
1783
+ switch (key) {
1784
+ case "installed":
1785
+ result.installed = value === "true";
1786
+ break;
1787
+ case "indexed":
1788
+ result.indexed = value === "true";
1789
+ break;
1790
+ case "freshnessStatus":
1791
+ result.freshnessStatus = value;
1792
+ break;
1793
+ case "lastIndexedAt":
1794
+ result.lastIndexedAt = value;
1795
+ break;
1796
+ case "lastIndexedRevision":
1797
+ result.lastIndexedRevision = value;
1798
+ break;
1799
+ case "lastIndexedBy":
1800
+ result.lastIndexedBy = value;
1801
+ break;
1802
+ case "installLog":
1803
+ result.installLog = value;
1804
+ break;
1805
+ case "indexLog":
1806
+ result.indexLog = value;
1807
+ break;
1808
+ }
1848
1809
  }
1849
- const result = database.prepare("INSERT INTO sessions (content_session_id, project, directory, created_at, last_active_at, prompt_count) VALUES (?, ?, ?, ?, ?, ?)").run(contentSessionId, project, directory, now, now, 1);
1850
- return {
1851
- id: result.lastInsertRowid,
1852
- content_session_id: contentSessionId,
1853
- project,
1854
- directory,
1855
- created_at: now,
1856
- last_active_at: now,
1857
- prompt_count: 1
1858
- };
1859
- }
1860
- function storeObservation(sessionId, toolName, toolInput, toolResponse, directory) {
1861
- const database = getDb();
1862
- const now = new Date().toISOString();
1863
- const serializedInput = serializeToolInput(toolInput);
1864
- const truncatedResponse = toolResponse ? toolResponse.slice(0, 1e4) : null;
1865
- const result = executeWrite(database.transaction(() => {
1866
- const r = database.prepare("INSERT INTO observations (session_id, tool_name, tool_input, tool_response, directory, created_at) VALUES (?, ?, ?, ?, ?, ?)").run(sessionId, toolName, serializedInput, truncatedResponse, directory, now);
1867
- database.prepare("UPDATE sessions SET last_active_at = ? WHERE id = ?").run(now, sessionId);
1868
- return r;
1869
- }), `storeObservation(${toolName})`);
1870
- return {
1871
- id: result.lastInsertRowid,
1872
- session_id: sessionId,
1873
- tool_name: toolName,
1874
- tool_input: parseToolInput(serializedInput),
1875
- tool_response: truncatedResponse,
1876
- directory,
1877
- created_at: now
1878
- };
1879
- }
1880
- function storeSummary(sessionId, content, metadata) {
1881
- const database = getDb();
1882
- const now = new Date().toISOString();
1883
- const serializedMetadata = metadata ? JSON.stringify(metadata) : null;
1884
- const id = executeWrite(database.transaction(() => {
1885
- database.prepare("INSERT OR REPLACE INTO summaries (session_id, content, metadata, created_at) VALUES (?, ?, ?, ?)").run(sessionId, content, serializedMetadata, now);
1886
- database.prepare("UPDATE sessions SET summary = ? WHERE id = ?").run(content.slice(0, 2000), sessionId);
1887
- return database.prepare("SELECT last_insert_rowid() as id").get().id;
1888
- }), `storeSummary(session=${sessionId})`);
1889
- debugLog(`storeSummary: wrote ${content.length} chars${metadata ? ` + ${JSON.stringify(metadata).length}B metadata` : ""} for session ${sessionId}`);
1890
- return {
1891
- id,
1892
- session_id: sessionId,
1893
- content,
1894
- metadata: metadata ?? null,
1895
- created_at: now
1896
- };
1897
- }
1898
- function getRecentSessions(directory, limit = 5) {
1899
- const database = getDb();
1900
- return database.prepare(`SELECT * FROM sessions
1901
- WHERE directory = ?
1902
- ORDER BY last_active_at DESC
1903
- LIMIT ?`).all(directory, limit);
1904
- }
1905
- function getObservationsForSession(sessionId) {
1906
- const database = getDb();
1907
- const observations = database.prepare("SELECT * FROM observations WHERE session_id = ? ORDER BY created_at ASC").all(sessionId);
1908
- return observations.map((obs) => ({
1909
- ...obs,
1910
- tool_input: parseToolInput(obs.tool_input)
1911
- }));
1912
- }
1913
- function getSessionSummary(sessionId) {
1914
- const database = getDb();
1915
- const row = database.prepare("SELECT * FROM summaries WHERE session_id = ?").get(sessionId);
1916
- if (!row)
1917
- return null;
1918
1810
  return {
1919
- ...row,
1920
- metadata: row.metadata ? JSON.parse(row.metadata) : null
1811
+ installed: result.installed ?? false,
1812
+ indexed: result.indexed ?? false,
1813
+ lastIndexedAt: result.lastIndexedAt ?? "",
1814
+ lastIndexedRevision: result.lastIndexedRevision ?? "",
1815
+ lastIndexedBy: result.lastIndexedBy ?? "",
1816
+ freshnessStatus: result.freshnessStatus ?? "unknown",
1817
+ installLog: result.installLog ?? "",
1818
+ indexLog: result.indexLog ?? ""
1921
1819
  };
1922
1820
  }
1923
- function getSessionByContentSessionId(contentSessionId) {
1924
- const database = getDb();
1925
- return database.prepare("SELECT * FROM sessions WHERE content_session_id = ?").get(contentSessionId) || null;
1926
- }
1927
- function getRecentObservations(directory, limit = 50) {
1928
- const database = getDb();
1929
- const rows = database.prepare(`SELECT o.*, s.project, s.content_session_id, s.created_at as session_created
1930
- FROM observations o
1931
- JOIN sessions s ON o.session_id = s.id
1932
- WHERE o.directory = ?
1933
- ORDER BY o.created_at DESC
1934
- LIMIT ?`).all(directory, limit);
1935
- return rows.map((row) => ({
1936
- observation: {
1937
- ...row,
1938
- tool_input: parseToolInput(row.tool_input)
1939
- },
1940
- session: {
1941
- content_session_id: row.content_session_id,
1942
- project: row.project,
1943
- directory,
1944
- created_at: row.session_created
1945
- }
1946
- }));
1947
- }
1948
- function searchObservations(directory, query, limit = 10) {
1949
- const database = getDb();
1950
- const pattern = `%${query}%`;
1951
- const rows = database.prepare(`SELECT o.*, s.project, s.content_session_id, s.created_at as session_created
1952
- FROM observations o
1953
- JOIN sessions s ON o.session_id = s.id
1954
- WHERE o.directory = ? AND (o.tool_name LIKE ? OR o.tool_input LIKE ? OR o.tool_response LIKE ?)
1955
- ORDER BY o.created_at DESC
1956
- LIMIT ?`).all(directory, pattern, pattern, pattern, limit);
1957
- return rows.map((row) => ({
1958
- observation: {
1959
- ...row,
1960
- tool_input: parseToolInput(row.tool_input)
1961
- },
1962
- session: {
1963
- content_session_id: row.content_session_id,
1964
- project: row.project,
1965
- directory,
1966
- created_at: row.session_created
1967
- }
1968
- }));
1821
+ function writeCodegraphMeta(dir, meta) {
1822
+ const base = codebaseDir(dir);
1823
+ if (!existsSync15(base))
1824
+ mkdirSync10(base, { recursive: true });
1825
+ const lines = [
1826
+ "# Codegraph Metadata",
1827
+ "",
1828
+ `**installed:** ${meta.installed}`,
1829
+ `**indexed:** ${meta.indexed}`,
1830
+ `**lastIndexedAt:** ${meta.lastIndexedAt}`,
1831
+ `**lastIndexedRevision:** ${meta.lastIndexedRevision}`,
1832
+ `**lastIndexedBy:** ${meta.lastIndexedBy}`,
1833
+ `**freshnessStatus:** ${meta.freshnessStatus}`,
1834
+ `**installLog:** ${meta.installLog}`,
1835
+ `**indexLog:** ${meta.indexLog}`
1836
+ ];
1837
+ writeFileSync14(metaPath(dir), lines.join(`
1838
+ `), "utf-8");
1839
+ }
1840
+ function isCodegraphFresh(dir, maxAgeMs = MAX_FRESHNESS_MS) {
1841
+ const meta = readCodegraphMeta(dir);
1842
+ if (!meta.indexed)
1843
+ return false;
1844
+ if (meta.freshnessStatus === "stale")
1845
+ return false;
1846
+ if (!meta.lastIndexedAt)
1847
+ return false;
1848
+ const age = Date.now() - new Date(meta.lastIndexedAt).getTime();
1849
+ return age < maxAgeMs;
1969
1850
  }
1970
- function getContextForDirectory(directory, maxObservations = 20) {
1971
- const recentObs = getRecentObservations(directory, maxObservations);
1972
- if (recentObs.length === 0)
1851
+ function getCurrentRevision(dir) {
1852
+ try {
1853
+ const result = spawnSync("git", ["rev-parse", "--short", "HEAD"], {
1854
+ cwd: dir,
1855
+ encoding: "utf-8",
1856
+ timeout: 5000,
1857
+ stdio: "pipe"
1858
+ });
1859
+ return result.status === 0 ? (result.stdout ?? "").trim() : "";
1860
+ } catch {
1973
1861
  return "";
1974
- const lines = ["## Recent Context"];
1975
- for (const { observation, session } of recentObs) {
1976
- const date = observation.created_at ? new Date(observation.created_at).toLocaleDateString() : "unknown";
1977
- lines.push(`
1978
- ### [${date}] ${session.project} - ${observation.tool_name}`);
1979
- if (observation.tool_input && Object.keys(observation.tool_input).length > 0) {
1980
- const preview = JSON.stringify(observation.tool_input).slice(0, 200);
1981
- lines.push(`Input: ${preview}${preview.length >= 200 ? "..." : ""}`);
1982
- }
1983
- if (observation.tool_response) {
1984
- const preview = observation.tool_response.slice(0, 300);
1985
- lines.push(`Output: ${preview}${observation.tool_response.length > 300 ? "..." : ""}`);
1986
- }
1987
1862
  }
1988
- const summaryRows = getDb().prepare(`SELECT su.* FROM summaries su
1989
- JOIN sessions s ON su.session_id = s.id
1990
- WHERE s.directory = ?
1991
- ORDER BY su.created_at DESC
1992
- LIMIT 3`).all(directory);
1993
- const summaries = summaryRows.map((r) => ({
1994
- ...r,
1995
- metadata: r.metadata ? JSON.parse(r.metadata) : null
1996
- }));
1997
- if (summaries.length > 0) {
1998
- lines.push(`
1999
- ## Session Summaries`);
2000
- for (const sum of summaries) {
2001
- const date = sum.created_at ? new Date(sum.created_at).toLocaleDateString() : "unknown";
2002
- lines.push(`
2003
- ### [${date}]`);
2004
- lines.push(sum.content.slice(0, 2000));
2005
- }
2006
- }
2007
- return lines.join(`
2008
- `);
2009
1863
  }
2010
- function getDbSettings() {
2011
- const database = getDb();
2012
- const journalMode = database.prepare("PRAGMA journal_mode").get().journal_mode;
2013
- const busyTimeout = database.prepare("PRAGMA busy_timeout").get().timeout;
2014
- const synchronous = database.prepare("PRAGMA synchronous").get().synchronous;
2015
- const walAutocheckpoint = database.prepare("PRAGMA wal_autocheckpoint").get().wal_autocheckpoint;
2016
- return { journal_mode: journalMode, busy_timeout: busyTimeout, synchronous, wal_autocheckpoint: walAutocheckpoint };
2017
- }
2018
-
2019
- // src/tools/memory-search.ts
2020
- var memorySearchTool = tool16({
2021
- description: "Search FlowDeck memory for past observations, sessions, and context. Use to recall what was worked on previously.",
2022
- args: {
2023
- query: tool16.schema.string().optional().describe("Search query for memory (searches tool names, inputs, and outputs)"),
2024
- session_id: tool16.schema.string().optional().describe("Specific session ID to retrieve observations from"),
2025
- limit: tool16.schema.number().optional().describe("Maximum number of results (default: 10)")
2026
- },
2027
- async execute(args, context) {
2028
- const directory = context.directory ?? process.cwd();
2029
- const limit = args.limit ?? 10;
2030
- if (args.session_id) {
2031
- const sessions2 = getRecentSessions(directory, 100);
2032
- const targetSession = sessions2.find((s) => String(s.id) === args.session_id || s.content_session_id === args.session_id);
2033
- if (!targetSession) {
2034
- return JSON.stringify({ error: "Session not found", session_id: args.session_id });
2035
- }
2036
- const observations = getObservationsForSession(targetSession.id);
2037
- const summary = getSessionSummary(targetSession.id);
2038
- return JSON.stringify({
2039
- session: targetSession,
2040
- summary: summary ? {
2041
- content: summary.content,
2042
- metadata: summary.metadata,
2043
- created_at: summary.created_at
2044
- } : null,
2045
- observations: observations.map((o) => ({
2046
- tool_name: o.tool_name,
2047
- tool_input: o.tool_input,
2048
- tool_response: o.tool_response ? o.tool_response.slice(0, 500) + (o.tool_response.length > 500 ? "..." : "") : null,
2049
- created_at: o.created_at
2050
- }))
2051
- });
2052
- }
2053
- if (args.query) {
2054
- const results = searchObservations(directory, args.query, limit);
2055
- if (results.length === 0) {
2056
- return JSON.stringify({ message: `No results found for "${args.query}"`, results: [] });
2057
- }
2058
- return JSON.stringify({
2059
- query: args.query,
2060
- count: results.length,
2061
- results: results.map(({ observation, session }) => ({
2062
- tool_name: observation.tool_name,
2063
- tool_input: observation.tool_input,
2064
- tool_response: observation.tool_response ? observation.tool_response.slice(0, 300) + (observation.tool_response.length > 300 ? "..." : "") : null,
2065
- project: session.project,
2066
- date: observation.created_at
2067
- }))
2068
- });
2069
- }
2070
- const sessions = getRecentSessions(directory, limit);
2071
- if (sessions.length === 0) {
2072
- return JSON.stringify({ message: "No previous sessions found in this directory", sessions: [] });
2073
- }
2074
- return JSON.stringify({
2075
- message: "Recent sessions",
2076
- count: sessions.length,
2077
- sessions: sessions.map((s) => ({
2078
- id: s.id,
2079
- content_session_id: s.content_session_id,
2080
- project: s.project,
2081
- created_at: s.created_at,
2082
- last_active_at: s.last_active_at,
2083
- summary: s.summary
2084
- }))
1864
+ function getChangedFilesSince(dir, revision) {
1865
+ if (!revision)
1866
+ return [];
1867
+ try {
1868
+ const result = spawnSync("git", ["diff", "--name-only", revision, "HEAD"], {
1869
+ cwd: dir,
1870
+ encoding: "utf-8",
1871
+ timeout: 5000,
1872
+ stdio: "pipe"
2085
1873
  });
1874
+ if (result.status !== 0)
1875
+ return [];
1876
+ return (result.stdout ?? "").trim().split(`
1877
+ `).filter(Boolean);
1878
+ } catch {
1879
+ return [];
2086
1880
  }
2087
- });
2088
-
2089
- // src/tools/memory-status.ts
2090
- import { tool as tool17 } from "@opencode-ai/plugin";
2091
- import { existsSync as existsSync16 } from "fs";
2092
- import { join as join16 } from "path";
2093
- import { homedir as homedir2 } from "os";
2094
- function resolveDbPath() {
2095
- return join16(process.env.FLOWDECK_MEMORY_DIR ?? join16(homedir2(), ".flowdeck-memory"), "memory.db");
2096
- }
2097
- var memoryStatusTool = tool17({
2098
- description: "Check FlowDeck memory database status, statistics, and recent sessions",
2099
- args: {},
2100
- async execute(_args, context) {
2101
- const directory = context?.directory ?? process.cwd();
2102
- const dbPath = resolveDbPath();
2103
- try {
2104
- const exists = existsSync16(dbPath);
2105
- if (!exists) {
2106
- return JSON.stringify({
2107
- database_exists: false,
2108
- path: dbPath,
2109
- status: "NOT_INITIALIZED"
2110
- }, null, 2);
2111
- }
2112
- const settings = getDbSettings();
2113
- const recentSessions = getRecentSessions(directory, 5);
2114
- const sessionStats = recentSessions.map((s) => {
2115
- const observations = getObservationsForSession(s.id);
2116
- const summary = getSessionSummary(s.id);
2117
- return {
2118
- project: s.project,
2119
- directory: s.directory,
2120
- content_session_id: s.content_session_id,
2121
- observations_in_session: observations.length,
2122
- last_active: s.last_active_at,
2123
- prompt_count: s.prompt_count,
2124
- has_summary: !!summary,
2125
- summary_length: summary?.content.length ?? 0,
2126
- summary_preview: summary?.content.slice(0, 200) ?? null,
2127
- handoff_metadata: summary?.metadata ?? null
2128
- };
2129
- });
2130
- return JSON.stringify({
2131
- database_exists: true,
2132
- path: dbPath,
2133
- status: "ACTIVE",
2134
- pragma_settings: settings,
2135
- recent_sessions_in_directory: sessionStats
2136
- }, null, 2);
2137
- } catch (err) {
2138
- return JSON.stringify({
2139
- status: "ERROR",
2140
- error: String(err),
2141
- path: dbPath
2142
- }, null, 2);
2143
- }
2144
- }
2145
- });
2146
-
2147
- // src/hooks/memory-hook.ts
2148
- var MAX_TOOL_RESPONSE = 1e4;
2149
- var MAX_SUMMARY_STORAGE = 50000;
2150
- var activeSessions = new Map;
2151
- function extractProjectFromDirectory(directory) {
2152
- const parts = directory.split("/");
2153
- return parts[parts.length - 1] || "unknown";
2154
- }
2155
- function truncate(str, max) {
2156
- if (!str || str.length <= max)
2157
- return str || "";
2158
- return str.slice(0, max);
2159
- }
2160
- function buildHandoffMetadata(sessionId, directory, summaryText, observations) {
2161
- const fileTools = new Set(["edit", "create", "view", "read", "hash-edit", "str-replace-editor"]);
2162
- const importantFilesSet = new Set;
2163
- for (const obs of observations) {
2164
- if (fileTools.has(obs.tool_name) && obs.tool_input) {
2165
- const path = obs.tool_input.path;
2166
- if (path)
2167
- importantFilesSet.add(path);
2168
- }
2169
- }
2170
- const toolNamesUsed = [...new Set(observations.map((o) => o.tool_name).filter((t) => t !== "assistant_message"))];
2171
- function extractBullets(text) {
2172
- return text.split(`
2173
- `).filter((l) => /^\s*[-*]/.test(l)).map((l) => l.replace(/^\s*[-*]\s+/, "").trim()).filter(Boolean);
2174
- }
2175
- const sections = {};
2176
- let currentSection = "";
2177
- const currentLines = [];
2178
- for (const line of summaryText.split(`
2179
- `)) {
2180
- const header = line.match(/^##\s+\d+\.\s+(.+)/);
2181
- if (header) {
2182
- if (currentSection)
2183
- sections[currentSection] = currentLines.join(`
2184
- `).trim();
2185
- currentSection = header[1].trim();
2186
- currentLines.length = 0;
2187
- } else {
2188
- currentLines.push(line);
2189
- }
2190
- }
2191
- if (currentSection)
2192
- sections[currentSection] = currentLines.join(`
2193
- `).trim();
2194
- const completed = extractBullets(sections["Work Completed"] ?? "");
2195
- const pending = extractBullets(sections["Remaining Tasks"] ?? "");
2196
- return {
2197
- workflow_name: extractProjectFromDirectory(directory),
2198
- current_status: "compacted",
2199
- current_stage: null,
2200
- completed_stages: completed,
2201
- pending_stages: pending,
2202
- key_decisions: [],
2203
- blockers: [],
2204
- important_files: [...importantFilesSet].slice(0, 30),
2205
- approvals: [],
2206
- open_questions: [],
2207
- next_steps: pending.slice(0, 5),
2208
- tool_names_used: toolNamesUsed.slice(0, 20),
2209
- observation_count: observations.length,
2210
- updated_at: new Date().toISOString()
2211
- };
2212
1881
  }
2213
- function onSessionCreated(directory, contentSessionId, prompt) {
2214
- const project = extractProjectFromDirectory(directory);
2215
- const session = initSession(contentSessionId, project, directory);
2216
- activeSessions.set(contentSessionId, {
2217
- sessionId: session.id,
2218
- contentSessionId,
2219
- project,
2220
- directory
2221
- });
2222
- return session;
2223
- }
2224
- function onToolExecuted(contentSessionId, toolName, toolInput, toolResponse, directory) {
2225
- let ctx = activeSessions.get(contentSessionId);
2226
- if (!ctx) {
2227
- const project = extractProjectFromDirectory(directory);
2228
- const session = initSession(contentSessionId, project, directory);
2229
- ctx = {
2230
- sessionId: session.id,
2231
- contentSessionId,
2232
- project,
2233
- directory
1882
+ function hasChangedSinceLastIndex(dir) {
1883
+ const meta = readCodegraphMeta(dir);
1884
+ if (!meta.indexed || !meta.lastIndexedRevision)
1885
+ return true;
1886
+ const changed = getChangedFilesSince(dir, meta.lastIndexedRevision);
1887
+ return changed.length > 0;
1888
+ }
1889
+ function installCodegraph() {
1890
+ if (isCodegraphInstalled()) {
1891
+ return {
1892
+ success: true,
1893
+ alreadyInstalled: true,
1894
+ log: "[codegraph] Already installed — skipping install"
2234
1895
  };
2235
- activeSessions.set(contentSessionId, ctx);
2236
1896
  }
2237
1897
  try {
2238
- storeObservation(ctx.sessionId, truncate(toolName, 200), toolInput, toolResponse ? truncate(toolResponse, MAX_TOOL_RESPONSE) : null, directory);
1898
+ const result = spawnSync("npm", ["install", "-g", "@colbymchenry/codegraph"], {
1899
+ encoding: "utf-8",
1900
+ timeout: 120000,
1901
+ stdio: "pipe"
1902
+ });
1903
+ if (result.status === 0) {
1904
+ return {
1905
+ success: true,
1906
+ alreadyInstalled: false,
1907
+ log: `[codegraph] Install succeeded: ${(result.stdout ?? "").trim()}`
1908
+ };
1909
+ }
1910
+ return {
1911
+ success: false,
1912
+ alreadyInstalled: false,
1913
+ log: (result.stdout ?? "").trim(),
1914
+ error: (result.stderr ?? "").trim() || `npm exited with code ${result.status}`
1915
+ };
2239
1916
  } catch (err) {
2240
- console.warn(`[FlowDeck Memory] Failed to store observation for tool "${toolName}":`, err);
1917
+ return {
1918
+ success: false,
1919
+ alreadyInstalled: false,
1920
+ log: "",
1921
+ error: String(err)
1922
+ };
2241
1923
  }
2242
1924
  }
2243
- function onMessageUpdated(contentSessionId, role, content, directory) {
2244
- if (role !== "assistant")
2245
- return;
2246
- if (!content || !content.trim())
2247
- return;
2248
- let ctx = activeSessions.get(contentSessionId);
2249
- if (!ctx) {
2250
- const project = extractProjectFromDirectory(directory);
2251
- const session = initSession(contentSessionId, project, directory);
2252
- ctx = {
2253
- sessionId: session.id,
2254
- contentSessionId,
2255
- project,
2256
- directory
1925
+ function initCodegraphIndex(dir, agent) {
1926
+ const installResult = installCodegraph();
1927
+ if (!installResult.success) {
1928
+ return {
1929
+ success: false,
1930
+ full: false,
1931
+ log: installResult.log,
1932
+ changedFiles: [],
1933
+ error: `codegraph install failed: ${installResult.error}`
2257
1934
  };
2258
- activeSessions.set(contentSessionId, ctx);
2259
1935
  }
1936
+ const meta = readCodegraphMeta(dir);
1937
+ const alreadyIndexed = isCodegraphIndexed(dir);
1938
+ const revision = getCurrentRevision(dir);
1939
+ const changedFiles = alreadyIndexed && meta.lastIndexedRevision ? getChangedFilesSince(dir, meta.lastIndexedRevision) : [];
1940
+ const needsFullRebuild = !alreadyIndexed || !meta.indexed;
1941
+ const cmd = needsFullRebuild && !alreadyIndexed ? ["init", "--index"] : ["index", "--force"];
2260
1942
  try {
2261
- storeObservation(ctx.sessionId, "assistant_message", { role }, truncate(content, MAX_TOOL_RESPONSE), directory);
1943
+ const result = spawnSync("codegraph", cmd, {
1944
+ cwd: dir,
1945
+ encoding: "utf-8",
1946
+ timeout: 300000,
1947
+ stdio: "pipe"
1948
+ });
1949
+ const success = result.status === 0;
1950
+ const log = [
1951
+ `[codegraph] Full index ${success ? "succeeded" : "failed"} (cmd: codegraph ${cmd.join(" ")})`,
1952
+ `[codegraph] Install: ${installResult.alreadyInstalled ? "skipped (already installed)" : "ran successfully"}`,
1953
+ `[codegraph] Revision: ${revision || "(no git)"}`,
1954
+ `[codegraph] Changed files since last index: ${changedFiles.length}`,
1955
+ success ? (result.stdout ?? "").trim() : (result.stderr ?? "").trim()
1956
+ ].filter(Boolean).join(`
1957
+ `);
1958
+ const now = new Date().toISOString();
1959
+ writeCodegraphMeta(dir, {
1960
+ installed: true,
1961
+ indexed: success,
1962
+ lastIndexedAt: success ? now : meta.lastIndexedAt,
1963
+ lastIndexedRevision: success ? revision : meta.lastIndexedRevision,
1964
+ lastIndexedBy: agent,
1965
+ freshnessStatus: success ? "fresh" : "stale",
1966
+ installLog: installResult.log,
1967
+ indexLog: log
1968
+ });
1969
+ return { success, full: true, log, changedFiles, error: success ? undefined : (result.stderr ?? "").trim() };
2262
1970
  } catch (err) {
2263
- console.warn("[FlowDeck Memory] Failed to store assistant message observation:", err);
1971
+ const errMsg = String(err);
1972
+ writeCodegraphMeta(dir, {
1973
+ installed: isCodegraphInstalled(),
1974
+ indexed: false,
1975
+ lastIndexedAt: "",
1976
+ lastIndexedRevision: "",
1977
+ lastIndexedBy: agent,
1978
+ freshnessStatus: "stale",
1979
+ installLog: installResult.log,
1980
+ indexLog: `[codegraph] Index failed: ${errMsg}`
1981
+ });
1982
+ return {
1983
+ success: false,
1984
+ full: true,
1985
+ log: `[codegraph] Index failed: ${errMsg}`,
1986
+ changedFiles,
1987
+ error: errMsg
1988
+ };
2264
1989
  }
2265
1990
  }
2266
- function onSessionCompact(contentSessionId, summary) {
2267
- let ctx = activeSessions.get(contentSessionId);
2268
- if (!ctx) {
2269
- const dbSession = getSessionByContentSessionId(contentSessionId);
2270
- if (!dbSession) {
2271
- console.warn(`[FlowDeck Memory] onSessionCompact: no session found for contentSessionId=${contentSessionId} — summary discarded`);
2272
- return;
2273
- }
2274
- ctx = {
2275
- sessionId: dbSession.id,
2276
- contentSessionId,
2277
- project: dbSession.project,
2278
- directory: dbSession.directory
1991
+ function refreshCodegraphIndex(dir, agent) {
1992
+ const installResult = installCodegraph();
1993
+ if (!installResult.success) {
1994
+ return {
1995
+ success: false,
1996
+ full: false,
1997
+ log: installResult.log,
1998
+ changedFiles: [],
1999
+ error: `codegraph install failed: ${installResult.error}`
2279
2000
  };
2280
- activeSessions.set(contentSessionId, ctx);
2281
2001
  }
2282
- const storedContent = truncate(summary, MAX_SUMMARY_STORAGE);
2283
- try {
2284
- const observations = getObservationsForSession(ctx.sessionId);
2285
- const metadata = buildHandoffMetadata(ctx.sessionId, ctx.directory, summary, observations);
2286
- storeSummary(ctx.sessionId, storedContent, metadata);
2287
- } catch (err) {
2288
- console.warn(`[FlowDeck Memory] Failed to store compaction summary for session ${ctx.sessionId}:`, err);
2289
- }
2290
- }
2291
- function onSessionEnd(contentSessionId, lastMessage) {
2292
- let ctx = activeSessions.get(contentSessionId);
2293
- if (!ctx) {
2294
- const dbSession = getSessionByContentSessionId(contentSessionId);
2295
- if (dbSession) {
2296
- ctx = {
2297
- sessionId: dbSession.id,
2298
- contentSessionId,
2299
- project: dbSession.project,
2300
- directory: dbSession.directory
2301
- };
2302
- }
2002
+ if (!isCodegraphIndexed(dir)) {
2003
+ return initCodegraphIndex(dir, agent);
2303
2004
  }
2304
- if (ctx && lastMessage && lastMessage.trim()) {
2305
- try {
2306
- const observations = getObservationsForSession(ctx.sessionId);
2307
- const metadata = buildHandoffMetadata(ctx.sessionId, ctx.directory, lastMessage, observations);
2308
- storeSummary(ctx.sessionId, truncate(lastMessage, MAX_SUMMARY_STORAGE), metadata);
2309
- } catch (err) {
2310
- console.warn(`[FlowDeck Memory] Failed to store end-of-session summary for session ${ctx?.sessionId}:`, err);
2005
+ const meta = readCodegraphMeta(dir);
2006
+ const revision = getCurrentRevision(dir);
2007
+ const changedFiles = meta.lastIndexedRevision ? getChangedFilesSince(dir, meta.lastIndexedRevision) : [];
2008
+ try {
2009
+ const result = spawnSync("codegraph", ["sync"], {
2010
+ cwd: dir,
2011
+ encoding: "utf-8",
2012
+ timeout: 120000,
2013
+ stdio: "pipe"
2014
+ });
2015
+ const success = result.status === 0;
2016
+ const log = [
2017
+ `[codegraph] Incremental sync ${success ? "succeeded" : "failed"}`,
2018
+ `[codegraph] Revision: ${revision || "(no git)"}`,
2019
+ `[codegraph] Changed files: ${changedFiles.length}`,
2020
+ success ? (result.stdout ?? "").trim() : (result.stderr ?? "").trim()
2021
+ ].filter(Boolean).join(`
2022
+ `);
2023
+ if (!success) {
2024
+ return initCodegraphIndex(dir, agent);
2311
2025
  }
2026
+ const now = new Date().toISOString();
2027
+ writeCodegraphMeta(dir, {
2028
+ installed: true,
2029
+ indexed: true,
2030
+ lastIndexedAt: now,
2031
+ lastIndexedRevision: revision,
2032
+ lastIndexedBy: agent,
2033
+ freshnessStatus: "fresh",
2034
+ installLog: installResult.log,
2035
+ indexLog: log
2036
+ });
2037
+ return { success: true, full: false, log, changedFiles };
2038
+ } catch (err) {
2039
+ return initCodegraphIndex(dir, agent);
2312
2040
  }
2313
- activeSessions.delete(contentSessionId);
2314
- }
2315
- function getSessionContext(directory, contentSessionId) {
2316
- const context = getContextForDirectory(directory, 30);
2317
- const previousSessions = getRecentSessions(directory, 5);
2318
- return { context, previousSessions };
2319
2041
  }
2320
- function clearSession(contentSessionId) {
2321
- activeSessions.delete(contentSessionId);
2042
+ function markCodegraphStale(dir) {
2043
+ const meta = readCodegraphMeta(dir);
2044
+ writeCodegraphMeta(dir, { ...meta, freshnessStatus: "stale" });
2322
2045
  }
2323
- var memoryHook = {
2324
- onSessionCreated,
2325
- onToolExecuted,
2326
- onMessageUpdated,
2327
- onSessionCompact,
2328
- onSessionEnd,
2329
- getSessionContext,
2330
- clearSession
2331
- };
2046
+
2047
+ // src/tools/codegraph-tool.ts
2048
+ var codegraphTool = tool16({
2049
+ description: "Manage codegraph code intelligence layer: detect installation, initialize or refresh the code index, query status. " + "When .codegraph/ exists agents should prefer codegraph MCP tools (codegraph_context, codegraph_explore, codegraph_search, " + "codegraph_callers, codegraph_callees, codegraph_impact, codegraph_trace) over direct file exploration.",
2050
+ args: {
2051
+ action: tool16.schema.enum(["check", "install", "init", "refresh", "status", "mark-stale"]),
2052
+ agent: tool16.schema.string().optional()
2053
+ },
2054
+ async execute(args, context) {
2055
+ const dir = context.directory ?? process.cwd();
2056
+ const agent = args.agent ?? "codegraph-tool";
2057
+ switch (args.action) {
2058
+ case "check": {
2059
+ const installed = isCodegraphInstalled();
2060
+ const indexed = isCodegraphIndexed(dir);
2061
+ const meta = readCodegraphMeta(dir);
2062
+ const fresh = isCodegraphFresh(dir);
2063
+ const changed = hasChangedSinceLastIndex(dir);
2064
+ return JSON.stringify({
2065
+ installed,
2066
+ indexed,
2067
+ fresh,
2068
+ hasChangedSinceLastIndex: changed,
2069
+ lastIndexedAt: meta.lastIndexedAt,
2070
+ lastIndexedRevision: meta.lastIndexedRevision,
2071
+ freshnessStatus: meta.freshnessStatus,
2072
+ recommendation: !installed ? "run action=install then action=init" : !indexed ? "run action=init to build the code index" : !fresh || changed ? "run action=refresh to update the stale index" : "codegraph index is fresh — use codegraph MCP tools directly"
2073
+ });
2074
+ }
2075
+ case "install": {
2076
+ const result = installCodegraph();
2077
+ return JSON.stringify({
2078
+ ...result,
2079
+ note: result.success && !result.alreadyInstalled ? "codegraph installed. Run action=init to build the project index." : result.alreadyInstalled ? "codegraph was already installed." : `Install failed: ${result.error}`
2080
+ });
2081
+ }
2082
+ case "init": {
2083
+ const result = initCodegraphIndex(dir, agent);
2084
+ return JSON.stringify({
2085
+ ...result,
2086
+ note: result.success ? `codegraph index built (${result.full ? "full" : "incremental"}). ` + `codegraph MCP tools are now available for code understanding.` : `codegraph init failed: ${result.error}`
2087
+ });
2088
+ }
2089
+ case "refresh": {
2090
+ const result = refreshCodegraphIndex(dir, agent);
2091
+ return JSON.stringify({
2092
+ ...result,
2093
+ note: result.success ? `codegraph index refreshed. Changed files since last index: ${result.changedFiles.length}` : `codegraph refresh failed: ${result.error}`
2094
+ });
2095
+ }
2096
+ case "status": {
2097
+ const installed = isCodegraphInstalled();
2098
+ const indexed = isCodegraphIndexed(dir);
2099
+ const meta = readCodegraphMeta(dir);
2100
+ const fresh = isCodegraphFresh(dir);
2101
+ return JSON.stringify({
2102
+ installed,
2103
+ indexed,
2104
+ fresh,
2105
+ meta,
2106
+ mcp: {
2107
+ available: installed && indexed,
2108
+ tools: [
2109
+ "codegraph_context",
2110
+ "codegraph_trace",
2111
+ "codegraph_explore",
2112
+ "codegraph_search",
2113
+ "codegraph_callers",
2114
+ "codegraph_callees",
2115
+ "codegraph_impact",
2116
+ "codegraph_node",
2117
+ "codegraph_status",
2118
+ "codegraph_files"
2119
+ ],
2120
+ guidance: installed && indexed ? "Use codegraph MCP tools for code understanding. Prefer over file exploration." : "codegraph not ready. Run action=init first."
2121
+ }
2122
+ });
2123
+ }
2124
+ case "mark-stale": {
2125
+ markCodegraphStale(dir);
2126
+ return JSON.stringify({ success: true, message: "codegraph index marked stale — next init will do a full rebuild" });
2127
+ }
2128
+ }
2129
+ }
2130
+ });
2332
2131
 
2333
2132
  // src/hooks/guard-rails.ts
2334
- import { existsSync as existsSync18, readFileSync as readFileSync15 } from "fs";
2335
- import { join as join18 } from "path";
2133
+ import { existsSync as existsSync17, readFileSync as readFileSync16 } from "fs";
2134
+ import { join as join17 } from "path";
2336
2135
 
2337
2136
  // src/config/loader.ts
2338
- import { existsSync as existsSync17, readFileSync as readFileSync14 } from "fs";
2339
- import { join as join17 } from "path";
2340
- import { homedir as homedir3 } from "os";
2137
+ import { existsSync as existsSync16, readFileSync as readFileSync15 } from "fs";
2138
+ import { join as join16 } from "path";
2139
+ import { homedir } from "os";
2341
2140
  var CONFIG_FILENAME = "flowdeck.json";
2342
2141
  function getGlobalConfigDir() {
2343
- return process.env.OPENCODE_CONFIG_DIR || (process.env.XDG_CONFIG_HOME ? join17(process.env.XDG_CONFIG_HOME, "opencode") : join17(homedir3(), ".config", "opencode"));
2142
+ return process.env.OPENCODE_CONFIG_DIR || (process.env.XDG_CONFIG_HOME ? join16(process.env.XDG_CONFIG_HOME, "opencode") : join16(homedir(), ".config", "opencode"));
2344
2143
  }
2345
2144
  function loadFlowDeckConfig(directory) {
2346
2145
  const candidates = [];
2347
2146
  if (directory) {
2348
- candidates.push(join17(directory, ".opencode", CONFIG_FILENAME));
2147
+ candidates.push(join16(directory, ".opencode", CONFIG_FILENAME));
2349
2148
  }
2350
- candidates.push(join17(getGlobalConfigDir(), CONFIG_FILENAME));
2149
+ candidates.push(join16(getGlobalConfigDir(), CONFIG_FILENAME));
2351
2150
  for (const configPath of candidates) {
2352
- if (existsSync17(configPath)) {
2151
+ if (existsSync16(configPath)) {
2353
2152
  try {
2354
- const content = readFileSync14(configPath, "utf-8");
2153
+ const content = readFileSync15(configPath, "utf-8");
2355
2154
  return JSON.parse(content);
2356
2155
  } catch {
2357
2156
  console.warn(`[flowdeck] Failed to load config from ${configPath}`);
@@ -2380,9 +2179,9 @@ var PLANNING_DIR2 = ".planning";
2380
2179
  var CONFIG_FILE = "config.json";
2381
2180
  var STATE_FILE2 = "STATE.md";
2382
2181
  function resolveExecutionMode(configPath, trustScore, volatility) {
2383
- if (existsSync18(configPath)) {
2182
+ if (existsSync17(configPath)) {
2384
2183
  try {
2385
- const config = JSON.parse(readFileSync15(configPath, "utf-8"));
2184
+ const config = JSON.parse(readFileSync16(configPath, "utf-8"));
2386
2185
  if (config.execution_mode === "review-only")
2387
2186
  return "review-only";
2388
2187
  if (config.execution_mode === "guarded")
@@ -2436,22 +2235,22 @@ async function guardRailsHook(ctx, input, _output) {
2436
2235
  if (!ENABLED)
2437
2236
  return;
2438
2237
  const dir = ctx.directory;
2439
- const planningDirPath = join18(dir, PLANNING_DIR2);
2238
+ const planningDirPath = join17(dir, PLANNING_DIR2);
2440
2239
  const codebaseDirectory = codebaseDir(dir);
2441
- const configPath = join18(planningDirPath, CONFIG_FILE);
2442
- const statePath2 = join18(planningDirPath, STATE_FILE2);
2240
+ const configPath = join17(planningDirPath, CONFIG_FILE);
2241
+ const statePath2 = join17(planningDirPath, STATE_FILE2);
2443
2242
  const workspaceRoot = findWorkspaceRoot(dir);
2444
2243
  if (workspaceRoot && dir !== workspaceRoot) {
2445
2244
  const config = getWorkspaceConfig(dir);
2446
- if (config && config.workspace_mode === "shared" && !existsSync18(planningDirPath)) {
2245
+ if (config && config.workspace_mode === "shared" && !existsSync17(planningDirPath)) {
2447
2246
  const msg = `No .planning/ in this sub-repo. Switch to workspace root: cd ${workspaceRoot}`;
2448
2247
  throw new Error(`[flowdeck] BLOCK: ${msg}`);
2449
2248
  }
2450
2249
  }
2451
2250
  if (input.tool === "write" || input.tool === "edit") {
2452
- if (!existsSync18(planningDirPath))
2251
+ if (!existsSync17(planningDirPath))
2453
2252
  return;
2454
- if (!existsSync18(codebaseDirectory)) {
2253
+ if (!existsSync17(codebaseDirectory)) {
2455
2254
  throw new Error(`[flowdeck] WARNING: .codebase/ not found. Run /fd-map-codebase to map the codebase.`);
2456
2255
  }
2457
2256
  const execMode = resolveExecutionMode(configPath, null);
@@ -2507,15 +2306,15 @@ function getDesignGateMessage(dir) {
2507
2306
  }
2508
2307
  function planSuggestsUiHeavy(dir, phase) {
2509
2308
  const planPath = phasePlanPath(dir, phase);
2510
- if (!existsSync18(planPath))
2309
+ if (!existsSync17(planPath))
2511
2310
  return false;
2512
- const planContent = readFileSync15(planPath, "utf-8");
2311
+ const planContent = readFileSync16(planPath, "utf-8");
2513
2312
  return isUiHeavyTask(planContent);
2514
2313
  }
2515
2314
  function effectiveSeverity(configPath, statePath2) {
2516
- if (existsSync18(configPath)) {
2315
+ if (existsSync17(configPath)) {
2517
2316
  try {
2518
- const configContent = readFileSync15(configPath, "utf-8");
2317
+ const configContent = readFileSync16(configPath, "utf-8");
2519
2318
  const config = JSON.parse(configContent);
2520
2319
  if (config.guard_enforcement === "warn")
2521
2320
  return "warn";
@@ -2531,10 +2330,10 @@ function getEffectiveSeverity(configPath, statePath2) {
2531
2330
  return effectiveSeverity(configPath, statePath2);
2532
2331
  }
2533
2332
  function getPlanConfirmed(statePath2) {
2534
- if (!existsSync18(statePath2))
2333
+ if (!existsSync17(statePath2))
2535
2334
  return false;
2536
2335
  try {
2537
- const content = readFileSync15(statePath2, "utf-8");
2336
+ const content = readFileSync16(statePath2, "utf-8");
2538
2337
  const match = content.match(/plan_confirmed:\s*(true|false)/i);
2539
2338
  return match ? match[1].toLowerCase() === "true" : false;
2540
2339
  } catch {
@@ -2542,32 +2341,32 @@ function getPlanConfirmed(statePath2) {
2542
2341
  }
2543
2342
  }
2544
2343
  function getWarningMessage(planningDir2) {
2545
- if (!existsSync18(join18(planningDir2, STATE_FILE2))) {
2546
- return "No .planning/ found. Run /fd-new-project first.";
2344
+ if (!existsSync17(join17(planningDir2, STATE_FILE2))) {
2345
+ return "No STATE.md found. Run /fd-map-codebase then /fd-new-feature to start a feature.";
2547
2346
  }
2548
2347
  return "Plan not confirmed. Run /fd-plan and confirm to enable execution.";
2549
2348
  }
2550
2349
  function getBlockMessage(planningDir2) {
2551
- if (!existsSync18(join18(planningDir2, STATE_FILE2))) {
2552
- return "No .planning/ found. Run /fd-new-project first.";
2350
+ if (!existsSync17(join17(planningDir2, STATE_FILE2))) {
2351
+ return "No STATE.md found. Run /fd-map-codebase then /fd-new-feature to start a feature.";
2553
2352
  }
2554
2353
  return "Plan not confirmed. Run /fd-plan and confirm to enable execution.";
2555
2354
  }
2556
2355
 
2557
2356
  // src/hooks/tool-guard.ts
2558
- import { existsSync as existsSync19, readFileSync as readFileSync16 } from "fs";
2559
- import { join as join19 } from "path";
2357
+ import { existsSync as existsSync18, readFileSync as readFileSync17 } from "fs";
2358
+ import { join as join18 } from "path";
2560
2359
  var IS_ENABLED = () => process.env.FLOWDECK_TOOL_GUARD_ENABLED === "on";
2561
2360
  var BLOCKED_PATTERNS = {
2562
2361
  read: [".env", ".pem", ".key", ".secret"],
2563
2362
  write: ["node_modules"],
2564
2363
  bash: ["rm -rf"]
2565
2364
  };
2566
- function isBlocked(tool18, args) {
2567
- const patterns = BLOCKED_PATTERNS[tool18];
2365
+ function isBlocked(tool17, args) {
2366
+ const patterns = BLOCKED_PATTERNS[tool17];
2568
2367
  if (!patterns)
2569
2368
  return null;
2570
- if (tool18 === "bash") {
2369
+ if (tool17 === "bash") {
2571
2370
  const cmd = args.command;
2572
2371
  if (!cmd)
2573
2372
  return null;
@@ -2578,7 +2377,7 @@ function isBlocked(tool18, args) {
2578
2377
  }
2579
2378
  return null;
2580
2379
  }
2581
- if (tool18 === "read") {
2380
+ if (tool17 === "read") {
2582
2381
  const filePath = args.filePath;
2583
2382
  if (!filePath)
2584
2383
  return null;
@@ -2589,7 +2388,7 @@ function isBlocked(tool18, args) {
2589
2388
  }
2590
2389
  return null;
2591
2390
  }
2592
- if (tool18 === "write") {
2391
+ if (tool17 === "write") {
2593
2392
  const filePath = args.filePath;
2594
2393
  if (!filePath)
2595
2394
  return null;
@@ -2603,11 +2402,11 @@ function isBlocked(tool18, args) {
2603
2402
  return null;
2604
2403
  }
2605
2404
  function checkArchConstraint(directory, filePath) {
2606
- const constraintsPath = join19(codebaseDir(directory), "CONSTRAINTS.md");
2607
- if (!existsSync19(constraintsPath))
2405
+ const constraintsPath = join18(codebaseDir(directory), "CONSTRAINTS.md");
2406
+ if (!existsSync18(constraintsPath))
2608
2407
  return null;
2609
2408
  try {
2610
- const content = readFileSync16(constraintsPath, "utf-8");
2409
+ const content = readFileSync17(constraintsPath, "utf-8");
2611
2410
  const match = content.match(/## Forbidden Paths\n([\s\S]*?)(?:\n##|$)/);
2612
2411
  if (!match)
2613
2412
  return null;
@@ -2648,9 +2447,9 @@ function isUiDesignApprovalRequired(directory) {
2648
2447
  return !(state.design_stage === "handoff_complete" && state.design_approved);
2649
2448
  }
2650
2449
  const planPath = phasePlanPath(directory, state.phase || 1);
2651
- if (!existsSync19(planPath))
2450
+ if (!existsSync18(planPath))
2652
2451
  return false;
2653
- const planContent = readFileSync16(planPath, "utf-8");
2452
+ const planContent = readFileSync17(planPath, "utf-8");
2654
2453
  if (!isUiHeavyTask(planContent))
2655
2454
  return false;
2656
2455
  return !(state.design_stage === "handoff_complete" && state.design_approved);
@@ -2679,19 +2478,18 @@ async function toolGuardHook(ctx, input, output) {
2679
2478
  }
2680
2479
 
2681
2480
  // src/hooks/session-start.ts
2682
- import { existsSync as existsSync20, readFileSync as readFileSync17 } from "fs";
2481
+ import { existsSync as existsSync19, readFileSync as readFileSync18 } from "fs";
2683
2482
  async function sessionStartHook(ctx) {
2684
2483
  const planningDir2 = ctx.directory + "/.planning";
2685
2484
  const codebaseDirectory = codebaseDir(ctx.directory);
2686
2485
  const workspaceRoot = findWorkspaceRoot(ctx.directory);
2687
2486
  const config = workspaceRoot ? getWorkspaceConfig(ctx.directory) : null;
2688
- if (!existsSync20(planningDir2)) {
2487
+ if (!existsSync19(planningDir2)) {
2689
2488
  return {
2690
2489
  flowdeck_phase: null,
2691
2490
  flowdeck_status: "no_plan",
2692
- flowdeck_warning: "Run /fd-new-project or /fd-map-codebase to initialize.",
2693
- flowdeck_has_codebase: existsSync20(codebaseDirectory),
2694
- flowdeck_session_context: getContextForDirectory(ctx.directory),
2491
+ flowdeck_warning: "Run /fd-map-codebase to index the codebase, then /fd-new-feature to start a feature.",
2492
+ flowdeck_has_codebase: existsSync19(codebaseDirectory),
2695
2493
  ...workspaceRoot && config?.sub_repos ? {
2696
2494
  flowdeck_workspace_root: workspaceRoot,
2697
2495
  flowdeck_sub_repos: config.sub_repos,
@@ -2702,17 +2500,15 @@ async function sessionStartHook(ctx) {
2702
2500
  }
2703
2501
  try {
2704
2502
  const stateFilePath = statePath(ctx.directory);
2705
- const content = readFileSync17(stateFilePath, "utf-8");
2503
+ const content = readFileSync18(stateFilePath, "utf-8");
2706
2504
  const state = parseState(content);
2707
2505
  const currentPhase = state["current_phase"] || {};
2708
- const sessionContext = getContextForDirectory(ctx.directory);
2709
2506
  const result = {
2710
2507
  flowdeck_phase: currentPhase["phase"] ?? null,
2711
2508
  flowdeck_status: currentPhase["status"] ?? null,
2712
2509
  flowdeck_steps_pending: currentPhase["steps_pending"] ?? null,
2713
2510
  flowdeck_last_action: currentPhase["last_action"] ?? null,
2714
- flowdeck_has_codebase: existsSync20(codebaseDirectory),
2715
- flowdeck_session_context: sessionContext
2511
+ flowdeck_has_codebase: existsSync19(codebaseDirectory)
2716
2512
  };
2717
2513
  if (workspaceRoot && config?.sub_repos && config.sub_repos.length > 0) {
2718
2514
  result.flowdeck_workspace_root = workspaceRoot;
@@ -2727,8 +2523,7 @@ async function sessionStartHook(ctx) {
2727
2523
  flowdeck_phase: null,
2728
2524
  flowdeck_status: "error",
2729
2525
  flowdeck_warning: "State file unreadable. Continuing without flowdeck context.",
2730
- flowdeck_has_codebase: existsSync20(codebaseDirectory),
2731
- flowdeck_session_context: getContextForDirectory(ctx.directory)
2526
+ flowdeck_has_codebase: existsSync19(codebaseDirectory)
2732
2527
  };
2733
2528
  if (workspaceRoot && config?.sub_repos && config.sub_repos.length > 0) {
2734
2529
  result.flowdeck_workspace_root = workspaceRoot;
@@ -2745,16 +2540,22 @@ import { execFile } from "child_process";
2745
2540
  var INTERACTIVE_COMMANDS = new Set([
2746
2541
  "discuss",
2747
2542
  "plan",
2748
- "review-code",
2749
2543
  "deploy-check",
2750
- "new-project"
2544
+ "ask",
2545
+ "resume"
2751
2546
  ]);
2752
2547
  var COMPLETION_COMMANDS = new Set([
2753
2548
  "new-feature",
2754
2549
  "fix-bug",
2755
2550
  "write-docs",
2756
- "checkpoint"
2551
+ "checkpoint",
2552
+ "done",
2553
+ "execute",
2554
+ "verify"
2757
2555
  ]);
2556
+ function normalizeCommandName(raw) {
2557
+ return raw.replace(/^\//, "").replace(/^fd-/, "");
2558
+ }
2758
2559
  function notify(title, body, level = "info") {
2759
2560
  const platform = process.platform;
2760
2561
  try {
@@ -2786,16 +2587,88 @@ function tryTerminalBell() {
2786
2587
  process.stdout.write("\x07");
2787
2588
  } catch {}
2788
2589
  }
2789
- function notifySessionIdle() {
2790
- notify("FlowDeck Task Completed", "Agent is idle and waiting for your next instruction", "info");
2590
+
2591
+ class NotificationController {
2592
+ pendingCommand = null;
2593
+ lastNotifiedKey = null;
2594
+ notifyFn;
2595
+ log;
2596
+ constructor(notifyFn = notify, log = () => {}) {
2597
+ this.notifyFn = notifyFn;
2598
+ this.log = log;
2599
+ }
2600
+ onCommandExecuted(rawCommand) {
2601
+ const name = normalizeCommandName(rawCommand);
2602
+ if (!INTERACTIVE_COMMANDS.has(name) && !COMPLETION_COMMANDS.has(name)) {
2603
+ this.log(`[notify] command.executed: "${name}" — not a tracked command, skipping`);
2604
+ return;
2605
+ }
2606
+ this.log(`[notify] command.executed: "${name}" recorded as pending`);
2607
+ this.pendingCommand = name;
2608
+ this.lastNotifiedKey = null;
2609
+ }
2610
+ onSessionIdle(hasEdits) {
2611
+ if (this.pendingCommand) {
2612
+ const name = this.pendingCommand;
2613
+ const dedupeKey = `idle:${name}`;
2614
+ if (this.lastNotifiedKey === dedupeKey) {
2615
+ this.log(`[notify] suppressed duplicate: state=session.idle command=${name}`);
2616
+ return;
2617
+ }
2618
+ const reason = INTERACTIVE_COMMANDS.has(name) ? "input_required" : "completed";
2619
+ this.log(`[notify] firing notification: reason=${reason} command=${name} source=session.idle`);
2620
+ if (reason === "input_required") {
2621
+ this.notifyFn(`FlowDeck: /${name}`, "Your input is needed — please check OpenCode", "critical");
2622
+ } else {
2623
+ this.notifyFn(`FlowDeck: /${name} complete`, "Review the output and choose your next step", "info");
2624
+ }
2625
+ this.lastNotifiedKey = dedupeKey;
2626
+ this.pendingCommand = null;
2627
+ return;
2628
+ }
2629
+ if (hasEdits) {
2630
+ const dedupeKey = "idle:generic";
2631
+ if (this.lastNotifiedKey === dedupeKey) {
2632
+ this.log(`[notify] suppressed duplicate: state=session.idle source=generic`);
2633
+ return;
2634
+ }
2635
+ this.log(`[notify] firing notification: reason=completed source=session.idle (generic, has edits)`);
2636
+ this.notifyFn("FlowDeck Task Completed", "Agent is idle and waiting for your next instruction", "info");
2637
+ this.lastNotifiedKey = dedupeKey;
2638
+ } else {
2639
+ this.log(`[notify] session.idle — no pending command, no edits — suppressed`);
2640
+ }
2641
+ }
2642
+ onSessionError(errorMsg) {
2643
+ const snippet = errorMsg.slice(0, 60);
2644
+ const dedupeKey = `error:${snippet}`;
2645
+ if (this.lastNotifiedKey === dedupeKey) {
2646
+ this.log(`[notify] suppressed duplicate: state=session.error`);
2647
+ return;
2648
+ }
2649
+ this.log(`[notify] firing notification: reason=error source=session.error`);
2650
+ this.notifyFn("FlowDeck Error", snippet || "An error occurred", "critical");
2651
+ this.lastNotifiedKey = dedupeKey;
2652
+ this.pendingCommand = null;
2653
+ }
2654
+ reset() {
2655
+ this.pendingCommand = null;
2656
+ this.lastNotifiedKey = null;
2657
+ }
2658
+ getPendingCommand() {
2659
+ return this.pendingCommand;
2660
+ }
2661
+ getLastNotifiedKey() {
2662
+ return this.lastNotifiedKey;
2663
+ }
2791
2664
  }
2792
- function notifyPermissionNeeded(tool18) {
2793
- notify("FlowDeck Permission Required", `Agent needs approval to use tool: ${tool18}`, "critical");
2665
+ function notifyPermissionNeeded(tool17) {
2666
+ notify("FlowDeck Permission Required", `Agent needs approval to use tool: ${tool17}`, "critical");
2794
2667
  }
2795
2668
 
2796
2669
  // src/hooks/patch-trust.ts
2797
- import { existsSync as existsSync21, readFileSync as readFileSync18 } from "fs";
2798
- import { join as join20 } from "path";
2670
+ import { existsSync as existsSync20, readFileSync as readFileSync19 } from "fs";
2671
+ import { join as join19 } from "path";
2799
2672
  var HIGH_RISK_KEYWORDS = [
2800
2673
  "password",
2801
2674
  "secret",
@@ -2817,11 +2690,11 @@ var HIGH_RISK_KEYWORDS = [
2817
2690
  "privilege"
2818
2691
  ];
2819
2692
  function loadVolatility(directory) {
2820
- const p = join20(codebaseDir(directory), "VOLATILITY.json");
2821
- if (!existsSync21(p))
2693
+ const p = join19(codebaseDir(directory), "VOLATILITY.json");
2694
+ if (!existsSync20(p))
2822
2695
  return {};
2823
2696
  try {
2824
- const data = JSON.parse(readFileSync18(p, "utf-8"));
2697
+ const data = JSON.parse(readFileSync19(p, "utf-8"));
2825
2698
  const map = {};
2826
2699
  for (const entry of data.entries ?? [])
2827
2700
  map[entry.path] = entry.stability;
@@ -2831,11 +2704,11 @@ function loadVolatility(directory) {
2831
2704
  }
2832
2705
  }
2833
2706
  function loadFailedPaths(directory) {
2834
- const p = join20(codebaseDir(directory), "FAILURES.json");
2835
- if (!existsSync21(p))
2707
+ const p = join19(codebaseDir(directory), "FAILURES.json");
2708
+ if (!existsSync20(p))
2836
2709
  return [];
2837
2710
  try {
2838
- const data = JSON.parse(readFileSync18(p, "utf-8"));
2711
+ const data = JSON.parse(readFileSync19(p, "utf-8"));
2839
2712
  return (data.entries ?? []).flatMap((e) => e.affected_paths ?? []);
2840
2713
  } catch {
2841
2714
  return [];
@@ -2900,8 +2773,8 @@ async function patchTrustHook(ctx, input, output) {
2900
2773
  }
2901
2774
 
2902
2775
  // src/hooks/decision-trace-hook.ts
2903
- import { existsSync as existsSync22, mkdirSync as mkdirSync11, appendFileSync as appendFileSync3 } from "fs";
2904
- import { join as join21 } from "path";
2776
+ import { existsSync as existsSync21, mkdirSync as mkdirSync11, appendFileSync as appendFileSync3 } from "fs";
2777
+ import { join as join20 } from "path";
2905
2778
  async function decisionTraceHook(ctx, input, output) {
2906
2779
  if (input.tool !== "write" && input.tool !== "edit")
2907
2780
  return;
@@ -2910,7 +2783,7 @@ async function decisionTraceHook(ctx, input, output) {
2910
2783
  return;
2911
2784
  const base = codebaseDir(ctx.directory);
2912
2785
  try {
2913
- if (!existsSync22(base))
2786
+ if (!existsSync21(base))
2914
2787
  mkdirSync11(base, { recursive: true });
2915
2788
  const entry = {
2916
2789
  timestamp: new Date().toISOString(),
@@ -2923,23 +2796,23 @@ async function decisionTraceHook(ctx, input, output) {
2923
2796
  risk_level: "unknown",
2924
2797
  auto_recorded: true
2925
2798
  };
2926
- appendFileSync3(join21(base, "DECISIONS.jsonl"), JSON.stringify(entry) + `
2799
+ appendFileSync3(join20(base, "DECISIONS.jsonl"), JSON.stringify(entry) + `
2927
2800
  `, "utf-8");
2928
2801
  } catch {}
2929
2802
  }
2930
2803
 
2931
2804
  // src/services/telemetry.ts
2932
- import { existsSync as existsSync23, readFileSync as readFileSync19, appendFileSync as appendFileSync4, mkdirSync as mkdirSync12 } from "fs";
2933
- import { join as join22 } from "path";
2805
+ import { existsSync as existsSync22, readFileSync as readFileSync20, appendFileSync as appendFileSync4, mkdirSync as mkdirSync12 } from "fs";
2806
+ import { join as join21 } from "path";
2934
2807
  import { randomUUID } from "crypto";
2935
2808
  function telemetryPath(dir) {
2936
- return join22(codebaseDir(dir), "TELEMETRY.jsonl");
2809
+ return join21(codebaseDir(dir), "TELEMETRY.jsonl");
2937
2810
  }
2938
2811
  function appendEvent(dir, partial) {
2939
2812
  if (process.env.TELEMETRY_ENABLED !== "true")
2940
2813
  return null;
2941
2814
  const cd = codebaseDir(dir);
2942
- if (!existsSync23(cd))
2815
+ if (!existsSync22(cd))
2943
2816
  mkdirSync12(cd, { recursive: true });
2944
2817
  const event = {
2945
2818
  id: randomUUID(),
@@ -2976,34 +2849,34 @@ function inferStatus(output) {
2976
2849
  }
2977
2850
  async function telemetryHook(context, toolInput, output) {
2978
2851
  const dir = context.directory ?? process.cwd();
2979
- const tool18 = toolInput.name ?? toolInput.tool ?? "unknown";
2852
+ const tool17 = toolInput.name ?? toolInput.tool ?? "unknown";
2980
2853
  const ids = resolveIds(toolInput);
2981
2854
  appendEvent(dir, {
2982
2855
  session_id: ids.session_id,
2983
2856
  run_id: ids.run_id,
2984
2857
  event: "tool.call",
2985
- tool: tool18,
2858
+ tool: tool17,
2986
2859
  status: "ok",
2987
2860
  meta: { parameters: output.args ?? {} }
2988
2861
  });
2989
2862
  }
2990
2863
  async function telemetryAfterHook(context, toolInput, output) {
2991
2864
  const dir = context.directory ?? process.cwd();
2992
- const tool18 = toolInput.name ?? toolInput.tool ?? "unknown";
2865
+ const tool17 = toolInput.name ?? toolInput.tool ?? "unknown";
2993
2866
  const ids = resolveIds(toolInput);
2994
2867
  const status = inferStatus(output);
2995
2868
  appendEvent(dir, {
2996
2869
  session_id: ids.session_id,
2997
2870
  run_id: ids.run_id,
2998
2871
  event: "tool.complete",
2999
- tool: tool18,
2872
+ tool: tool17,
3000
2873
  status
3001
2874
  });
3002
2875
  }
3003
2876
 
3004
2877
  // src/services/approval-manager.ts
3005
- import { existsSync as existsSync24, readFileSync as readFileSync20, writeFileSync as writeFileSync14, mkdirSync as mkdirSync13 } from "fs";
3006
- import { join as join23 } from "path";
2878
+ import { existsSync as existsSync23, readFileSync as readFileSync21, writeFileSync as writeFileSync15, mkdirSync as mkdirSync13 } from "fs";
2879
+ import { join as join22 } from "path";
3007
2880
  var APPROVAL_TTL_MS = 30 * 60 * 1000;
3008
2881
  var SENSITIVE_PATTERNS = [
3009
2882
  /auth/i,
@@ -3040,14 +2913,14 @@ function isSensitivePath(filePath) {
3040
2913
  return SENSITIVE_PATTERNS.some((p) => p.test(filePath));
3041
2914
  }
3042
2915
  function approvalsPath(dir) {
3043
- return join23(codebaseDir(dir), "APPROVALS.json");
2916
+ return join22(codebaseDir(dir), "APPROVALS.json");
3044
2917
  }
3045
2918
  function loadStore2(dir) {
3046
2919
  const p = approvalsPath(dir);
3047
- if (!existsSync24(p))
2920
+ if (!existsSync23(p))
3048
2921
  return { requests: [] };
3049
2922
  try {
3050
- return JSON.parse(readFileSync20(p, "utf-8"));
2923
+ return JSON.parse(readFileSync21(p, "utf-8"));
3051
2924
  } catch {
3052
2925
  return { requests: [] };
3053
2926
  }
@@ -3065,8 +2938,8 @@ async function approvalHook(context, toolInput, output) {
3065
2938
  if (!ENABLED2)
3066
2939
  return;
3067
2940
  const dir = context.directory ?? process.cwd();
3068
- const tool18 = toolInput.name ?? toolInput.tool ?? "";
3069
- if (!WRITE_TOOLS.has(tool18))
2941
+ const tool17 = toolInput.name ?? toolInput.tool ?? "";
2942
+ if (!WRITE_TOOLS.has(tool17))
3070
2943
  return;
3071
2944
  const args = output.args ?? {};
3072
2945
  const filePath = String(args.path ?? args.file_path ?? args.filename ?? "");
@@ -3081,7 +2954,7 @@ async function approvalHook(context, toolInput, output) {
3081
2954
  session_id: process.env.OPENCODE_SESSION_ID ?? "session-0",
3082
2955
  run_id: process.env.OPENCODE_RUN_ID ?? "run-0",
3083
2956
  event: "approval.request",
3084
- tool: tool18,
2957
+ tool: tool17,
3085
2958
  status: "blocked",
3086
2959
  files: [filePath],
3087
2960
  meta: { trigger: "sensitive_file", file: filePath }
@@ -3142,8 +3015,8 @@ function createContextWindowMonitorHook() {
3142
3015
  }
3143
3016
 
3144
3017
  // src/hooks/shell-env-hook.ts
3145
- import { existsSync as existsSync25, readFileSync as readFileSync21 } from "fs";
3146
- import { join as join24 } from "path";
3018
+ import { existsSync as existsSync24, readFileSync as readFileSync22 } from "fs";
3019
+ import { join as join23 } from "path";
3147
3020
  import { createRequire } from "module";
3148
3021
  var _version;
3149
3022
  function getVersion() {
@@ -3179,7 +3052,7 @@ var MARKER_TO_LANG = {
3179
3052
  };
3180
3053
  function detectPackageManager(root) {
3181
3054
  for (const [lockfile, pm] of Object.entries(LOCKFILE_TO_PM)) {
3182
- if (existsSync25(join24(root, lockfile)))
3055
+ if (existsSync24(join23(root, lockfile)))
3183
3056
  return pm;
3184
3057
  }
3185
3058
  return;
@@ -3188,7 +3061,7 @@ function detectLanguages(root) {
3188
3061
  const langs = [];
3189
3062
  const seen = new Set;
3190
3063
  for (const [marker, lang] of Object.entries(MARKER_TO_LANG)) {
3191
- if (!seen.has(lang) && existsSync25(join24(root, marker))) {
3064
+ if (!seen.has(lang) && existsSync24(join23(root, marker))) {
3192
3065
  langs.push(lang);
3193
3066
  seen.add(lang);
3194
3067
  }
@@ -3196,11 +3069,11 @@ function detectLanguages(root) {
3196
3069
  return langs;
3197
3070
  }
3198
3071
  function readCurrentPhase(root) {
3199
- const statePath2 = join24(root, ".planning", "STATE.md");
3200
- if (!existsSync25(statePath2))
3072
+ const statePath2 = join23(root, ".planning", "STATE.md");
3073
+ if (!existsSync24(statePath2))
3201
3074
  return;
3202
3075
  try {
3203
- const content = readFileSync21(statePath2, "utf-8");
3076
+ const content = readFileSync22(statePath2, "utf-8");
3204
3077
  const match = content.match(/phase:\s*(\S+)/i);
3205
3078
  return match?.[1];
3206
3079
  } catch {
@@ -3249,8 +3122,15 @@ function createTodoHook(client) {
3249
3122
  // src/hooks/file-tracker.ts
3250
3123
  class SessionFileTracker {
3251
3124
  changes = new Map;
3125
+ onFileChange;
3126
+ setOnFileChange(callback) {
3127
+ this.onFileChange = callback;
3128
+ }
3252
3129
  record(path, type) {
3253
3130
  this.changes.set(path, { path, type });
3131
+ if (this.onFileChange) {
3132
+ this.onFileChange(path, type);
3133
+ }
3254
3134
  }
3255
3135
  getChanges() {
3256
3136
  return [...this.changes.values()];
@@ -3284,7 +3164,6 @@ function createSessionIdleHook(client, tracker) {
3284
3164
  const edited = tracker.getEditedPaths();
3285
3165
  if (edited.length === 0)
3286
3166
  return;
3287
- notifySessionIdle();
3288
3167
  const summary = `[FlowDeck] Session idle — ${edited.length} file(s) modified this session`;
3289
3168
  await client.app.log({ body: { service: "flowdeck", level: "info", message: summary } }).catch(() => {});
3290
3169
  const preview = edited.slice(0, 10);
@@ -3299,8 +3178,8 @@ function createSessionIdleHook(client, tracker) {
3299
3178
  }
3300
3179
 
3301
3180
  // src/hooks/compaction-hook.ts
3302
- import { existsSync as existsSync26, readFileSync as readFileSync22 } from "fs";
3303
- import { join as join25 } from "path";
3181
+ import { existsSync as existsSync25, readFileSync as readFileSync23 } from "fs";
3182
+ import { join as join24 } from "path";
3304
3183
  var STRUCTURED_SUMMARY_PROMPT = `
3305
3184
  When summarizing this session, you MUST include the following sections:
3306
3185
 
@@ -3339,11 +3218,11 @@ For each: agent name, status, description, session_id.
3339
3218
  **RESUME, DON'T RESTART.** Use session_id to continue existing sessions.
3340
3219
  `;
3341
3220
  function readPlanningState2(directory) {
3342
- const statePath2 = join25(directory, ".planning", "STATE.md");
3343
- if (!existsSync26(statePath2))
3221
+ const statePath2 = join24(directory, ".planning", "STATE.md");
3222
+ if (!existsSync25(statePath2))
3344
3223
  return null;
3345
3224
  try {
3346
- const content = readFileSync22(statePath2, "utf-8");
3225
+ const content = readFileSync23(statePath2, "utf-8");
3347
3226
  return content.slice(0, 1500);
3348
3227
  } catch {
3349
3228
  return null;
@@ -3360,6 +3239,18 @@ function createCompactionHook(ctx, tracker) {
3360
3239
  sections.push("```");
3361
3240
  sections.push("");
3362
3241
  }
3242
+ const indexPath = join24(ctx.directory, ".planning", "CODEBASE_INDEX.md");
3243
+ let indexSummary = "";
3244
+ if (existsSync25(indexPath)) {
3245
+ try {
3246
+ const indexContent = readFileSync23(indexPath, "utf-8");
3247
+ indexSummary = "\n## Codebase Index\n```\n" + indexContent.slice(0, 800) + "\n```\n";
3248
+ } catch {}
3249
+ }
3250
+ if (indexSummary) {
3251
+ sections.push(indexSummary);
3252
+ sections.push("");
3253
+ }
3363
3254
  const edited = tracker.getEditedPaths();
3364
3255
  if (edited.length > 0) {
3365
3256
  sections.push("## Recently Edited Files");
@@ -3370,12 +3261,6 @@ function createCompactionHook(ctx, tracker) {
3370
3261
  sections.push(`- … and ${edited.length - 20} more`);
3371
3262
  sections.push("");
3372
3263
  }
3373
- const sessionContext = getContextForDirectory(ctx.directory);
3374
- if (sessionContext) {
3375
- sections.push("## Previous Sessions Context");
3376
- sections.push(sessionContext);
3377
- sections.push("");
3378
- }
3379
3264
  output.context.push(sections.join(`
3380
3265
  `));
3381
3266
  output.prompt = STRUCTURED_SUMMARY_PROMPT.trim();
@@ -3587,6 +3472,14 @@ function createFlowDeckMcps() {
3587
3472
  oauth: false
3588
3473
  };
3589
3474
  }
3475
+ if (!disabled.has("codegraph") && isCodegraphInstalled()) {
3476
+ mcps.codegraph = {
3477
+ type: "local",
3478
+ command: "codegraph",
3479
+ args: ["serve", "--mcp"],
3480
+ enabled: true
3481
+ };
3482
+ }
3590
3483
  return mcps;
3591
3484
  }
3592
3485
 
@@ -3624,7 +3517,7 @@ MUST execute at session start:
3624
3517
  3. Check which steps are marked complete
3625
3518
  4. Begin execution from the first incomplete step
3626
3519
 
3627
- If STATE.md does not exist, tell the user: "No STATE.md found. Run \`/fd-new-project\` to initialize."
3520
+ If STATE.md does not exist, tell the user: "No STATE.md found. Run \`/fd-map-codebase\` then \`/fd-new-feature\` to start a feature."
3628
3521
 
3629
3522
  ## Phase Gating
3630
3523
 
@@ -3635,6 +3528,30 @@ If the project is in another phase:
3635
3528
  - **plan** phase: "Run \`/fd-plan\` to create the implementation plan first."
3636
3529
  - **review** phase: "Run \`/fd-verify\` to complete the review phase."
3637
3530
 
3531
+ ## State-First Read Strategy
3532
+
3533
+ Before delegating any agent that needs codebase context:
3534
+ 1. Read \`STATE.md\` — check \`freshnessStatus\` and \`lastUpdatedAt\`
3535
+ 2. Read \`.planning/CODEBASE_INDEX.md\` — check \`freshnessStatus\`
3536
+ 3. If \`freshnessStatus === "fresh"\` AND needed files exist in \`fileSnapshots\`:
3537
+ → Use the existing state. Do NOT re-explore the codebase.
3538
+ → Log: "[StateManager] Skipped codebase exploration — state is fresh"
3539
+ 4. If state is missing, stale, or insufficient:
3540
+ → Delegate to @code-explorer with specific question
3541
+ → After exploration completes, file-tracker auto-publishes to CODEBASE_INDEX.md
3542
+ → Log: "[StateManager] Triggered re-exploration — state was stale"
3543
+
3544
+ State becomes **stale** when:
3545
+ - \`lastUpdatedAt\` > 5 minutes ago
3546
+ - Phase transitions
3547
+ - New plan confirmed
3548
+ - User runs /fd-checkpoint or /fd-resume
3549
+
3550
+ State becomes **fresh** when:
3551
+ - Any agent writes to CODEBASE_INDEX.md
3552
+ - updatePlanningState() is called
3553
+ - file-tracker hook fires after a file edit
3554
+
3638
3555
  ## Step Execution
3639
3556
 
3640
3557
  For each incomplete step in PLAN.md:
@@ -3897,6 +3814,11 @@ var PLANNER_PROMPT = `You create implementation plans that developers can execut
3897
3814
  3. Check for conflicts with existing design decisions
3898
3815
  4. Define new interfaces if needed (before implementation)
3899
3816
 
3817
+ ### Codebase Context First
3818
+ 1. Read \`.planning/CODEBASE_INDEX.md\` — check if freshnessStatus is "fresh"
3819
+ 2. If fresh and needed files are in fileSnapshots, use the existing summaries
3820
+ 3. Only explore the codebase if the index is missing, stale, or incomplete
3821
+
3900
3822
  ### Step Breakdown
3901
3823
  - Each step maps to a single file or closely related file group
3902
3824
  - Steps are ordered by dependency (foundation first, UI last)
@@ -4958,13 +4880,36 @@ var createDocUpdaterAgent = (model, customPrompt, customAppendPrompt) => {
4958
4880
  // src/agents/mapper.ts
4959
4881
  var MAPPER_PROMPT = `You read source files and produce accurate documentation. You report only what you can verify by reading the code directly.
4960
4882
 
4883
+ ## CodeGraph-First Policy
4884
+
4885
+ Before using grep or reading files, check whether codegraph is available:
4886
+
4887
+ Use the \`codegraph\` tool with \`action=check\`. If codegraph is installed and the index is fresh:
4888
+ - Use codegraph MCP tools as your primary source of code understanding
4889
+ - Log: "codegraph available — using symbol index for mapping"
4890
+
4891
+ **Tool selection when codegraph is available:**
4892
+
4893
+ | Mapping task | Preferred Tool |
4894
+ |-------------|----------------|
4895
+ | Map a module / feature area | \`codegraph_context\` |
4896
+ | Find exported symbols | \`codegraph_search\` |
4897
+ | Read a function's source | \`codegraph_node\` |
4898
+ | Survey multiple related symbols | \`codegraph_explore\` |
4899
+ | Trace a data flow | \`codegraph_trace\` |
4900
+ | List files in an area | \`codegraph_files\` |
4901
+
4902
+ The returned source from codegraph is authoritative — do NOT re-open those files unless you need to see something specific codegraph didn't include.
4903
+
4904
+ **If codegraph is NOT available:** fall back to direct file reads as below.
4905
+
4961
4906
  ## Factual-Only Constraint
4962
4907
 
4963
4908
  - If you are not certain about something, write: \`UNKNOWN — needs verification\`
4964
4909
  - Never fill gaps with assumptions or what "probably" works
4965
4910
  - Every claim must be traceable to a specific file and line
4966
4911
 
4967
- ## Reading Source Files
4912
+ ## Reading Source Files (fallback when codegraph unavailable)
4968
4913
 
4969
4914
  - Read files directly using file tools — do not rely on memory
4970
4915
  - Note exact file paths for every claim you make
@@ -4995,13 +4940,14 @@ Write only your assigned file. Read existing \`.codebase/\` files before writing
4995
4940
  - Identify runtime, framework, database, testing, and build tools
4996
4941
 
4997
4942
  ### ARCHITECTURE.md
4943
+ - Use \`codegraph_context\` on entry points to map the architecture (if codegraph available)
4998
4944
  - Identify major components and their responsibilities
4999
4945
  - Map data flow from input to output
5000
4946
  - Document integration points (external APIs, databases, queues)
5001
4947
  - Draw component diagram in text format
5002
4948
 
5003
4949
  ### CONVENTIONS.md
5004
- - Find actual naming patterns by reading source files
4950
+ - Find actual naming patterns by reading source files or using \`codegraph_explore\`
5005
4951
  - Include file:line examples for each pattern
5006
4952
  - Document import style (relative paths? barrel exports? absolute aliases?)
5007
4953
  - Document error handling pattern from real code
@@ -5038,6 +4984,35 @@ var createMapperAgent = (model, customPrompt, customAppendPrompt) => {
5038
4984
  // src/agents/code-explorer.ts
5039
4985
  var CODE_EXPLORER_PROMPT = `You map unfamiliar code before anyone touches it. You are read-only. You report what you find, not what you expect.
5040
4986
 
4987
+ ## CodeGraph-First Policy
4988
+
4989
+ **Before any file exploration, check whether codegraph is available:**
4990
+
4991
+ Use the \`codegraph\` tool with \`action=check\`. If codegraph is installed and the index is fresh:
4992
+ - Use codegraph MCP tools as your primary source of code understanding
4993
+ - This is faster and more accurate than grep + file reads
4994
+ - Log: "codegraph available — using code intelligence index"
4995
+
4996
+ **Tool selection when codegraph is available:**
4997
+
4998
+ | Task | Preferred Tool |
4999
+ |------|----------------|
5000
+ | Map an area or feature | \`codegraph_context\` |
5001
+ | Find a symbol by name | \`codegraph_search\` |
5002
+ | Trace a call path | \`codegraph_trace\` |
5003
+ | Callers of a function | \`codegraph_callers\` |
5004
+ | Callees of a function | \`codegraph_callees\` |
5005
+ | Impact before changing | \`codegraph_impact\` |
5006
+ | Read symbol source | \`codegraph_node\` |
5007
+ | Survey related symbols | \`codegraph_explore\` |
5008
+ | List files in an area | \`codegraph_files\` |
5009
+
5010
+ The returned source from codegraph is complete and authoritative — treat it as already read. Do NOT re-open those files.
5011
+ Reach for grep/Read only to confirm a specific detail codegraph didn't cover.
5012
+
5013
+ **If codegraph is NOT available (not installed or not indexed):**
5014
+ Fall back to direct file exploration using the process below.
5015
+
5041
5016
  ## Your Outputs
5042
5017
 
5043
5018
  **File structure:**
@@ -5060,7 +5035,7 @@ var CODE_EXPLORER_PROMPT = `You map unfamiliar code before anyone touches it. Yo
5060
5035
  - Error handling approach (throw, return, Result type)
5061
5036
  - Testing patterns (file co-location, separate __tests__, naming)
5062
5037
 
5063
- ## Exploration Process
5038
+ ## Exploration Process (fallback when codegraph unavailable)
5064
5039
 
5065
5040
  1. \`ls -la\` the top-level directory — understand the layout
5066
5041
  2. Read \`package.json\`, \`go.mod\`, \`Cargo.toml\`, or equivalent — identify the tech stack and dependencies
@@ -5071,7 +5046,7 @@ var CODE_EXPLORER_PROMPT = `You map unfamiliar code before anyone touches it. Yo
5071
5046
  4. Trace the most important call path relevant to the current task
5072
5047
  5. Read test files to understand expected behavior
5073
5048
 
5074
- ## Quick Commands
5049
+ ## Quick Commands (fallback)
5075
5050
 
5076
5051
  \`\`\`bash
5077
5052
  # Find all TypeScript files
@@ -5089,6 +5064,7 @@ grep -r "export.*functionName" src/
5089
5064
 
5090
5065
  ## Rules
5091
5066
 
5067
+ - **CodeGraph first** — if codegraph index is available, use it before reaching for grep or file reads
5092
5068
  - **Read-only** — never modify files during exploration
5093
5069
  - **State uncertainty** — if you are not sure what something does, say so
5094
5070
  - **Report what you see** — not what you expect or what would make sense
@@ -5099,6 +5075,11 @@ grep -r "export.*functionName" src/
5099
5075
  \`\`\`markdown
5100
5076
  ## Codebase Exploration
5101
5077
 
5078
+ ### CodeGraph Status
5079
+ - installed: yes/no
5080
+ - indexed: yes/no
5081
+ - used: yes/no (if yes: list tools used)
5082
+
5102
5083
  ### Structure
5103
5084
  \`\`\`
5104
5085
  src/
@@ -5125,6 +5106,18 @@ Request → \`src/routes/users.ts:34\` → \`src/services/user-service.ts:89\`
5125
5106
  - \`src/services/user-service.ts\` — core business logic
5126
5107
  - \`src/db/user-repo.ts\` — data access
5127
5108
  - \`src/types/user.ts\` — data model definition
5109
+
5110
+ ## After Exploration
5111
+
5112
+ After completing your exploration, summarize what you found so it can be recorded:
5113
+
5114
+ - **Files explored:** List the paths you actually read or analyzed
5115
+ - **CodeGraph tools used:** List any codegraph MCP tools you invoked
5116
+ - **Key finding:** One-sentence summary of the most important insight
5117
+ - **Ready to proceed:** yes | no — whether you have enough context to continue
5118
+
5119
+ This information is used to update the shared CODEBASE_INDEX.md so subsequent
5120
+ stages can skip redundant exploration.
5128
5121
  \`\`\``;
5129
5122
  var createCodeExplorerAgent = (model, customPrompt, customAppendPrompt) => {
5130
5123
  const prompt = resolvePrompt(CODE_EXPLORER_PROMPT, customPrompt, customAppendPrompt);
@@ -5454,16 +5447,78 @@ var DISCUSSER_PROMPT = `You extract clear requirements through focused questioni
5454
5447
 
5455
5448
  Load \`.planning/PROJECT.md\` first if it exists. Use existing context to avoid asking about already-decided things.
5456
5449
 
5457
- ## Questioning Strategy
5450
+ ## The RecommendedQuestion Format
5451
+
5452
+ Every question you emit to the user MUST be wrapped in a structured recommendation envelope. Never emit a bare question.
5453
+
5454
+ Format:
5455
+ \`\`\`
5456
+ Question:
5457
+ <the actual question>
5458
+
5459
+ Recommendation:
5460
+ <your recommended answer>
5461
+
5462
+ Rationale:
5463
+ <why this recommendation — ground it in repo evidence: cite specific files,
5464
+ prior decisions, tech stack, or policy rules. Do not make recommendations
5465
+ from thin air if the repo already contains evidence.>
5466
+
5467
+ Alternatives:
5468
+ <other valid options, one per line (optional)>
5469
+
5470
+ Default if no response:
5471
+ <what the system does if you receive no reply>
5472
+ \`\`\`
5473
+
5474
+ ## Examples
5475
+
5476
+ ✅ Good (question with recommendation):
5477
+ \`\`\`
5478
+ Question:
5479
+ Should this task use the design-first workflow?
5480
+
5481
+ Recommendation:
5482
+ Yes.
5483
+
5484
+ Rationale:
5485
+ The task description mentions "dashboard" and "UI", which means it is
5486
+ UI-heavy. The codebase has a design agent available (see src/agents/).
5487
+ The supervisor policy in src/agents/supervisor.ts requires design approval
5488
+ for UI-heavy tasks before the execute phase. Starting with design-first
5489
+ is the safest and most expedient path.
5490
+
5491
+ Alternatives:
5492
+ No — skip design and use a lightweight workflow. Faster but riskier for UI work.
5493
+
5494
+ Default if no response:
5495
+ Proceed with design-first workflow (recommendation applied automatically).
5496
+ \`\`\`
5497
+
5498
+ ❌ Bad (bare question — never do this):
5499
+ "What workflow should we use?"
5500
+
5501
+ ❌ Bad (recommendation without rationale):
5502
+ "Should we use TypeScript? Recommendation: Yes. Default: use TypeScript."
5503
+ (Every recommendation needs a rationale grounded in evidence.)
5504
+
5505
+ ## Questioning Rules
5458
5506
 
5459
5507
  - **ONE question per turn** — never ask two questions at once
5460
5508
  - **Follow-up when unclear** — if an answer is ambiguous, ask for clarification before moving on
5461
5509
  - **Targeted focus** — each question uncovers one specific decision
5510
+ - **Grounded recommendations** — base recommendations on PROJECT.md goals, prior DISCUSS.md decisions, tech stack, available agents, or explicit policy rules
5511
+ - **Skip answerable questions** — if the answer is already in PROJECT.md, STATE.md, or prior DISCUSS.md files, skip the question and record it as suppressed
5462
5512
 
5463
- \`\`\`
5464
- ✅ Good: "Should users be able to reset their password via email?"
5513
+ ## Suppressed Questions
5514
+
5515
+ If a question can be answered from exploration evidence, skip it and record it:
5465
5516
 
5466
- ❌ Bad: "What authentication features do you need, and how should password reset work, and do you want social login?"
5517
+ \`\`\`markdown
5518
+ ## Suppressed Questions
5519
+
5520
+ - "What tech stack?" → answered by: tech stack detection (Node.js/TypeScript from package.json)
5521
+ - "Is the project initialised?" → answered by: PROJECT.md exists
5467
5522
  \`\`\`
5468
5523
 
5469
5524
  ## Decision Tracking
@@ -5481,16 +5536,28 @@ D-03: Social login — excluded from MVP scope
5481
5536
 
5482
5537
  ## Conflict Detection
5483
5538
 
5484
- If a new answer conflicts with a previous decision, flag it immediately:
5539
+ If a new answer conflicts with a previous decision, flag it immediately with a RecommendedQuestion:
5485
5540
 
5486
5541
  \`\`\`
5487
5542
  CONFLICT: D-04 (users can stay logged in for 30 days) conflicts with D-01 (JWT, stateless).
5488
- Long-lived JWTs create security risks. Options:
5489
- 1. Use refresh tokens with short-lived access tokens
5490
- 2. Use sessions instead of JWT
5491
- 3. Accept the 30-day JWT with a revocation list
5492
5543
 
5493
- Which do you want?
5544
+ Question:
5545
+ A long-lived JWT creates a security risk. How do you want to handle session persistence?
5546
+
5547
+ Recommendation:
5548
+ Use refresh tokens with short-lived access tokens.
5549
+
5550
+ Rationale:
5551
+ D-01 specified JWT (stateless). Refresh tokens preserve statelessness while
5552
+ allowing short-lived access tokens that limit exposure window. This is the
5553
+ most secure option that satisfies D-01.
5554
+
5555
+ Alternatives:
5556
+ - Use sessions instead of JWT (conflicts with D-01)
5557
+ - Accept 30-day JWT with a revocation list (complex to implement)
5558
+
5559
+ Default if no response:
5560
+ Use refresh tokens with short-lived access tokens (most secure option).
5494
5561
  \`\`\`
5495
5562
 
5496
5563
  ## Saving Decisions
@@ -5508,6 +5575,18 @@ D-01: [topic] — [choice]
5508
5575
  D-02: [topic] — [choice]
5509
5576
  Rationale: [why]
5510
5577
 
5578
+ ## Answered Recommendations
5579
+
5580
+ RQ-01: [question]
5581
+ Recommendation: [recommended answer]
5582
+ User choice: [what they said]
5583
+ Rationale: [why the system recommended it]
5584
+ Stage: discuss
5585
+
5586
+ ## Suppressed Questions
5587
+
5588
+ - "<question>" → answered by: <evidence source>
5589
+
5511
5590
  ## Open Questions
5512
5591
  - [anything unresolved]
5513
5592
 
@@ -5515,40 +5594,6 @@ D-02: [topic] — [choice]
5515
5594
  - [explicitly excluded items]
5516
5595
  \`\`\`
5517
5596
 
5518
- ## Question Bank
5519
-
5520
- Use these question categories to ensure thorough coverage:
5521
-
5522
- **Scope:**
5523
- - What is included in this feature?
5524
- - What is explicitly excluded?
5525
- - What is the MVP vs. nice-to-have?
5526
-
5527
- **Constraints:**
5528
- - Timeline or deadline?
5529
- - Budget or infrastructure limits?
5530
- - Technology constraints (must use X, cannot use Y)?
5531
-
5532
- **Integration:**
5533
- - Does this interact with existing systems?
5534
- - External APIs or services needed?
5535
-
5536
- **User experience:**
5537
- - Walk me through the user flow step by step
5538
- - What happens when something goes wrong?
5539
-
5540
- **Error handling:**
5541
- - What should happen when [specific failure] occurs?
5542
- - Who is notified on failure?
5543
-
5544
- **Performance:**
5545
- - How many users / requests / records expected?
5546
- - Acceptable response time?
5547
-
5548
- **Security:**
5549
- - Who can access this feature?
5550
- - What data is sensitive?
5551
-
5552
5597
  ## Completion Criteria
5553
5598
 
5554
5599
  Discussion is complete when:
@@ -6327,7 +6372,7 @@ You sit above the orchestrator's execution path. Your only job is to inspect an
6327
6372
 
6328
6373
  fd-ask, fd-checkpoint, fd-deploy-check, fd-design, fd-discuss, fd-doctor,
6329
6374
  fd-execute, fd-fix-bug, fd-map-codebase, fd-multi-repo, fd-new-feature,
6330
- fd-new-project, fd-plan, fd-quick, fd-reflect, fd-resume, fd-status,
6375
+ fd-plan, fd-quick, fd-reflect, fd-resume, fd-status,
6331
6376
  fd-suggest, fd-translate-intent, fd-verify, fd-write-docs
6332
6377
 
6333
6378
  ## Registered Agents (source of truth — do not add to this list)
@@ -6919,7 +6964,6 @@ var REGISTERED_COMMANDS = [
6919
6964
  "fd-map-codebase",
6920
6965
  "fd-multi-repo",
6921
6966
  "fd-new-feature",
6922
- "fd-new-project",
6923
6967
  "fd-plan",
6924
6968
  "fd-quick",
6925
6969
  "fd-reflect",
@@ -6928,7 +6972,8 @@ var REGISTERED_COMMANDS = [
6928
6972
  "fd-suggest",
6929
6973
  "fd-translate-intent",
6930
6974
  "fd-verify",
6931
- "fd-write-docs"
6975
+ "fd-write-docs",
6976
+ "fd-done"
6932
6977
  ];
6933
6978
  function resolveSupervisorConfig(directory) {
6934
6979
  try {
@@ -7065,12 +7110,12 @@ function computeConfidence(exists, policyResult, ctx) {
7065
7110
  return 0.45;
7066
7111
  return 0.95;
7067
7112
  }
7068
- function resolveDecision(exists, policyResult, confidenceScore, threshold, ctx) {
7113
+ function resolveDecision(exists, policyResult, confidenceScore, threshold, ctx, clarificationQuestion) {
7069
7114
  if (!exists) {
7070
7115
  return { decision: "block", approvalStatus: "denied" };
7071
7116
  }
7072
7117
  if (ctx.approvalRequired && !ctx.approvalGranted) {
7073
- return { decision: "escalate", approvalStatus: "escalated" };
7118
+ return { decision: "escalate", approvalStatus: "escalated", clarificationQuestion };
7074
7119
  }
7075
7120
  if (!policyResult.passed) {
7076
7121
  if (policyResult.requiredChanges.length > 0) {
@@ -7079,11 +7124,11 @@ function resolveDecision(exists, policyResult, confidenceScore, threshold, ctx)
7079
7124
  return { decision: "block", approvalStatus: "denied" };
7080
7125
  }
7081
7126
  if (confidenceScore < threshold) {
7082
- return { decision: "escalate", approvalStatus: "escalated" };
7127
+ return { decision: "escalate", approvalStatus: "escalated", clarificationQuestion };
7083
7128
  }
7084
7129
  return { decision: "approve", approvalStatus: "approved" };
7085
7130
  }
7086
- function runSupervisorReview(directory, targetName, ctx = {}) {
7131
+ function runSupervisorReview(directory, targetName, ctx = {}, clarificationQuestion) {
7087
7132
  const config = resolveSupervisorConfig(directory);
7088
7133
  const reviewPhase = ctx.reviewPhase ?? "preflight";
7089
7134
  const timestamp2 = new Date().toISOString();
@@ -7131,7 +7176,7 @@ function runSupervisorReview(directory, targetName, ctx = {}) {
7131
7176
  }
7132
7177
  const policyResult = targetType === "command" ? checkCommandPolicy(targetName, ctx) : checkAgentPolicy(targetName, ctx);
7133
7178
  const confidenceScore = computeConfidence(exists, policyResult, ctx);
7134
- const { decision, approvalStatus } = resolveDecision(exists, policyResult, confidenceScore, config.confidenceThreshold, ctx);
7179
+ const { decision, approvalStatus, clarificationQuestion: escalationQuestion } = resolveDecision(exists, policyResult, confidenceScore, config.confidenceThreshold, ctx, clarificationQuestion);
7135
7180
  const reasons = policyResult.reasons.length > 0 ? policyResult.reasons : decision === "approve" ? [`Target "${targetName}" reviewed and approved for execution`] : [`Target "${targetName}" reviewed — decision: ${decision}`];
7136
7181
  const supervisorDecision = {
7137
7182
  decision,
@@ -7145,7 +7190,8 @@ function runSupervisorReview(directory, targetName, ctx = {}) {
7145
7190
  approvalStatus,
7146
7191
  confidenceScore,
7147
7192
  reviewPhase,
7148
- timestamp: timestamp2
7193
+ timestamp: timestamp2,
7194
+ ...escalationQuestion ? { clarificationQuestion: escalationQuestion } : {}
7149
7195
  };
7150
7196
  _emitTelemetry(directory, supervisorDecision, ctx);
7151
7197
  return supervisorDecision;
@@ -7185,13 +7231,13 @@ function _emitTelemetry(directory, decision, ctx) {
7185
7231
  // src/index.ts
7186
7232
  function loadRulePaths() {
7187
7233
  const __dir = dirname4(fileURLToPath2(import.meta.url));
7188
- const rulesDir = join26(__dir, "..", "src", "rules");
7189
- if (!existsSync27(rulesDir))
7234
+ const rulesDir = join25(__dir, "..", "src", "rules");
7235
+ if (!existsSync26(rulesDir))
7190
7236
  return [];
7191
7237
  const paths = [];
7192
7238
  function walk(dir) {
7193
7239
  for (const entry of readdirSync3(dir, { withFileTypes: true })) {
7194
- const full = join26(dir, entry.name);
7240
+ const full = join25(dir, entry.name);
7195
7241
  if (entry.isDirectory()) {
7196
7242
  walk(full);
7197
7243
  } else if (entry.isFile() && entry.name.endsWith(".md") && entry.name !== "README.md") {
@@ -7204,8 +7250,8 @@ function loadRulePaths() {
7204
7250
  }
7205
7251
  function loadCommands() {
7206
7252
  const __dir = dirname4(fileURLToPath2(import.meta.url));
7207
- const commandsDir = join26(__dir, "..", "src", "commands");
7208
- if (!existsSync27(commandsDir))
7253
+ const commandsDir = join25(__dir, "..", "src", "commands");
7254
+ if (!existsSync26(commandsDir))
7209
7255
  return {};
7210
7256
  const commands = {};
7211
7257
  try {
@@ -7213,7 +7259,7 @@ function loadCommands() {
7213
7259
  if (!file.endsWith(".md"))
7214
7260
  continue;
7215
7261
  const name = basename(file, ".md");
7216
- const raw = readFileSync23(join26(commandsDir, file), "utf-8");
7262
+ const raw = readFileSync24(join25(commandsDir, file), "utf-8");
7217
7263
  let description;
7218
7264
  let template = raw;
7219
7265
  const fmMatch = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/);
@@ -7243,6 +7289,7 @@ var plugin = async (input, _options) => {
7243
7289
  const orchestratorGuard = new OrchestratorGuard;
7244
7290
  const appLog = (msg) => client.app.log({ body: { service: "flowdeck", level: "info", message: msg } }).catch(() => {});
7245
7291
  const autoLearnHook = createAutoLearnHook(client, fileTracker, directory, appLog);
7292
+ const notifCtrl = new NotificationController(undefined, appLog);
7246
7293
  const agentConfigs = getAgentConfigs({});
7247
7294
  const mcps = createFlowDeckMcps();
7248
7295
  return {
@@ -7294,8 +7341,8 @@ var plugin = async (input, _options) => {
7294
7341
  }
7295
7342
  }
7296
7343
  }
7297
- const skillsDir = join26(dirname4(fileURLToPath2(import.meta.url)), "..", "src", "skills");
7298
- if (existsSync27(skillsDir)) {
7344
+ const skillsDir = join25(dirname4(fileURLToPath2(import.meta.url)), "..", "src", "skills");
7345
+ if (existsSync26(skillsDir)) {
7299
7346
  const cfgAny = cfg;
7300
7347
  if (!cfgAny.skills || typeof cfgAny.skills !== "object") {
7301
7348
  cfgAny.skills = { paths: [] };
@@ -7336,51 +7383,33 @@ var plugin = async (input, _options) => {
7336
7383
  "context-generator": contextGeneratorTool,
7337
7384
  "create-skill": createSkillTool,
7338
7385
  reflect: reflectTool,
7339
- "memory-search": memorySearchTool,
7340
- "memory-status": memoryStatusTool
7386
+ codegraph: codegraphTool
7341
7387
  },
7342
7388
  "shell.env": shellEnvHook,
7343
7389
  "todo.updated": todoHook,
7344
7390
  "file.edited": fileEdited,
7345
7391
  "file.watcher.updated": fileWatcherUpdated,
7346
7392
  "experimental.session.compacting": compactionHook,
7393
+ "command.execute.before": async (_input, _output) => {},
7347
7394
  "permission.ask": async (input2, _output) => {
7348
7395
  notifyPermissionNeeded(input2.title);
7349
7396
  },
7350
7397
  event: async ({ event }) => {
7351
7398
  const type = event?.type ?? "";
7352
- try {
7353
- if (type === "session.created" || type === "session.started") {
7354
- const sessionId = event?.sessionID ?? event?.sessionId ?? "";
7355
- if (sessionId) {
7356
- memoryHook.onSessionCreated(directory, sessionId, event?.prompt);
7357
- }
7358
- await sessionStartHook({ directory });
7359
- } else if (type === "message.updated") {
7360
- const msgEvent = event?.event ?? event;
7361
- const sessionId = msgEvent?.sessionID ?? msgEvent?.sessionId ?? "";
7362
- if (sessionId) {
7363
- memoryHook.onMessageUpdated(sessionId, msgEvent.role, msgEvent.content, directory);
7364
- }
7365
- } else if (type === "session.compacted") {
7366
- const compactEvent = event?.event ?? event;
7367
- const sessionId = compactEvent?.sessionID ?? compactEvent?.sessionId ?? "";
7368
- if (sessionId) {
7369
- memoryHook.onSessionCompact(sessionId, compactEvent.summary ?? "");
7370
- }
7371
- } else if (type === "session.deleted") {
7372
- const delEvent = event?.event ?? event;
7373
- const sessionId = delEvent?.sessionID ?? delEvent?.sessionId ?? "";
7374
- if (sessionId) {
7375
- memoryHook.onSessionEnd(sessionId);
7376
- }
7399
+ if (type === "session.created" || type === "session.started") {
7400
+ await sessionStartHook({ directory });
7401
+ }
7402
+ if (type === "command.executed") {
7403
+ const commandName = event?.properties?.name ?? "";
7404
+ if (commandName) {
7405
+ notifCtrl.onCommandExecuted(commandName);
7377
7406
  }
7378
- } catch (err) {
7379
- console.error("[FlowDeck Memory] Event handler error:", err);
7380
7407
  }
7381
7408
  await contextMonitor.event({ event });
7382
7409
  orchestratorGuard.onEvent(event);
7383
7410
  if (type === "session.idle") {
7411
+ const hasEdits = fileTracker.getEditedPaths().length > 0;
7412
+ notifCtrl.onSessionIdle(hasEdits);
7384
7413
  try {
7385
7414
  await sessionIdleHook();
7386
7415
  await autoLearnHook();
@@ -7388,6 +7417,11 @@ var plugin = async (input, _options) => {
7388
7417
  fileTracker.clear();
7389
7418
  }
7390
7419
  }
7420
+ if (type === "session.error") {
7421
+ const err = event?.properties?.error;
7422
+ const errorMsg = (err && typeof err === "object" && "message" in err ? String(err.message) : undefined) ?? (typeof err === "string" ? err : undefined) ?? "An unexpected error occurred";
7423
+ notifCtrl.onSessionError(errorMsg);
7424
+ }
7391
7425
  },
7392
7426
  "tool.execute.before": async (toolInput, toolOutput) => {
7393
7427
  if ((toolInput.tool === "read" || toolInput.tool === "view") && toolOutput?.args) {
@@ -7437,14 +7471,6 @@ var plugin = async (input, _options) => {
7437
7471
  },
7438
7472
  "tool.execute.after": async (toolInput, toolOutput) => {
7439
7473
  await telemetryAfterHook({ directory }, toolInput, toolOutput);
7440
- try {
7441
- const sessionId = toolInput?.sessionID ?? toolInput?.sessionId ?? "";
7442
- if (sessionId && toolInput?.tool) {
7443
- memoryHook.onToolExecuted(sessionId, toolInput.tool, toolInput, toolOutput?.output ?? null, directory);
7444
- }
7445
- } catch (err) {
7446
- console.error("[FlowDeck Memory] Tool execution error:", err);
7447
- }
7448
7474
  const afterToolName = toolInput.tool ?? toolInput.name ?? "";
7449
7475
  if (afterToolName === "delegate" || afterToolName === "run-pipeline") {
7450
7476
  try {