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