@dv.nghiem/flowdeck 0.3.8 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. package/README.md +13 -122
  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.map +1 -1
  9. package/dist/hooks/file-tracker.d.ts +6 -0
  10. package/dist/hooks/file-tracker.d.ts.map +1 -1
  11. package/dist/hooks/notifications.d.ts.map +1 -1
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +752 -785
  14. package/dist/lib/completion-validator.d.ts +51 -0
  15. package/dist/lib/completion-validator.d.ts.map +1 -0
  16. package/dist/lib/recommended-question.d.ts +24 -0
  17. package/dist/lib/recommended-question.d.ts.map +1 -0
  18. package/dist/lib/research-gate.d.ts +97 -0
  19. package/dist/lib/research-gate.d.ts.map +1 -0
  20. package/dist/lib/research-gate.test.d.ts +2 -0
  21. package/dist/lib/research-gate.test.d.ts.map +1 -0
  22. package/dist/mcp/index.d.ts +14 -3
  23. package/dist/mcp/index.d.ts.map +1 -1
  24. package/dist/services/codegraph.d.ts +36 -0
  25. package/dist/services/codegraph.d.ts.map +1 -0
  26. package/dist/services/codegraph.test.d.ts +2 -0
  27. package/dist/services/codegraph.test.d.ts.map +1 -0
  28. package/dist/services/command-validator.d.ts +11 -0
  29. package/dist/services/command-validator.d.ts.map +1 -1
  30. package/dist/services/preflight-explorer.d.ts +130 -0
  31. package/dist/services/preflight-explorer.d.ts.map +1 -0
  32. package/dist/services/preflight-explorer.test.d.ts +25 -0
  33. package/dist/services/preflight-explorer.test.d.ts.map +1 -0
  34. package/dist/services/question-guard.d.ts +96 -0
  35. package/dist/services/question-guard.d.ts.map +1 -0
  36. package/dist/services/quick-router.d.ts +40 -1
  37. package/dist/services/quick-router.d.ts.map +1 -1
  38. package/dist/services/recommended-question.test.d.ts +2 -0
  39. package/dist/services/recommended-question.test.d.ts.map +1 -0
  40. package/dist/services/supervisor-binding.d.ts +3 -1
  41. package/dist/services/supervisor-binding.d.ts.map +1 -1
  42. package/dist/tools/codebase-index.d.ts +30 -0
  43. package/dist/tools/codebase-index.d.ts.map +1 -0
  44. package/dist/tools/codebase-index.test.d.ts +2 -0
  45. package/dist/tools/codebase-index.test.d.ts.map +1 -0
  46. package/dist/tools/codegraph-tool.d.ts +3 -0
  47. package/dist/tools/codegraph-tool.d.ts.map +1 -0
  48. package/dist/tools/planning-state-lib.d.ts +23 -0
  49. package/dist/tools/planning-state-lib.d.ts.map +1 -1
  50. package/docs/agents/index.md +154 -0
  51. package/docs/commands/fd-ask.md +71 -39
  52. package/docs/commands/fd-checkpoint.md +63 -8
  53. package/docs/commands/fd-deploy-check.md +166 -9
  54. package/docs/commands/fd-design.md +101 -0
  55. package/docs/commands/fd-discuss.md +87 -20
  56. package/docs/commands/fd-doctor.md +100 -13
  57. package/docs/commands/fd-done.md +215 -0
  58. package/docs/commands/fd-execute.md +104 -0
  59. package/docs/commands/fd-fix-bug.md +144 -24
  60. package/docs/commands/fd-map-codebase.md +85 -21
  61. package/docs/commands/fd-multi-repo.md +155 -40
  62. package/docs/commands/fd-new-feature.md +63 -19
  63. package/docs/commands/fd-plan.md +80 -27
  64. package/docs/commands/fd-quick.md +143 -29
  65. package/docs/commands/fd-reflect.md +81 -13
  66. package/docs/commands/fd-resume.md +65 -8
  67. package/docs/commands/fd-status.md +80 -12
  68. package/docs/commands/fd-suggest.md +114 -0
  69. package/docs/commands/fd-translate-intent.md +69 -9
  70. package/docs/commands/fd-verify.md +71 -14
  71. package/docs/commands/fd-write-docs.md +121 -8
  72. package/docs/concepts/architecture.md +163 -0
  73. package/docs/concepts/governance.md +242 -0
  74. package/docs/concepts/intelligence.md +145 -0
  75. package/docs/concepts/multi-repo.md +227 -0
  76. package/docs/concepts/workflows.md +205 -0
  77. package/docs/configuration/index.md +208 -0
  78. package/docs/configuration/opencode-settings.md +98 -0
  79. package/docs/getting-started/first-project.md +126 -0
  80. package/docs/getting-started/installation.md +73 -0
  81. package/docs/getting-started/quick-start.md +74 -0
  82. package/docs/index.md +36 -72
  83. package/docs/reference/hooks.md +176 -0
  84. package/docs/reference/rules.md +109 -0
  85. package/docs/skills/code-review.md +47 -0
  86. package/docs/skills/index.md +148 -0
  87. package/docs/skills/planning.md +39 -0
  88. package/package.json +1 -1
  89. package/src/commands/fd-deploy-check.md +2 -2
  90. package/src/commands/fd-discuss.md +128 -7
  91. package/src/commands/fd-done.md +196 -0
  92. package/src/commands/fd-execute.md +43 -6
  93. package/src/commands/fd-fix-bug.md +43 -6
  94. package/src/commands/fd-map-codebase.md +100 -20
  95. package/src/commands/fd-multi-repo.md +1 -1
  96. package/src/commands/fd-new-feature.md +14 -5
  97. package/src/commands/fd-plan.md +38 -1
  98. package/src/commands/fd-quick.md +77 -14
  99. package/src/commands/fd-resume.md +1 -1
  100. package/src/commands/fd-status.md +1 -1
  101. package/src/commands/fd-verify.md +16 -2
  102. package/src/commands/fd-write-docs.md +30 -5
  103. package/src/rules/common/behavioral.md +63 -0
  104. package/src/skills/codebase-mapping/SKILL.md +1 -1
  105. package/src/skills/context-load/SKILL.md +1 -1
  106. package/src/skills/multi-repo/SKILL.md +1 -1
  107. package/src/skills/repo-memory-graph/SKILL.md +1 -1
  108. package/dist/hooks/memory-hook.d.ts +0 -28
  109. package/dist/hooks/memory-hook.d.ts.map +0 -1
  110. package/dist/services/memory-store.d.ts +0 -73
  111. package/dist/services/memory-store.d.ts.map +0 -1
  112. package/dist/services/memory-store.test.d.ts +0 -2
  113. package/dist/services/memory-store.test.d.ts.map +0 -1
  114. package/dist/tools/memory-search.d.ts +0 -3
  115. package/dist/tools/memory-search.d.ts.map +0 -1
  116. package/dist/tools/memory-status.d.ts +0 -3
  117. package/dist/tools/memory-status.d.ts.map +0 -1
  118. package/docs/USER_GUIDE.md +0 -20
  119. package/docs/agents.md +0 -544
  120. package/docs/best-practices.md +0 -47
  121. package/docs/commands/fd-new-project.md +0 -24
  122. package/docs/commands.md +0 -557
  123. package/docs/configuration.md +0 -325
  124. package/docs/design-first-workflow.md +0 -94
  125. package/docs/feature-integration-architecture.md +0 -227
  126. package/docs/installation.md +0 -123
  127. package/docs/intelligence.md +0 -370
  128. package/docs/memory.md +0 -69
  129. package/docs/multi-repo.md +0 -201
  130. package/docs/notifications.md +0 -170
  131. package/docs/optimization-baseline.md +0 -21
  132. package/docs/quick-start.md +0 -197
  133. package/docs/rules.md +0 -432
  134. package/docs/skills.md +0 -417
  135. package/docs/workflows.md +0 -134
  136. 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
