@dv.nghiem/flowdeck 0.4.11 → 0.4.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +0 -2
- package/dist/dashboard/lib/state-reader.d.ts +2 -1
- package/dist/dashboard/lib/state-reader.d.ts.map +1 -1
- package/dist/dashboard/server.mjs +128 -13
- package/dist/dashboard/types.d.ts +12 -0
- package/dist/dashboard/types.d.ts.map +1 -1
- package/dist/hooks/orchestrator-guard-hook.d.ts.map +1 -1
- package/dist/hooks/shell-env-hook.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +125 -334
- package/dist/services/loop-detector.d.ts.map +1 -1
- package/docs/getting-started/installation.md +0 -18
- package/docs/index.md +0 -1
- package/docs/reference/hooks.md +1 -16
- package/package.json +6 -6
- package/src/rules/common/agent-defense.md +66 -0
- package/src/rules/common/agent-orchestration.md +35 -1
- package/src/skills/context-budget/SKILL.md +266 -0
- package/src/skills/context-guard/SKILL.md +172 -0
- package/src/skills/context-steward/SKILL.md +297 -0
- package/src/skills/decision-trace/SKILL.md +137 -0
- package/src/skills/research-first/SKILL.md +344 -0
- package/src/skills/session-persistence/SKILL.md +320 -0
- package/src/skills/telemetry-steward/SKILL.md +191 -0
- package/dist/services/rtk-manager.d.ts +0 -80
- package/dist/services/rtk-manager.d.ts.map +0 -1
- package/dist/services/rtk-policy.d.ts +0 -26
- package/dist/services/rtk-policy.d.ts.map +0 -1
- package/dist/tools/rtk-setup.d.ts +0 -22
- package/dist/tools/rtk-setup.d.ts.map +0 -1
- package/docs/reference/rtk.md +0 -162
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
|
-
import { readFileSync as readFileSync25, readdirSync as readdirSync3, existsSync as
|
|
3
|
-
import { join as
|
|
2
|
+
import { readFileSync as readFileSync25, readdirSync as readdirSync3, existsSync as existsSync26 } from "fs";
|
|
3
|
+
import { join as join25, basename as basename2 } from "path";
|
|
4
4
|
import { dirname as dirname3 } from "path";
|
|
5
5
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
6
6
|
|
|
@@ -1807,212 +1807,25 @@ var listRulesTool = tool11({
|
|
|
1807
1807
|
}
|
|
1808
1808
|
});
|
|
1809
1809
|
|
|
1810
|
-
// src/tools/
|
|
1810
|
+
// src/tools/merge-assist.ts
|
|
1811
1811
|
import { tool as tool12 } from "@opencode-ai/plugin";
|
|
1812
|
-
|
|
1813
|
-
|
|
1812
|
+
import { readFileSync as readFileSync15, writeFileSync as writeFileSync11, existsSync as existsSync15, mkdirSync as mkdirSync10 } from "fs";
|
|
1813
|
+
import { join as join15 } from "path";
|
|
1814
1814
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
1815
|
-
import { existsSync as existsSync14 } from "fs";
|
|
1816
|
-
import { homedir } from "os";
|
|
1817
|
-
import { join as join14 } from "path";
|
|
1818
|
-
|
|
1819
|
-
// src/services/rtk-policy.ts
|
|
1820
|
-
var SUPPORTED_COMMANDS = new Set([
|
|
1821
|
-
"git",
|
|
1822
|
-
"npm",
|
|
1823
|
-
"npx",
|
|
1824
|
-
"bun",
|
|
1825
|
-
"pnpm",
|
|
1826
|
-
"yarn",
|
|
1827
|
-
"tsc",
|
|
1828
|
-
"eslint",
|
|
1829
|
-
"biome",
|
|
1830
|
-
"oxlint",
|
|
1831
|
-
"jest",
|
|
1832
|
-
"vitest",
|
|
1833
|
-
"pytest",
|
|
1834
|
-
"cargo",
|
|
1835
|
-
"docker",
|
|
1836
|
-
"kubectl",
|
|
1837
|
-
"gh"
|
|
1838
|
-
]);
|
|
1839
|
-
var COMPACT_GIT_SUBCOMMANDS = new Set([
|
|
1840
|
-
"rev-parse",
|
|
1841
|
-
"hash-object",
|
|
1842
|
-
"cat-file",
|
|
1843
|
-
"ls-files",
|
|
1844
|
-
"ls-tree",
|
|
1845
|
-
"show-ref",
|
|
1846
|
-
"for-each-ref",
|
|
1847
|
-
"symbolic-ref",
|
|
1848
|
-
"config"
|
|
1849
|
-
]);
|
|
1850
|
-
var NEVER_WRAP = new Set(["codegraph", "curl", "sh", "bash", "zsh", "fish", "node", "python", "python3"]);
|
|
1851
|
-
function getSupportedCommands() {
|
|
1852
|
-
return [...SUPPORTED_COMMANDS].sort();
|
|
1853
|
-
}
|
|
1854
|
-
|
|
1855
|
-
// src/services/rtk-manager.ts
|
|
1856
|
-
var INSTALL_INSTRUCTIONS = [
|
|
1857
|
-
"rtk is not installed. To install it manually:",
|
|
1858
|
-
" Linux/macOS: curl -fsSL https://raw.githubusercontent.com/rtk-ai/rtk/refs/heads/master/install.sh | sh",
|
|
1859
|
-
" Then add ~/.local/bin to your PATH if needed.",
|
|
1860
|
-
"After installation, call rtk-setup again to verify detection."
|
|
1861
|
-
].join(`
|
|
1862
|
-
`);
|
|
1863
|
-
var CANDIDATE_PATHS = [join14(homedir(), ".local", "bin", "rtk"), "/usr/local/bin/rtk", "/usr/bin/rtk"];
|
|
1864
|
-
function detectRtk() {
|
|
1865
|
-
const fromPath = spawnSync2("rtk", ["--version"], { encoding: "utf-8", timeout: 5000 });
|
|
1866
|
-
if (fromPath.status === 0) {
|
|
1867
|
-
const version = (fromPath.stdout ?? "").trim().split(`
|
|
1868
|
-
`)[0] ?? "";
|
|
1869
|
-
return { installed: true, binPath: "rtk", version };
|
|
1870
|
-
}
|
|
1871
|
-
for (const candidate of CANDIDATE_PATHS) {
|
|
1872
|
-
if (!existsSync14(candidate))
|
|
1873
|
-
continue;
|
|
1874
|
-
const result = spawnSync2(candidate, ["--version"], { encoding: "utf-8", timeout: 5000 });
|
|
1875
|
-
if (result.status === 0) {
|
|
1876
|
-
const version = (result.stdout ?? "").trim().split(`
|
|
1877
|
-
`)[0] ?? "";
|
|
1878
|
-
return { installed: true, binPath: candidate, version };
|
|
1879
|
-
}
|
|
1880
|
-
}
|
|
1881
|
-
return {
|
|
1882
|
-
installed: false,
|
|
1883
|
-
error: "rtk binary not found in PATH or known install locations"
|
|
1884
|
-
};
|
|
1885
|
-
}
|
|
1886
|
-
function initRtk(binPath) {
|
|
1887
|
-
try {
|
|
1888
|
-
const result = spawnSync2(binPath, ["init", "-g"], {
|
|
1889
|
-
encoding: "utf-8",
|
|
1890
|
-
timeout: 30000,
|
|
1891
|
-
stdio: "pipe"
|
|
1892
|
-
});
|
|
1893
|
-
if (result.status !== 0) {
|
|
1894
|
-
return {
|
|
1895
|
-
success: false,
|
|
1896
|
-
log: (result.stdout ?? "").trim(),
|
|
1897
|
-
telemetryDisabled: false,
|
|
1898
|
-
error: (result.stderr ?? "").trim() || `rtk init -g exited with code ${result.status}`
|
|
1899
|
-
};
|
|
1900
|
-
}
|
|
1901
|
-
const telResult = spawnSync2(binPath, ["telemetry", "disable"], {
|
|
1902
|
-
encoding: "utf-8",
|
|
1903
|
-
timeout: 1e4,
|
|
1904
|
-
stdio: "pipe"
|
|
1905
|
-
});
|
|
1906
|
-
return {
|
|
1907
|
-
success: true,
|
|
1908
|
-
log: [
|
|
1909
|
-
`[rtk] init -g succeeded: ${(result.stdout ?? "").trim()}`,
|
|
1910
|
-
`[rtk] telemetry disable: ${telResult.status === 0 ? "ok" : `failed (code ${telResult.status}) — ${(telResult.stderr ?? "").trim()}`}`
|
|
1911
|
-
].filter(Boolean).join(`
|
|
1912
|
-
`),
|
|
1913
|
-
telemetryDisabled: telResult.status === 0
|
|
1914
|
-
};
|
|
1915
|
-
} catch (err) {
|
|
1916
|
-
return { success: false, log: "", telemetryDisabled: false, error: String(err) };
|
|
1917
|
-
}
|
|
1918
|
-
}
|
|
1919
|
-
function getRtkStatus(opts) {
|
|
1920
|
-
const detection = detectRtk();
|
|
1921
|
-
if (!detection.installed) {
|
|
1922
|
-
return {
|
|
1923
|
-
installed: false,
|
|
1924
|
-
initAttempted: false,
|
|
1925
|
-
initSuccess: false,
|
|
1926
|
-
telemetryDisabled: false,
|
|
1927
|
-
installInstructions: INSTALL_INSTRUCTIONS
|
|
1928
|
-
};
|
|
1929
|
-
}
|
|
1930
|
-
let initAttempted = false;
|
|
1931
|
-
let initSuccess = false;
|
|
1932
|
-
let telemetryDisabled = false;
|
|
1933
|
-
if (opts?.runInit && detection.binPath) {
|
|
1934
|
-
initAttempted = true;
|
|
1935
|
-
const initResult = initRtk(detection.binPath);
|
|
1936
|
-
initSuccess = initResult.success;
|
|
1937
|
-
telemetryDisabled = initResult.telemetryDisabled;
|
|
1938
|
-
}
|
|
1939
|
-
return {
|
|
1940
|
-
installed: true,
|
|
1941
|
-
binPath: detection.binPath,
|
|
1942
|
-
version: detection.version,
|
|
1943
|
-
initAttempted,
|
|
1944
|
-
initSuccess,
|
|
1945
|
-
telemetryDisabled
|
|
1946
|
-
};
|
|
1947
|
-
}
|
|
1948
|
-
|
|
1949
|
-
// src/tools/rtk-setup.ts
|
|
1950
|
-
var rtkSetupTool = tool12({
|
|
1951
|
-
description: [
|
|
1952
|
-
"Detect, initialize, and report status of rtk (output compression proxy for CLI commands).",
|
|
1953
|
-
"rtk reduces noisy CLI output (git, npm, test runners, linters, docker) by 60-90%.",
|
|
1954
|
-
"Call this to check if rtk is available, to run `rtk init -g`, or to get the binary path.",
|
|
1955
|
-
"When RTK_INSTALLED=true in the environment, use `$RTK_BIN git status` for compressed output."
|
|
1956
|
-
].join(" "),
|
|
1957
|
-
args: {
|
|
1958
|
-
action: tool12.schema.enum(["status", "init"]).optional().describe("'status' — detect and report rtk state (default). " + "'init' — detect, then run `rtk init -g` to install the bash hook. " + "Use 'init' only once per environment setup.")
|
|
1959
|
-
},
|
|
1960
|
-
async execute(args) {
|
|
1961
|
-
const action = args.action ?? "status";
|
|
1962
|
-
const runInit = action === "init";
|
|
1963
|
-
const status = getRtkStatus({ runInit });
|
|
1964
|
-
const lines = ["## rtk Status"];
|
|
1965
|
-
if (status.installed) {
|
|
1966
|
-
lines.push(`- **Installed**: yes`);
|
|
1967
|
-
lines.push(`- **Binary**: ${status.binPath ?? "rtk (in PATH)"}`);
|
|
1968
|
-
if (status.version)
|
|
1969
|
-
lines.push(`- **Version**: ${status.version}`);
|
|
1970
|
-
if (runInit) {
|
|
1971
|
-
if (status.initAttempted) {
|
|
1972
|
-
lines.push(`- **Init**: ${status.initSuccess ? "✓ succeeded (bash hook installed)" : "✗ failed"}`);
|
|
1973
|
-
lines.push(`- **Telemetry**: ${status.telemetryDisabled ? "✓ disabled (`rtk telemetry disable` ran)" : "⚠ disable step failed — run `rtk telemetry disable` manually"}`);
|
|
1974
|
-
if (status.initSuccess) {
|
|
1975
|
-
lines.push("", " **Bash hook caveat**: `rtk init -g` writes to Claude Code / Copilot global config.", " Whether it fires in non-interactive shell sessions depends on the runtime.", " For reliable compression, use `$RTK_BIN <cmd>` explicitly.", " `RTK_TELEMETRY_DISABLED=1` is always injected into bash sessions by FlowDeck.");
|
|
1976
|
-
}
|
|
1977
|
-
}
|
|
1978
|
-
} else {
|
|
1979
|
-
lines.push("- **Init**: not requested (pass `action: 'init'` to install bash hook)");
|
|
1980
|
-
lines.push("- **Telemetry**: `RTK_TELEMETRY_DISABLED=1` is always set in bash sessions by FlowDeck");
|
|
1981
|
-
}
|
|
1982
|
-
lines.push("", "### Using rtk", "In bash commands, replace `git status` with `$RTK_BIN git status`.", "The `RTK_BIN` env var is injected by FlowDeck into every bash session when rtk is detected.", "", "### Supported commands", getSupportedCommands().map((c) => `- \`${c}\``).join(`
|
|
1983
|
-
`));
|
|
1984
|
-
} else {
|
|
1985
|
-
lines.push("- **Installed**: no", "");
|
|
1986
|
-
lines.push("### Install rtk");
|
|
1987
|
-
if (status.installInstructions) {
|
|
1988
|
-
lines.push("```", status.installInstructions, "```");
|
|
1989
|
-
}
|
|
1990
|
-
lines.push("", "After installing, call `rtk-setup` again to verify detection.");
|
|
1991
|
-
}
|
|
1992
|
-
return lines.join(`
|
|
1993
|
-
`);
|
|
1994
|
-
}
|
|
1995
|
-
});
|
|
1996
|
-
|
|
1997
|
-
// src/tools/merge-assist.ts
|
|
1998
|
-
import { tool as tool13 } from "@opencode-ai/plugin";
|
|
1999
|
-
import { readFileSync as readFileSync15, writeFileSync as writeFileSync11, existsSync as existsSync16, mkdirSync as mkdirSync10 } from "fs";
|
|
2000
|
-
import { join as join16 } from "path";
|
|
2001
|
-
import { spawnSync as spawnSync3 } from "child_process";
|
|
2002
1815
|
|
|
2003
1816
|
// src/lib/logger.ts
|
|
2004
|
-
import { appendFileSync as appendFileSync3, existsSync as
|
|
2005
|
-
import { join as
|
|
1817
|
+
import { appendFileSync as appendFileSync3, existsSync as existsSync14, mkdirSync as mkdirSync9 } from "fs";
|
|
1818
|
+
import { join as join14 } from "path";
|
|
2006
1819
|
var LOG_DIR = ".opencode";
|
|
2007
1820
|
var LOG_FILE = "flowdeck.log";
|
|
2008
1821
|
function ensureLogDir(logDir) {
|
|
2009
|
-
if (!
|
|
1822
|
+
if (!existsSync14(logDir)) {
|
|
2010
1823
|
mkdirSync9(logDir, { recursive: true });
|
|
2011
1824
|
}
|
|
2012
1825
|
}
|
|
2013
1826
|
function logWrite(directory, level, source, message) {
|
|
2014
|
-
const logDir =
|
|
2015
|
-
const logFile =
|
|
1827
|
+
const logDir = join14(directory, LOG_DIR);
|
|
1828
|
+
const logFile = join14(logDir, LOG_FILE);
|
|
2016
1829
|
try {
|
|
2017
1830
|
ensureLogDir(logDir);
|
|
2018
1831
|
const entry = {
|
|
@@ -2029,14 +1842,14 @@ function logWrite(directory, level, source, message) {
|
|
|
2029
1842
|
// src/tools/merge-assist.ts
|
|
2030
1843
|
var MERGE_ASSIST_FILE = "MERGE_ASSIST.json";
|
|
2031
1844
|
function statePath2(directory) {
|
|
2032
|
-
return
|
|
1845
|
+
return join15(codebaseDir(directory), MERGE_ASSIST_FILE);
|
|
2033
1846
|
}
|
|
2034
1847
|
function emptyState() {
|
|
2035
1848
|
return { version: "1.0", lastUpdated: new Date().toISOString(), sessions: {} };
|
|
2036
1849
|
}
|
|
2037
1850
|
function readState(directory) {
|
|
2038
1851
|
const p = statePath2(directory);
|
|
2039
|
-
if (!
|
|
1852
|
+
if (!existsSync15(p))
|
|
2040
1853
|
return emptyState();
|
|
2041
1854
|
try {
|
|
2042
1855
|
return JSON.parse(readFileSync15(p, "utf-8"));
|
|
@@ -2047,7 +1860,7 @@ function readState(directory) {
|
|
|
2047
1860
|
function writeState(directory, state) {
|
|
2048
1861
|
try {
|
|
2049
1862
|
const base = codebaseDir(directory);
|
|
2050
|
-
if (!
|
|
1863
|
+
if (!existsSync15(base))
|
|
2051
1864
|
mkdirSync10(base, { recursive: true });
|
|
2052
1865
|
const newState = { ...state, lastUpdated: new Date().toISOString() };
|
|
2053
1866
|
writeFileSync11(statePath2(directory), JSON.stringify(newState, null, 2), "utf-8");
|
|
@@ -2063,7 +1876,7 @@ function generateId() {
|
|
|
2063
1876
|
return `ma-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
2064
1877
|
}
|
|
2065
1878
|
function safeGit(cwd, args) {
|
|
2066
|
-
const result =
|
|
1879
|
+
const result = spawnSync2("git", args, { cwd, encoding: "utf-8" });
|
|
2067
1880
|
return {
|
|
2068
1881
|
stdout: result.stdout?.trim() ?? "",
|
|
2069
1882
|
stderr: result.stderr?.trim() ?? "",
|
|
@@ -2071,7 +1884,7 @@ function safeGit(cwd, args) {
|
|
|
2071
1884
|
};
|
|
2072
1885
|
}
|
|
2073
1886
|
function isGitRepo(cwd) {
|
|
2074
|
-
return
|
|
1887
|
+
return existsSync15(join15(cwd, ".git")) && safeGit(cwd, ["rev-parse", "--git-dir"]).status === 0;
|
|
2075
1888
|
}
|
|
2076
1889
|
function branchExists(cwd, branch) {
|
|
2077
1890
|
return safeGit(cwd, ["show-ref", "--verify", "--quiet", `refs/heads/${branch}`]).status === 0;
|
|
@@ -2254,18 +2067,18 @@ function updateConfirmation(session, step, approved) {
|
|
|
2254
2067
|
function isStepApproved(session, step) {
|
|
2255
2068
|
return session.confirmations.some((c) => c.step === step && c.status === "approved");
|
|
2256
2069
|
}
|
|
2257
|
-
var mergeAssistTool =
|
|
2070
|
+
var mergeAssistTool = tool12({
|
|
2258
2071
|
description: "Human-in-the-loop selective branch integration. Provides structured analysis and confirmation state management for cherry-pick or manual port workflows. Never executes state-changing git commands.",
|
|
2259
2072
|
args: {
|
|
2260
|
-
action:
|
|
2261
|
-
targetBranch:
|
|
2262
|
-
sourceBranch:
|
|
2263
|
-
featureDescription:
|
|
2264
|
-
sessionId:
|
|
2265
|
-
selectedCommits:
|
|
2266
|
-
step:
|
|
2267
|
-
approved:
|
|
2268
|
-
integrationBranch:
|
|
2073
|
+
action: tool12.schema.enum(["start", "inspect", "plan", "confirm", "abort", "status", "list"]),
|
|
2074
|
+
targetBranch: tool12.schema.string().optional(),
|
|
2075
|
+
sourceBranch: tool12.schema.string().optional(),
|
|
2076
|
+
featureDescription: tool12.schema.string().optional(),
|
|
2077
|
+
sessionId: tool12.schema.string().optional(),
|
|
2078
|
+
selectedCommits: tool12.schema.array(tool12.schema.string()).optional(),
|
|
2079
|
+
step: tool12.schema.string().optional(),
|
|
2080
|
+
approved: tool12.schema.boolean().optional(),
|
|
2081
|
+
integrationBranch: tool12.schema.string().optional()
|
|
2269
2082
|
},
|
|
2270
2083
|
async execute(args, context) {
|
|
2271
2084
|
const dir = context.directory ?? process.cwd();
|
|
@@ -2470,8 +2283,8 @@ var mergeAssistTool = tool13({
|
|
|
2470
2283
|
});
|
|
2471
2284
|
|
|
2472
2285
|
// src/hooks/guard-rails.ts
|
|
2473
|
-
import { existsSync as
|
|
2474
|
-
import { join as
|
|
2286
|
+
import { existsSync as existsSync17, readFileSync as readFileSync17 } from "fs";
|
|
2287
|
+
import { join as join17 } from "path";
|
|
2475
2288
|
|
|
2476
2289
|
// src/lib/task-routing.ts
|
|
2477
2290
|
var UI_HEAVY_KEYWORDS = [
|
|
@@ -2517,21 +2330,21 @@ function isUiHeavyTask(input) {
|
|
|
2517
2330
|
}
|
|
2518
2331
|
|
|
2519
2332
|
// src/config/loader.ts
|
|
2520
|
-
import { existsSync as
|
|
2521
|
-
import { join as
|
|
2522
|
-
import { homedir
|
|
2333
|
+
import { existsSync as existsSync16, readFileSync as readFileSync16 } from "fs";
|
|
2334
|
+
import { join as join16 } from "path";
|
|
2335
|
+
import { homedir } from "os";
|
|
2523
2336
|
var CONFIG_FILENAME = "flowdeck.json";
|
|
2524
2337
|
function getGlobalConfigDir() {
|
|
2525
|
-
return process.env.OPENCODE_CONFIG_DIR || (process.env.XDG_CONFIG_HOME ?
|
|
2338
|
+
return process.env.OPENCODE_CONFIG_DIR || (process.env.XDG_CONFIG_HOME ? join16(process.env.XDG_CONFIG_HOME, "opencode") : join16(homedir(), ".config", "opencode"));
|
|
2526
2339
|
}
|
|
2527
2340
|
function loadFlowDeckConfig(directory) {
|
|
2528
2341
|
const candidates = [];
|
|
2529
2342
|
if (directory) {
|
|
2530
|
-
candidates.push(
|
|
2343
|
+
candidates.push(join16(directory, ".opencode", CONFIG_FILENAME));
|
|
2531
2344
|
}
|
|
2532
|
-
candidates.push(
|
|
2345
|
+
candidates.push(join16(getGlobalConfigDir(), CONFIG_FILENAME));
|
|
2533
2346
|
for (const configPath of candidates) {
|
|
2534
|
-
if (
|
|
2347
|
+
if (existsSync16(configPath)) {
|
|
2535
2348
|
try {
|
|
2536
2349
|
const content = readFileSync16(configPath, "utf-8");
|
|
2537
2350
|
return JSON.parse(content);
|
|
@@ -2560,7 +2373,7 @@ var PLANNING_DIR2 = ".planning";
|
|
|
2560
2373
|
var CONFIG_FILE = "config.json";
|
|
2561
2374
|
var STATE_FILE2 = "STATE.md";
|
|
2562
2375
|
function resolveExecutionMode(configPath, trustScore, volatility) {
|
|
2563
|
-
if (
|
|
2376
|
+
if (existsSync17(configPath)) {
|
|
2564
2377
|
try {
|
|
2565
2378
|
const config = JSON.parse(readFileSync17(configPath, "utf-8"));
|
|
2566
2379
|
if (config.execution_mode === "review-only")
|
|
@@ -2616,22 +2429,22 @@ async function guardRailsHook(ctx, input, _output) {
|
|
|
2616
2429
|
if (!ENABLED)
|
|
2617
2430
|
return;
|
|
2618
2431
|
const dir = ctx.directory;
|
|
2619
|
-
const planningDirPath =
|
|
2432
|
+
const planningDirPath = join17(dir, PLANNING_DIR2);
|
|
2620
2433
|
const codebaseDirectory = codebaseDir(dir);
|
|
2621
|
-
const configPath =
|
|
2622
|
-
const statePath3 =
|
|
2434
|
+
const configPath = join17(planningDirPath, CONFIG_FILE);
|
|
2435
|
+
const statePath3 = join17(planningDirPath, STATE_FILE2);
|
|
2623
2436
|
const workspaceRoot = findWorkspaceRoot(dir);
|
|
2624
2437
|
if (workspaceRoot && dir !== workspaceRoot) {
|
|
2625
2438
|
const config = getWorkspaceConfig(dir);
|
|
2626
|
-
if (config && config.workspace_mode === "shared" && !
|
|
2439
|
+
if (config && config.workspace_mode === "shared" && !existsSync17(planningDirPath)) {
|
|
2627
2440
|
const msg = `No .planning/ in this sub-repo. Switch to workspace root: cd ${workspaceRoot}`;
|
|
2628
2441
|
throw new Error(`[flowdeck] BLOCK: ${msg}`);
|
|
2629
2442
|
}
|
|
2630
2443
|
}
|
|
2631
2444
|
if (input.tool === "write" || input.tool === "edit") {
|
|
2632
|
-
if (!
|
|
2445
|
+
if (!existsSync17(planningDirPath))
|
|
2633
2446
|
return;
|
|
2634
|
-
if (!
|
|
2447
|
+
if (!existsSync17(codebaseDirectory)) {
|
|
2635
2448
|
throw new Error(`[flowdeck] WARNING: .codebase/ not found. Run /fd-map-codebase to map the codebase.`);
|
|
2636
2449
|
}
|
|
2637
2450
|
const execMode = resolveExecutionMode(configPath, null);
|
|
@@ -2687,13 +2500,13 @@ function getDesignGateMessage(dir) {
|
|
|
2687
2500
|
}
|
|
2688
2501
|
function planSuggestsUiHeavy(dir, phase) {
|
|
2689
2502
|
const planPath = phasePlanPath(dir, phase);
|
|
2690
|
-
if (!
|
|
2503
|
+
if (!existsSync17(planPath))
|
|
2691
2504
|
return false;
|
|
2692
2505
|
const planContent = readFileSync17(planPath, "utf-8");
|
|
2693
2506
|
return isUiHeavyTask(planContent);
|
|
2694
2507
|
}
|
|
2695
2508
|
function effectiveSeverity(configPath, statePath3) {
|
|
2696
|
-
if (
|
|
2509
|
+
if (existsSync17(configPath)) {
|
|
2697
2510
|
try {
|
|
2698
2511
|
const configContent = readFileSync17(configPath, "utf-8");
|
|
2699
2512
|
const config = JSON.parse(configContent);
|
|
@@ -2711,7 +2524,7 @@ function getEffectiveSeverity(configPath, statePath3) {
|
|
|
2711
2524
|
return effectiveSeverity(configPath, statePath3);
|
|
2712
2525
|
}
|
|
2713
2526
|
function getPlanConfirmed(statePath3) {
|
|
2714
|
-
if (!
|
|
2527
|
+
if (!existsSync17(statePath3))
|
|
2715
2528
|
return false;
|
|
2716
2529
|
try {
|
|
2717
2530
|
const content = readFileSync17(statePath3, "utf-8");
|
|
@@ -2722,32 +2535,32 @@ function getPlanConfirmed(statePath3) {
|
|
|
2722
2535
|
}
|
|
2723
2536
|
}
|
|
2724
2537
|
function getWarningMessage(planningDir2) {
|
|
2725
|
-
if (!
|
|
2538
|
+
if (!existsSync17(join17(planningDir2, STATE_FILE2))) {
|
|
2726
2539
|
return "No STATE.md found. Run /fd-map-codebase then /fd-new-feature to start a feature.";
|
|
2727
2540
|
}
|
|
2728
2541
|
return "Plan not confirmed. Run /fd-plan and confirm to enable execution.";
|
|
2729
2542
|
}
|
|
2730
2543
|
function getBlockMessage(planningDir2) {
|
|
2731
|
-
if (!
|
|
2544
|
+
if (!existsSync17(join17(planningDir2, STATE_FILE2))) {
|
|
2732
2545
|
return "No STATE.md found. Run /fd-map-codebase then /fd-new-feature to start a feature.";
|
|
2733
2546
|
}
|
|
2734
2547
|
return "Plan not confirmed. Run /fd-plan and confirm to enable execution.";
|
|
2735
2548
|
}
|
|
2736
2549
|
|
|
2737
2550
|
// src/hooks/tool-guard.ts
|
|
2738
|
-
import { existsSync as
|
|
2739
|
-
import { join as
|
|
2551
|
+
import { existsSync as existsSync18, readFileSync as readFileSync18 } from "fs";
|
|
2552
|
+
import { join as join18 } from "path";
|
|
2740
2553
|
var IS_ENABLED = () => process.env.FLOWDECK_TOOL_GUARD_ENABLED === "on";
|
|
2741
2554
|
var BLOCKED_PATTERNS = {
|
|
2742
2555
|
read: [".env", ".pem", ".key", ".secret"],
|
|
2743
2556
|
write: ["node_modules"],
|
|
2744
2557
|
bash: ["rm -rf"]
|
|
2745
2558
|
};
|
|
2746
|
-
function isBlocked(
|
|
2747
|
-
const patterns = BLOCKED_PATTERNS[
|
|
2559
|
+
function isBlocked(tool13, args) {
|
|
2560
|
+
const patterns = BLOCKED_PATTERNS[tool13];
|
|
2748
2561
|
if (!patterns)
|
|
2749
2562
|
return null;
|
|
2750
|
-
if (
|
|
2563
|
+
if (tool13 === "bash") {
|
|
2751
2564
|
const cmd = args.command;
|
|
2752
2565
|
if (!cmd)
|
|
2753
2566
|
return null;
|
|
@@ -2758,7 +2571,7 @@ function isBlocked(tool14, args) {
|
|
|
2758
2571
|
}
|
|
2759
2572
|
return null;
|
|
2760
2573
|
}
|
|
2761
|
-
if (
|
|
2574
|
+
if (tool13 === "read") {
|
|
2762
2575
|
const filePath = args.filePath;
|
|
2763
2576
|
if (!filePath)
|
|
2764
2577
|
return null;
|
|
@@ -2769,7 +2582,7 @@ function isBlocked(tool14, args) {
|
|
|
2769
2582
|
}
|
|
2770
2583
|
return null;
|
|
2771
2584
|
}
|
|
2772
|
-
if (
|
|
2585
|
+
if (tool13 === "write") {
|
|
2773
2586
|
const filePath = args.filePath;
|
|
2774
2587
|
if (!filePath)
|
|
2775
2588
|
return null;
|
|
@@ -2783,8 +2596,8 @@ function isBlocked(tool14, args) {
|
|
|
2783
2596
|
return null;
|
|
2784
2597
|
}
|
|
2785
2598
|
function checkArchConstraint(directory, filePath) {
|
|
2786
|
-
const constraintsPath =
|
|
2787
|
-
if (!
|
|
2599
|
+
const constraintsPath = join18(codebaseDir(directory), "CONSTRAINTS.md");
|
|
2600
|
+
if (!existsSync18(constraintsPath))
|
|
2788
2601
|
return null;
|
|
2789
2602
|
try {
|
|
2790
2603
|
const content = readFileSync18(constraintsPath, "utf-8");
|
|
@@ -2828,7 +2641,7 @@ function isUiDesignApprovalRequired(directory) {
|
|
|
2828
2641
|
return !(state.design_stage === "handoff_complete" && state.design_approved);
|
|
2829
2642
|
}
|
|
2830
2643
|
const planPath = phasePlanPath(directory, state.phase || 1);
|
|
2831
|
-
if (!
|
|
2644
|
+
if (!existsSync18(planPath))
|
|
2832
2645
|
return false;
|
|
2833
2646
|
const planContent = readFileSync18(planPath, "utf-8");
|
|
2834
2647
|
if (!isUiHeavyTask(planContent))
|
|
@@ -2859,18 +2672,18 @@ async function toolGuardHook(ctx, input, output) {
|
|
|
2859
2672
|
}
|
|
2860
2673
|
|
|
2861
2674
|
// src/hooks/session-start.ts
|
|
2862
|
-
import { existsSync as
|
|
2675
|
+
import { existsSync as existsSync19, readFileSync as readFileSync19 } from "fs";
|
|
2863
2676
|
async function sessionStartHook(ctx) {
|
|
2864
2677
|
const planningDir2 = ctx.directory + "/.planning";
|
|
2865
2678
|
const codebaseDirectory = codebaseDir(ctx.directory);
|
|
2866
2679
|
const workspaceRoot = findWorkspaceRoot(ctx.directory);
|
|
2867
2680
|
const config = workspaceRoot ? getWorkspaceConfig(ctx.directory) : null;
|
|
2868
|
-
if (!
|
|
2681
|
+
if (!existsSync19(planningDir2)) {
|
|
2869
2682
|
return {
|
|
2870
2683
|
flowdeck_phase: null,
|
|
2871
2684
|
flowdeck_status: "no_plan",
|
|
2872
2685
|
flowdeck_warning: "Run /fd-map-codebase to index the codebase, then /fd-new-feature to start a feature.",
|
|
2873
|
-
flowdeck_has_codebase:
|
|
2686
|
+
flowdeck_has_codebase: existsSync19(codebaseDirectory),
|
|
2874
2687
|
...workspaceRoot && config?.sub_repos ? {
|
|
2875
2688
|
flowdeck_workspace_root: workspaceRoot,
|
|
2876
2689
|
flowdeck_sub_repos: config.sub_repos,
|
|
@@ -2889,7 +2702,7 @@ async function sessionStartHook(ctx) {
|
|
|
2889
2702
|
flowdeck_status: currentPhase["status"] ?? null,
|
|
2890
2703
|
flowdeck_steps_pending: currentPhase["steps_pending"] ?? null,
|
|
2891
2704
|
flowdeck_last_action: currentPhase["last_action"] ?? null,
|
|
2892
|
-
flowdeck_has_codebase:
|
|
2705
|
+
flowdeck_has_codebase: existsSync19(codebaseDirectory)
|
|
2893
2706
|
};
|
|
2894
2707
|
if (workspaceRoot && config?.sub_repos && config.sub_repos.length > 0) {
|
|
2895
2708
|
result.flowdeck_workspace_root = workspaceRoot;
|
|
@@ -2903,7 +2716,7 @@ async function sessionStartHook(ctx) {
|
|
|
2903
2716
|
flowdeck_phase: null,
|
|
2904
2717
|
flowdeck_status: "error",
|
|
2905
2718
|
flowdeck_warning: "State file unreadable. Continuing without flowdeck context.",
|
|
2906
|
-
flowdeck_has_codebase:
|
|
2719
|
+
flowdeck_has_codebase: existsSync19(codebaseDirectory)
|
|
2907
2720
|
};
|
|
2908
2721
|
if (workspaceRoot && config?.sub_repos && config.sub_repos.length > 0) {
|
|
2909
2722
|
result.flowdeck_workspace_root = workspaceRoot;
|
|
@@ -3035,13 +2848,13 @@ class NotificationController {
|
|
|
3035
2848
|
return this.lastNotifiedKey;
|
|
3036
2849
|
}
|
|
3037
2850
|
}
|
|
3038
|
-
function notifyPermissionNeeded(
|
|
3039
|
-
notify("FlowDeck Permission Required", `Agent needs approval to use tool: ${
|
|
2851
|
+
function notifyPermissionNeeded(tool13) {
|
|
2852
|
+
notify("FlowDeck Permission Required", `Agent needs approval to use tool: ${tool13}`, "critical");
|
|
3040
2853
|
}
|
|
3041
2854
|
|
|
3042
2855
|
// src/hooks/patch-trust.ts
|
|
3043
|
-
import { existsSync as
|
|
3044
|
-
import { join as
|
|
2856
|
+
import { existsSync as existsSync20, readFileSync as readFileSync20 } from "fs";
|
|
2857
|
+
import { join as join19 } from "path";
|
|
3045
2858
|
var HIGH_RISK_KEYWORDS = [
|
|
3046
2859
|
"password",
|
|
3047
2860
|
"secret",
|
|
@@ -3063,8 +2876,8 @@ var HIGH_RISK_KEYWORDS = [
|
|
|
3063
2876
|
"privilege"
|
|
3064
2877
|
];
|
|
3065
2878
|
function loadFailedPaths(directory) {
|
|
3066
|
-
const p =
|
|
3067
|
-
if (!
|
|
2879
|
+
const p = join19(codebaseDir(directory), "FAILURES.json");
|
|
2880
|
+
if (!existsSync20(p))
|
|
3068
2881
|
return [];
|
|
3069
2882
|
try {
|
|
3070
2883
|
const data = JSON.parse(readFileSync20(p, "utf-8"));
|
|
@@ -3120,8 +2933,8 @@ async function patchTrustHook(ctx, input, output) {
|
|
|
3120
2933
|
}
|
|
3121
2934
|
|
|
3122
2935
|
// src/hooks/decision-trace-hook.ts
|
|
3123
|
-
import { existsSync as
|
|
3124
|
-
import { join as
|
|
2936
|
+
import { existsSync as existsSync21, mkdirSync as mkdirSync11, appendFileSync as appendFileSync4 } from "fs";
|
|
2937
|
+
import { join as join20 } from "path";
|
|
3125
2938
|
async function decisionTraceHook(ctx, input, output) {
|
|
3126
2939
|
if (input.tool !== "write" && input.tool !== "edit")
|
|
3127
2940
|
return;
|
|
@@ -3130,7 +2943,7 @@ async function decisionTraceHook(ctx, input, output) {
|
|
|
3130
2943
|
return;
|
|
3131
2944
|
const base = codebaseDir(ctx.directory);
|
|
3132
2945
|
try {
|
|
3133
|
-
if (!
|
|
2946
|
+
if (!existsSync21(base))
|
|
3134
2947
|
mkdirSync11(base, { recursive: true });
|
|
3135
2948
|
const entry = {
|
|
3136
2949
|
timestamp: new Date().toISOString(),
|
|
@@ -3143,14 +2956,14 @@ async function decisionTraceHook(ctx, input, output) {
|
|
|
3143
2956
|
risk_level: "unknown",
|
|
3144
2957
|
auto_recorded: true
|
|
3145
2958
|
};
|
|
3146
|
-
appendFileSync4(
|
|
2959
|
+
appendFileSync4(join20(base, "DECISIONS.jsonl"), JSON.stringify(entry) + `
|
|
3147
2960
|
`, "utf-8");
|
|
3148
2961
|
} catch {}
|
|
3149
2962
|
}
|
|
3150
2963
|
|
|
3151
2964
|
// src/services/approval-manager.ts
|
|
3152
|
-
import { existsSync as
|
|
3153
|
-
import { join as
|
|
2965
|
+
import { existsSync as existsSync22, readFileSync as readFileSync21, writeFileSync as writeFileSync12, mkdirSync as mkdirSync12 } from "fs";
|
|
2966
|
+
import { join as join21 } from "path";
|
|
3154
2967
|
var APPROVAL_TTL_MS = 30 * 60 * 1000;
|
|
3155
2968
|
var SENSITIVE_PATTERNS = [
|
|
3156
2969
|
/auth/i,
|
|
@@ -3187,11 +3000,11 @@ function isSensitivePath(filePath) {
|
|
|
3187
3000
|
return SENSITIVE_PATTERNS.some((p) => p.test(filePath));
|
|
3188
3001
|
}
|
|
3189
3002
|
function approvalsPath(dir) {
|
|
3190
|
-
return
|
|
3003
|
+
return join21(codebaseDir(dir), "APPROVALS.json");
|
|
3191
3004
|
}
|
|
3192
3005
|
function loadStore(dir) {
|
|
3193
3006
|
const p = approvalsPath(dir);
|
|
3194
|
-
if (!
|
|
3007
|
+
if (!existsSync22(p))
|
|
3195
3008
|
return { requests: [] };
|
|
3196
3009
|
try {
|
|
3197
3010
|
return JSON.parse(readFileSync21(p, "utf-8"));
|
|
@@ -3212,8 +3025,8 @@ async function approvalHook(context, toolInput, output) {
|
|
|
3212
3025
|
if (!ENABLED2)
|
|
3213
3026
|
return;
|
|
3214
3027
|
const dir = context.directory ?? process.cwd();
|
|
3215
|
-
const
|
|
3216
|
-
if (!WRITE_TOOLS.has(
|
|
3028
|
+
const tool13 = toolInput.name ?? toolInput.tool ?? "";
|
|
3029
|
+
if (!WRITE_TOOLS.has(tool13))
|
|
3217
3030
|
return;
|
|
3218
3031
|
const args = output.args ?? {};
|
|
3219
3032
|
const filePath = String(args.path ?? args.file_path ?? args.filename ?? "");
|
|
@@ -3230,8 +3043,8 @@ async function approvalHook(context, toolInput, output) {
|
|
|
3230
3043
|
}
|
|
3231
3044
|
|
|
3232
3045
|
// src/services/event-logger.ts
|
|
3233
|
-
import { existsSync as
|
|
3234
|
-
import { join as
|
|
3046
|
+
import { existsSync as existsSync23, mkdirSync as mkdirSync13, appendFileSync as appendFileSync5, readFileSync as readFileSync22, writeFileSync as writeFileSync13, renameSync, unlinkSync, statSync } from "fs";
|
|
3047
|
+
import { join as join22, resolve as resolve2, sep } from "path";
|
|
3235
3048
|
var SENSITIVE_KEYS = [
|
|
3236
3049
|
"password",
|
|
3237
3050
|
"token",
|
|
@@ -3301,10 +3114,10 @@ function logEvent(directory, event, log) {
|
|
|
3301
3114
|
lastPersistenceError = "Invalid directory";
|
|
3302
3115
|
return false;
|
|
3303
3116
|
}
|
|
3304
|
-
const logDir =
|
|
3305
|
-
const logPath =
|
|
3117
|
+
const logDir = join22(directory, ".opencode");
|
|
3118
|
+
const logPath = join22(logDir, "flowdeck-events.jsonl");
|
|
3306
3119
|
try {
|
|
3307
|
-
if (!
|
|
3120
|
+
if (!existsSync23(logDir)) {
|
|
3308
3121
|
mkdirSync13(logDir, { recursive: true });
|
|
3309
3122
|
}
|
|
3310
3123
|
appendFileSync5(logPath, JSON.stringify(event) + `
|
|
@@ -3614,45 +3427,45 @@ function stableStringify(obj) {
|
|
|
3614
3427
|
return `{${pairs.join(",")}}`;
|
|
3615
3428
|
}
|
|
3616
3429
|
function resolveEnvVars(command) {
|
|
3617
|
-
return command.replace(/\$
|
|
3430
|
+
return command.replace(/\$HOME\b/gi, "~").replace(/\$USER\b/gi, "user");
|
|
3618
3431
|
}
|
|
3619
3432
|
function collapseWhitespace(input) {
|
|
3620
3433
|
return input.replace(/\s+/g, " ").trim();
|
|
3621
3434
|
}
|
|
3622
3435
|
function normalizeAction(toolName, args) {
|
|
3623
|
-
const
|
|
3624
|
-
if (
|
|
3436
|
+
const tool13 = toolName.toLowerCase();
|
|
3437
|
+
if (tool13 === "bash" || tool13 === "shell") {
|
|
3625
3438
|
const command = typeof args.command === "string" ? args.command : "";
|
|
3626
3439
|
const normalized = collapseWhitespace(resolveEnvVars(command)).toLowerCase();
|
|
3627
3440
|
return `shell:${normalized}`;
|
|
3628
3441
|
}
|
|
3629
|
-
if (
|
|
3442
|
+
if (tool13 === "read" || tool13 === "view") {
|
|
3630
3443
|
const filePath = typeof args.filePath === "string" ? args.filePath : "";
|
|
3631
3444
|
try {
|
|
3632
|
-
return `${
|
|
3445
|
+
return `${tool13}:${resolve3(filePath || "")}`;
|
|
3633
3446
|
} catch {
|
|
3634
|
-
return `${
|
|
3447
|
+
return `${tool13}:${filePath}`;
|
|
3635
3448
|
}
|
|
3636
3449
|
}
|
|
3637
|
-
if (
|
|
3450
|
+
if (tool13 === "write" || tool13 === "edit") {
|
|
3638
3451
|
const filePath = typeof args.filePath === "string" ? args.filePath : "";
|
|
3639
3452
|
try {
|
|
3640
|
-
return `${
|
|
3453
|
+
return `${tool13}:${resolve3(filePath || "")}`;
|
|
3641
3454
|
} catch {
|
|
3642
|
-
return `${
|
|
3455
|
+
return `${tool13}:${filePath}`;
|
|
3643
3456
|
}
|
|
3644
3457
|
}
|
|
3645
|
-
if (
|
|
3458
|
+
if (tool13 === "grep" || tool13 === "glob" || tool13 === "search") {
|
|
3646
3459
|
const pattern = typeof args.pattern === "string" ? args.pattern : "";
|
|
3647
3460
|
const path = typeof args.path === "string" ? args.path : "";
|
|
3648
|
-
return `${
|
|
3461
|
+
return `${tool13}:${pattern}:${resolve3(path || ".")}`;
|
|
3649
3462
|
}
|
|
3650
3463
|
const sorted = stableStringify(args);
|
|
3651
|
-
return `${
|
|
3464
|
+
return `${tool13}:${sorted}`;
|
|
3652
3465
|
}
|
|
3653
3466
|
function classifyObservation(toolName, previous, output, status, similarityThreshold) {
|
|
3654
3467
|
const outputPreview = getOutputPreview(output);
|
|
3655
|
-
const
|
|
3468
|
+
const tool13 = toolName.toLowerCase();
|
|
3656
3469
|
if (status === "blocked") {
|
|
3657
3470
|
return { observation: "same_result", outputHash: hashOutput(output), outputPreview };
|
|
3658
3471
|
}
|
|
@@ -3666,7 +3479,7 @@ function classifyObservation(toolName, previous, output, status, similarityThres
|
|
|
3666
3479
|
outputPreview: errorMessage.slice(0, 200)
|
|
3667
3480
|
};
|
|
3668
3481
|
}
|
|
3669
|
-
if (
|
|
3482
|
+
if (tool13 === "write" || tool13 === "edit") {
|
|
3670
3483
|
const contentHash = hashOutput(output);
|
|
3671
3484
|
return { observation: "new_information", outputHash: contentHash, outputPreview };
|
|
3672
3485
|
}
|
|
@@ -3677,7 +3490,7 @@ function classifyObservation(toolName, previous, output, status, similarityThres
|
|
|
3677
3490
|
if (outputHash === previous.outputHash) {
|
|
3678
3491
|
return { observation: "same_result", outputHash, outputPreview };
|
|
3679
3492
|
}
|
|
3680
|
-
if (NON_MUTATING_TOOLS.has(
|
|
3493
|
+
if (NON_MUTATING_TOOLS.has(tool13)) {
|
|
3681
3494
|
const similarity = lineSimilarity(outputPreview, previous.outputPreview);
|
|
3682
3495
|
if (similarity >= similarityThreshold) {
|
|
3683
3496
|
return { observation: "no_progress", outputHash, outputPreview };
|
|
@@ -3686,25 +3499,25 @@ function classifyObservation(toolName, previous, output, status, similarityThres
|
|
|
3686
3499
|
return { observation: "new_information", outputHash, outputPreview };
|
|
3687
3500
|
}
|
|
3688
3501
|
function redactForDisplay(toolName, normalizedKey) {
|
|
3689
|
-
const
|
|
3690
|
-
if (
|
|
3502
|
+
const tool13 = toolName.toLowerCase();
|
|
3503
|
+
if (tool13 === "bash" || tool13 === "shell") {
|
|
3691
3504
|
const idx2 = normalizedKey.indexOf(":");
|
|
3692
3505
|
const cmd = idx2 >= 0 ? normalizedKey.slice(idx2 + 1) : normalizedKey;
|
|
3693
3506
|
const preview = cmd.slice(0, 30);
|
|
3694
3507
|
const hash = djb2Hash(cmd);
|
|
3695
|
-
return `${
|
|
3508
|
+
return `${tool13}:"${preview}" (hash: ${hash})`;
|
|
3696
3509
|
}
|
|
3697
3510
|
const idx = normalizedKey.indexOf(":");
|
|
3698
3511
|
if (idx >= 0) {
|
|
3699
3512
|
const body = normalizedKey.slice(idx + 1);
|
|
3700
3513
|
if (body.startsWith("/") || body.startsWith(".") || body.includes("/")) {
|
|
3701
|
-
return `${
|
|
3514
|
+
return `${tool13}:"${body}"`;
|
|
3702
3515
|
}
|
|
3703
3516
|
const preview = body.slice(0, 30);
|
|
3704
3517
|
const hash = djb2Hash(body);
|
|
3705
|
-
return `${
|
|
3518
|
+
return `${tool13}:"${preview}" (hash: ${hash})`;
|
|
3706
3519
|
}
|
|
3707
|
-
return `${
|
|
3520
|
+
return `${tool13}:"${normalizedKey}"`;
|
|
3708
3521
|
}
|
|
3709
3522
|
|
|
3710
3523
|
class LoopDetector {
|
|
@@ -3904,8 +3717,8 @@ function createContextWindowMonitorHook() {
|
|
|
3904
3717
|
}
|
|
3905
3718
|
|
|
3906
3719
|
// src/hooks/shell-env-hook.ts
|
|
3907
|
-
import { existsSync as
|
|
3908
|
-
import { join as
|
|
3720
|
+
import { existsSync as existsSync24, readFileSync as readFileSync23 } from "fs";
|
|
3721
|
+
import { join as join23 } from "path";
|
|
3909
3722
|
import { createRequire } from "module";
|
|
3910
3723
|
var _version;
|
|
3911
3724
|
function getVersion() {
|
|
@@ -3941,7 +3754,7 @@ var MARKER_TO_LANG = {
|
|
|
3941
3754
|
};
|
|
3942
3755
|
function detectPackageManager(root) {
|
|
3943
3756
|
for (const [lockfile, pm] of Object.entries(LOCKFILE_TO_PM)) {
|
|
3944
|
-
if (
|
|
3757
|
+
if (existsSync24(join23(root, lockfile)))
|
|
3945
3758
|
return pm;
|
|
3946
3759
|
}
|
|
3947
3760
|
return;
|
|
@@ -3950,7 +3763,7 @@ function detectLanguages(root) {
|
|
|
3950
3763
|
const langs = [];
|
|
3951
3764
|
const seen = new Set;
|
|
3952
3765
|
for (const [marker, lang] of Object.entries(MARKER_TO_LANG)) {
|
|
3953
|
-
if (!seen.has(lang) &&
|
|
3766
|
+
if (!seen.has(lang) && existsSync24(join23(root, marker))) {
|
|
3954
3767
|
langs.push(lang);
|
|
3955
3768
|
seen.add(lang);
|
|
3956
3769
|
}
|
|
@@ -3958,8 +3771,8 @@ function detectLanguages(root) {
|
|
|
3958
3771
|
return langs;
|
|
3959
3772
|
}
|
|
3960
3773
|
function readCurrentPhase(root) {
|
|
3961
|
-
const statePath3 =
|
|
3962
|
-
if (!
|
|
3774
|
+
const statePath3 = join23(root, ".planning", "STATE.md");
|
|
3775
|
+
if (!existsSync24(statePath3))
|
|
3963
3776
|
return;
|
|
3964
3777
|
try {
|
|
3965
3778
|
const content = readFileSync23(statePath3, "utf-8");
|
|
@@ -3969,18 +3782,6 @@ function readCurrentPhase(root) {
|
|
|
3969
3782
|
return;
|
|
3970
3783
|
}
|
|
3971
3784
|
}
|
|
3972
|
-
var _rtkDetection;
|
|
3973
|
-
function getRtkDetection() {
|
|
3974
|
-
if (_rtkDetection !== undefined)
|
|
3975
|
-
return _rtkDetection;
|
|
3976
|
-
try {
|
|
3977
|
-
const det = detectRtk();
|
|
3978
|
-
_rtkDetection = { installed: det.installed, binPath: det.binPath };
|
|
3979
|
-
} catch {
|
|
3980
|
-
_rtkDetection = { installed: false };
|
|
3981
|
-
}
|
|
3982
|
-
return _rtkDetection;
|
|
3983
|
-
}
|
|
3984
3785
|
function createShellEnvHook(ctx) {
|
|
3985
3786
|
const root = ctx.worktree || ctx.directory;
|
|
3986
3787
|
return async (_input, output) => {
|
|
@@ -3998,14 +3799,6 @@ function createShellEnvHook(ctx) {
|
|
|
3998
3799
|
const phase = readCurrentPhase(root);
|
|
3999
3800
|
if (phase)
|
|
4000
3801
|
output.env.FLOWDECK_PHASE = phase;
|
|
4001
|
-
const rtk = getRtkDetection();
|
|
4002
|
-
output.env.RTK_INSTALLED = rtk.installed ? "true" : "false";
|
|
4003
|
-
if (rtk.installed && rtk.binPath) {
|
|
4004
|
-
output.env.RTK_BIN = rtk.binPath;
|
|
4005
|
-
}
|
|
4006
|
-
if (rtk.installed) {
|
|
4007
|
-
output.env.RTK_TELEMETRY_DISABLED = "1";
|
|
4008
|
-
}
|
|
4009
3802
|
};
|
|
4010
3803
|
}
|
|
4011
3804
|
|
|
@@ -4087,8 +3880,8 @@ function createSessionIdleHook(client, tracker) {
|
|
|
4087
3880
|
}
|
|
4088
3881
|
|
|
4089
3882
|
// src/hooks/compaction-hook.ts
|
|
4090
|
-
import { existsSync as
|
|
4091
|
-
import { join as
|
|
3883
|
+
import { existsSync as existsSync25, readFileSync as readFileSync24 } from "fs";
|
|
3884
|
+
import { join as join24 } from "path";
|
|
4092
3885
|
var STRUCTURED_SUMMARY_PROMPT = `
|
|
4093
3886
|
When summarizing this session, you MUST include the following sections:
|
|
4094
3887
|
|
|
@@ -4129,7 +3922,7 @@ For each: agent name, status, description, session_id.
|
|
|
4129
3922
|
var _lastInjected = new Map;
|
|
4130
3923
|
function readPlanningState2(directory) {
|
|
4131
3924
|
const sp = statePath(directory);
|
|
4132
|
-
if (!
|
|
3925
|
+
if (!existsSync25(sp))
|
|
4133
3926
|
return null;
|
|
4134
3927
|
try {
|
|
4135
3928
|
const content = readFileSync24(sp, "utf-8");
|
|
@@ -4161,15 +3954,15 @@ function createCompactionHook(ctx, tracker) {
|
|
|
4161
3954
|
sections.push(`_State unchanged since last compaction. summaryVersion=${currentStateVersion}_`);
|
|
4162
3955
|
sections.push("");
|
|
4163
3956
|
}
|
|
4164
|
-
const indexPath2 =
|
|
4165
|
-
if (indexChanged &&
|
|
3957
|
+
const indexPath2 = join24(ctx.directory, ".planning", "CODEBASE_INDEX.md");
|
|
3958
|
+
if (indexChanged && existsSync25(indexPath2)) {
|
|
4166
3959
|
try {
|
|
4167
3960
|
const indexContent = readFileSync24(indexPath2, "utf-8");
|
|
4168
3961
|
const indexSummary = "\n## Codebase Index\n```\n" + indexContent.slice(0, 800) + "\n```\n";
|
|
4169
3962
|
sections.push(indexSummary);
|
|
4170
3963
|
sections.push("");
|
|
4171
3964
|
} catch {}
|
|
4172
|
-
} else if (
|
|
3965
|
+
} else if (existsSync25(indexPath2)) {
|
|
4173
3966
|
sections.push(`## Codebase Index (unchanged, v${currentIndexVersion})`);
|
|
4174
3967
|
sections.push(`_Index unchanged since last compaction. summaryVersion=${currentIndexVersion}_`);
|
|
4175
3968
|
sections.push("");
|
|
@@ -4250,7 +4043,6 @@ var ALWAYS_ALLOWED = new Set([
|
|
|
4250
4043
|
"load-rules",
|
|
4251
4044
|
"list-rules",
|
|
4252
4045
|
"council",
|
|
4253
|
-
"rtk-setup",
|
|
4254
4046
|
"hash-edit",
|
|
4255
4047
|
"failure-replay"
|
|
4256
4048
|
]);
|
|
@@ -4288,7 +4080,7 @@ function blockMessage(toolName) {
|
|
|
4288
4080
|
` + ` @tester — tests, builds, and shell-heavy verification
|
|
4289
4081
|
` + ` @writer — documentation writing
|
|
4290
4082
|
|
|
4291
|
-
` + `Allowed tools for orchestrator: read, search, planning-state, codebase-state, repo-memory, decision-trace, policy-engine, reflect, codegraph, load-rules, council,
|
|
4083
|
+
` + `Allowed tools for orchestrator: read, search, planning-state, codebase-state, repo-memory, decision-trace, policy-engine, reflect, codegraph, load-rules, council, hash-edit, failure-replay.
|
|
4292
4084
|
|
|
4293
4085
|
` + `To disable this guard: set FLOWDECK_ORCHESTRATOR_GUARD=off`;
|
|
4294
4086
|
}
|
|
@@ -4403,14 +4195,14 @@ async function runAutoLearner(client, directory, appLog) {
|
|
|
4403
4195
|
}
|
|
4404
4196
|
|
|
4405
4197
|
// src/mcp/index.ts
|
|
4406
|
-
import { spawnSync as
|
|
4198
|
+
import { spawnSync as spawnSync3 } from "child_process";
|
|
4407
4199
|
function getDisabledMcps() {
|
|
4408
4200
|
const raw = process.env.FLOWDECK_DISABLE_MCP ?? "";
|
|
4409
4201
|
return new Set(raw.split(",").map((s) => s.trim()).filter(Boolean));
|
|
4410
4202
|
}
|
|
4411
4203
|
function isLauncherAvailable(launcher) {
|
|
4412
4204
|
try {
|
|
4413
|
-
const result =
|
|
4205
|
+
const result = spawnSync3(launcher, ["--version"], {
|
|
4414
4206
|
encoding: "utf-8",
|
|
4415
4207
|
timeout: 5000,
|
|
4416
4208
|
stdio: "pipe"
|
|
@@ -7759,8 +7551,8 @@ function getAgentConfigs(agentModels) {
|
|
|
7759
7551
|
// src/index.ts
|
|
7760
7552
|
function lazyLoadRulePaths(projectRoot) {
|
|
7761
7553
|
const __dir = dirname3(fileURLToPath2(import.meta.url));
|
|
7762
|
-
const rulesDir =
|
|
7763
|
-
if (!
|
|
7554
|
+
const rulesDir = join25(__dir, "..", "src", "rules");
|
|
7555
|
+
if (!existsSync26(rulesDir))
|
|
7764
7556
|
return { paths: [], diagnostics: "[LazyRuleLoader] rules directory not found" };
|
|
7765
7557
|
const detectedLanguages = detectProjectLanguages(projectRoot);
|
|
7766
7558
|
const paths = getStartupRulePaths(rulesDir, detectedLanguages);
|
|
@@ -7770,8 +7562,8 @@ function lazyLoadRulePaths(projectRoot) {
|
|
|
7770
7562
|
}
|
|
7771
7563
|
function loadCommands() {
|
|
7772
7564
|
const __dir = dirname3(fileURLToPath2(import.meta.url));
|
|
7773
|
-
const commandsDir =
|
|
7774
|
-
if (!
|
|
7565
|
+
const commandsDir = join25(__dir, "..", "src", "commands");
|
|
7566
|
+
if (!existsSync26(commandsDir))
|
|
7775
7567
|
return {};
|
|
7776
7568
|
const commands = {};
|
|
7777
7569
|
try {
|
|
@@ -7779,7 +7571,7 @@ function loadCommands() {
|
|
|
7779
7571
|
if (!file.endsWith(".md"))
|
|
7780
7572
|
continue;
|
|
7781
7573
|
const name = basename2(file, ".md");
|
|
7782
|
-
const raw = readFileSync25(
|
|
7574
|
+
const raw = readFileSync25(join25(commandsDir, file), "utf-8");
|
|
7783
7575
|
let description;
|
|
7784
7576
|
let template = raw;
|
|
7785
7577
|
const fmMatch = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/);
|
|
@@ -7872,8 +7664,8 @@ var plugin = async (input, _options) => {
|
|
|
7872
7664
|
}
|
|
7873
7665
|
}
|
|
7874
7666
|
}
|
|
7875
|
-
const skillsDir =
|
|
7876
|
-
if (
|
|
7667
|
+
const skillsDir = join25(dirname3(fileURLToPath2(import.meta.url)), "..", "src", "skills");
|
|
7668
|
+
if (existsSync26(skillsDir)) {
|
|
7877
7669
|
const cfgAny = cfg;
|
|
7878
7670
|
if (!cfgAny.skills || typeof cfgAny.skills !== "object") {
|
|
7879
7671
|
cfgAny.skills = { paths: [] };
|
|
@@ -7912,7 +7704,6 @@ var plugin = async (input, _options) => {
|
|
|
7912
7704
|
codegraph: codegraphTool,
|
|
7913
7705
|
"load-rules": loadRulesTool,
|
|
7914
7706
|
"list-rules": listRulesTool,
|
|
7915
|
-
"rtk-setup": rtkSetupTool,
|
|
7916
7707
|
"merge-assist": mergeAssistTool
|
|
7917
7708
|
},
|
|
7918
7709
|
"shell.env": shellEnvHook,
|