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