@dv.nghiem/flowdeck 0.3.5 → 0.3.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/dist/agents/index.d.ts +2 -1
  2. package/dist/agents/index.d.ts.map +1 -1
  3. package/dist/agents/supervisor.d.ts +3 -0
  4. package/dist/agents/supervisor.d.ts.map +1 -0
  5. package/dist/config/schema.d.ts +36 -0
  6. package/dist/config/schema.d.ts.map +1 -1
  7. package/dist/dashboard/lib/state-reader.d.ts.map +1 -1
  8. package/dist/dashboard/server.mjs +0 -37
  9. package/dist/dashboard/types.d.ts +0 -2
  10. package/dist/dashboard/types.d.ts.map +1 -1
  11. package/dist/dashboard/views/index.ejs +0 -6
  12. package/dist/dashboard/views/partials/header.ejs +0 -4
  13. package/dist/hooks/memory-hook.d.ts +7 -0
  14. package/dist/hooks/memory-hook.d.ts.map +1 -1
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +1072 -91
  17. package/dist/services/agent-contract-registry.d.ts.map +1 -1
  18. package/dist/services/memory-store.d.ts +34 -1
  19. package/dist/services/memory-store.d.ts.map +1 -1
  20. package/dist/services/memory-store.test.d.ts +2 -0
  21. package/dist/services/memory-store.test.d.ts.map +1 -0
  22. package/dist/services/supervisor-binding.d.ts +114 -0
  23. package/dist/services/supervisor-binding.d.ts.map +1 -0
  24. package/dist/services/supervisor.test.d.ts +14 -0
  25. package/dist/services/supervisor.test.d.ts.map +1 -0
  26. package/dist/services/telemetry.d.ts +1 -1
  27. package/dist/services/telemetry.d.ts.map +1 -1
  28. package/dist/services/workflow-scorecard.d.ts +20 -0
  29. package/dist/services/workflow-scorecard.d.ts.map +1 -1
  30. package/dist/tools/memory-search.d.ts.map +1 -1
  31. package/dist/tools/memory-status.d.ts.map +1 -1
  32. package/docs/agents.md +1 -1
  33. package/docs/commands/fd-ask.md +1 -1
  34. package/docs/commands/fd-deploy-check.md +1 -1
  35. package/docs/commands/fd-discuss.md +1 -1
  36. package/docs/commands/fd-fix-bug.md +1 -1
  37. package/docs/commands/fd-new-feature.md +1 -1
  38. package/docs/commands/fd-verify.md +18 -0
  39. package/docs/commands/fd-write-docs.md +1 -1
  40. package/docs/feature-integration-architecture.md +1 -1
  41. package/docs/notifications.md +2 -2
  42. package/docs/quick-start.md +1 -1
  43. package/docs/skills.md +1 -1
  44. package/package.json +1 -1
  45. package/src/commands/fd-new-project.md +0 -1
  46. package/src/rules/common/agent-orchestration.md +1 -1
  47. package/src/skills/design-tokens/SKILL.md +250 -0
  48. package/src/skills/git-release/SKILL.md +1 -1
  49. package/src/skills/ui-design/SKILL.md +313 -0
package/dist/index.js CHANGED
@@ -1709,23 +1709,34 @@ import { Database } from "bun:sqlite";
1709
1709
  import { existsSync as existsSync15, mkdirSync as mkdirSync10 } from "fs";
1710
1710
  import { join as join15 } from "path";
1711
1711
  import { homedir } from "os";
