@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.
- package/dist/agents/index.d.ts +2 -1
- package/dist/agents/index.d.ts.map +1 -1
- package/dist/agents/supervisor.d.ts +3 -0
- package/dist/agents/supervisor.d.ts.map +1 -0
- package/dist/config/schema.d.ts +36 -0
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/dashboard/lib/state-reader.d.ts.map +1 -1
- package/dist/dashboard/server.mjs +0 -37
- package/dist/dashboard/types.d.ts +0 -2
- package/dist/dashboard/types.d.ts.map +1 -1
- package/dist/dashboard/views/index.ejs +0 -6
- package/dist/dashboard/views/partials/header.ejs +0 -4
- package/dist/hooks/memory-hook.d.ts +7 -0
- package/dist/hooks/memory-hook.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1072 -91
- package/dist/services/agent-contract-registry.d.ts.map +1 -1
- package/dist/services/memory-store.d.ts +34 -1
- package/dist/services/memory-store.d.ts.map +1 -1
- package/dist/services/memory-store.test.d.ts +2 -0
- package/dist/services/memory-store.test.d.ts.map +1 -0
- package/dist/services/supervisor-binding.d.ts +114 -0
- package/dist/services/supervisor-binding.d.ts.map +1 -0
- package/dist/services/supervisor.test.d.ts +14 -0
- package/dist/services/supervisor.test.d.ts.map +1 -0
- package/dist/services/telemetry.d.ts +1 -1
- package/dist/services/telemetry.d.ts.map +1 -1
- package/dist/services/workflow-scorecard.d.ts +20 -0
- package/dist/services/workflow-scorecard.d.ts.map +1 -1
- package/dist/tools/memory-search.d.ts.map +1 -1
- package/dist/tools/memory-status.d.ts.map +1 -1
- package/docs/agents.md +1 -1
- package/docs/commands/fd-ask.md +1 -1
- package/docs/commands/fd-deploy-check.md +1 -1
- package/docs/commands/fd-discuss.md +1 -1
- package/docs/commands/fd-fix-bug.md +1 -1
- package/docs/commands/fd-new-feature.md +1 -1
- package/docs/commands/fd-verify.md +18 -0
- package/docs/commands/fd-write-docs.md +1 -1
- package/docs/feature-integration-architecture.md +1 -1
- package/docs/notifications.md +2 -2
- package/docs/quick-start.md +1 -1
- package/docs/skills.md +1 -1
- package/package.json +1 -1
- package/src/commands/fd-new-project.md +0 -1
- package/src/rules/common/agent-orchestration.md +1 -1
- package/src/skills/design-tokens/SKILL.md +250 -0
- package/src/skills/git-release/SKILL.md +1 -1
- 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
|
-
|
|
1713
|
-
|
|
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
|
-
|
|
1723
|
-
|
|
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
|
|
1809
|
-
|
|
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(
|
|
1815
|
-
tool_response:
|
|
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
|
-
|
|
1824
|
-
|
|
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
|
|
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
|
|
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,
|
|
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
|
-
|
|
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,
|
|
2100
|
+
async execute(_args, context) {
|
|
2101
|
+
const directory = context?.directory ?? process.cwd();
|
|
2102
|
+
const dbPath = resolveDbPath();
|
|
2002
2103
|
try {
|
|
2003
|
-
const exists = existsSync16(
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2121
|
-
if (!ctx)
|
|
2122
|
-
|
|
2123
|
-
|
|
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
|
-
|
|
2127
|
-
if (!ctx)
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
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.
|
|
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
|
};
|