@dv.nghiem/flowdeck 0.3.9 → 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.
- package/README.md +13 -21
- package/dist/agents/code-explorer.d.ts.map +1 -1
- package/dist/agents/mapper.d.ts.map +1 -1
- package/dist/agents/orchestrator.d.ts.map +1 -1
- package/dist/agents/planner.d.ts.map +1 -1
- package/dist/agents/specialist.d.ts.map +1 -1
- package/dist/dashboard/server.mjs +12 -2
- package/dist/hooks/compaction-hook.d.ts +1 -2
- package/dist/hooks/compaction-hook.d.ts.map +1 -1
- package/dist/hooks/file-tracker.d.ts +6 -0
- package/dist/hooks/file-tracker.d.ts.map +1 -1
- package/dist/hooks/notifications.d.ts.map +1 -1
- package/dist/hooks/session-start.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +742 -794
- package/dist/lib/completion-validator.d.ts +51 -0
- package/dist/lib/completion-validator.d.ts.map +1 -0
- package/dist/lib/recommended-question.d.ts +24 -0
- package/dist/lib/recommended-question.d.ts.map +1 -0
- package/dist/lib/research-gate.d.ts +97 -0
- package/dist/lib/research-gate.d.ts.map +1 -0
- package/dist/lib/research-gate.test.d.ts +2 -0
- package/dist/lib/research-gate.test.d.ts.map +1 -0
- package/dist/mcp/index.d.ts +12 -2
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/services/codegraph.d.ts +36 -0
- package/dist/services/codegraph.d.ts.map +1 -0
- package/dist/services/codegraph.test.d.ts +2 -0
- package/dist/services/codegraph.test.d.ts.map +1 -0
- package/dist/services/question-guard.d.ts +4 -0
- package/dist/services/question-guard.d.ts.map +1 -1
- package/dist/services/recommended-question.test.d.ts +2 -0
- package/dist/services/recommended-question.test.d.ts.map +1 -0
- package/dist/services/supervisor-binding.d.ts +3 -1
- package/dist/services/supervisor-binding.d.ts.map +1 -1
- package/dist/tools/codebase-index.d.ts +30 -0
- package/dist/tools/codebase-index.d.ts.map +1 -0
- package/dist/tools/codebase-index.test.d.ts +2 -0
- package/dist/tools/codebase-index.test.d.ts.map +1 -0
- package/dist/tools/codegraph-tool.d.ts +3 -0
- package/dist/tools/codegraph-tool.d.ts.map +1 -0
- package/dist/tools/planning-state-lib.d.ts +23 -0
- package/dist/tools/planning-state-lib.d.ts.map +1 -1
- package/docs/agents/index.md +154 -0
- package/docs/commands/fd-ask.md +71 -39
- package/docs/commands/fd-checkpoint.md +63 -8
- package/docs/commands/fd-deploy-check.md +166 -9
- package/docs/commands/fd-design.md +101 -0
- package/docs/commands/fd-discuss.md +87 -20
- package/docs/commands/fd-doctor.md +100 -13
- package/docs/commands/fd-done.md +215 -0
- package/docs/commands/fd-execute.md +104 -0
- package/docs/commands/fd-fix-bug.md +144 -24
- package/docs/commands/fd-map-codebase.md +85 -21
- package/docs/commands/fd-multi-repo.md +155 -40
- package/docs/commands/fd-new-feature.md +63 -19
- package/docs/commands/fd-plan.md +80 -27
- package/docs/commands/fd-quick.md +143 -29
- package/docs/commands/fd-reflect.md +81 -13
- package/docs/commands/fd-resume.md +65 -8
- package/docs/commands/fd-status.md +80 -12
- package/docs/commands/fd-suggest.md +114 -0
- package/docs/commands/fd-translate-intent.md +69 -9
- package/docs/commands/fd-verify.md +71 -14
- package/docs/commands/fd-write-docs.md +121 -8
- package/docs/concepts/architecture.md +163 -0
- package/docs/concepts/governance.md +242 -0
- package/docs/concepts/intelligence.md +145 -0
- package/docs/concepts/multi-repo.md +227 -0
- package/docs/concepts/workflows.md +205 -0
- package/docs/configuration/index.md +208 -0
- package/docs/configuration/opencode-settings.md +98 -0
- package/docs/getting-started/first-project.md +126 -0
- package/docs/getting-started/installation.md +73 -0
- package/docs/getting-started/quick-start.md +74 -0
- package/docs/index.md +36 -72
- package/docs/reference/hooks.md +176 -0
- package/docs/reference/rules.md +109 -0
- package/docs/skills/code-review.md +47 -0
- package/docs/skills/index.md +148 -0
- package/docs/skills/planning.md +39 -0
- package/package.json +1 -1
- package/src/commands/fd-discuss.md +74 -10
- package/src/commands/fd-done.md +196 -0
- package/src/commands/fd-execute.md +43 -6
- package/src/commands/fd-fix-bug.md +43 -6
- package/src/commands/fd-map-codebase.md +99 -19
- package/src/commands/fd-new-feature.md +14 -5
- package/src/commands/fd-plan.md +38 -1
- package/src/commands/fd-quick.md +1 -1
- package/src/commands/fd-resume.md +1 -1
- package/src/commands/fd-status.md +1 -1
- package/src/commands/fd-verify.md +16 -2
- package/src/commands/fd-write-docs.md +30 -5
- package/src/skills/context-load/SKILL.md +1 -1
- package/dist/hooks/memory-hook.d.ts +0 -28
- package/dist/hooks/memory-hook.d.ts.map +0 -1
- package/dist/services/memory-store.d.ts +0 -73
- package/dist/services/memory-store.d.ts.map +0 -1
- package/dist/services/memory-store.test.d.ts +0 -2
- package/dist/services/memory-store.test.d.ts.map +0 -1
- package/dist/tools/memory-search.d.ts +0 -3
- package/dist/tools/memory-search.d.ts.map +0 -1
- package/dist/tools/memory-status.d.ts +0 -3
- package/dist/tools/memory-status.d.ts.map +0 -1
- package/docs/USER_GUIDE.md +0 -20
- package/docs/agents.md +0 -544
- package/docs/best-practices.md +0 -47
- package/docs/commands/fd-new-project.md +0 -24
- package/docs/commands.md +0 -557
- package/docs/configuration.md +0 -325
- package/docs/design-first-workflow.md +0 -94
- package/docs/feature-integration-architecture.md +0 -227
- package/docs/installation.md +0 -123
- package/docs/intelligence.md +0 -370
- package/docs/memory.md +0 -69
- package/docs/multi-repo.md +0 -201
- package/docs/notifications.md +0 -170
- package/docs/optimization-baseline.md +0 -21
- package/docs/quick-start.md +0 -197
- package/docs/rules.md +0 -432
- package/docs/skills.md +0 -417
- package/docs/workflows.md +0 -134
- 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
|
|
3
|
-
import { join as
|
|
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/
|
|
1714
|
+
// src/tools/codegraph-tool.ts
|
|
1705
1715
|
import { tool as tool16 } from "@opencode-ai/plugin";
|
|
1706
1716
|
|
|
1707
|
-
// src/services/
|
|
1708
|
-
import {
|
|
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
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
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
|
|
1726
|
+
function isCodegraphInstalled() {
|
|
1792
1727
|
try {
|
|
1793
|
-
|
|
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
|
-
|
|
1796
|
-
while (Date.now() < end) {}
|
|
1735
|
+
return false;
|
|
1797
1736
|
}
|
|
1798
1737
|
}
|
|
1799
|
-
function
|
|
1800
|
-
|
|
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
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
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
|
-
|
|
1756
|
+
const content = readFileSync14(path, "utf-8");
|
|
1757
|
+
return parseCodegraphMeta(content);
|
|
1837
1758
|
} catch {
|
|
1838
|
-
return
|
|
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
|
|
1842
|
-
const
|
|
1843
|
-
const
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
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
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
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
|
|
1861
|
-
const
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
const
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
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
|
|
1971
|
-
|
|
1972
|
-
|
|
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
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
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
|
|
2214
|
-
const
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1917
|
+
return {
|
|
1918
|
+
success: false,
|
|
1919
|
+
alreadyInstalled: false,
|
|
1920
|
+
log: "",
|
|
1921
|
+
error: String(err)
|
|
1922
|
+
};
|
|
2241
1923
|
}
|
|
2242
1924
|
}
|
|
2243
|
-
function
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
2267
|
-
|
|
2268
|
-
if (!
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
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
|
-
|
|
2283
|
-
|
|
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
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
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
2041
|
}
|
|
2315
|
-
function
|
|
2316
|
-
const
|
|
2317
|
-
|
|
2318
|
-
return { context, previousSessions };
|
|
2042
|
+
function markCodegraphStale(dir) {
|
|
2043
|
+
const meta = readCodegraphMeta(dir);
|
|
2044
|
+
writeCodegraphMeta(dir, { ...meta, freshnessStatus: "stale" });
|
|
2319
2045
|
}
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
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
|
|
2335
|
-
import { join as
|
|
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
|
|
2339
|
-
import { join as
|
|
2340
|
-
import { homedir
|
|
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 ?
|
|
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(
|
|
2147
|
+
candidates.push(join16(directory, ".opencode", CONFIG_FILENAME));
|
|
2349
2148
|
}
|
|
2350
|
-
candidates.push(
|
|
2149
|
+
candidates.push(join16(getGlobalConfigDir(), CONFIG_FILENAME));
|
|
2351
2150
|
for (const configPath of candidates) {
|
|
2352
|
-
if (
|
|
2151
|
+
if (existsSync16(configPath)) {
|
|
2353
2152
|
try {
|
|
2354
|
-
const content =
|
|
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 (
|
|
2182
|
+
if (existsSync17(configPath)) {
|
|
2384
2183
|
try {
|
|
2385
|
-
const config = JSON.parse(
|
|
2184
|
+
const config = JSON.parse(readFileSync16(configPath, "utf-8"));
|
|
2386
2185
|
if (config.execution_mode === "review-only")
|
|
2387
2186
|
return "review-only";
|
|
2388
2187
|
if (config.execution_mode === "guarded")
|
|
@@ -2436,22 +2235,22 @@ async function guardRailsHook(ctx, input, _output) {
|
|
|
2436
2235
|
if (!ENABLED)
|
|
2437
2236
|
return;
|
|
2438
2237
|
const dir = ctx.directory;
|
|
2439
|
-
const planningDirPath =
|
|
2238
|
+
const planningDirPath = join17(dir, PLANNING_DIR2);
|
|
2440
2239
|
const codebaseDirectory = codebaseDir(dir);
|
|
2441
|
-
const configPath =
|
|
2442
|
-
const statePath2 =
|
|
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" && !
|
|
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 (!
|
|
2251
|
+
if (!existsSync17(planningDirPath))
|
|
2453
2252
|
return;
|
|
2454
|
-
if (!
|
|
2253
|
+
if (!existsSync17(codebaseDirectory)) {
|
|
2455
2254
|
throw new Error(`[flowdeck] WARNING: .codebase/ not found. Run /fd-map-codebase to map the codebase.`);
|
|
2456
2255
|
}
|
|
2457
2256
|
const execMode = resolveExecutionMode(configPath, null);
|
|
@@ -2507,15 +2306,15 @@ function getDesignGateMessage(dir) {
|
|
|
2507
2306
|
}
|
|
2508
2307
|
function planSuggestsUiHeavy(dir, phase) {
|
|
2509
2308
|
const planPath = phasePlanPath(dir, phase);
|
|
2510
|
-
if (!
|
|
2309
|
+
if (!existsSync17(planPath))
|
|
2511
2310
|
return false;
|
|
2512
|
-
const planContent =
|
|
2311
|
+
const planContent = readFileSync16(planPath, "utf-8");
|
|
2513
2312
|
return isUiHeavyTask(planContent);
|
|
2514
2313
|
}
|
|
2515
2314
|
function effectiveSeverity(configPath, statePath2) {
|
|
2516
|
-
if (
|
|
2315
|
+
if (existsSync17(configPath)) {
|
|
2517
2316
|
try {
|
|
2518
|
-
const configContent =
|
|
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 (!
|
|
2333
|
+
if (!existsSync17(statePath2))
|
|
2535
2334
|
return false;
|
|
2536
2335
|
try {
|
|
2537
|
-
const content =
|
|
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 (!
|
|
2546
|
-
return "No .
|
|
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 (!
|
|
2552
|
-
return "No .
|
|
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
|
|
2559
|
-
import { join as
|
|
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(
|
|
2567
|
-
const patterns = BLOCKED_PATTERNS[
|
|
2365
|
+
function isBlocked(tool17, args) {
|
|
2366
|
+
const patterns = BLOCKED_PATTERNS[tool17];
|
|
2568
2367
|
if (!patterns)
|
|
2569
2368
|
return null;
|
|
2570
|
-
if (
|
|
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 (
|
|
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 (
|
|
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 =
|
|
2607
|
-
if (!
|
|
2405
|
+
const constraintsPath = join18(codebaseDir(directory), "CONSTRAINTS.md");
|
|
2406
|
+
if (!existsSync18(constraintsPath))
|
|
2608
2407
|
return null;
|
|
2609
2408
|
try {
|
|
2610
|
-
const content =
|
|
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 (!
|
|
2450
|
+
if (!existsSync18(planPath))
|
|
2652
2451
|
return false;
|
|
2653
|
-
const planContent =
|
|
2452
|
+
const planContent = readFileSync17(planPath, "utf-8");
|
|
2654
2453
|
if (!isUiHeavyTask(planContent))
|
|
2655
2454
|
return false;
|
|
2656
2455
|
return !(state.design_stage === "handoff_complete" && state.design_approved);
|
|
@@ -2679,19 +2478,18 @@ async function toolGuardHook(ctx, input, output) {
|
|
|
2679
2478
|
}
|
|
2680
2479
|
|
|
2681
2480
|
// src/hooks/session-start.ts
|
|
2682
|
-
import { existsSync as
|
|
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 (!
|
|
2487
|
+
if (!existsSync19(planningDir2)) {
|
|
2689
2488
|
return {
|
|
2690
2489
|
flowdeck_phase: null,
|
|
2691
2490
|
flowdeck_status: "no_plan",
|
|
2692
|
-
flowdeck_warning: "Run /fd-
|
|
2693
|
-
flowdeck_has_codebase:
|
|
2694
|
-
flowdeck_session_context: getContextForDirectory(ctx.directory),
|
|
2491
|
+
flowdeck_warning: "Run /fd-map-codebase to index the codebase, then /fd-new-feature to start a feature.",
|
|
2492
|
+
flowdeck_has_codebase: existsSync19(codebaseDirectory),
|
|
2695
2493
|
...workspaceRoot && config?.sub_repos ? {
|
|
2696
2494
|
flowdeck_workspace_root: workspaceRoot,
|
|
2697
2495
|
flowdeck_sub_repos: config.sub_repos,
|
|
@@ -2702,17 +2500,15 @@ async function sessionStartHook(ctx) {
|
|
|
2702
2500
|
}
|
|
2703
2501
|
try {
|
|
2704
2502
|
const stateFilePath = statePath(ctx.directory);
|
|
2705
|
-
const content =
|
|
2503
|
+
const content = readFileSync18(stateFilePath, "utf-8");
|
|
2706
2504
|
const state = parseState(content);
|
|
2707
2505
|
const currentPhase = state["current_phase"] || {};
|
|
2708
|
-
const sessionContext = getContextForDirectory(ctx.directory);
|
|
2709
2506
|
const result = {
|
|
2710
2507
|
flowdeck_phase: currentPhase["phase"] ?? null,
|
|
2711
2508
|
flowdeck_status: currentPhase["status"] ?? null,
|
|
2712
2509
|
flowdeck_steps_pending: currentPhase["steps_pending"] ?? null,
|
|
2713
2510
|
flowdeck_last_action: currentPhase["last_action"] ?? null,
|
|
2714
|
-
flowdeck_has_codebase:
|
|
2715
|
-
flowdeck_session_context: sessionContext
|
|
2511
|
+
flowdeck_has_codebase: existsSync19(codebaseDirectory)
|
|
2716
2512
|
};
|
|
2717
2513
|
if (workspaceRoot && config?.sub_repos && config.sub_repos.length > 0) {
|
|
2718
2514
|
result.flowdeck_workspace_root = workspaceRoot;
|
|
@@ -2727,8 +2523,7 @@ async function sessionStartHook(ctx) {
|
|
|
2727
2523
|
flowdeck_phase: null,
|
|
2728
2524
|
flowdeck_status: "error",
|
|
2729
2525
|
flowdeck_warning: "State file unreadable. Continuing without flowdeck context.",
|
|
2730
|
-
flowdeck_has_codebase:
|
|
2731
|
-
flowdeck_session_context: getContextForDirectory(ctx.directory)
|
|
2526
|
+
flowdeck_has_codebase: existsSync19(codebaseDirectory)
|
|
2732
2527
|
};
|
|
2733
2528
|
if (workspaceRoot && config?.sub_repos && config.sub_repos.length > 0) {
|
|
2734
2529
|
result.flowdeck_workspace_root = workspaceRoot;
|
|
@@ -2745,15 +2540,18 @@ import { execFile } from "child_process";
|
|
|
2745
2540
|
var INTERACTIVE_COMMANDS = new Set([
|
|
2746
2541
|
"discuss",
|
|
2747
2542
|
"plan",
|
|
2748
|
-
"review-code",
|
|
2749
2543
|
"deploy-check",
|
|
2750
|
-
"
|
|
2544
|
+
"ask",
|
|
2545
|
+
"resume"
|
|
2751
2546
|
]);
|
|
2752
2547
|
var COMPLETION_COMMANDS = new Set([
|
|
2753
2548
|
"new-feature",
|
|
2754
2549
|
"fix-bug",
|
|
2755
2550
|
"write-docs",
|
|
2756
|
-
"checkpoint"
|
|
2551
|
+
"checkpoint",
|
|
2552
|
+
"done",
|
|
2553
|
+
"execute",
|
|
2554
|
+
"verify"
|
|
2757
2555
|
]);
|
|
2758
2556
|
function notify(title, body, level = "info") {
|
|
2759
2557
|
const platform = process.platform;
|
|
@@ -2786,16 +2584,24 @@ function tryTerminalBell() {
|
|
|
2786
2584
|
process.stdout.write("\x07");
|
|
2787
2585
|
} catch {}
|
|
2788
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
|
+
}
|
|
2789
2595
|
function notifySessionIdle() {
|
|
2790
2596
|
notify("FlowDeck Task Completed", "Agent is idle and waiting for your next instruction", "info");
|
|
2791
2597
|
}
|
|
2792
|
-
function notifyPermissionNeeded(
|
|
2793
|
-
notify("FlowDeck Permission Required", `Agent needs approval to use tool: ${
|
|
2598
|
+
function notifyPermissionNeeded(tool17) {
|
|
2599
|
+
notify("FlowDeck Permission Required", `Agent needs approval to use tool: ${tool17}`, "critical");
|
|
2794
2600
|
}
|
|
2795
2601
|
|
|
2796
2602
|
// src/hooks/patch-trust.ts
|
|
2797
|
-
import { existsSync as
|
|
2798
|
-
import { join as
|
|
2603
|
+
import { existsSync as existsSync20, readFileSync as readFileSync19 } from "fs";
|
|
2604
|
+
import { join as join19 } from "path";
|
|
2799
2605
|
var HIGH_RISK_KEYWORDS = [
|
|
2800
2606
|
"password",
|
|
2801
2607
|
"secret",
|
|
@@ -2817,11 +2623,11 @@ var HIGH_RISK_KEYWORDS = [
|
|
|
2817
2623
|
"privilege"
|
|
2818
2624
|
];
|
|
2819
2625
|
function loadVolatility(directory) {
|
|
2820
|
-
const p =
|
|
2821
|
-
if (!
|
|
2626
|
+
const p = join19(codebaseDir(directory), "VOLATILITY.json");
|
|
2627
|
+
if (!existsSync20(p))
|
|
2822
2628
|
return {};
|
|
2823
2629
|
try {
|
|
2824
|
-
const data = JSON.parse(
|
|
2630
|
+
const data = JSON.parse(readFileSync19(p, "utf-8"));
|
|
2825
2631
|
const map = {};
|
|
2826
2632
|
for (const entry of data.entries ?? [])
|
|
2827
2633
|
map[entry.path] = entry.stability;
|
|
@@ -2831,11 +2637,11 @@ function loadVolatility(directory) {
|
|
|
2831
2637
|
}
|
|
2832
2638
|
}
|
|
2833
2639
|
function loadFailedPaths(directory) {
|
|
2834
|
-
const p =
|
|
2835
|
-
if (!
|
|
2640
|
+
const p = join19(codebaseDir(directory), "FAILURES.json");
|
|
2641
|
+
if (!existsSync20(p))
|
|
2836
2642
|
return [];
|
|
2837
2643
|
try {
|
|
2838
|
-
const data = JSON.parse(
|
|
2644
|
+
const data = JSON.parse(readFileSync19(p, "utf-8"));
|
|
2839
2645
|
return (data.entries ?? []).flatMap((e) => e.affected_paths ?? []);
|
|
2840
2646
|
} catch {
|
|
2841
2647
|
return [];
|
|
@@ -2900,8 +2706,8 @@ async function patchTrustHook(ctx, input, output) {
|
|
|
2900
2706
|
}
|
|
2901
2707
|
|
|
2902
2708
|
// src/hooks/decision-trace-hook.ts
|
|
2903
|
-
import { existsSync as
|
|
2904
|
-
import { join as
|
|
2709
|
+
import { existsSync as existsSync21, mkdirSync as mkdirSync11, appendFileSync as appendFileSync3 } from "fs";
|
|
2710
|
+
import { join as join20 } from "path";
|
|
2905
2711
|
async function decisionTraceHook(ctx, input, output) {
|
|
2906
2712
|
if (input.tool !== "write" && input.tool !== "edit")
|
|
2907
2713
|
return;
|
|
@@ -2910,7 +2716,7 @@ async function decisionTraceHook(ctx, input, output) {
|
|
|
2910
2716
|
return;
|
|
2911
2717
|
const base = codebaseDir(ctx.directory);
|
|
2912
2718
|
try {
|
|
2913
|
-
if (!
|
|
2719
|
+
if (!existsSync21(base))
|
|
2914
2720
|
mkdirSync11(base, { recursive: true });
|
|
2915
2721
|
const entry = {
|
|
2916
2722
|
timestamp: new Date().toISOString(),
|
|
@@ -2923,23 +2729,23 @@ async function decisionTraceHook(ctx, input, output) {
|
|
|
2923
2729
|
risk_level: "unknown",
|
|
2924
2730
|
auto_recorded: true
|
|
2925
2731
|
};
|
|
2926
|
-
appendFileSync3(
|
|
2732
|
+
appendFileSync3(join20(base, "DECISIONS.jsonl"), JSON.stringify(entry) + `
|
|
2927
2733
|
`, "utf-8");
|
|
2928
2734
|
} catch {}
|
|
2929
2735
|
}
|
|
2930
2736
|
|
|
2931
2737
|
// src/services/telemetry.ts
|
|
2932
|
-
import { existsSync as
|
|
2933
|
-
import { join as
|
|
2738
|
+
import { existsSync as existsSync22, readFileSync as readFileSync20, appendFileSync as appendFileSync4, mkdirSync as mkdirSync12 } from "fs";
|
|
2739
|
+
import { join as join21 } from "path";
|
|
2934
2740
|
import { randomUUID } from "crypto";
|
|
2935
2741
|
function telemetryPath(dir) {
|
|
2936
|
-
return
|
|
2742
|
+
return join21(codebaseDir(dir), "TELEMETRY.jsonl");
|
|
2937
2743
|
}
|
|
2938
2744
|
function appendEvent(dir, partial) {
|
|
2939
2745
|
if (process.env.TELEMETRY_ENABLED !== "true")
|
|
2940
2746
|
return null;
|
|
2941
2747
|
const cd = codebaseDir(dir);
|
|
2942
|
-
if (!
|
|
2748
|
+
if (!existsSync22(cd))
|
|
2943
2749
|
mkdirSync12(cd, { recursive: true });
|
|
2944
2750
|
const event = {
|
|
2945
2751
|
id: randomUUID(),
|
|
@@ -2976,34 +2782,34 @@ function inferStatus(output) {
|
|
|
2976
2782
|
}
|
|
2977
2783
|
async function telemetryHook(context, toolInput, output) {
|
|
2978
2784
|
const dir = context.directory ?? process.cwd();
|
|
2979
|
-
const
|
|
2785
|
+
const tool17 = toolInput.name ?? toolInput.tool ?? "unknown";
|
|
2980
2786
|
const ids = resolveIds(toolInput);
|
|
2981
2787
|
appendEvent(dir, {
|
|
2982
2788
|
session_id: ids.session_id,
|
|
2983
2789
|
run_id: ids.run_id,
|
|
2984
2790
|
event: "tool.call",
|
|
2985
|
-
tool:
|
|
2791
|
+
tool: tool17,
|
|
2986
2792
|
status: "ok",
|
|
2987
2793
|
meta: { parameters: output.args ?? {} }
|
|
2988
2794
|
});
|
|
2989
2795
|
}
|
|
2990
2796
|
async function telemetryAfterHook(context, toolInput, output) {
|
|
2991
2797
|
const dir = context.directory ?? process.cwd();
|
|
2992
|
-
const
|
|
2798
|
+
const tool17 = toolInput.name ?? toolInput.tool ?? "unknown";
|
|
2993
2799
|
const ids = resolveIds(toolInput);
|
|
2994
2800
|
const status = inferStatus(output);
|
|
2995
2801
|
appendEvent(dir, {
|
|
2996
2802
|
session_id: ids.session_id,
|
|
2997
2803
|
run_id: ids.run_id,
|
|
2998
2804
|
event: "tool.complete",
|
|
2999
|
-
tool:
|
|
2805
|
+
tool: tool17,
|
|
3000
2806
|
status
|
|
3001
2807
|
});
|
|
3002
2808
|
}
|
|
3003
2809
|
|
|
3004
2810
|
// src/services/approval-manager.ts
|
|
3005
|
-
import { existsSync as
|
|
3006
|
-
import { join as
|
|
2811
|
+
import { existsSync as existsSync23, readFileSync as readFileSync21, writeFileSync as writeFileSync15, mkdirSync as mkdirSync13 } from "fs";
|
|
2812
|
+
import { join as join22 } from "path";
|
|
3007
2813
|
var APPROVAL_TTL_MS = 30 * 60 * 1000;
|
|
3008
2814
|
var SENSITIVE_PATTERNS = [
|
|
3009
2815
|
/auth/i,
|
|
@@ -3040,14 +2846,14 @@ function isSensitivePath(filePath) {
|
|
|
3040
2846
|
return SENSITIVE_PATTERNS.some((p) => p.test(filePath));
|
|
3041
2847
|
}
|
|
3042
2848
|
function approvalsPath(dir) {
|
|
3043
|
-
return
|
|
2849
|
+
return join22(codebaseDir(dir), "APPROVALS.json");
|
|
3044
2850
|
}
|
|
3045
2851
|
function loadStore2(dir) {
|
|
3046
2852
|
const p = approvalsPath(dir);
|
|
3047
|
-
if (!
|
|
2853
|
+
if (!existsSync23(p))
|
|
3048
2854
|
return { requests: [] };
|
|
3049
2855
|
try {
|
|
3050
|
-
return JSON.parse(
|
|
2856
|
+
return JSON.parse(readFileSync21(p, "utf-8"));
|
|
3051
2857
|
} catch {
|
|
3052
2858
|
return { requests: [] };
|
|
3053
2859
|
}
|
|
@@ -3065,8 +2871,8 @@ async function approvalHook(context, toolInput, output) {
|
|
|
3065
2871
|
if (!ENABLED2)
|
|
3066
2872
|
return;
|
|
3067
2873
|
const dir = context.directory ?? process.cwd();
|
|
3068
|
-
const
|
|
3069
|
-
if (!WRITE_TOOLS.has(
|
|
2874
|
+
const tool17 = toolInput.name ?? toolInput.tool ?? "";
|
|
2875
|
+
if (!WRITE_TOOLS.has(tool17))
|
|
3070
2876
|
return;
|
|
3071
2877
|
const args = output.args ?? {};
|
|
3072
2878
|
const filePath = String(args.path ?? args.file_path ?? args.filename ?? "");
|
|
@@ -3081,7 +2887,7 @@ async function approvalHook(context, toolInput, output) {
|
|
|
3081
2887
|
session_id: process.env.OPENCODE_SESSION_ID ?? "session-0",
|
|
3082
2888
|
run_id: process.env.OPENCODE_RUN_ID ?? "run-0",
|
|
3083
2889
|
event: "approval.request",
|
|
3084
|
-
tool:
|
|
2890
|
+
tool: tool17,
|
|
3085
2891
|
status: "blocked",
|
|
3086
2892
|
files: [filePath],
|
|
3087
2893
|
meta: { trigger: "sensitive_file", file: filePath }
|
|
@@ -3142,8 +2948,8 @@ function createContextWindowMonitorHook() {
|
|
|
3142
2948
|
}
|
|
3143
2949
|
|
|
3144
2950
|
// src/hooks/shell-env-hook.ts
|
|
3145
|
-
import { existsSync as
|
|
3146
|
-
import { join as
|
|
2951
|
+
import { existsSync as existsSync24, readFileSync as readFileSync22 } from "fs";
|
|
2952
|
+
import { join as join23 } from "path";
|
|
3147
2953
|
import { createRequire } from "module";
|
|
3148
2954
|
var _version;
|
|
3149
2955
|
function getVersion() {
|
|
@@ -3179,7 +2985,7 @@ var MARKER_TO_LANG = {
|
|
|
3179
2985
|
};
|
|
3180
2986
|
function detectPackageManager(root) {
|
|
3181
2987
|
for (const [lockfile, pm] of Object.entries(LOCKFILE_TO_PM)) {
|
|
3182
|
-
if (
|
|
2988
|
+
if (existsSync24(join23(root, lockfile)))
|
|
3183
2989
|
return pm;
|
|
3184
2990
|
}
|
|
3185
2991
|
return;
|
|
@@ -3188,7 +2994,7 @@ function detectLanguages(root) {
|
|
|
3188
2994
|
const langs = [];
|
|
3189
2995
|
const seen = new Set;
|
|
3190
2996
|
for (const [marker, lang] of Object.entries(MARKER_TO_LANG)) {
|
|
3191
|
-
if (!seen.has(lang) &&
|
|
2997
|
+
if (!seen.has(lang) && existsSync24(join23(root, marker))) {
|
|
3192
2998
|
langs.push(lang);
|
|
3193
2999
|
seen.add(lang);
|
|
3194
3000
|
}
|
|
@@ -3196,11 +3002,11 @@ function detectLanguages(root) {
|
|
|
3196
3002
|
return langs;
|
|
3197
3003
|
}
|
|
3198
3004
|
function readCurrentPhase(root) {
|
|
3199
|
-
const statePath2 =
|
|
3200
|
-
if (!
|
|
3005
|
+
const statePath2 = join23(root, ".planning", "STATE.md");
|
|
3006
|
+
if (!existsSync24(statePath2))
|
|
3201
3007
|
return;
|
|
3202
3008
|
try {
|
|
3203
|
-
const content =
|
|
3009
|
+
const content = readFileSync22(statePath2, "utf-8");
|
|
3204
3010
|
const match = content.match(/phase:\s*(\S+)/i);
|
|
3205
3011
|
return match?.[1];
|
|
3206
3012
|
} catch {
|
|
@@ -3249,8 +3055,15 @@ function createTodoHook(client) {
|
|
|
3249
3055
|
// src/hooks/file-tracker.ts
|
|
3250
3056
|
class SessionFileTracker {
|
|
3251
3057
|
changes = new Map;
|
|
3058
|
+
onFileChange;
|
|
3059
|
+
setOnFileChange(callback) {
|
|
3060
|
+
this.onFileChange = callback;
|
|
3061
|
+
}
|
|
3252
3062
|
record(path, type) {
|
|
3253
3063
|
this.changes.set(path, { path, type });
|
|
3064
|
+
if (this.onFileChange) {
|
|
3065
|
+
this.onFileChange(path, type);
|
|
3066
|
+
}
|
|
3254
3067
|
}
|
|
3255
3068
|
getChanges() {
|
|
3256
3069
|
return [...this.changes.values()];
|
|
@@ -3299,8 +3112,8 @@ function createSessionIdleHook(client, tracker) {
|
|
|
3299
3112
|
}
|
|
3300
3113
|
|
|
3301
3114
|
// src/hooks/compaction-hook.ts
|
|
3302
|
-
import { existsSync as
|
|
3303
|
-
import { join as
|
|
3115
|
+
import { existsSync as existsSync25, readFileSync as readFileSync23 } from "fs";
|
|
3116
|
+
import { join as join24 } from "path";
|
|
3304
3117
|
var STRUCTURED_SUMMARY_PROMPT = `
|
|
3305
3118
|
When summarizing this session, you MUST include the following sections:
|
|
3306
3119
|
|
|
@@ -3339,11 +3152,11 @@ For each: agent name, status, description, session_id.
|
|
|
3339
3152
|
**RESUME, DON'T RESTART.** Use session_id to continue existing sessions.
|
|
3340
3153
|
`;
|
|
3341
3154
|
function readPlanningState2(directory) {
|
|
3342
|
-
const statePath2 =
|
|
3343
|
-
if (!
|
|
3155
|
+
const statePath2 = join24(directory, ".planning", "STATE.md");
|
|
3156
|
+
if (!existsSync25(statePath2))
|
|
3344
3157
|
return null;
|
|
3345
3158
|
try {
|
|
3346
|
-
const content =
|
|
3159
|
+
const content = readFileSync23(statePath2, "utf-8");
|
|
3347
3160
|
return content.slice(0, 1500);
|
|
3348
3161
|
} catch {
|
|
3349
3162
|
return null;
|
|
@@ -3360,6 +3173,18 @@ function createCompactionHook(ctx, tracker) {
|
|
|
3360
3173
|
sections.push("```");
|
|
3361
3174
|
sections.push("");
|
|
3362
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
|
+
}
|
|
3363
3188
|
const edited = tracker.getEditedPaths();
|
|
3364
3189
|
if (edited.length > 0) {
|
|
3365
3190
|
sections.push("## Recently Edited Files");
|
|
@@ -3370,12 +3195,6 @@ function createCompactionHook(ctx, tracker) {
|
|
|
3370
3195
|
sections.push(`- … and ${edited.length - 20} more`);
|
|
3371
3196
|
sections.push("");
|
|
3372
3197
|
}
|
|
3373
|
-
const sessionContext = getContextForDirectory(ctx.directory);
|
|
3374
|
-
if (sessionContext) {
|
|
3375
|
-
sections.push("## Previous Sessions Context");
|
|
3376
|
-
sections.push(sessionContext);
|
|
3377
|
-
sections.push("");
|
|
3378
|
-
}
|
|
3379
3198
|
output.context.push(sections.join(`
|
|
3380
3199
|
`));
|
|
3381
3200
|
output.prompt = STRUCTURED_SUMMARY_PROMPT.trim();
|
|
@@ -3587,6 +3406,14 @@ function createFlowDeckMcps() {
|
|
|
3587
3406
|
oauth: false
|
|
3588
3407
|
};
|
|
3589
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
|
+
}
|
|
3590
3417
|
return mcps;
|
|
3591
3418
|
}
|
|
3592
3419
|
|
|
@@ -3624,7 +3451,7 @@ MUST execute at session start:
|
|
|
3624
3451
|
3. Check which steps are marked complete
|
|
3625
3452
|
4. Begin execution from the first incomplete step
|
|
3626
3453
|
|
|
3627
|
-
If STATE.md does not exist, tell the user: "No STATE.md found. Run \`/fd-new-
|
|
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."
|
|
3628
3455
|
|
|
3629
3456
|
## Phase Gating
|
|
3630
3457
|
|
|
@@ -3635,6 +3462,30 @@ If the project is in another phase:
|
|
|
3635
3462
|
- **plan** phase: "Run \`/fd-plan\` to create the implementation plan first."
|
|
3636
3463
|
- **review** phase: "Run \`/fd-verify\` to complete the review phase."
|
|
3637
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
|
+
|
|
3638
3489
|
## Step Execution
|
|
3639
3490
|
|
|
3640
3491
|
For each incomplete step in PLAN.md:
|
|
@@ -3897,6 +3748,11 @@ var PLANNER_PROMPT = `You create implementation plans that developers can execut
|
|
|
3897
3748
|
3. Check for conflicts with existing design decisions
|
|
3898
3749
|
4. Define new interfaces if needed (before implementation)
|
|
3899
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
|
+
|
|
3900
3756
|
### Step Breakdown
|
|
3901
3757
|
- Each step maps to a single file or closely related file group
|
|
3902
3758
|
- Steps are ordered by dependency (foundation first, UI last)
|
|
@@ -4958,13 +4814,36 @@ var createDocUpdaterAgent = (model, customPrompt, customAppendPrompt) => {
|
|
|
4958
4814
|
// src/agents/mapper.ts
|
|
4959
4815
|
var MAPPER_PROMPT = `You read source files and produce accurate documentation. You report only what you can verify by reading the code directly.
|
|
4960
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
|
+
|
|
4961
4840
|
## Factual-Only Constraint
|
|
4962
4841
|
|
|
4963
4842
|
- If you are not certain about something, write: \`UNKNOWN — needs verification\`
|
|
4964
4843
|
- Never fill gaps with assumptions or what "probably" works
|
|
4965
4844
|
- Every claim must be traceable to a specific file and line
|
|
4966
4845
|
|
|
4967
|
-
## Reading Source Files
|
|
4846
|
+
## Reading Source Files (fallback when codegraph unavailable)
|
|
4968
4847
|
|
|
4969
4848
|
- Read files directly using file tools — do not rely on memory
|
|
4970
4849
|
- Note exact file paths for every claim you make
|
|
@@ -4995,13 +4874,14 @@ Write only your assigned file. Read existing \`.codebase/\` files before writing
|
|
|
4995
4874
|
- Identify runtime, framework, database, testing, and build tools
|
|
4996
4875
|
|
|
4997
4876
|
### ARCHITECTURE.md
|
|
4877
|
+
- Use \`codegraph_context\` on entry points to map the architecture (if codegraph available)
|
|
4998
4878
|
- Identify major components and their responsibilities
|
|
4999
4879
|
- Map data flow from input to output
|
|
5000
4880
|
- Document integration points (external APIs, databases, queues)
|
|
5001
4881
|
- Draw component diagram in text format
|
|
5002
4882
|
|
|
5003
4883
|
### CONVENTIONS.md
|
|
5004
|
-
- Find actual naming patterns by reading source files
|
|
4884
|
+
- Find actual naming patterns by reading source files or using \`codegraph_explore\`
|
|
5005
4885
|
- Include file:line examples for each pattern
|
|
5006
4886
|
- Document import style (relative paths? barrel exports? absolute aliases?)
|
|
5007
4887
|
- Document error handling pattern from real code
|
|
@@ -5038,6 +4918,35 @@ var createMapperAgent = (model, customPrompt, customAppendPrompt) => {
|
|
|
5038
4918
|
// src/agents/code-explorer.ts
|
|
5039
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.
|
|
5040
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
|
+
|
|
5041
4950
|
## Your Outputs
|
|
5042
4951
|
|
|
5043
4952
|
**File structure:**
|
|
@@ -5060,7 +4969,7 @@ var CODE_EXPLORER_PROMPT = `You map unfamiliar code before anyone touches it. Yo
|
|
|
5060
4969
|
- Error handling approach (throw, return, Result type)
|
|
5061
4970
|
- Testing patterns (file co-location, separate __tests__, naming)
|
|
5062
4971
|
|
|
5063
|
-
## Exploration Process
|
|
4972
|
+
## Exploration Process (fallback when codegraph unavailable)
|
|
5064
4973
|
|
|
5065
4974
|
1. \`ls -la\` the top-level directory — understand the layout
|
|
5066
4975
|
2. Read \`package.json\`, \`go.mod\`, \`Cargo.toml\`, or equivalent — identify the tech stack and dependencies
|
|
@@ -5071,7 +4980,7 @@ var CODE_EXPLORER_PROMPT = `You map unfamiliar code before anyone touches it. Yo
|
|
|
5071
4980
|
4. Trace the most important call path relevant to the current task
|
|
5072
4981
|
5. Read test files to understand expected behavior
|
|
5073
4982
|
|
|
5074
|
-
## Quick Commands
|
|
4983
|
+
## Quick Commands (fallback)
|
|
5075
4984
|
|
|
5076
4985
|
\`\`\`bash
|
|
5077
4986
|
# Find all TypeScript files
|
|
@@ -5089,6 +4998,7 @@ grep -r "export.*functionName" src/
|
|
|
5089
4998
|
|
|
5090
4999
|
## Rules
|
|
5091
5000
|
|
|
5001
|
+
- **CodeGraph first** — if codegraph index is available, use it before reaching for grep or file reads
|
|
5092
5002
|
- **Read-only** — never modify files during exploration
|
|
5093
5003
|
- **State uncertainty** — if you are not sure what something does, say so
|
|
5094
5004
|
- **Report what you see** — not what you expect or what would make sense
|
|
@@ -5099,6 +5009,11 @@ grep -r "export.*functionName" src/
|
|
|
5099
5009
|
\`\`\`markdown
|
|
5100
5010
|
## Codebase Exploration
|
|
5101
5011
|
|
|
5012
|
+
### CodeGraph Status
|
|
5013
|
+
- installed: yes/no
|
|
5014
|
+
- indexed: yes/no
|
|
5015
|
+
- used: yes/no (if yes: list tools used)
|
|
5016
|
+
|
|
5102
5017
|
### Structure
|
|
5103
5018
|
\`\`\`
|
|
5104
5019
|
src/
|
|
@@ -5125,6 +5040,18 @@ Request → \`src/routes/users.ts:34\` → \`src/services/user-service.ts:89\`
|
|
|
5125
5040
|
- \`src/services/user-service.ts\` — core business logic
|
|
5126
5041
|
- \`src/db/user-repo.ts\` — data access
|
|
5127
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.
|
|
5128
5055
|
\`\`\``;
|
|
5129
5056
|
var createCodeExplorerAgent = (model, customPrompt, customAppendPrompt) => {
|
|
5130
5057
|
const prompt = resolvePrompt(CODE_EXPLORER_PROMPT, customPrompt, customAppendPrompt);
|
|
@@ -5454,16 +5381,78 @@ var DISCUSSER_PROMPT = `You extract clear requirements through focused questioni
|
|
|
5454
5381
|
|
|
5455
5382
|
Load \`.planning/PROJECT.md\` first if it exists. Use existing context to avoid asking about already-decided things.
|
|
5456
5383
|
|
|
5457
|
-
##
|
|
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
|
|
5458
5440
|
|
|
5459
5441
|
- **ONE question per turn** — never ask two questions at once
|
|
5460
5442
|
- **Follow-up when unclear** — if an answer is ambiguous, ask for clarification before moving on
|
|
5461
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
|
|
5462
5446
|
|
|
5463
|
-
|
|
5464
|
-
|
|
5447
|
+
## Suppressed Questions
|
|
5448
|
+
|
|
5449
|
+
If a question can be answered from exploration evidence, skip it and record it:
|
|
5465
5450
|
|
|
5466
|
-
|
|
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
|
|
5467
5456
|
\`\`\`
|
|
5468
5457
|
|
|
5469
5458
|
## Decision Tracking
|
|
@@ -5481,16 +5470,28 @@ D-03: Social login — excluded from MVP scope
|
|
|
5481
5470
|
|
|
5482
5471
|
## Conflict Detection
|
|
5483
5472
|
|
|
5484
|
-
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:
|
|
5485
5474
|
|
|
5486
5475
|
\`\`\`
|
|
5487
5476
|
CONFLICT: D-04 (users can stay logged in for 30 days) conflicts with D-01 (JWT, stateless).
|
|
5488
|
-
Long-lived JWTs create security risks. Options:
|
|
5489
|
-
1. Use refresh tokens with short-lived access tokens
|
|
5490
|
-
2. Use sessions instead of JWT
|
|
5491
|
-
3. Accept the 30-day JWT with a revocation list
|
|
5492
5477
|
|
|
5493
|
-
|
|
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).
|
|
5494
5495
|
\`\`\`
|
|
5495
5496
|
|
|
5496
5497
|
## Saving Decisions
|
|
@@ -5508,6 +5509,18 @@ D-01: [topic] — [choice]
|
|
|
5508
5509
|
D-02: [topic] — [choice]
|
|
5509
5510
|
Rationale: [why]
|
|
5510
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
|
+
|
|
5511
5524
|
## Open Questions
|
|
5512
5525
|
- [anything unresolved]
|
|
5513
5526
|
|
|
@@ -5515,40 +5528,6 @@ D-02: [topic] — [choice]
|
|
|
5515
5528
|
- [explicitly excluded items]
|
|
5516
5529
|
\`\`\`
|
|
5517
5530
|
|
|
5518
|
-
## Question Bank
|
|
5519
|
-
|
|
5520
|
-
Use these question categories to ensure thorough coverage:
|
|
5521
|
-
|
|
5522
|
-
**Scope:**
|
|
5523
|
-
- What is included in this feature?
|
|
5524
|
-
- What is explicitly excluded?
|
|
5525
|
-
- What is the MVP vs. nice-to-have?
|
|
5526
|
-
|
|
5527
|
-
**Constraints:**
|
|
5528
|
-
- Timeline or deadline?
|
|
5529
|
-
- Budget or infrastructure limits?
|
|
5530
|
-
- Technology constraints (must use X, cannot use Y)?
|
|
5531
|
-
|
|
5532
|
-
**Integration:**
|
|
5533
|
-
- Does this interact with existing systems?
|
|
5534
|
-
- External APIs or services needed?
|
|
5535
|
-
|
|
5536
|
-
**User experience:**
|
|
5537
|
-
- Walk me through the user flow step by step
|
|
5538
|
-
- What happens when something goes wrong?
|
|
5539
|
-
|
|
5540
|
-
**Error handling:**
|
|
5541
|
-
- What should happen when [specific failure] occurs?
|
|
5542
|
-
- Who is notified on failure?
|
|
5543
|
-
|
|
5544
|
-
**Performance:**
|
|
5545
|
-
- How many users / requests / records expected?
|
|
5546
|
-
- Acceptable response time?
|
|
5547
|
-
|
|
5548
|
-
**Security:**
|
|
5549
|
-
- Who can access this feature?
|
|
5550
|
-
- What data is sensitive?
|
|
5551
|
-
|
|
5552
5531
|
## Completion Criteria
|
|
5553
5532
|
|
|
5554
5533
|
Discussion is complete when:
|
|
@@ -6327,7 +6306,7 @@ You sit above the orchestrator's execution path. Your only job is to inspect an
|
|
|
6327
6306
|
|
|
6328
6307
|
fd-ask, fd-checkpoint, fd-deploy-check, fd-design, fd-discuss, fd-doctor,
|
|
6329
6308
|
fd-execute, fd-fix-bug, fd-map-codebase, fd-multi-repo, fd-new-feature,
|
|
6330
|
-
fd-
|
|
6309
|
+
fd-plan, fd-quick, fd-reflect, fd-resume, fd-status,
|
|
6331
6310
|
fd-suggest, fd-translate-intent, fd-verify, fd-write-docs
|
|
6332
6311
|
|
|
6333
6312
|
## Registered Agents (source of truth — do not add to this list)
|
|
@@ -6919,7 +6898,6 @@ var REGISTERED_COMMANDS = [
|
|
|
6919
6898
|
"fd-map-codebase",
|
|
6920
6899
|
"fd-multi-repo",
|
|
6921
6900
|
"fd-new-feature",
|
|
6922
|
-
"fd-new-project",
|
|
6923
6901
|
"fd-plan",
|
|
6924
6902
|
"fd-quick",
|
|
6925
6903
|
"fd-reflect",
|
|
@@ -6928,7 +6906,8 @@ var REGISTERED_COMMANDS = [
|
|
|
6928
6906
|
"fd-suggest",
|
|
6929
6907
|
"fd-translate-intent",
|
|
6930
6908
|
"fd-verify",
|
|
6931
|
-
"fd-write-docs"
|
|
6909
|
+
"fd-write-docs",
|
|
6910
|
+
"fd-done"
|
|
6932
6911
|
];
|
|
6933
6912
|
function resolveSupervisorConfig(directory) {
|
|
6934
6913
|
try {
|
|
@@ -7065,12 +7044,12 @@ function computeConfidence(exists, policyResult, ctx) {
|
|
|
7065
7044
|
return 0.45;
|
|
7066
7045
|
return 0.95;
|
|
7067
7046
|
}
|
|
7068
|
-
function resolveDecision(exists, policyResult, confidenceScore, threshold, ctx) {
|
|
7047
|
+
function resolveDecision(exists, policyResult, confidenceScore, threshold, ctx, clarificationQuestion) {
|
|
7069
7048
|
if (!exists) {
|
|
7070
7049
|
return { decision: "block", approvalStatus: "denied" };
|
|
7071
7050
|
}
|
|
7072
7051
|
if (ctx.approvalRequired && !ctx.approvalGranted) {
|
|
7073
|
-
return { decision: "escalate", approvalStatus: "escalated" };
|
|
7052
|
+
return { decision: "escalate", approvalStatus: "escalated", clarificationQuestion };
|
|
7074
7053
|
}
|
|
7075
7054
|
if (!policyResult.passed) {
|
|
7076
7055
|
if (policyResult.requiredChanges.length > 0) {
|
|
@@ -7079,11 +7058,11 @@ function resolveDecision(exists, policyResult, confidenceScore, threshold, ctx)
|
|
|
7079
7058
|
return { decision: "block", approvalStatus: "denied" };
|
|
7080
7059
|
}
|
|
7081
7060
|
if (confidenceScore < threshold) {
|
|
7082
|
-
return { decision: "escalate", approvalStatus: "escalated" };
|
|
7061
|
+
return { decision: "escalate", approvalStatus: "escalated", clarificationQuestion };
|
|
7083
7062
|
}
|
|
7084
7063
|
return { decision: "approve", approvalStatus: "approved" };
|
|
7085
7064
|
}
|
|
7086
|
-
function runSupervisorReview(directory, targetName, ctx = {}) {
|
|
7065
|
+
function runSupervisorReview(directory, targetName, ctx = {}, clarificationQuestion) {
|
|
7087
7066
|
const config = resolveSupervisorConfig(directory);
|
|
7088
7067
|
const reviewPhase = ctx.reviewPhase ?? "preflight";
|
|
7089
7068
|
const timestamp2 = new Date().toISOString();
|
|
@@ -7131,7 +7110,7 @@ function runSupervisorReview(directory, targetName, ctx = {}) {
|
|
|
7131
7110
|
}
|
|
7132
7111
|
const policyResult = targetType === "command" ? checkCommandPolicy(targetName, ctx) : checkAgentPolicy(targetName, ctx);
|
|
7133
7112
|
const confidenceScore = computeConfidence(exists, policyResult, ctx);
|
|
7134
|
-
const { decision, approvalStatus } = resolveDecision(exists, policyResult, confidenceScore, config.confidenceThreshold, ctx);
|
|
7113
|
+
const { decision, approvalStatus, clarificationQuestion: escalationQuestion } = resolveDecision(exists, policyResult, confidenceScore, config.confidenceThreshold, ctx, clarificationQuestion);
|
|
7135
7114
|
const reasons = policyResult.reasons.length > 0 ? policyResult.reasons : decision === "approve" ? [`Target "${targetName}" reviewed and approved for execution`] : [`Target "${targetName}" reviewed — decision: ${decision}`];
|
|
7136
7115
|
const supervisorDecision = {
|
|
7137
7116
|
decision,
|
|
@@ -7145,7 +7124,8 @@ function runSupervisorReview(directory, targetName, ctx = {}) {
|
|
|
7145
7124
|
approvalStatus,
|
|
7146
7125
|
confidenceScore,
|
|
7147
7126
|
reviewPhase,
|
|
7148
|
-
timestamp: timestamp2
|
|
7127
|
+
timestamp: timestamp2,
|
|
7128
|
+
...escalationQuestion ? { clarificationQuestion: escalationQuestion } : {}
|
|
7149
7129
|
};
|
|
7150
7130
|
_emitTelemetry(directory, supervisorDecision, ctx);
|
|
7151
7131
|
return supervisorDecision;
|
|
@@ -7185,13 +7165,13 @@ function _emitTelemetry(directory, decision, ctx) {
|
|
|
7185
7165
|
// src/index.ts
|
|
7186
7166
|
function loadRulePaths() {
|
|
7187
7167
|
const __dir = dirname4(fileURLToPath2(import.meta.url));
|
|
7188
|
-
const rulesDir =
|
|
7189
|
-
if (!
|
|
7168
|
+
const rulesDir = join25(__dir, "..", "src", "rules");
|
|
7169
|
+
if (!existsSync26(rulesDir))
|
|
7190
7170
|
return [];
|
|
7191
7171
|
const paths = [];
|
|
7192
7172
|
function walk(dir) {
|
|
7193
7173
|
for (const entry of readdirSync3(dir, { withFileTypes: true })) {
|
|
7194
|
-
const full =
|
|
7174
|
+
const full = join25(dir, entry.name);
|
|
7195
7175
|
if (entry.isDirectory()) {
|
|
7196
7176
|
walk(full);
|
|
7197
7177
|
} else if (entry.isFile() && entry.name.endsWith(".md") && entry.name !== "README.md") {
|
|
@@ -7204,8 +7184,8 @@ function loadRulePaths() {
|
|
|
7204
7184
|
}
|
|
7205
7185
|
function loadCommands() {
|
|
7206
7186
|
const __dir = dirname4(fileURLToPath2(import.meta.url));
|
|
7207
|
-
const commandsDir =
|
|
7208
|
-
if (!
|
|
7187
|
+
const commandsDir = join25(__dir, "..", "src", "commands");
|
|
7188
|
+
if (!existsSync26(commandsDir))
|
|
7209
7189
|
return {};
|
|
7210
7190
|
const commands = {};
|
|
7211
7191
|
try {
|
|
@@ -7213,7 +7193,7 @@ function loadCommands() {
|
|
|
7213
7193
|
if (!file.endsWith(".md"))
|
|
7214
7194
|
continue;
|
|
7215
7195
|
const name = basename(file, ".md");
|
|
7216
|
-
const raw =
|
|
7196
|
+
const raw = readFileSync24(join25(commandsDir, file), "utf-8");
|
|
7217
7197
|
let description;
|
|
7218
7198
|
let template = raw;
|
|
7219
7199
|
const fmMatch = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/);
|
|
@@ -7294,8 +7274,8 @@ var plugin = async (input, _options) => {
|
|
|
7294
7274
|
}
|
|
7295
7275
|
}
|
|
7296
7276
|
}
|
|
7297
|
-
const skillsDir =
|
|
7298
|
-
if (
|
|
7277
|
+
const skillsDir = join25(dirname4(fileURLToPath2(import.meta.url)), "..", "src", "skills");
|
|
7278
|
+
if (existsSync26(skillsDir)) {
|
|
7299
7279
|
const cfgAny = cfg;
|
|
7300
7280
|
if (!cfgAny.skills || typeof cfgAny.skills !== "object") {
|
|
7301
7281
|
cfgAny.skills = { paths: [] };
|
|
@@ -7336,47 +7316,23 @@ var plugin = async (input, _options) => {
|
|
|
7336
7316
|
"context-generator": contextGeneratorTool,
|
|
7337
7317
|
"create-skill": createSkillTool,
|
|
7338
7318
|
reflect: reflectTool,
|
|
7339
|
-
|
|
7340
|
-
"memory-status": memoryStatusTool
|
|
7319
|
+
codegraph: codegraphTool
|
|
7341
7320
|
},
|
|
7342
7321
|
"shell.env": shellEnvHook,
|
|
7343
7322
|
"todo.updated": todoHook,
|
|
7344
7323
|
"file.edited": fileEdited,
|
|
7345
7324
|
"file.watcher.updated": fileWatcherUpdated,
|
|
7346
7325
|
"experimental.session.compacting": compactionHook,
|
|
7326
|
+
"command.execute.before": async (input2, _output) => {
|
|
7327
|
+
notifyCommandInteraction(input2.command);
|
|
7328
|
+
},
|
|
7347
7329
|
"permission.ask": async (input2, _output) => {
|
|
7348
7330
|
notifyPermissionNeeded(input2.title);
|
|
7349
7331
|
},
|
|
7350
7332
|
event: async ({ event }) => {
|
|
7351
7333
|
const type = event?.type ?? "";
|
|
7352
|
-
|
|
7353
|
-
|
|
7354
|
-
const sessionId = event?.sessionID ?? event?.sessionId ?? "";
|
|
7355
|
-
if (sessionId) {
|
|
7356
|
-
memoryHook.onSessionCreated(directory, sessionId, event?.prompt);
|
|
7357
|
-
}
|
|
7358
|
-
await sessionStartHook({ directory });
|
|
7359
|
-
} else if (type === "message.updated") {
|
|
7360
|
-
const msgEvent = event?.event ?? event;
|
|
7361
|
-
const sessionId = msgEvent?.sessionID ?? msgEvent?.sessionId ?? "";
|
|
7362
|
-
if (sessionId) {
|
|
7363
|
-
memoryHook.onMessageUpdated(sessionId, msgEvent.role, msgEvent.content, directory);
|
|
7364
|
-
}
|
|
7365
|
-
} else if (type === "session.compacted") {
|
|
7366
|
-
const compactEvent = event?.event ?? event;
|
|
7367
|
-
const sessionId = compactEvent?.sessionID ?? compactEvent?.sessionId ?? "";
|
|
7368
|
-
if (sessionId) {
|
|
7369
|
-
memoryHook.onSessionCompact(sessionId, compactEvent.summary ?? "");
|
|
7370
|
-
}
|
|
7371
|
-
} else if (type === "session.deleted") {
|
|
7372
|
-
const delEvent = event?.event ?? event;
|
|
7373
|
-
const sessionId = delEvent?.sessionID ?? delEvent?.sessionId ?? "";
|
|
7374
|
-
if (sessionId) {
|
|
7375
|
-
memoryHook.onSessionEnd(sessionId);
|
|
7376
|
-
}
|
|
7377
|
-
}
|
|
7378
|
-
} catch (err) {
|
|
7379
|
-
console.error("[FlowDeck Memory] Event handler error:", err);
|
|
7334
|
+
if (type === "session.created" || type === "session.started") {
|
|
7335
|
+
await sessionStartHook({ directory });
|
|
7380
7336
|
}
|
|
7381
7337
|
await contextMonitor.event({ event });
|
|
7382
7338
|
orchestratorGuard.onEvent(event);
|
|
@@ -7437,14 +7393,6 @@ var plugin = async (input, _options) => {
|
|
|
7437
7393
|
},
|
|
7438
7394
|
"tool.execute.after": async (toolInput, toolOutput) => {
|
|
7439
7395
|
await telemetryAfterHook({ directory }, toolInput, toolOutput);
|
|
7440
|
-
try {
|
|
7441
|
-
const sessionId = toolInput?.sessionID ?? toolInput?.sessionId ?? "";
|
|
7442
|
-
if (sessionId && toolInput?.tool) {
|
|
7443
|
-
memoryHook.onToolExecuted(sessionId, toolInput.tool, toolInput, toolOutput?.output ?? null, directory);
|
|
7444
|
-
}
|
|
7445
|
-
} catch (err) {
|
|
7446
|
-
console.error("[FlowDeck Memory] Tool execution error:", err);
|
|
7447
|
-
}
|
|
7448
7396
|
const afterToolName = toolInput.tool ?? toolInput.name ?? "";
|
|
7449
7397
|
if (afterToolName === "delegate" || afterToolName === "run-pipeline") {
|
|
7450
7398
|
try {
|