1712
- var MEMORY_DIR = join15(homedir(), ".flowdeck-memory");
1713
- var DB_PATH = join15(MEMORY_DIR, "memory.db");
1714
- function ensureDir() {
1715
- if (!existsSync15(MEMORY_DIR)) {
1716
- mkdirSync10(MEMORY_DIR, { recursive: true });
1717
- }
1712
+ function resolveMemoryDir() {
1713
+ return process.env.FLOWDECK_MEMORY_DIR ?? join15(homedir(), ".flowdeck-memory");
1718
1714
  }
1715
+ var JS_RETRY_COUNT = 3;
1716
+ var JS_RETRY_BASE_MS = 50;
1719
1717
  var db = null;
1718
+ function debugLog(msg) {
1719
+ if (process.env.FLOWDECK_MEMORY_DEBUG) {
1720
+ console.error(`[FlowDeck Memory] ${msg}`);
1721
+ }
1722
+ }
1720
1723
  function getDb() {
1721
1724
  if (!db) {
1722
- ensureDir();
1723
- db = new Database(DB_PATH);
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}`);
1724
1731
  initializeSchema(db);
1725
1732
  }
1726
1733
  return db;
1727
1734
  }
1728
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");
1729
1740
  const schema = `
1730
1741
  CREATE TABLE IF NOT EXISTS sessions (
1731
1742
  id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -1753,6 +1764,7 @@ function initializeSchema(database) {
1753
1764
  id INTEGER PRIMARY KEY AUTOINCREMENT,
1754
1765
  session_id INTEGER NOT NULL UNIQUE,
1755
1766
  content TEXT NOT NULL,
1767
+ metadata TEXT,
1756
1768
  created_at TEXT NOT NULL DEFAULT (datetime('now')),
1757
1769
  FOREIGN KEY (session_id) REFERENCES sessions(id)
1758
1770
  );
@@ -1764,6 +1776,49 @@ function initializeSchema(database) {
1764
1776
  CREATE INDEX IF NOT EXISTS idx_sessions_directory ON sessions(directory);
1765
1777
  `;
1766
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);
1790
+ }
1791
+ function sleepSync(ms) {
1792
+ try {
1793
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
1794
+ } catch {
1795
+ const end = Date.now() + ms;
1796
+ while (Date.now() < end) {}
1797
+ }
1798
+ }
1799
+ function executeWrite(fn, context) {
1800
+ for (let attempt = 0;attempt <= JS_RETRY_COUNT; attempt++) {
1801
+ const start = Date.now();
1802
+ try {
1803
+ const result = fn();
1804
+ const duration = Date.now() - start;
1805
+ if (attempt > 0) {
1806
+ debugLog(`${context}: succeeded after ${attempt} JS retr${attempt === 1 ? "y" : "ies"} (${duration}ms)`);
1807
+ } else {
1808
+ debugLog(`${context}: completed in ${duration}ms`);
1809
+ }
1810
+ return result;
1811
+ } catch (err) {
1812
+ if (isBusyError(err) && attempt < JS_RETRY_COUNT) {
1813
+ const delay = JS_RETRY_BASE_MS * (attempt + 1);
1814
+ debugLog(`${context}: SQLITE_BUSY — JS retry ${attempt + 1}/${JS_RETRY_COUNT} after ${delay}ms`);
1815
+ sleepSync(delay);
1816
+ continue;
1817
+ }
1818
+ throw err;
1819
+ }
1820
+ }
1821
+ throw new Error(`${context}: exhausted all retries`);
1767
1822
  }
1768
1823
  function serializeToolInput(input) {
1769
1824
  if (!input)
@@ -1791,7 +1846,7 @@ function initSession(contentSessionId, project, directory) {
1791
1846
  database.prepare("UPDATE sessions SET last_active_at = ?, prompt_count = prompt_count + 1 WHERE id = ?").run(now, existing.id);
1792
1847
  return { ...existing, last_active_at: now, prompt_count: (existing.prompt_count || 0) + 1 };
1793
1848
  }
1794
- const result = database.prepare("INSERT INTO sessions (content_session_id, project, directory, created_at, last_active_at) VALUES (?, ?, ?, ?, ?)").run(contentSessionId, project, directory, now, now);
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);
1795
1850
  return {
1796
1851
  id: result.lastInsertRowid,
1797
1852
  content_session_id: contentSessionId,
@@ -1805,27 +1860,38 @@ function initSession(contentSessionId, project, directory) {
1805
1860
  function storeObservation(sessionId, toolName, toolInput, toolResponse, directory) {
1806
1861
  const database = getDb();
1807
1862
  const now = new Date().toISOString();
1808
- const result = database.prepare("INSERT INTO observations (session_id, tool_name, tool_input, tool_response, directory, created_at) VALUES (?, ?, ?, ?, ?, ?)").run(sessionId, toolName, serializeToolInput(toolInput), toolResponse ? toolResponse.slice(0, 1e4) : null, directory, now);
1809
- database.prepare("UPDATE sessions SET last_active_at = ? WHERE id = ?").run(now, sessionId);
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})`);
1810
1870
  return {
1811
1871
  id: result.lastInsertRowid,
1812
1872
  session_id: sessionId,
1813
1873
  tool_name: toolName,
1814
- tool_input: parseToolInput(serializeToolInput(toolInput)),
1815
- tool_response: toolResponse ? toolResponse.slice(0, 1e4) : null,
1874
+ tool_input: parseToolInput(serializedInput),
1875
+ tool_response: truncatedResponse,
1816
1876
  directory,
1817
1877
  created_at: now
1818
1878
  };
1819
1879
  }
1820
- function storeSummary(sessionId, content) {
1880
+ function storeSummary(sessionId, content, metadata) {
1821
1881
  const database = getDb();
1822
1882
  const now = new Date().toISOString();
1823
- database.prepare("INSERT OR REPLACE INTO summaries (session_id, content, created_at) VALUES (?, ?, ?)").run(sessionId, content, now);
1824
- database.prepare("UPDATE sessions SET summary = ? WHERE id = ?").run(content, sessionId);
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}`);
1825
1890
  return {
1826
- id: database.prepare("SELECT last_insert_rowid() as id").get().id,
1891
+ id,
1827
1892
  session_id: sessionId,
1828
1893
  content,
1894
+ metadata: metadata ?? null,
1829
1895
  created_at: now
1830
1896
  };
1831
1897
  }
@@ -1844,6 +1910,20 @@ function getObservationsForSession(sessionId) {
1844
1910
  tool_input: parseToolInput(obs.tool_input)
1845
1911
  }));
1846
1912
  }
1913
+ function getSessionSummary(sessionId) {
1914
+ const database = getDb();
1915
+ const row = database.prepare("SELECT * FROM summaries WHERE session_id = ?").get(sessionId);
1916
+ if (!row)
1917
+ return null;
1918
+ return {
1919
+ ...row,
1920
+ metadata: row.metadata ? JSON.parse(row.metadata) : null
1921
+ };
1922
+ }
1923
+ function getSessionByContentSessionId(contentSessionId) {
1924
+ const database = getDb();
1925
+ return database.prepare("SELECT * FROM sessions WHERE content_session_id = ?").get(contentSessionId) || null;
1926
+ }
1847
1927
  function getRecentObservations(directory, limit = 50) {
1848
1928
  const database = getDb();
1849
1929
  const rows = database.prepare(`SELECT o.*, s.project, s.content_session_id, s.created_at as session_created
@@ -1905,11 +1985,15 @@ function getContextForDirectory(directory, maxObservations = 20) {
1905
1985
  lines.push(`Output: ${preview}${observation.tool_response.length > 300 ? "..." : ""}`);
1906
1986
  }
1907
1987
  }
1908
- const summaries = getDb().prepare(`SELECT su.* FROM summaries su
1988
+ const summaryRows = getDb().prepare(`SELECT su.* FROM summaries su
1909
1989
  JOIN sessions s ON su.session_id = s.id
1910
1990
  WHERE s.directory = ?
1911
1991
  ORDER BY su.created_at DESC
1912
1992
  LIMIT 3`).all(directory);
1993
+ const summaries = summaryRows.map((r) => ({
1994
+ ...r,
1995
+ metadata: r.metadata ? JSON.parse(r.metadata) : null
1996
+ }));
1913
1997
  if (summaries.length > 0) {
1914
1998
  lines.push(`
1915
1999
  ## Session Summaries`);
@@ -1917,12 +2001,20 @@ function getContextForDirectory(directory, maxObservations = 20) {
1917
2001
  const date = sum.created_at ? new Date(sum.created_at).toLocaleDateString() : "unknown";
1918
2002
  lines.push(`
1919
2003
  ### [${date}]`);
1920
- lines.push(sum.content.slice(0, 500));
2004
+ lines.push(sum.content.slice(0, 2000));
1921
2005
  }
1922
2006
  }
1923
2007
  return lines.join(`
1924
2008
  `);
1925
2009
  }
2010
+ function getDbSettings() {
2011
+ const database = getDb();
2012
+ const journalMode = database.prepare("PRAGMA journal_mode").get().journal_mode;
2013
+ const busyTimeout = database.prepare("PRAGMA busy_timeout").get().timeout;
2014
+ const synchronous = database.prepare("PRAGMA synchronous").get().synchronous;
2015
+ const walAutocheckpoint = database.prepare("PRAGMA wal_autocheckpoint").get().wal_autocheckpoint;
2016
+ return { journal_mode: journalMode, busy_timeout: busyTimeout, synchronous, wal_autocheckpoint: walAutocheckpoint };
2017
+ }
1926
2018
 
1927
2019
  // src/tools/memory-search.ts
1928
2020
  var memorySearchTool = tool16({
@@ -1942,8 +2034,14 @@ var memorySearchTool = tool16({
1942
2034
  return JSON.stringify({ error: "Session not found", session_id: args.session_id });
1943
2035
  }
1944
2036
  const observations = getObservationsForSession(targetSession.id);
2037
+ const summary = getSessionSummary(targetSession.id);
1945
2038
  return JSON.stringify({
1946
2039
  session: targetSession,
2040
+ summary: summary ? {
2041
+ content: summary.content,
2042
+ metadata: summary.metadata,
2043
+ created_at: summary.created_at
2044
+ } : null,
1947
2045
  observations: observations.map((o) => ({
1948
2046
  tool_name: o.tool_name,
1949
2047
  tool_input: o.tool_input,
@@ -1990,69 +2088,57 @@ var memorySearchTool = tool16({
1990
2088
 
1991
2089
  // src/tools/memory-status.ts
1992
2090
  import { tool as tool17 } from "@opencode-ai/plugin";
1993
- import { Database as Database2 } from "bun:sqlite";
1994
2091
  import { existsSync as existsSync16 } from "fs";
1995
2092
  import { join as join16 } from "path";
1996
2093
  import { homedir as homedir2 } from "os";
1997
- var DB_PATH2 = join16(homedir2(), ".flowdeck-memory", "memory.db");
2094
+ function resolveDbPath() {
2095
+ return join16(process.env.FLOWDECK_MEMORY_DIR ?? join16(homedir2(), ".flowdeck-memory"), "memory.db");
2096
+ }
1998
2097
  var memoryStatusTool = tool17({
1999
2098
  description: "Check FlowDeck memory database status, statistics, and recent sessions",
2000
2099
  args: {},
2001
- async execute(_args, _context) {
2100
+ async execute(_args, context) {
2101
+ const directory = context?.directory ?? process.cwd();
2102
+ const dbPath = resolveDbPath();
2002
2103
  try {
2003
- const exists = existsSync16(DB_PATH2);
2004
- const result = {
2005
- database_exists: exists,
2006
- path: DB_PATH2,
2007
- status: exists ? "ACTIVE" : "NOT_INITIALIZED",
2008
- statistics: null
2009
- };
2010
- if (exists) {
2011
- try {
2012
- const db2 = new Database2(DB_PATH2);
2013
- const sessions = db2.prepare("SELECT COUNT(*) as count FROM sessions").get();
2014
- const observations = db2.prepare("SELECT COUNT(*) as count FROM observations").get();
2015
- const summaries = db2.prepare("SELECT COUNT(*) as count FROM summaries").get();
2016
- const recentSessions = db2.prepare(`
2017
- SELECT
2018
- id,
2019
- content_session_id,
2020
- project,
2021
- directory,
2022
- created_at,
2023
- last_active_at,
2024
- prompt_count
2025
- FROM sessions
2026
- ORDER BY last_active_at DESC
2027
- LIMIT 5
2028
- `).all();
2029
- result.statistics = {
2030
- sessions: sessions.count,
2031
- observations: observations.count,
2032
- summaries: summaries.count,
2033
- recent_sessions: recentSessions.map((s) => {
2034
- const obsCount = db2.prepare("SELECT COUNT(*) as count FROM observations WHERE session_id = ?").get(s.id);
2035
- return {
2036
- project: s.project,
2037
- directory: s.directory,
2038
- observations_in_session: obsCount.count,
2039
- last_active: s.last_active_at,
2040
- prompt_count: s.prompt_count
2041
- };
2042
- })
2043
- };
2044
- db2.close();
2045
- } catch (err) {
2046
- result.status = "ERROR";
2047
- result.statistics = { error: String(err) };
2048
- }
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);
2049
2111
  }
2050
- return JSON.stringify(result, null, 2);
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);
2051
2137
  } catch (err) {
2052
2138
  return JSON.stringify({
2053
2139
  status: "ERROR",
2054
2140
  error: String(err),
2055
- path: DB_PATH2
2141
+ path: dbPath
2056
2142
  }, null, 2);
2057
2143
  }
2058
2144
  }
@@ -2060,7 +2146,7 @@ var memoryStatusTool = tool17({
2060
2146
 
2061
2147
  // src/hooks/memory-hook.ts
2062
2148
  var MAX_TOOL_RESPONSE = 1e4;
2063
- var MAX_PROMPT_LENGTH = 2000;
2149
+ var MAX_SUMMARY_STORAGE = 50000;
2064
2150
  var activeSessions = new Map;
2065
2151
  function extractProjectFromDirectory(directory) {
2066
2152
  const parts = directory.split("/");
@@ -2071,6 +2157,59 @@ function truncate(str, max) {
2071
2157
  return str || "";
2072
2158
  return str.slice(0, max);
2073
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
+ }
2074
2213
  function onSessionCreated(directory, contentSessionId, prompt) {
2075
2214
  const project = extractProjectFromDirectory(directory);
2076
2215
  const session = initSession(contentSessionId, project, directory);
@@ -2095,7 +2234,11 @@ function onToolExecuted(contentSessionId, toolName, toolInput, toolResponse, dir
2095
2234
  };
2096
2235
  activeSessions.set(contentSessionId, ctx);
2097
2236
  }
2098
- storeObservation(ctx.sessionId, truncate(toolName, 200), toolInput, toolResponse ? truncate(toolResponse, MAX_TOOL_RESPONSE) : null, directory);
2237
+ try {
2238
+ storeObservation(ctx.sessionId, truncate(toolName, 200), toolInput, toolResponse ? truncate(toolResponse, MAX_TOOL_RESPONSE) : null, directory);
2239
+ } catch (err) {
2240
+ console.warn(`[FlowDeck Memory] Failed to store observation for tool "${toolName}":`, err);
2241
+ }
2099
2242
  }
2100
2243
  function onMessageUpdated(contentSessionId, role, content, directory) {
2101
2244
  if (role !== "assistant")
@@ -2114,33 +2257,64 @@ function onMessageUpdated(contentSessionId, role, content, directory) {
2114
2257
  };
2115
2258
  activeSessions.set(contentSessionId, ctx);
2116
2259
  }
2117
- storeObservation(ctx.sessionId, "assistant_message", { role }, truncate(content, MAX_TOOL_RESPONSE), directory);
2260
+ try {
2261
+ storeObservation(ctx.sessionId, "assistant_message", { role }, truncate(content, MAX_TOOL_RESPONSE), directory);
2262
+ } catch (err) {
2263
+ console.warn("[FlowDeck Memory] Failed to store assistant message observation:", err);
2264
+ }
2118
2265
  }
2119
2266
  function onSessionCompact(contentSessionId, summary) {
2120
- const ctx = activeSessions.get(contentSessionId);
2121
- if (!ctx)
2122
- return;
2123
- storeSummary(ctx.sessionId, truncate(summary, MAX_PROMPT_LENGTH));
2267
+ let ctx = activeSessions.get(contentSessionId);
2268
+ if (!ctx) {
2269
+ const dbSession = getSessionByContentSessionId(contentSessionId);
2270
+ if (!dbSession) {
2271
+ console.warn(`[FlowDeck Memory] onSessionCompact: no session found for contentSessionId=${contentSessionId} — summary discarded`);
2272
+ return;
2273
+ }
2274
+ ctx = {
2275
+ sessionId: dbSession.id,
2276
+ contentSessionId,
2277
+ project: dbSession.project,
2278
+ directory: dbSession.directory
2279
+ };
2280
+ activeSessions.set(contentSessionId, ctx);
2281
+ }
2282
+ const storedContent = truncate(summary, MAX_SUMMARY_STORAGE);
2283
+ try {
2284
+ const observations = getObservationsForSession(ctx.sessionId);
2285
+ const metadata = buildHandoffMetadata(ctx.sessionId, ctx.directory, summary, observations);
2286
+ storeSummary(ctx.sessionId, storedContent, metadata);
2287
+ } catch (err) {
2288
+ console.warn(`[FlowDeck Memory] Failed to store compaction summary for session ${ctx.sessionId}:`, err);
2289
+ }
2124
2290
  }
2125
2291
  function onSessionEnd(contentSessionId, lastMessage) {
2126
- const ctx = activeSessions.get(contentSessionId);
2127
- if (!ctx)
2128
- return;
2129
- if (lastMessage && lastMessage.trim()) {
2130
- storeSummary(ctx.sessionId, truncate(lastMessage, MAX_PROMPT_LENGTH));
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
+ }
2303
+ }
2304
+ if (ctx && lastMessage && lastMessage.trim()) {
2305
+ try {
2306
+ const observations = getObservationsForSession(ctx.sessionId);
2307
+ const metadata = buildHandoffMetadata(ctx.sessionId, ctx.directory, lastMessage, observations);
2308
+ storeSummary(ctx.sessionId, truncate(lastMessage, MAX_SUMMARY_STORAGE), metadata);
2309
+ } catch (err) {
2310
+ console.warn(`[FlowDeck Memory] Failed to store end-of-session summary for session ${ctx?.sessionId}:`, err);
2311
+ }
2131
2312
  }
2132
2313
  activeSessions.delete(contentSessionId);
2133
2314
  }
2134
2315
  function getSessionContext(directory, contentSessionId) {
2135
2316
  const context = getContextForDirectory(directory, 30);
2136
2317
  const previousSessions = getRecentSessions(directory, 5);
2137
- if (previousSessions.length > 0 && activeSessions.has(contentSessionId)) {
2138
- const ctx = activeSessions.get(contentSessionId);
2139
- for (const prev of previousSessions) {
2140
- if (prev.content_session_id === contentSessionId)
2141
- continue;
2142
- }
2143
- }
2144
2318
  return { context, previousSessions };
2145
2319
  }
2146
2320
  function clearSession(contentSessionId) {
@@ -6104,6 +6278,133 @@ var createDesignAgent = (model, customPrompt, customAppendPrompt) => {
6104
6278
  };
6105
6279
  };
6106
6280
 
6281
+ // src/agents/supervisor.ts
6282
+ var SUPERVISOR_PROMPT = `You are the FlowDeck Supervisor Agent — a governance layer that reviews existing commands and agents before or after execution.
6283
+
6284
+ ## Role and Hard Constraints
6285
+
6286
+ **You review. You do not execute.**
6287
+
6288
+ You sit above the orchestrator's execution path. Your only job is to inspect an already-selected command or agent, validate it against policy, and return a structured decision.
6289
+
6290
+ ### You MUST NEVER:
6291
+ - Invent a new command name
6292
+ - Invent a new workflow definition
6293
+ - Suggest creating a new agent
6294
+ - Replace or duplicate the orchestrator
6295
+ - Execute implementation tasks
6296
+ - Become a second dispatcher
6297
+ - Modify the intent of an existing command
6298
+
6299
+ ### You MAY:
6300
+ - Inspect an existing registered command or agent
6301
+ - Validate that required stages are present
6302
+ - Detect policy violations in the selected target
6303
+ - Flag risk before execution
6304
+ - Decide: approve / revise / block / escalate
6305
+ - Request that the orchestrator obtain missing prerequisites
6306
+
6307
+ ## Registered Commands (source of truth — do not add to this list)
6308
+
6309
+ fd-ask, fd-checkpoint, fd-deploy-check, fd-design, fd-discuss, fd-doctor,
6310
+ fd-execute, fd-fix-bug, fd-map-codebase, fd-multi-repo, fd-new-feature,
6311
+ fd-new-project, fd-plan, fd-quick, fd-reflect, fd-resume, fd-status,
6312
+ fd-suggest, fd-translate-intent, fd-verify, fd-write-docs
6313
+
6314
+ ## Registered Agents (source of truth — do not add to this list)
6315
+
6316
+ orchestrator, planner, backend-coder, frontend-coder, devops, plan-checker,
6317
+ tester, reviewer, researcher, writer, security-auditor, doc-updater, mapper,
6318
+ code-explorer, debug-specialist, build-error-resolver, task-splitter, discusser,
6319
+ architect, risk-analyst, policy-enforcer, performance-optimizer, refactor-guide,
6320
+ auto-learner, design, supervisor
6321
+
6322
+ ## Policy Checks
6323
+
6324
+ When reviewing a command or agent, evaluate ONLY the following against what already exists:
6325
+
6326
+ ### Design-first policy
6327
+ - If the task is UI-heavy (dashboard, landing page, web app, UI, UX, admin panel) and the current phase is "execute", the design stage MUST have completed with approval.
6328
+ - If design approval is absent: decision = revise | required change = complete design stage first.
6329
+
6330
+ ### Bugfix regression policy
6331
+ - If the command is fd-fix-bug, a regression test MUST exist before implementation.
6332
+ - If no regression test: decision = revise | required change = write failing regression test first.
6333
+
6334
+ ### Phase ordering policy
6335
+ - fd-execute must only run in the "execute" phase.
6336
+ - If invoked in a different phase: decision = revise.
6337
+
6338
+ ### Missing inputs policy
6339
+ - If a registered agent has required inputs listed in its contract and they are absent: decision = revise.
6340
+
6341
+ ### Approval gate policy
6342
+ - If an operation requires explicit human approval and none was granted: decision = escalate.
6343
+
6344
+ ### Unregistered target policy
6345
+ - If the requested command or agent is NOT in the registered lists above: decision = block.
6346
+ - Do NOT suggest or create a replacement. Report that the target is unavailable.
6347
+
6348
+ ## Decision Output Format
6349
+
6350
+ Always respond with a valid JSON object matching this schema exactly:
6351
+
6352
+ \`\`\`json
6353
+ {
6354
+ "decision": "approve" | "revise" | "block" | "escalate",
6355
+ "targetType": "command" | "agent" | "workflow",
6356
+ "targetName": "<exact registered name>",
6357
+ "exists": true | false,
6358
+ "reasons": ["<human-readable reason>"],
6359
+ "missingRequirements": ["<what is absent>"],
6360
+ "riskFlags": ["<risk description>"],
6361
+ "requiredChanges": ["<what must change before proceeding>"],
6362
+ "approvalStatus": "approved" | "pending" | "denied" | "escalated",
6363
+ "confidenceScore": 0.0–1.0,
6364
+ "reviewPhase": "preflight" | "post-stage",
6365
+ "timestamp": "<ISO 8601>"
6366
+ }
6367
+ \`\`\`
6368
+
6369
+ ### Decision rules:
6370
+ - **approve**: target exists, all policy checks pass, confidence ≥ threshold
6371
+ - **revise**: target exists, fixable issues found — list requiredChanges so caller can resolve
6372
+ - **block**: target does not exist OR critical unfixable policy violation
6373
+ - **escalate**: human approval required OR confidence below threshold
6374
+
6375
+ ### On unregistered targets:
6376
+ If a requested command or workflow is not in the registered lists, set:
6377
+ - decision: "block"
6378
+ - exists: false
6379
+ - reasons: explain the target is not registered
6380
+ - requiredChanges: list valid registered alternatives
6381
+ - Do NOT invent a new command or workflow to substitute
6382
+
6383
+ ## Diagnostics
6384
+
6385
+ Before issuing a decision, log:
6386
+ 1. Which existing command/agent was reviewed
6387
+ 2. Whether it exists in the registry
6388
+ 3. Which policy checks ran
6389
+ 4. Why the decision was reached
6390
+ 5. Whether review is preflight or post-stage
6391
+ 6. Whether human escalation is recommended`;
6392
+ function createSupervisorAgent(model, customPrompt, customAppendPrompt) {
6393
+ const prompt = resolvePrompt(SUPERVISOR_PROMPT, customPrompt, customAppendPrompt);
6394
+ const definition = {
6395
+ name: "supervisor",
6396
+ description: "Governance supervisor that reviews existing commands and agents before execution. Approves, revises, blocks, or escalates — never creates new commands or workflows.",
6397
+ config: {
6398
+ temperature: 0.1,
6399
+ prompt
6400
+ }
6401
+ };
6402
+ if (typeof model === "string" && model) {
6403
+ definition.config.model = model;
6404
+ }
6405
+ return definition;
6406
+ }
6407
+
6107
6408
  // src/agents/index.ts
6108
6409
  var AGENT_NAMES = [
6109
6410
  "orchestrator",
@@ -6130,7 +6431,8 @@ var AGENT_NAMES = [
6130
6431
  "performance-optimizer",
6131
6432
  "refactor-guide",
6132
6433
  "auto-learner",
6133
- "design"
6434
+ "design",
6435
+ "supervisor"
6134
6436
  ];
6135
6437
  var PRIMARY_AGENTS = new Set(["orchestrator"]);
6136
6438
  var ALL_MODES_AGENTS = new Set;
@@ -6196,6 +6498,8 @@ function createAgent(name, model, customPrompt, customAppendPrompt) {
6196
6498
  return createAutoLearnerAgent(model);
6197
6499
  case "design":
6198
6500
  return createDesignAgent(model, customPrompt, customAppendPrompt);
6501
+ case "supervisor":
6502
+ return createSupervisorAgent(model, customPrompt, customAppendPrompt);
6199
6503
  default:
6200
6504
  console.warn(`[flowdeck] Unknown agent: ${name}`);
6201
6505
  return;
@@ -6233,6 +6537,632 @@ function getAgentConfigs(agentModels) {
6233
6537
  return configs;
6234
6538
  }
6235
6539
 
6540
+ // src/services/agent-contract-registry.ts
6541
+ var CONTRACTS = [
6542
+ {
6543
+ agent: "orchestrator",
6544
+ role: "Coordinate multi-agent execution. Delegates all work — never implements directly.",
6545
+ allowedTaskTypes: ["orchestration", "coordination", "delegation", "phase-management"],
6546
+ requiredInputs: ["STATE.md", "PLAN.md"],
6547
+ expectedOutputFields: ["delegated_steps", "completed_steps", "current_phase"],
6548
+ allowedTools: [
6549
+ "delegate",
6550
+ "run-pipeline",
6551
+ "council",
6552
+ "planning-state",
6553
+ "codebase-state",
6554
+ "workspace-state",
6555
+ "repo-memory",
6556
+ "decision-trace",
6557
+ "policy-engine",
6558
+ "context-generator",
6559
+ "create-skill",
6560
+ "reflect"
6561
+ ],
6562
+ forbiddenActions: [
6563
+ "write_file",
6564
+ "edit_file",
6565
+ "create_file",
6566
+ "bash",
6567
+ "patch",
6568
+ "apply_patch",
6569
+ "read source files directly"
6570
+ ],
6571
+ escalationConditions: [
6572
+ "delegated agent fails twice",
6573
+ "delegation budget exhausted",
6574
+ "deadlock detected",
6575
+ "all agents blocked on the same step"
6576
+ ],
6577
+ stopConditions: [
6578
+ "all PLAN.md steps completed",
6579
+ "user requests stop",
6580
+ "budget exceeded with no fallback"
6581
+ ],
6582
+ successCriteria: [
6583
+ "all plan steps delegated and completed",
6584
+ "STATE.md phase updated to review",
6585
+ "no implementation performed directly by orchestrator"
6586
+ ]
6587
+ },
6588
+ {
6589
+ agent: "planner",
6590
+ role: "Create detailed implementation plans. Output PLAN.md with numbered steps.",
6591
+ allowedTaskTypes: ["planning", "task-breakdown", "step-decomposition"],
6592
+ requiredInputs: ["task description or STATE.md"],
6593
+ expectedOutputFields: ["steps", "phase"],
6594
+ allowedTools: ["read", "glob", "grep", "planning-state", "workspace-state"],
6595
+ forbiddenActions: [
6596
+ "write source files",
6597
+ "run bash commands",
6598
+ "edit application code",
6599
+ "implement features"
6600
+ ],
6601
+ escalationConditions: [
6602
+ "requirements are ambiguous",
6603
+ "dependencies between steps unclear",
6604
+ "conflicting constraints"
6605
+ ],
6606
+ stopConditions: ["PLAN.md written and reviewed by plan-checker", "user confirms plan"],
6607
+ successCriteria: [
6608
+ "PLAN.md contains numbered steps with assigned agents",
6609
+ "each step has clear success criteria",
6610
+ "no implementation performed"
6611
+ ]
6612
+ },
6613
+ {
6614
+ agent: "plan-checker",
6615
+ role: "Review PLAN.md quality before execution. Read-only.",
6616
+ allowedTaskTypes: ["plan-review", "quality-check"],
6617
+ requiredInputs: ["PLAN.md"],
6618
+ expectedOutputFields: ["verdict", "issues", "recommendations"],
6619
+ allowedTools: ["read", "glob", "grep"],
6620
+ forbiddenActions: ["write or edit any files", "modify PLAN.md"],
6621
+ escalationConditions: ["plan is fundamentally flawed", "critical gaps found"],
6622
+ stopConditions: ["review complete", "verdict issued"],
6623
+ successCriteria: ["structured review output", "no file modifications"]
6624
+ },
6625
+ {
6626
+ agent: "design",
6627
+ role: "Design UX, wireframes, and visual systems for UI-heavy tasks.",
6628
+ allowedTaskTypes: ["ux-design", "wireframe", "visual-system", "design-handoff", "frontend-handoff"],
6629
+ requiredInputs: ["task description", "requirements"],
6630
+ expectedOutputFields: ["design_stage", "wireframes", "component_structure", "design_tokens"],
6631
+ allowedTools: ["read", "write", "glob", "grep", "planning-state"],
6632
+ forbiddenActions: [
6633
+ "run bash commands",
6634
+ "write application logic",
6635
+ "implement backend code",
6636
+ "implement React components"
6637
+ ],
6638
+ escalationConditions: [
6639
+ "design requirements unclear",
6640
+ "conflicting UX requirements",
6641
+ "brand guidelines missing"
6642
+ ],
6643
+ stopConditions: ["design_stage=handoff_complete", "design_approved=true"],
6644
+ successCriteria: [
6645
+ "design document written",
6646
+ "design_stage set to handoff_complete",
6647
+ "design_approved set to true",
6648
+ "no application code written"
6649
+ ]
6650
+ },
6651
+ {
6652
+ agent: "backend-coder",
6653
+ role: "Implement backend features: API, services, data layer, business logic.",
6654
+ allowedTaskTypes: ["implementation", "backend", "api", "database", "service", "bugfix"],
6655
+ requiredInputs: ["PLAN.md step description", "relevant context files"],
6656
+ expectedOutputFields: ["files_modified", "summary"],
6657
+ allowedTools: ["read", "write", "edit", "bash", "glob", "grep"],
6658
+ forbiddenActions: [
6659
+ "modify frontend UI component files",
6660
+ "change CI/CD config without devops involvement"
6661
+ ],
6662
+ escalationConditions: [
6663
+ "architecture decision needed",
6664
+ "security-sensitive change without audit",
6665
+ "database migration required"
6666
+ ],
6667
+ stopConditions: ["step implementation complete", "tests pass", "reviewer approves"],
6668
+ successCriteria: [
6669
+ "code written per plan step",
6670
+ "no regressions introduced",
6671
+ "tests exist or updated"
6672
+ ]
6673
+ },
6674
+ {
6675
+ agent: "frontend-coder",
6676
+ role: "Implement frontend features: UI components, client state, rendering.",
6677
+ allowedTaskTypes: ["implementation", "frontend", "ui", "component", "styling", "bugfix"],
6678
+ requiredInputs: ["PLAN.md step description", "design handoff for UI-heavy tasks"],
6679
+ expectedOutputFields: ["files_modified", "summary"],
6680
+ allowedTools: ["read", "write", "edit", "bash", "glob", "grep"],
6681
+ forbiddenActions: [
6682
+ "modify backend API files",
6683
+ "change server configuration",
6684
+ "implement without approved design for UI-heavy tasks"
6685
+ ],
6686
+ escalationConditions: [
6687
+ "design handoff missing for UI-heavy task",
6688
+ "component library or design system unclear"
6689
+ ],
6690
+ stopConditions: ["step implementation complete", "tests pass", "reviewer approves"],
6691
+ successCriteria: [
6692
+ "components implemented per approved design",
6693
+ "no regressions introduced",
6694
+ "tests exist or updated"
6695
+ ]
6696
+ },
6697
+ {
6698
+ agent: "devops",
6699
+ role: "Implement DevOps and infrastructure changes: CI/CD, deployment, infra scripts.",
6700
+ allowedTaskTypes: ["implementation", "ci-cd", "deployment", "infrastructure", "operations"],
6701
+ requiredInputs: ["PLAN.md step description"],
6702
+ expectedOutputFields: ["files_modified", "summary"],
6703
+ allowedTools: ["read", "write", "edit", "bash", "glob", "grep"],
6704
+ forbiddenActions: [
6705
+ "modify application source code",
6706
+ "deploy to production without approval"
6707
+ ],
6708
+ escalationConditions: [
6709
+ "production deployment requires approval",
6710
+ "destructive infra change"
6711
+ ],
6712
+ stopConditions: ["pipeline or infra change complete", "reviewer approves"],
6713
+ successCriteria: ["infrastructure code written per plan", "no prod deployment without approval"]
6714
+ },
6715
+ {
6716
+ agent: "tester",
6717
+ role: "Write and run tests following TDD principles. Tests before implementation.",
6718
+ allowedTaskTypes: ["testing", "tdd", "regression", "integration-test", "unit-test"],
6719
+ requiredInputs: ["feature or step description", "relevant source files"],
6720
+ expectedOutputFields: ["test_files_written", "tests_passing", "coverage_summary"],
6721
+ allowedTools: ["read", "write", "edit", "bash", "glob", "grep"],
6722
+ forbiddenActions: [
6723
+ "delete failing tests to make suite pass",
6724
+ "implement application features",
6725
+ "skip TDD cycle (red → green → refactor)"
6726
+ ],
6727
+ escalationConditions: [
6728
+ "test infrastructure broken",
6729
+ "flaky tests blocking all progress"
6730
+ ],
6731
+ stopConditions: ["all tests pass", "coverage meets threshold"],
6732
+ successCriteria: [
6733
+ "tests written before implementation",
6734
+ "all new tests pass",
6735
+ "no test deletions to fix failures"
6736
+ ]
6737
+ },
6738
+ {
6739
+ agent: "reviewer",
6740
+ role: "Review code quality, security, and convention adherence. Read-only.",
6741
+ allowedTaskTypes: ["review", "code-review", "quality-check"],
6742
+ requiredInputs: ["files to review", "context of changes"],
6743
+ expectedOutputFields: ["verdict", "issues", "recommendations"],
6744
+ allowedTools: ["read", "glob", "grep"],
6745
+ forbiddenActions: [
6746
+ "write or edit any files",
6747
+ "make code changes",
6748
+ "approve security-sensitive changes without security audit"
6749
+ ],
6750
+ escalationConditions: [
6751
+ "security issues found",
6752
+ "critical bugs found",
6753
+ "architectural violations"
6754
+ ],
6755
+ stopConditions: ["review complete", "verdict issued"],
6756
+ successCriteria: [
6757
+ "structured review output with severity levels",
6758
+ "issues categorized",
6759
+ "no file modifications"
6760
+ ]
6761
+ },
6762
+ {
6763
+ agent: "security-auditor",
6764
+ role: "Security audit: OWASP Top 10, injection, auth vulnerabilities. Read-only.",
6765
+ allowedTaskTypes: ["security-audit", "vulnerability-scan", "auth-review"],
6766
+ requiredInputs: ["files to audit", "change context"],
6767
+ expectedOutputFields: ["findings", "severity_breakdown", "recommendations"],
6768
+ allowedTools: ["read", "glob", "grep"],
6769
+ forbiddenActions: [
6770
+ "write or edit files",
6771
+ "make changes to fix vulnerabilities directly"
6772
+ ],
6773
+ escalationConditions: [
6774
+ "CRITICAL vulnerability found",
6775
+ "auth bypass detected",
6776
+ "data exposure found"
6777
+ ],
6778
+ stopConditions: ["audit complete", "all findings documented"],
6779
+ successCriteria: [
6780
+ "OWASP checklist evaluated",
6781
+ "findings documented with severity levels",
6782
+ "no file modifications"
6783
+ ]
6784
+ },
6785
+ {
6786
+ agent: "researcher",
6787
+ role: "Research documentation, APIs, best practices. Read-only analysis.",
6788
+ allowedTaskTypes: ["research", "api-lookup", "documentation", "best-practices"],
6789
+ requiredInputs: ["research topic or question"],
6790
+ expectedOutputFields: ["findings", "references", "recommendations"],
6791
+ allowedTools: ["read", "glob", "grep", "web-search"],
6792
+ forbiddenActions: ["write or edit files", "implement solutions"],
6793
+ escalationConditions: [
6794
+ "critical information unavailable",
6795
+ "conflicting official documentation"
6796
+ ],
6797
+ stopConditions: ["research question answered", "findings documented"],
6798
+ successCriteria: [
6799
+ "findings clearly summarized",
6800
+ "sources cited",
6801
+ "no file modifications"
6802
+ ]
6803
+ },
6804
+ {
6805
+ agent: "architect",
6806
+ role: "Design system architecture, create ADRs, define API contracts.",
6807
+ allowedTaskTypes: ["architecture", "adr", "api-design", "system-design"],
6808
+ requiredInputs: ["feature or system description", "existing codebase context"],
6809
+ expectedOutputFields: ["architecture_document", "adr", "api_contracts"],
6810
+ allowedTools: ["read", "write", "glob", "grep", "planning-state"],
6811
+ forbiddenActions: ["write application code", "run bash commands"],
6812
+ escalationConditions: [
6813
+ "major architectural conflict with existing system",
6814
+ "breaking API change required"
6815
+ ],
6816
+ stopConditions: ["ADR written", "architecture reviewed"],
6817
+ successCriteria: [
6818
+ "architecture documented with tradeoffs",
6819
+ "no application code written"
6820
+ ]
6821
+ },
6822
+ {
6823
+ agent: "writer",
6824
+ role: "Draft project documentation: README, API docs, user guides.",
6825
+ allowedTaskTypes: ["documentation", "readme", "api-docs", "user-guide"],
6826
+ requiredInputs: ["feature description or codebase context"],
6827
+ expectedOutputFields: ["documentation_files"],
6828
+ allowedTools: ["read", "write", "edit", "glob", "grep"],
6829
+ forbiddenActions: ["modify application code", "run bash commands"],
6830
+ escalationConditions: ["documentation scope unclear"],
6831
+ stopConditions: ["docs written", "user confirms completeness"],
6832
+ successCriteria: [
6833
+ "documentation written and accurate",
6834
+ "no application code changed"
6835
+ ]
6836
+ },
6837
+ {
6838
+ agent: "doc-updater",
6839
+ role: "Update existing documentation after code changes.",
6840
+ allowedTaskTypes: ["documentation-update", "doc-sync"],
6841
+ requiredInputs: ["changed files", "change summary"],
6842
+ expectedOutputFields: ["updated_docs"],
6843
+ allowedTools: ["read", "write", "edit", "glob", "grep"],
6844
+ forbiddenActions: [
6845
+ "modify application code",
6846
+ "delete documentation without replacement"
6847
+ ],
6848
+ escalationConditions: ["documentation conflicts with implementation"],
6849
+ stopConditions: ["docs updated and synced"],
6850
+ successCriteria: ["docs reflect current code", "no application code changed"]
6851
+ },
6852
+ {
6853
+ agent: "supervisor",
6854
+ role: "Governance review layer. Inspects existing commands/agents, validates policy, returns structured approve/revise/block/escalate decision. Never creates new commands or workflows.",
6855
+ allowedTaskTypes: ["governance-review", "policy-check", "pre-execution-review", "post-stage-review"],
6856
+ requiredInputs: ["target name (command or agent)", "task context"],
6857
+ expectedOutputFields: ["decision", "targetType", "targetName", "exists", "reasons", "missingRequirements", "riskFlags", "requiredChanges", "approvalStatus", "confidenceScore"],
6858
+ allowedTools: ["read", "glob", "grep", "planning-state", "policy-engine"],
6859
+ forbiddenActions: [
6860
+ "create new commands",
6861
+ "create new workflows",
6862
+ "invent new agent names",
6863
+ "modify command intent",
6864
+ "replace orchestrator",
6865
+ "become second dispatcher",
6866
+ "execute implementation tasks",
6867
+ "write or edit source files",
6868
+ "run bash commands",
6869
+ "modify PLAN.md or STATE.md"
6870
+ ],
6871
+ escalationConditions: [
6872
+ "human approval required and not granted",
6873
+ "confidence below threshold",
6874
+ "critical policy violation with no safe path forward"
6875
+ ],
6876
+ stopConditions: ["structured decision issued", "review complete"],
6877
+ successCriteria: [
6878
+ "structured SupervisorDecision returned",
6879
+ "no new commands or workflows created",
6880
+ "existing registry not modified",
6881
+ "decision is one of: approve, revise, block, escalate"
6882
+ ]
6883
+ }
6884
+ ];
6885
+ var REGISTRY = new Map(CONTRACTS.map((c) => [c.agent, c]));
6886
+ function getContract(agent) {
6887
+ return REGISTRY.get(agent) ?? null;
6888
+ }
6889
+
6890
+ // src/services/supervisor-binding.ts
6891
+ var REGISTERED_COMMANDS = [
6892
+ "fd-ask",
6893
+ "fd-checkpoint",
6894
+ "fd-deploy-check",
6895
+ "fd-design",
6896
+ "fd-discuss",
6897
+ "fd-doctor",
6898
+ "fd-execute",
6899
+ "fd-fix-bug",
6900
+ "fd-map-codebase",
6901
+ "fd-multi-repo",
6902
+ "fd-new-feature",
6903
+ "fd-new-project",
6904
+ "fd-plan",
6905
+ "fd-quick",
6906
+ "fd-reflect",
6907
+ "fd-resume",
6908
+ "fd-status",
6909
+ "fd-suggest",
6910
+ "fd-translate-intent",
6911
+ "fd-verify",
6912
+ "fd-write-docs"
6913
+ ];
6914
+ function resolveSupervisorConfig(directory) {
6915
+ try {
6916
+ const config = loadFlowDeckConfig(directory);
6917
+ const sup = config?.governance?.supervisor ?? {};
6918
+ return {
6919
+ enabled: sup.enabled ?? false,
6920
+ mode: sup.mode ?? "advisory",
6921
+ reviewedTargets: sup.reviewedTargets ?? [],
6922
+ canBlock: sup.canBlock ?? true,
6923
+ confidenceThreshold: sup.confidenceThreshold ?? 0.7,
6924
+ postExecutionReview: sup.postExecutionReview ?? false
6925
+ };
6926
+ } catch {
6927
+ return {
6928
+ enabled: false,
6929
+ mode: "advisory",
6930
+ reviewedTargets: [],
6931
+ canBlock: true,
6932
+ confidenceThreshold: 0.7,
6933
+ postExecutionReview: false
6934
+ };
6935
+ }
6936
+ }
6937
+ function isRegisteredCommand(name) {
6938
+ return REGISTERED_COMMANDS.includes(name);
6939
+ }
6940
+ function isRegisteredAgent(name) {
6941
+ return AGENT_NAMES.includes(name);
6942
+ }
6943
+ function isRegisteredTarget(name) {
6944
+ if (isRegisteredCommand(name))
6945
+ return { exists: true, type: "command" };
6946
+ if (isRegisteredAgent(name))
6947
+ return { exists: true, type: "agent" };
6948
+ return { exists: false, type: "agent" };
6949
+ }
6950
+ function checkCommandPolicy(commandName, ctx) {
6951
+ const reasons = [];
6952
+ const riskFlags = [];
6953
+ const missingRequirements = [];
6954
+ const requiredChanges = [];
6955
+ if (commandName === "fd-new-feature" || commandName === "fd-execute") {
6956
+ const taskLower = (ctx.taskDescription ?? "").toLowerCase();
6957
+ const isUiHeavy = /landing page|dashboard|admin panel|website|web app|ui|ux|interface|frontend|component/.test(taskLower);
6958
+ if (isUiHeavy && ctx.currentPhase === "execute" && ctx.designApprovalPresent === false) {
6959
+ missingRequirements.push("design approval (design stage must complete before execute for UI-heavy tasks)");
6960
+ riskFlags.push("UI-heavy task entering execute phase without design approval");
6961
+ requiredChanges.push("Run /fd-design first and obtain design approval before proceeding to execute");
6962
+ }
6963
+ }
6964
+ if (commandName === "fd-fix-bug") {
6965
+ if (ctx.regressionTestPresent === false) {
6966
+ missingRequirements.push("regression test (required before bugfix implementation)");
6967
+ riskFlags.push("Bugfix command invoked without a regression test");
6968
+ requiredChanges.push("Write a failing regression test before implementing the fix");
6969
+ }
6970
+ }
6971
+ if (commandName === "fd-deploy-check") {
6972
+ if (ctx.prerequisitesMet === false && ctx.missingInputs && ctx.missingInputs.length > 0) {
6973
+ missingRequirements.push(...ctx.missingInputs);
6974
+ riskFlags.push("Deploy check attempted with unmet prerequisites");
6975
+ }
6976
+ }
6977
+ if (commandName === "fd-execute" && ctx.currentPhase && ctx.currentPhase !== "execute") {
6978
+ riskFlags.push(`fd-execute invoked in phase "${ctx.currentPhase}" instead of "execute"`);
6979
+ requiredChanges.push(`Ensure project phase is "execute" before running fd-execute (currently: ${ctx.currentPhase})`);
6980
+ }
6981
+ if (ctx.approvalRequired && !ctx.approvalGranted) {
6982
+ missingRequirements.push("human approval (required for this command)");
6983
+ riskFlags.push("Approval gate not satisfied");
6984
+ requiredChanges.push("Obtain explicit human approval before proceeding");
6985
+ }
6986
+ const passed = missingRequirements.length === 0 && riskFlags.length === 0 && requiredChanges.length === 0;
6987
+ if (passed) {
6988
+ reasons.push(`Command "${commandName}" passed all policy checks`);
6989
+ }
6990
+ return { passed, reasons, riskFlags, missingRequirements, requiredChanges };
6991
+ }
6992
+ function checkAgentPolicy(agentName, ctx) {
6993
+ const reasons = [];
6994
+ const riskFlags = [];
6995
+ const missingRequirements = [];
6996
+ const requiredChanges = [];
6997
+ const contract = getContract(agentName);
6998
+ if (!contract) {
6999
+ riskFlags.push(`Agent "${agentName}" has no registered capability contract`);
7000
+ return { passed: false, reasons, riskFlags, missingRequirements, requiredChanges };
7001
+ }
7002
+ if (ctx.missingInputs && ctx.missingInputs.length > 0) {
7003
+ for (const missing of ctx.missingInputs) {
7004
+ const isRequired = contract.requiredInputs.some((r) => r.toLowerCase().includes(missing.toLowerCase()) || missing.toLowerCase().includes(r.toLowerCase()));
7005
+ if (isRequired) {
7006
+ missingRequirements.push(missing);
7007
+ requiredChanges.push(`Provide "${missing}" before delegating to ${agentName}`);
7008
+ }
7009
+ }
7010
+ }
7011
+ if (ctx.approvalRequired && !ctx.approvalGranted) {
7012
+ const needsApproval = contract.escalationConditions.some((c) => c.toLowerCase().includes("approval") || c.toLowerCase().includes("approve"));
7013
+ if (needsApproval) {
7014
+ missingRequirements.push("human approval");
7015
+ riskFlags.push(`Agent "${agentName}" requires approval via escalation condition`);
7016
+ requiredChanges.push("Obtain explicit human approval before proceeding");
7017
+ }
7018
+ }
7019
+ if (agentName === "design" || agentName === "frontend-coder") {
7020
+ const taskLower = (ctx.taskDescription ?? "").toLowerCase();
7021
+ const isUiHeavy = /landing page|dashboard|admin panel|website|web app|ui|ux|interface|frontend|component/.test(taskLower);
7022
+ if (agentName === "frontend-coder" && isUiHeavy && ctx.designApprovalPresent === false) {
7023
+ missingRequirements.push("design handoff approval");
7024
+ riskFlags.push("frontend-coder invoked for UI-heavy task without approved design handoff");
7025
+ requiredChanges.push("Complete design stage and obtain design approval before delegating to frontend-coder");
7026
+ }
7027
+ }
7028
+ const passed = missingRequirements.length === 0 && riskFlags.length === 0;
7029
+ if (passed) {
7030
+ reasons.push(`Agent "${agentName}" passed all policy checks`);
7031
+ }
7032
+ return { passed, reasons, riskFlags, missingRequirements, requiredChanges };
7033
+ }
7034
+ function computeConfidence(exists, policyResult, ctx) {
7035
+ if (!exists)
7036
+ return 0;
7037
+ if (policyResult.riskFlags.length >= 3)
7038
+ return 0.2;
7039
+ if (policyResult.riskFlags.length === 2)
7040
+ return 0.4;
7041
+ if (policyResult.riskFlags.length === 1)
7042
+ return 0.6;
7043
+ if (policyResult.missingRequirements.length > 0)
7044
+ return 0.5;
7045
+ if (ctx.prerequisitesMet === false)
7046
+ return 0.45;
7047
+ return 0.95;
7048
+ }
7049
+ function resolveDecision(exists, policyResult, confidenceScore, threshold, ctx) {
7050
+ if (!exists) {
7051
+ return { decision: "block", approvalStatus: "denied" };
7052
+ }
7053
+ if (ctx.approvalRequired && !ctx.approvalGranted) {
7054
+ return { decision: "escalate", approvalStatus: "escalated" };
7055
+ }
7056
+ if (!policyResult.passed) {
7057
+ if (policyResult.requiredChanges.length > 0) {
7058
+ return { decision: "revise", approvalStatus: "pending" };
7059
+ }
7060
+ return { decision: "block", approvalStatus: "denied" };
7061
+ }
7062
+ if (confidenceScore < threshold) {
7063
+ return { decision: "escalate", approvalStatus: "escalated" };
7064
+ }
7065
+ return { decision: "approve", approvalStatus: "approved" };
7066
+ }
7067
+ function runSupervisorReview(directory, targetName, ctx = {}) {
7068
+ const config = resolveSupervisorConfig(directory);
7069
+ const reviewPhase = ctx.reviewPhase ?? "preflight";
7070
+ const timestamp2 = new Date().toISOString();
7071
+ if (config.reviewedTargets.length > 0 && !config.reviewedTargets.includes(targetName)) {
7072
+ return {
7073
+ decision: "approve",
7074
+ targetType: "agent",
7075
+ targetName,
7076
+ exists: true,
7077
+ reasons: [`Target "${targetName}" is not in the reviewed targets list — auto-approved`],
7078
+ missingRequirements: [],
7079
+ riskFlags: [],
7080
+ requiredChanges: [],
7081
+ approvalStatus: "approved",
7082
+ confidenceScore: 1,
7083
+ reviewPhase,
7084
+ timestamp: timestamp2
7085
+ };
7086
+ }
7087
+ const { exists, type: targetType } = isRegisteredTarget(targetName);
7088
+ if (!exists) {
7089
+ const decision2 = {
7090
+ decision: "block",
7091
+ targetType,
7092
+ targetName,
7093
+ exists: false,
7094
+ reasons: [
7095
+ `Target "${targetName}" is not registered in the FlowDeck command or agent registry.`,
7096
+ "The supervisor does not create new commands or workflows.",
7097
+ "Only registered targets can be executed."
7098
+ ],
7099
+ missingRequirements: [],
7100
+ riskFlags: [`Unregistered target: "${targetName}"`],
7101
+ requiredChanges: [
7102
+ `Use one of the registered commands: ${REGISTERED_COMMANDS.join(", ")}`,
7103
+ `Or use one of the registered agents: ${AGENT_NAMES.join(", ")}`
7104
+ ],
7105
+ approvalStatus: "denied",
7106
+ confidenceScore: 0,
7107
+ reviewPhase,
7108
+ timestamp: timestamp2
7109
+ };
7110
+ _emitTelemetry(directory, decision2, ctx);
7111
+ return decision2;
7112
+ }
7113
+ const policyResult = targetType === "command" ? checkCommandPolicy(targetName, ctx) : checkAgentPolicy(targetName, ctx);
7114
+ const confidenceScore = computeConfidence(exists, policyResult, ctx);
7115
+ const { decision, approvalStatus } = resolveDecision(exists, policyResult, confidenceScore, config.confidenceThreshold, ctx);
7116
+ const reasons = policyResult.reasons.length > 0 ? policyResult.reasons : decision === "approve" ? [`Target "${targetName}" reviewed and approved for execution`] : [`Target "${targetName}" reviewed — decision: ${decision}`];
7117
+ const supervisorDecision = {
7118
+ decision,
7119
+ targetType,
7120
+ targetName,
7121
+ exists,
7122
+ reasons,
7123
+ missingRequirements: policyResult.missingRequirements,
7124
+ riskFlags: policyResult.riskFlags,
7125
+ requiredChanges: policyResult.requiredChanges,
7126
+ approvalStatus,
7127
+ confidenceScore,
7128
+ reviewPhase,
7129
+ timestamp: timestamp2
7130
+ };
7131
+ _emitTelemetry(directory, supervisorDecision, ctx);
7132
+ return supervisorDecision;
7133
+ }
7134
+ function shouldProceed(decision, mode, canBlock) {
7135
+ if (!decision.exists)
7136
+ return false;
7137
+ if (!canBlock)
7138
+ return true;
7139
+ if (mode === "strict") {
7140
+ return decision.decision === "approve" || decision.decision === "revise";
7141
+ }
7142
+ return decision.decision !== "block" || decision.confidenceScore > 0.3;
7143
+ }
7144
+ function _emitTelemetry(directory, decision, ctx) {
7145
+ try {
7146
+ appendEvent(directory, {
7147
+ session_id: ctx.session_id ?? "session-0",
7148
+ run_id: ctx.run_id ?? "unknown",
7149
+ event: "supervisor.review",
7150
+ agent: "supervisor",
7151
+ status: decision.decision === "approve" ? "ok" : decision.decision === "block" ? "blocked" : decision.decision === "escalate" ? "approved" : "ok",
7152
+ meta: {
7153
+ targetName: decision.targetName,
7154
+ targetType: decision.targetType,
7155
+ exists: decision.exists,
7156
+ decision: decision.decision,
7157
+ confidenceScore: decision.confidenceScore,
7158
+ riskFlags: decision.riskFlags,
7159
+ missingRequirements: decision.missingRequirements,
7160
+ reviewPhase: decision.reviewPhase
7161
+ }
7162
+ });
7163
+ } catch {}
7164
+ }
7165
+
6236
7166
  // src/index.ts
6237
7167
  function loadRulePaths() {
6238
7168
  const __dir = dirname4(fileURLToPath2(import.meta.url));
@@ -6423,7 +7353,7 @@ var plugin = async (input, _options) => {
6423
7353
  const delEvent = event?.event ?? event;
6424
7354
  const sessionId = delEvent?.sessionID ?? delEvent?.sessionId ?? "";
6425
7355
  if (sessionId) {
6426
- memoryHook.clearSession(sessionId);
7356
+ memoryHook.onSessionEnd(sessionId);
6427
7357
  }
6428
7358
  }
6429
7359
  } catch (err) {
@@ -6452,6 +7382,33 @@ var plugin = async (input, _options) => {
6452
7382
  }
6453
7383
  }
6454
7384
  orchestratorGuard.check(toolInput.sessionID ?? "", toolInput.tool ?? toolInput.name ?? "");
7385
+ const toolName = toolInput.tool ?? toolInput.name ?? "";
7386
+ if (toolName === "delegate" || toolName === "run-pipeline") {
7387
+ const supConfig = resolveSupervisorConfig(directory);
7388
+ if (supConfig.enabled) {
7389
+ const args = toolOutput?.args ?? toolInput?.args ?? {};
7390
+ const agentTarget = typeof args.agent === "string" ? args.agent.replace(/^@/, "") : Array.isArray(args.steps) && args.steps[0]?.agent ? String(args.steps[0].agent).replace(/^@/, "") : "";
7391
+ if (agentTarget) {
7392
+ const decision = runSupervisorReview(directory, agentTarget, {
7393
+ taskDescription: typeof args.prompt === "string" ? args.prompt : undefined,
7394
+ reviewPhase: "preflight",
7395
+ session_id: toolInput.sessionID ?? toolInput.sessionId ?? ""
7396
+ });
7397
+ const proceed = shouldProceed(decision, supConfig.mode, supConfig.canBlock);
7398
+ appLog(`[Supervisor] ${decision.reviewPhase} review of "${decision.targetName}": ` + `decision=${decision.decision} exists=${decision.exists} confidence=${decision.confidenceScore.toFixed(2)} ` + `${decision.riskFlags.length > 0 ? `risks=[${decision.riskFlags.join("; ")}]` : ""}`);
7399
+ if (!proceed) {
7400
+ const summary = [
7401
+ `[Supervisor] Execution blocked for target "${decision.targetName}".`,
7402
+ ...decision.reasons,
7403
+ ...decision.missingRequirements.length > 0 ? [`Missing: ${decision.missingRequirements.join(", ")}`] : [],
7404
+ ...decision.requiredChanges.length > 0 ? [`Required changes: ${decision.requiredChanges.join("; ")}`] : []
7405
+ ].join(`
7406
+ `);
7407
+ throw new Error(summary);
7408
+ }
7409
+ }
7410
+ }
7411
+ }
6455
7412
  await telemetryHook({ directory }, toolInput, toolOutput);
6456
7413
  await approvalHook({ directory }, toolInput, toolOutput);
6457
7414
  await guardRailsHook({ directory }, toolInput, toolOutput);
@@ -6469,6 +7426,30 @@ var plugin = async (input, _options) => {
6469
7426
  } catch (err) {
6470
7427
  console.error("[FlowDeck Memory] Tool execution error:", err);
6471
7428
  }
7429
+ const afterToolName = toolInput.tool ?? toolInput.name ?? "";
7430
+ if (afterToolName === "delegate" || afterToolName === "run-pipeline") {
7431
+ try {
7432
+ const supConfig = resolveSupervisorConfig(directory);
7433
+ if (supConfig.enabled && supConfig.postExecutionReview) {
7434
+ const args = toolOutput?.args ?? toolInput?.args ?? {};
7435
+ const agentTarget = typeof args.agent === "string" ? args.agent.replace(/^@/, "") : Array.isArray(args.steps) && args.steps[0]?.agent ? String(args.steps[0].agent).replace(/^@/, "") : "";
7436
+ if (agentTarget) {
7437
+ const executionErrored = toolOutput?.error != null || toolOutput?.status === "error" || typeof toolOutput?.output === "string" && toolOutput.output.startsWith("Error:");
7438
+ const decision = runSupervisorReview(directory, agentTarget, {
7439
+ taskDescription: typeof args.prompt === "string" ? args.prompt : undefined,
7440
+ reviewPhase: "post-stage",
7441
+ session_id: toolInput.sessionID ?? toolInput.sessionId ?? "",
7442
+ prerequisitesMet: !executionErrored
7443
+ });
7444
+ const logLevel = decision.decision === "block" || decision.decision === "escalate" ? "[Supervisor][WARN]" : "[Supervisor]";
7445
+ appLog(`${logLevel} post-stage review of "${decision.targetName}": ` + `decision=${decision.decision} exists=${decision.exists} confidence=${decision.confidenceScore.toFixed(2)} ` + `executionErrored=${executionErrored} ` + `${decision.riskFlags.length > 0 ? `risks=[${decision.riskFlags.join("; ")}]` : ""}`);
7446
+ if (supConfig.mode === "strict" && !shouldProceed(decision, "strict", supConfig.canBlock)) {
7447
+ appLog(`[Supervisor][STRICT] Post-execution governance violation detected for "${decision.targetName}". ` + `Review the scorecard and telemetry for this run. ` + `Reasons: ${decision.reasons.join("; ")}`);
7448
+ }
7449
+ }
7450
+ }
7451
+ } catch {}
7452
+ }
6472
7453
  await contextMonitor["tool.execute.after"](toolInput, toolOutput);
6473
7454
  }
6474
7455
  };