@dv.nghiem/flowdeck 0.4.1 → 0.4.3
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 +3 -0
- package/dist/agents/index.d.ts +13 -0
- package/dist/agents/index.d.ts.map +1 -1
- package/dist/config/schema.d.ts +19 -0
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/hooks/compaction-hook.d.ts +6 -1
- package/dist/hooks/compaction-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 +1246 -321
- package/dist/mcp/index.d.ts +2 -3
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/services/artifact-store.d.ts +39 -0
- package/dist/services/artifact-store.d.ts.map +1 -0
- package/dist/services/artifact-store.test.d.ts +2 -0
- package/dist/services/artifact-store.test.d.ts.map +1 -0
- package/dist/services/context-assembler.d.ts +29 -0
- package/dist/services/context-assembler.d.ts.map +1 -0
- package/dist/services/context-assembler.test.d.ts +2 -0
- package/dist/services/context-assembler.test.d.ts.map +1 -0
- package/dist/services/cost-budget.d.ts +53 -0
- package/dist/services/cost-budget.d.ts.map +1 -0
- package/dist/services/cost-budget.test.d.ts +2 -0
- package/dist/services/cost-budget.test.d.ts.map +1 -0
- package/dist/services/cost-estimator.d.ts +103 -0
- package/dist/services/cost-estimator.d.ts.map +1 -0
- package/dist/services/cost-estimator.test.d.ts +2 -0
- package/dist/services/cost-estimator.test.d.ts.map +1 -0
- package/dist/services/draft-verifier.d.ts +48 -0
- package/dist/services/draft-verifier.d.ts.map +1 -0
- package/dist/services/draft-verifier.test.d.ts +2 -0
- package/dist/services/draft-verifier.test.d.ts.map +1 -0
- package/dist/services/index.d.ts +13 -0
- package/dist/services/index.d.ts.map +1 -1
- package/dist/services/lazy-rule-loader.d.ts +104 -0
- package/dist/services/lazy-rule-loader.d.ts.map +1 -0
- package/dist/services/lazy-rule-loader.test.d.ts +23 -0
- package/dist/services/lazy-rule-loader.test.d.ts.map +1 -0
- package/dist/services/model-router-ext.test.d.ts +2 -0
- package/dist/services/model-router-ext.test.d.ts.map +1 -0
- package/dist/services/model-router.d.ts +52 -0
- package/dist/services/model-router.d.ts.map +1 -0
- package/dist/services/model-router.test.d.ts +2 -0
- package/dist/services/model-router.test.d.ts.map +1 -0
- package/dist/services/prompt-cache-ext.test.d.ts +2 -0
- package/dist/services/prompt-cache-ext.test.d.ts.map +1 -0
- package/dist/services/prompt-cache.d.ts +61 -0
- package/dist/services/prompt-cache.d.ts.map +1 -0
- package/dist/services/prompt-cache.test.d.ts +2 -0
- package/dist/services/prompt-cache.test.d.ts.map +1 -0
- package/dist/services/rtk-manager.d.ts +80 -0
- package/dist/services/rtk-manager.d.ts.map +1 -0
- package/dist/services/rtk-manager.test.d.ts +2 -0
- package/dist/services/rtk-manager.test.d.ts.map +1 -0
- package/dist/services/rtk-policy.d.ts +26 -0
- package/dist/services/rtk-policy.d.ts.map +1 -0
- package/dist/services/rtk-policy.test.d.ts +2 -0
- package/dist/services/rtk-policy.test.d.ts.map +1 -0
- package/dist/services/rule-engine.d.ts +29 -0
- package/dist/services/rule-engine.d.ts.map +1 -0
- package/dist/services/rule-engine.test.d.ts +2 -0
- package/dist/services/rule-engine.test.d.ts.map +1 -0
- package/dist/services/task-batcher.d.ts +48 -0
- package/dist/services/task-batcher.d.ts.map +1 -0
- package/dist/services/task-batcher.test.d.ts +2 -0
- package/dist/services/task-batcher.test.d.ts.map +1 -0
- package/dist/services/telemetry.d.ts +6 -0
- package/dist/services/telemetry.d.ts.map +1 -1
- package/dist/services/token-budget.d.ts +44 -0
- package/dist/services/token-budget.d.ts.map +1 -0
- package/dist/services/token-budget.test.d.ts +2 -0
- package/dist/services/token-budget.test.d.ts.map +1 -0
- package/dist/services/token-metrics-ext.test.d.ts +2 -0
- package/dist/services/token-metrics-ext.test.d.ts.map +1 -0
- package/dist/services/token-metrics.d.ts +97 -0
- package/dist/services/token-metrics.d.ts.map +1 -0
- package/dist/services/token-metrics.test.d.ts +2 -0
- package/dist/services/token-metrics.test.d.ts.map +1 -0
- package/dist/tools/codegraph-tool.d.ts.map +1 -1
- package/dist/tools/council.d.ts.map +1 -1
- package/dist/tools/delegate.d.ts.map +1 -1
- package/dist/tools/load-rules.d.ts +25 -0
- package/dist/tools/load-rules.d.ts.map +1 -0
- package/dist/tools/rtk-setup.d.ts +22 -0
- package/dist/tools/rtk-setup.d.ts.map +1 -0
- package/dist/tools/run-pipeline.d.ts.map +1 -1
- package/docs/commands/fd-map-codebase.md +2 -1
- package/docs/configuration/index.md +26 -0
- package/docs/getting-started/installation.md +20 -0
- package/docs/reference/hooks.md +16 -1
- package/docs/reference/rtk.md +162 -0
- package/package.json +1 -1
- package/src/rules/common/agent-orchestration.md +7 -0
- package/src/rules/common/behavioral.md +7 -0
- package/src/rules/common/coding-style.md +7 -0
- package/src/rules/common/git-workflow.md +7 -0
- package/src/rules/common/security.md +7 -0
- package/src/rules/common/testing.md +7 -0
- package/src/rules/golang/patterns.md +7 -0
- package/src/rules/java/patterns.md +7 -0
- package/src/rules/python/patterns.md +7 -0
- package/src/rules/rust/patterns.md +7 -0
- package/src/rules/typescript/patterns.md +7 -0
package/dist/index.js
CHANGED
|
@@ -1,50 +1,201 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
3
|
+
|
|
1
4
|
// src/index.ts
|
|
2
|
-
import {
|
|
3
|
-
import { join as
|
|
4
|
-
import { dirname as
|
|
5
|
-
import { fileURLToPath as
|
|
5
|
+
import { readFileSync as readFileSync31, readdirSync as readdirSync5, existsSync as existsSync33 } from "fs";
|
|
6
|
+
import { join as join31, basename as basename2 } from "path";
|
|
7
|
+
import { dirname as dirname5 } from "path";
|
|
8
|
+
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
9
|
+
|
|
10
|
+
// src/services/lazy-rule-loader.ts
|
|
11
|
+
import { existsSync, readFileSync, readdirSync } from "fs";
|
|
12
|
+
import { join, basename } from "path";
|
|
13
|
+
var _discoveryCache = new Map;
|
|
14
|
+
function parseFrontmatter(content) {
|
|
15
|
+
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n/);
|
|
16
|
+
if (!match)
|
|
17
|
+
return {};
|
|
18
|
+
const fm = {};
|
|
19
|
+
for (const line of match[1].split(`
|
|
20
|
+
`)) {
|
|
21
|
+
const kv = line.match(/^(\w[\w_-]*):\s*(.+)$/);
|
|
22
|
+
if (!kv)
|
|
23
|
+
continue;
|
|
24
|
+
const [, key, raw] = kv;
|
|
25
|
+
const val = raw.trim();
|
|
26
|
+
if (val.startsWith("[") && val.endsWith("]")) {
|
|
27
|
+
fm[key] = val.slice(1, -1).split(",").map((s) => s.trim()).filter(Boolean);
|
|
28
|
+
} else if (val === "true") {
|
|
29
|
+
fm[key] = true;
|
|
30
|
+
} else if (val === "false") {
|
|
31
|
+
fm[key] = false;
|
|
32
|
+
} else {
|
|
33
|
+
fm[key] = val;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return fm;
|
|
37
|
+
}
|
|
38
|
+
function readRuleMetadata(filePath) {
|
|
39
|
+
try {
|
|
40
|
+
const fullContent = readFileSync(filePath, "utf-8");
|
|
41
|
+
const head = fullContent.slice(0, 1024);
|
|
42
|
+
const fm = parseFrontmatter(head);
|
|
43
|
+
const hasFrontmatter = Object.keys(fm).length > 0;
|
|
44
|
+
return {
|
|
45
|
+
path: filePath,
|
|
46
|
+
description: typeof fm.description === "string" ? fm.description : "",
|
|
47
|
+
always_on: hasFrontmatter ? fm.always_on === true : true,
|
|
48
|
+
stages: Array.isArray(fm.stages) ? fm.stages : [],
|
|
49
|
+
languages: Array.isArray(fm.languages) ? fm.languages : []
|
|
50
|
+
};
|
|
51
|
+
} catch {
|
|
52
|
+
return { path: filePath, description: "", always_on: true, stages: [], languages: [] };
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function discoverRules(rulesDir) {
|
|
56
|
+
const cached = _discoveryCache.get(rulesDir);
|
|
57
|
+
if (cached)
|
|
58
|
+
return cached;
|
|
59
|
+
if (!existsSync(rulesDir))
|
|
60
|
+
return [];
|
|
61
|
+
const results = [];
|
|
62
|
+
function walk(dir) {
|
|
63
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
64
|
+
const full = join(dir, entry.name);
|
|
65
|
+
if (entry.isDirectory()) {
|
|
66
|
+
walk(full);
|
|
67
|
+
} else if (entry.isFile() && entry.name.endsWith(".md") && entry.name !== "README.md") {
|
|
68
|
+
results.push(readRuleMetadata(full));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
walk(rulesDir);
|
|
73
|
+
_discoveryCache.set(rulesDir, results);
|
|
74
|
+
return results;
|
|
75
|
+
}
|
|
76
|
+
function selectRulePaths(rulesDir, context = {}) {
|
|
77
|
+
const all = discoverRules(rulesDir);
|
|
78
|
+
const selected = [];
|
|
79
|
+
const skipped = [];
|
|
80
|
+
const reasons = {};
|
|
81
|
+
const detectedLangs = context.languages ?? [];
|
|
82
|
+
for (const rule of all) {
|
|
83
|
+
const name = basename(rule.path);
|
|
84
|
+
if (rule.always_on) {
|
|
85
|
+
selected.push(rule);
|
|
86
|
+
reasons[rule.path] = "always_on=true";
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (rule.languages.length > 0) {
|
|
90
|
+
const matches = rule.languages.some((l) => detectedLangs.includes(l));
|
|
91
|
+
if (!matches) {
|
|
92
|
+
skipped.push(rule);
|
|
93
|
+
reasons[rule.path] = `language_mismatch: rule=${rule.languages.join("|")} detected=${detectedLangs.join("|") || "unknown"}`;
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (rule.stages.length > 0 && context.stage) {
|
|
98
|
+
if (!rule.stages.includes(context.stage)) {
|
|
99
|
+
skipped.push(rule);
|
|
100
|
+
reasons[rule.path] = `stage_mismatch: rule=${rule.stages.join("|")} current=${context.stage}`;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
selected.push(rule);
|
|
105
|
+
reasons[rule.path] = rule.languages.length > 0 ? `language_match: ${rule.languages.join("|")}` : rule.stages.length > 0 ? `stage_match: ${rule.stages.join("|")} (no current stage filter)` : "default_include: no language/stage restriction";
|
|
106
|
+
}
|
|
107
|
+
return { selected, skipped, reasons, total_discovered: all.length };
|
|
108
|
+
}
|
|
109
|
+
function getStartupRulePaths(rulesDir, detectedLanguages) {
|
|
110
|
+
const selection = selectRulePaths(rulesDir, { languages: detectedLanguages });
|
|
111
|
+
return selection.selected.map((r) => r.path);
|
|
112
|
+
}
|
|
113
|
+
function detectProjectLanguages(projectRoot) {
|
|
114
|
+
const langs = [];
|
|
115
|
+
if (existsSync(join(projectRoot, "package.json"))) {
|
|
116
|
+
try {
|
|
117
|
+
const pkg = JSON.parse(readFileSync(join(projectRoot, "package.json"), "utf-8"));
|
|
118
|
+
const hasTsConfig = existsSync(join(projectRoot, "tsconfig.json"));
|
|
119
|
+
const deps = { ...pkg.dependencies ?? {}, ...pkg.devDependencies ?? {} };
|
|
120
|
+
if (hasTsConfig || "typescript" in deps || "@types/node" in deps) {
|
|
121
|
+
langs.push("typescript");
|
|
122
|
+
} else {
|
|
123
|
+
langs.push("javascript");
|
|
124
|
+
}
|
|
125
|
+
} catch {
|
|
126
|
+
langs.push("javascript");
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if (existsSync(join(projectRoot, "go.mod"))) {
|
|
130
|
+
langs.push("go");
|
|
131
|
+
}
|
|
132
|
+
if (existsSync(join(projectRoot, "Cargo.toml"))) {
|
|
133
|
+
langs.push("rust");
|
|
134
|
+
}
|
|
135
|
+
if (existsSync(join(projectRoot, "pom.xml")) || existsSync(join(projectRoot, "build.gradle")) || existsSync(join(projectRoot, "build.gradle.kts"))) {
|
|
136
|
+
langs.push("java");
|
|
137
|
+
}
|
|
138
|
+
if (existsSync(join(projectRoot, "requirements.txt")) || existsSync(join(projectRoot, "pyproject.toml")) || existsSync(join(projectRoot, "setup.py")) || existsSync(join(projectRoot, "setup.cfg"))) {
|
|
139
|
+
langs.push("python");
|
|
140
|
+
}
|
|
141
|
+
return [...new Set(langs)];
|
|
142
|
+
}
|
|
143
|
+
function buildSelectionDiagnostics(selection, context) {
|
|
144
|
+
const lines = [
|
|
145
|
+
`[LazyRuleLoader] discovered=${selection.total_discovered}` + ` selected=${selection.selected.length}` + ` skipped=${selection.skipped.length}`,
|
|
146
|
+
`[LazyRuleLoader] context:` + ` languages=[${(context.languages ?? []).join(",")}]` + ` stage=${context.stage ?? "none"}`
|
|
147
|
+
];
|
|
148
|
+
for (const r of selection.selected) {
|
|
149
|
+
lines.push(`[LazyRuleLoader] LOAD ${basename(r.path)}: ${selection.reasons[r.path]}`);
|
|
150
|
+
}
|
|
151
|
+
for (const r of selection.skipped) {
|
|
152
|
+
lines.push(`[LazyRuleLoader] SKIP ${basename(r.path)}: ${selection.reasons[r.path]}`);
|
|
153
|
+
}
|
|
154
|
+
return lines.join(`
|
|
155
|
+
`);
|
|
156
|
+
}
|
|
6
157
|
|
|
7
158
|
// src/tools/planning-state.ts
|
|
8
|
-
import { join as
|
|
159
|
+
import { join as join4 } from "path";
|
|
9
160
|
import { tool as tool2 } from "@opencode-ai/plugin";
|
|
10
|
-
import { readFileSync as
|
|
161
|
+
import { readFileSync as readFileSync4, writeFileSync as writeFileSync3, existsSync as existsSync4 } from "fs";
|
|
11
162
|
|
|
12
163
|
// src/tools/planning-state-lib.ts
|
|
13
|
-
import { join as
|
|
14
|
-
import { readFileSync as
|
|
164
|
+
import { join as join3, dirname, resolve } from "path";
|
|
165
|
+
import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, existsSync as existsSync3 } from "fs";
|
|
15
166
|
|
|
16
167
|
// src/tools/codebase-state.ts
|
|
17
168
|
import { tool } from "@opencode-ai/plugin";
|
|
18
|
-
import { readFileSync, writeFileSync, existsSync, readdirSync, mkdirSync } from "fs";
|
|
19
|
-
import { join } from "path";
|
|
169
|
+
import { readFileSync as readFileSync2, writeFileSync, existsSync as existsSync2, readdirSync as readdirSync2, mkdirSync } from "fs";
|
|
170
|
+
import { join as join2 } from "path";
|
|
20
171
|
var CODEBASE_DIR = ".codebase";
|
|
21
172
|
function codebaseDir(directory) {
|
|
22
|
-
return
|
|
173
|
+
return join2(directory, CODEBASE_DIR);
|
|
23
174
|
}
|
|
24
175
|
function codebaseFilePath(directory, filename) {
|
|
25
|
-
return
|
|
176
|
+
return join2(codebaseDir(directory), filename);
|
|
26
177
|
}
|
|
27
178
|
function listCodebaseFiles(directory) {
|
|
28
179
|
const base = codebaseDir(directory);
|
|
29
|
-
if (!
|
|
180
|
+
if (!existsSync2(base))
|
|
30
181
|
return [];
|
|
31
|
-
return
|
|
182
|
+
return readdirSync2(base).filter((f) => f.endsWith(".md") || f.endsWith(".json"));
|
|
32
183
|
}
|
|
33
184
|
async function readCodebaseContext(dir, files) {
|
|
34
185
|
const results = {};
|
|
35
186
|
for (const file of files) {
|
|
36
187
|
const filePath = codebaseFilePath(dir, file);
|
|
37
|
-
if (!
|
|
188
|
+
if (!existsSync2(filePath)) {
|
|
38
189
|
results[file] = { error: `File not found: ${file}` };
|
|
39
190
|
continue;
|
|
40
191
|
}
|
|
41
|
-
results[file] =
|
|
192
|
+
results[file] = readFileSync2(filePath, "utf-8");
|
|
42
193
|
}
|
|
43
194
|
return results;
|
|
44
195
|
}
|
|
45
196
|
async function updateCodebaseFile(dir, filename, content) {
|
|
46
197
|
const base = codebaseDir(dir);
|
|
47
|
-
if (!
|
|
198
|
+
if (!existsSync2(base)) {
|
|
48
199
|
mkdirSync(base, { recursive: true });
|
|
49
200
|
}
|
|
50
201
|
const filePath = codebaseFilePath(dir, filename);
|
|
@@ -53,7 +204,7 @@ async function updateCodebaseFile(dir, filename, content) {
|
|
|
53
204
|
}
|
|
54
205
|
async function codebaseExists(dir) {
|
|
55
206
|
const base = codebaseDir(dir);
|
|
56
|
-
if (!
|
|
207
|
+
if (!existsSync2(base)) {
|
|
57
208
|
return { exists: false, files: [] };
|
|
58
209
|
}
|
|
59
210
|
const files = listCodebaseFiles(dir);
|
|
@@ -91,16 +242,16 @@ var STATE_FILE = "STATE.md";
|
|
|
91
242
|
var PLAN_FILE = "PLAN.md";
|
|
92
243
|
var RESULT_FILE = "RESULT.md";
|
|
93
244
|
function planningDir(directory) {
|
|
94
|
-
return
|
|
245
|
+
return join3(directory, PLANNING_DIR);
|
|
95
246
|
}
|
|
96
247
|
function statePath(directory) {
|
|
97
|
-
return
|
|
248
|
+
return join3(planningDir(directory), STATE_FILE);
|
|
98
249
|
}
|
|
99
250
|
function phasePlanPath(directory, phase) {
|
|
100
|
-
return
|
|
251
|
+
return join3(planningDir(directory), "phases", `phase-${phase}`, PLAN_FILE);
|
|
101
252
|
}
|
|
102
253
|
function resultPath(directory, phase) {
|
|
103
|
-
return
|
|
254
|
+
return join3(planningDir(directory), "phases", `phase-${phase}`, RESULT_FILE);
|
|
104
255
|
}
|
|
105
256
|
function parseState(content) {
|
|
106
257
|
const result = { exists: false };
|
|
@@ -156,7 +307,7 @@ ${entry}
|
|
|
156
307
|
}
|
|
157
308
|
function readPlanningState(dir) {
|
|
158
309
|
const sp = statePath(dir);
|
|
159
|
-
if (!
|
|
310
|
+
if (!existsSync3(sp)) {
|
|
160
311
|
return {
|
|
161
312
|
phase: 0,
|
|
162
313
|
status: "",
|
|
@@ -178,7 +329,7 @@ function readPlanningState(dir) {
|
|
|
178
329
|
freshnessStatus: "unknown"
|
|
179
330
|
};
|
|
180
331
|
}
|
|
181
|
-
const content =
|
|
332
|
+
const content = readFileSync3(sp, "utf-8");
|
|
182
333
|
const parsed = parseState(content);
|
|
183
334
|
return {
|
|
184
335
|
phase: parsed.phase || 1,
|
|
@@ -226,10 +377,10 @@ function parseTDDState(parsed) {
|
|
|
226
377
|
function findWorkspaceRoot(startDir) {
|
|
227
378
|
let current = startDir;
|
|
228
379
|
for (;; ) {
|
|
229
|
-
const configPath =
|
|
230
|
-
if (
|
|
380
|
+
const configPath = join3(current, ".planning", "config.json");
|
|
381
|
+
if (existsSync3(configPath)) {
|
|
231
382
|
try {
|
|
232
|
-
const config = JSON.parse(
|
|
383
|
+
const config = JSON.parse(readFileSync3(configPath, "utf-8"));
|
|
233
384
|
if (config.sub_repos && Array.isArray(config.sub_repos) && config.sub_repos.length > 0) {
|
|
234
385
|
return current;
|
|
235
386
|
}
|
|
@@ -254,11 +405,11 @@ function getWorkspaceConfig(dir) {
|
|
|
254
405
|
const root = findWorkspaceRoot(dir);
|
|
255
406
|
if (!root)
|
|
256
407
|
return null;
|
|
257
|
-
const configPath =
|
|
258
|
-
if (!
|
|
408
|
+
const configPath = join3(root, ".planning", "config.json");
|
|
409
|
+
if (!existsSync3(configPath))
|
|
259
410
|
return null;
|
|
260
411
|
try {
|
|
261
|
-
const config = JSON.parse(
|
|
412
|
+
const config = JSON.parse(readFileSync3(configPath, "utf-8"));
|
|
262
413
|
return {
|
|
263
414
|
sub_repos: Array.isArray(config.sub_repos) ? config.sub_repos : null,
|
|
264
415
|
workspace_mode: config.workspace_mode === "per-repo" ? "per-repo" : "shared",
|
|
@@ -300,19 +451,19 @@ var planningStateTool = tool2({
|
|
|
300
451
|
async execute(args, context) {
|
|
301
452
|
const dir = context.directory ?? process.cwd();
|
|
302
453
|
const sp = statePath(dir);
|
|
303
|
-
if (!
|
|
454
|
+
if (!existsSync4(sp)) {
|
|
304
455
|
return JSON.stringify({ error: "STATE.md not found. Initialize project first." });
|
|
305
456
|
}
|
|
306
457
|
switch (args.action) {
|
|
307
458
|
case "read": {
|
|
308
|
-
const content =
|
|
459
|
+
const content = readFileSync4(sp, "utf-8");
|
|
309
460
|
return JSON.stringify({ exists: true, ...parseState(content) });
|
|
310
461
|
}
|
|
311
462
|
case "update": {
|
|
312
463
|
const u = args.updates;
|
|
313
464
|
if (!u)
|
|
314
465
|
return JSON.stringify({ error: "No updates provided" });
|
|
315
|
-
let content =
|
|
466
|
+
let content = readFileSync4(sp, "utf-8");
|
|
316
467
|
const upsertLine = (current, key, value) => {
|
|
317
468
|
const pattern = new RegExp(`^${key}:\\s*.*$`, "m");
|
|
318
469
|
if (pattern.test(current))
|
|
@@ -366,38 +517,38 @@ ${blockersMd}
|
|
|
366
517
|
return JSON.stringify({ success: true, updated_at: timestamp() });
|
|
367
518
|
}
|
|
368
519
|
case "read_plan": {
|
|
369
|
-
const stateContent =
|
|
520
|
+
const stateContent = readFileSync4(sp, "utf-8");
|
|
370
521
|
const phaseMatch = stateContent.match(/^phase:\s*(\d+)/m);
|
|
371
522
|
if (!phaseMatch)
|
|
372
523
|
return JSON.stringify({ error: "No phase found in STATE.md" });
|
|
373
524
|
const phase = parseInt(phaseMatch[1], 10);
|
|
374
525
|
const planFileMatch = stateContent.match(/^plan_file:\s*(.+)/m);
|
|
375
|
-
const planFile = planFileMatch ? planFileMatch[1].trim() :
|
|
376
|
-
if (!
|
|
526
|
+
const planFile = planFileMatch ? planFileMatch[1].trim() : join4(planningDir(dir), "phases", `phase-${phase}`, PLAN_FILE2);
|
|
527
|
+
if (!existsSync4(planFile))
|
|
377
528
|
return JSON.stringify({ error: `Plan file not found: ${planFile}` });
|
|
378
|
-
return JSON.stringify({ phase, plan_file: planFile, content:
|
|
529
|
+
return JSON.stringify({ phase, plan_file: planFile, content: readFileSync4(planFile, "utf-8") });
|
|
379
530
|
}
|
|
380
531
|
case "mark_complete": {
|
|
381
532
|
const step = args.step;
|
|
382
533
|
const summary = args.summary;
|
|
383
534
|
if (step === undefined || !summary)
|
|
384
535
|
return JSON.stringify({ error: "step and summary required" });
|
|
385
|
-
const stateContent =
|
|
536
|
+
const stateContent = readFileSync4(sp, "utf-8");
|
|
386
537
|
const phaseMatch = stateContent.match(/^phase:\s*(\d+)/m);
|
|
387
538
|
if (!phaseMatch)
|
|
388
539
|
return JSON.stringify({ error: "No phase in STATE.md" });
|
|
389
540
|
const phase = parseInt(phaseMatch[1], 10);
|
|
390
541
|
const planFile = phasePlanPath(dir, phase);
|
|
391
542
|
const resultFile = resultPath(dir, phase);
|
|
392
|
-
if (
|
|
393
|
-
let planContent =
|
|
543
|
+
if (existsSync4(planFile)) {
|
|
544
|
+
let planContent = readFileSync4(planFile, "utf-8");
|
|
394
545
|
planContent = planContent.replace(new RegExp(`(\\[ \\])\\s*Step\\s+${step}\\b`, "i"), `[x] Step ${step}`);
|
|
395
546
|
writeFileSync3(planFile, planContent, "utf-8");
|
|
396
547
|
}
|
|
397
548
|
const entry = `- Step ${step} complete (${timestamp()}): ${summary}
|
|
398
549
|
`;
|
|
399
|
-
if (
|
|
400
|
-
writeFileSync3(resultFile,
|
|
550
|
+
if (existsSync4(resultFile)) {
|
|
551
|
+
writeFileSync3(resultFile, readFileSync4(resultFile, "utf-8") + entry, "utf-8");
|
|
401
552
|
} else {
|
|
402
553
|
writeFileSync3(resultFile, `# Phase ${phase} Results
|
|
403
554
|
|
|
@@ -428,19 +579,19 @@ ${entry}`, "utf-8");
|
|
|
428
579
|
|
|
429
580
|
// src/tools/workspace-state.ts
|
|
430
581
|
import { tool as tool3 } from "@opencode-ai/plugin";
|
|
431
|
-
import { readFileSync as
|
|
432
|
-
import { join as
|
|
582
|
+
import { readFileSync as readFileSync5, writeFileSync as writeFileSync4, existsSync as existsSync5 } from "fs";
|
|
583
|
+
import { join as join5 } from "path";
|
|
433
584
|
function getRepoName(repoPath) {
|
|
434
585
|
return repoPath.split(/[/\\]/).pop() || repoPath;
|
|
435
586
|
}
|
|
436
587
|
function readWorkspaceStateFile(workspaceRoot) {
|
|
437
|
-
const sp =
|
|
438
|
-
if (!
|
|
588
|
+
const sp = join5(planningDir(workspaceRoot), "STATE.md");
|
|
589
|
+
if (!existsSync5(sp))
|
|
439
590
|
return null;
|
|
440
|
-
return
|
|
591
|
+
return readFileSync5(sp, "utf-8");
|
|
441
592
|
}
|
|
442
593
|
function writeWorkspaceStateFile(workspaceRoot, content) {
|
|
443
|
-
const sp =
|
|
594
|
+
const sp = join5(planningDir(workspaceRoot), "STATE.md");
|
|
444
595
|
writeFileSync4(sp, content, "utf-8");
|
|
445
596
|
}
|
|
446
597
|
function parseWorkspaceState(content) {
|
|
@@ -495,12 +646,12 @@ async function updateWorkspaceContextAction(dir, mode, workspaceRoot, updates) {
|
|
|
495
646
|
return { success: true, updated_at: timestamp() };
|
|
496
647
|
}
|
|
497
648
|
async function listSubReposAction(dir, subRepos, workspaceRoot) {
|
|
498
|
-
const resolved = resolveSubRepos(
|
|
649
|
+
const resolved = resolveSubRepos(join5(workspaceRoot, ".planning", "config.json"), subRepos);
|
|
499
650
|
const repos = [];
|
|
500
651
|
for (const repoPath of resolved) {
|
|
501
652
|
const repoName = getRepoName(repoPath);
|
|
502
653
|
const planningPath = planningDir(repoPath);
|
|
503
|
-
const hasPlanning =
|
|
654
|
+
const hasPlanning = existsSync5(planningPath);
|
|
504
655
|
repos.push({
|
|
505
656
|
name: repoName,
|
|
506
657
|
path: repoPath,
|
|
@@ -512,20 +663,20 @@ async function listSubReposAction(dir, subRepos, workspaceRoot) {
|
|
|
512
663
|
async function getSubRepoStateAction(dir, repoName, subRepos, workspaceRoot, mode) {
|
|
513
664
|
if (!repoName)
|
|
514
665
|
return { error: "repo name is required" };
|
|
515
|
-
const resolved = resolveSubRepos(
|
|
666
|
+
const resolved = resolveSubRepos(join5(workspaceRoot, ".planning", "config.json"), subRepos);
|
|
516
667
|
const targetPath = resolved.find((p) => getRepoName(p) === repoName);
|
|
517
668
|
if (!targetPath) {
|
|
518
669
|
return { error: "not_found", path: repoName, message: `Repo '${repoName}' not found in sub_repos` };
|
|
519
670
|
}
|
|
520
671
|
const planningPath = planningDir(targetPath);
|
|
521
|
-
if (!
|
|
672
|
+
if (!existsSync5(planningPath)) {
|
|
522
673
|
return { error: "not_found", path: targetPath };
|
|
523
674
|
}
|
|
524
675
|
const sp = statePath(targetPath);
|
|
525
|
-
if (!
|
|
676
|
+
if (!existsSync5(sp)) {
|
|
526
677
|
return { error: "not_found", path: targetPath, message: `.planning/STATE.md not found in ${repoName}` };
|
|
527
678
|
}
|
|
528
|
-
const stateContent =
|
|
679
|
+
const stateContent = readFileSync5(sp, "utf-8");
|
|
529
680
|
const parsed = parseWorkspaceState(stateContent);
|
|
530
681
|
return {
|
|
531
682
|
repo_path: targetPath,
|
|
@@ -573,24 +724,24 @@ var workspaceStateTool = tool3({
|
|
|
573
724
|
import { tool as tool4 } from "@opencode-ai/plugin";
|
|
574
725
|
|
|
575
726
|
// src/services/agent-performance.ts
|
|
576
|
-
import { existsSync as
|
|
577
|
-
import { join as
|
|
727
|
+
import { existsSync as existsSync6, readFileSync as readFileSync6, writeFileSync as writeFileSync5, mkdirSync as mkdirSync2 } from "fs";
|
|
728
|
+
import { join as join6 } from "path";
|
|
578
729
|
function perfPath(dir) {
|
|
579
|
-
return
|
|
730
|
+
return join6(codebaseDir(dir), "AGENT_PERF.json");
|
|
580
731
|
}
|
|
581
732
|
function loadStore(dir) {
|
|
582
733
|
const p = perfPath(dir);
|
|
583
|
-
if (!
|
|
734
|
+
if (!existsSync6(p))
|
|
584
735
|
return { entries: [], updated_at: new Date().toISOString() };
|
|
585
736
|
try {
|
|
586
|
-
return JSON.parse(
|
|
737
|
+
return JSON.parse(readFileSync6(p, "utf-8"));
|
|
587
738
|
} catch {
|
|
588
739
|
return { entries: [], updated_at: new Date().toISOString() };
|
|
589
740
|
}
|
|
590
741
|
}
|
|
591
742
|
function saveStore(dir, store) {
|
|
592
743
|
const cd = codebaseDir(dir);
|
|
593
|
-
if (!
|
|
744
|
+
if (!existsSync6(cd))
|
|
594
745
|
mkdirSync2(cd, { recursive: true });
|
|
595
746
|
writeFileSync5(perfPath(dir), JSON.stringify(store, null, 2), "utf-8");
|
|
596
747
|
}
|
|
@@ -732,7 +883,8 @@ function createRunPipelineTool(client) {
|
|
|
732
883
|
})),
|
|
733
884
|
initial_context: tool4.schema.string().optional(),
|
|
734
885
|
abort_on_failure: tool4.schema.boolean().optional().default(true),
|
|
735
|
-
retry_attempts: tool4.schema.number().optional().default(1)
|
|
886
|
+
retry_attempts: tool4.schema.number().optional().default(1),
|
|
887
|
+
max_carry_chars: tool4.schema.number().optional()
|
|
736
888
|
},
|
|
737
889
|
async execute(args, context) {
|
|
738
890
|
const startTime = Date.now();
|
|
@@ -818,9 +970,10 @@ ${step.prompt}` : step.prompt;
|
|
|
818
970
|
continue;
|
|
819
971
|
}
|
|
820
972
|
const output = extractText(promptRes.data?.parts ?? []);
|
|
821
|
-
trace.push({ agent: step.agent, session_id: createRes.data.id, task_type: taskType, model: "", input: stepInput, output: output || "(no text output)", duration_ms: Date.now() - stepStart, success: true });
|
|
973
|
+
trace.push({ agent: step.agent, session_id: createRes.data.id, task_type: taskType, model: "", input: stepInput, output: output || "(no text output)", duration_ms: Date.now() - stepStart, success: true, context_chars: carryContext.length });
|
|
822
974
|
recordRun(context.directory, step.agent, "", taskType, true, Date.now() - stepStart);
|
|
823
|
-
|
|
975
|
+
const rawOutput = output || "";
|
|
976
|
+
carryContext = typeof args.max_carry_chars === "number" && rawOutput.length > args.max_carry_chars ? rawOutput.slice(rawOutput.length - args.max_carry_chars) : rawOutput;
|
|
824
977
|
}
|
|
825
978
|
} finally {
|
|
826
979
|
context.abort.removeEventListener("abort", abortHandler);
|
|
@@ -836,6 +989,399 @@ ${step.prompt}` : step.prompt;
|
|
|
836
989
|
|
|
837
990
|
// src/tools/delegate.ts
|
|
838
991
|
import { tool as tool5 } from "@opencode-ai/plugin";
|
|
992
|
+
import { existsSync as existsSync11, readFileSync as readFileSync11 } from "fs";
|
|
993
|
+
|
|
994
|
+
// src/services/prompt-cache.ts
|
|
995
|
+
import { createHash } from "crypto";
|
|
996
|
+
import { existsSync as existsSync7, readFileSync as readFileSync7, writeFileSync as writeFileSync6, readdirSync as readdirSync3, statSync, mkdirSync as mkdirSync3 } from "fs";
|
|
997
|
+
import { join as join7 } from "path";
|
|
998
|
+
var CACHEABLE_AGENTS = new Set([
|
|
999
|
+
"researcher",
|
|
1000
|
+
"code-explorer",
|
|
1001
|
+
"reviewer",
|
|
1002
|
+
"plan-checker",
|
|
1003
|
+
"security-auditor",
|
|
1004
|
+
"question-guard",
|
|
1005
|
+
"quick-router"
|
|
1006
|
+
]);
|
|
1007
|
+
var CACHE_DIR_NAME = "prompt-cache";
|
|
1008
|
+
var MAX_CACHE_ENTRIES = 200;
|
|
1009
|
+
var DEFAULT_TTL_MS = 30 * 60 * 1000;
|
|
1010
|
+
function cacheDir(dir) {
|
|
1011
|
+
return join7(codebaseDir(dir), CACHE_DIR_NAME);
|
|
1012
|
+
}
|
|
1013
|
+
function entryPath(dir, key) {
|
|
1014
|
+
return join7(cacheDir(dir), `${key}.json`);
|
|
1015
|
+
}
|
|
1016
|
+
function readEntry(dir, key, stateVersion, indexVersion) {
|
|
1017
|
+
const path = entryPath(dir, key);
|
|
1018
|
+
if (!existsSync7(path))
|
|
1019
|
+
return null;
|
|
1020
|
+
try {
|
|
1021
|
+
const entry = JSON.parse(readFileSync7(path, "utf-8"));
|
|
1022
|
+
const age = Date.now() - new Date(entry.created_at).getTime();
|
|
1023
|
+
if (age > entry.ttl_ms)
|
|
1024
|
+
return null;
|
|
1025
|
+
if (entry.state_version !== stateVersion || entry.index_version !== indexVersion)
|
|
1026
|
+
return null;
|
|
1027
|
+
return entry.response;
|
|
1028
|
+
} catch {
|
|
1029
|
+
return null;
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
function hashKey(agent, prompt, context, stateVersion, indexVersion) {
|
|
1033
|
+
const raw = JSON.stringify({ agent, prompt: prompt.trim(), context: context.trim(), stateVersion, indexVersion });
|
|
1034
|
+
return createHash("sha256").update(raw).digest("hex").slice(0, 32);
|
|
1035
|
+
}
|
|
1036
|
+
function normalizeForCache(text) {
|
|
1037
|
+
return text.replace(/\s+/g, " ").trim();
|
|
1038
|
+
}
|
|
1039
|
+
function hashKeyNormalized(agent, prompt, context, stateVersion, indexVersion) {
|
|
1040
|
+
const normalized = JSON.stringify({
|
|
1041
|
+
agent,
|
|
1042
|
+
prompt: normalizeForCache(prompt),
|
|
1043
|
+
context: normalizeForCache(context),
|
|
1044
|
+
stateVersion,
|
|
1045
|
+
indexVersion
|
|
1046
|
+
});
|
|
1047
|
+
return createHash("sha256").update(normalized).digest("hex").slice(0, 32);
|
|
1048
|
+
}
|
|
1049
|
+
function getCached(dir, agent, prompt, context, stateVersion, indexVersion, safe_to_cache = false) {
|
|
1050
|
+
if (!safe_to_cache)
|
|
1051
|
+
return null;
|
|
1052
|
+
if (!CACHEABLE_AGENTS.has(agent))
|
|
1053
|
+
return null;
|
|
1054
|
+
const exactKey = hashKey(agent, prompt, context, stateVersion, indexVersion);
|
|
1055
|
+
const exactResult = readEntry(dir, exactKey, stateVersion, indexVersion);
|
|
1056
|
+
if (exactResult !== null)
|
|
1057
|
+
return exactResult;
|
|
1058
|
+
const normKey = hashKeyNormalized(agent, prompt, context, stateVersion, indexVersion);
|
|
1059
|
+
if (normKey === exactKey)
|
|
1060
|
+
return null;
|
|
1061
|
+
return readEntry(dir, normKey, stateVersion, indexVersion);
|
|
1062
|
+
}
|
|
1063
|
+
function setCached(dir, agent, prompt, context, stateVersion, indexVersion, response, safe_to_cache = false, ttl_ms = DEFAULT_TTL_MS) {
|
|
1064
|
+
if (!safe_to_cache)
|
|
1065
|
+
return;
|
|
1066
|
+
if (!CACHEABLE_AGENTS.has(agent))
|
|
1067
|
+
return;
|
|
1068
|
+
const cd = cacheDir(dir);
|
|
1069
|
+
if (!existsSync7(cd))
|
|
1070
|
+
mkdirSync3(cd, { recursive: true });
|
|
1071
|
+
const key = hashKey(agent, prompt, context, stateVersion, indexVersion);
|
|
1072
|
+
const entry = {
|
|
1073
|
+
key,
|
|
1074
|
+
agent,
|
|
1075
|
+
state_version: stateVersion,
|
|
1076
|
+
index_version: indexVersion,
|
|
1077
|
+
created_at: new Date().toISOString(),
|
|
1078
|
+
ttl_ms,
|
|
1079
|
+
response
|
|
1080
|
+
};
|
|
1081
|
+
writeFileSync6(entryPath(dir, key), JSON.stringify(entry, null, 2), "utf-8");
|
|
1082
|
+
pruneExpired(dir);
|
|
1083
|
+
}
|
|
1084
|
+
function pruneExpired(dir) {
|
|
1085
|
+
const cd = cacheDir(dir);
|
|
1086
|
+
if (!existsSync7(cd))
|
|
1087
|
+
return;
|
|
1088
|
+
try {
|
|
1089
|
+
const files = readdirSync3(cd).filter((f) => f.endsWith(".json"));
|
|
1090
|
+
const now = Date.now();
|
|
1091
|
+
const entries = [];
|
|
1092
|
+
for (const f of files) {
|
|
1093
|
+
const p = join7(cd, f);
|
|
1094
|
+
try {
|
|
1095
|
+
const entry = JSON.parse(readFileSync7(p, "utf-8"));
|
|
1096
|
+
const age = now - new Date(entry.created_at).getTime();
|
|
1097
|
+
entries.push({ path: p, created_at: new Date(entry.created_at).getTime(), expired: age > entry.ttl_ms });
|
|
1098
|
+
} catch {
|
|
1099
|
+
entries.push({ path: p, created_at: 0, expired: true });
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
let deleted = 0;
|
|
1103
|
+
for (const e of entries) {
|
|
1104
|
+
if (e.expired) {
|
|
1105
|
+
try {
|
|
1106
|
+
__require("fs").unlinkSync(e.path);
|
|
1107
|
+
} catch {}
|
|
1108
|
+
deleted++;
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
const valid = entries.filter((e) => !e.expired).sort((a, b) => a.created_at - b.created_at);
|
|
1112
|
+
const excess = valid.length - MAX_CACHE_ENTRIES;
|
|
1113
|
+
for (let i = 0;i < excess; i++) {
|
|
1114
|
+
try {
|
|
1115
|
+
__require("fs").unlinkSync(valid[i].path);
|
|
1116
|
+
} catch {}
|
|
1117
|
+
}
|
|
1118
|
+
} catch {}
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
// src/tools/codebase-index.ts
|
|
1122
|
+
import { readFileSync as readFileSync8, writeFileSync as writeFileSync7, existsSync as existsSync8, mkdirSync as mkdirSync4 } from "fs";
|
|
1123
|
+
import { join as join8 } from "path";
|
|
1124
|
+
var CODEBASE_INDEX_FILE = "CODEBASE_INDEX.md";
|
|
1125
|
+
function indexPath(dir) {
|
|
1126
|
+
return join8(planningDir(dir), CODEBASE_INDEX_FILE);
|
|
1127
|
+
}
|
|
1128
|
+
function readCodebaseIndex(dir) {
|
|
1129
|
+
const path = indexPath(dir);
|
|
1130
|
+
if (!existsSync8(path)) {
|
|
1131
|
+
return {
|
|
1132
|
+
exists: false,
|
|
1133
|
+
lastUpdatedAt: "",
|
|
1134
|
+
lastUpdatedBy: "",
|
|
1135
|
+
sourceStage: "",
|
|
1136
|
+
changedFiles: [],
|
|
1137
|
+
fileSnapshots: {},
|
|
1138
|
+
explorationHistory: [],
|
|
1139
|
+
summaryVersion: 0,
|
|
1140
|
+
freshnessStatus: "unknown"
|
|
1141
|
+
};
|
|
1142
|
+
}
|
|
1143
|
+
try {
|
|
1144
|
+
const content = readFileSync8(path, "utf-8");
|
|
1145
|
+
return parseCodebaseIndexContent(content);
|
|
1146
|
+
} catch {
|
|
1147
|
+
return {
|
|
1148
|
+
exists: false,
|
|
1149
|
+
lastUpdatedAt: "",
|
|
1150
|
+
lastUpdatedBy: "",
|
|
1151
|
+
sourceStage: "",
|
|
1152
|
+
changedFiles: [],
|
|
1153
|
+
fileSnapshots: {},
|
|
1154
|
+
explorationHistory: [],
|
|
1155
|
+
summaryVersion: 0,
|
|
1156
|
+
freshnessStatus: "unknown"
|
|
1157
|
+
};
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
function parseCodebaseIndexContent(content) {
|
|
1161
|
+
const result = { exists: true };
|
|
1162
|
+
for (const line of content.split(`
|
|
1163
|
+
`)) {
|
|
1164
|
+
if (line.startsWith("#") || line.trim() === "")
|
|
1165
|
+
continue;
|
|
1166
|
+
const strippedLine = line.replace(/\*\*/g, "").replace(/^([a-zA-Z_][a-zA-Z0-9_]*):\s*(.*)/, "$1: $2");
|
|
1167
|
+
const kvMatch = strippedLine.match(/^([a-zA-Z_][a-zA-Z0-9_]*):\s*(.*)/);
|
|
1168
|
+
if (!kvMatch)
|
|
1169
|
+
continue;
|
|
1170
|
+
const key = kvMatch[1].trim();
|
|
1171
|
+
const value = kvMatch[2].trim();
|
|
1172
|
+
if (key === "changedFiles") {
|
|
1173
|
+
result.changedFiles = value.replace(/[\[\]]/g, "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
1174
|
+
} else if (key === "summaryVersion") {
|
|
1175
|
+
result.summaryVersion = parseInt(value, 10) || 0;
|
|
1176
|
+
} else if (key === "freshnessStatus") {
|
|
1177
|
+
result.freshnessStatus = value;
|
|
1178
|
+
} else if (key === "lastUpdatedAt" || key === "lastUpdatedBy" || key === "sourceStage") {
|
|
1179
|
+
result[key] = value.replace(/^["']|["']$/g, "");
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
let blockCount = 0;
|
|
1183
|
+
for (const jsonMatch of content.matchAll(/```json\n([\s\S]*?)\n```/g)) {
|
|
1184
|
+
if (blockCount >= 2)
|
|
1185
|
+
break;
|
|
1186
|
+
blockCount++;
|
|
1187
|
+
try {
|
|
1188
|
+
const parsed = JSON.parse(jsonMatch[1]);
|
|
1189
|
+
if (parsed.fileSnapshots)
|
|
1190
|
+
result.fileSnapshots = parsed.fileSnapshots;
|
|
1191
|
+
if (parsed.explorationHistory)
|
|
1192
|
+
result.explorationHistory = parsed.explorationHistory;
|
|
1193
|
+
if (!parsed.fileSnapshots && !parsed.explorationHistory) {
|
|
1194
|
+
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
1195
|
+
if (!result.fileSnapshots)
|
|
1196
|
+
result.fileSnapshots = {};
|
|
1197
|
+
Object.assign(result.fileSnapshots, parsed);
|
|
1198
|
+
} else if (Array.isArray(parsed)) {
|
|
1199
|
+
result.explorationHistory = parsed;
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
} catch {
|
|
1203
|
+
result.freshnessStatus = "unknown";
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
return {
|
|
1207
|
+
exists: true,
|
|
1208
|
+
lastUpdatedAt: result.lastUpdatedAt || "",
|
|
1209
|
+
lastUpdatedBy: result.lastUpdatedBy || "",
|
|
1210
|
+
sourceStage: result.sourceStage || "",
|
|
1211
|
+
changedFiles: result.changedFiles || [],
|
|
1212
|
+
fileSnapshots: result.fileSnapshots || {},
|
|
1213
|
+
explorationHistory: result.explorationHistory || [],
|
|
1214
|
+
summaryVersion: result.summaryVersion || 0,
|
|
1215
|
+
freshnessStatus: result.freshnessStatus || "unknown"
|
|
1216
|
+
};
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
// src/services/token-metrics.ts
|
|
1220
|
+
import { existsSync as existsSync9, readFileSync as readFileSync9, appendFileSync, mkdirSync as mkdirSync5 } from "fs";
|
|
1221
|
+
import { join as join9 } from "path";
|
|
1222
|
+
function estimateTokens(text) {
|
|
1223
|
+
return Math.ceil(text.length / 4);
|
|
1224
|
+
}
|
|
1225
|
+
function metricsPath(dir) {
|
|
1226
|
+
return join9(codebaseDir(dir), "TOKEN_METRICS.jsonl");
|
|
1227
|
+
}
|
|
1228
|
+
function appendEvent(dir, event) {
|
|
1229
|
+
const cd = codebaseDir(dir);
|
|
1230
|
+
if (!existsSync9(cd))
|
|
1231
|
+
mkdirSync5(cd, { recursive: true });
|
|
1232
|
+
appendFileSync(metricsPath(dir), JSON.stringify(event) + `
|
|
1233
|
+
`, "utf-8");
|
|
1234
|
+
}
|
|
1235
|
+
function recordModelCall(dir, workflow_id, stage, inputText, outputText, agent, duration_ms, model, est_cost_usd) {
|
|
1236
|
+
const est_input_tokens = estimateTokens(inputText);
|
|
1237
|
+
const est_output_tokens = estimateTokens(outputText);
|
|
1238
|
+
appendEvent(dir, {
|
|
1239
|
+
ts: new Date().toISOString(),
|
|
1240
|
+
workflow_id,
|
|
1241
|
+
stage,
|
|
1242
|
+
event: "model_call",
|
|
1243
|
+
agent,
|
|
1244
|
+
model,
|
|
1245
|
+
est_input_tokens,
|
|
1246
|
+
est_output_tokens,
|
|
1247
|
+
input_chars: inputText.length,
|
|
1248
|
+
output_chars: outputText.length,
|
|
1249
|
+
duration_ms,
|
|
1250
|
+
est_cost_usd
|
|
1251
|
+
});
|
|
1252
|
+
}
|
|
1253
|
+
function recordCacheHit(dir, workflow_id, stage, inputText, agent, model) {
|
|
1254
|
+
appendEvent(dir, {
|
|
1255
|
+
ts: new Date().toISOString(),
|
|
1256
|
+
workflow_id,
|
|
1257
|
+
stage,
|
|
1258
|
+
event: "cache_hit",
|
|
1259
|
+
agent,
|
|
1260
|
+
model,
|
|
1261
|
+
est_input_tokens: estimateTokens(inputText),
|
|
1262
|
+
est_output_tokens: 0,
|
|
1263
|
+
input_chars: inputText.length,
|
|
1264
|
+
output_chars: 0
|
|
1265
|
+
});
|
|
1266
|
+
}
|
|
1267
|
+
function recordRetryCall(dir, workflow_id, stage, inputText, outputText, agent, duration_ms, model, est_cost_usd) {
|
|
1268
|
+
const est_input_tokens = estimateTokens(inputText);
|
|
1269
|
+
const est_output_tokens = estimateTokens(outputText);
|
|
1270
|
+
appendEvent(dir, {
|
|
1271
|
+
ts: new Date().toISOString(),
|
|
1272
|
+
workflow_id,
|
|
1273
|
+
stage,
|
|
1274
|
+
event: "retry",
|
|
1275
|
+
agent,
|
|
1276
|
+
model,
|
|
1277
|
+
est_input_tokens,
|
|
1278
|
+
est_output_tokens,
|
|
1279
|
+
input_chars: inputText.length,
|
|
1280
|
+
output_chars: outputText.length,
|
|
1281
|
+
duration_ms,
|
|
1282
|
+
est_cost_usd
|
|
1283
|
+
});
|
|
1284
|
+
}
|
|
1285
|
+
var _workflowTimers = new Map;
|
|
1286
|
+
|
|
1287
|
+
// src/config/loader.ts
|
|
1288
|
+
import { existsSync as existsSync10, readFileSync as readFileSync10 } from "fs";
|
|
1289
|
+
import { join as join10 } from "path";
|
|
1290
|
+
import { homedir } from "os";
|
|
1291
|
+
var CONFIG_FILENAME = "flowdeck.json";
|
|
1292
|
+
function getGlobalConfigDir() {
|
|
1293
|
+
return process.env.OPENCODE_CONFIG_DIR || (process.env.XDG_CONFIG_HOME ? join10(process.env.XDG_CONFIG_HOME, "opencode") : join10(homedir(), ".config", "opencode"));
|
|
1294
|
+
}
|
|
1295
|
+
function loadFlowDeckConfig(directory) {
|
|
1296
|
+
const candidates = [];
|
|
1297
|
+
if (directory) {
|
|
1298
|
+
candidates.push(join10(directory, ".opencode", CONFIG_FILENAME));
|
|
1299
|
+
}
|
|
1300
|
+
candidates.push(join10(getGlobalConfigDir(), CONFIG_FILENAME));
|
|
1301
|
+
for (const configPath of candidates) {
|
|
1302
|
+
if (existsSync10(configPath)) {
|
|
1303
|
+
try {
|
|
1304
|
+
const content = readFileSync10(configPath, "utf-8");
|
|
1305
|
+
return JSON.parse(content);
|
|
1306
|
+
} catch {
|
|
1307
|
+
console.warn(`[flowdeck] Failed to load config from ${configPath}`);
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
return {};
|
|
1312
|
+
}
|
|
1313
|
+
function resolveDesignFirstConfig(config) {
|
|
1314
|
+
return {
|
|
1315
|
+
enabled: config.designFirst?.enabled ?? true,
|
|
1316
|
+
enforcement: config.designFirst?.enforcement ?? "strict",
|
|
1317
|
+
requireApprovalBeforeImplementation: config.designFirst?.requireApprovalBeforeImplementation ?? true,
|
|
1318
|
+
modelOverrides: config.designFirst?.modelOverrides ?? {},
|
|
1319
|
+
defaultSkillsByTaskType: config.designFirst?.defaultSkillsByTaskType ?? {
|
|
1320
|
+
"landing-page": ["landing-page-design", "wireframe-planning", "design-system-definition", "frontend-handoff"],
|
|
1321
|
+
dashboard: ["dashboard-design", "ui-ux-planning", "wireframe-planning", "responsive-review"],
|
|
1322
|
+
"admin-panel": ["ui-ux-planning", "wireframe-planning", "design-system-definition", "frontend-handoff"],
|
|
1323
|
+
"app-screen": ["app-shell-design", "ui-ux-planning", "wireframe-planning", "responsive-review"],
|
|
1324
|
+
"general-ui": ["ui-ux-planning", "wireframe-planning", "design-system-definition", "frontend-handoff"]
|
|
1325
|
+
}
|
|
1326
|
+
};
|
|
1327
|
+
}
|
|
1328
|
+
// src/services/cost-estimator.ts
|
|
1329
|
+
var PRICING_TABLE = [
|
|
1330
|
+
{ prefix: "claude-opus-4", pricing: { input: 15, output: 75 } },
|
|
1331
|
+
{ prefix: "claude-opus", pricing: { input: 15, output: 75 } },
|
|
1332
|
+
{ prefix: "claude-sonnet-4", pricing: { input: 3, output: 15 } },
|
|
1333
|
+
{ prefix: "claude-sonnet-3-5", pricing: { input: 3, output: 15 } },
|
|
1334
|
+
{ prefix: "claude-sonnet-3", pricing: { input: 3, output: 15 } },
|
|
1335
|
+
{ prefix: "claude-sonnet", pricing: { input: 3, output: 15 } },
|
|
1336
|
+
{ prefix: "claude-haiku-4", pricing: { input: 0.8, output: 4 } },
|
|
1337
|
+
{ prefix: "claude-haiku-3-5", pricing: { input: 0.8, output: 4 } },
|
|
1338
|
+
{ prefix: "claude-haiku", pricing: { input: 0.25, output: 1.25 } },
|
|
1339
|
+
{ prefix: "claude-3-opus", pricing: { input: 15, output: 75 } },
|
|
1340
|
+
{ prefix: "claude-3-5-sonnet", pricing: { input: 3, output: 15 } },
|
|
1341
|
+
{ prefix: "claude-3-sonnet", pricing: { input: 3, output: 15 } },
|
|
1342
|
+
{ prefix: "claude-3-haiku", pricing: { input: 0.25, output: 1.25 } },
|
|
1343
|
+
{ prefix: "claude", pricing: { input: 3, output: 15 } },
|
|
1344
|
+
{ prefix: "gpt-5.4-mini", pricing: { input: 0.15, output: 0.6 } },
|
|
1345
|
+
{ prefix: "gpt-5-mini", pricing: { input: 0.15, output: 0.6 } },
|
|
1346
|
+
{ prefix: "gpt-4.1", pricing: { input: 2, output: 8 } },
|
|
1347
|
+
{ prefix: "gpt-4o-mini", pricing: { input: 0.15, output: 0.6 } },
|
|
1348
|
+
{ prefix: "gpt-4o", pricing: { input: 2.5, output: 10 } },
|
|
1349
|
+
{ prefix: "gpt-4-turbo", pricing: { input: 10, output: 30 } },
|
|
1350
|
+
{ prefix: "gpt-4", pricing: { input: 30, output: 60 } },
|
|
1351
|
+
{ prefix: "gpt-3.5", pricing: { input: 0.5, output: 1.5 } },
|
|
1352
|
+
{ prefix: "gpt-5", pricing: { input: 10, output: 30 } },
|
|
1353
|
+
{ prefix: "o3-mini", pricing: { input: 1.1, output: 4.4 } },
|
|
1354
|
+
{ prefix: "o3", pricing: { input: 10, output: 40 } },
|
|
1355
|
+
{ prefix: "o1-mini", pricing: { input: 1.1, output: 4.4 } },
|
|
1356
|
+
{ prefix: "o1", pricing: { input: 15, output: 60 } },
|
|
1357
|
+
{ prefix: "gemini-2.0-flash", pricing: { input: 0.1, output: 0.4 } },
|
|
1358
|
+
{ prefix: "gemini-2.5-flash", pricing: { input: 0.15, output: 0.6 } },
|
|
1359
|
+
{ prefix: "gemini-2.5-pro", pricing: { input: 1.25, output: 5 } },
|
|
1360
|
+
{ prefix: "gemini-1.5-flash", pricing: { input: 0.075, output: 0.3 } },
|
|
1361
|
+
{ prefix: "gemini-1.5-pro", pricing: { input: 1.25, output: 5 } },
|
|
1362
|
+
{ prefix: "gemini", pricing: { input: 0.1, output: 0.4 } },
|
|
1363
|
+
{ prefix: "github-copilot/sonnet", pricing: { input: 3, output: 15 } },
|
|
1364
|
+
{ prefix: "github-copilot/haiku", pricing: { input: 0.25, output: 1.25 } },
|
|
1365
|
+
{ prefix: "github-copilot/gpt-4", pricing: { input: 2.5, output: 10 } },
|
|
1366
|
+
{ prefix: "github-copilot", pricing: { input: 3, output: 15 } }
|
|
1367
|
+
];
|
|
1368
|
+
var FALLBACK_PRICING = { input: 3, output: 15 };
|
|
1369
|
+
function getModelPricing(model) {
|
|
1370
|
+
if (!model)
|
|
1371
|
+
return FALLBACK_PRICING;
|
|
1372
|
+
const lower = model.toLowerCase();
|
|
1373
|
+
for (const entry of PRICING_TABLE) {
|
|
1374
|
+
if (lower.startsWith(entry.prefix.toLowerCase()))
|
|
1375
|
+
return entry.pricing;
|
|
1376
|
+
}
|
|
1377
|
+
return FALLBACK_PRICING;
|
|
1378
|
+
}
|
|
1379
|
+
function estimateCostUSD(model, inputTokens, outputTokens) {
|
|
1380
|
+
const pricing = getModelPricing(model);
|
|
1381
|
+
return inputTokens / 1e6 * pricing.input + outputTokens / 1e6 * pricing.output;
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
// src/tools/delegate.ts
|
|
839
1385
|
function extractText2(parts) {
|
|
840
1386
|
return parts.filter((p) => p.type === "text" && typeof p.text === "string").map((p) => p.text).join(`
|
|
841
1387
|
`);
|
|
@@ -848,13 +1394,56 @@ function createDelegateTool(client) {
|
|
|
848
1394
|
prompt: tool5.schema.string(),
|
|
849
1395
|
context: tool5.schema.string().optional(),
|
|
850
1396
|
task_type: tool5.schema.string().optional(),
|
|
851
|
-
retry_attempts: tool5.schema.number().optional().default(1)
|
|
1397
|
+
retry_attempts: tool5.schema.number().optional().default(1),
|
|
1398
|
+
safe_to_cache: tool5.schema.boolean().optional().default(false),
|
|
1399
|
+
cache_ttl_ms: tool5.schema.number().optional(),
|
|
1400
|
+
workflow_id: tool5.schema.string().optional(),
|
|
1401
|
+
stage: tool5.schema.string().optional()
|
|
852
1402
|
},
|
|
853
1403
|
async execute(args, context) {
|
|
854
1404
|
const startTime = Date.now();
|
|
855
1405
|
const taskType = normalizeTaskType(args.task_type, args.agent);
|
|
856
1406
|
const retryAttempts = typeof args.retry_attempts === "number" ? args.retry_attempts : 1;
|
|
857
1407
|
const maxRetries = Math.max(0, Math.floor(retryAttempts));
|
|
1408
|
+
let agentModel = "";
|
|
1409
|
+
try {
|
|
1410
|
+
const cfg = loadFlowDeckConfig(context.directory);
|
|
1411
|
+
agentModel = cfg.agents?.[args.agent]?.model ?? "";
|
|
1412
|
+
} catch {}
|
|
1413
|
+
const metricsWorkflowId = args.workflow_id ?? "";
|
|
1414
|
+
const metricsStage = args.stage ?? "delegate";
|
|
1415
|
+
const fullPrompt = args.context ? `${args.context}
|
|
1416
|
+
|
|
1417
|
+
---
|
|
1418
|
+
|
|
1419
|
+
${args.prompt}` : args.prompt;
|
|
1420
|
+
const safe_to_cache = args.safe_to_cache === true && CACHEABLE_AGENTS.has(args.agent);
|
|
1421
|
+
let stateVersion = 0;
|
|
1422
|
+
let indexVersion = 0;
|
|
1423
|
+
if (safe_to_cache) {
|
|
1424
|
+
const index = readCodebaseIndex(context.directory);
|
|
1425
|
+
const sp = statePath(context.directory);
|
|
1426
|
+
const rawState = existsSync11(sp) ? readFileSync11(sp, "utf-8") : "";
|
|
1427
|
+
const state = rawState ? parseState(rawState) : {};
|
|
1428
|
+
stateVersion = typeof state.summaryVersion === "number" ? state.summaryVersion : 0;
|
|
1429
|
+
indexVersion = typeof index.summaryVersion === "number" ? index.summaryVersion : 0;
|
|
1430
|
+
const cached = getCached(context.directory, args.agent, fullPrompt, args.context ?? "", stateVersion, indexVersion, true);
|
|
1431
|
+
if (cached !== null) {
|
|
1432
|
+
if (metricsWorkflowId) {
|
|
1433
|
+
recordCacheHit(context.directory, metricsWorkflowId, metricsStage, fullPrompt, args.agent, agentModel);
|
|
1434
|
+
}
|
|
1435
|
+
return JSON.stringify({
|
|
1436
|
+
agent: args.agent,
|
|
1437
|
+
success: true,
|
|
1438
|
+
output: cached,
|
|
1439
|
+
task_type: taskType,
|
|
1440
|
+
model: "",
|
|
1441
|
+
retries_used: 0,
|
|
1442
|
+
duration_ms: Date.now() - startTime,
|
|
1443
|
+
cached: true
|
|
1444
|
+
});
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
858
1447
|
const createRes = await client.session.create({
|
|
859
1448
|
body: { parentID: context.sessionID, title: `${args.agent}-delegate` },
|
|
860
1449
|
query: { directory: context.directory }
|
|
@@ -874,7 +1463,7 @@ function createDelegateTool(client) {
|
|
|
874
1463
|
query: { directory: context.directory }
|
|
875
1464
|
}).catch(() => {});
|
|
876
1465
|
});
|
|
877
|
-
const
|
|
1466
|
+
const fullPromptForSession = args.context ? `${args.context}
|
|
878
1467
|
|
|
879
1468
|
---
|
|
880
1469
|
|
|
@@ -882,17 +1471,23 @@ ${args.prompt}` : args.prompt;
|
|
|
882
1471
|
let promptRes = null;
|
|
883
1472
|
let retriesUsed = 0;
|
|
884
1473
|
for (let attempt = 0;attempt <= maxRetries; attempt++) {
|
|
1474
|
+
const attemptStart = Date.now();
|
|
885
1475
|
promptRes = await client.session.prompt({
|
|
886
1476
|
path: { id: childId },
|
|
887
1477
|
body: {
|
|
888
1478
|
agent: args.agent,
|
|
889
|
-
parts: [{ type: "text", text:
|
|
1479
|
+
parts: [{ type: "text", text: fullPromptForSession }],
|
|
890
1480
|
tools: { question: false }
|
|
891
1481
|
},
|
|
892
1482
|
query: { directory: context.directory }
|
|
893
1483
|
});
|
|
894
1484
|
if (!shouldRetry(promptRes) || attempt === maxRetries)
|
|
895
1485
|
break;
|
|
1486
|
+
if (metricsWorkflowId) {
|
|
1487
|
+
const retryInputTokens = estimateTokens(fullPromptForSession);
|
|
1488
|
+
const retryCostUsd = agentModel ? estimateCostUSD(agentModel, retryInputTokens, 0) : undefined;
|
|
1489
|
+
recordRetryCall(context.directory, metricsWorkflowId, metricsStage, fullPromptForSession, "", args.agent, Date.now() - attemptStart, agentModel, retryCostUsd);
|
|
1490
|
+
}
|
|
896
1491
|
retriesUsed++;
|
|
897
1492
|
}
|
|
898
1493
|
if (!promptRes || promptRes.error) {
|
|
@@ -924,6 +1519,15 @@ ${args.prompt}` : args.prompt;
|
|
|
924
1519
|
}
|
|
925
1520
|
const output = extractText2(promptRes.data?.parts ?? []);
|
|
926
1521
|
recordRun(context.directory, args.agent, "", taskType, true, Date.now() - startTime);
|
|
1522
|
+
if (metricsWorkflowId) {
|
|
1523
|
+
const inputTokens = estimateTokens(fullPromptForSession);
|
|
1524
|
+
const outputTokens = estimateTokens(output);
|
|
1525
|
+
const costUsd = agentModel ? estimateCostUSD(agentModel, inputTokens, outputTokens) : undefined;
|
|
1526
|
+
recordModelCall(context.directory, metricsWorkflowId, metricsStage, fullPromptForSession, output, args.agent, Date.now() - startTime, agentModel, costUsd);
|
|
1527
|
+
}
|
|
1528
|
+
if (safe_to_cache && output) {
|
|
1529
|
+
setCached(context.directory, args.agent, fullPromptForSession, args.context ?? "", stateVersion, indexVersion, output, true, args.cache_ttl_ms);
|
|
1530
|
+
}
|
|
927
1531
|
return JSON.stringify({
|
|
928
1532
|
agent: args.agent,
|
|
929
1533
|
session_id: childId,
|
|
@@ -940,31 +1544,31 @@ ${args.prompt}` : args.prompt;
|
|
|
940
1544
|
|
|
941
1545
|
// src/tools/repo-memory.ts
|
|
942
1546
|
import { tool as tool6 } from "@opencode-ai/plugin";
|
|
943
|
-
import { readFileSync as
|
|
944
|
-
import { join as
|
|
1547
|
+
import { readFileSync as readFileSync12, writeFileSync as writeFileSync8, existsSync as existsSync12, mkdirSync as mkdirSync6 } from "fs";
|
|
1548
|
+
import { join as join11 } from "path";
|
|
945
1549
|
var MEMORY_FILE = "MEMORY.json";
|
|
946
1550
|
function memoryPath(directory) {
|
|
947
|
-
return
|
|
1551
|
+
return join11(codebaseDir(directory), MEMORY_FILE);
|
|
948
1552
|
}
|
|
949
1553
|
function emptyMemory() {
|
|
950
1554
|
return { version: "1.0", last_updated: new Date().toISOString(), nodes: {} };
|
|
951
1555
|
}
|
|
952
1556
|
function readMemory(directory) {
|
|
953
1557
|
const p = memoryPath(directory);
|
|
954
|
-
if (!
|
|
1558
|
+
if (!existsSync12(p))
|
|
955
1559
|
return emptyMemory();
|
|
956
1560
|
try {
|
|
957
|
-
return JSON.parse(
|
|
1561
|
+
return JSON.parse(readFileSync12(p, "utf-8"));
|
|
958
1562
|
} catch {
|
|
959
1563
|
return emptyMemory();
|
|
960
1564
|
}
|
|
961
1565
|
}
|
|
962
1566
|
function writeMemory(directory, memory) {
|
|
963
1567
|
const base = codebaseDir(directory);
|
|
964
|
-
if (!
|
|
965
|
-
|
|
1568
|
+
if (!existsSync12(base))
|
|
1569
|
+
mkdirSync6(base, { recursive: true });
|
|
966
1570
|
memory.last_updated = new Date().toISOString();
|
|
967
|
-
|
|
1571
|
+
writeFileSync8(memoryPath(directory), JSON.stringify(memory, null, 2), "utf-8");
|
|
968
1572
|
}
|
|
969
1573
|
var repoMemoryTool = tool6({
|
|
970
1574
|
description: "Repo Memory Graph: read/write/query persistent architecture graph in .codebase/MEMORY.json (modules, dependencies, ownership, bug history, conventions)",
|
|
@@ -1041,28 +1645,28 @@ var repoMemoryTool = tool6({
|
|
|
1041
1645
|
|
|
1042
1646
|
// src/tools/failure-replay.ts
|
|
1043
1647
|
import { tool as tool7 } from "@opencode-ai/plugin";
|
|
1044
|
-
import { readFileSync as
|
|
1045
|
-
import { join as
|
|
1648
|
+
import { readFileSync as readFileSync13, writeFileSync as writeFileSync9, existsSync as existsSync13, mkdirSync as mkdirSync7 } from "fs";
|
|
1649
|
+
import { join as join12 } from "path";
|
|
1046
1650
|
var FAILURES_FILE = "FAILURES.json";
|
|
1047
1651
|
function failuresPath(directory) {
|
|
1048
|
-
return
|
|
1652
|
+
return join12(codebaseDir(directory), FAILURES_FILE);
|
|
1049
1653
|
}
|
|
1050
1654
|
function readStore(directory) {
|
|
1051
1655
|
const p = failuresPath(directory);
|
|
1052
|
-
if (!
|
|
1656
|
+
if (!existsSync13(p))
|
|
1053
1657
|
return { version: "1.0", last_updated: new Date().toISOString(), entries: [] };
|
|
1054
1658
|
try {
|
|
1055
|
-
return JSON.parse(
|
|
1659
|
+
return JSON.parse(readFileSync13(p, "utf-8"));
|
|
1056
1660
|
} catch {
|
|
1057
1661
|
return { version: "1.0", last_updated: new Date().toISOString(), entries: [] };
|
|
1058
1662
|
}
|
|
1059
1663
|
}
|
|
1060
1664
|
function writeStore(directory, store) {
|
|
1061
1665
|
const base = codebaseDir(directory);
|
|
1062
|
-
if (!
|
|
1063
|
-
|
|
1666
|
+
if (!existsSync13(base))
|
|
1667
|
+
mkdirSync7(base, { recursive: true });
|
|
1064
1668
|
store.last_updated = new Date().toISOString();
|
|
1065
|
-
|
|
1669
|
+
writeFileSync9(failuresPath(directory), JSON.stringify(store, null, 2), "utf-8");
|
|
1066
1670
|
}
|
|
1067
1671
|
var failureReplayTool = tool7({
|
|
1068
1672
|
description: "Failure Replay Engine: record and query past failures (reverted commits, failed deployments, flaky tests, bug fixes) in .codebase/FAILURES.json so the agent avoids repeating mistakes",
|
|
@@ -1146,17 +1750,17 @@ var failureReplayTool = tool7({
|
|
|
1146
1750
|
|
|
1147
1751
|
// src/tools/decision-trace.ts
|
|
1148
1752
|
import { tool as tool8 } from "@opencode-ai/plugin";
|
|
1149
|
-
import { readFileSync as
|
|
1150
|
-
import { join as
|
|
1753
|
+
import { readFileSync as readFileSync14, existsSync as existsSync14, mkdirSync as mkdirSync8, appendFileSync as appendFileSync2 } from "fs";
|
|
1754
|
+
import { join as join13 } from "path";
|
|
1151
1755
|
var DECISIONS_FILE = "DECISIONS.jsonl";
|
|
1152
1756
|
function decisionsPath(directory) {
|
|
1153
|
-
return
|
|
1757
|
+
return join13(codebaseDir(directory), DECISIONS_FILE);
|
|
1154
1758
|
}
|
|
1155
1759
|
function readDecisions(directory) {
|
|
1156
1760
|
const p = decisionsPath(directory);
|
|
1157
|
-
if (!
|
|
1761
|
+
if (!existsSync14(p))
|
|
1158
1762
|
return [];
|
|
1159
|
-
return
|
|
1763
|
+
return readFileSync14(p, "utf-8").split(`
|
|
1160
1764
|
`).filter((l) => l.trim()).map((l) => {
|
|
1161
1765
|
try {
|
|
1162
1766
|
return JSON.parse(l);
|
|
@@ -1196,10 +1800,10 @@ var decisionTraceTool = tool8({
|
|
|
1196
1800
|
case "record": {
|
|
1197
1801
|
if (!args.entry)
|
|
1198
1802
|
return JSON.stringify({ error: "entry required" });
|
|
1199
|
-
if (!
|
|
1200
|
-
|
|
1803
|
+
if (!existsSync14(base))
|
|
1804
|
+
mkdirSync8(base, { recursive: true });
|
|
1201
1805
|
const entry = { ...args.entry, timestamp: new Date().toISOString() };
|
|
1202
|
-
|
|
1806
|
+
appendFileSync2(decisionsPath(dir), JSON.stringify(entry) + `
|
|
1203
1807
|
`, "utf-8");
|
|
1204
1808
|
return JSON.stringify({ success: true, id: args.entry.id });
|
|
1205
1809
|
}
|
|
@@ -1231,28 +1835,28 @@ var decisionTraceTool = tool8({
|
|
|
1231
1835
|
|
|
1232
1836
|
// src/tools/volatility-map.ts
|
|
1233
1837
|
import { tool as tool9 } from "@opencode-ai/plugin";
|
|
1234
|
-
import { readFileSync as
|
|
1235
|
-
import { join as
|
|
1838
|
+
import { readFileSync as readFileSync15, writeFileSync as writeFileSync11, existsSync as existsSync15, mkdirSync as mkdirSync9 } from "fs";
|
|
1839
|
+
import { join as join14 } from "path";
|
|
1236
1840
|
var VOLATILITY_FILE = "VOLATILITY.json";
|
|
1237
1841
|
function volatilityPath(directory) {
|
|
1238
|
-
return
|
|
1842
|
+
return join14(codebaseDir(directory), VOLATILITY_FILE);
|
|
1239
1843
|
}
|
|
1240
1844
|
function readStore2(directory) {
|
|
1241
1845
|
const p = volatilityPath(directory);
|
|
1242
|
-
if (!
|
|
1846
|
+
if (!existsSync15(p))
|
|
1243
1847
|
return { version: "1.0", last_updated: new Date().toISOString(), generated_at: new Date().toISOString(), entries: [] };
|
|
1244
1848
|
try {
|
|
1245
|
-
return JSON.parse(
|
|
1849
|
+
return JSON.parse(readFileSync15(p, "utf-8"));
|
|
1246
1850
|
} catch {
|
|
1247
1851
|
return { version: "1.0", last_updated: new Date().toISOString(), generated_at: new Date().toISOString(), entries: [] };
|
|
1248
1852
|
}
|
|
1249
1853
|
}
|
|
1250
1854
|
function writeStore2(directory, store) {
|
|
1251
1855
|
const base = codebaseDir(directory);
|
|
1252
|
-
if (!
|
|
1253
|
-
|
|
1856
|
+
if (!existsSync15(base))
|
|
1857
|
+
mkdirSync9(base, { recursive: true });
|
|
1254
1858
|
store.last_updated = new Date().toISOString();
|
|
1255
|
-
|
|
1859
|
+
writeFileSync11(volatilityPath(directory), JSON.stringify(store, null, 2), "utf-8");
|
|
1256
1860
|
}
|
|
1257
1861
|
function stabilityLabel(churn, hotfixes, todos) {
|
|
1258
1862
|
const score = churn + hotfixes * 10 + todos * 2;
|
|
@@ -1339,28 +1943,28 @@ var volatilityMapTool = tool9({
|
|
|
1339
1943
|
|
|
1340
1944
|
// src/tools/policy-engine.ts
|
|
1341
1945
|
import { tool as tool10 } from "@opencode-ai/plugin";
|
|
1342
|
-
import { readFileSync as
|
|
1343
|
-
import { join as
|
|
1946
|
+
import { readFileSync as readFileSync16, writeFileSync as writeFileSync12, existsSync as existsSync16, mkdirSync as mkdirSync10 } from "fs";
|
|
1947
|
+
import { join as join15 } from "path";
|
|
1344
1948
|
var POLICIES_FILE = "POLICIES.json";
|
|
1345
1949
|
function policiesPath(directory) {
|
|
1346
|
-
return
|
|
1950
|
+
return join15(codebaseDir(directory), POLICIES_FILE);
|
|
1347
1951
|
}
|
|
1348
1952
|
function readStore3(directory) {
|
|
1349
1953
|
const p = policiesPath(directory);
|
|
1350
|
-
if (!
|
|
1954
|
+
if (!existsSync16(p))
|
|
1351
1955
|
return { version: "1.0", last_updated: new Date().toISOString(), policies: [] };
|
|
1352
1956
|
try {
|
|
1353
|
-
return JSON.parse(
|
|
1957
|
+
return JSON.parse(readFileSync16(p, "utf-8"));
|
|
1354
1958
|
} catch {
|
|
1355
1959
|
return { version: "1.0", last_updated: new Date().toISOString(), policies: [] };
|
|
1356
1960
|
}
|
|
1357
1961
|
}
|
|
1358
1962
|
function writeStore3(directory, store) {
|
|
1359
1963
|
const base = codebaseDir(directory);
|
|
1360
|
-
if (!
|
|
1361
|
-
|
|
1964
|
+
if (!existsSync16(base))
|
|
1965
|
+
mkdirSync10(base, { recursive: true });
|
|
1362
1966
|
store.last_updated = new Date().toISOString();
|
|
1363
|
-
|
|
1967
|
+
writeFileSync12(policiesPath(directory), JSON.stringify(store, null, 2), "utf-8");
|
|
1364
1968
|
}
|
|
1365
1969
|
var policyEngineTool = tool10({
|
|
1366
1970
|
description: "Self-Healing Policy Engine: manage .codebase/POLICIES.json — add, list, query, toggle, and record violations of editing policies learned from past failures",
|
|
@@ -1442,8 +2046,8 @@ var policyEngineTool = tool10({
|
|
|
1442
2046
|
|
|
1443
2047
|
// src/tools/hash-edit.ts
|
|
1444
2048
|
import { tool as tool11 } from "@opencode-ai/plugin";
|
|
1445
|
-
import { readFileSync as
|
|
1446
|
-
import { createHash } from "crypto";
|
|
2049
|
+
import { readFileSync as readFileSync17, writeFileSync as writeFileSync13 } from "fs";
|
|
2050
|
+
import { createHash as createHash2 } from "crypto";
|
|
1447
2051
|
var hashEditTool = tool11({
|
|
1448
2052
|
description: "Reliable file editing with content verification. Takes a target content, its expected MD5 hash, and replacement content. Only applies if the hash matches, preventing edits on stale file versions.",
|
|
1449
2053
|
args: {
|
|
@@ -1456,7 +2060,7 @@ var hashEditTool = tool11({
|
|
|
1456
2060
|
const fullPath = args.filePath.startsWith("/") ? args.filePath : `${context.directory}/${args.filePath}`;
|
|
1457
2061
|
let content;
|
|
1458
2062
|
try {
|
|
1459
|
-
content =
|
|
2063
|
+
content = readFileSync17(fullPath, "utf-8");
|
|
1460
2064
|
} catch (e) {
|
|
1461
2065
|
return `Error: Could not read file ${args.filePath}`;
|
|
1462
2066
|
}
|
|
@@ -1464,37 +2068,76 @@ var hashEditTool = tool11({
|
|
|
1464
2068
|
return `Error: Target content not found in ${args.filePath}. It may have been modified by another agent.`;
|
|
1465
2069
|
}
|
|
1466
2070
|
if (args.expectedHash) {
|
|
1467
|
-
const actualHash =
|
|
2071
|
+
const actualHash = createHash2("md5").update(args.targetContent).digest("hex");
|
|
1468
2072
|
if (actualHash !== args.expectedHash) {
|
|
1469
2073
|
return `Error: Hash mismatch for target content. Expected ${args.expectedHash}, got ${actualHash}. Refusing to edit stale content.`;
|
|
1470
2074
|
}
|
|
1471
2075
|
}
|
|
1472
2076
|
const newContent = content.replace(args.targetContent, args.replacementContent);
|
|
1473
|
-
|
|
2077
|
+
writeFileSync13(fullPath, newContent, "utf-8");
|
|
1474
2078
|
return `Successfully updated ${args.filePath} using hash-anchored edit.`;
|
|
1475
2079
|
}
|
|
1476
2080
|
});
|
|
1477
2081
|
|
|
1478
2082
|
// src/tools/council.ts
|
|
1479
2083
|
import { tool as tool12 } from "@opencode-ai/plugin";
|
|
1480
|
-
import { appendFileSync as
|
|
1481
|
-
import { join as
|
|
2084
|
+
import { appendFileSync as appendFileSync3, existsSync as existsSync17, mkdirSync as mkdirSync11 } from "fs";
|
|
2085
|
+
import { join as join16 } from "path";
|
|
2086
|
+
import { createHash as createHash3 } from "crypto";
|
|
2087
|
+
import { readFileSync as readFileSync18 } from "fs";
|
|
2088
|
+
var _councilCache = new Map;
|
|
2089
|
+
var COUNCIL_CACHE_TTL_MS = 20 * 60 * 1000;
|
|
2090
|
+
function councilCacheKey(task, agents, stateVersion, indexVersion) {
|
|
2091
|
+
const sorted = [...agents].sort();
|
|
2092
|
+
return createHash3("sha256").update(JSON.stringify({ task: task.trim(), agents: sorted, sv: stateVersion, iv: indexVersion })).digest("hex").slice(0, 32);
|
|
2093
|
+
}
|
|
2094
|
+
async function runWithConcurrencyLimit(tasks, limit) {
|
|
2095
|
+
const results = new Array(tasks.length);
|
|
2096
|
+
let next = 0;
|
|
2097
|
+
async function worker() {
|
|
2098
|
+
while (next < tasks.length) {
|
|
2099
|
+
const idx = next++;
|
|
2100
|
+
results[idx] = await tasks[idx]();
|
|
2101
|
+
}
|
|
2102
|
+
}
|
|
2103
|
+
const workers = Array.from({ length: Math.min(limit, tasks.length) }, () => worker());
|
|
2104
|
+
await Promise.all(workers);
|
|
2105
|
+
return results;
|
|
2106
|
+
}
|
|
1482
2107
|
function createCouncilTool(client) {
|
|
1483
2108
|
return tool12({
|
|
1484
|
-
description: "Run an ensemble of agents (Council) on the same task to reach consensus or compare approaches. Runs
|
|
2109
|
+
description: "Run an ensemble of agents (Council) on the same task to reach consensus or compare approaches. Runs specialized agents in parallel (bounded concurrency) and returns their synthesized outputs.",
|
|
1485
2110
|
args: {
|
|
1486
2111
|
task: tool12.schema.string(),
|
|
1487
|
-
agents: tool12.schema.array(tool12.schema.string()).optional()
|
|
2112
|
+
agents: tool12.schema.array(tool12.schema.string()).optional(),
|
|
2113
|
+
force_fresh: tool12.schema.boolean().optional().default(false),
|
|
2114
|
+
max_concurrency: tool12.schema.number().optional().default(3)
|
|
1488
2115
|
},
|
|
1489
2116
|
async execute(args, context) {
|
|
1490
2117
|
const agents = args.agents || ["architect", "reviewer", "backend-coder"];
|
|
2118
|
+
const concurrencyLimit = Math.max(1, Math.min(5, typeof args.max_concurrency === "number" ? args.max_concurrency : 3));
|
|
2119
|
+
const index = readCodebaseIndex(context.directory);
|
|
2120
|
+
const sp = statePath(context.directory);
|
|
2121
|
+
const rawState = existsSync17(sp) ? readFileSync18(sp, "utf-8") : "";
|
|
2122
|
+
const state = rawState ? parseState(rawState) : {};
|
|
2123
|
+
const stateVersion = typeof state.summaryVersion === "number" ? state.summaryVersion : 0;
|
|
2124
|
+
const indexVersion = typeof index.summaryVersion === "number" ? index.summaryVersion : 0;
|
|
2125
|
+
if (!args.force_fresh) {
|
|
2126
|
+
const cacheKey2 = councilCacheKey(args.task, agents, stateVersion, indexVersion);
|
|
2127
|
+
const cached = _councilCache.get(cacheKey2);
|
|
2128
|
+
if (cached && Date.now() - cached.cached_at < COUNCIL_CACHE_TTL_MS) {
|
|
2129
|
+
return cached.synthesis + `
|
|
2130
|
+
|
|
2131
|
+
<!-- council: cached result -->`;
|
|
2132
|
+
}
|
|
2133
|
+
}
|
|
1491
2134
|
const tasks = agents.map((agent) => ({
|
|
1492
2135
|
agent,
|
|
1493
2136
|
prompt: `TASK: ${args.task}
|
|
1494
2137
|
|
|
1495
2138
|
Please provide your best analysis/implementation for this task. Your output will be compared with other agents in a council.`
|
|
1496
2139
|
}));
|
|
1497
|
-
const results = await
|
|
2140
|
+
const results = await runWithConcurrencyLimit(tasks.map((task) => async () => {
|
|
1498
2141
|
const createRes = await client.session.create({
|
|
1499
2142
|
body: { parentID: context.sessionID, title: `Council: ${task.agent}` },
|
|
1500
2143
|
query: { directory: context.directory }
|
|
@@ -1514,7 +2157,7 @@ Please provide your best analysis/implementation for this task. Your output will
|
|
|
1514
2157
|
const output = (promptRes.data?.parts ?? []).filter((p) => p.type === "text").map((p) => p.text).join(`
|
|
1515
2158
|
`);
|
|
1516
2159
|
return { agent: task.agent, output: output || "(no output)" };
|
|
1517
|
-
}));
|
|
2160
|
+
}), concurrencyLimit);
|
|
1518
2161
|
const synthesisPrompt = `You are a Council Synthesizer. Below are the outputs from ${results.length} different agents on the same task.
|
|
1519
2162
|
|
|
1520
2163
|
TASK: ${args.task}
|
|
@@ -1542,6 +2185,8 @@ Please synthesize these results. Identify areas of agreement, resolve conflicts,
|
|
|
1542
2185
|
synthesis,
|
|
1543
2186
|
created_at: new Date().toISOString()
|
|
1544
2187
|
});
|
|
2188
|
+
const cacheKey = councilCacheKey(args.task, agents, stateVersion, indexVersion);
|
|
2189
|
+
_councilCache.set(cacheKey, { synthesis, cached_at: Date.now() });
|
|
1545
2190
|
return synthesis;
|
|
1546
2191
|
}
|
|
1547
2192
|
});
|
|
@@ -1549,18 +2194,18 @@ Please synthesize these results. Identify areas of agreement, resolve conflicts,
|
|
|
1549
2194
|
function persistCouncilResult(directory, payload) {
|
|
1550
2195
|
try {
|
|
1551
2196
|
const base = codebaseDir(directory);
|
|
1552
|
-
if (!
|
|
1553
|
-
|
|
1554
|
-
const path =
|
|
1555
|
-
|
|
2197
|
+
if (!existsSync17(base))
|
|
2198
|
+
mkdirSync11(base, { recursive: true });
|
|
2199
|
+
const path = join16(base, "COUNCILS.jsonl");
|
|
2200
|
+
appendFileSync3(path, JSON.stringify(payload) + `
|
|
1556
2201
|
`, "utf-8");
|
|
1557
2202
|
} catch {}
|
|
1558
2203
|
}
|
|
1559
2204
|
|
|
1560
2205
|
// src/tools/context-generator.ts
|
|
1561
2206
|
import { tool as tool13 } from "@opencode-ai/plugin";
|
|
1562
|
-
import { writeFileSync as
|
|
1563
|
-
import { join as
|
|
2207
|
+
import { writeFileSync as writeFileSync14, existsSync as existsSync18, readFileSync as readFileSync19, readdirSync as readdirSync4, statSync as statSync2 } from "fs";
|
|
2208
|
+
import { join as join17 } from "path";
|
|
1564
2209
|
var contextGeneratorTool = tool13({
|
|
1565
2210
|
description: "Auto-generate or update hierarchical context files (AGENTS.md, CLAUDE.md) throughout the project. These files provide critical grounding for AI agents.",
|
|
1566
2211
|
args: {
|
|
@@ -1569,20 +2214,20 @@ var contextGeneratorTool = tool13({
|
|
|
1569
2214
|
},
|
|
1570
2215
|
async execute(args, context) {
|
|
1571
2216
|
const root = context.directory;
|
|
1572
|
-
const target = args.targetDir ?
|
|
1573
|
-
if (!
|
|
2217
|
+
const target = args.targetDir ? join17(root, args.targetDir) : root;
|
|
2218
|
+
if (!existsSync18(target)) {
|
|
1574
2219
|
return `Error: Directory ${target} does not exist.`;
|
|
1575
2220
|
}
|
|
1576
|
-
const agentsMdPath =
|
|
1577
|
-
if (
|
|
2221
|
+
const agentsMdPath = join17(target, "AGENTS.md");
|
|
2222
|
+
if (existsSync18(agentsMdPath) && !args.force) {
|
|
1578
2223
|
return `AGENTS.md already exists in ${target}. Use force: true to overwrite.`;
|
|
1579
2224
|
}
|
|
1580
|
-
const pkgPath =
|
|
2225
|
+
const pkgPath = join17(root, "package.json");
|
|
1581
2226
|
let projectName = "Project";
|
|
1582
2227
|
let techStack = "Unknown";
|
|
1583
|
-
if (
|
|
2228
|
+
if (existsSync18(pkgPath)) {
|
|
1584
2229
|
try {
|
|
1585
|
-
const pkg = JSON.parse(
|
|
2230
|
+
const pkg = JSON.parse(readFileSync19(pkgPath, "utf-8"));
|
|
1586
2231
|
projectName = pkg.name || projectName;
|
|
1587
2232
|
techStack = Object.keys(pkg.dependencies || {}).slice(0, 5).join(", ");
|
|
1588
2233
|
} catch {}
|
|
@@ -1599,8 +2244,8 @@ var contextGeneratorTool = tool13({
|
|
|
1599
2244
|
3. **Planning**: Always check \`.planning/STATE.md\` before executing major changes.
|
|
1600
2245
|
|
|
1601
2246
|
## Directory Map
|
|
1602
|
-
${
|
|
1603
|
-
const s =
|
|
2247
|
+
${readdirSync4(target).slice(0, 10).map((f) => {
|
|
2248
|
+
const s = statSync2(join17(target, f));
|
|
1604
2249
|
return `- \`${f}\`${s.isDirectory() ? "/" : ""} : [Description]`;
|
|
1605
2250
|
}).join(`
|
|
1606
2251
|
`)}
|
|
@@ -1608,17 +2253,17 @@ ${readdirSync2(target).slice(0, 10).map((f) => {
|
|
|
1608
2253
|
---
|
|
1609
2254
|
Generated by FlowDeck Context Generator.
|
|
1610
2255
|
`;
|
|
1611
|
-
|
|
2256
|
+
writeFileSync14(agentsMdPath, content, "utf-8");
|
|
1612
2257
|
return `Successfully generated AGENTS.md in ${target}.`;
|
|
1613
2258
|
}
|
|
1614
2259
|
});
|
|
1615
2260
|
|
|
1616
2261
|
// src/tools/create-skill.ts
|
|
1617
2262
|
import { tool as tool14 } from "@opencode-ai/plugin";
|
|
1618
|
-
import { mkdirSync as
|
|
1619
|
-
import { join as
|
|
2263
|
+
import { mkdirSync as mkdirSync12, writeFileSync as writeFileSync15, existsSync as existsSync19 } from "fs";
|
|
2264
|
+
import { join as join18, dirname as dirname3 } from "path";
|
|
1620
2265
|
import { fileURLToPath } from "url";
|
|
1621
|
-
var SKILLS_DIR =
|
|
2266
|
+
var SKILLS_DIR = join18(dirname3(fileURLToPath(import.meta.url)), "..", "skills");
|
|
1622
2267
|
var createSkillTool = tool14({
|
|
1623
2268
|
description: "Create a new reusable skill in the FlowDeck skill library (src/skills/). " + "Use this when you discover a repeatable pattern, solve a novel problem with human guidance, " + "or want to capture domain knowledge for future sessions.",
|
|
1624
2269
|
args: {
|
|
@@ -1628,9 +2273,9 @@ var createSkillTool = tool14({
|
|
|
1628
2273
|
tags: tool14.schema.array(tool14.schema.string()).optional().describe("Optional tags for categorisation, e.g. ['performance', 'typescript']")
|
|
1629
2274
|
},
|
|
1630
2275
|
async execute(args) {
|
|
1631
|
-
const skillDir =
|
|
1632
|
-
const skillFile =
|
|
1633
|
-
if (
|
|
2276
|
+
const skillDir = join18(SKILLS_DIR, args.name);
|
|
2277
|
+
const skillFile = join18(skillDir, "SKILL.md");
|
|
2278
|
+
if (existsSync19(skillFile)) {
|
|
1634
2279
|
return `Skill '${args.name}' already exists at ${skillFile}.
|
|
1635
2280
|
` + `Use a different name or delete the existing skill directory first.`;
|
|
1636
2281
|
}
|
|
@@ -1645,8 +2290,8 @@ origin: FlowDeck (self-learned)${tagLine}
|
|
|
1645
2290
|
`;
|
|
1646
2291
|
const fullContent = frontmatter + args.content.trimStart();
|
|
1647
2292
|
try {
|
|
1648
|
-
|
|
1649
|
-
|
|
2293
|
+
mkdirSync12(skillDir, { recursive: true });
|
|
2294
|
+
writeFileSync15(skillFile, fullContent, "utf-8");
|
|
1650
2295
|
return `✓ Skill '${args.name}' created at ${skillFile}
|
|
1651
2296
|
|
|
1652
2297
|
` + `The skill is now part of the FlowDeck library. Restart OpenCode to load it into the active session.`;
|
|
@@ -1658,8 +2303,8 @@ origin: FlowDeck (self-learned)${tagLine}
|
|
|
1658
2303
|
|
|
1659
2304
|
// src/tools/reflect.ts
|
|
1660
2305
|
import { tool as tool15 } from "@opencode-ai/plugin";
|
|
1661
|
-
import { existsSync as
|
|
1662
|
-
import { join as
|
|
2306
|
+
import { existsSync as existsSync20, readFileSync as readFileSync20 } from "fs";
|
|
2307
|
+
import { join as join19 } from "path";
|
|
1663
2308
|
var MAX_ARTIFACT_BYTES = 4000;
|
|
1664
2309
|
function tail(text, maxBytes) {
|
|
1665
2310
|
if (text.length <= maxBytes)
|
|
@@ -1688,11 +2333,11 @@ var reflectTool = tool15({
|
|
|
1688
2333
|
];
|
|
1689
2334
|
let found = 0;
|
|
1690
2335
|
for (const [rel, label] of ARTIFACT_PATHS) {
|
|
1691
|
-
const full =
|
|
1692
|
-
if (!
|
|
2336
|
+
const full = join19(root, rel);
|
|
2337
|
+
if (!existsSync20(full))
|
|
1693
2338
|
continue;
|
|
1694
2339
|
try {
|
|
1695
|
-
const raw =
|
|
2340
|
+
const raw = readFileSync20(full, "utf-8").trim();
|
|
1696
2341
|
if (!raw)
|
|
1697
2342
|
continue;
|
|
1698
2343
|
const count = raw.split(`
|
|
@@ -1716,12 +2361,12 @@ import { tool as tool16 } from "@opencode-ai/plugin";
|
|
|
1716
2361
|
|
|
1717
2362
|
// src/services/codegraph.ts
|
|
1718
2363
|
import { spawnSync } from "child_process";
|
|
1719
|
-
import { existsSync as
|
|
1720
|
-
import { join as
|
|
2364
|
+
import { existsSync as existsSync21, readFileSync as readFileSync21, writeFileSync as writeFileSync16, mkdirSync as mkdirSync13 } from "fs";
|
|
2365
|
+
import { join as join20 } from "path";
|
|
1721
2366
|
var CODEGRAPH_META_FILE = "CODEGRAPH.md";
|
|
1722
2367
|
var MAX_FRESHNESS_MS = 30 * 60 * 1000;
|
|
1723
2368
|
function metaPath(dir) {
|
|
1724
|
-
return
|
|
2369
|
+
return join20(codebaseDir(dir), CODEGRAPH_META_FILE);
|
|
1725
2370
|
}
|
|
1726
2371
|
function isCodegraphInstalled() {
|
|
1727
2372
|
try {
|
|
@@ -1736,11 +2381,11 @@ function isCodegraphInstalled() {
|
|
|
1736
2381
|
}
|
|
1737
2382
|
}
|
|
1738
2383
|
function isCodegraphIndexed(dir) {
|
|
1739
|
-
return
|
|
2384
|
+
return existsSync21(join20(dir, ".codegraph", "codegraph.db"));
|
|
1740
2385
|
}
|
|
1741
2386
|
function readCodegraphMeta(dir) {
|
|
1742
2387
|
const path = metaPath(dir);
|
|
1743
|
-
if (!
|
|
2388
|
+
if (!existsSync21(path)) {
|
|
1744
2389
|
return {
|
|
1745
2390
|
installed: false,
|
|
1746
2391
|
indexed: false,
|
|
@@ -1753,7 +2398,7 @@ function readCodegraphMeta(dir) {
|
|
|
1753
2398
|
};
|
|
1754
2399
|
}
|
|
1755
2400
|
try {
|
|
1756
|
-
const content =
|
|
2401
|
+
const content = readFileSync21(path, "utf-8");
|
|
1757
2402
|
return parseCodegraphMeta(content);
|
|
1758
2403
|
} catch {
|
|
1759
2404
|
return {
|
|
@@ -1820,8 +2465,8 @@ function parseCodegraphMeta(content) {
|
|
|
1820
2465
|
}
|
|
1821
2466
|
function writeCodegraphMeta(dir, meta) {
|
|
1822
2467
|
const base = codebaseDir(dir);
|
|
1823
|
-
if (!
|
|
1824
|
-
|
|
2468
|
+
if (!existsSync21(base))
|
|
2469
|
+
mkdirSync13(base, { recursive: true });
|
|
1825
2470
|
const lines = [
|
|
1826
2471
|
"# Codegraph Metadata",
|
|
1827
2472
|
"",
|
|
@@ -1834,7 +2479,7 @@ function writeCodegraphMeta(dir, meta) {
|
|
|
1834
2479
|
`**installLog:** ${meta.installLog}`,
|
|
1835
2480
|
`**indexLog:** ${meta.indexLog}`
|
|
1836
2481
|
];
|
|
1837
|
-
|
|
2482
|
+
writeFileSync16(metaPath(dir), lines.join(`
|
|
1838
2483
|
`), "utf-8");
|
|
1839
2484
|
}
|
|
1840
2485
|
function isCodegraphFresh(dir, maxAgeMs = MAX_FRESHNESS_MS) {
|
|
@@ -2046,7 +2691,7 @@ function markCodegraphStale(dir) {
|
|
|
2046
2691
|
|
|
2047
2692
|
// src/tools/codegraph-tool.ts
|
|
2048
2693
|
var codegraphTool = tool16({
|
|
2049
|
-
description: "Manage codegraph
|
|
2694
|
+
description: "Manage codegraph lifecycle only: check installation, install, init/rebuild the index, refresh (incremental sync), " + "query status, or mark-stale. Valid actions: check | install | init | refresh | status | mark-stale. " + "Do NOT use this tool for code intelligence queries (files, search, callers, callees, etc.) — " + "those are available as codegraph MCP tools (codegraph_files, codegraph_search, codegraph_context, " + "codegraph_explore, codegraph_callers, codegraph_callees, codegraph_impact, codegraph_trace) " + "when the index is ready.",
|
|
2050
2695
|
args: {
|
|
2051
2696
|
action: tool16.schema.enum(["check", "install", "init", "refresh", "status", "mark-stale"]),
|
|
2052
2697
|
agent: tool16.schema.string().optional()
|
|
@@ -2125,63 +2770,310 @@ var codegraphTool = tool16({
|
|
|
2125
2770
|
markCodegraphStale(dir);
|
|
2126
2771
|
return JSON.stringify({ success: true, message: "codegraph index marked stale — next init will do a full rebuild" });
|
|
2127
2772
|
}
|
|
2773
|
+
default: {
|
|
2774
|
+
const unknownAction = args.action;
|
|
2775
|
+
return JSON.stringify({
|
|
2776
|
+
success: false,
|
|
2777
|
+
error: `Unknown action "${unknownAction}". Valid actions: check, install, init, refresh, status, mark-stale.`,
|
|
2778
|
+
hint: `For code intelligence queries (files, search, callers, etc.) use the codegraph MCP tools directly: ` + `codegraph_files, codegraph_search, codegraph_context, codegraph_explore, codegraph_callers, ` + `codegraph_callees, codegraph_impact, codegraph_trace.`
|
|
2779
|
+
});
|
|
2780
|
+
}
|
|
2128
2781
|
}
|
|
2129
2782
|
}
|
|
2130
2783
|
});
|
|
2131
2784
|
|
|
2132
|
-
// src/
|
|
2133
|
-
import {
|
|
2134
|
-
import {
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2785
|
+
// src/tools/load-rules.ts
|
|
2786
|
+
import { tool as tool17 } from "@opencode-ai/plugin";
|
|
2787
|
+
import { existsSync as existsSync22, readFileSync as readFileSync22 } from "fs";
|
|
2788
|
+
import { join as join21, dirname as dirname4 } from "path";
|
|
2789
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
2790
|
+
var RULES_DIR = join21(dirname4(fileURLToPath2(import.meta.url)), "..", "rules");
|
|
2791
|
+
var _loadedPaths = new Set;
|
|
2792
|
+
var loadRulesTool = tool17({
|
|
2793
|
+
description: "Load additional rule modules on demand for the current workflow stage. " + "Use this at the start of a new stage (execute, verify, fix-bug) to load " + "coding-style, security, testing, and language-specific rules that were not " + "injected at startup. Returns the full text of selected rules. " + "Already-loaded rules are not returned again (suppressed to avoid duplication).",
|
|
2794
|
+
args: {
|
|
2795
|
+
stage: tool17.schema.string().optional().describe("Current workflow stage: discuss | plan | execute | verify | fix-bug | write-docs"),
|
|
2796
|
+
languages: tool17.schema.array(tool17.schema.string()).optional().describe("Project languages to load rules for, e.g. ['typescript']. " + "Omit to use all languages (returns all matching stage rules)."),
|
|
2797
|
+
force_reload: tool17.schema.boolean().optional().default(false).describe("When true, return rules even if they were already loaded in this session. " + "Use only when stage context has changed and you need a fresh load.")
|
|
2798
|
+
},
|
|
2799
|
+
async execute(args) {
|
|
2800
|
+
const rulesDir = existsSync22(RULES_DIR) ? RULES_DIR : null;
|
|
2801
|
+
if (!rulesDir) {
|
|
2802
|
+
return JSON.stringify({
|
|
2803
|
+
loaded: [],
|
|
2804
|
+
skipped_already_loaded: [],
|
|
2805
|
+
skipped_no_match: [],
|
|
2806
|
+
content: "",
|
|
2807
|
+
error: `Rules directory not found at ${RULES_DIR}`
|
|
2808
|
+
});
|
|
2809
|
+
}
|
|
2810
|
+
const context = {
|
|
2811
|
+
stage: args.stage,
|
|
2812
|
+
languages: args.languages
|
|
2813
|
+
};
|
|
2814
|
+
const selection = selectRulePaths(rulesDir, context);
|
|
2815
|
+
const diagnostics = buildSelectionDiagnostics(selection, context);
|
|
2816
|
+
const loaded = [];
|
|
2817
|
+
const skippedAlreadyLoaded = [];
|
|
2818
|
+
const contents = [];
|
|
2819
|
+
for (const rule of selection.selected) {
|
|
2820
|
+
const name = ruleShortName(rule);
|
|
2821
|
+
if (!args.force_reload && _loadedPaths.has(rule.path)) {
|
|
2822
|
+
skippedAlreadyLoaded.push(name);
|
|
2823
|
+
continue;
|
|
2824
|
+
}
|
|
2152
2825
|
try {
|
|
2153
|
-
const
|
|
2154
|
-
|
|
2826
|
+
const text = readFileSync22(rule.path, "utf-8");
|
|
2827
|
+
contents.push(`## ${name}
|
|
2828
|
+
|
|
2829
|
+
${text}`);
|
|
2830
|
+
_loadedPaths.add(rule.path);
|
|
2831
|
+
loaded.push(name);
|
|
2155
2832
|
} catch {
|
|
2156
|
-
|
|
2833
|
+
loaded.push(`${name} (read error)`);
|
|
2157
2834
|
}
|
|
2158
2835
|
}
|
|
2836
|
+
const skippedNoMatch = selection.skipped.map((r) => ({
|
|
2837
|
+
name: ruleShortName(r),
|
|
2838
|
+
reason: selection.reasons[r.path]
|
|
2839
|
+
}));
|
|
2840
|
+
return JSON.stringify({
|
|
2841
|
+
loaded,
|
|
2842
|
+
skipped_already_loaded: skippedAlreadyLoaded,
|
|
2843
|
+
skipped_no_match: skippedNoMatch,
|
|
2844
|
+
total_available: selection.total_discovered,
|
|
2845
|
+
diagnostics,
|
|
2846
|
+
content: contents.join(`
|
|
2847
|
+
|
|
2848
|
+
---
|
|
2849
|
+
|
|
2850
|
+
`)
|
|
2851
|
+
});
|
|
2852
|
+
}
|
|
2853
|
+
});
|
|
2854
|
+
function ruleShortName(rule) {
|
|
2855
|
+
return rule.path.replace(RULES_DIR + "/", "").replace(/\.md$/, "");
|
|
2856
|
+
}
|
|
2857
|
+
var listRulesTool = tool17({
|
|
2858
|
+
description: "List all available FlowDeck rule modules with their metadata (description, always_on, " + "stages, languages). Use this before calling load-rules to see what is available. " + "Does NOT load rule content — only returns metadata for discovery.",
|
|
2859
|
+
args: {},
|
|
2860
|
+
async execute() {
|
|
2861
|
+
const rulesDir = existsSync22(RULES_DIR) ? RULES_DIR : null;
|
|
2862
|
+
if (!rulesDir) {
|
|
2863
|
+
return JSON.stringify({ rules: [], error: `Rules directory not found at ${RULES_DIR}` });
|
|
2864
|
+
}
|
|
2865
|
+
const all = discoverRules(rulesDir);
|
|
2866
|
+
return JSON.stringify({
|
|
2867
|
+
total: all.length,
|
|
2868
|
+
rules: all.map((r) => ({
|
|
2869
|
+
name: r.path.replace(RULES_DIR + "/", "").replace(/\.md$/, ""),
|
|
2870
|
+
description: r.description,
|
|
2871
|
+
always_on: r.always_on,
|
|
2872
|
+
stages: r.stages,
|
|
2873
|
+
languages: r.languages,
|
|
2874
|
+
loaded: _loadedPaths.has(r.path)
|
|
2875
|
+
}))
|
|
2876
|
+
});
|
|
2877
|
+
}
|
|
2878
|
+
});
|
|
2879
|
+
|
|
2880
|
+
// src/tools/rtk-setup.ts
|
|
2881
|
+
import { tool as tool18 } from "@opencode-ai/plugin";
|
|
2882
|
+
|
|
2883
|
+
// src/services/rtk-manager.ts
|
|
2884
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
2885
|
+
import { existsSync as existsSync23 } from "fs";
|
|
2886
|
+
import { homedir as homedir2 } from "os";
|
|
2887
|
+
import { join as join22 } from "path";
|
|
2888
|
+
|
|
2889
|
+
// src/services/rtk-policy.ts
|
|
2890
|
+
var SUPPORTED_COMMANDS = new Set([
|
|
2891
|
+
"git",
|
|
2892
|
+
"npm",
|
|
2893
|
+
"npx",
|
|
2894
|
+
"bun",
|
|
2895
|
+
"pnpm",
|
|
2896
|
+
"yarn",
|
|
2897
|
+
"tsc",
|
|
2898
|
+
"eslint",
|
|
2899
|
+
"biome",
|
|
2900
|
+
"oxlint",
|
|
2901
|
+
"jest",
|
|
2902
|
+
"vitest",
|
|
2903
|
+
"pytest",
|
|
2904
|
+
"cargo",
|
|
2905
|
+
"docker",
|
|
2906
|
+
"kubectl",
|
|
2907
|
+
"gh"
|
|
2908
|
+
]);
|
|
2909
|
+
var COMPACT_GIT_SUBCOMMANDS = new Set([
|
|
2910
|
+
"rev-parse",
|
|
2911
|
+
"hash-object",
|
|
2912
|
+
"cat-file",
|
|
2913
|
+
"ls-files",
|
|
2914
|
+
"ls-tree",
|
|
2915
|
+
"show-ref",
|
|
2916
|
+
"for-each-ref",
|
|
2917
|
+
"symbolic-ref",
|
|
2918
|
+
"config"
|
|
2919
|
+
]);
|
|
2920
|
+
var NEVER_WRAP = new Set(["codegraph", "curl", "sh", "bash", "zsh", "fish", "node", "python", "python3"]);
|
|
2921
|
+
function getSupportedCommands() {
|
|
2922
|
+
return [...SUPPORTED_COMMANDS].sort();
|
|
2923
|
+
}
|
|
2924
|
+
|
|
2925
|
+
// src/services/rtk-manager.ts
|
|
2926
|
+
var INSTALL_INSTRUCTIONS = [
|
|
2927
|
+
"rtk is not installed. To install it manually:",
|
|
2928
|
+
" Linux/macOS: curl -fsSL https://raw.githubusercontent.com/rtk-ai/rtk/refs/heads/master/install.sh | sh",
|
|
2929
|
+
" Then add ~/.local/bin to your PATH if needed.",
|
|
2930
|
+
"After installation, call rtk-setup again to verify detection."
|
|
2931
|
+
].join(`
|
|
2932
|
+
`);
|
|
2933
|
+
var CANDIDATE_PATHS = [join22(homedir2(), ".local", "bin", "rtk"), "/usr/local/bin/rtk", "/usr/bin/rtk"];
|
|
2934
|
+
function detectRtk() {
|
|
2935
|
+
const fromPath = spawnSync2("rtk", ["--version"], { encoding: "utf-8", timeout: 5000 });
|
|
2936
|
+
if (fromPath.status === 0) {
|
|
2937
|
+
const version = (fromPath.stdout ?? "").trim().split(`
|
|
2938
|
+
`)[0] ?? "";
|
|
2939
|
+
return { installed: true, binPath: "rtk", version };
|
|
2940
|
+
}
|
|
2941
|
+
for (const candidate of CANDIDATE_PATHS) {
|
|
2942
|
+
if (!existsSync23(candidate))
|
|
2943
|
+
continue;
|
|
2944
|
+
const result = spawnSync2(candidate, ["--version"], { encoding: "utf-8", timeout: 5000 });
|
|
2945
|
+
if (result.status === 0) {
|
|
2946
|
+
const version = (result.stdout ?? "").trim().split(`
|
|
2947
|
+
`)[0] ?? "";
|
|
2948
|
+
return { installed: true, binPath: candidate, version };
|
|
2949
|
+
}
|
|
2159
2950
|
}
|
|
2160
|
-
return {};
|
|
2161
|
-
}
|
|
2162
|
-
function resolveDesignFirstConfig(config) {
|
|
2163
2951
|
return {
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2952
|
+
installed: false,
|
|
2953
|
+
error: "rtk binary not found in PATH or known install locations"
|
|
2954
|
+
};
|
|
2955
|
+
}
|
|
2956
|
+
function initRtk(binPath) {
|
|
2957
|
+
try {
|
|
2958
|
+
const result = spawnSync2(binPath, ["init", "-g"], {
|
|
2959
|
+
encoding: "utf-8",
|
|
2960
|
+
timeout: 30000,
|
|
2961
|
+
stdio: "pipe"
|
|
2962
|
+
});
|
|
2963
|
+
if (result.status !== 0) {
|
|
2964
|
+
return {
|
|
2965
|
+
success: false,
|
|
2966
|
+
log: (result.stdout ?? "").trim(),
|
|
2967
|
+
telemetryDisabled: false,
|
|
2968
|
+
error: (result.stderr ?? "").trim() || `rtk init -g exited with code ${result.status}`
|
|
2969
|
+
};
|
|
2174
2970
|
}
|
|
2971
|
+
const telResult = spawnSync2(binPath, ["telemetry", "disable"], {
|
|
2972
|
+
encoding: "utf-8",
|
|
2973
|
+
timeout: 1e4,
|
|
2974
|
+
stdio: "pipe"
|
|
2975
|
+
});
|
|
2976
|
+
return {
|
|
2977
|
+
success: true,
|
|
2978
|
+
log: [
|
|
2979
|
+
`[rtk] init -g succeeded: ${(result.stdout ?? "").trim()}`,
|
|
2980
|
+
`[rtk] telemetry disable: ${telResult.status === 0 ? "ok" : `failed (code ${telResult.status}) — ${(telResult.stderr ?? "").trim()}`}`
|
|
2981
|
+
].filter(Boolean).join(`
|
|
2982
|
+
`),
|
|
2983
|
+
telemetryDisabled: telResult.status === 0
|
|
2984
|
+
};
|
|
2985
|
+
} catch (err) {
|
|
2986
|
+
return { success: false, log: "", telemetryDisabled: false, error: String(err) };
|
|
2987
|
+
}
|
|
2988
|
+
}
|
|
2989
|
+
function getRtkStatus(opts) {
|
|
2990
|
+
const detection = detectRtk();
|
|
2991
|
+
if (!detection.installed) {
|
|
2992
|
+
return {
|
|
2993
|
+
installed: false,
|
|
2994
|
+
initAttempted: false,
|
|
2995
|
+
initSuccess: false,
|
|
2996
|
+
telemetryDisabled: false,
|
|
2997
|
+
installInstructions: INSTALL_INSTRUCTIONS
|
|
2998
|
+
};
|
|
2999
|
+
}
|
|
3000
|
+
let initAttempted = false;
|
|
3001
|
+
let initSuccess = false;
|
|
3002
|
+
let telemetryDisabled = false;
|
|
3003
|
+
if (opts?.runInit && detection.binPath) {
|
|
3004
|
+
initAttempted = true;
|
|
3005
|
+
const initResult = initRtk(detection.binPath);
|
|
3006
|
+
initSuccess = initResult.success;
|
|
3007
|
+
telemetryDisabled = initResult.telemetryDisabled;
|
|
3008
|
+
}
|
|
3009
|
+
return {
|
|
3010
|
+
installed: true,
|
|
3011
|
+
binPath: detection.binPath,
|
|
3012
|
+
version: detection.version,
|
|
3013
|
+
initAttempted,
|
|
3014
|
+
initSuccess,
|
|
3015
|
+
telemetryDisabled
|
|
2175
3016
|
};
|
|
2176
3017
|
}
|
|
3018
|
+
|
|
3019
|
+
// src/tools/rtk-setup.ts
|
|
3020
|
+
var rtkSetupTool = tool18({
|
|
3021
|
+
description: [
|
|
3022
|
+
"Detect, initialize, and report status of rtk (output compression proxy for CLI commands).",
|
|
3023
|
+
"rtk reduces noisy CLI output (git, npm, test runners, linters, docker) by 60-90%.",
|
|
3024
|
+
"Call this to check if rtk is available, to run `rtk init -g`, or to get the binary path.",
|
|
3025
|
+
"When RTK_INSTALLED=true in the environment, use `$RTK_BIN git status` for compressed output."
|
|
3026
|
+
].join(" "),
|
|
3027
|
+
args: {
|
|
3028
|
+
action: tool18.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.")
|
|
3029
|
+
},
|
|
3030
|
+
async execute(args) {
|
|
3031
|
+
const action = args.action ?? "status";
|
|
3032
|
+
const runInit = action === "init";
|
|
3033
|
+
const status = getRtkStatus({ runInit });
|
|
3034
|
+
const lines = ["## rtk Status"];
|
|
3035
|
+
if (status.installed) {
|
|
3036
|
+
lines.push(`- **Installed**: yes`);
|
|
3037
|
+
lines.push(`- **Binary**: ${status.binPath ?? "rtk (in PATH)"}`);
|
|
3038
|
+
if (status.version)
|
|
3039
|
+
lines.push(`- **Version**: ${status.version}`);
|
|
3040
|
+
if (runInit) {
|
|
3041
|
+
if (status.initAttempted) {
|
|
3042
|
+
lines.push(`- **Init**: ${status.initSuccess ? "✓ succeeded (bash hook installed)" : "✗ failed"}`);
|
|
3043
|
+
lines.push(`- **Telemetry**: ${status.telemetryDisabled ? "✓ disabled (`rtk telemetry disable` ran)" : "⚠ disable step failed — run `rtk telemetry disable` manually"}`);
|
|
3044
|
+
if (status.initSuccess) {
|
|
3045
|
+
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.");
|
|
3046
|
+
}
|
|
3047
|
+
}
|
|
3048
|
+
} else {
|
|
3049
|
+
lines.push("- **Init**: not requested (pass `action: 'init'` to install bash hook)");
|
|
3050
|
+
lines.push("- **Telemetry**: `RTK_TELEMETRY_DISABLED=1` is always set in bash sessions by FlowDeck");
|
|
3051
|
+
}
|
|
3052
|
+
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(`
|
|
3053
|
+
`));
|
|
3054
|
+
} else {
|
|
3055
|
+
lines.push("- **Installed**: no", "");
|
|
3056
|
+
lines.push("### Install rtk");
|
|
3057
|
+
if (status.installInstructions) {
|
|
3058
|
+
lines.push("```", status.installInstructions, "```");
|
|
3059
|
+
}
|
|
3060
|
+
lines.push("", "After installing, call `rtk-setup` again to verify detection.");
|
|
3061
|
+
}
|
|
3062
|
+
return lines.join(`
|
|
3063
|
+
`);
|
|
3064
|
+
}
|
|
3065
|
+
});
|
|
3066
|
+
|
|
2177
3067
|
// src/hooks/guard-rails.ts
|
|
3068
|
+
import { existsSync as existsSync24, readFileSync as readFileSync23 } from "fs";
|
|
3069
|
+
import { join as join23 } from "path";
|
|
2178
3070
|
var PLANNING_DIR2 = ".planning";
|
|
2179
3071
|
var CONFIG_FILE = "config.json";
|
|
2180
3072
|
var STATE_FILE2 = "STATE.md";
|
|
2181
3073
|
function resolveExecutionMode(configPath, trustScore, volatility) {
|
|
2182
|
-
if (
|
|
3074
|
+
if (existsSync24(configPath)) {
|
|
2183
3075
|
try {
|
|
2184
|
-
const config = JSON.parse(
|
|
3076
|
+
const config = JSON.parse(readFileSync23(configPath, "utf-8"));
|
|
2185
3077
|
if (config.execution_mode === "review-only")
|
|
2186
3078
|
return "review-only";
|
|
2187
3079
|
if (config.execution_mode === "guarded")
|
|
@@ -2235,22 +3127,22 @@ async function guardRailsHook(ctx, input, _output) {
|
|
|
2235
3127
|
if (!ENABLED)
|
|
2236
3128
|
return;
|
|
2237
3129
|
const dir = ctx.directory;
|
|
2238
|
-
const planningDirPath =
|
|
3130
|
+
const planningDirPath = join23(dir, PLANNING_DIR2);
|
|
2239
3131
|
const codebaseDirectory = codebaseDir(dir);
|
|
2240
|
-
const configPath =
|
|
2241
|
-
const statePath2 =
|
|
3132
|
+
const configPath = join23(planningDirPath, CONFIG_FILE);
|
|
3133
|
+
const statePath2 = join23(planningDirPath, STATE_FILE2);
|
|
2242
3134
|
const workspaceRoot = findWorkspaceRoot(dir);
|
|
2243
3135
|
if (workspaceRoot && dir !== workspaceRoot) {
|
|
2244
3136
|
const config = getWorkspaceConfig(dir);
|
|
2245
|
-
if (config && config.workspace_mode === "shared" && !
|
|
3137
|
+
if (config && config.workspace_mode === "shared" && !existsSync24(planningDirPath)) {
|
|
2246
3138
|
const msg = `No .planning/ in this sub-repo. Switch to workspace root: cd ${workspaceRoot}`;
|
|
2247
3139
|
throw new Error(`[flowdeck] BLOCK: ${msg}`);
|
|
2248
3140
|
}
|
|
2249
3141
|
}
|
|
2250
3142
|
if (input.tool === "write" || input.tool === "edit") {
|
|
2251
|
-
if (!
|
|
3143
|
+
if (!existsSync24(planningDirPath))
|
|
2252
3144
|
return;
|
|
2253
|
-
if (!
|
|
3145
|
+
if (!existsSync24(codebaseDirectory)) {
|
|
2254
3146
|
throw new Error(`[flowdeck] WARNING: .codebase/ not found. Run /fd-map-codebase to map the codebase.`);
|
|
2255
3147
|
}
|
|
2256
3148
|
const execMode = resolveExecutionMode(configPath, null);
|
|
@@ -2306,15 +3198,15 @@ function getDesignGateMessage(dir) {
|
|
|
2306
3198
|
}
|
|
2307
3199
|
function planSuggestsUiHeavy(dir, phase) {
|
|
2308
3200
|
const planPath = phasePlanPath(dir, phase);
|
|
2309
|
-
if (!
|
|
3201
|
+
if (!existsSync24(planPath))
|
|
2310
3202
|
return false;
|
|
2311
|
-
const planContent =
|
|
3203
|
+
const planContent = readFileSync23(planPath, "utf-8");
|
|
2312
3204
|
return isUiHeavyTask(planContent);
|
|
2313
3205
|
}
|
|
2314
3206
|
function effectiveSeverity(configPath, statePath2) {
|
|
2315
|
-
if (
|
|
3207
|
+
if (existsSync24(configPath)) {
|
|
2316
3208
|
try {
|
|
2317
|
-
const configContent =
|
|
3209
|
+
const configContent = readFileSync23(configPath, "utf-8");
|
|
2318
3210
|
const config = JSON.parse(configContent);
|
|
2319
3211
|
if (config.guard_enforcement === "warn")
|
|
2320
3212
|
return "warn";
|
|
@@ -2330,10 +3222,10 @@ function getEffectiveSeverity(configPath, statePath2) {
|
|
|
2330
3222
|
return effectiveSeverity(configPath, statePath2);
|
|
2331
3223
|
}
|
|
2332
3224
|
function getPlanConfirmed(statePath2) {
|
|
2333
|
-
if (!
|
|
3225
|
+
if (!existsSync24(statePath2))
|
|
2334
3226
|
return false;
|
|
2335
3227
|
try {
|
|
2336
|
-
const content =
|
|
3228
|
+
const content = readFileSync23(statePath2, "utf-8");
|
|
2337
3229
|
const match = content.match(/plan_confirmed:\s*(true|false)/i);
|
|
2338
3230
|
return match ? match[1].toLowerCase() === "true" : false;
|
|
2339
3231
|
} catch {
|
|
@@ -2341,32 +3233,32 @@ function getPlanConfirmed(statePath2) {
|
|
|
2341
3233
|
}
|
|
2342
3234
|
}
|
|
2343
3235
|
function getWarningMessage(planningDir2) {
|
|
2344
|
-
if (!
|
|
3236
|
+
if (!existsSync24(join23(planningDir2, STATE_FILE2))) {
|
|
2345
3237
|
return "No STATE.md found. Run /fd-map-codebase then /fd-new-feature to start a feature.";
|
|
2346
3238
|
}
|
|
2347
3239
|
return "Plan not confirmed. Run /fd-plan and confirm to enable execution.";
|
|
2348
3240
|
}
|
|
2349
3241
|
function getBlockMessage(planningDir2) {
|
|
2350
|
-
if (!
|
|
3242
|
+
if (!existsSync24(join23(planningDir2, STATE_FILE2))) {
|
|
2351
3243
|
return "No STATE.md found. Run /fd-map-codebase then /fd-new-feature to start a feature.";
|
|
2352
3244
|
}
|
|
2353
3245
|
return "Plan not confirmed. Run /fd-plan and confirm to enable execution.";
|
|
2354
3246
|
}
|
|
2355
3247
|
|
|
2356
3248
|
// src/hooks/tool-guard.ts
|
|
2357
|
-
import { existsSync as
|
|
2358
|
-
import { join as
|
|
3249
|
+
import { existsSync as existsSync25, readFileSync as readFileSync24 } from "fs";
|
|
3250
|
+
import { join as join24 } from "path";
|
|
2359
3251
|
var IS_ENABLED = () => process.env.FLOWDECK_TOOL_GUARD_ENABLED === "on";
|
|
2360
3252
|
var BLOCKED_PATTERNS = {
|
|
2361
3253
|
read: [".env", ".pem", ".key", ".secret"],
|
|
2362
3254
|
write: ["node_modules"],
|
|
2363
3255
|
bash: ["rm -rf"]
|
|
2364
3256
|
};
|
|
2365
|
-
function isBlocked(
|
|
2366
|
-
const patterns = BLOCKED_PATTERNS[
|
|
3257
|
+
function isBlocked(tool19, args) {
|
|
3258
|
+
const patterns = BLOCKED_PATTERNS[tool19];
|
|
2367
3259
|
if (!patterns)
|
|
2368
3260
|
return null;
|
|
2369
|
-
if (
|
|
3261
|
+
if (tool19 === "bash") {
|
|
2370
3262
|
const cmd = args.command;
|
|
2371
3263
|
if (!cmd)
|
|
2372
3264
|
return null;
|
|
@@ -2377,7 +3269,7 @@ function isBlocked(tool17, args) {
|
|
|
2377
3269
|
}
|
|
2378
3270
|
return null;
|
|
2379
3271
|
}
|
|
2380
|
-
if (
|
|
3272
|
+
if (tool19 === "read") {
|
|
2381
3273
|
const filePath = args.filePath;
|
|
2382
3274
|
if (!filePath)
|
|
2383
3275
|
return null;
|
|
@@ -2388,7 +3280,7 @@ function isBlocked(tool17, args) {
|
|
|
2388
3280
|
}
|
|
2389
3281
|
return null;
|
|
2390
3282
|
}
|
|
2391
|
-
if (
|
|
3283
|
+
if (tool19 === "write") {
|
|
2392
3284
|
const filePath = args.filePath;
|
|
2393
3285
|
if (!filePath)
|
|
2394
3286
|
return null;
|
|
@@ -2402,11 +3294,11 @@ function isBlocked(tool17, args) {
|
|
|
2402
3294
|
return null;
|
|
2403
3295
|
}
|
|
2404
3296
|
function checkArchConstraint(directory, filePath) {
|
|
2405
|
-
const constraintsPath =
|
|
2406
|
-
if (!
|
|
3297
|
+
const constraintsPath = join24(codebaseDir(directory), "CONSTRAINTS.md");
|
|
3298
|
+
if (!existsSync25(constraintsPath))
|
|
2407
3299
|
return null;
|
|
2408
3300
|
try {
|
|
2409
|
-
const content =
|
|
3301
|
+
const content = readFileSync24(constraintsPath, "utf-8");
|
|
2410
3302
|
const match = content.match(/## Forbidden Paths\n([\s\S]*?)(?:\n##|$)/);
|
|
2411
3303
|
if (!match)
|
|
2412
3304
|
return null;
|
|
@@ -2447,9 +3339,9 @@ function isUiDesignApprovalRequired(directory) {
|
|
|
2447
3339
|
return !(state.design_stage === "handoff_complete" && state.design_approved);
|
|
2448
3340
|
}
|
|
2449
3341
|
const planPath = phasePlanPath(directory, state.phase || 1);
|
|
2450
|
-
if (!
|
|
3342
|
+
if (!existsSync25(planPath))
|
|
2451
3343
|
return false;
|
|
2452
|
-
const planContent =
|
|
3344
|
+
const planContent = readFileSync24(planPath, "utf-8");
|
|
2453
3345
|
if (!isUiHeavyTask(planContent))
|
|
2454
3346
|
return false;
|
|
2455
3347
|
return !(state.design_stage === "handoff_complete" && state.design_approved);
|
|
@@ -2478,18 +3370,18 @@ async function toolGuardHook(ctx, input, output) {
|
|
|
2478
3370
|
}
|
|
2479
3371
|
|
|
2480
3372
|
// src/hooks/session-start.ts
|
|
2481
|
-
import { existsSync as
|
|
3373
|
+
import { existsSync as existsSync26, readFileSync as readFileSync25 } from "fs";
|
|
2482
3374
|
async function sessionStartHook(ctx) {
|
|
2483
3375
|
const planningDir2 = ctx.directory + "/.planning";
|
|
2484
3376
|
const codebaseDirectory = codebaseDir(ctx.directory);
|
|
2485
3377
|
const workspaceRoot = findWorkspaceRoot(ctx.directory);
|
|
2486
3378
|
const config = workspaceRoot ? getWorkspaceConfig(ctx.directory) : null;
|
|
2487
|
-
if (!
|
|
3379
|
+
if (!existsSync26(planningDir2)) {
|
|
2488
3380
|
return {
|
|
2489
3381
|
flowdeck_phase: null,
|
|
2490
3382
|
flowdeck_status: "no_plan",
|
|
2491
3383
|
flowdeck_warning: "Run /fd-map-codebase to index the codebase, then /fd-new-feature to start a feature.",
|
|
2492
|
-
flowdeck_has_codebase:
|
|
3384
|
+
flowdeck_has_codebase: existsSync26(codebaseDirectory),
|
|
2493
3385
|
...workspaceRoot && config?.sub_repos ? {
|
|
2494
3386
|
flowdeck_workspace_root: workspaceRoot,
|
|
2495
3387
|
flowdeck_sub_repos: config.sub_repos,
|
|
@@ -2500,7 +3392,7 @@ async function sessionStartHook(ctx) {
|
|
|
2500
3392
|
}
|
|
2501
3393
|
try {
|
|
2502
3394
|
const stateFilePath = statePath(ctx.directory);
|
|
2503
|
-
const content =
|
|
3395
|
+
const content = readFileSync25(stateFilePath, "utf-8");
|
|
2504
3396
|
const state = parseState(content);
|
|
2505
3397
|
const currentPhase = state["current_phase"] || {};
|
|
2506
3398
|
const result = {
|
|
@@ -2508,7 +3400,7 @@ async function sessionStartHook(ctx) {
|
|
|
2508
3400
|
flowdeck_status: currentPhase["status"] ?? null,
|
|
2509
3401
|
flowdeck_steps_pending: currentPhase["steps_pending"] ?? null,
|
|
2510
3402
|
flowdeck_last_action: currentPhase["last_action"] ?? null,
|
|
2511
|
-
flowdeck_has_codebase:
|
|
3403
|
+
flowdeck_has_codebase: existsSync26(codebaseDirectory)
|
|
2512
3404
|
};
|
|
2513
3405
|
if (workspaceRoot && config?.sub_repos && config.sub_repos.length > 0) {
|
|
2514
3406
|
result.flowdeck_workspace_root = workspaceRoot;
|
|
@@ -2523,7 +3415,7 @@ async function sessionStartHook(ctx) {
|
|
|
2523
3415
|
flowdeck_phase: null,
|
|
2524
3416
|
flowdeck_status: "error",
|
|
2525
3417
|
flowdeck_warning: "State file unreadable. Continuing without flowdeck context.",
|
|
2526
|
-
flowdeck_has_codebase:
|
|
3418
|
+
flowdeck_has_codebase: existsSync26(codebaseDirectory)
|
|
2527
3419
|
};
|
|
2528
3420
|
if (workspaceRoot && config?.sub_repos && config.sub_repos.length > 0) {
|
|
2529
3421
|
result.flowdeck_workspace_root = workspaceRoot;
|
|
@@ -2662,13 +3554,13 @@ class NotificationController {
|
|
|
2662
3554
|
return this.lastNotifiedKey;
|
|
2663
3555
|
}
|
|
2664
3556
|
}
|
|
2665
|
-
function notifyPermissionNeeded(
|
|
2666
|
-
notify("FlowDeck Permission Required", `Agent needs approval to use tool: ${
|
|
3557
|
+
function notifyPermissionNeeded(tool19) {
|
|
3558
|
+
notify("FlowDeck Permission Required", `Agent needs approval to use tool: ${tool19}`, "critical");
|
|
2667
3559
|
}
|
|
2668
3560
|
|
|
2669
3561
|
// src/hooks/patch-trust.ts
|
|
2670
|
-
import { existsSync as
|
|
2671
|
-
import { join as
|
|
3562
|
+
import { existsSync as existsSync27, readFileSync as readFileSync26 } from "fs";
|
|
3563
|
+
import { join as join25 } from "path";
|
|
2672
3564
|
var HIGH_RISK_KEYWORDS = [
|
|
2673
3565
|
"password",
|
|
2674
3566
|
"secret",
|
|
@@ -2690,11 +3582,11 @@ var HIGH_RISK_KEYWORDS = [
|
|
|
2690
3582
|
"privilege"
|
|
2691
3583
|
];
|
|
2692
3584
|
function loadVolatility(directory) {
|
|
2693
|
-
const p =
|
|
2694
|
-
if (!
|
|
3585
|
+
const p = join25(codebaseDir(directory), "VOLATILITY.json");
|
|
3586
|
+
if (!existsSync27(p))
|
|
2695
3587
|
return {};
|
|
2696
3588
|
try {
|
|
2697
|
-
const data = JSON.parse(
|
|
3589
|
+
const data = JSON.parse(readFileSync26(p, "utf-8"));
|
|
2698
3590
|
const map = {};
|
|
2699
3591
|
for (const entry of data.entries ?? [])
|
|
2700
3592
|
map[entry.path] = entry.stability;
|
|
@@ -2704,11 +3596,11 @@ function loadVolatility(directory) {
|
|
|
2704
3596
|
}
|
|
2705
3597
|
}
|
|
2706
3598
|
function loadFailedPaths(directory) {
|
|
2707
|
-
const p =
|
|
2708
|
-
if (!
|
|
3599
|
+
const p = join25(codebaseDir(directory), "FAILURES.json");
|
|
3600
|
+
if (!existsSync27(p))
|
|
2709
3601
|
return [];
|
|
2710
3602
|
try {
|
|
2711
|
-
const data = JSON.parse(
|
|
3603
|
+
const data = JSON.parse(readFileSync26(p, "utf-8"));
|
|
2712
3604
|
return (data.entries ?? []).flatMap((e) => e.affected_paths ?? []);
|
|
2713
3605
|
} catch {
|
|
2714
3606
|
return [];
|
|
@@ -2773,8 +3665,8 @@ async function patchTrustHook(ctx, input, output) {
|
|
|
2773
3665
|
}
|
|
2774
3666
|
|
|
2775
3667
|
// src/hooks/decision-trace-hook.ts
|
|
2776
|
-
import { existsSync as
|
|
2777
|
-
import { join as
|
|
3668
|
+
import { existsSync as existsSync28, mkdirSync as mkdirSync14, appendFileSync as appendFileSync4 } from "fs";
|
|
3669
|
+
import { join as join26 } from "path";
|
|
2778
3670
|
async function decisionTraceHook(ctx, input, output) {
|
|
2779
3671
|
if (input.tool !== "write" && input.tool !== "edit")
|
|
2780
3672
|
return;
|
|
@@ -2783,8 +3675,8 @@ async function decisionTraceHook(ctx, input, output) {
|
|
|
2783
3675
|
return;
|
|
2784
3676
|
const base = codebaseDir(ctx.directory);
|
|
2785
3677
|
try {
|
|
2786
|
-
if (!
|
|
2787
|
-
|
|
3678
|
+
if (!existsSync28(base))
|
|
3679
|
+
mkdirSync14(base, { recursive: true });
|
|
2788
3680
|
const entry = {
|
|
2789
3681
|
timestamp: new Date().toISOString(),
|
|
2790
3682
|
file_path: filePath,
|
|
@@ -2796,30 +3688,30 @@ async function decisionTraceHook(ctx, input, output) {
|
|
|
2796
3688
|
risk_level: "unknown",
|
|
2797
3689
|
auto_recorded: true
|
|
2798
3690
|
};
|
|
2799
|
-
|
|
3691
|
+
appendFileSync4(join26(base, "DECISIONS.jsonl"), JSON.stringify(entry) + `
|
|
2800
3692
|
`, "utf-8");
|
|
2801
3693
|
} catch {}
|
|
2802
3694
|
}
|
|
2803
3695
|
|
|
2804
3696
|
// src/services/telemetry.ts
|
|
2805
|
-
import { existsSync as
|
|
2806
|
-
import { join as
|
|
3697
|
+
import { existsSync as existsSync29, readFileSync as readFileSync27, appendFileSync as appendFileSync5, mkdirSync as mkdirSync15 } from "fs";
|
|
3698
|
+
import { join as join27 } from "path";
|
|
2807
3699
|
import { randomUUID } from "crypto";
|
|
2808
3700
|
function telemetryPath(dir) {
|
|
2809
|
-
return
|
|
3701
|
+
return join27(codebaseDir(dir), "TELEMETRY.jsonl");
|
|
2810
3702
|
}
|
|
2811
|
-
function
|
|
3703
|
+
function appendEvent2(dir, partial) {
|
|
2812
3704
|
if (process.env.TELEMETRY_ENABLED !== "true")
|
|
2813
3705
|
return null;
|
|
2814
3706
|
const cd = codebaseDir(dir);
|
|
2815
|
-
if (!
|
|
2816
|
-
|
|
3707
|
+
if (!existsSync29(cd))
|
|
3708
|
+
mkdirSync15(cd, { recursive: true });
|
|
2817
3709
|
const event = {
|
|
2818
3710
|
id: randomUUID(),
|
|
2819
3711
|
ts: new Date().toISOString(),
|
|
2820
3712
|
...partial
|
|
2821
3713
|
};
|
|
2822
|
-
|
|
3714
|
+
appendFileSync5(telemetryPath(dir), JSON.stringify(event) + `
|
|
2823
3715
|
`, "utf-8");
|
|
2824
3716
|
return event;
|
|
2825
3717
|
}
|
|
@@ -2849,34 +3741,34 @@ function inferStatus(output) {
|
|
|
2849
3741
|
}
|
|
2850
3742
|
async function telemetryHook(context, toolInput, output) {
|
|
2851
3743
|
const dir = context.directory ?? process.cwd();
|
|
2852
|
-
const
|
|
3744
|
+
const tool19 = toolInput.name ?? toolInput.tool ?? "unknown";
|
|
2853
3745
|
const ids = resolveIds(toolInput);
|
|
2854
|
-
|
|
3746
|
+
appendEvent2(dir, {
|
|
2855
3747
|
session_id: ids.session_id,
|
|
2856
3748
|
run_id: ids.run_id,
|
|
2857
3749
|
event: "tool.call",
|
|
2858
|
-
tool:
|
|
3750
|
+
tool: tool19,
|
|
2859
3751
|
status: "ok",
|
|
2860
3752
|
meta: { parameters: output.args ?? {} }
|
|
2861
3753
|
});
|
|
2862
3754
|
}
|
|
2863
3755
|
async function telemetryAfterHook(context, toolInput, output) {
|
|
2864
3756
|
const dir = context.directory ?? process.cwd();
|
|
2865
|
-
const
|
|
3757
|
+
const tool19 = toolInput.name ?? toolInput.tool ?? "unknown";
|
|
2866
3758
|
const ids = resolveIds(toolInput);
|
|
2867
3759
|
const status = inferStatus(output);
|
|
2868
|
-
|
|
3760
|
+
appendEvent2(dir, {
|
|
2869
3761
|
session_id: ids.session_id,
|
|
2870
3762
|
run_id: ids.run_id,
|
|
2871
3763
|
event: "tool.complete",
|
|
2872
|
-
tool:
|
|
3764
|
+
tool: tool19,
|
|
2873
3765
|
status
|
|
2874
3766
|
});
|
|
2875
3767
|
}
|
|
2876
3768
|
|
|
2877
3769
|
// src/services/approval-manager.ts
|
|
2878
|
-
import { existsSync as
|
|
2879
|
-
import { join as
|
|
3770
|
+
import { existsSync as existsSync30, readFileSync as readFileSync28, writeFileSync as writeFileSync17, mkdirSync as mkdirSync16 } from "fs";
|
|
3771
|
+
import { join as join28 } from "path";
|
|
2880
3772
|
var APPROVAL_TTL_MS = 30 * 60 * 1000;
|
|
2881
3773
|
var SENSITIVE_PATTERNS = [
|
|
2882
3774
|
/auth/i,
|
|
@@ -2913,14 +3805,14 @@ function isSensitivePath(filePath) {
|
|
|
2913
3805
|
return SENSITIVE_PATTERNS.some((p) => p.test(filePath));
|
|
2914
3806
|
}
|
|
2915
3807
|
function approvalsPath(dir) {
|
|
2916
|
-
return
|
|
3808
|
+
return join28(codebaseDir(dir), "APPROVALS.json");
|
|
2917
3809
|
}
|
|
2918
3810
|
function loadStore2(dir) {
|
|
2919
3811
|
const p = approvalsPath(dir);
|
|
2920
|
-
if (!
|
|
3812
|
+
if (!existsSync30(p))
|
|
2921
3813
|
return { requests: [] };
|
|
2922
3814
|
try {
|
|
2923
|
-
return JSON.parse(
|
|
3815
|
+
return JSON.parse(readFileSync28(p, "utf-8"));
|
|
2924
3816
|
} catch {
|
|
2925
3817
|
return { requests: [] };
|
|
2926
3818
|
}
|
|
@@ -2938,8 +3830,8 @@ async function approvalHook(context, toolInput, output) {
|
|
|
2938
3830
|
if (!ENABLED2)
|
|
2939
3831
|
return;
|
|
2940
3832
|
const dir = context.directory ?? process.cwd();
|
|
2941
|
-
const
|
|
2942
|
-
if (!WRITE_TOOLS.has(
|
|
3833
|
+
const tool19 = toolInput.name ?? toolInput.tool ?? "";
|
|
3834
|
+
if (!WRITE_TOOLS.has(tool19))
|
|
2943
3835
|
return;
|
|
2944
3836
|
const args = output.args ?? {};
|
|
2945
3837
|
const filePath = String(args.path ?? args.file_path ?? args.filename ?? "");
|
|
@@ -2950,11 +3842,11 @@ async function approvalHook(context, toolInput, output) {
|
|
|
2950
3842
|
const approval = checkApproval(dir, filePath, "");
|
|
2951
3843
|
if (approval)
|
|
2952
3844
|
return;
|
|
2953
|
-
|
|
3845
|
+
appendEvent2(dir, {
|
|
2954
3846
|
session_id: process.env.OPENCODE_SESSION_ID ?? "session-0",
|
|
2955
3847
|
run_id: process.env.OPENCODE_RUN_ID ?? "run-0",
|
|
2956
3848
|
event: "approval.request",
|
|
2957
|
-
tool:
|
|
3849
|
+
tool: tool19,
|
|
2958
3850
|
status: "blocked",
|
|
2959
3851
|
files: [filePath],
|
|
2960
3852
|
meta: { trigger: "sensitive_file", file: filePath }
|
|
@@ -3015,15 +3907,15 @@ function createContextWindowMonitorHook() {
|
|
|
3015
3907
|
}
|
|
3016
3908
|
|
|
3017
3909
|
// src/hooks/shell-env-hook.ts
|
|
3018
|
-
import { existsSync as
|
|
3019
|
-
import { join as
|
|
3020
|
-
import { createRequire } from "module";
|
|
3910
|
+
import { existsSync as existsSync31, readFileSync as readFileSync29 } from "fs";
|
|
3911
|
+
import { join as join29 } from "path";
|
|
3912
|
+
import { createRequire as createRequire2 } from "module";
|
|
3021
3913
|
var _version;
|
|
3022
3914
|
function getVersion() {
|
|
3023
3915
|
if (_version)
|
|
3024
3916
|
return _version;
|
|
3025
3917
|
try {
|
|
3026
|
-
const require2 =
|
|
3918
|
+
const require2 = createRequire2(import.meta.url);
|
|
3027
3919
|
const pkg = require2("../../package.json");
|
|
3028
3920
|
_version = pkg.version ?? "0.0.0";
|
|
3029
3921
|
} catch {
|
|
@@ -3052,7 +3944,7 @@ var MARKER_TO_LANG = {
|
|
|
3052
3944
|
};
|
|
3053
3945
|
function detectPackageManager(root) {
|
|
3054
3946
|
for (const [lockfile, pm] of Object.entries(LOCKFILE_TO_PM)) {
|
|
3055
|
-
if (
|
|
3947
|
+
if (existsSync31(join29(root, lockfile)))
|
|
3056
3948
|
return pm;
|
|
3057
3949
|
}
|
|
3058
3950
|
return;
|
|
@@ -3061,7 +3953,7 @@ function detectLanguages(root) {
|
|
|
3061
3953
|
const langs = [];
|
|
3062
3954
|
const seen = new Set;
|
|
3063
3955
|
for (const [marker, lang] of Object.entries(MARKER_TO_LANG)) {
|
|
3064
|
-
if (!seen.has(lang) &&
|
|
3956
|
+
if (!seen.has(lang) && existsSync31(join29(root, marker))) {
|
|
3065
3957
|
langs.push(lang);
|
|
3066
3958
|
seen.add(lang);
|
|
3067
3959
|
}
|
|
@@ -3069,17 +3961,29 @@ function detectLanguages(root) {
|
|
|
3069
3961
|
return langs;
|
|
3070
3962
|
}
|
|
3071
3963
|
function readCurrentPhase(root) {
|
|
3072
|
-
const statePath2 =
|
|
3073
|
-
if (!
|
|
3964
|
+
const statePath2 = join29(root, ".planning", "STATE.md");
|
|
3965
|
+
if (!existsSync31(statePath2))
|
|
3074
3966
|
return;
|
|
3075
3967
|
try {
|
|
3076
|
-
const content =
|
|
3968
|
+
const content = readFileSync29(statePath2, "utf-8");
|
|
3077
3969
|
const match = content.match(/phase:\s*(\S+)/i);
|
|
3078
3970
|
return match?.[1];
|
|
3079
3971
|
} catch {
|
|
3080
3972
|
return;
|
|
3081
3973
|
}
|
|
3082
3974
|
}
|
|
3975
|
+
var _rtkDetection;
|
|
3976
|
+
function getRtkDetection() {
|
|
3977
|
+
if (_rtkDetection !== undefined)
|
|
3978
|
+
return _rtkDetection;
|
|
3979
|
+
try {
|
|
3980
|
+
const det = detectRtk();
|
|
3981
|
+
_rtkDetection = { installed: det.installed, binPath: det.binPath };
|
|
3982
|
+
} catch {
|
|
3983
|
+
_rtkDetection = { installed: false };
|
|
3984
|
+
}
|
|
3985
|
+
return _rtkDetection;
|
|
3986
|
+
}
|
|
3083
3987
|
function createShellEnvHook(ctx) {
|
|
3084
3988
|
const root = ctx.worktree || ctx.directory;
|
|
3085
3989
|
return async (_input, output) => {
|
|
@@ -3097,6 +4001,14 @@ function createShellEnvHook(ctx) {
|
|
|
3097
4001
|
const phase = readCurrentPhase(root);
|
|
3098
4002
|
if (phase)
|
|
3099
4003
|
output.env.FLOWDECK_PHASE = phase;
|
|
4004
|
+
const rtk = getRtkDetection();
|
|
4005
|
+
output.env.RTK_INSTALLED = rtk.installed ? "true" : "false";
|
|
4006
|
+
if (rtk.installed && rtk.binPath) {
|
|
4007
|
+
output.env.RTK_BIN = rtk.binPath;
|
|
4008
|
+
}
|
|
4009
|
+
if (rtk.installed) {
|
|
4010
|
+
output.env.RTK_TELEMETRY_DISABLED = "1";
|
|
4011
|
+
}
|
|
3100
4012
|
};
|
|
3101
4013
|
}
|
|
3102
4014
|
|
|
@@ -3178,8 +4090,8 @@ function createSessionIdleHook(client, tracker) {
|
|
|
3178
4090
|
}
|
|
3179
4091
|
|
|
3180
4092
|
// src/hooks/compaction-hook.ts
|
|
3181
|
-
import { existsSync as
|
|
3182
|
-
import { join as
|
|
4093
|
+
import { existsSync as existsSync32, readFileSync as readFileSync30 } from "fs";
|
|
4094
|
+
import { join as join30 } from "path";
|
|
3183
4095
|
var STRUCTURED_SUMMARY_PROMPT = `
|
|
3184
4096
|
When summarizing this session, you MUST include the following sections:
|
|
3185
4097
|
|
|
@@ -3217,40 +4129,58 @@ List all background agent tasks spawned this session.
|
|
|
3217
4129
|
For each: agent name, status, description, session_id.
|
|
3218
4130
|
**RESUME, DON'T RESTART.** Use session_id to continue existing sessions.
|
|
3219
4131
|
`;
|
|
4132
|
+
var _lastInjected = new Map;
|
|
3220
4133
|
function readPlanningState2(directory) {
|
|
3221
|
-
const
|
|
3222
|
-
if (!
|
|
4134
|
+
const sp = statePath(directory);
|
|
4135
|
+
if (!existsSync32(sp))
|
|
3223
4136
|
return null;
|
|
3224
4137
|
try {
|
|
3225
|
-
const content =
|
|
3226
|
-
|
|
4138
|
+
const content = readFileSync30(sp, "utf-8");
|
|
4139
|
+
const parsed = parseState(content);
|
|
4140
|
+
const version = typeof parsed.summaryVersion === "number" ? parsed.summaryVersion : 0;
|
|
4141
|
+
return { content: content.slice(0, 1500), version };
|
|
3227
4142
|
} catch {
|
|
3228
4143
|
return null;
|
|
3229
4144
|
}
|
|
3230
4145
|
}
|
|
3231
4146
|
function createCompactionHook(ctx, tracker) {
|
|
3232
|
-
return async (
|
|
4147
|
+
return async (input, output) => {
|
|
3233
4148
|
const sections = ["# FlowDeck Context (preserve across compaction)", ""];
|
|
3234
|
-
const
|
|
3235
|
-
|
|
4149
|
+
const stateData = readPlanningState2(ctx.directory);
|
|
4150
|
+
const indexData = readCodebaseIndex(ctx.directory);
|
|
4151
|
+
const currentStateVersion = stateData?.version ?? 0;
|
|
4152
|
+
const currentIndexVersion = indexData.summaryVersion ?? 0;
|
|
4153
|
+
const lastSnapshot = _lastInjected.get(input.sessionID);
|
|
4154
|
+
const stateChanged = !lastSnapshot || lastSnapshot.stateVersion !== currentStateVersion;
|
|
4155
|
+
const indexChanged = !lastSnapshot || lastSnapshot.indexVersion !== currentIndexVersion;
|
|
4156
|
+
if (stateChanged && stateData) {
|
|
3236
4157
|
sections.push("## Planning State");
|
|
3237
4158
|
sections.push("```");
|
|
3238
|
-
sections.push(
|
|
4159
|
+
sections.push(stateData.content.trim());
|
|
3239
4160
|
sections.push("```");
|
|
3240
4161
|
sections.push("");
|
|
4162
|
+
} else if (stateData) {
|
|
4163
|
+
sections.push(`## Planning State (unchanged, v${currentStateVersion})`);
|
|
4164
|
+
sections.push(`_State unchanged since last compaction. summaryVersion=${currentStateVersion}_`);
|
|
4165
|
+
sections.push("");
|
|
3241
4166
|
}
|
|
3242
|
-
const
|
|
3243
|
-
|
|
3244
|
-
if (existsSync25(indexPath)) {
|
|
4167
|
+
const indexPath2 = join30(ctx.directory, ".planning", "CODEBASE_INDEX.md");
|
|
4168
|
+
if (indexChanged && existsSync32(indexPath2)) {
|
|
3245
4169
|
try {
|
|
3246
|
-
const indexContent =
|
|
3247
|
-
indexSummary = "\n## Codebase Index\n```\n" + indexContent.slice(0, 800) + "\n```\n";
|
|
4170
|
+
const indexContent = readFileSync30(indexPath2, "utf-8");
|
|
4171
|
+
const indexSummary = "\n## Codebase Index\n```\n" + indexContent.slice(0, 800) + "\n```\n";
|
|
4172
|
+
sections.push(indexSummary);
|
|
4173
|
+
sections.push("");
|
|
3248
4174
|
} catch {}
|
|
3249
|
-
}
|
|
3250
|
-
|
|
3251
|
-
sections.push(
|
|
4175
|
+
} else if (existsSync32(indexPath2)) {
|
|
4176
|
+
sections.push(`## Codebase Index (unchanged, v${currentIndexVersion})`);
|
|
4177
|
+
sections.push(`_Index unchanged since last compaction. summaryVersion=${currentIndexVersion}_`);
|
|
3252
4178
|
sections.push("");
|
|
3253
4179
|
}
|
|
4180
|
+
_lastInjected.set(input.sessionID, {
|
|
4181
|
+
stateVersion: currentStateVersion,
|
|
4182
|
+
indexVersion: currentIndexVersion
|
|
4183
|
+
});
|
|
3254
4184
|
const edited = tracker.getEditedPaths();
|
|
3255
4185
|
if (edited.length > 0) {
|
|
3256
4186
|
sections.push("## Recently Edited Files");
|
|
@@ -3475,8 +4405,7 @@ function createFlowDeckMcps() {
|
|
|
3475
4405
|
if (!disabled.has("codegraph") && isCodegraphInstalled()) {
|
|
3476
4406
|
mcps.codegraph = {
|
|
3477
4407
|
type: "local",
|
|
3478
|
-
command: "codegraph",
|
|
3479
|
-
args: ["serve", "--mcp"],
|
|
4408
|
+
command: ["codegraph", "serve", "--mcp"],
|
|
3480
4409
|
enabled: true
|
|
3481
4410
|
};
|
|
3482
4411
|
}
|
|
@@ -7208,7 +8137,7 @@ function shouldProceed(decision, mode, canBlock) {
|
|
|
7208
8137
|
}
|
|
7209
8138
|
function _emitTelemetry(directory, decision, ctx) {
|
|
7210
8139
|
try {
|
|
7211
|
-
|
|
8140
|
+
appendEvent2(directory, {
|
|
7212
8141
|
session_id: ctx.session_id ?? "session-0",
|
|
7213
8142
|
run_id: ctx.run_id ?? "unknown",
|
|
7214
8143
|
event: "supervisor.review",
|
|
@@ -7229,37 +8158,29 @@ function _emitTelemetry(directory, decision, ctx) {
|
|
|
7229
8158
|
}
|
|
7230
8159
|
|
|
7231
8160
|
// src/index.ts
|
|
7232
|
-
function
|
|
7233
|
-
const __dir =
|
|
7234
|
-
const rulesDir =
|
|
7235
|
-
if (!
|
|
7236
|
-
return [];
|
|
7237
|
-
const
|
|
7238
|
-
|
|
7239
|
-
|
|
7240
|
-
|
|
7241
|
-
|
|
7242
|
-
walk(full);
|
|
7243
|
-
} else if (entry.isFile() && entry.name.endsWith(".md") && entry.name !== "README.md") {
|
|
7244
|
-
paths.push(full);
|
|
7245
|
-
}
|
|
7246
|
-
}
|
|
7247
|
-
}
|
|
7248
|
-
walk(rulesDir);
|
|
7249
|
-
return paths;
|
|
8161
|
+
function lazyLoadRulePaths(projectRoot) {
|
|
8162
|
+
const __dir = dirname5(fileURLToPath3(import.meta.url));
|
|
8163
|
+
const rulesDir = join31(__dir, "..", "src", "rules");
|
|
8164
|
+
if (!existsSync33(rulesDir))
|
|
8165
|
+
return { paths: [], diagnostics: "[LazyRuleLoader] rules directory not found" };
|
|
8166
|
+
const detectedLanguages = detectProjectLanguages(projectRoot);
|
|
8167
|
+
const paths = getStartupRulePaths(rulesDir, detectedLanguages);
|
|
8168
|
+
const selection = selectRulePaths(rulesDir, { languages: detectedLanguages });
|
|
8169
|
+
const diagnostics = buildSelectionDiagnostics(selection, { languages: detectedLanguages });
|
|
8170
|
+
return { paths, diagnostics };
|
|
7250
8171
|
}
|
|
7251
8172
|
function loadCommands() {
|
|
7252
|
-
const __dir =
|
|
7253
|
-
const commandsDir =
|
|
7254
|
-
if (!
|
|
8173
|
+
const __dir = dirname5(fileURLToPath3(import.meta.url));
|
|
8174
|
+
const commandsDir = join31(__dir, "..", "src", "commands");
|
|
8175
|
+
if (!existsSync33(commandsDir))
|
|
7255
8176
|
return {};
|
|
7256
8177
|
const commands = {};
|
|
7257
8178
|
try {
|
|
7258
|
-
for (const file of
|
|
8179
|
+
for (const file of readdirSync5(commandsDir)) {
|
|
7259
8180
|
if (!file.endsWith(".md"))
|
|
7260
8181
|
continue;
|
|
7261
|
-
const name =
|
|
7262
|
-
const raw =
|
|
8182
|
+
const name = basename2(file, ".md");
|
|
8183
|
+
const raw = readFileSync31(join31(commandsDir, file), "utf-8");
|
|
7263
8184
|
let description;
|
|
7264
8185
|
let template = raw;
|
|
7265
8186
|
const fmMatch = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/);
|
|
@@ -7341,8 +8262,8 @@ var plugin = async (input, _options) => {
|
|
|
7341
8262
|
}
|
|
7342
8263
|
}
|
|
7343
8264
|
}
|
|
7344
|
-
const skillsDir =
|
|
7345
|
-
if (
|
|
8265
|
+
const skillsDir = join31(dirname5(fileURLToPath3(import.meta.url)), "..", "src", "skills");
|
|
8266
|
+
if (existsSync33(skillsDir)) {
|
|
7346
8267
|
const cfgAny = cfg;
|
|
7347
8268
|
if (!cfgAny.skills || typeof cfgAny.skills !== "object") {
|
|
7348
8269
|
cfgAny.skills = { paths: [] };
|
|
@@ -7354,7 +8275,8 @@ var plugin = async (input, _options) => {
|
|
|
7354
8275
|
cfgSkills.paths.push(skillsDir);
|
|
7355
8276
|
}
|
|
7356
8277
|
}
|
|
7357
|
-
const rulePaths =
|
|
8278
|
+
const { paths: rulePaths, diagnostics: rulesDiag } = lazyLoadRulePaths(directory);
|
|
8279
|
+
appLog(rulesDiag);
|
|
7358
8280
|
if (rulePaths.length > 0) {
|
|
7359
8281
|
if (!Array.isArray(cfg.instructions)) {
|
|
7360
8282
|
cfg.instructions = [];
|
|
@@ -7383,7 +8305,10 @@ var plugin = async (input, _options) => {
|
|
|
7383
8305
|
"context-generator": contextGeneratorTool,
|
|
7384
8306
|
"create-skill": createSkillTool,
|
|
7385
8307
|
reflect: reflectTool,
|
|
7386
|
-
codegraph: codegraphTool
|
|
8308
|
+
codegraph: codegraphTool,
|
|
8309
|
+
"load-rules": loadRulesTool,
|
|
8310
|
+
"list-rules": listRulesTool,
|
|
8311
|
+
"rtk-setup": rtkSetupTool
|
|
7387
8312
|
},
|
|
7388
8313
|
"shell.env": shellEnvHook,
|
|
7389
8314
|
"todo.updated": todoHook,
|