1810
  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
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 ?? ""
1858
1819
  };
1859
1820
  }
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
- return {
1919
- ...row,
1920
- metadata: row.metadata ? JSON.parse(row.metadata) : null
1921
- };
1922
- }
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
- }
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
1863
  }
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,23 +2235,23 @@ 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)) {
2455
- throw new Error(`[flowdeck] WARNING: .codebase/ not found. Run /map-codebase to map the codebase.`);
2253
+ if (!existsSync17(codebaseDirectory)) {
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);
2458
2257
  if (execMode === "review-only") {
@@ -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,18 +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),
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),
2694
2493
  ...workspaceRoot && config?.sub_repos ? {
2695
2494
  flowdeck_workspace_root: workspaceRoot,
2696
2495
  flowdeck_sub_repos: config.sub_repos,
@@ -2701,7 +2500,7 @@ async function sessionStartHook(ctx) {
2701
2500
  }
2702
2501
  try {
2703
2502
  const stateFilePath = statePath(ctx.directory);
2704
- const content = readFileSync17(stateFilePath, "utf-8");
2503
+ const content = readFileSync18(stateFilePath, "utf-8");
2705
2504
  const state = parseState(content);
2706
2505
  const currentPhase = state["current_phase"] || {};
2707
2506
  const result = {
@@ -2709,7 +2508,7 @@ async function sessionStartHook(ctx) {
2709
2508
  flowdeck_status: currentPhase["status"] ?? null,
2710
2509
  flowdeck_steps_pending: currentPhase["steps_pending"] ?? null,
2711
2510
  flowdeck_last_action: currentPhase["last_action"] ?? null,
2712
- flowdeck_has_codebase: existsSync20(codebaseDirectory)
2511
+ flowdeck_has_codebase: existsSync19(codebaseDirectory)
2713
2512
  };
2714
2513
  if (workspaceRoot && config?.sub_repos && config.sub_repos.length > 0) {
2715
2514
  result.flowdeck_workspace_root = workspaceRoot;
@@ -2724,7 +2523,7 @@ async function sessionStartHook(ctx) {
2724
2523
  flowdeck_phase: null,
2725
2524
  flowdeck_status: "error",
2726
2525
  flowdeck_warning: "State file unreadable. Continuing without flowdeck context.",
2727
- flowdeck_has_codebase: existsSync20(codebaseDirectory)
2526
+ flowdeck_has_codebase: existsSync19(codebaseDirectory)
2728
2527
  };
2729
2528
  if (workspaceRoot && config?.sub_repos && config.sub_repos.length > 0) {
2730
2529
  result.flowdeck_workspace_root = workspaceRoot;
@@ -2741,15 +2540,18 @@ import { execFile } from "child_process";
2741
2540
  var INTERACTIVE_COMMANDS = new Set([
2742
2541
  "discuss",
2743
2542
  "plan",
2744
- "review-code",
2745
2543
  "deploy-check",
2746
- "new-project"
2544
+ "ask",
2545
+ "resume"
2747
2546
  ]);
2748
2547
  var COMPLETION_COMMANDS = new Set([
2749
2548
  "new-feature",
2750
2549
  "fix-bug",
2751
2550
  "write-docs",
2752
- "checkpoint"
2551
+ "checkpoint",
2552
+ "done",
2553
+ "execute",
2554
+ "verify"
2753
2555
  ]);
2754
2556
  function notify(title, body, level = "info") {
2755
2557
  const platform = process.platform;
@@ -2782,16 +2584,24 @@ function tryTerminalBell() {
2782
2584
  process.stdout.write("\x07");
2783
2585
  } catch {}
2784
2586
  }
2587
+ function notifyCommandInteraction(command) {
2588
+ const name = command.replace(/^\//, "").replace(/^fd-/, "");
2589
+ if (INTERACTIVE_COMMANDS.has(name)) {
2590
+ notify(`FlowDeck: /${name}`, "Your input is needed — please check OpenCode", "critical");
2591
+ } else if (COMPLETION_COMMANDS.has(name)) {
2592
+ notify(`FlowDeck: /${name} complete`, "Review the output and choose your next step", "info");
2593
+ }
2594
+ }
2785
2595
  function notifySessionIdle() {
2786
2596
  notify("FlowDeck Task Completed", "Agent is idle and waiting for your next instruction", "info");
2787
2597
  }
2788
- function notifyPermissionNeeded(tool18) {
2789
- notify("FlowDeck Permission Required", `Agent needs approval to use tool: ${tool18}`, "critical");
2598
+ function notifyPermissionNeeded(tool17) {
2599
+ notify("FlowDeck Permission Required", `Agent needs approval to use tool: ${tool17}`, "critical");
2790
2600
  }
2791
2601
 
2792
2602
  // src/hooks/patch-trust.ts
2793
- import { existsSync as existsSync21, readFileSync as readFileSync18 } from "fs";
2794
- import { join as join20 } from "path";
2603
+ import { existsSync as existsSync20, readFileSync as readFileSync19 } from "fs";
2604
+ import { join as join19 } from "path";
2795
2605
  var HIGH_RISK_KEYWORDS = [
2796
2606
  "password",
2797
2607
  "secret",
@@ -2813,11 +2623,11 @@ var HIGH_RISK_KEYWORDS = [
2813
2623
  "privilege"
2814
2624
  ];
2815
2625
  function loadVolatility(directory) {
2816
- const p = join20(codebaseDir(directory), "VOLATILITY.json");
2817
- if (!existsSync21(p))
2626
+ const p = join19(codebaseDir(directory), "VOLATILITY.json");
2627
+ if (!existsSync20(p))
2818
2628
  return {};
2819
2629
  try {
2820
- const data = JSON.parse(readFileSync18(p, "utf-8"));
2630
+ const data = JSON.parse(readFileSync19(p, "utf-8"));
2821
2631
  const map = {};
2822
2632
  for (const entry of data.entries ?? [])
2823
2633
  map[entry.path] = entry.stability;
@@ -2827,11 +2637,11 @@ function loadVolatility(directory) {
2827
2637
  }
2828
2638
  }
2829
2639
  function loadFailedPaths(directory) {
2830
- const p = join20(codebaseDir(directory), "FAILURES.json");
2831
- if (!existsSync21(p))
2640
+ const p = join19(codebaseDir(directory), "FAILURES.json");
2641
+ if (!existsSync20(p))
2832
2642
  return [];
2833
2643
  try {
2834
- const data = JSON.parse(readFileSync18(p, "utf-8"));
2644
+ const data = JSON.parse(readFileSync19(p, "utf-8"));
2835
2645
  return (data.entries ?? []).flatMap((e) => e.affected_paths ?? []);
2836
2646
  } catch {
2837
2647
  return [];
@@ -2896,8 +2706,8 @@ async function patchTrustHook(ctx, input, output) {
2896
2706
  }
2897
2707
 
2898
2708
  // src/hooks/decision-trace-hook.ts
2899
- import { existsSync as existsSync22, mkdirSync as mkdirSync11, appendFileSync as appendFileSync3 } from "fs";
2900
- import { join as join21 } from "path";
2709
+ import { existsSync as existsSync21, mkdirSync as mkdirSync11, appendFileSync as appendFileSync3 } from "fs";
2710
+ import { join as join20 } from "path";
2901
2711
  async function decisionTraceHook(ctx, input, output) {
2902
2712
  if (input.tool !== "write" && input.tool !== "edit")
2903
2713
  return;
@@ -2906,7 +2716,7 @@ async function decisionTraceHook(ctx, input, output) {
2906
2716
  return;
2907
2717
  const base = codebaseDir(ctx.directory);
2908
2718
  try {
2909
- if (!existsSync22(base))
2719
+ if (!existsSync21(base))
2910
2720
  mkdirSync11(base, { recursive: true });
2911
2721
  const entry = {
2912
2722
  timestamp: new Date().toISOString(),
@@ -2919,23 +2729,23 @@ async function decisionTraceHook(ctx, input, output) {
2919
2729
  risk_level: "unknown",
2920
2730
  auto_recorded: true
2921
2731
  };
2922
- appendFileSync3(join21(base, "DECISIONS.jsonl"), JSON.stringify(entry) + `
2732
+ appendFileSync3(join20(base, "DECISIONS.jsonl"), JSON.stringify(entry) + `
2923
2733
  `, "utf-8");
2924
2734
  } catch {}
2925
2735
  }
2926
2736
 
2927
2737
  // src/services/telemetry.ts
2928
- import { existsSync as existsSync23, readFileSync as readFileSync19, appendFileSync as appendFileSync4, mkdirSync as mkdirSync12 } from "fs";
2929
- import { join as join22 } from "path";
2738
+ import { existsSync as existsSync22, readFileSync as readFileSync20, appendFileSync as appendFileSync4, mkdirSync as mkdirSync12 } from "fs";
2739
+ import { join as join21 } from "path";
2930
2740
  import { randomUUID } from "crypto";
2931
2741
  function telemetryPath(dir) {
2932
- return join22(codebaseDir(dir), "TELEMETRY.jsonl");
2742
+ return join21(codebaseDir(dir), "TELEMETRY.jsonl");
2933
2743
  }
2934
2744
  function appendEvent(dir, partial) {
2935
2745
  if (process.env.TELEMETRY_ENABLED !== "true")
2936
2746
  return null;
2937
2747
  const cd = codebaseDir(dir);
2938
- if (!existsSync23(cd))
2748
+ if (!existsSync22(cd))
2939
2749
  mkdirSync12(cd, { recursive: true });
2940
2750
  const event = {
2941
2751
  id: randomUUID(),
@@ -2972,34 +2782,34 @@ function inferStatus(output) {
2972
2782
  }
2973
2783
  async function telemetryHook(context, toolInput, output) {
2974
2784
  const dir = context.directory ?? process.cwd();
2975
- const tool18 = toolInput.name ?? toolInput.tool ?? "unknown";
2785
+ const tool17 = toolInput.name ?? toolInput.tool ?? "unknown";
2976
2786
  const ids = resolveIds(toolInput);
2977
2787
  appendEvent(dir, {
2978
2788
  session_id: ids.session_id,
2979
2789
  run_id: ids.run_id,
2980
2790
  event: "tool.call",
2981
- tool: tool18,
2791
+ tool: tool17,
2982
2792
  status: "ok",
2983
2793
  meta: { parameters: output.args ?? {} }
2984
2794
  });
2985
2795
  }
2986
2796
  async function telemetryAfterHook(context, toolInput, output) {
2987
2797
  const dir = context.directory ?? process.cwd();
2988
- const tool18 = toolInput.name ?? toolInput.tool ?? "unknown";
2798
+ const tool17 = toolInput.name ?? toolInput.tool ?? "unknown";
2989
2799
  const ids = resolveIds(toolInput);
2990
2800
  const status = inferStatus(output);
2991
2801
  appendEvent(dir, {
2992
2802
  session_id: ids.session_id,
2993
2803
  run_id: ids.run_id,
2994
2804
  event: "tool.complete",
2995
- tool: tool18,
2805
+ tool: tool17,
2996
2806
  status
2997
2807
  });
2998
2808
  }
2999
2809
 
3000
2810
  // src/services/approval-manager.ts
3001
- import { existsSync as existsSync24, readFileSync as readFileSync20, writeFileSync as writeFileSync14, mkdirSync as mkdirSync13 } from "fs";
3002
- import { join as join23 } from "path";
2811
+ import { existsSync as existsSync23, readFileSync as readFileSync21, writeFileSync as writeFileSync15, mkdirSync as mkdirSync13 } from "fs";
2812
+ import { join as join22 } from "path";
3003
2813
  var APPROVAL_TTL_MS = 30 * 60 * 1000;
3004
2814
  var SENSITIVE_PATTERNS = [
3005
2815
  /auth/i,
@@ -3036,14 +2846,14 @@ function isSensitivePath(filePath) {
3036
2846
  return SENSITIVE_PATTERNS.some((p) => p.test(filePath));
3037
2847
  }
3038
2848
  function approvalsPath(dir) {
3039
- return join23(codebaseDir(dir), "APPROVALS.json");
2849
+ return join22(codebaseDir(dir), "APPROVALS.json");
3040
2850
  }
3041
2851
  function loadStore2(dir) {
3042
2852
  const p = approvalsPath(dir);
3043
- if (!existsSync24(p))
2853
+ if (!existsSync23(p))
3044
2854
  return { requests: [] };
3045
2855
  try {
3046
- return JSON.parse(readFileSync20(p, "utf-8"));
2856
+ return JSON.parse(readFileSync21(p, "utf-8"));
3047
2857
  } catch {
3048
2858
  return { requests: [] };
3049
2859
  }
@@ -3061,8 +2871,8 @@ async function approvalHook(context, toolInput, output) {
3061
2871
  if (!ENABLED2)
3062
2872
  return;
3063
2873
  const dir = context.directory ?? process.cwd();
3064
- const tool18 = toolInput.name ?? toolInput.tool ?? "";
3065
- if (!WRITE_TOOLS.has(tool18))
2874
+ const tool17 = toolInput.name ?? toolInput.tool ?? "";
2875
+ if (!WRITE_TOOLS.has(tool17))
3066
2876
  return;
3067
2877
  const args = output.args ?? {};
3068
2878
  const filePath = String(args.path ?? args.file_path ?? args.filename ?? "");
@@ -3077,7 +2887,7 @@ async function approvalHook(context, toolInput, output) {
3077
2887
  session_id: process.env.OPENCODE_SESSION_ID ?? "session-0",
3078
2888
  run_id: process.env.OPENCODE_RUN_ID ?? "run-0",
3079
2889
  event: "approval.request",
3080
- tool: tool18,
2890
+ tool: tool17,
3081
2891
  status: "blocked",
3082
2892
  files: [filePath],
3083
2893
  meta: { trigger: "sensitive_file", file: filePath }
@@ -3138,8 +2948,8 @@ function createContextWindowMonitorHook() {
3138
2948
  }
3139
2949
 
3140
2950
  // src/hooks/shell-env-hook.ts
3141
- import { existsSync as existsSync25, readFileSync as readFileSync21 } from "fs";
3142
- import { join as join24 } from "path";
2951
+ import { existsSync as existsSync24, readFileSync as readFileSync22 } from "fs";
2952
+ import { join as join23 } from "path";
3143
2953
  import { createRequire } from "module";
3144
2954
  var _version;
3145
2955
  function getVersion() {
@@ -3175,7 +2985,7 @@ var MARKER_TO_LANG = {
3175
2985
  };
3176
2986
  function detectPackageManager(root) {
3177
2987
  for (const [lockfile, pm] of Object.entries(LOCKFILE_TO_PM)) {
3178
- if (existsSync25(join24(root, lockfile)))
2988
+ if (existsSync24(join23(root, lockfile)))
3179
2989
  return pm;
3180
2990
  }
3181
2991
  return;
@@ -3184,7 +2994,7 @@ function detectLanguages(root) {
3184
2994
  const langs = [];
3185
2995
  const seen = new Set;
3186
2996
  for (const [marker, lang] of Object.entries(MARKER_TO_LANG)) {
3187
- if (!seen.has(lang) && existsSync25(join24(root, marker))) {
2997
+ if (!seen.has(lang) && existsSync24(join23(root, marker))) {
3188
2998
  langs.push(lang);
3189
2999
  seen.add(lang);
3190
3000
  }
@@ -3192,11 +3002,11 @@ function detectLanguages(root) {
3192
3002
  return langs;
3193
3003
  }
3194
3004
  function readCurrentPhase(root) {
3195
- const statePath2 = join24(root, ".planning", "STATE.md");
3196
- if (!existsSync25(statePath2))
3005
+ const statePath2 = join23(root, ".planning", "STATE.md");
3006
+ if (!existsSync24(statePath2))
3197
3007
  return;
3198
3008
  try {
3199
- const content = readFileSync21(statePath2, "utf-8");
3009
+ const content = readFileSync22(statePath2, "utf-8");
3200
3010
  const match = content.match(/phase:\s*(\S+)/i);
3201
3011
  return match?.[1];
3202
3012
  } catch {
@@ -3245,8 +3055,15 @@ function createTodoHook(client) {
3245
3055
  // src/hooks/file-tracker.ts
3246
3056
  class SessionFileTracker {
3247
3057
  changes = new Map;
3058
+ onFileChange;
3059
+ setOnFileChange(callback) {
3060
+ this.onFileChange = callback;
3061
+ }
3248
3062
  record(path, type) {
3249
3063
  this.changes.set(path, { path, type });
3064
+ if (this.onFileChange) {
3065
+ this.onFileChange(path, type);
3066
+ }
3250
3067
  }
3251
3068
  getChanges() {
3252
3069
  return [...this.changes.values()];
@@ -3295,8 +3112,8 @@ function createSessionIdleHook(client, tracker) {
3295
3112
  }
3296
3113
 
3297
3114
  // src/hooks/compaction-hook.ts
3298
- import { existsSync as existsSync26, readFileSync as readFileSync22 } from "fs";
3299
- import { join as join25 } from "path";
3115
+ import { existsSync as existsSync25, readFileSync as readFileSync23 } from "fs";
3116
+ import { join as join24 } from "path";
3300
3117
  var STRUCTURED_SUMMARY_PROMPT = `
3301
3118
  When summarizing this session, you MUST include the following sections:
3302
3119
 
@@ -3335,11 +3152,11 @@ For each: agent name, status, description, session_id.
3335
3152
  **RESUME, DON'T RESTART.** Use session_id to continue existing sessions.
3336
3153
  `;
3337
3154
  function readPlanningState2(directory) {
3338
- const statePath2 = join25(directory, ".planning", "STATE.md");
3339
- if (!existsSync26(statePath2))
3155
+ const statePath2 = join24(directory, ".planning", "STATE.md");
3156
+ if (!existsSync25(statePath2))
3340
3157
  return null;
3341
3158
  try {
3342
- const content = readFileSync22(statePath2, "utf-8");
3159
+ const content = readFileSync23(statePath2, "utf-8");
3343
3160
  return content.slice(0, 1500);
3344
3161
  } catch {
3345
3162
  return null;
@@ -3356,6 +3173,18 @@ function createCompactionHook(ctx, tracker) {
3356
3173
  sections.push("```");
3357
3174
  sections.push("");
3358
3175
  }
3176
+ const indexPath = join24(ctx.directory, ".planning", "CODEBASE_INDEX.md");
3177
+ let indexSummary = "";
3178
+ if (existsSync25(indexPath)) {
3179
+ try {
3180
+ const indexContent = readFileSync23(indexPath, "utf-8");
3181
+ indexSummary = "\n## Codebase Index\n```\n" + indexContent.slice(0, 800) + "\n```\n";
3182
+ } catch {}
3183
+ }
3184
+ if (indexSummary) {
3185
+ sections.push(indexSummary);
3186
+ sections.push("");
3187
+ }
3359
3188
  const edited = tracker.getEditedPaths();
3360
3189
  if (edited.length > 0) {
3361
3190
  sections.push("## Recently Edited Files");
@@ -3568,6 +3397,23 @@ function createFlowDeckMcps() {
3568
3397
  oauth: false
3569
3398
  };
3570
3399
  }
3400
+ if (!disabled.has("github")) {
3401
+ mcps.github = {
3402
+ type: "remote",
3403
+ url: "https://api.githubcopilot.com/mcp/",
3404
+ enabled: true,
3405
+ ...process.env.GITHUB_TOKEN ? { headers: { Authorization: `Bearer ${process.env.GITHUB_TOKEN}` } } : {},
3406
+ oauth: false
3407
+ };
3408
+ }
3409
+ if (!disabled.has("codegraph") && isCodegraphInstalled()) {
3410
+ mcps.codegraph = {
3411
+ type: "local",
3412
+ command: "codegraph",
3413
+ args: ["serve", "--mcp"],
3414
+ enabled: true
3415
+ };
3416
+ }
3571
3417
  return mcps;
3572
3418
  }
3573
3419
 
@@ -3605,7 +3451,7 @@ MUST execute at session start:
3605
3451
  3. Check which steps are marked complete
3606
3452
  4. Begin execution from the first incomplete step
3607
3453
 
3608
- If STATE.md does not exist, tell the user: "No STATE.md found. Run \`/fd-new-project\` to initialize."
3454
+ 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."
3609
3455
 
3610
3456
  ## Phase Gating
3611
3457
 
@@ -3616,6 +3462,30 @@ If the project is in another phase:
3616
3462
  - **plan** phase: "Run \`/fd-plan\` to create the implementation plan first."
3617
3463
  - **review** phase: "Run \`/fd-verify\` to complete the review phase."
3618
3464
 
3465
+ ## State-First Read Strategy
3466
+
3467
+ Before delegating any agent that needs codebase context:
3468
+ 1. Read \`STATE.md\` — check \`freshnessStatus\` and \`lastUpdatedAt\`
3469
+ 2. Read \`.planning/CODEBASE_INDEX.md\` — check \`freshnessStatus\`
3470
+ 3. If \`freshnessStatus === "fresh"\` AND needed files exist in \`fileSnapshots\`:
3471
+ → Use the existing state. Do NOT re-explore the codebase.
3472
+ → Log: "[StateManager] Skipped codebase exploration — state is fresh"
3473
+ 4. If state is missing, stale, or insufficient:
3474
+ → Delegate to @code-explorer with specific question
3475
+ → After exploration completes, file-tracker auto-publishes to CODEBASE_INDEX.md
3476
+ → Log: "[StateManager] Triggered re-exploration — state was stale"
3477
+
3478
+ State becomes **stale** when:
3479
+ - \`lastUpdatedAt\` > 5 minutes ago
3480
+ - Phase transitions
3481
+ - New plan confirmed
3482
+ - User runs /fd-checkpoint or /fd-resume
3483
+
3484
+ State becomes **fresh** when:
3485
+ - Any agent writes to CODEBASE_INDEX.md
3486
+ - updatePlanningState() is called
3487
+ - file-tracker hook fires after a file edit
3488
+
3619
3489
  ## Step Execution
3620
3490
 
3621
3491
  For each incomplete step in PLAN.md:
@@ -3878,6 +3748,11 @@ var PLANNER_PROMPT = `You create implementation plans that developers can execut
3878
3748
  3. Check for conflicts with existing design decisions
3879
3749
  4. Define new interfaces if needed (before implementation)
3880
3750
 
3751
+ ### Codebase Context First
3752
+ 1. Read \`.planning/CODEBASE_INDEX.md\` — check if freshnessStatus is "fresh"
3753
+ 2. If fresh and needed files are in fileSnapshots, use the existing summaries
3754
+ 3. Only explore the codebase if the index is missing, stale, or incomplete
3755
+
3881
3756
  ### Step Breakdown
3882
3757
  - Each step maps to a single file or closely related file group
3883
3758
  - Steps are ordered by dependency (foundation first, UI last)
@@ -4939,13 +4814,36 @@ var createDocUpdaterAgent = (model, customPrompt, customAppendPrompt) => {
4939
4814
  // src/agents/mapper.ts
4940
4815
  var MAPPER_PROMPT = `You read source files and produce accurate documentation. You report only what you can verify by reading the code directly.
4941
4816
 
4817
+ ## CodeGraph-First Policy
4818
+
4819
+ Before using grep or reading files, check whether codegraph is available:
4820
+
4821
+ Use the \`codegraph\` tool with \`action=check\`. If codegraph is installed and the index is fresh:
4822
+ - Use codegraph MCP tools as your primary source of code understanding
4823
+ - Log: "codegraph available — using symbol index for mapping"
4824
+
4825
+ **Tool selection when codegraph is available:**
4826
+
4827
+ | Mapping task | Preferred Tool |
4828
+ |-------------|----------------|
4829
+ | Map a module / feature area | \`codegraph_context\` |
4830
+ | Find exported symbols | \`codegraph_search\` |
4831
+ | Read a function's source | \`codegraph_node\` |
4832
+ | Survey multiple related symbols | \`codegraph_explore\` |
4833
+ | Trace a data flow | \`codegraph_trace\` |
4834
+ | List files in an area | \`codegraph_files\` |
4835
+
4836
+ The returned source from codegraph is authoritative — do NOT re-open those files unless you need to see something specific codegraph didn't include.
4837
+
4838
+ **If codegraph is NOT available:** fall back to direct file reads as below.
4839
+
4942
4840
  ## Factual-Only Constraint
4943
4841
 
4944
4842
  - If you are not certain about something, write: \`UNKNOWN — needs verification\`
4945
4843
  - Never fill gaps with assumptions or what "probably" works
4946
4844
  - Every claim must be traceable to a specific file and line
4947
4845
 
4948
- ## Reading Source Files
4846
+ ## Reading Source Files (fallback when codegraph unavailable)
4949
4847
 
4950
4848
  - Read files directly using file tools — do not rely on memory
4951
4849
  - Note exact file paths for every claim you make
@@ -4976,13 +4874,14 @@ Write only your assigned file. Read existing \`.codebase/\` files before writing
4976
4874
  - Identify runtime, framework, database, testing, and build tools
4977
4875
 
4978
4876
  ### ARCHITECTURE.md
4877
+ - Use \`codegraph_context\` on entry points to map the architecture (if codegraph available)
4979
4878
  - Identify major components and their responsibilities
4980
4879
  - Map data flow from input to output
4981
4880
  - Document integration points (external APIs, databases, queues)
4982
4881
  - Draw component diagram in text format
4983
4882
 
4984
4883
  ### CONVENTIONS.md
4985
- - Find actual naming patterns by reading source files
4884
+ - Find actual naming patterns by reading source files or using \`codegraph_explore\`
4986
4885
  - Include file:line examples for each pattern
4987
4886
  - Document import style (relative paths? barrel exports? absolute aliases?)
4988
4887
  - Document error handling pattern from real code
@@ -5019,6 +4918,35 @@ var createMapperAgent = (model, customPrompt, customAppendPrompt) => {
5019
4918
  // src/agents/code-explorer.ts
5020
4919
  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.
5021
4920
 
4921
+ ## CodeGraph-First Policy
4922
+
4923
+ **Before any file exploration, check whether codegraph is available:**
4924
+
4925
+ Use the \`codegraph\` tool with \`action=check\`. If codegraph is installed and the index is fresh:
4926
+ - Use codegraph MCP tools as your primary source of code understanding
4927
+ - This is faster and more accurate than grep + file reads
4928
+ - Log: "codegraph available — using code intelligence index"
4929
+
4930
+ **Tool selection when codegraph is available:**
4931
+
4932
+ | Task | Preferred Tool |
4933
+ |------|----------------|
4934
+ | Map an area or feature | \`codegraph_context\` |
4935
+ | Find a symbol by name | \`codegraph_search\` |
4936
+ | Trace a call path | \`codegraph_trace\` |
4937
+ | Callers of a function | \`codegraph_callers\` |
4938
+ | Callees of a function | \`codegraph_callees\` |
4939
+ | Impact before changing | \`codegraph_impact\` |
4940
+ | Read symbol source | \`codegraph_node\` |
4941
+ | Survey related symbols | \`codegraph_explore\` |
4942
+ | List files in an area | \`codegraph_files\` |
4943
+
4944
+ The returned source from codegraph is complete and authoritative — treat it as already read. Do NOT re-open those files.
4945
+ Reach for grep/Read only to confirm a specific detail codegraph didn't cover.
4946
+
4947
+ **If codegraph is NOT available (not installed or not indexed):**
4948
+ Fall back to direct file exploration using the process below.
4949
+
5022
4950
  ## Your Outputs
5023
4951
 
5024
4952
  **File structure:**
@@ -5041,7 +4969,7 @@ var CODE_EXPLORER_PROMPT = `You map unfamiliar code before anyone touches it. Yo
5041
4969
  - Error handling approach (throw, return, Result type)
5042
4970
  - Testing patterns (file co-location, separate __tests__, naming)
5043
4971
 
5044
- ## Exploration Process
4972
+ ## Exploration Process (fallback when codegraph unavailable)
5045
4973
 
5046
4974
  1. \`ls -la\` the top-level directory — understand the layout
5047
4975
  2. Read \`package.json\`, \`go.mod\`, \`Cargo.toml\`, or equivalent — identify the tech stack and dependencies
@@ -5052,7 +4980,7 @@ var CODE_EXPLORER_PROMPT = `You map unfamiliar code before anyone touches it. Yo
5052
4980
  4. Trace the most important call path relevant to the current task
5053
4981
  5. Read test files to understand expected behavior
5054
4982
 
5055
- ## Quick Commands
4983
+ ## Quick Commands (fallback)
5056
4984
 
5057
4985
  \`\`\`bash
5058
4986
  # Find all TypeScript files
@@ -5070,6 +4998,7 @@ grep -r "export.*functionName" src/
5070
4998
 
5071
4999
  ## Rules
5072
5000
 
5001
+ - **CodeGraph first** — if codegraph index is available, use it before reaching for grep or file reads
5073
5002
  - **Read-only** — never modify files during exploration
5074
5003
  - **State uncertainty** — if you are not sure what something does, say so
5075
5004
  - **Report what you see** — not what you expect or what would make sense
@@ -5080,6 +5009,11 @@ grep -r "export.*functionName" src/
5080
5009
  \`\`\`markdown
5081
5010
  ## Codebase Exploration
5082
5011
 
5012
+ ### CodeGraph Status
5013
+ - installed: yes/no
5014
+ - indexed: yes/no
5015
+ - used: yes/no (if yes: list tools used)
5016
+
5083
5017
  ### Structure
5084
5018
  \`\`\`
5085
5019
  src/
@@ -5106,6 +5040,18 @@ Request → \`src/routes/users.ts:34\` → \`src/services/user-service.ts:89\`
5106
5040
  - \`src/services/user-service.ts\` — core business logic
5107
5041
  - \`src/db/user-repo.ts\` — data access
5108
5042
  - \`src/types/user.ts\` — data model definition
5043
+
5044
+ ## After Exploration
5045
+
5046
+ After completing your exploration, summarize what you found so it can be recorded:
5047
+
5048
+ - **Files explored:** List the paths you actually read or analyzed
5049
+ - **CodeGraph tools used:** List any codegraph MCP tools you invoked
5050
+ - **Key finding:** One-sentence summary of the most important insight
5051
+ - **Ready to proceed:** yes | no — whether you have enough context to continue
5052
+
5053
+ This information is used to update the shared CODEBASE_INDEX.md so subsequent
5054
+ stages can skip redundant exploration.
5109
5055
  \`\`\``;
5110
5056
  var createCodeExplorerAgent = (model, customPrompt, customAppendPrompt) => {
5111
5057
  const prompt = resolvePrompt(CODE_EXPLORER_PROMPT, customPrompt, customAppendPrompt);
@@ -5435,16 +5381,78 @@ var DISCUSSER_PROMPT = `You extract clear requirements through focused questioni
5435
5381
 
5436
5382
  Load \`.planning/PROJECT.md\` first if it exists. Use existing context to avoid asking about already-decided things.
5437
5383
 
5438
- ## Questioning Strategy
5384
+ ## The RecommendedQuestion Format
5385
+
5386
+ Every question you emit to the user MUST be wrapped in a structured recommendation envelope. Never emit a bare question.
5387
+
5388
+ Format:
5389
+ \`\`\`
5390
+ Question:
5391
+ <the actual question>
5392
+
5393
+ Recommendation:
5394
+ <your recommended answer>
5395
+
5396
+ Rationale:
5397
+ <why this recommendation — ground it in repo evidence: cite specific files,
5398
+ prior decisions, tech stack, or policy rules. Do not make recommendations
5399
+ from thin air if the repo already contains evidence.>
5400
+
5401
+ Alternatives:
5402
+ <other valid options, one per line (optional)>
5403
+
5404
+ Default if no response:
5405
+ <what the system does if you receive no reply>
5406
+ \`\`\`
5407
+
5408
+ ## Examples
5409
+
5410
+ ✅ Good (question with recommendation):
5411
+ \`\`\`
5412
+ Question:
5413
+ Should this task use the design-first workflow?
5414
+
5415
+ Recommendation:
5416
+ Yes.
5417
+
5418
+ Rationale:
5419
+ The task description mentions "dashboard" and "UI", which means it is
5420
+ UI-heavy. The codebase has a design agent available (see src/agents/).
5421
+ The supervisor policy in src/agents/supervisor.ts requires design approval
5422
+ for UI-heavy tasks before the execute phase. Starting with design-first
5423
+ is the safest and most expedient path.
5424
+
5425
+ Alternatives:
5426
+ No — skip design and use a lightweight workflow. Faster but riskier for UI work.
5427
+
5428
+ Default if no response:
5429
+ Proceed with design-first workflow (recommendation applied automatically).
5430
+ \`\`\`
5431
+
5432
+ ❌ Bad (bare question — never do this):
5433
+ "What workflow should we use?"
5434
+
5435
+ ❌ Bad (recommendation without rationale):
5436
+ "Should we use TypeScript? Recommendation: Yes. Default: use TypeScript."
5437
+ (Every recommendation needs a rationale grounded in evidence.)
5438
+
5439
+ ## Questioning Rules
5439
5440
 
5440
5441
  - **ONE question per turn** — never ask two questions at once
5441
5442
  - **Follow-up when unclear** — if an answer is ambiguous, ask for clarification before moving on
5442
5443
  - **Targeted focus** — each question uncovers one specific decision
5444
+ - **Grounded recommendations** — base recommendations on PROJECT.md goals, prior DISCUSS.md decisions, tech stack, available agents, or explicit policy rules
5445
+ - **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
5443
5446
 
5444
- \`\`\`
5445
- ✅ Good: "Should users be able to reset their password via email?"
5447
+ ## Suppressed Questions
5448
+
5449
+ If a question can be answered from exploration evidence, skip it and record it:
5446
5450
 
5447
- ❌ Bad: "What authentication features do you need, and how should password reset work, and do you want social login?"
5451
+ \`\`\`markdown
5452
+ ## Suppressed Questions
5453
+
5454
+ - "What tech stack?" → answered by: tech stack detection (Node.js/TypeScript from package.json)
5455
+ - "Is the project initialised?" → answered by: PROJECT.md exists
5448
5456
  \`\`\`
5449
5457
 
5450
5458
  ## Decision Tracking
@@ -5462,16 +5470,28 @@ D-03: Social login — excluded from MVP scope
5462
5470
 
5463
5471
  ## Conflict Detection
5464
5472
 
5465
- If a new answer conflicts with a previous decision, flag it immediately:
5473
+ If a new answer conflicts with a previous decision, flag it immediately with a RecommendedQuestion:
5466
5474
 
5467
5475
  \`\`\`
5468
5476
  CONFLICT: D-04 (users can stay logged in for 30 days) conflicts with D-01 (JWT, stateless).
5469
- Long-lived JWTs create security risks. Options:
5470
- 1. Use refresh tokens with short-lived access tokens
5471
- 2. Use sessions instead of JWT
5472
- 3. Accept the 30-day JWT with a revocation list
5473
5477
 
5474
- Which do you want?
5478
+ Question:
5479
+ A long-lived JWT creates a security risk. How do you want to handle session persistence?
5480
+
5481
+ Recommendation:
5482
+ Use refresh tokens with short-lived access tokens.
5483
+
5484
+ Rationale:
5485
+ D-01 specified JWT (stateless). Refresh tokens preserve statelessness while
5486
+ allowing short-lived access tokens that limit exposure window. This is the
5487
+ most secure option that satisfies D-01.
5488
+
5489
+ Alternatives:
5490
+ - Use sessions instead of JWT (conflicts with D-01)
5491
+ - Accept 30-day JWT with a revocation list (complex to implement)
5492
+
5493
+ Default if no response:
5494
+ Use refresh tokens with short-lived access tokens (most secure option).
5475
5495
  \`\`\`
5476
5496
 
5477
5497
  ## Saving Decisions
@@ -5489,6 +5509,18 @@ D-01: [topic] — [choice]
5489
5509
  D-02: [topic] — [choice]
5490
5510
  Rationale: [why]
5491
5511
 
5512
+ ## Answered Recommendations
5513
+
5514
+ RQ-01: [question]
5515
+ Recommendation: [recommended answer]
5516
+ User choice: [what they said]
5517
+ Rationale: [why the system recommended it]
5518
+ Stage: discuss
5519
+
5520
+ ## Suppressed Questions
5521
+
5522
+ - "<question>" → answered by: <evidence source>
5523
+
5492
5524
  ## Open Questions
5493
5525
  - [anything unresolved]
5494
5526
 
@@ -5496,40 +5528,6 @@ D-02: [topic] — [choice]
5496
5528
  - [explicitly excluded items]
5497
5529
  \`\`\`
5498
5530
 
5499
- ## Question Bank
5500
-
5501
- Use these question categories to ensure thorough coverage:
5502
-
5503
- **Scope:**
5504
- - What is included in this feature?
5505
- - What is explicitly excluded?
5506
- - What is the MVP vs. nice-to-have?
5507
-
5508
- **Constraints:**
5509
- - Timeline or deadline?
5510
- - Budget or infrastructure limits?
5511
- - Technology constraints (must use X, cannot use Y)?
5512
-
5513
- **Integration:**
5514
- - Does this interact with existing systems?
5515
- - External APIs or services needed?
5516
-
5517
- **User experience:**
5518
- - Walk me through the user flow step by step
5519
- - What happens when something goes wrong?
5520
-
5521
- **Error handling:**
5522
- - What should happen when [specific failure] occurs?
5523
- - Who is notified on failure?
5524
-
5525
- **Performance:**
5526
- - How many users / requests / records expected?
5527
- - Acceptable response time?
5528
-
5529
- **Security:**
5530
- - Who can access this feature?
5531
- - What data is sensitive?
5532
-
5533
5531
  ## Completion Criteria
5534
5532
 
5535
5533
  Discussion is complete when:
@@ -6308,7 +6306,7 @@ You sit above the orchestrator's execution path. Your only job is to inspect an
6308
6306
 
6309
6307
  fd-ask, fd-checkpoint, fd-deploy-check, fd-design, fd-discuss, fd-doctor,
6310
6308
  fd-execute, fd-fix-bug, fd-map-codebase, fd-multi-repo, fd-new-feature,
6311
- fd-new-project, fd-plan, fd-quick, fd-reflect, fd-resume, fd-status,
6309
+ fd-plan, fd-quick, fd-reflect, fd-resume, fd-status,
6312
6310
  fd-suggest, fd-translate-intent, fd-verify, fd-write-docs
6313
6311
 
6314
6312
  ## Registered Agents (source of truth — do not add to this list)
@@ -6900,7 +6898,6 @@ var REGISTERED_COMMANDS = [
6900
6898
  "fd-map-codebase",
6901
6899
  "fd-multi-repo",
6902
6900
  "fd-new-feature",
6903
- "fd-new-project",
6904
6901
  "fd-plan",
6905
6902
  "fd-quick",
6906
6903
  "fd-reflect",
@@ -6909,7 +6906,8 @@ var REGISTERED_COMMANDS = [
6909
6906
  "fd-suggest",
6910
6907
  "fd-translate-intent",
6911
6908
  "fd-verify",
6912
- "fd-write-docs"
6909
+ "fd-write-docs",
6910
+ "fd-done"
6913
6911
  ];
6914
6912
  function resolveSupervisorConfig(directory) {
6915
6913
  try {
@@ -7046,12 +7044,12 @@ function computeConfidence(exists, policyResult, ctx) {
7046
7044
  return 0.45;
7047
7045
  return 0.95;
7048
7046
  }
7049
- function resolveDecision(exists, policyResult, confidenceScore, threshold, ctx) {
7047
+ function resolveDecision(exists, policyResult, confidenceScore, threshold, ctx, clarificationQuestion) {
7050
7048
  if (!exists) {
7051
7049
  return { decision: "block", approvalStatus: "denied" };
7052
7050
  }
7053
7051
  if (ctx.approvalRequired && !ctx.approvalGranted) {
7054
- return { decision: "escalate", approvalStatus: "escalated" };
7052
+ return { decision: "escalate", approvalStatus: "escalated", clarificationQuestion };
7055
7053
  }
7056
7054
  if (!policyResult.passed) {
7057
7055
  if (policyResult.requiredChanges.length > 0) {
@@ -7060,11 +7058,11 @@ function resolveDecision(exists, policyResult, confidenceScore, threshold, ctx)
7060
7058
  return { decision: "block", approvalStatus: "denied" };
7061
7059
  }
7062
7060
  if (confidenceScore < threshold) {
7063
- return { decision: "escalate", approvalStatus: "escalated" };
7061
+ return { decision: "escalate", approvalStatus: "escalated", clarificationQuestion };
7064
7062
  }
7065
7063
  return { decision: "approve", approvalStatus: "approved" };
7066
7064
  }
7067
- function runSupervisorReview(directory, targetName, ctx = {}) {
7065
+ function runSupervisorReview(directory, targetName, ctx = {}, clarificationQuestion) {
7068
7066
  const config = resolveSupervisorConfig(directory);
7069
7067
  const reviewPhase = ctx.reviewPhase ?? "preflight";
7070
7068
  const timestamp2 = new Date().toISOString();
@@ -7112,7 +7110,7 @@ function runSupervisorReview(directory, targetName, ctx = {}) {
7112
7110
  }
7113
7111
  const policyResult = targetType === "command" ? checkCommandPolicy(targetName, ctx) : checkAgentPolicy(targetName, ctx);
7114
7112
  const confidenceScore = computeConfidence(exists, policyResult, ctx);
7115
- const { decision, approvalStatus } = resolveDecision(exists, policyResult, confidenceScore, config.confidenceThreshold, ctx);
7113
+ const { decision, approvalStatus, clarificationQuestion: escalationQuestion } = resolveDecision(exists, policyResult, confidenceScore, config.confidenceThreshold, ctx, clarificationQuestion);
7116
7114
  const reasons = policyResult.reasons.length > 0 ? policyResult.reasons : decision === "approve" ? [`Target "${targetName}" reviewed and approved for execution`] : [`Target "${targetName}" reviewed — decision: ${decision}`];
7117
7115
  const supervisorDecision = {
7118
7116
  decision,
@@ -7126,7 +7124,8 @@ function runSupervisorReview(directory, targetName, ctx = {}) {
7126
7124
  approvalStatus,
7127
7125
  confidenceScore,
7128
7126
  reviewPhase,
7129
- timestamp: timestamp2
7127
+ timestamp: timestamp2,
7128
+ ...escalationQuestion ? { clarificationQuestion: escalationQuestion } : {}
7130
7129
  };
7131
7130
  _emitTelemetry(directory, supervisorDecision, ctx);
7132
7131
  return supervisorDecision;
@@ -7166,13 +7165,13 @@ function _emitTelemetry(directory, decision, ctx) {
7166
7165
  // src/index.ts
7167
7166
  function loadRulePaths() {
7168
7167
  const __dir = dirname4(fileURLToPath2(import.meta.url));
7169
- const rulesDir = join26(__dir, "..", "src", "rules");
7170
- if (!existsSync27(rulesDir))
7168
+ const rulesDir = join25(__dir, "..", "src", "rules");
7169
+ if (!existsSync26(rulesDir))
7171
7170
  return [];
7172
7171
  const paths = [];
7173
7172
  function walk(dir) {
7174
7173
  for (const entry of readdirSync3(dir, { withFileTypes: true })) {
7175
- const full = join26(dir, entry.name);
7174
+ const full = join25(dir, entry.name);
7176
7175
  if (entry.isDirectory()) {
7177
7176
  walk(full);
7178
7177
  } else if (entry.isFile() && entry.name.endsWith(".md") && entry.name !== "README.md") {
@@ -7185,8 +7184,8 @@ function loadRulePaths() {
7185
7184
  }
7186
7185
  function loadCommands() {
7187
7186
  const __dir = dirname4(fileURLToPath2(import.meta.url));
7188
- const commandsDir = join26(__dir, "..", "src", "commands");
7189
- if (!existsSync27(commandsDir))
7187
+ const commandsDir = join25(__dir, "..", "src", "commands");
7188
+ if (!existsSync26(commandsDir))
7190
7189
  return {};
7191
7190
  const commands = {};
7192
7191
  try {
@@ -7194,7 +7193,7 @@ function loadCommands() {
7194
7193
  if (!file.endsWith(".md"))
7195
7194
  continue;
7196
7195
  const name = basename(file, ".md");
7197
- const raw = readFileSync23(join26(commandsDir, file), "utf-8");
7196
+ const raw = readFileSync24(join25(commandsDir, file), "utf-8");
7198
7197
  let description;
7199
7198
  let template = raw;
7200
7199
  const fmMatch = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/);
@@ -7275,8 +7274,8 @@ var plugin = async (input, _options) => {
7275
7274
  }
7276
7275
  }
7277
7276
  }
7278
- const skillsDir = join26(dirname4(fileURLToPath2(import.meta.url)), "..", "src", "skills");
7279
- if (existsSync27(skillsDir)) {
7277
+ const skillsDir = join25(dirname4(fileURLToPath2(import.meta.url)), "..", "src", "skills");
7278
+ if (existsSync26(skillsDir)) {
7280
7279
  const cfgAny = cfg;
7281
7280
  if (!cfgAny.skills || typeof cfgAny.skills !== "object") {
7282
7281
  cfgAny.skills = { paths: [] };
@@ -7317,47 +7316,23 @@ var plugin = async (input, _options) => {
7317
7316
  "context-generator": contextGeneratorTool,
7318
7317
  "create-skill": createSkillTool,
7319
7318
  reflect: reflectTool,
7320
- "memory-search": memorySearchTool,
7321
- "memory-status": memoryStatusTool
7319
+ codegraph: codegraphTool
7322
7320
  },
7323
7321
  "shell.env": shellEnvHook,
7324
7322
  "todo.updated": todoHook,
7325
7323
  "file.edited": fileEdited,
7326
7324
  "file.watcher.updated": fileWatcherUpdated,
7327
7325
  "experimental.session.compacting": compactionHook,
7326
+ "command.execute.before": async (input2, _output) => {
7327
+ notifyCommandInteraction(input2.command);
7328
+ },
7328
7329
  "permission.ask": async (input2, _output) => {
7329
7330
  notifyPermissionNeeded(input2.title);
7330
7331
  },
7331
7332
  event: async ({ event }) => {
7332
7333
  const type = event?.type ?? "";
7333
- try {
7334
- if (type === "session.created" || type === "session.started") {
7335
- const sessionId = event?.sessionID ?? event?.sessionId ?? "";
7336
- if (sessionId) {
7337
- memoryHook.onSessionCreated(directory, sessionId, event?.prompt);
7338
- }
7339
- await sessionStartHook({ directory });
7340
- } else if (type === "message.updated") {
7341
- const msgEvent = event?.event ?? event;
7342
- const sessionId = msgEvent?.sessionID ?? msgEvent?.sessionId ?? "";
7343
- if (sessionId) {
7344
- memoryHook.onMessageUpdated(sessionId, msgEvent.role, msgEvent.content, directory);
7345
- }
7346
- } else if (type === "session.compacted") {
7347
- const compactEvent = event?.event ?? event;
7348
- const sessionId = compactEvent?.sessionID ?? compactEvent?.sessionId ?? "";
7349
- if (sessionId) {
7350
- memoryHook.onSessionCompact(sessionId, compactEvent.summary ?? "");
7351
- }
7352
- } else if (type === "session.deleted") {
7353
- const delEvent = event?.event ?? event;
7354
- const sessionId = delEvent?.sessionID ?? delEvent?.sessionId ?? "";
7355
- if (sessionId) {
7356
- memoryHook.onSessionEnd(sessionId);
7357
- }
7358
- }
7359
- } catch (err) {
7360
- console.error("[FlowDeck Memory] Event handler error:", err);
7334
+ if (type === "session.created" || type === "session.started") {
7335
+ await sessionStartHook({ directory });
7361
7336
  }
7362
7337
  await contextMonitor.event({ event });
7363
7338
  orchestratorGuard.onEvent(event);
@@ -7418,14 +7393,6 @@ var plugin = async (input, _options) => {
7418
7393
  },
7419
7394
  "tool.execute.after": async (toolInput, toolOutput) => {
7420
7395
  await telemetryAfterHook({ directory }, toolInput, toolOutput);
7421
- try {
7422
- const sessionId = toolInput?.sessionID ?? toolInput?.sessionId ?? "";
7423
- if (sessionId && toolInput?.tool) {
7424
- memoryHook.onToolExecuted(sessionId, toolInput.tool, toolInput, toolOutput?.output ?? null, directory);
7425
- }
7426
- } catch (err) {
7427
- console.error("[FlowDeck Memory] Tool execution error:", err);
7428
- }
7429
7396
  const afterToolName = toolInput.tool ?? toolInput.name ?? "";
7430
7397
  if (afterToolName === "delegate" || afterToolName === "run-pipeline") {
7431
7398
  try {