@dv.nghiem/flowdeck 0.4.2 → 0.4.4
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/hooks/telemetry-hook.d.ts +3 -2
- package/dist/hooks/telemetry-hook.d.ts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1588 -336
- package/dist/services/activity-reporter.d.ts +96 -0
- package/dist/services/activity-reporter.d.ts.map +1 -0
- package/dist/services/activity-reporter.test.d.ts +2 -0
- package/dist/services/activity-reporter.test.d.ts.map +1 -0
- 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 +15 -1
- 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/council.d.ts.map +1 -1
- package/dist/tools/delegate.d.ts +2 -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 +2 -1
- 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
|
}
|
|
@@ -716,12 +867,215 @@ function isUiHeavyTask(input) {
|
|
|
716
867
|
return !hasOnlyNonUiSignals;
|
|
717
868
|
}
|
|
718
869
|
|
|
870
|
+
// src/services/activity-reporter.ts
|
|
871
|
+
var SUMMARY_MAX_NORMAL = 120;
|
|
872
|
+
var SUMMARY_MAX_DEBUG = 600;
|
|
873
|
+
var HEARTBEAT_INTERVAL_MS = 15000;
|
|
874
|
+
var TOAST_ON_START_TOOLS = new Set(["delegate", "run-pipeline"]);
|
|
875
|
+
function isDebugMode() {
|
|
876
|
+
return process.env.FLOWDECK_DEBUG === "true" || process.env.FLOWDECK_DEBUG === "1";
|
|
877
|
+
}
|
|
878
|
+
function summarize(text, maxLen = SUMMARY_MAX_NORMAL) {
|
|
879
|
+
if (!text)
|
|
880
|
+
return "";
|
|
881
|
+
const s = text.trim().replace(/\s+/g, " ");
|
|
882
|
+
return s.length <= maxLen ? s : s.slice(0, maxLen - 1) + "…";
|
|
883
|
+
}
|
|
884
|
+
function fmtDuration(ms) {
|
|
885
|
+
if (ms < 1000)
|
|
886
|
+
return `${ms}ms`;
|
|
887
|
+
return `${(ms / 1000).toFixed(1)}s`;
|
|
888
|
+
}
|
|
889
|
+
function normalizeCommandName(raw) {
|
|
890
|
+
return raw.replace(/^\//, "").replace(/^fd-/, "");
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
class ActivityReporter {
|
|
894
|
+
log;
|
|
895
|
+
toastFn;
|
|
896
|
+
startTimes = new Map;
|
|
897
|
+
heartbeats = new Map;
|
|
898
|
+
constructor(log, toast) {
|
|
899
|
+
this.log = log;
|
|
900
|
+
this.toastFn = toast;
|
|
901
|
+
}
|
|
902
|
+
emit(msg) {
|
|
903
|
+
try {
|
|
904
|
+
this.log(msg);
|
|
905
|
+
} catch {}
|
|
906
|
+
}
|
|
907
|
+
toastNow(msg, variant, duration) {
|
|
908
|
+
if (!this.toastFn)
|
|
909
|
+
return;
|
|
910
|
+
try {
|
|
911
|
+
this.toastFn(msg, variant, duration);
|
|
912
|
+
} catch {}
|
|
913
|
+
}
|
|
914
|
+
trackStart(key) {
|
|
915
|
+
this.startTimes.set(key, Date.now());
|
|
916
|
+
const toolName = key.split(":").pop() ?? key;
|
|
917
|
+
const interval = setInterval(() => {
|
|
918
|
+
const startMs = this.startTimes.get(key);
|
|
919
|
+
if (startMs === undefined)
|
|
920
|
+
return;
|
|
921
|
+
const elapsed = Date.now() - startMs;
|
|
922
|
+
const msg = `[⋯ ${toolName}] still running (${fmtDuration(elapsed)})`;
|
|
923
|
+
this.emit(msg);
|
|
924
|
+
this.toastNow(msg, "info", 8000);
|
|
925
|
+
}, HEARTBEAT_INTERVAL_MS);
|
|
926
|
+
if (typeof interval.unref === "function") {
|
|
927
|
+
interval.unref();
|
|
928
|
+
}
|
|
929
|
+
this.heartbeats.set(key, interval);
|
|
930
|
+
}
|
|
931
|
+
elapsedMs(key) {
|
|
932
|
+
const interval = this.heartbeats.get(key);
|
|
933
|
+
if (interval !== undefined) {
|
|
934
|
+
clearInterval(interval);
|
|
935
|
+
this.heartbeats.delete(key);
|
|
936
|
+
}
|
|
937
|
+
const t = this.startTimes.get(key);
|
|
938
|
+
if (t === undefined)
|
|
939
|
+
return;
|
|
940
|
+
this.startTimes.delete(key);
|
|
941
|
+
return Date.now() - t;
|
|
942
|
+
}
|
|
943
|
+
reportToolStarted(tool4, inputSummary, meta = {}) {
|
|
944
|
+
const maxLen = isDebugMode() ? SUMMARY_MAX_DEBUG : SUMMARY_MAX_NORMAL;
|
|
945
|
+
const parts = [`[→ ${tool4}]`];
|
|
946
|
+
if (meta.agent)
|
|
947
|
+
parts.push(`agent=${meta.agent}`);
|
|
948
|
+
if (inputSummary)
|
|
949
|
+
parts.push(summarize(inputSummary, maxLen));
|
|
950
|
+
if (isDebugMode()) {
|
|
951
|
+
if (meta.session_id)
|
|
952
|
+
parts.push(`session=${meta.session_id}`);
|
|
953
|
+
if (meta.stage)
|
|
954
|
+
parts.push(`stage=${meta.stage}`);
|
|
955
|
+
if (meta.run_id)
|
|
956
|
+
parts.push(`run=${meta.run_id}`);
|
|
957
|
+
}
|
|
958
|
+
this.emit(parts.join(" "));
|
|
959
|
+
if (TOAST_ON_START_TOOLS.has(tool4)) {
|
|
960
|
+
const agentPart = meta.agent ? ` @${meta.agent}` : "";
|
|
961
|
+
const inputPart = inputSummary ? `: ${summarize(inputSummary, 60)}` : "";
|
|
962
|
+
this.toastNow(`→ ${tool4}${agentPart}${inputPart}`, "info", 3000);
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
reportToolCompleted(tool4, durationMs, resultSummary, meta = {}) {
|
|
966
|
+
const maxLen = isDebugMode() ? SUMMARY_MAX_DEBUG : SUMMARY_MAX_NORMAL;
|
|
967
|
+
const dur = durationMs !== undefined ? ` (${fmtDuration(durationMs)})` : "";
|
|
968
|
+
const parts = [`[✓ ${tool4}]${dur}`];
|
|
969
|
+
if (meta.agent)
|
|
970
|
+
parts.push(`agent=${meta.agent}`);
|
|
971
|
+
if (resultSummary)
|
|
972
|
+
parts.push(summarize(resultSummary, maxLen));
|
|
973
|
+
if (isDebugMode() && meta.retry_count && meta.retry_count > 0) {
|
|
974
|
+
parts.push(`retries=${meta.retry_count}`);
|
|
975
|
+
}
|
|
976
|
+
this.emit(parts.join(" "));
|
|
977
|
+
}
|
|
978
|
+
reportToolFailed(tool4, durationMs, error, meta = {}) {
|
|
979
|
+
const dur = durationMs !== undefined ? ` (${fmtDuration(durationMs)})` : "";
|
|
980
|
+
const parts = [`[✗ ${tool4}]${dur}`];
|
|
981
|
+
if (meta.agent)
|
|
982
|
+
parts.push(`agent=${meta.agent}`);
|
|
983
|
+
parts.push(`error=${summarize(error, isDebugMode() ? SUMMARY_MAX_DEBUG : 200)}`);
|
|
984
|
+
if (isDebugMode() && meta.retry_count && meta.retry_count > 0) {
|
|
985
|
+
parts.push(`retries=${meta.retry_count}`);
|
|
986
|
+
}
|
|
987
|
+
this.emit(parts.join(" "));
|
|
988
|
+
this.toastNow(`✗ ${tool4}${dur}: ${summarize(error, 80)}`, "error", 8000);
|
|
989
|
+
}
|
|
990
|
+
reportToolRetried(tool4, attempt, reason, meta = {}) {
|
|
991
|
+
const parts = [`[↺ ${tool4}] retry attempt=${attempt}`];
|
|
992
|
+
if (meta.agent)
|
|
993
|
+
parts.push(`agent=${meta.agent}`);
|
|
994
|
+
if (reason)
|
|
995
|
+
parts.push(`reason=${summarize(reason, 80)}`);
|
|
996
|
+
this.emit(parts.join(" "));
|
|
997
|
+
this.toastNow(`↺ ${tool4} retry #${attempt}${meta.agent ? ` @${meta.agent}` : ""}`, "warning", 5000);
|
|
998
|
+
}
|
|
999
|
+
reportToolFallback(fromTool, toTool, reason, meta = {}) {
|
|
1000
|
+
const parts = [`[⇢ fallback] ${fromTool} → ${toTool}`];
|
|
1001
|
+
if (reason)
|
|
1002
|
+
parts.push(`reason=${summarize(reason, 80)}`);
|
|
1003
|
+
if (meta.agent)
|
|
1004
|
+
parts.push(`agent=${meta.agent}`);
|
|
1005
|
+
this.emit(parts.join(" "));
|
|
1006
|
+
this.toastNow(`⇢ fallback: ${fromTool} → ${toTool}`, "info", 4000);
|
|
1007
|
+
}
|
|
1008
|
+
reportCacheHit(tool4, agent, meta = {}) {
|
|
1009
|
+
const parts = [`[≡ ${tool4}] cache hit agent=${agent}`];
|
|
1010
|
+
if (isDebugMode() && meta.session_id)
|
|
1011
|
+
parts.push(`session=${meta.session_id}`);
|
|
1012
|
+
this.emit(parts.join(" "));
|
|
1013
|
+
}
|
|
1014
|
+
reportSkipped(tool4, reason, meta = {}) {
|
|
1015
|
+
const parts = [`[⊘ ${tool4}] skipped`];
|
|
1016
|
+
if (reason)
|
|
1017
|
+
parts.push(`reason=${summarize(reason, 80)}`);
|
|
1018
|
+
if (meta.agent)
|
|
1019
|
+
parts.push(`agent=${meta.agent}`);
|
|
1020
|
+
this.emit(parts.join(" "));
|
|
1021
|
+
}
|
|
1022
|
+
reportStageProgress(stage, status, detail, meta = {}) {
|
|
1023
|
+
const icon = {
|
|
1024
|
+
started: "▶",
|
|
1025
|
+
running: "⋯",
|
|
1026
|
+
complete: "●",
|
|
1027
|
+
failed: "✗",
|
|
1028
|
+
waiting: "⌛"
|
|
1029
|
+
};
|
|
1030
|
+
const sym = icon[status] ?? "·";
|
|
1031
|
+
const parts = [`[${sym} ${stage}] ${status}`];
|
|
1032
|
+
if (detail)
|
|
1033
|
+
parts.push(summarize(detail));
|
|
1034
|
+
if (isDebugMode() && meta.workflow_id)
|
|
1035
|
+
parts.push(`workflow=${meta.workflow_id}`);
|
|
1036
|
+
this.emit(parts.join(" "));
|
|
1037
|
+
const detailPart = detail ? `: ${summarize(detail, 60)}` : "";
|
|
1038
|
+
switch (status) {
|
|
1039
|
+
case "started":
|
|
1040
|
+
this.toastNow(`▶ ${stage} started${detailPart}`, "info", 3000);
|
|
1041
|
+
break;
|
|
1042
|
+
case "complete":
|
|
1043
|
+
this.toastNow(`● ${stage} complete${detailPart}`, "success", 4000);
|
|
1044
|
+
break;
|
|
1045
|
+
case "failed":
|
|
1046
|
+
this.toastNow(`✗ ${stage} failed${detailPart}`, "error", 8000);
|
|
1047
|
+
break;
|
|
1048
|
+
case "waiting":
|
|
1049
|
+
this.toastNow(`⌛ ${stage}: waiting for input${detailPart}`, "warning", 30000);
|
|
1050
|
+
break;
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
reportWaitingForApproval(tool4, _meta = {}) {
|
|
1054
|
+
const msg = `⌛ Approval required: ${tool4}`;
|
|
1055
|
+
this.emit(msg);
|
|
1056
|
+
this.toastNow(msg, "warning", 30000);
|
|
1057
|
+
}
|
|
1058
|
+
reportCommandStarted(command) {
|
|
1059
|
+
const cmd = normalizeCommandName(command);
|
|
1060
|
+
const msg = `▶ /${cmd} started`;
|
|
1061
|
+
this.emit(msg);
|
|
1062
|
+
this.toastNow(msg, "info", 2500);
|
|
1063
|
+
}
|
|
1064
|
+
reportCommandCompleted(command, hasEdits) {
|
|
1065
|
+
const cmd = normalizeCommandName(command);
|
|
1066
|
+
const detail = hasEdits ? " (files modified)" : "";
|
|
1067
|
+
const msg = `● /${cmd} complete${detail}`;
|
|
1068
|
+
this.emit(msg);
|
|
1069
|
+
this.toastNow(msg, "success", 5000);
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
|
|
719
1073
|
// src/tools/run-pipeline.ts
|
|
720
1074
|
function extractText(parts) {
|
|
721
1075
|
return parts.filter((p) => p.type === "text" && typeof p.text === "string").map((p) => p.text).join(`
|
|
722
1076
|
`);
|
|
723
1077
|
}
|
|
724
|
-
function createRunPipelineTool(client) {
|
|
1078
|
+
function createRunPipelineTool(client, reporter) {
|
|
725
1079
|
return tool4({
|
|
726
1080
|
description: "Run agents in sequential pipeline. Each step's output is appended to the next step's context. One fresh child session per step. Returns full trace with session ID, input/output/duration per step.",
|
|
727
1081
|
args: {
|
|
@@ -732,7 +1086,8 @@ function createRunPipelineTool(client) {
|
|
|
732
1086
|
})),
|
|
733
1087
|
initial_context: tool4.schema.string().optional(),
|
|
734
1088
|
abort_on_failure: tool4.schema.boolean().optional().default(true),
|
|
735
|
-
retry_attempts: tool4.schema.number().optional().default(1)
|
|
1089
|
+
retry_attempts: tool4.schema.number().optional().default(1),
|
|
1090
|
+
max_carry_chars: tool4.schema.number().optional()
|
|
736
1091
|
},
|
|
737
1092
|
async execute(args, context) {
|
|
738
1093
|
const startTime = Date.now();
|
|
@@ -741,6 +1096,8 @@ function createRunPipelineTool(client) {
|
|
|
741
1096
|
let aborted = false;
|
|
742
1097
|
const retryAttempts = typeof args.retry_attempts === "number" ? args.retry_attempts : 1;
|
|
743
1098
|
const maxRetries = Math.max(0, Math.floor(retryAttempts));
|
|
1099
|
+
const totalSteps = args.steps.length;
|
|
1100
|
+
reporter?.reportStageProgress("pipeline", "started", `${totalSteps} step(s)`);
|
|
744
1101
|
let inflightChildId = null;
|
|
745
1102
|
const abortHandler = () => {
|
|
746
1103
|
if (inflightChildId) {
|
|
@@ -752,7 +1109,8 @@ function createRunPipelineTool(client) {
|
|
|
752
1109
|
};
|
|
753
1110
|
context.abort.addEventListener("abort", abortHandler);
|
|
754
1111
|
try {
|
|
755
|
-
for (
|
|
1112
|
+
for (let stepIdx = 0;stepIdx < args.steps.length; stepIdx++) {
|
|
1113
|
+
const step = args.steps[stepIdx];
|
|
756
1114
|
if (context.abort.aborted) {
|
|
757
1115
|
aborted = true;
|
|
758
1116
|
break;
|
|
@@ -764,6 +1122,10 @@ function createRunPipelineTool(client) {
|
|
|
764
1122
|
---
|
|
765
1123
|
|
|
766
1124
|
${step.prompt}` : step.prompt;
|
|
1125
|
+
reporter?.reportToolStarted("run-pipeline", summarize(step.prompt, 80), {
|
|
1126
|
+
agent: step.agent,
|
|
1127
|
+
stage: `step ${stepIdx + 1}/${totalSteps}`
|
|
1128
|
+
});
|
|
767
1129
|
const createRes = await client.session.create({
|
|
768
1130
|
body: { parentID: context.sessionID, title: `${step.agent}-pipeline` },
|
|
769
1131
|
query: { directory: context.directory }
|
|
@@ -771,6 +1133,7 @@ ${step.prompt}` : step.prompt;
|
|
|
771
1133
|
if (createRes.error || !createRes.data?.id) {
|
|
772
1134
|
const errMsg = `Failed to create session: ${createRes.error?.detail ?? "unknown"}`;
|
|
773
1135
|
trace.push({ agent: step.agent, task_type: taskType, model: "", input: stepInput, output: errMsg, duration_ms: Date.now() - stepStart, success: false });
|
|
1136
|
+
reporter?.reportToolFailed("run-pipeline", Date.now() - stepStart, errMsg, { agent: step.agent });
|
|
774
1137
|
aborted = true;
|
|
775
1138
|
break;
|
|
776
1139
|
}
|
|
@@ -790,6 +1153,7 @@ ${step.prompt}` : step.prompt;
|
|
|
790
1153
|
if (!shouldRetry(promptRes) || attempt === maxRetries)
|
|
791
1154
|
break;
|
|
792
1155
|
retriesUsed++;
|
|
1156
|
+
reporter?.reportToolRetried("run-pipeline", retriesUsed, "prompt response indicated retry", { agent: step.agent });
|
|
793
1157
|
}
|
|
794
1158
|
inflightChildId = null;
|
|
795
1159
|
if (context.abort.aborted) {
|
|
@@ -800,6 +1164,7 @@ ${step.prompt}` : step.prompt;
|
|
|
800
1164
|
const errMsg = `Prompt failed: ${promptRes?.error?.detail ?? "unknown"}`;
|
|
801
1165
|
trace.push({ agent: step.agent, session_id: createRes.data.id, task_type: taskType, model: "", input: stepInput, output: `${errMsg}${retriesUsed > 0 ? ` (retries: ${retriesUsed})` : ""}`, duration_ms: Date.now() - stepStart, success: false });
|
|
802
1166
|
recordRun(context.directory, step.agent, "", taskType, false, Date.now() - stepStart);
|
|
1167
|
+
reporter?.reportToolFailed("run-pipeline", Date.now() - stepStart, errMsg, { agent: step.agent, retry_count: retriesUsed });
|
|
803
1168
|
if (args.abort_on_failure) {
|
|
804
1169
|
aborted = true;
|
|
805
1170
|
break;
|
|
@@ -811,6 +1176,7 @@ ${step.prompt}` : step.prompt;
|
|
|
811
1176
|
const errMsg = `Agent error: ${JSON.stringify(info.error)}`;
|
|
812
1177
|
trace.push({ agent: step.agent, session_id: createRes.data.id, task_type: taskType, model: "", input: stepInput, output: `${errMsg}${retriesUsed > 0 ? ` (retries: ${retriesUsed})` : ""}`, duration_ms: Date.now() - stepStart, success: false });
|
|
813
1178
|
recordRun(context.directory, step.agent, "", taskType, false, Date.now() - stepStart);
|
|
1179
|
+
reporter?.reportToolFailed("run-pipeline", Date.now() - stepStart, errMsg, { agent: step.agent, retry_count: retriesUsed });
|
|
814
1180
|
if (args.abort_on_failure) {
|
|
815
1181
|
aborted = true;
|
|
816
1182
|
break;
|
|
@@ -818,16 +1184,28 @@ ${step.prompt}` : step.prompt;
|
|
|
818
1184
|
continue;
|
|
819
1185
|
}
|
|
820
1186
|
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 });
|
|
1187
|
+
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
1188
|
recordRun(context.directory, step.agent, "", taskType, true, Date.now() - stepStart);
|
|
823
|
-
|
|
1189
|
+
reporter?.reportToolCompleted("run-pipeline", Date.now() - stepStart, summarize(output, 80), {
|
|
1190
|
+
agent: step.agent,
|
|
1191
|
+
retry_count: retriesUsed,
|
|
1192
|
+
stage: `step ${stepIdx + 1}/${totalSteps}`
|
|
1193
|
+
});
|
|
1194
|
+
const rawOutput = output || "";
|
|
1195
|
+
carryContext = typeof args.max_carry_chars === "number" && rawOutput.length > args.max_carry_chars ? rawOutput.slice(rawOutput.length - args.max_carry_chars) : rawOutput;
|
|
824
1196
|
}
|
|
825
1197
|
} finally {
|
|
826
1198
|
context.abort.removeEventListener("abort", abortHandler);
|
|
827
1199
|
}
|
|
1200
|
+
const totalDuration = Date.now() - startTime;
|
|
1201
|
+
if (aborted) {
|
|
1202
|
+
reporter?.reportStageProgress("pipeline", "failed", `aborted after ${trace.length}/${totalSteps} steps`);
|
|
1203
|
+
} else {
|
|
1204
|
+
reporter?.reportStageProgress("pipeline", "complete", `${totalSteps} step(s) in ${totalDuration}ms`);
|
|
1205
|
+
}
|
|
828
1206
|
return JSON.stringify({
|
|
829
1207
|
steps: trace,
|
|
830
|
-
total_duration_ms:
|
|
1208
|
+
total_duration_ms: totalDuration,
|
|
831
1209
|
aborted
|
|
832
1210
|
});
|
|
833
1211
|
}
|
|
@@ -836,11 +1214,404 @@ ${step.prompt}` : step.prompt;
|
|
|
836
1214
|
|
|
837
1215
|
// src/tools/delegate.ts
|
|
838
1216
|
import { tool as tool5 } from "@opencode-ai/plugin";
|
|
1217
|
+
import { existsSync as existsSync11, readFileSync as readFileSync11 } from "fs";
|
|
1218
|
+
|
|
1219
|
+
// src/services/prompt-cache.ts
|
|
1220
|
+
import { createHash } from "crypto";
|
|
1221
|
+
import { existsSync as existsSync7, readFileSync as readFileSync7, writeFileSync as writeFileSync6, readdirSync as readdirSync3, statSync, mkdirSync as mkdirSync3 } from "fs";
|
|
1222
|
+
import { join as join7 } from "path";
|
|
1223
|
+
var CACHEABLE_AGENTS = new Set([
|
|
1224
|
+
"researcher",
|
|
1225
|
+
"code-explorer",
|
|
1226
|
+
"reviewer",
|
|
1227
|
+
"plan-checker",
|
|
1228
|
+
"security-auditor",
|
|
1229
|
+
"question-guard",
|
|
1230
|
+
"quick-router"
|
|
1231
|
+
]);
|
|
1232
|
+
var CACHE_DIR_NAME = "prompt-cache";
|
|
1233
|
+
var MAX_CACHE_ENTRIES = 200;
|
|
1234
|
+
var DEFAULT_TTL_MS = 30 * 60 * 1000;
|
|
1235
|
+
function cacheDir(dir) {
|
|
1236
|
+
return join7(codebaseDir(dir), CACHE_DIR_NAME);
|
|
1237
|
+
}
|
|
1238
|
+
function entryPath(dir, key) {
|
|
1239
|
+
return join7(cacheDir(dir), `${key}.json`);
|
|
1240
|
+
}
|
|
1241
|
+
function readEntry(dir, key, stateVersion, indexVersion) {
|
|
1242
|
+
const path = entryPath(dir, key);
|
|
1243
|
+
if (!existsSync7(path))
|
|
1244
|
+
return null;
|
|
1245
|
+
try {
|
|
1246
|
+
const entry = JSON.parse(readFileSync7(path, "utf-8"));
|
|
1247
|
+
const age = Date.now() - new Date(entry.created_at).getTime();
|
|
1248
|
+
if (age > entry.ttl_ms)
|
|
1249
|
+
return null;
|
|
1250
|
+
if (entry.state_version !== stateVersion || entry.index_version !== indexVersion)
|
|
1251
|
+
return null;
|
|
1252
|
+
return entry.response;
|
|
1253
|
+
} catch {
|
|
1254
|
+
return null;
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
function hashKey(agent, prompt, context, stateVersion, indexVersion) {
|
|
1258
|
+
const raw = JSON.stringify({ agent, prompt: prompt.trim(), context: context.trim(), stateVersion, indexVersion });
|
|
1259
|
+
return createHash("sha256").update(raw).digest("hex").slice(0, 32);
|
|
1260
|
+
}
|
|
1261
|
+
function normalizeForCache(text) {
|
|
1262
|
+
return text.replace(/\s+/g, " ").trim();
|
|
1263
|
+
}
|
|
1264
|
+
function hashKeyNormalized(agent, prompt, context, stateVersion, indexVersion) {
|
|
1265
|
+
const normalized = JSON.stringify({
|
|
1266
|
+
agent,
|
|
1267
|
+
prompt: normalizeForCache(prompt),
|
|
1268
|
+
context: normalizeForCache(context),
|
|
1269
|
+
stateVersion,
|
|
1270
|
+
indexVersion
|
|
1271
|
+
});
|
|
1272
|
+
return createHash("sha256").update(normalized).digest("hex").slice(0, 32);
|
|
1273
|
+
}
|
|
1274
|
+
function getCached(dir, agent, prompt, context, stateVersion, indexVersion, safe_to_cache = false) {
|
|
1275
|
+
if (!safe_to_cache)
|
|
1276
|
+
return null;
|
|
1277
|
+
if (!CACHEABLE_AGENTS.has(agent))
|
|
1278
|
+
return null;
|
|
1279
|
+
const exactKey = hashKey(agent, prompt, context, stateVersion, indexVersion);
|
|
1280
|
+
const exactResult = readEntry(dir, exactKey, stateVersion, indexVersion);
|
|
1281
|
+
if (exactResult !== null)
|
|
1282
|
+
return exactResult;
|
|
1283
|
+
const normKey = hashKeyNormalized(agent, prompt, context, stateVersion, indexVersion);
|
|
1284
|
+
if (normKey === exactKey)
|
|
1285
|
+
return null;
|
|
1286
|
+
return readEntry(dir, normKey, stateVersion, indexVersion);
|
|
1287
|
+
}
|
|
1288
|
+
function setCached(dir, agent, prompt, context, stateVersion, indexVersion, response, safe_to_cache = false, ttl_ms = DEFAULT_TTL_MS) {
|
|
1289
|
+
if (!safe_to_cache)
|
|
1290
|
+
return;
|
|
1291
|
+
if (!CACHEABLE_AGENTS.has(agent))
|
|
1292
|
+
return;
|
|
1293
|
+
const cd = cacheDir(dir);
|
|
1294
|
+
if (!existsSync7(cd))
|
|
1295
|
+
mkdirSync3(cd, { recursive: true });
|
|
1296
|
+
const key = hashKey(agent, prompt, context, stateVersion, indexVersion);
|
|
1297
|
+
const entry = {
|
|
1298
|
+
key,
|
|
1299
|
+
agent,
|
|
1300
|
+
state_version: stateVersion,
|
|
1301
|
+
index_version: indexVersion,
|
|
1302
|
+
created_at: new Date().toISOString(),
|
|
1303
|
+
ttl_ms,
|
|
1304
|
+
response
|
|
1305
|
+
};
|
|
1306
|
+
writeFileSync6(entryPath(dir, key), JSON.stringify(entry, null, 2), "utf-8");
|
|
1307
|
+
pruneExpired(dir);
|
|
1308
|
+
}
|
|
1309
|
+
function pruneExpired(dir) {
|
|
1310
|
+
const cd = cacheDir(dir);
|
|
1311
|
+
if (!existsSync7(cd))
|
|
1312
|
+
return;
|
|
1313
|
+
try {
|
|
1314
|
+
const files = readdirSync3(cd).filter((f) => f.endsWith(".json"));
|
|
1315
|
+
const now = Date.now();
|
|
1316
|
+
const entries = [];
|
|
1317
|
+
for (const f of files) {
|
|
1318
|
+
const p = join7(cd, f);
|
|
1319
|
+
try {
|
|
1320
|
+
const entry = JSON.parse(readFileSync7(p, "utf-8"));
|
|
1321
|
+
const age = now - new Date(entry.created_at).getTime();
|
|
1322
|
+
entries.push({ path: p, created_at: new Date(entry.created_at).getTime(), expired: age > entry.ttl_ms });
|
|
1323
|
+
} catch {
|
|
1324
|
+
entries.push({ path: p, created_at: 0, expired: true });
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
let deleted = 0;
|
|
1328
|
+
for (const e of entries) {
|
|
1329
|
+
if (e.expired) {
|
|
1330
|
+
try {
|
|
1331
|
+
__require("fs").unlinkSync(e.path);
|
|
1332
|
+
} catch {}
|
|
1333
|
+
deleted++;
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
const valid = entries.filter((e) => !e.expired).sort((a, b) => a.created_at - b.created_at);
|
|
1337
|
+
const excess = valid.length - MAX_CACHE_ENTRIES;
|
|
1338
|
+
for (let i = 0;i < excess; i++) {
|
|
1339
|
+
try {
|
|
1340
|
+
__require("fs").unlinkSync(valid[i].path);
|
|
1341
|
+
} catch {}
|
|
1342
|
+
}
|
|
1343
|
+
} catch {}
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
// src/tools/codebase-index.ts
|
|
1347
|
+
import { readFileSync as readFileSync8, writeFileSync as writeFileSync7, existsSync as existsSync8, mkdirSync as mkdirSync4 } from "fs";
|
|
1348
|
+
import { join as join8 } from "path";
|
|
1349
|
+
var CODEBASE_INDEX_FILE = "CODEBASE_INDEX.md";
|
|
1350
|
+
function indexPath(dir) {
|
|
1351
|
+
return join8(planningDir(dir), CODEBASE_INDEX_FILE);
|
|
1352
|
+
}
|
|
1353
|
+
function readCodebaseIndex(dir) {
|
|
1354
|
+
const path = indexPath(dir);
|
|
1355
|
+
if (!existsSync8(path)) {
|
|
1356
|
+
return {
|
|
1357
|
+
exists: false,
|
|
1358
|
+
lastUpdatedAt: "",
|
|
1359
|
+
lastUpdatedBy: "",
|
|
1360
|
+
sourceStage: "",
|
|
1361
|
+
changedFiles: [],
|
|
1362
|
+
fileSnapshots: {},
|
|
1363
|
+
explorationHistory: [],
|
|
1364
|
+
summaryVersion: 0,
|
|
1365
|
+
freshnessStatus: "unknown"
|
|
1366
|
+
};
|
|
1367
|
+
}
|
|
1368
|
+
try {
|
|
1369
|
+
const content = readFileSync8(path, "utf-8");
|
|
1370
|
+
return parseCodebaseIndexContent(content);
|
|
1371
|
+
} catch {
|
|
1372
|
+
return {
|
|
1373
|
+
exists: false,
|
|
1374
|
+
lastUpdatedAt: "",
|
|
1375
|
+
lastUpdatedBy: "",
|
|
1376
|
+
sourceStage: "",
|
|
1377
|
+
changedFiles: [],
|
|
1378
|
+
fileSnapshots: {},
|
|
1379
|
+
explorationHistory: [],
|
|
1380
|
+
summaryVersion: 0,
|
|
1381
|
+
freshnessStatus: "unknown"
|
|
1382
|
+
};
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
function parseCodebaseIndexContent(content) {
|
|
1386
|
+
const result = { exists: true };
|
|
1387
|
+
for (const line of content.split(`
|
|
1388
|
+
`)) {
|
|
1389
|
+
if (line.startsWith("#") || line.trim() === "")
|
|
1390
|
+
continue;
|
|
1391
|
+
const strippedLine = line.replace(/\*\*/g, "").replace(/^([a-zA-Z_][a-zA-Z0-9_]*):\s*(.*)/, "$1: $2");
|
|
1392
|
+
const kvMatch = strippedLine.match(/^([a-zA-Z_][a-zA-Z0-9_]*):\s*(.*)/);
|
|
1393
|
+
if (!kvMatch)
|
|
1394
|
+
continue;
|
|
1395
|
+
const key = kvMatch[1].trim();
|
|
1396
|
+
const value = kvMatch[2].trim();
|
|
1397
|
+
if (key === "changedFiles") {
|
|
1398
|
+
result.changedFiles = value.replace(/[\[\]]/g, "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
1399
|
+
} else if (key === "summaryVersion") {
|
|
1400
|
+
result.summaryVersion = parseInt(value, 10) || 0;
|
|
1401
|
+
} else if (key === "freshnessStatus") {
|
|
1402
|
+
result.freshnessStatus = value;
|
|
1403
|
+
} else if (key === "lastUpdatedAt" || key === "lastUpdatedBy" || key === "sourceStage") {
|
|
1404
|
+
result[key] = value.replace(/^["']|["']$/g, "");
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
let blockCount = 0;
|
|
1408
|
+
for (const jsonMatch of content.matchAll(/```json\n([\s\S]*?)\n```/g)) {
|
|
1409
|
+
if (blockCount >= 2)
|
|
1410
|
+
break;
|
|
1411
|
+
blockCount++;
|
|
1412
|
+
try {
|
|
1413
|
+
const parsed = JSON.parse(jsonMatch[1]);
|
|
1414
|
+
if (parsed.fileSnapshots)
|
|
1415
|
+
result.fileSnapshots = parsed.fileSnapshots;
|
|
1416
|
+
if (parsed.explorationHistory)
|
|
1417
|
+
result.explorationHistory = parsed.explorationHistory;
|
|
1418
|
+
if (!parsed.fileSnapshots && !parsed.explorationHistory) {
|
|
1419
|
+
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
1420
|
+
if (!result.fileSnapshots)
|
|
1421
|
+
result.fileSnapshots = {};
|
|
1422
|
+
Object.assign(result.fileSnapshots, parsed);
|
|
1423
|
+
} else if (Array.isArray(parsed)) {
|
|
1424
|
+
result.explorationHistory = parsed;
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
} catch {
|
|
1428
|
+
result.freshnessStatus = "unknown";
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
return {
|
|
1432
|
+
exists: true,
|
|
1433
|
+
lastUpdatedAt: result.lastUpdatedAt || "",
|
|
1434
|
+
lastUpdatedBy: result.lastUpdatedBy || "",
|
|
1435
|
+
sourceStage: result.sourceStage || "",
|
|
1436
|
+
changedFiles: result.changedFiles || [],
|
|
1437
|
+
fileSnapshots: result.fileSnapshots || {},
|
|
1438
|
+
explorationHistory: result.explorationHistory || [],
|
|
1439
|
+
summaryVersion: result.summaryVersion || 0,
|
|
1440
|
+
freshnessStatus: result.freshnessStatus || "unknown"
|
|
1441
|
+
};
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
// src/services/token-metrics.ts
|
|
1445
|
+
import { existsSync as existsSync9, readFileSync as readFileSync9, appendFileSync, mkdirSync as mkdirSync5 } from "fs";
|
|
1446
|
+
import { join as join9 } from "path";
|
|
1447
|
+
function estimateTokens(text) {
|
|
1448
|
+
return Math.ceil(text.length / 4);
|
|
1449
|
+
}
|
|
1450
|
+
function metricsPath(dir) {
|
|
1451
|
+
return join9(codebaseDir(dir), "TOKEN_METRICS.jsonl");
|
|
1452
|
+
}
|
|
1453
|
+
function appendEvent(dir, event) {
|
|
1454
|
+
const cd = codebaseDir(dir);
|
|
1455
|
+
if (!existsSync9(cd))
|
|
1456
|
+
mkdirSync5(cd, { recursive: true });
|
|
1457
|
+
appendFileSync(metricsPath(dir), JSON.stringify(event) + `
|
|
1458
|
+
`, "utf-8");
|
|
1459
|
+
}
|
|
1460
|
+
function recordModelCall(dir, workflow_id, stage, inputText, outputText, agent, duration_ms, model, est_cost_usd) {
|
|
1461
|
+
const est_input_tokens = estimateTokens(inputText);
|
|
1462
|
+
const est_output_tokens = estimateTokens(outputText);
|
|
1463
|
+
appendEvent(dir, {
|
|
1464
|
+
ts: new Date().toISOString(),
|
|
1465
|
+
workflow_id,
|
|
1466
|
+
stage,
|
|
1467
|
+
event: "model_call",
|
|
1468
|
+
agent,
|
|
1469
|
+
model,
|
|
1470
|
+
est_input_tokens,
|
|
1471
|
+
est_output_tokens,
|
|
1472
|
+
input_chars: inputText.length,
|
|
1473
|
+
output_chars: outputText.length,
|
|
1474
|
+
duration_ms,
|
|
1475
|
+
est_cost_usd
|
|
1476
|
+
});
|
|
1477
|
+
}
|
|
1478
|
+
function recordCacheHit(dir, workflow_id, stage, inputText, agent, model) {
|
|
1479
|
+
appendEvent(dir, {
|
|
1480
|
+
ts: new Date().toISOString(),
|
|
1481
|
+
workflow_id,
|
|
1482
|
+
stage,
|
|
1483
|
+
event: "cache_hit",
|
|
1484
|
+
agent,
|
|
1485
|
+
model,
|
|
1486
|
+
est_input_tokens: estimateTokens(inputText),
|
|
1487
|
+
est_output_tokens: 0,
|
|
1488
|
+
input_chars: inputText.length,
|
|
1489
|
+
output_chars: 0
|
|
1490
|
+
});
|
|
1491
|
+
}
|
|
1492
|
+
function recordRetryCall(dir, workflow_id, stage, inputText, outputText, agent, duration_ms, model, est_cost_usd) {
|
|
1493
|
+
const est_input_tokens = estimateTokens(inputText);
|
|
1494
|
+
const est_output_tokens = estimateTokens(outputText);
|
|
1495
|
+
appendEvent(dir, {
|
|
1496
|
+
ts: new Date().toISOString(),
|
|
1497
|
+
workflow_id,
|
|
1498
|
+
stage,
|
|
1499
|
+
event: "retry",
|
|
1500
|
+
agent,
|
|
1501
|
+
model,
|
|
1502
|
+
est_input_tokens,
|
|
1503
|
+
est_output_tokens,
|
|
1504
|
+
input_chars: inputText.length,
|
|
1505
|
+
output_chars: outputText.length,
|
|
1506
|
+
duration_ms,
|
|
1507
|
+
est_cost_usd
|
|
1508
|
+
});
|
|
1509
|
+
}
|
|
1510
|
+
var _workflowTimers = new Map;
|
|
1511
|
+
|
|
1512
|
+
// src/config/loader.ts
|
|
1513
|
+
import { existsSync as existsSync10, readFileSync as readFileSync10 } from "fs";
|
|
1514
|
+
import { join as join10 } from "path";
|
|
1515
|
+
import { homedir } from "os";
|
|
1516
|
+
var CONFIG_FILENAME = "flowdeck.json";
|
|
1517
|
+
function getGlobalConfigDir() {
|
|
1518
|
+
return process.env.OPENCODE_CONFIG_DIR || (process.env.XDG_CONFIG_HOME ? join10(process.env.XDG_CONFIG_HOME, "opencode") : join10(homedir(), ".config", "opencode"));
|
|
1519
|
+
}
|
|
1520
|
+
function loadFlowDeckConfig(directory) {
|
|
1521
|
+
const candidates = [];
|
|
1522
|
+
if (directory) {
|
|
1523
|
+
candidates.push(join10(directory, ".opencode", CONFIG_FILENAME));
|
|
1524
|
+
}
|
|
1525
|
+
candidates.push(join10(getGlobalConfigDir(), CONFIG_FILENAME));
|
|
1526
|
+
for (const configPath of candidates) {
|
|
1527
|
+
if (existsSync10(configPath)) {
|
|
1528
|
+
try {
|
|
1529
|
+
const content = readFileSync10(configPath, "utf-8");
|
|
1530
|
+
return JSON.parse(content);
|
|
1531
|
+
} catch {
|
|
1532
|
+
console.warn(`[flowdeck] Failed to load config from ${configPath}`);
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
return {};
|
|
1537
|
+
}
|
|
1538
|
+
function resolveDesignFirstConfig(config) {
|
|
1539
|
+
return {
|
|
1540
|
+
enabled: config.designFirst?.enabled ?? true,
|
|
1541
|
+
enforcement: config.designFirst?.enforcement ?? "strict",
|
|
1542
|
+
requireApprovalBeforeImplementation: config.designFirst?.requireApprovalBeforeImplementation ?? true,
|
|
1543
|
+
modelOverrides: config.designFirst?.modelOverrides ?? {},
|
|
1544
|
+
defaultSkillsByTaskType: config.designFirst?.defaultSkillsByTaskType ?? {
|
|
1545
|
+
"landing-page": ["landing-page-design", "wireframe-planning", "design-system-definition", "frontend-handoff"],
|
|
1546
|
+
dashboard: ["dashboard-design", "ui-ux-planning", "wireframe-planning", "responsive-review"],
|
|
1547
|
+
"admin-panel": ["ui-ux-planning", "wireframe-planning", "design-system-definition", "frontend-handoff"],
|
|
1548
|
+
"app-screen": ["app-shell-design", "ui-ux-planning", "wireframe-planning", "responsive-review"],
|
|
1549
|
+
"general-ui": ["ui-ux-planning", "wireframe-planning", "design-system-definition", "frontend-handoff"]
|
|
1550
|
+
}
|
|
1551
|
+
};
|
|
1552
|
+
}
|
|
1553
|
+
// src/services/cost-estimator.ts
|
|
1554
|
+
var PRICING_TABLE = [
|
|
1555
|
+
{ prefix: "claude-opus-4", pricing: { input: 15, output: 75 } },
|
|
1556
|
+
{ prefix: "claude-opus", pricing: { input: 15, output: 75 } },
|
|
1557
|
+
{ prefix: "claude-sonnet-4", pricing: { input: 3, output: 15 } },
|
|
1558
|
+
{ prefix: "claude-sonnet-3-5", pricing: { input: 3, output: 15 } },
|
|
1559
|
+
{ prefix: "claude-sonnet-3", pricing: { input: 3, output: 15 } },
|
|
1560
|
+
{ prefix: "claude-sonnet", pricing: { input: 3, output: 15 } },
|
|
1561
|
+
{ prefix: "claude-haiku-4", pricing: { input: 0.8, output: 4 } },
|
|
1562
|
+
{ prefix: "claude-haiku-3-5", pricing: { input: 0.8, output: 4 } },
|
|
1563
|
+
{ prefix: "claude-haiku", pricing: { input: 0.25, output: 1.25 } },
|
|
1564
|
+
{ prefix: "claude-3-opus", pricing: { input: 15, output: 75 } },
|
|
1565
|
+
{ prefix: "claude-3-5-sonnet", pricing: { input: 3, output: 15 } },
|
|
1566
|
+
{ prefix: "claude-3-sonnet", pricing: { input: 3, output: 15 } },
|
|
1567
|
+
{ prefix: "claude-3-haiku", pricing: { input: 0.25, output: 1.25 } },
|
|
1568
|
+
{ prefix: "claude", pricing: { input: 3, output: 15 } },
|
|
1569
|
+
{ prefix: "gpt-5.4-mini", pricing: { input: 0.15, output: 0.6 } },
|
|
1570
|
+
{ prefix: "gpt-5-mini", pricing: { input: 0.15, output: 0.6 } },
|
|
1571
|
+
{ prefix: "gpt-4.1", pricing: { input: 2, output: 8 } },
|
|
1572
|
+
{ prefix: "gpt-4o-mini", pricing: { input: 0.15, output: 0.6 } },
|
|
1573
|
+
{ prefix: "gpt-4o", pricing: { input: 2.5, output: 10 } },
|
|
1574
|
+
{ prefix: "gpt-4-turbo", pricing: { input: 10, output: 30 } },
|
|
1575
|
+
{ prefix: "gpt-4", pricing: { input: 30, output: 60 } },
|
|
1576
|
+
{ prefix: "gpt-3.5", pricing: { input: 0.5, output: 1.5 } },
|
|
1577
|
+
{ prefix: "gpt-5", pricing: { input: 10, output: 30 } },
|
|
1578
|
+
{ prefix: "o3-mini", pricing: { input: 1.1, output: 4.4 } },
|
|
1579
|
+
{ prefix: "o3", pricing: { input: 10, output: 40 } },
|
|
1580
|
+
{ prefix: "o1-mini", pricing: { input: 1.1, output: 4.4 } },
|
|
1581
|
+
{ prefix: "o1", pricing: { input: 15, output: 60 } },
|
|
1582
|
+
{ prefix: "gemini-2.0-flash", pricing: { input: 0.1, output: 0.4 } },
|
|
1583
|
+
{ prefix: "gemini-2.5-flash", pricing: { input: 0.15, output: 0.6 } },
|
|
1584
|
+
{ prefix: "gemini-2.5-pro", pricing: { input: 1.25, output: 5 } },
|
|
1585
|
+
{ prefix: "gemini-1.5-flash", pricing: { input: 0.075, output: 0.3 } },
|
|
1586
|
+
{ prefix: "gemini-1.5-pro", pricing: { input: 1.25, output: 5 } },
|
|
1587
|
+
{ prefix: "gemini", pricing: { input: 0.1, output: 0.4 } },
|
|
1588
|
+
{ prefix: "github-copilot/sonnet", pricing: { input: 3, output: 15 } },
|
|
1589
|
+
{ prefix: "github-copilot/haiku", pricing: { input: 0.25, output: 1.25 } },
|
|
1590
|
+
{ prefix: "github-copilot/gpt-4", pricing: { input: 2.5, output: 10 } },
|
|
1591
|
+
{ prefix: "github-copilot", pricing: { input: 3, output: 15 } }
|
|
1592
|
+
];
|
|
1593
|
+
var FALLBACK_PRICING = { input: 3, output: 15 };
|
|
1594
|
+
function getModelPricing(model) {
|
|
1595
|
+
if (!model)
|
|
1596
|
+
return FALLBACK_PRICING;
|
|
1597
|
+
const lower = model.toLowerCase();
|
|
1598
|
+
for (const entry of PRICING_TABLE) {
|
|
1599
|
+
if (lower.startsWith(entry.prefix.toLowerCase()))
|
|
1600
|
+
return entry.pricing;
|
|
1601
|
+
}
|
|
1602
|
+
return FALLBACK_PRICING;
|
|
1603
|
+
}
|
|
1604
|
+
function estimateCostUSD(model, inputTokens, outputTokens) {
|
|
1605
|
+
const pricing = getModelPricing(model);
|
|
1606
|
+
return inputTokens / 1e6 * pricing.input + outputTokens / 1e6 * pricing.output;
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
// src/tools/delegate.ts
|
|
839
1610
|
function extractText2(parts) {
|
|
840
1611
|
return parts.filter((p) => p.type === "text" && typeof p.text === "string").map((p) => p.text).join(`
|
|
841
1612
|
`);
|
|
842
1613
|
}
|
|
843
|
-
function createDelegateTool(client) {
|
|
1614
|
+
function createDelegateTool(client, reporter) {
|
|
844
1615
|
return tool5({
|
|
845
1616
|
description: "Delegate a task to a single agent via a child session. Returns the agent's output.",
|
|
846
1617
|
args: {
|
|
@@ -848,13 +1619,61 @@ function createDelegateTool(client) {
|
|
|
848
1619
|
prompt: tool5.schema.string(),
|
|
849
1620
|
context: tool5.schema.string().optional(),
|
|
850
1621
|
task_type: tool5.schema.string().optional(),
|
|
851
|
-
retry_attempts: tool5.schema.number().optional().default(1)
|
|
1622
|
+
retry_attempts: tool5.schema.number().optional().default(1),
|
|
1623
|
+
safe_to_cache: tool5.schema.boolean().optional().default(false),
|
|
1624
|
+
cache_ttl_ms: tool5.schema.number().optional(),
|
|
1625
|
+
workflow_id: tool5.schema.string().optional(),
|
|
1626
|
+
stage: tool5.schema.string().optional()
|
|
852
1627
|
},
|
|
853
1628
|
async execute(args, context) {
|
|
854
1629
|
const startTime = Date.now();
|
|
855
1630
|
const taskType = normalizeTaskType(args.task_type, args.agent);
|
|
856
1631
|
const retryAttempts = typeof args.retry_attempts === "number" ? args.retry_attempts : 1;
|
|
857
1632
|
const maxRetries = Math.max(0, Math.floor(retryAttempts));
|
|
1633
|
+
let agentModel = "";
|
|
1634
|
+
try {
|
|
1635
|
+
const cfg = loadFlowDeckConfig(context.directory);
|
|
1636
|
+
agentModel = cfg.agents?.[args.agent]?.model ?? "";
|
|
1637
|
+
} catch {}
|
|
1638
|
+
const metricsWorkflowId = args.workflow_id ?? "";
|
|
1639
|
+
const metricsStage = args.stage ?? "delegate";
|
|
1640
|
+
const fullPrompt = args.context ? `${args.context}
|
|
1641
|
+
|
|
1642
|
+
---
|
|
1643
|
+
|
|
1644
|
+
${args.prompt}` : args.prompt;
|
|
1645
|
+
reporter?.reportToolStarted("delegate", summarize(args.prompt, 100), {
|
|
1646
|
+
agent: args.agent,
|
|
1647
|
+
stage: args.stage
|
|
1648
|
+
});
|
|
1649
|
+
const safe_to_cache = args.safe_to_cache === true && CACHEABLE_AGENTS.has(args.agent);
|
|
1650
|
+
let stateVersion = 0;
|
|
1651
|
+
let indexVersion = 0;
|
|
1652
|
+
if (safe_to_cache) {
|
|
1653
|
+
const index = readCodebaseIndex(context.directory);
|
|
1654
|
+
const sp = statePath(context.directory);
|
|
1655
|
+
const rawState = existsSync11(sp) ? readFileSync11(sp, "utf-8") : "";
|
|
1656
|
+
const state = rawState ? parseState(rawState) : {};
|
|
1657
|
+
stateVersion = typeof state.summaryVersion === "number" ? state.summaryVersion : 0;
|
|
1658
|
+
indexVersion = typeof index.summaryVersion === "number" ? index.summaryVersion : 0;
|
|
1659
|
+
const cached = getCached(context.directory, args.agent, fullPrompt, args.context ?? "", stateVersion, indexVersion, true);
|
|
1660
|
+
if (cached !== null) {
|
|
1661
|
+
if (metricsWorkflowId) {
|
|
1662
|
+
recordCacheHit(context.directory, metricsWorkflowId, metricsStage, fullPrompt, args.agent, agentModel);
|
|
1663
|
+
}
|
|
1664
|
+
reporter?.reportCacheHit("delegate", args.agent);
|
|
1665
|
+
return JSON.stringify({
|
|
1666
|
+
agent: args.agent,
|
|
1667
|
+
success: true,
|
|
1668
|
+
output: cached,
|
|
1669
|
+
task_type: taskType,
|
|
1670
|
+
model: "",
|
|
1671
|
+
retries_used: 0,
|
|
1672
|
+
duration_ms: Date.now() - startTime,
|
|
1673
|
+
cached: true
|
|
1674
|
+
});
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
858
1677
|
const createRes = await client.session.create({
|
|
859
1678
|
body: { parentID: context.sessionID, title: `${args.agent}-delegate` },
|
|
860
1679
|
query: { directory: context.directory }
|
|
@@ -874,7 +1693,7 @@ function createDelegateTool(client) {
|
|
|
874
1693
|
query: { directory: context.directory }
|
|
875
1694
|
}).catch(() => {});
|
|
876
1695
|
});
|
|
877
|
-
const
|
|
1696
|
+
const fullPromptForSession = args.context ? `${args.context}
|
|
878
1697
|
|
|
879
1698
|
---
|
|
880
1699
|
|
|
@@ -882,26 +1701,38 @@ ${args.prompt}` : args.prompt;
|
|
|
882
1701
|
let promptRes = null;
|
|
883
1702
|
let retriesUsed = 0;
|
|
884
1703
|
for (let attempt = 0;attempt <= maxRetries; attempt++) {
|
|
1704
|
+
const attemptStart = Date.now();
|
|
885
1705
|
promptRes = await client.session.prompt({
|
|
886
1706
|
path: { id: childId },
|
|
887
1707
|
body: {
|
|
888
1708
|
agent: args.agent,
|
|
889
|
-
parts: [{ type: "text", text:
|
|
1709
|
+
parts: [{ type: "text", text: fullPromptForSession }],
|
|
890
1710
|
tools: { question: false }
|
|
891
1711
|
},
|
|
892
1712
|
query: { directory: context.directory }
|
|
893
1713
|
});
|
|
894
1714
|
if (!shouldRetry(promptRes) || attempt === maxRetries)
|
|
895
1715
|
break;
|
|
1716
|
+
if (metricsWorkflowId) {
|
|
1717
|
+
const retryInputTokens = estimateTokens(fullPromptForSession);
|
|
1718
|
+
const retryCostUsd = agentModel ? estimateCostUSD(agentModel, retryInputTokens, 0) : undefined;
|
|
1719
|
+
recordRetryCall(context.directory, metricsWorkflowId, metricsStage, fullPromptForSession, "", args.agent, Date.now() - attemptStart, agentModel, retryCostUsd);
|
|
1720
|
+
}
|
|
896
1721
|
retriesUsed++;
|
|
1722
|
+
reporter?.reportToolRetried("delegate", retriesUsed, "prompt response indicated retry", { agent: args.agent });
|
|
897
1723
|
}
|
|
898
1724
|
if (!promptRes || promptRes.error) {
|
|
1725
|
+
const errMsg = `Prompt failed: ${promptRes?.error?.detail ?? "unknown"}`;
|
|
899
1726
|
recordRun(context.directory, args.agent, "", taskType, false, Date.now() - startTime);
|
|
1727
|
+
reporter?.reportToolFailed("delegate", Date.now() - startTime, errMsg, {
|
|
1728
|
+
agent: args.agent,
|
|
1729
|
+
retry_count: retriesUsed
|
|
1730
|
+
});
|
|
900
1731
|
return JSON.stringify({
|
|
901
1732
|
agent: args.agent,
|
|
902
1733
|
session_id: childId,
|
|
903
1734
|
success: false,
|
|
904
|
-
error:
|
|
1735
|
+
error: errMsg,
|
|
905
1736
|
task_type: taskType,
|
|
906
1737
|
model: "",
|
|
907
1738
|
retries_used: retriesUsed,
|
|
@@ -910,12 +1741,17 @@ ${args.prompt}` : args.prompt;
|
|
|
910
1741
|
}
|
|
911
1742
|
const info = promptRes.data?.info;
|
|
912
1743
|
if (info?.error) {
|
|
1744
|
+
const errMsg = `Agent error: ${JSON.stringify(info.error)}`;
|
|
913
1745
|
recordRun(context.directory, args.agent, "", taskType, false, Date.now() - startTime);
|
|
1746
|
+
reporter?.reportToolFailed("delegate", Date.now() - startTime, errMsg, {
|
|
1747
|
+
agent: args.agent,
|
|
1748
|
+
retry_count: retriesUsed
|
|
1749
|
+
});
|
|
914
1750
|
return JSON.stringify({
|
|
915
1751
|
agent: args.agent,
|
|
916
1752
|
session_id: childId,
|
|
917
1753
|
success: false,
|
|
918
|
-
error:
|
|
1754
|
+
error: errMsg,
|
|
919
1755
|
task_type: taskType,
|
|
920
1756
|
model: "",
|
|
921
1757
|
retries_used: retriesUsed,
|
|
@@ -924,6 +1760,19 @@ ${args.prompt}` : args.prompt;
|
|
|
924
1760
|
}
|
|
925
1761
|
const output = extractText2(promptRes.data?.parts ?? []);
|
|
926
1762
|
recordRun(context.directory, args.agent, "", taskType, true, Date.now() - startTime);
|
|
1763
|
+
if (metricsWorkflowId) {
|
|
1764
|
+
const inputTokens = estimateTokens(fullPromptForSession);
|
|
1765
|
+
const outputTokens = estimateTokens(output);
|
|
1766
|
+
const costUsd = agentModel ? estimateCostUSD(agentModel, inputTokens, outputTokens) : undefined;
|
|
1767
|
+
recordModelCall(context.directory, metricsWorkflowId, metricsStage, fullPromptForSession, output, args.agent, Date.now() - startTime, agentModel, costUsd);
|
|
1768
|
+
}
|
|
1769
|
+
reporter?.reportToolCompleted("delegate", Date.now() - startTime, summarize(output, 80), {
|
|
1770
|
+
agent: args.agent,
|
|
1771
|
+
retry_count: retriesUsed
|
|
1772
|
+
});
|
|
1773
|
+
if (safe_to_cache && output) {
|
|
1774
|
+
setCached(context.directory, args.agent, fullPromptForSession, args.context ?? "", stateVersion, indexVersion, output, true, args.cache_ttl_ms);
|
|
1775
|
+
}
|
|
927
1776
|
return JSON.stringify({
|
|
928
1777
|
agent: args.agent,
|
|
929
1778
|
session_id: childId,
|
|
@@ -940,31 +1789,31 @@ ${args.prompt}` : args.prompt;
|
|
|
940
1789
|
|
|
941
1790
|
// src/tools/repo-memory.ts
|
|
942
1791
|
import { tool as tool6 } from "@opencode-ai/plugin";
|
|
943
|
-
import { readFileSync as
|
|
944
|
-
import { join as
|
|
1792
|
+
import { readFileSync as readFileSync12, writeFileSync as writeFileSync8, existsSync as existsSync12, mkdirSync as mkdirSync6 } from "fs";
|
|
1793
|
+
import { join as join11 } from "path";
|
|
945
1794
|
var MEMORY_FILE = "MEMORY.json";
|
|
946
1795
|
function memoryPath(directory) {
|
|
947
|
-
return
|
|
1796
|
+
return join11(codebaseDir(directory), MEMORY_FILE);
|
|
948
1797
|
}
|
|
949
1798
|
function emptyMemory() {
|
|
950
1799
|
return { version: "1.0", last_updated: new Date().toISOString(), nodes: {} };
|
|
951
1800
|
}
|
|
952
1801
|
function readMemory(directory) {
|
|
953
1802
|
const p = memoryPath(directory);
|
|
954
|
-
if (!
|
|
1803
|
+
if (!existsSync12(p))
|
|
955
1804
|
return emptyMemory();
|
|
956
1805
|
try {
|
|
957
|
-
return JSON.parse(
|
|
1806
|
+
return JSON.parse(readFileSync12(p, "utf-8"));
|
|
958
1807
|
} catch {
|
|
959
1808
|
return emptyMemory();
|
|
960
1809
|
}
|
|
961
1810
|
}
|
|
962
1811
|
function writeMemory(directory, memory) {
|
|
963
1812
|
const base = codebaseDir(directory);
|
|
964
|
-
if (!
|
|
965
|
-
|
|
1813
|
+
if (!existsSync12(base))
|
|
1814
|
+
mkdirSync6(base, { recursive: true });
|
|
966
1815
|
memory.last_updated = new Date().toISOString();
|
|
967
|
-
|
|
1816
|
+
writeFileSync8(memoryPath(directory), JSON.stringify(memory, null, 2), "utf-8");
|
|
968
1817
|
}
|
|
969
1818
|
var repoMemoryTool = tool6({
|
|
970
1819
|
description: "Repo Memory Graph: read/write/query persistent architecture graph in .codebase/MEMORY.json (modules, dependencies, ownership, bug history, conventions)",
|
|
@@ -1041,28 +1890,28 @@ var repoMemoryTool = tool6({
|
|
|
1041
1890
|
|
|
1042
1891
|
// src/tools/failure-replay.ts
|
|
1043
1892
|
import { tool as tool7 } from "@opencode-ai/plugin";
|
|
1044
|
-
import { readFileSync as
|
|
1045
|
-
import { join as
|
|
1893
|
+
import { readFileSync as readFileSync13, writeFileSync as writeFileSync9, existsSync as existsSync13, mkdirSync as mkdirSync7 } from "fs";
|
|
1894
|
+
import { join as join12 } from "path";
|
|
1046
1895
|
var FAILURES_FILE = "FAILURES.json";
|
|
1047
1896
|
function failuresPath(directory) {
|
|
1048
|
-
return
|
|
1897
|
+
return join12(codebaseDir(directory), FAILURES_FILE);
|
|
1049
1898
|
}
|
|
1050
1899
|
function readStore(directory) {
|
|
1051
1900
|
const p = failuresPath(directory);
|
|
1052
|
-
if (!
|
|
1901
|
+
if (!existsSync13(p))
|
|
1053
1902
|
return { version: "1.0", last_updated: new Date().toISOString(), entries: [] };
|
|
1054
1903
|
try {
|
|
1055
|
-
return JSON.parse(
|
|
1904
|
+
return JSON.parse(readFileSync13(p, "utf-8"));
|
|
1056
1905
|
} catch {
|
|
1057
1906
|
return { version: "1.0", last_updated: new Date().toISOString(), entries: [] };
|
|
1058
1907
|
}
|
|
1059
1908
|
}
|
|
1060
1909
|
function writeStore(directory, store) {
|
|
1061
1910
|
const base = codebaseDir(directory);
|
|
1062
|
-
if (!
|
|
1063
|
-
|
|
1911
|
+
if (!existsSync13(base))
|
|
1912
|
+
mkdirSync7(base, { recursive: true });
|
|
1064
1913
|
store.last_updated = new Date().toISOString();
|
|
1065
|
-
|
|
1914
|
+
writeFileSync9(failuresPath(directory), JSON.stringify(store, null, 2), "utf-8");
|
|
1066
1915
|
}
|
|
1067
1916
|
var failureReplayTool = tool7({
|
|
1068
1917
|
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 +1995,17 @@ var failureReplayTool = tool7({
|
|
|
1146
1995
|
|
|
1147
1996
|
// src/tools/decision-trace.ts
|
|
1148
1997
|
import { tool as tool8 } from "@opencode-ai/plugin";
|
|
1149
|
-
import { readFileSync as
|
|
1150
|
-
import { join as
|
|
1998
|
+
import { readFileSync as readFileSync14, existsSync as existsSync14, mkdirSync as mkdirSync8, appendFileSync as appendFileSync2 } from "fs";
|
|
1999
|
+
import { join as join13 } from "path";
|
|
1151
2000
|
var DECISIONS_FILE = "DECISIONS.jsonl";
|
|
1152
2001
|
function decisionsPath(directory) {
|
|
1153
|
-
return
|
|
2002
|
+
return join13(codebaseDir(directory), DECISIONS_FILE);
|
|
1154
2003
|
}
|
|
1155
2004
|
function readDecisions(directory) {
|
|
1156
2005
|
const p = decisionsPath(directory);
|
|
1157
|
-
if (!
|
|
2006
|
+
if (!existsSync14(p))
|
|
1158
2007
|
return [];
|
|
1159
|
-
return
|
|
2008
|
+
return readFileSync14(p, "utf-8").split(`
|
|
1160
2009
|
`).filter((l) => l.trim()).map((l) => {
|
|
1161
2010
|
try {
|
|
1162
2011
|
return JSON.parse(l);
|
|
@@ -1196,10 +2045,10 @@ var decisionTraceTool = tool8({
|
|
|
1196
2045
|
case "record": {
|
|
1197
2046
|
if (!args.entry)
|
|
1198
2047
|
return JSON.stringify({ error: "entry required" });
|
|
1199
|
-
if (!
|
|
1200
|
-
|
|
2048
|
+
if (!existsSync14(base))
|
|
2049
|
+
mkdirSync8(base, { recursive: true });
|
|
1201
2050
|
const entry = { ...args.entry, timestamp: new Date().toISOString() };
|
|
1202
|
-
|
|
2051
|
+
appendFileSync2(decisionsPath(dir), JSON.stringify(entry) + `
|
|
1203
2052
|
`, "utf-8");
|
|
1204
2053
|
return JSON.stringify({ success: true, id: args.entry.id });
|
|
1205
2054
|
}
|
|
@@ -1231,28 +2080,28 @@ var decisionTraceTool = tool8({
|
|
|
1231
2080
|
|
|
1232
2081
|
// src/tools/volatility-map.ts
|
|
1233
2082
|
import { tool as tool9 } from "@opencode-ai/plugin";
|
|
1234
|
-
import { readFileSync as
|
|
1235
|
-
import { join as
|
|
2083
|
+
import { readFileSync as readFileSync15, writeFileSync as writeFileSync11, existsSync as existsSync15, mkdirSync as mkdirSync9 } from "fs";
|
|
2084
|
+
import { join as join14 } from "path";
|
|
1236
2085
|
var VOLATILITY_FILE = "VOLATILITY.json";
|
|
1237
2086
|
function volatilityPath(directory) {
|
|
1238
|
-
return
|
|
2087
|
+
return join14(codebaseDir(directory), VOLATILITY_FILE);
|
|
1239
2088
|
}
|
|
1240
2089
|
function readStore2(directory) {
|
|
1241
2090
|
const p = volatilityPath(directory);
|
|
1242
|
-
if (!
|
|
2091
|
+
if (!existsSync15(p))
|
|
1243
2092
|
return { version: "1.0", last_updated: new Date().toISOString(), generated_at: new Date().toISOString(), entries: [] };
|
|
1244
2093
|
try {
|
|
1245
|
-
return JSON.parse(
|
|
2094
|
+
return JSON.parse(readFileSync15(p, "utf-8"));
|
|
1246
2095
|
} catch {
|
|
1247
2096
|
return { version: "1.0", last_updated: new Date().toISOString(), generated_at: new Date().toISOString(), entries: [] };
|
|
1248
2097
|
}
|
|
1249
2098
|
}
|
|
1250
2099
|
function writeStore2(directory, store) {
|
|
1251
2100
|
const base = codebaseDir(directory);
|
|
1252
|
-
if (!
|
|
1253
|
-
|
|
2101
|
+
if (!existsSync15(base))
|
|
2102
|
+
mkdirSync9(base, { recursive: true });
|
|
1254
2103
|
store.last_updated = new Date().toISOString();
|
|
1255
|
-
|
|
2104
|
+
writeFileSync11(volatilityPath(directory), JSON.stringify(store, null, 2), "utf-8");
|
|
1256
2105
|
}
|
|
1257
2106
|
function stabilityLabel(churn, hotfixes, todos) {
|
|
1258
2107
|
const score = churn + hotfixes * 10 + todos * 2;
|
|
@@ -1339,28 +2188,28 @@ var volatilityMapTool = tool9({
|
|
|
1339
2188
|
|
|
1340
2189
|
// src/tools/policy-engine.ts
|
|
1341
2190
|
import { tool as tool10 } from "@opencode-ai/plugin";
|
|
1342
|
-
import { readFileSync as
|
|
1343
|
-
import { join as
|
|
2191
|
+
import { readFileSync as readFileSync16, writeFileSync as writeFileSync12, existsSync as existsSync16, mkdirSync as mkdirSync10 } from "fs";
|
|
2192
|
+
import { join as join15 } from "path";
|
|
1344
2193
|
var POLICIES_FILE = "POLICIES.json";
|
|
1345
2194
|
function policiesPath(directory) {
|
|
1346
|
-
return
|
|
2195
|
+
return join15(codebaseDir(directory), POLICIES_FILE);
|
|
1347
2196
|
}
|
|
1348
2197
|
function readStore3(directory) {
|
|
1349
2198
|
const p = policiesPath(directory);
|
|
1350
|
-
if (!
|
|
2199
|
+
if (!existsSync16(p))
|
|
1351
2200
|
return { version: "1.0", last_updated: new Date().toISOString(), policies: [] };
|
|
1352
2201
|
try {
|
|
1353
|
-
return JSON.parse(
|
|
2202
|
+
return JSON.parse(readFileSync16(p, "utf-8"));
|
|
1354
2203
|
} catch {
|
|
1355
2204
|
return { version: "1.0", last_updated: new Date().toISOString(), policies: [] };
|
|
1356
2205
|
}
|
|
1357
2206
|
}
|
|
1358
2207
|
function writeStore3(directory, store) {
|
|
1359
2208
|
const base = codebaseDir(directory);
|
|
1360
|
-
if (!
|
|
1361
|
-
|
|
2209
|
+
if (!existsSync16(base))
|
|
2210
|
+
mkdirSync10(base, { recursive: true });
|
|
1362
2211
|
store.last_updated = new Date().toISOString();
|
|
1363
|
-
|
|
2212
|
+
writeFileSync12(policiesPath(directory), JSON.stringify(store, null, 2), "utf-8");
|
|
1364
2213
|
}
|
|
1365
2214
|
var policyEngineTool = tool10({
|
|
1366
2215
|
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 +2291,8 @@ var policyEngineTool = tool10({
|
|
|
1442
2291
|
|
|
1443
2292
|
// src/tools/hash-edit.ts
|
|
1444
2293
|
import { tool as tool11 } from "@opencode-ai/plugin";
|
|
1445
|
-
import { readFileSync as
|
|
1446
|
-
import { createHash } from "crypto";
|
|
2294
|
+
import { readFileSync as readFileSync17, writeFileSync as writeFileSync13 } from "fs";
|
|
2295
|
+
import { createHash as createHash2 } from "crypto";
|
|
1447
2296
|
var hashEditTool = tool11({
|
|
1448
2297
|
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
2298
|
args: {
|
|
@@ -1456,7 +2305,7 @@ var hashEditTool = tool11({
|
|
|
1456
2305
|
const fullPath = args.filePath.startsWith("/") ? args.filePath : `${context.directory}/${args.filePath}`;
|
|
1457
2306
|
let content;
|
|
1458
2307
|
try {
|
|
1459
|
-
content =
|
|
2308
|
+
content = readFileSync17(fullPath, "utf-8");
|
|
1460
2309
|
} catch (e) {
|
|
1461
2310
|
return `Error: Could not read file ${args.filePath}`;
|
|
1462
2311
|
}
|
|
@@ -1464,37 +2313,76 @@ var hashEditTool = tool11({
|
|
|
1464
2313
|
return `Error: Target content not found in ${args.filePath}. It may have been modified by another agent.`;
|
|
1465
2314
|
}
|
|
1466
2315
|
if (args.expectedHash) {
|
|
1467
|
-
const actualHash =
|
|
2316
|
+
const actualHash = createHash2("md5").update(args.targetContent).digest("hex");
|
|
1468
2317
|
if (actualHash !== args.expectedHash) {
|
|
1469
2318
|
return `Error: Hash mismatch for target content. Expected ${args.expectedHash}, got ${actualHash}. Refusing to edit stale content.`;
|
|
1470
2319
|
}
|
|
1471
2320
|
}
|
|
1472
2321
|
const newContent = content.replace(args.targetContent, args.replacementContent);
|
|
1473
|
-
|
|
2322
|
+
writeFileSync13(fullPath, newContent, "utf-8");
|
|
1474
2323
|
return `Successfully updated ${args.filePath} using hash-anchored edit.`;
|
|
1475
2324
|
}
|
|
1476
2325
|
});
|
|
1477
2326
|
|
|
1478
2327
|
// src/tools/council.ts
|
|
1479
2328
|
import { tool as tool12 } from "@opencode-ai/plugin";
|
|
1480
|
-
import { appendFileSync as
|
|
1481
|
-
import { join as
|
|
2329
|
+
import { appendFileSync as appendFileSync3, existsSync as existsSync17, mkdirSync as mkdirSync11 } from "fs";
|
|
2330
|
+
import { join as join16 } from "path";
|
|
2331
|
+
import { createHash as createHash3 } from "crypto";
|
|
2332
|
+
import { readFileSync as readFileSync18 } from "fs";
|
|
2333
|
+
var _councilCache = new Map;
|
|
2334
|
+
var COUNCIL_CACHE_TTL_MS = 20 * 60 * 1000;
|
|
2335
|
+
function councilCacheKey(task, agents, stateVersion, indexVersion) {
|
|
2336
|
+
const sorted = [...agents].sort();
|
|
2337
|
+
return createHash3("sha256").update(JSON.stringify({ task: task.trim(), agents: sorted, sv: stateVersion, iv: indexVersion })).digest("hex").slice(0, 32);
|
|
2338
|
+
}
|
|
2339
|
+
async function runWithConcurrencyLimit(tasks, limit) {
|
|
2340
|
+
const results = new Array(tasks.length);
|
|
2341
|
+
let next = 0;
|
|
2342
|
+
async function worker() {
|
|
2343
|
+
while (next < tasks.length) {
|
|
2344
|
+
const idx = next++;
|
|
2345
|
+
results[idx] = await tasks[idx]();
|
|
2346
|
+
}
|
|
2347
|
+
}
|
|
2348
|
+
const workers = Array.from({ length: Math.min(limit, tasks.length) }, () => worker());
|
|
2349
|
+
await Promise.all(workers);
|
|
2350
|
+
return results;
|
|
2351
|
+
}
|
|
1482
2352
|
function createCouncilTool(client) {
|
|
1483
2353
|
return tool12({
|
|
1484
|
-
description: "Run an ensemble of agents (Council) on the same task to reach consensus or compare approaches. Runs
|
|
2354
|
+
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
2355
|
args: {
|
|
1486
2356
|
task: tool12.schema.string(),
|
|
1487
|
-
agents: tool12.schema.array(tool12.schema.string()).optional()
|
|
2357
|
+
agents: tool12.schema.array(tool12.schema.string()).optional(),
|
|
2358
|
+
force_fresh: tool12.schema.boolean().optional().default(false),
|
|
2359
|
+
max_concurrency: tool12.schema.number().optional().default(3)
|
|
1488
2360
|
},
|
|
1489
2361
|
async execute(args, context) {
|
|
1490
2362
|
const agents = args.agents || ["architect", "reviewer", "backend-coder"];
|
|
2363
|
+
const concurrencyLimit = Math.max(1, Math.min(5, typeof args.max_concurrency === "number" ? args.max_concurrency : 3));
|
|
2364
|
+
const index = readCodebaseIndex(context.directory);
|
|
2365
|
+
const sp = statePath(context.directory);
|
|
2366
|
+
const rawState = existsSync17(sp) ? readFileSync18(sp, "utf-8") : "";
|
|
2367
|
+
const state = rawState ? parseState(rawState) : {};
|
|
2368
|
+
const stateVersion = typeof state.summaryVersion === "number" ? state.summaryVersion : 0;
|
|
2369
|
+
const indexVersion = typeof index.summaryVersion === "number" ? index.summaryVersion : 0;
|
|
2370
|
+
if (!args.force_fresh) {
|
|
2371
|
+
const cacheKey2 = councilCacheKey(args.task, agents, stateVersion, indexVersion);
|
|
2372
|
+
const cached = _councilCache.get(cacheKey2);
|
|
2373
|
+
if (cached && Date.now() - cached.cached_at < COUNCIL_CACHE_TTL_MS) {
|
|
2374
|
+
return cached.synthesis + `
|
|
2375
|
+
|
|
2376
|
+
<!-- council: cached result -->`;
|
|
2377
|
+
}
|
|
2378
|
+
}
|
|
1491
2379
|
const tasks = agents.map((agent) => ({
|
|
1492
2380
|
agent,
|
|
1493
2381
|
prompt: `TASK: ${args.task}
|
|
1494
2382
|
|
|
1495
2383
|
Please provide your best analysis/implementation for this task. Your output will be compared with other agents in a council.`
|
|
1496
2384
|
}));
|
|
1497
|
-
const results = await
|
|
2385
|
+
const results = await runWithConcurrencyLimit(tasks.map((task) => async () => {
|
|
1498
2386
|
const createRes = await client.session.create({
|
|
1499
2387
|
body: { parentID: context.sessionID, title: `Council: ${task.agent}` },
|
|
1500
2388
|
query: { directory: context.directory }
|
|
@@ -1514,7 +2402,7 @@ Please provide your best analysis/implementation for this task. Your output will
|
|
|
1514
2402
|
const output = (promptRes.data?.parts ?? []).filter((p) => p.type === "text").map((p) => p.text).join(`
|
|
1515
2403
|
`);
|
|
1516
2404
|
return { agent: task.agent, output: output || "(no output)" };
|
|
1517
|
-
}));
|
|
2405
|
+
}), concurrencyLimit);
|
|
1518
2406
|
const synthesisPrompt = `You are a Council Synthesizer. Below are the outputs from ${results.length} different agents on the same task.
|
|
1519
2407
|
|
|
1520
2408
|
TASK: ${args.task}
|
|
@@ -1542,6 +2430,8 @@ Please synthesize these results. Identify areas of agreement, resolve conflicts,
|
|
|
1542
2430
|
synthesis,
|
|
1543
2431
|
created_at: new Date().toISOString()
|
|
1544
2432
|
});
|
|
2433
|
+
const cacheKey = councilCacheKey(args.task, agents, stateVersion, indexVersion);
|
|
2434
|
+
_councilCache.set(cacheKey, { synthesis, cached_at: Date.now() });
|
|
1545
2435
|
return synthesis;
|
|
1546
2436
|
}
|
|
1547
2437
|
});
|
|
@@ -1549,18 +2439,18 @@ Please synthesize these results. Identify areas of agreement, resolve conflicts,
|
|
|
1549
2439
|
function persistCouncilResult(directory, payload) {
|
|
1550
2440
|
try {
|
|
1551
2441
|
const base = codebaseDir(directory);
|
|
1552
|
-
if (!
|
|
1553
|
-
|
|
1554
|
-
const path =
|
|
1555
|
-
|
|
2442
|
+
if (!existsSync17(base))
|
|
2443
|
+
mkdirSync11(base, { recursive: true });
|
|
2444
|
+
const path = join16(base, "COUNCILS.jsonl");
|
|
2445
|
+
appendFileSync3(path, JSON.stringify(payload) + `
|
|
1556
2446
|
`, "utf-8");
|
|
1557
2447
|
} catch {}
|
|
1558
2448
|
}
|
|
1559
2449
|
|
|
1560
2450
|
// src/tools/context-generator.ts
|
|
1561
2451
|
import { tool as tool13 } from "@opencode-ai/plugin";
|
|
1562
|
-
import { writeFileSync as
|
|
1563
|
-
import { join as
|
|
2452
|
+
import { writeFileSync as writeFileSync14, existsSync as existsSync18, readFileSync as readFileSync19, readdirSync as readdirSync4, statSync as statSync2 } from "fs";
|
|
2453
|
+
import { join as join17 } from "path";
|
|
1564
2454
|
var contextGeneratorTool = tool13({
|
|
1565
2455
|
description: "Auto-generate or update hierarchical context files (AGENTS.md, CLAUDE.md) throughout the project. These files provide critical grounding for AI agents.",
|
|
1566
2456
|
args: {
|
|
@@ -1569,20 +2459,20 @@ var contextGeneratorTool = tool13({
|
|
|
1569
2459
|
},
|
|
1570
2460
|
async execute(args, context) {
|
|
1571
2461
|
const root = context.directory;
|
|
1572
|
-
const target = args.targetDir ?
|
|
1573
|
-
if (!
|
|
2462
|
+
const target = args.targetDir ? join17(root, args.targetDir) : root;
|
|
2463
|
+
if (!existsSync18(target)) {
|
|
1574
2464
|
return `Error: Directory ${target} does not exist.`;
|
|
1575
2465
|
}
|
|
1576
|
-
const agentsMdPath =
|
|
1577
|
-
if (
|
|
2466
|
+
const agentsMdPath = join17(target, "AGENTS.md");
|
|
2467
|
+
if (existsSync18(agentsMdPath) && !args.force) {
|
|
1578
2468
|
return `AGENTS.md already exists in ${target}. Use force: true to overwrite.`;
|
|
1579
2469
|
}
|
|
1580
|
-
const pkgPath =
|
|
2470
|
+
const pkgPath = join17(root, "package.json");
|
|
1581
2471
|
let projectName = "Project";
|
|
1582
2472
|
let techStack = "Unknown";
|
|
1583
|
-
if (
|
|
2473
|
+
if (existsSync18(pkgPath)) {
|
|
1584
2474
|
try {
|
|
1585
|
-
const pkg = JSON.parse(
|
|
2475
|
+
const pkg = JSON.parse(readFileSync19(pkgPath, "utf-8"));
|
|
1586
2476
|
projectName = pkg.name || projectName;
|
|
1587
2477
|
techStack = Object.keys(pkg.dependencies || {}).slice(0, 5).join(", ");
|
|
1588
2478
|
} catch {}
|
|
@@ -1599,8 +2489,8 @@ var contextGeneratorTool = tool13({
|
|
|
1599
2489
|
3. **Planning**: Always check \`.planning/STATE.md\` before executing major changes.
|
|
1600
2490
|
|
|
1601
2491
|
## Directory Map
|
|
1602
|
-
${
|
|
1603
|
-
const s =
|
|
2492
|
+
${readdirSync4(target).slice(0, 10).map((f) => {
|
|
2493
|
+
const s = statSync2(join17(target, f));
|
|
1604
2494
|
return `- \`${f}\`${s.isDirectory() ? "/" : ""} : [Description]`;
|
|
1605
2495
|
}).join(`
|
|
1606
2496
|
`)}
|
|
@@ -1608,17 +2498,17 @@ ${readdirSync2(target).slice(0, 10).map((f) => {
|
|
|
1608
2498
|
---
|
|
1609
2499
|
Generated by FlowDeck Context Generator.
|
|
1610
2500
|
`;
|
|
1611
|
-
|
|
2501
|
+
writeFileSync14(agentsMdPath, content, "utf-8");
|
|
1612
2502
|
return `Successfully generated AGENTS.md in ${target}.`;
|
|
1613
2503
|
}
|
|
1614
2504
|
});
|
|
1615
2505
|
|
|
1616
2506
|
// src/tools/create-skill.ts
|
|
1617
2507
|
import { tool as tool14 } from "@opencode-ai/plugin";
|
|
1618
|
-
import { mkdirSync as
|
|
1619
|
-
import { join as
|
|
2508
|
+
import { mkdirSync as mkdirSync12, writeFileSync as writeFileSync15, existsSync as existsSync19 } from "fs";
|
|
2509
|
+
import { join as join18, dirname as dirname3 } from "path";
|
|
1620
2510
|
import { fileURLToPath } from "url";
|
|
1621
|
-
var SKILLS_DIR =
|
|
2511
|
+
var SKILLS_DIR = join18(dirname3(fileURLToPath(import.meta.url)), "..", "skills");
|
|
1622
2512
|
var createSkillTool = tool14({
|
|
1623
2513
|
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
2514
|
args: {
|
|
@@ -1628,9 +2518,9 @@ var createSkillTool = tool14({
|
|
|
1628
2518
|
tags: tool14.schema.array(tool14.schema.string()).optional().describe("Optional tags for categorisation, e.g. ['performance', 'typescript']")
|
|
1629
2519
|
},
|
|
1630
2520
|
async execute(args) {
|
|
1631
|
-
const skillDir =
|
|
1632
|
-
const skillFile =
|
|
1633
|
-
if (
|
|
2521
|
+
const skillDir = join18(SKILLS_DIR, args.name);
|
|
2522
|
+
const skillFile = join18(skillDir, "SKILL.md");
|
|
2523
|
+
if (existsSync19(skillFile)) {
|
|
1634
2524
|
return `Skill '${args.name}' already exists at ${skillFile}.
|
|
1635
2525
|
` + `Use a different name or delete the existing skill directory first.`;
|
|
1636
2526
|
}
|
|
@@ -1645,8 +2535,8 @@ origin: FlowDeck (self-learned)${tagLine}
|
|
|
1645
2535
|
`;
|
|
1646
2536
|
const fullContent = frontmatter + args.content.trimStart();
|
|
1647
2537
|
try {
|
|
1648
|
-
|
|
1649
|
-
|
|
2538
|
+
mkdirSync12(skillDir, { recursive: true });
|
|
2539
|
+
writeFileSync15(skillFile, fullContent, "utf-8");
|
|
1650
2540
|
return `✓ Skill '${args.name}' created at ${skillFile}
|
|
1651
2541
|
|
|
1652
2542
|
` + `The skill is now part of the FlowDeck library. Restart OpenCode to load it into the active session.`;
|
|
@@ -1658,8 +2548,8 @@ origin: FlowDeck (self-learned)${tagLine}
|
|
|
1658
2548
|
|
|
1659
2549
|
// src/tools/reflect.ts
|
|
1660
2550
|
import { tool as tool15 } from "@opencode-ai/plugin";
|
|
1661
|
-
import { existsSync as
|
|
1662
|
-
import { join as
|
|
2551
|
+
import { existsSync as existsSync20, readFileSync as readFileSync20 } from "fs";
|
|
2552
|
+
import { join as join19 } from "path";
|
|
1663
2553
|
var MAX_ARTIFACT_BYTES = 4000;
|
|
1664
2554
|
function tail(text, maxBytes) {
|
|
1665
2555
|
if (text.length <= maxBytes)
|
|
@@ -1688,11 +2578,11 @@ var reflectTool = tool15({
|
|
|
1688
2578
|
];
|
|
1689
2579
|
let found = 0;
|
|
1690
2580
|
for (const [rel, label] of ARTIFACT_PATHS) {
|
|
1691
|
-
const full =
|
|
1692
|
-
if (!
|
|
2581
|
+
const full = join19(root, rel);
|
|
2582
|
+
if (!existsSync20(full))
|
|
1693
2583
|
continue;
|
|
1694
2584
|
try {
|
|
1695
|
-
const raw =
|
|
2585
|
+
const raw = readFileSync20(full, "utf-8").trim();
|
|
1696
2586
|
if (!raw)
|
|
1697
2587
|
continue;
|
|
1698
2588
|
const count = raw.split(`
|
|
@@ -1716,12 +2606,12 @@ import { tool as tool16 } from "@opencode-ai/plugin";
|
|
|
1716
2606
|
|
|
1717
2607
|
// src/services/codegraph.ts
|
|
1718
2608
|
import { spawnSync } from "child_process";
|
|
1719
|
-
import { existsSync as
|
|
1720
|
-
import { join as
|
|
2609
|
+
import { existsSync as existsSync21, readFileSync as readFileSync21, writeFileSync as writeFileSync16, mkdirSync as mkdirSync13 } from "fs";
|
|
2610
|
+
import { join as join20 } from "path";
|
|
1721
2611
|
var CODEGRAPH_META_FILE = "CODEGRAPH.md";
|
|
1722
2612
|
var MAX_FRESHNESS_MS = 30 * 60 * 1000;
|
|
1723
2613
|
function metaPath(dir) {
|
|
1724
|
-
return
|
|
2614
|
+
return join20(codebaseDir(dir), CODEGRAPH_META_FILE);
|
|
1725
2615
|
}
|
|
1726
2616
|
function isCodegraphInstalled() {
|
|
1727
2617
|
try {
|
|
@@ -1736,11 +2626,11 @@ function isCodegraphInstalled() {
|
|
|
1736
2626
|
}
|
|
1737
2627
|
}
|
|
1738
2628
|
function isCodegraphIndexed(dir) {
|
|
1739
|
-
return
|
|
2629
|
+
return existsSync21(join20(dir, ".codegraph", "codegraph.db"));
|
|
1740
2630
|
}
|
|
1741
2631
|
function readCodegraphMeta(dir) {
|
|
1742
2632
|
const path = metaPath(dir);
|
|
1743
|
-
if (!
|
|
2633
|
+
if (!existsSync21(path)) {
|
|
1744
2634
|
return {
|
|
1745
2635
|
installed: false,
|
|
1746
2636
|
indexed: false,
|
|
@@ -1753,7 +2643,7 @@ function readCodegraphMeta(dir) {
|
|
|
1753
2643
|
};
|
|
1754
2644
|
}
|
|
1755
2645
|
try {
|
|
1756
|
-
const content =
|
|
2646
|
+
const content = readFileSync21(path, "utf-8");
|
|
1757
2647
|
return parseCodegraphMeta(content);
|
|
1758
2648
|
} catch {
|
|
1759
2649
|
return {
|
|
@@ -1820,8 +2710,8 @@ function parseCodegraphMeta(content) {
|
|
|
1820
2710
|
}
|
|
1821
2711
|
function writeCodegraphMeta(dir, meta) {
|
|
1822
2712
|
const base = codebaseDir(dir);
|
|
1823
|
-
if (!
|
|
1824
|
-
|
|
2713
|
+
if (!existsSync21(base))
|
|
2714
|
+
mkdirSync13(base, { recursive: true });
|
|
1825
2715
|
const lines = [
|
|
1826
2716
|
"# Codegraph Metadata",
|
|
1827
2717
|
"",
|
|
@@ -1834,7 +2724,7 @@ function writeCodegraphMeta(dir, meta) {
|
|
|
1834
2724
|
`**installLog:** ${meta.installLog}`,
|
|
1835
2725
|
`**indexLog:** ${meta.indexLog}`
|
|
1836
2726
|
];
|
|
1837
|
-
|
|
2727
|
+
writeFileSync16(metaPath(dir), lines.join(`
|
|
1838
2728
|
`), "utf-8");
|
|
1839
2729
|
}
|
|
1840
2730
|
function isCodegraphFresh(dir, maxAgeMs = MAX_FRESHNESS_MS) {
|
|
@@ -2137,59 +3027,298 @@ var codegraphTool = tool16({
|
|
|
2137
3027
|
}
|
|
2138
3028
|
});
|
|
2139
3029
|
|
|
2140
|
-
// src/
|
|
2141
|
-
import {
|
|
2142
|
-
import {
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
3030
|
+
// src/tools/load-rules.ts
|
|
3031
|
+
import { tool as tool17 } from "@opencode-ai/plugin";
|
|
3032
|
+
import { existsSync as existsSync22, readFileSync as readFileSync22 } from "fs";
|
|
3033
|
+
import { join as join21, dirname as dirname4 } from "path";
|
|
3034
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
3035
|
+
var RULES_DIR = join21(dirname4(fileURLToPath2(import.meta.url)), "..", "rules");
|
|
3036
|
+
var _loadedPaths = new Set;
|
|
3037
|
+
var loadRulesTool = tool17({
|
|
3038
|
+
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).",
|
|
3039
|
+
args: {
|
|
3040
|
+
stage: tool17.schema.string().optional().describe("Current workflow stage: discuss | plan | execute | verify | fix-bug | write-docs"),
|
|
3041
|
+
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)."),
|
|
3042
|
+
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.")
|
|
3043
|
+
},
|
|
3044
|
+
async execute(args) {
|
|
3045
|
+
const rulesDir = existsSync22(RULES_DIR) ? RULES_DIR : null;
|
|
3046
|
+
if (!rulesDir) {
|
|
3047
|
+
return JSON.stringify({
|
|
3048
|
+
loaded: [],
|
|
3049
|
+
skipped_already_loaded: [],
|
|
3050
|
+
skipped_no_match: [],
|
|
3051
|
+
content: "",
|
|
3052
|
+
error: `Rules directory not found at ${RULES_DIR}`
|
|
3053
|
+
});
|
|
3054
|
+
}
|
|
3055
|
+
const context = {
|
|
3056
|
+
stage: args.stage,
|
|
3057
|
+
languages: args.languages
|
|
3058
|
+
};
|
|
3059
|
+
const selection = selectRulePaths(rulesDir, context);
|
|
3060
|
+
const diagnostics = buildSelectionDiagnostics(selection, context);
|
|
3061
|
+
const loaded = [];
|
|
3062
|
+
const skippedAlreadyLoaded = [];
|
|
3063
|
+
const contents = [];
|
|
3064
|
+
for (const rule of selection.selected) {
|
|
3065
|
+
const name = ruleShortName(rule);
|
|
3066
|
+
if (!args.force_reload && _loadedPaths.has(rule.path)) {
|
|
3067
|
+
skippedAlreadyLoaded.push(name);
|
|
3068
|
+
continue;
|
|
3069
|
+
}
|
|
2160
3070
|
try {
|
|
2161
|
-
const
|
|
2162
|
-
|
|
3071
|
+
const text = readFileSync22(rule.path, "utf-8");
|
|
3072
|
+
contents.push(`## ${name}
|
|
3073
|
+
|
|
3074
|
+
${text}`);
|
|
3075
|
+
_loadedPaths.add(rule.path);
|
|
3076
|
+
loaded.push(name);
|
|
2163
3077
|
} catch {
|
|
2164
|
-
|
|
3078
|
+
loaded.push(`${name} (read error)`);
|
|
2165
3079
|
}
|
|
2166
3080
|
}
|
|
3081
|
+
const skippedNoMatch = selection.skipped.map((r) => ({
|
|
3082
|
+
name: ruleShortName(r),
|
|
3083
|
+
reason: selection.reasons[r.path]
|
|
3084
|
+
}));
|
|
3085
|
+
return JSON.stringify({
|
|
3086
|
+
loaded,
|
|
3087
|
+
skipped_already_loaded: skippedAlreadyLoaded,
|
|
3088
|
+
skipped_no_match: skippedNoMatch,
|
|
3089
|
+
total_available: selection.total_discovered,
|
|
3090
|
+
diagnostics,
|
|
3091
|
+
content: contents.join(`
|
|
3092
|
+
|
|
3093
|
+
---
|
|
3094
|
+
|
|
3095
|
+
`)
|
|
3096
|
+
});
|
|
3097
|
+
}
|
|
3098
|
+
});
|
|
3099
|
+
function ruleShortName(rule) {
|
|
3100
|
+
return rule.path.replace(RULES_DIR + "/", "").replace(/\.md$/, "");
|
|
3101
|
+
}
|
|
3102
|
+
var listRulesTool = tool17({
|
|
3103
|
+
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.",
|
|
3104
|
+
args: {},
|
|
3105
|
+
async execute() {
|
|
3106
|
+
const rulesDir = existsSync22(RULES_DIR) ? RULES_DIR : null;
|
|
3107
|
+
if (!rulesDir) {
|
|
3108
|
+
return JSON.stringify({ rules: [], error: `Rules directory not found at ${RULES_DIR}` });
|
|
3109
|
+
}
|
|
3110
|
+
const all = discoverRules(rulesDir);
|
|
3111
|
+
return JSON.stringify({
|
|
3112
|
+
total: all.length,
|
|
3113
|
+
rules: all.map((r) => ({
|
|
3114
|
+
name: r.path.replace(RULES_DIR + "/", "").replace(/\.md$/, ""),
|
|
3115
|
+
description: r.description,
|
|
3116
|
+
always_on: r.always_on,
|
|
3117
|
+
stages: r.stages,
|
|
3118
|
+
languages: r.languages,
|
|
3119
|
+
loaded: _loadedPaths.has(r.path)
|
|
3120
|
+
}))
|
|
3121
|
+
});
|
|
3122
|
+
}
|
|
3123
|
+
});
|
|
3124
|
+
|
|
3125
|
+
// src/tools/rtk-setup.ts
|
|
3126
|
+
import { tool as tool18 } from "@opencode-ai/plugin";
|
|
3127
|
+
|
|
3128
|
+
// src/services/rtk-manager.ts
|
|
3129
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
3130
|
+
import { existsSync as existsSync23 } from "fs";
|
|
3131
|
+
import { homedir as homedir2 } from "os";
|
|
3132
|
+
import { join as join22 } from "path";
|
|
3133
|
+
|
|
3134
|
+
// src/services/rtk-policy.ts
|
|
3135
|
+
var SUPPORTED_COMMANDS = new Set([
|
|
3136
|
+
"git",
|
|
3137
|
+
"npm",
|
|
3138
|
+
"npx",
|
|
3139
|
+
"bun",
|
|
3140
|
+
"pnpm",
|
|
3141
|
+
"yarn",
|
|
3142
|
+
"tsc",
|
|
3143
|
+
"eslint",
|
|
3144
|
+
"biome",
|
|
3145
|
+
"oxlint",
|
|
3146
|
+
"jest",
|
|
3147
|
+
"vitest",
|
|
3148
|
+
"pytest",
|
|
3149
|
+
"cargo",
|
|
3150
|
+
"docker",
|
|
3151
|
+
"kubectl",
|
|
3152
|
+
"gh"
|
|
3153
|
+
]);
|
|
3154
|
+
var COMPACT_GIT_SUBCOMMANDS = new Set([
|
|
3155
|
+
"rev-parse",
|
|
3156
|
+
"hash-object",
|
|
3157
|
+
"cat-file",
|
|
3158
|
+
"ls-files",
|
|
3159
|
+
"ls-tree",
|
|
3160
|
+
"show-ref",
|
|
3161
|
+
"for-each-ref",
|
|
3162
|
+
"symbolic-ref",
|
|
3163
|
+
"config"
|
|
3164
|
+
]);
|
|
3165
|
+
var NEVER_WRAP = new Set(["codegraph", "curl", "sh", "bash", "zsh", "fish", "node", "python", "python3"]);
|
|
3166
|
+
function getSupportedCommands() {
|
|
3167
|
+
return [...SUPPORTED_COMMANDS].sort();
|
|
3168
|
+
}
|
|
3169
|
+
|
|
3170
|
+
// src/services/rtk-manager.ts
|
|
3171
|
+
var INSTALL_INSTRUCTIONS = [
|
|
3172
|
+
"rtk is not installed. To install it manually:",
|
|
3173
|
+
" Linux/macOS: curl -fsSL https://raw.githubusercontent.com/rtk-ai/rtk/refs/heads/master/install.sh | sh",
|
|
3174
|
+
" Then add ~/.local/bin to your PATH if needed.",
|
|
3175
|
+
"After installation, call rtk-setup again to verify detection."
|
|
3176
|
+
].join(`
|
|
3177
|
+
`);
|
|
3178
|
+
var CANDIDATE_PATHS = [join22(homedir2(), ".local", "bin", "rtk"), "/usr/local/bin/rtk", "/usr/bin/rtk"];
|
|
3179
|
+
function detectRtk() {
|
|
3180
|
+
const fromPath = spawnSync2("rtk", ["--version"], { encoding: "utf-8", timeout: 5000 });
|
|
3181
|
+
if (fromPath.status === 0) {
|
|
3182
|
+
const version = (fromPath.stdout ?? "").trim().split(`
|
|
3183
|
+
`)[0] ?? "";
|
|
3184
|
+
return { installed: true, binPath: "rtk", version };
|
|
3185
|
+
}
|
|
3186
|
+
for (const candidate of CANDIDATE_PATHS) {
|
|
3187
|
+
if (!existsSync23(candidate))
|
|
3188
|
+
continue;
|
|
3189
|
+
const result = spawnSync2(candidate, ["--version"], { encoding: "utf-8", timeout: 5000 });
|
|
3190
|
+
if (result.status === 0) {
|
|
3191
|
+
const version = (result.stdout ?? "").trim().split(`
|
|
3192
|
+
`)[0] ?? "";
|
|
3193
|
+
return { installed: true, binPath: candidate, version };
|
|
3194
|
+
}
|
|
2167
3195
|
}
|
|
2168
|
-
return {};
|
|
2169
|
-
}
|
|
2170
|
-
function resolveDesignFirstConfig(config) {
|
|
2171
3196
|
return {
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
3197
|
+
installed: false,
|
|
3198
|
+
error: "rtk binary not found in PATH or known install locations"
|
|
3199
|
+
};
|
|
3200
|
+
}
|
|
3201
|
+
function initRtk(binPath) {
|
|
3202
|
+
try {
|
|
3203
|
+
const result = spawnSync2(binPath, ["init", "-g"], {
|
|
3204
|
+
encoding: "utf-8",
|
|
3205
|
+
timeout: 30000,
|
|
3206
|
+
stdio: "pipe"
|
|
3207
|
+
});
|
|
3208
|
+
if (result.status !== 0) {
|
|
3209
|
+
return {
|
|
3210
|
+
success: false,
|
|
3211
|
+
log: (result.stdout ?? "").trim(),
|
|
3212
|
+
telemetryDisabled: false,
|
|
3213
|
+
error: (result.stderr ?? "").trim() || `rtk init -g exited with code ${result.status}`
|
|
3214
|
+
};
|
|
2182
3215
|
}
|
|
3216
|
+
const telResult = spawnSync2(binPath, ["telemetry", "disable"], {
|
|
3217
|
+
encoding: "utf-8",
|
|
3218
|
+
timeout: 1e4,
|
|
3219
|
+
stdio: "pipe"
|
|
3220
|
+
});
|
|
3221
|
+
return {
|
|
3222
|
+
success: true,
|
|
3223
|
+
log: [
|
|
3224
|
+
`[rtk] init -g succeeded: ${(result.stdout ?? "").trim()}`,
|
|
3225
|
+
`[rtk] telemetry disable: ${telResult.status === 0 ? "ok" : `failed (code ${telResult.status}) — ${(telResult.stderr ?? "").trim()}`}`
|
|
3226
|
+
].filter(Boolean).join(`
|
|
3227
|
+
`),
|
|
3228
|
+
telemetryDisabled: telResult.status === 0
|
|
3229
|
+
};
|
|
3230
|
+
} catch (err) {
|
|
3231
|
+
return { success: false, log: "", telemetryDisabled: false, error: String(err) };
|
|
3232
|
+
}
|
|
3233
|
+
}
|
|
3234
|
+
function getRtkStatus(opts) {
|
|
3235
|
+
const detection = detectRtk();
|
|
3236
|
+
if (!detection.installed) {
|
|
3237
|
+
return {
|
|
3238
|
+
installed: false,
|
|
3239
|
+
initAttempted: false,
|
|
3240
|
+
initSuccess: false,
|
|
3241
|
+
telemetryDisabled: false,
|
|
3242
|
+
installInstructions: INSTALL_INSTRUCTIONS
|
|
3243
|
+
};
|
|
3244
|
+
}
|
|
3245
|
+
let initAttempted = false;
|
|
3246
|
+
let initSuccess = false;
|
|
3247
|
+
let telemetryDisabled = false;
|
|
3248
|
+
if (opts?.runInit && detection.binPath) {
|
|
3249
|
+
initAttempted = true;
|
|
3250
|
+
const initResult = initRtk(detection.binPath);
|
|
3251
|
+
initSuccess = initResult.success;
|
|
3252
|
+
telemetryDisabled = initResult.telemetryDisabled;
|
|
3253
|
+
}
|
|
3254
|
+
return {
|
|
3255
|
+
installed: true,
|
|
3256
|
+
binPath: detection.binPath,
|
|
3257
|
+
version: detection.version,
|
|
3258
|
+
initAttempted,
|
|
3259
|
+
initSuccess,
|
|
3260
|
+
telemetryDisabled
|
|
2183
3261
|
};
|
|
2184
3262
|
}
|
|
3263
|
+
|
|
3264
|
+
// src/tools/rtk-setup.ts
|
|
3265
|
+
var rtkSetupTool = tool18({
|
|
3266
|
+
description: [
|
|
3267
|
+
"Detect, initialize, and report status of rtk (output compression proxy for CLI commands).",
|
|
3268
|
+
"rtk reduces noisy CLI output (git, npm, test runners, linters, docker) by 60-90%.",
|
|
3269
|
+
"Call this to check if rtk is available, to run `rtk init -g`, or to get the binary path.",
|
|
3270
|
+
"When RTK_INSTALLED=true in the environment, use `$RTK_BIN git status` for compressed output."
|
|
3271
|
+
].join(" "),
|
|
3272
|
+
args: {
|
|
3273
|
+
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.")
|
|
3274
|
+
},
|
|
3275
|
+
async execute(args) {
|
|
3276
|
+
const action = args.action ?? "status";
|
|
3277
|
+
const runInit = action === "init";
|
|
3278
|
+
const status = getRtkStatus({ runInit });
|
|
3279
|
+
const lines = ["## rtk Status"];
|
|
3280
|
+
if (status.installed) {
|
|
3281
|
+
lines.push(`- **Installed**: yes`);
|
|
3282
|
+
lines.push(`- **Binary**: ${status.binPath ?? "rtk (in PATH)"}`);
|
|
3283
|
+
if (status.version)
|
|
3284
|
+
lines.push(`- **Version**: ${status.version}`);
|
|
3285
|
+
if (runInit) {
|
|
3286
|
+
if (status.initAttempted) {
|
|
3287
|
+
lines.push(`- **Init**: ${status.initSuccess ? "✓ succeeded (bash hook installed)" : "✗ failed"}`);
|
|
3288
|
+
lines.push(`- **Telemetry**: ${status.telemetryDisabled ? "✓ disabled (`rtk telemetry disable` ran)" : "⚠ disable step failed — run `rtk telemetry disable` manually"}`);
|
|
3289
|
+
if (status.initSuccess) {
|
|
3290
|
+
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.");
|
|
3291
|
+
}
|
|
3292
|
+
}
|
|
3293
|
+
} else {
|
|
3294
|
+
lines.push("- **Init**: not requested (pass `action: 'init'` to install bash hook)");
|
|
3295
|
+
lines.push("- **Telemetry**: `RTK_TELEMETRY_DISABLED=1` is always set in bash sessions by FlowDeck");
|
|
3296
|
+
}
|
|
3297
|
+
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(`
|
|
3298
|
+
`));
|
|
3299
|
+
} else {
|
|
3300
|
+
lines.push("- **Installed**: no", "");
|
|
3301
|
+
lines.push("### Install rtk");
|
|
3302
|
+
if (status.installInstructions) {
|
|
3303
|
+
lines.push("```", status.installInstructions, "```");
|
|
3304
|
+
}
|
|
3305
|
+
lines.push("", "After installing, call `rtk-setup` again to verify detection.");
|
|
3306
|
+
}
|
|
3307
|
+
return lines.join(`
|
|
3308
|
+
`);
|
|
3309
|
+
}
|
|
3310
|
+
});
|
|
3311
|
+
|
|
2185
3312
|
// src/hooks/guard-rails.ts
|
|
3313
|
+
import { existsSync as existsSync24, readFileSync as readFileSync23 } from "fs";
|
|
3314
|
+
import { join as join23 } from "path";
|
|
2186
3315
|
var PLANNING_DIR2 = ".planning";
|
|
2187
3316
|
var CONFIG_FILE = "config.json";
|
|
2188
3317
|
var STATE_FILE2 = "STATE.md";
|
|
2189
3318
|
function resolveExecutionMode(configPath, trustScore, volatility) {
|
|
2190
|
-
if (
|
|
3319
|
+
if (existsSync24(configPath)) {
|
|
2191
3320
|
try {
|
|
2192
|
-
const config = JSON.parse(
|
|
3321
|
+
const config = JSON.parse(readFileSync23(configPath, "utf-8"));
|
|
2193
3322
|
if (config.execution_mode === "review-only")
|
|
2194
3323
|
return "review-only";
|
|
2195
3324
|
if (config.execution_mode === "guarded")
|
|
@@ -2243,22 +3372,22 @@ async function guardRailsHook(ctx, input, _output) {
|
|
|
2243
3372
|
if (!ENABLED)
|
|
2244
3373
|
return;
|
|
2245
3374
|
const dir = ctx.directory;
|
|
2246
|
-
const planningDirPath =
|
|
3375
|
+
const planningDirPath = join23(dir, PLANNING_DIR2);
|
|
2247
3376
|
const codebaseDirectory = codebaseDir(dir);
|
|
2248
|
-
const configPath =
|
|
2249
|
-
const statePath2 =
|
|
3377
|
+
const configPath = join23(planningDirPath, CONFIG_FILE);
|
|
3378
|
+
const statePath2 = join23(planningDirPath, STATE_FILE2);
|
|
2250
3379
|
const workspaceRoot = findWorkspaceRoot(dir);
|
|
2251
3380
|
if (workspaceRoot && dir !== workspaceRoot) {
|
|
2252
3381
|
const config = getWorkspaceConfig(dir);
|
|
2253
|
-
if (config && config.workspace_mode === "shared" && !
|
|
3382
|
+
if (config && config.workspace_mode === "shared" && !existsSync24(planningDirPath)) {
|
|
2254
3383
|
const msg = `No .planning/ in this sub-repo. Switch to workspace root: cd ${workspaceRoot}`;
|
|
2255
3384
|
throw new Error(`[flowdeck] BLOCK: ${msg}`);
|
|
2256
3385
|
}
|
|
2257
3386
|
}
|
|
2258
3387
|
if (input.tool === "write" || input.tool === "edit") {
|
|
2259
|
-
if (!
|
|
3388
|
+
if (!existsSync24(planningDirPath))
|
|
2260
3389
|
return;
|
|
2261
|
-
if (!
|
|
3390
|
+
if (!existsSync24(codebaseDirectory)) {
|
|
2262
3391
|
throw new Error(`[flowdeck] WARNING: .codebase/ not found. Run /fd-map-codebase to map the codebase.`);
|
|
2263
3392
|
}
|
|
2264
3393
|
const execMode = resolveExecutionMode(configPath, null);
|
|
@@ -2314,15 +3443,15 @@ function getDesignGateMessage(dir) {
|
|
|
2314
3443
|
}
|
|
2315
3444
|
function planSuggestsUiHeavy(dir, phase) {
|
|
2316
3445
|
const planPath = phasePlanPath(dir, phase);
|
|
2317
|
-
if (!
|
|
3446
|
+
if (!existsSync24(planPath))
|
|
2318
3447
|
return false;
|
|
2319
|
-
const planContent =
|
|
3448
|
+
const planContent = readFileSync23(planPath, "utf-8");
|
|
2320
3449
|
return isUiHeavyTask(planContent);
|
|
2321
3450
|
}
|
|
2322
3451
|
function effectiveSeverity(configPath, statePath2) {
|
|
2323
|
-
if (
|
|
3452
|
+
if (existsSync24(configPath)) {
|
|
2324
3453
|
try {
|
|
2325
|
-
const configContent =
|
|
3454
|
+
const configContent = readFileSync23(configPath, "utf-8");
|
|
2326
3455
|
const config = JSON.parse(configContent);
|
|
2327
3456
|
if (config.guard_enforcement === "warn")
|
|
2328
3457
|
return "warn";
|
|
@@ -2338,10 +3467,10 @@ function getEffectiveSeverity(configPath, statePath2) {
|
|
|
2338
3467
|
return effectiveSeverity(configPath, statePath2);
|
|
2339
3468
|
}
|
|
2340
3469
|
function getPlanConfirmed(statePath2) {
|
|
2341
|
-
if (!
|
|
3470
|
+
if (!existsSync24(statePath2))
|
|
2342
3471
|
return false;
|
|
2343
3472
|
try {
|
|
2344
|
-
const content =
|
|
3473
|
+
const content = readFileSync23(statePath2, "utf-8");
|
|
2345
3474
|
const match = content.match(/plan_confirmed:\s*(true|false)/i);
|
|
2346
3475
|
return match ? match[1].toLowerCase() === "true" : false;
|
|
2347
3476
|
} catch {
|
|
@@ -2349,32 +3478,32 @@ function getPlanConfirmed(statePath2) {
|
|
|
2349
3478
|
}
|
|
2350
3479
|
}
|
|
2351
3480
|
function getWarningMessage(planningDir2) {
|
|
2352
|
-
if (!
|
|
3481
|
+
if (!existsSync24(join23(planningDir2, STATE_FILE2))) {
|
|
2353
3482
|
return "No STATE.md found. Run /fd-map-codebase then /fd-new-feature to start a feature.";
|
|
2354
3483
|
}
|
|
2355
3484
|
return "Plan not confirmed. Run /fd-plan and confirm to enable execution.";
|
|
2356
3485
|
}
|
|
2357
3486
|
function getBlockMessage(planningDir2) {
|
|
2358
|
-
if (!
|
|
3487
|
+
if (!existsSync24(join23(planningDir2, STATE_FILE2))) {
|
|
2359
3488
|
return "No STATE.md found. Run /fd-map-codebase then /fd-new-feature to start a feature.";
|
|
2360
3489
|
}
|
|
2361
3490
|
return "Plan not confirmed. Run /fd-plan and confirm to enable execution.";
|
|
2362
3491
|
}
|
|
2363
3492
|
|
|
2364
3493
|
// src/hooks/tool-guard.ts
|
|
2365
|
-
import { existsSync as
|
|
2366
|
-
import { join as
|
|
3494
|
+
import { existsSync as existsSync25, readFileSync as readFileSync24 } from "fs";
|
|
3495
|
+
import { join as join24 } from "path";
|
|
2367
3496
|
var IS_ENABLED = () => process.env.FLOWDECK_TOOL_GUARD_ENABLED === "on";
|
|
2368
3497
|
var BLOCKED_PATTERNS = {
|
|
2369
3498
|
read: [".env", ".pem", ".key", ".secret"],
|
|
2370
3499
|
write: ["node_modules"],
|
|
2371
3500
|
bash: ["rm -rf"]
|
|
2372
3501
|
};
|
|
2373
|
-
function isBlocked(
|
|
2374
|
-
const patterns = BLOCKED_PATTERNS[
|
|
3502
|
+
function isBlocked(tool19, args) {
|
|
3503
|
+
const patterns = BLOCKED_PATTERNS[tool19];
|
|
2375
3504
|
if (!patterns)
|
|
2376
3505
|
return null;
|
|
2377
|
-
if (
|
|
3506
|
+
if (tool19 === "bash") {
|
|
2378
3507
|
const cmd = args.command;
|
|
2379
3508
|
if (!cmd)
|
|
2380
3509
|
return null;
|
|
@@ -2385,7 +3514,7 @@ function isBlocked(tool17, args) {
|
|
|
2385
3514
|
}
|
|
2386
3515
|
return null;
|
|
2387
3516
|
}
|
|
2388
|
-
if (
|
|
3517
|
+
if (tool19 === "read") {
|
|
2389
3518
|
const filePath = args.filePath;
|
|
2390
3519
|
if (!filePath)
|
|
2391
3520
|
return null;
|
|
@@ -2396,7 +3525,7 @@ function isBlocked(tool17, args) {
|
|
|
2396
3525
|
}
|
|
2397
3526
|
return null;
|
|
2398
3527
|
}
|
|
2399
|
-
if (
|
|
3528
|
+
if (tool19 === "write") {
|
|
2400
3529
|
const filePath = args.filePath;
|
|
2401
3530
|
if (!filePath)
|
|
2402
3531
|
return null;
|
|
@@ -2410,11 +3539,11 @@ function isBlocked(tool17, args) {
|
|
|
2410
3539
|
return null;
|
|
2411
3540
|
}
|
|
2412
3541
|
function checkArchConstraint(directory, filePath) {
|
|
2413
|
-
const constraintsPath =
|
|
2414
|
-
if (!
|
|
3542
|
+
const constraintsPath = join24(codebaseDir(directory), "CONSTRAINTS.md");
|
|
3543
|
+
if (!existsSync25(constraintsPath))
|
|
2415
3544
|
return null;
|
|
2416
3545
|
try {
|
|
2417
|
-
const content =
|
|
3546
|
+
const content = readFileSync24(constraintsPath, "utf-8");
|
|
2418
3547
|
const match = content.match(/## Forbidden Paths\n([\s\S]*?)(?:\n##|$)/);
|
|
2419
3548
|
if (!match)
|
|
2420
3549
|
return null;
|
|
@@ -2455,9 +3584,9 @@ function isUiDesignApprovalRequired(directory) {
|
|
|
2455
3584
|
return !(state.design_stage === "handoff_complete" && state.design_approved);
|
|
2456
3585
|
}
|
|
2457
3586
|
const planPath = phasePlanPath(directory, state.phase || 1);
|
|
2458
|
-
if (!
|
|
3587
|
+
if (!existsSync25(planPath))
|
|
2459
3588
|
return false;
|
|
2460
|
-
const planContent =
|
|
3589
|
+
const planContent = readFileSync24(planPath, "utf-8");
|
|
2461
3590
|
if (!isUiHeavyTask(planContent))
|
|
2462
3591
|
return false;
|
|
2463
3592
|
return !(state.design_stage === "handoff_complete" && state.design_approved);
|
|
@@ -2486,18 +3615,18 @@ async function toolGuardHook(ctx, input, output) {
|
|
|
2486
3615
|
}
|
|
2487
3616
|
|
|
2488
3617
|
// src/hooks/session-start.ts
|
|
2489
|
-
import { existsSync as
|
|
3618
|
+
import { existsSync as existsSync26, readFileSync as readFileSync25 } from "fs";
|
|
2490
3619
|
async function sessionStartHook(ctx) {
|
|
2491
3620
|
const planningDir2 = ctx.directory + "/.planning";
|
|
2492
3621
|
const codebaseDirectory = codebaseDir(ctx.directory);
|
|
2493
3622
|
const workspaceRoot = findWorkspaceRoot(ctx.directory);
|
|
2494
3623
|
const config = workspaceRoot ? getWorkspaceConfig(ctx.directory) : null;
|
|
2495
|
-
if (!
|
|
3624
|
+
if (!existsSync26(planningDir2)) {
|
|
2496
3625
|
return {
|
|
2497
3626
|
flowdeck_phase: null,
|
|
2498
3627
|
flowdeck_status: "no_plan",
|
|
2499
3628
|
flowdeck_warning: "Run /fd-map-codebase to index the codebase, then /fd-new-feature to start a feature.",
|
|
2500
|
-
flowdeck_has_codebase:
|
|
3629
|
+
flowdeck_has_codebase: existsSync26(codebaseDirectory),
|
|
2501
3630
|
...workspaceRoot && config?.sub_repos ? {
|
|
2502
3631
|
flowdeck_workspace_root: workspaceRoot,
|
|
2503
3632
|
flowdeck_sub_repos: config.sub_repos,
|
|
@@ -2508,7 +3637,7 @@ async function sessionStartHook(ctx) {
|
|
|
2508
3637
|
}
|
|
2509
3638
|
try {
|
|
2510
3639
|
const stateFilePath = statePath(ctx.directory);
|
|
2511
|
-
const content =
|
|
3640
|
+
const content = readFileSync25(stateFilePath, "utf-8");
|
|
2512
3641
|
const state = parseState(content);
|
|
2513
3642
|
const currentPhase = state["current_phase"] || {};
|
|
2514
3643
|
const result = {
|
|
@@ -2516,7 +3645,7 @@ async function sessionStartHook(ctx) {
|
|
|
2516
3645
|
flowdeck_status: currentPhase["status"] ?? null,
|
|
2517
3646
|
flowdeck_steps_pending: currentPhase["steps_pending"] ?? null,
|
|
2518
3647
|
flowdeck_last_action: currentPhase["last_action"] ?? null,
|
|
2519
|
-
flowdeck_has_codebase:
|
|
3648
|
+
flowdeck_has_codebase: existsSync26(codebaseDirectory)
|
|
2520
3649
|
};
|
|
2521
3650
|
if (workspaceRoot && config?.sub_repos && config.sub_repos.length > 0) {
|
|
2522
3651
|
result.flowdeck_workspace_root = workspaceRoot;
|
|
@@ -2531,7 +3660,7 @@ async function sessionStartHook(ctx) {
|
|
|
2531
3660
|
flowdeck_phase: null,
|
|
2532
3661
|
flowdeck_status: "error",
|
|
2533
3662
|
flowdeck_warning: "State file unreadable. Continuing without flowdeck context.",
|
|
2534
|
-
flowdeck_has_codebase:
|
|
3663
|
+
flowdeck_has_codebase: existsSync26(codebaseDirectory)
|
|
2535
3664
|
};
|
|
2536
3665
|
if (workspaceRoot && config?.sub_repos && config.sub_repos.length > 0) {
|
|
2537
3666
|
result.flowdeck_workspace_root = workspaceRoot;
|
|
@@ -2561,7 +3690,7 @@ var COMPLETION_COMMANDS = new Set([
|
|
|
2561
3690
|
"execute",
|
|
2562
3691
|
"verify"
|
|
2563
3692
|
]);
|
|
2564
|
-
function
|
|
3693
|
+
function normalizeCommandName2(raw) {
|
|
2565
3694
|
return raw.replace(/^\//, "").replace(/^fd-/, "");
|
|
2566
3695
|
}
|
|
2567
3696
|
function notify(title, body, level = "info") {
|
|
@@ -2606,7 +3735,7 @@ class NotificationController {
|
|
|
2606
3735
|
this.log = log;
|
|
2607
3736
|
}
|
|
2608
3737
|
onCommandExecuted(rawCommand) {
|
|
2609
|
-
const name =
|
|
3738
|
+
const name = normalizeCommandName2(rawCommand);
|
|
2610
3739
|
if (!INTERACTIVE_COMMANDS.has(name) && !COMPLETION_COMMANDS.has(name)) {
|
|
2611
3740
|
this.log(`[notify] command.executed: "${name}" — not a tracked command, skipping`);
|
|
2612
3741
|
return;
|
|
@@ -2670,13 +3799,13 @@ class NotificationController {
|
|
|
2670
3799
|
return this.lastNotifiedKey;
|
|
2671
3800
|
}
|
|
2672
3801
|
}
|
|
2673
|
-
function notifyPermissionNeeded(
|
|
2674
|
-
notify("FlowDeck Permission Required", `Agent needs approval to use tool: ${
|
|
3802
|
+
function notifyPermissionNeeded(tool19) {
|
|
3803
|
+
notify("FlowDeck Permission Required", `Agent needs approval to use tool: ${tool19}`, "critical");
|
|
2675
3804
|
}
|
|
2676
3805
|
|
|
2677
3806
|
// src/hooks/patch-trust.ts
|
|
2678
|
-
import { existsSync as
|
|
2679
|
-
import { join as
|
|
3807
|
+
import { existsSync as existsSync27, readFileSync as readFileSync26 } from "fs";
|
|
3808
|
+
import { join as join25 } from "path";
|
|
2680
3809
|
var HIGH_RISK_KEYWORDS = [
|
|
2681
3810
|
"password",
|
|
2682
3811
|
"secret",
|
|
@@ -2698,11 +3827,11 @@ var HIGH_RISK_KEYWORDS = [
|
|
|
2698
3827
|
"privilege"
|
|
2699
3828
|
];
|
|
2700
3829
|
function loadVolatility(directory) {
|
|
2701
|
-
const p =
|
|
2702
|
-
if (!
|
|
3830
|
+
const p = join25(codebaseDir(directory), "VOLATILITY.json");
|
|
3831
|
+
if (!existsSync27(p))
|
|
2703
3832
|
return {};
|
|
2704
3833
|
try {
|
|
2705
|
-
const data = JSON.parse(
|
|
3834
|
+
const data = JSON.parse(readFileSync26(p, "utf-8"));
|
|
2706
3835
|
const map = {};
|
|
2707
3836
|
for (const entry of data.entries ?? [])
|
|
2708
3837
|
map[entry.path] = entry.stability;
|
|
@@ -2712,11 +3841,11 @@ function loadVolatility(directory) {
|
|
|
2712
3841
|
}
|
|
2713
3842
|
}
|
|
2714
3843
|
function loadFailedPaths(directory) {
|
|
2715
|
-
const p =
|
|
2716
|
-
if (!
|
|
3844
|
+
const p = join25(codebaseDir(directory), "FAILURES.json");
|
|
3845
|
+
if (!existsSync27(p))
|
|
2717
3846
|
return [];
|
|
2718
3847
|
try {
|
|
2719
|
-
const data = JSON.parse(
|
|
3848
|
+
const data = JSON.parse(readFileSync26(p, "utf-8"));
|
|
2720
3849
|
return (data.entries ?? []).flatMap((e) => e.affected_paths ?? []);
|
|
2721
3850
|
} catch {
|
|
2722
3851
|
return [];
|
|
@@ -2781,8 +3910,8 @@ async function patchTrustHook(ctx, input, output) {
|
|
|
2781
3910
|
}
|
|
2782
3911
|
|
|
2783
3912
|
// src/hooks/decision-trace-hook.ts
|
|
2784
|
-
import { existsSync as
|
|
2785
|
-
import { join as
|
|
3913
|
+
import { existsSync as existsSync28, mkdirSync as mkdirSync14, appendFileSync as appendFileSync4 } from "fs";
|
|
3914
|
+
import { join as join26 } from "path";
|
|
2786
3915
|
async function decisionTraceHook(ctx, input, output) {
|
|
2787
3916
|
if (input.tool !== "write" && input.tool !== "edit")
|
|
2788
3917
|
return;
|
|
@@ -2791,8 +3920,8 @@ async function decisionTraceHook(ctx, input, output) {
|
|
|
2791
3920
|
return;
|
|
2792
3921
|
const base = codebaseDir(ctx.directory);
|
|
2793
3922
|
try {
|
|
2794
|
-
if (!
|
|
2795
|
-
|
|
3923
|
+
if (!existsSync28(base))
|
|
3924
|
+
mkdirSync14(base, { recursive: true });
|
|
2796
3925
|
const entry = {
|
|
2797
3926
|
timestamp: new Date().toISOString(),
|
|
2798
3927
|
file_path: filePath,
|
|
@@ -2804,35 +3933,61 @@ async function decisionTraceHook(ctx, input, output) {
|
|
|
2804
3933
|
risk_level: "unknown",
|
|
2805
3934
|
auto_recorded: true
|
|
2806
3935
|
};
|
|
2807
|
-
|
|
3936
|
+
appendFileSync4(join26(base, "DECISIONS.jsonl"), JSON.stringify(entry) + `
|
|
2808
3937
|
`, "utf-8");
|
|
2809
3938
|
} catch {}
|
|
2810
3939
|
}
|
|
2811
3940
|
|
|
2812
3941
|
// src/services/telemetry.ts
|
|
2813
|
-
import { existsSync as
|
|
2814
|
-
import { join as
|
|
3942
|
+
import { existsSync as existsSync29, readFileSync as readFileSync27, appendFileSync as appendFileSync5, mkdirSync as mkdirSync15 } from "fs";
|
|
3943
|
+
import { join as join27 } from "path";
|
|
2815
3944
|
import { randomUUID } from "crypto";
|
|
2816
3945
|
function telemetryPath(dir) {
|
|
2817
|
-
return
|
|
3946
|
+
return join27(codebaseDir(dir), "TELEMETRY.jsonl");
|
|
2818
3947
|
}
|
|
2819
|
-
function
|
|
3948
|
+
function appendEvent2(dir, partial) {
|
|
2820
3949
|
if (process.env.TELEMETRY_ENABLED !== "true")
|
|
2821
3950
|
return null;
|
|
2822
3951
|
const cd = codebaseDir(dir);
|
|
2823
|
-
if (!
|
|
2824
|
-
|
|
3952
|
+
if (!existsSync29(cd))
|
|
3953
|
+
mkdirSync15(cd, { recursive: true });
|
|
2825
3954
|
const event = {
|
|
2826
3955
|
id: randomUUID(),
|
|
2827
3956
|
ts: new Date().toISOString(),
|
|
2828
3957
|
...partial
|
|
2829
3958
|
};
|
|
2830
|
-
|
|
3959
|
+
appendFileSync5(telemetryPath(dir), JSON.stringify(event) + `
|
|
2831
3960
|
`, "utf-8");
|
|
2832
3961
|
return event;
|
|
2833
3962
|
}
|
|
2834
3963
|
|
|
2835
3964
|
// src/hooks/telemetry-hook.ts
|
|
3965
|
+
var toolStartTimes = new Map;
|
|
3966
|
+
var REPORTABLE_TOOLS = new Set([
|
|
3967
|
+
"delegate",
|
|
3968
|
+
"run-pipeline",
|
|
3969
|
+
"council",
|
|
3970
|
+
"bash",
|
|
3971
|
+
"write",
|
|
3972
|
+
"edit",
|
|
3973
|
+
"read",
|
|
3974
|
+
"codegraph",
|
|
3975
|
+
"codebase-state",
|
|
3976
|
+
"planning-state",
|
|
3977
|
+
"workspace-state",
|
|
3978
|
+
"repo-memory",
|
|
3979
|
+
"hash-edit",
|
|
3980
|
+
"context-generator",
|
|
3981
|
+
"volatility-map",
|
|
3982
|
+
"failure-replay",
|
|
3983
|
+
"decision-trace",
|
|
3984
|
+
"policy-engine",
|
|
3985
|
+
"reflect"
|
|
3986
|
+
]);
|
|
3987
|
+
var SELF_LOGGING_TOOLS = new Set(["delegate", "run-pipeline", "council"]);
|
|
3988
|
+
function correlationKey(sessionId, runId, tool19) {
|
|
3989
|
+
return `${sessionId}:${runId}:${tool19}`;
|
|
3990
|
+
}
|
|
2836
3991
|
function resolveIds(toolInput) {
|
|
2837
3992
|
const session_id = toolInput.sessionID ?? toolInput.sessionId ?? process.env.OPENCODE_SESSION_ID ?? "session-0";
|
|
2838
3993
|
const run_id = toolInput.messageID ?? toolInput.messageId ?? toolInput.runID ?? toolInput.runId ?? process.env.OPENCODE_RUN_ID ?? "run-0";
|
|
@@ -2855,36 +4010,87 @@ function inferStatus(output) {
|
|
|
2855
4010
|
return "ok";
|
|
2856
4011
|
}
|
|
2857
4012
|
}
|
|
2858
|
-
|
|
4013
|
+
function buildInputSummary(tool19, args) {
|
|
4014
|
+
if (tool19 === "delegate" || tool19 === "run-pipeline" || tool19 === "council") {
|
|
4015
|
+
return "";
|
|
4016
|
+
}
|
|
4017
|
+
if (tool19 === "bash" || tool19 === "write" || tool19 === "edit" || tool19 === "read") {
|
|
4018
|
+
const path = args.path ?? args.filePath ?? args.file ?? "";
|
|
4019
|
+
const cmd = args.command ?? args.cmd ?? "";
|
|
4020
|
+
return path || cmd ? summarize(String(path || cmd), 80) : "";
|
|
4021
|
+
}
|
|
4022
|
+
const firstStr = Object.values(args).find((v) => typeof v === "string");
|
|
4023
|
+
return firstStr ? summarize(firstStr, 80) : "";
|
|
4024
|
+
}
|
|
4025
|
+
async function telemetryHook(context, toolInput, output, reporter) {
|
|
2859
4026
|
const dir = context.directory ?? process.cwd();
|
|
2860
|
-
const
|
|
4027
|
+
const tool19 = toolInput.name ?? toolInput.tool ?? "unknown";
|
|
2861
4028
|
const ids = resolveIds(toolInput);
|
|
2862
|
-
|
|
4029
|
+
const key = correlationKey(ids.session_id, ids.run_id, tool19);
|
|
4030
|
+
toolStartTimes.set(key, Date.now());
|
|
4031
|
+
appendEvent2(dir, {
|
|
2863
4032
|
session_id: ids.session_id,
|
|
2864
4033
|
run_id: ids.run_id,
|
|
2865
4034
|
event: "tool.call",
|
|
2866
|
-
tool:
|
|
4035
|
+
tool: tool19,
|
|
2867
4036
|
status: "ok",
|
|
2868
4037
|
meta: { parameters: output.args ?? {} }
|
|
2869
4038
|
});
|
|
4039
|
+
if (reporter && REPORTABLE_TOOLS.has(tool19) && !SELF_LOGGING_TOOLS.has(tool19)) {
|
|
4040
|
+
const inputSummary = buildInputSummary(tool19, output.args ?? {});
|
|
4041
|
+
reporter.reportToolStarted(tool19, inputSummary, {
|
|
4042
|
+
session_id: ids.session_id,
|
|
4043
|
+
run_id: ids.run_id
|
|
4044
|
+
});
|
|
4045
|
+
}
|
|
4046
|
+
if (reporter && REPORTABLE_TOOLS.has(tool19)) {
|
|
4047
|
+
reporter.trackStart(key);
|
|
4048
|
+
}
|
|
2870
4049
|
}
|
|
2871
|
-
async function telemetryAfterHook(context, toolInput, output) {
|
|
4050
|
+
async function telemetryAfterHook(context, toolInput, output, reporter) {
|
|
2872
4051
|
const dir = context.directory ?? process.cwd();
|
|
2873
|
-
const
|
|
4052
|
+
const tool19 = toolInput.name ?? toolInput.tool ?? "unknown";
|
|
2874
4053
|
const ids = resolveIds(toolInput);
|
|
4054
|
+
const key = correlationKey(ids.session_id, ids.run_id, tool19);
|
|
2875
4055
|
const status = inferStatus(output);
|
|
2876
|
-
|
|
4056
|
+
let duration_ms;
|
|
4057
|
+
if (reporter && REPORTABLE_TOOLS.has(tool19)) {
|
|
4058
|
+
duration_ms = reporter.elapsedMs(key);
|
|
4059
|
+
}
|
|
4060
|
+
if (duration_ms === undefined) {
|
|
4061
|
+
const startMs = toolStartTimes.get(key);
|
|
4062
|
+
if (startMs !== undefined)
|
|
4063
|
+
duration_ms = Date.now() - startMs;
|
|
4064
|
+
}
|
|
4065
|
+
toolStartTimes.delete(key);
|
|
4066
|
+
let result_summary;
|
|
4067
|
+
if (typeof output.output === "string") {
|
|
4068
|
+
result_summary = summarize(output.output, 100);
|
|
4069
|
+
} else if (output.error) {
|
|
4070
|
+
result_summary = summarize(String(output.error), 100);
|
|
4071
|
+
}
|
|
4072
|
+
appendEvent2(dir, {
|
|
2877
4073
|
session_id: ids.session_id,
|
|
2878
4074
|
run_id: ids.run_id,
|
|
2879
|
-
event: "tool.complete",
|
|
2880
|
-
tool:
|
|
2881
|
-
status
|
|
4075
|
+
event: status === "error" ? "tool.failed" : "tool.complete",
|
|
4076
|
+
tool: tool19,
|
|
4077
|
+
status,
|
|
4078
|
+
duration_ms,
|
|
4079
|
+
result_summary
|
|
2882
4080
|
});
|
|
4081
|
+
if (reporter && REPORTABLE_TOOLS.has(tool19) && !SELF_LOGGING_TOOLS.has(tool19)) {
|
|
4082
|
+
if (status === "error") {
|
|
4083
|
+
const errText = output.error ? String(output.error) : typeof output.output === "string" ? output.output : "unknown error";
|
|
4084
|
+
reporter.reportToolFailed(tool19, duration_ms, errText);
|
|
4085
|
+
} else {
|
|
4086
|
+
reporter.reportToolCompleted(tool19, duration_ms, result_summary ?? "");
|
|
4087
|
+
}
|
|
4088
|
+
}
|
|
2883
4089
|
}
|
|
2884
4090
|
|
|
2885
4091
|
// src/services/approval-manager.ts
|
|
2886
|
-
import { existsSync as
|
|
2887
|
-
import { join as
|
|
4092
|
+
import { existsSync as existsSync30, readFileSync as readFileSync28, writeFileSync as writeFileSync17, mkdirSync as mkdirSync16 } from "fs";
|
|
4093
|
+
import { join as join28 } from "path";
|
|
2888
4094
|
var APPROVAL_TTL_MS = 30 * 60 * 1000;
|
|
2889
4095
|
var SENSITIVE_PATTERNS = [
|
|
2890
4096
|
/auth/i,
|
|
@@ -2921,14 +4127,14 @@ function isSensitivePath(filePath) {
|
|
|
2921
4127
|
return SENSITIVE_PATTERNS.some((p) => p.test(filePath));
|
|
2922
4128
|
}
|
|
2923
4129
|
function approvalsPath(dir) {
|
|
2924
|
-
return
|
|
4130
|
+
return join28(codebaseDir(dir), "APPROVALS.json");
|
|
2925
4131
|
}
|
|
2926
4132
|
function loadStore2(dir) {
|
|
2927
4133
|
const p = approvalsPath(dir);
|
|
2928
|
-
if (!
|
|
4134
|
+
if (!existsSync30(p))
|
|
2929
4135
|
return { requests: [] };
|
|
2930
4136
|
try {
|
|
2931
|
-
return JSON.parse(
|
|
4137
|
+
return JSON.parse(readFileSync28(p, "utf-8"));
|
|
2932
4138
|
} catch {
|
|
2933
4139
|
return { requests: [] };
|
|
2934
4140
|
}
|
|
@@ -2946,8 +4152,8 @@ async function approvalHook(context, toolInput, output) {
|
|
|
2946
4152
|
if (!ENABLED2)
|
|
2947
4153
|
return;
|
|
2948
4154
|
const dir = context.directory ?? process.cwd();
|
|
2949
|
-
const
|
|
2950
|
-
if (!WRITE_TOOLS.has(
|
|
4155
|
+
const tool19 = toolInput.name ?? toolInput.tool ?? "";
|
|
4156
|
+
if (!WRITE_TOOLS.has(tool19))
|
|
2951
4157
|
return;
|
|
2952
4158
|
const args = output.args ?? {};
|
|
2953
4159
|
const filePath = String(args.path ?? args.file_path ?? args.filename ?? "");
|
|
@@ -2958,11 +4164,11 @@ async function approvalHook(context, toolInput, output) {
|
|
|
2958
4164
|
const approval = checkApproval(dir, filePath, "");
|
|
2959
4165
|
if (approval)
|
|
2960
4166
|
return;
|
|
2961
|
-
|
|
4167
|
+
appendEvent2(dir, {
|
|
2962
4168
|
session_id: process.env.OPENCODE_SESSION_ID ?? "session-0",
|
|
2963
4169
|
run_id: process.env.OPENCODE_RUN_ID ?? "run-0",
|
|
2964
4170
|
event: "approval.request",
|
|
2965
|
-
tool:
|
|
4171
|
+
tool: tool19,
|
|
2966
4172
|
status: "blocked",
|
|
2967
4173
|
files: [filePath],
|
|
2968
4174
|
meta: { trigger: "sensitive_file", file: filePath }
|
|
@@ -3023,15 +4229,15 @@ function createContextWindowMonitorHook() {
|
|
|
3023
4229
|
}
|
|
3024
4230
|
|
|
3025
4231
|
// src/hooks/shell-env-hook.ts
|
|
3026
|
-
import { existsSync as
|
|
3027
|
-
import { join as
|
|
3028
|
-
import { createRequire } from "module";
|
|
4232
|
+
import { existsSync as existsSync31, readFileSync as readFileSync29 } from "fs";
|
|
4233
|
+
import { join as join29 } from "path";
|
|
4234
|
+
import { createRequire as createRequire2 } from "module";
|
|
3029
4235
|
var _version;
|
|
3030
4236
|
function getVersion() {
|
|
3031
4237
|
if (_version)
|
|
3032
4238
|
return _version;
|
|
3033
4239
|
try {
|
|
3034
|
-
const require2 =
|
|
4240
|
+
const require2 = createRequire2(import.meta.url);
|
|
3035
4241
|
const pkg = require2("../../package.json");
|
|
3036
4242
|
_version = pkg.version ?? "0.0.0";
|
|
3037
4243
|
} catch {
|
|
@@ -3060,7 +4266,7 @@ var MARKER_TO_LANG = {
|
|
|
3060
4266
|
};
|
|
3061
4267
|
function detectPackageManager(root) {
|
|
3062
4268
|
for (const [lockfile, pm] of Object.entries(LOCKFILE_TO_PM)) {
|
|
3063
|
-
if (
|
|
4269
|
+
if (existsSync31(join29(root, lockfile)))
|
|
3064
4270
|
return pm;
|
|
3065
4271
|
}
|
|
3066
4272
|
return;
|
|
@@ -3069,7 +4275,7 @@ function detectLanguages(root) {
|
|
|
3069
4275
|
const langs = [];
|
|
3070
4276
|
const seen = new Set;
|
|
3071
4277
|
for (const [marker, lang] of Object.entries(MARKER_TO_LANG)) {
|
|
3072
|
-
if (!seen.has(lang) &&
|
|
4278
|
+
if (!seen.has(lang) && existsSync31(join29(root, marker))) {
|
|
3073
4279
|
langs.push(lang);
|
|
3074
4280
|
seen.add(lang);
|
|
3075
4281
|
}
|
|
@@ -3077,17 +4283,29 @@ function detectLanguages(root) {
|
|
|
3077
4283
|
return langs;
|
|
3078
4284
|
}
|
|
3079
4285
|
function readCurrentPhase(root) {
|
|
3080
|
-
const statePath2 =
|
|
3081
|
-
if (!
|
|
4286
|
+
const statePath2 = join29(root, ".planning", "STATE.md");
|
|
4287
|
+
if (!existsSync31(statePath2))
|
|
3082
4288
|
return;
|
|
3083
4289
|
try {
|
|
3084
|
-
const content =
|
|
4290
|
+
const content = readFileSync29(statePath2, "utf-8");
|
|
3085
4291
|
const match = content.match(/phase:\s*(\S+)/i);
|
|
3086
4292
|
return match?.[1];
|
|
3087
4293
|
} catch {
|
|
3088
4294
|
return;
|
|
3089
4295
|
}
|
|
3090
4296
|
}
|
|
4297
|
+
var _rtkDetection;
|
|
4298
|
+
function getRtkDetection() {
|
|
4299
|
+
if (_rtkDetection !== undefined)
|
|
4300
|
+
return _rtkDetection;
|
|
4301
|
+
try {
|
|
4302
|
+
const det = detectRtk();
|
|
4303
|
+
_rtkDetection = { installed: det.installed, binPath: det.binPath };
|
|
4304
|
+
} catch {
|
|
4305
|
+
_rtkDetection = { installed: false };
|
|
4306
|
+
}
|
|
4307
|
+
return _rtkDetection;
|
|
4308
|
+
}
|
|
3091
4309
|
function createShellEnvHook(ctx) {
|
|
3092
4310
|
const root = ctx.worktree || ctx.directory;
|
|
3093
4311
|
return async (_input, output) => {
|
|
@@ -3105,6 +4323,14 @@ function createShellEnvHook(ctx) {
|
|
|
3105
4323
|
const phase = readCurrentPhase(root);
|
|
3106
4324
|
if (phase)
|
|
3107
4325
|
output.env.FLOWDECK_PHASE = phase;
|
|
4326
|
+
const rtk = getRtkDetection();
|
|
4327
|
+
output.env.RTK_INSTALLED = rtk.installed ? "true" : "false";
|
|
4328
|
+
if (rtk.installed && rtk.binPath) {
|
|
4329
|
+
output.env.RTK_BIN = rtk.binPath;
|
|
4330
|
+
}
|
|
4331
|
+
if (rtk.installed) {
|
|
4332
|
+
output.env.RTK_TELEMETRY_DISABLED = "1";
|
|
4333
|
+
}
|
|
3108
4334
|
};
|
|
3109
4335
|
}
|
|
3110
4336
|
|
|
@@ -3186,8 +4412,8 @@ function createSessionIdleHook(client, tracker) {
|
|
|
3186
4412
|
}
|
|
3187
4413
|
|
|
3188
4414
|
// src/hooks/compaction-hook.ts
|
|
3189
|
-
import { existsSync as
|
|
3190
|
-
import { join as
|
|
4415
|
+
import { existsSync as existsSync32, readFileSync as readFileSync30 } from "fs";
|
|
4416
|
+
import { join as join30 } from "path";
|
|
3191
4417
|
var STRUCTURED_SUMMARY_PROMPT = `
|
|
3192
4418
|
When summarizing this session, you MUST include the following sections:
|
|
3193
4419
|
|
|
@@ -3225,40 +4451,58 @@ List all background agent tasks spawned this session.
|
|
|
3225
4451
|
For each: agent name, status, description, session_id.
|
|
3226
4452
|
**RESUME, DON'T RESTART.** Use session_id to continue existing sessions.
|
|
3227
4453
|
`;
|
|
4454
|
+
var _lastInjected = new Map;
|
|
3228
4455
|
function readPlanningState2(directory) {
|
|
3229
|
-
const
|
|
3230
|
-
if (!
|
|
4456
|
+
const sp = statePath(directory);
|
|
4457
|
+
if (!existsSync32(sp))
|
|
3231
4458
|
return null;
|
|
3232
4459
|
try {
|
|
3233
|
-
const content =
|
|
3234
|
-
|
|
4460
|
+
const content = readFileSync30(sp, "utf-8");
|
|
4461
|
+
const parsed = parseState(content);
|
|
4462
|
+
const version = typeof parsed.summaryVersion === "number" ? parsed.summaryVersion : 0;
|
|
4463
|
+
return { content: content.slice(0, 1500), version };
|
|
3235
4464
|
} catch {
|
|
3236
4465
|
return null;
|
|
3237
4466
|
}
|
|
3238
4467
|
}
|
|
3239
4468
|
function createCompactionHook(ctx, tracker) {
|
|
3240
|
-
return async (
|
|
4469
|
+
return async (input, output) => {
|
|
3241
4470
|
const sections = ["# FlowDeck Context (preserve across compaction)", ""];
|
|
3242
|
-
const
|
|
3243
|
-
|
|
4471
|
+
const stateData = readPlanningState2(ctx.directory);
|
|
4472
|
+
const indexData = readCodebaseIndex(ctx.directory);
|
|
4473
|
+
const currentStateVersion = stateData?.version ?? 0;
|
|
4474
|
+
const currentIndexVersion = indexData.summaryVersion ?? 0;
|
|
4475
|
+
const lastSnapshot = _lastInjected.get(input.sessionID);
|
|
4476
|
+
const stateChanged = !lastSnapshot || lastSnapshot.stateVersion !== currentStateVersion;
|
|
4477
|
+
const indexChanged = !lastSnapshot || lastSnapshot.indexVersion !== currentIndexVersion;
|
|
4478
|
+
if (stateChanged && stateData) {
|
|
3244
4479
|
sections.push("## Planning State");
|
|
3245
4480
|
sections.push("```");
|
|
3246
|
-
sections.push(
|
|
4481
|
+
sections.push(stateData.content.trim());
|
|
3247
4482
|
sections.push("```");
|
|
3248
4483
|
sections.push("");
|
|
4484
|
+
} else if (stateData) {
|
|
4485
|
+
sections.push(`## Planning State (unchanged, v${currentStateVersion})`);
|
|
4486
|
+
sections.push(`_State unchanged since last compaction. summaryVersion=${currentStateVersion}_`);
|
|
4487
|
+
sections.push("");
|
|
3249
4488
|
}
|
|
3250
|
-
const
|
|
3251
|
-
|
|
3252
|
-
if (existsSync25(indexPath)) {
|
|
4489
|
+
const indexPath2 = join30(ctx.directory, ".planning", "CODEBASE_INDEX.md");
|
|
4490
|
+
if (indexChanged && existsSync32(indexPath2)) {
|
|
3253
4491
|
try {
|
|
3254
|
-
const indexContent =
|
|
3255
|
-
indexSummary = "\n## Codebase Index\n```\n" + indexContent.slice(0, 800) + "\n```\n";
|
|
4492
|
+
const indexContent = readFileSync30(indexPath2, "utf-8");
|
|
4493
|
+
const indexSummary = "\n## Codebase Index\n```\n" + indexContent.slice(0, 800) + "\n```\n";
|
|
4494
|
+
sections.push(indexSummary);
|
|
4495
|
+
sections.push("");
|
|
3256
4496
|
} catch {}
|
|
3257
|
-
}
|
|
3258
|
-
|
|
3259
|
-
sections.push(
|
|
4497
|
+
} else if (existsSync32(indexPath2)) {
|
|
4498
|
+
sections.push(`## Codebase Index (unchanged, v${currentIndexVersion})`);
|
|
4499
|
+
sections.push(`_Index unchanged since last compaction. summaryVersion=${currentIndexVersion}_`);
|
|
3260
4500
|
sections.push("");
|
|
3261
4501
|
}
|
|
4502
|
+
_lastInjected.set(input.sessionID, {
|
|
4503
|
+
stateVersion: currentStateVersion,
|
|
4504
|
+
indexVersion: currentIndexVersion
|
|
4505
|
+
});
|
|
3262
4506
|
const edited = tracker.getEditedPaths();
|
|
3263
4507
|
if (edited.length > 0) {
|
|
3264
4508
|
sections.push("## Recently Edited Files");
|
|
@@ -7215,7 +8459,7 @@ function shouldProceed(decision, mode, canBlock) {
|
|
|
7215
8459
|
}
|
|
7216
8460
|
function _emitTelemetry(directory, decision, ctx) {
|
|
7217
8461
|
try {
|
|
7218
|
-
|
|
8462
|
+
appendEvent2(directory, {
|
|
7219
8463
|
session_id: ctx.session_id ?? "session-0",
|
|
7220
8464
|
run_id: ctx.run_id ?? "unknown",
|
|
7221
8465
|
event: "supervisor.review",
|
|
@@ -7236,37 +8480,29 @@ function _emitTelemetry(directory, decision, ctx) {
|
|
|
7236
8480
|
}
|
|
7237
8481
|
|
|
7238
8482
|
// src/index.ts
|
|
7239
|
-
function
|
|
7240
|
-
const __dir =
|
|
7241
|
-
const rulesDir =
|
|
7242
|
-
if (!
|
|
7243
|
-
return [];
|
|
7244
|
-
const
|
|
7245
|
-
|
|
7246
|
-
|
|
7247
|
-
|
|
7248
|
-
|
|
7249
|
-
walk(full);
|
|
7250
|
-
} else if (entry.isFile() && entry.name.endsWith(".md") && entry.name !== "README.md") {
|
|
7251
|
-
paths.push(full);
|
|
7252
|
-
}
|
|
7253
|
-
}
|
|
7254
|
-
}
|
|
7255
|
-
walk(rulesDir);
|
|
7256
|
-
return paths;
|
|
8483
|
+
function lazyLoadRulePaths(projectRoot) {
|
|
8484
|
+
const __dir = dirname5(fileURLToPath3(import.meta.url));
|
|
8485
|
+
const rulesDir = join31(__dir, "..", "src", "rules");
|
|
8486
|
+
if (!existsSync33(rulesDir))
|
|
8487
|
+
return { paths: [], diagnostics: "[LazyRuleLoader] rules directory not found" };
|
|
8488
|
+
const detectedLanguages = detectProjectLanguages(projectRoot);
|
|
8489
|
+
const paths = getStartupRulePaths(rulesDir, detectedLanguages);
|
|
8490
|
+
const selection = selectRulePaths(rulesDir, { languages: detectedLanguages });
|
|
8491
|
+
const diagnostics = buildSelectionDiagnostics(selection, { languages: detectedLanguages });
|
|
8492
|
+
return { paths, diagnostics };
|
|
7257
8493
|
}
|
|
7258
8494
|
function loadCommands() {
|
|
7259
|
-
const __dir =
|
|
7260
|
-
const commandsDir =
|
|
7261
|
-
if (!
|
|
8495
|
+
const __dir = dirname5(fileURLToPath3(import.meta.url));
|
|
8496
|
+
const commandsDir = join31(__dir, "..", "src", "commands");
|
|
8497
|
+
if (!existsSync33(commandsDir))
|
|
7262
8498
|
return {};
|
|
7263
8499
|
const commands = {};
|
|
7264
8500
|
try {
|
|
7265
|
-
for (const file of
|
|
8501
|
+
for (const file of readdirSync5(commandsDir)) {
|
|
7266
8502
|
if (!file.endsWith(".md"))
|
|
7267
8503
|
continue;
|
|
7268
|
-
const name =
|
|
7269
|
-
const raw =
|
|
8504
|
+
const name = basename2(file, ".md");
|
|
8505
|
+
const raw = readFileSync31(join31(commandsDir, file), "utf-8");
|
|
7270
8506
|
let description;
|
|
7271
8507
|
let template = raw;
|
|
7272
8508
|
const fmMatch = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/);
|
|
@@ -7283,8 +8519,11 @@ function loadCommands() {
|
|
|
7283
8519
|
}
|
|
7284
8520
|
var plugin = async (input, _options) => {
|
|
7285
8521
|
const { directory, client, worktree } = input;
|
|
7286
|
-
const
|
|
7287
|
-
const
|
|
8522
|
+
const appLog = (msg) => client.app.log({ body: { service: "flowdeck", level: "info", message: msg } }).catch(() => {});
|
|
8523
|
+
const toastFn = (message, variant, duration) => client.tui.showToast({ body: { message, variant, duration }, query: { directory } }).catch(() => {});
|
|
8524
|
+
const activityReporter = new ActivityReporter(appLog, toastFn);
|
|
8525
|
+
const runPipelineTool = createRunPipelineTool(client, activityReporter);
|
|
8526
|
+
const delegateTool = createDelegateTool(client, activityReporter);
|
|
7288
8527
|
const councilTool = createCouncilTool(client);
|
|
7289
8528
|
const fileTracker = new SessionFileTracker;
|
|
7290
8529
|
const { fileEdited, fileWatcherUpdated } = createFileTrackerHooks(fileTracker);
|
|
@@ -7294,11 +8533,11 @@ var plugin = async (input, _options) => {
|
|
|
7294
8533
|
const sessionIdleHook = createSessionIdleHook(client, fileTracker);
|
|
7295
8534
|
const compactionHook = createCompactionHook({ directory }, fileTracker);
|
|
7296
8535
|
const orchestratorGuard = new OrchestratorGuard;
|
|
7297
|
-
const appLog = (msg) => client.app.log({ body: { service: "flowdeck", level: "info", message: msg } }).catch(() => {});
|
|
7298
8536
|
const autoLearnHook = createAutoLearnHook(client, fileTracker, directory, appLog);
|
|
7299
8537
|
const notifCtrl = new NotificationController(undefined, appLog);
|
|
7300
8538
|
const agentConfigs = getAgentConfigs({});
|
|
7301
8539
|
const mcps = createFlowDeckMcps();
|
|
8540
|
+
let lastExecutedCommand = null;
|
|
7302
8541
|
return {
|
|
7303
8542
|
name: "@dv.nghiem/flowdeck",
|
|
7304
8543
|
agent: agentConfigs,
|
|
@@ -7348,8 +8587,8 @@ var plugin = async (input, _options) => {
|
|
|
7348
8587
|
}
|
|
7349
8588
|
}
|
|
7350
8589
|
}
|
|
7351
|
-
const skillsDir =
|
|
7352
|
-
if (
|
|
8590
|
+
const skillsDir = join31(dirname5(fileURLToPath3(import.meta.url)), "..", "src", "skills");
|
|
8591
|
+
if (existsSync33(skillsDir)) {
|
|
7353
8592
|
const cfgAny = cfg;
|
|
7354
8593
|
if (!cfgAny.skills || typeof cfgAny.skills !== "object") {
|
|
7355
8594
|
cfgAny.skills = { paths: [] };
|
|
@@ -7361,7 +8600,8 @@ var plugin = async (input, _options) => {
|
|
|
7361
8600
|
cfgSkills.paths.push(skillsDir);
|
|
7362
8601
|
}
|
|
7363
8602
|
}
|
|
7364
|
-
const rulePaths =
|
|
8603
|
+
const { paths: rulePaths, diagnostics: rulesDiag } = lazyLoadRulePaths(directory);
|
|
8604
|
+
appLog(rulesDiag);
|
|
7365
8605
|
if (rulePaths.length > 0) {
|
|
7366
8606
|
if (!Array.isArray(cfg.instructions)) {
|
|
7367
8607
|
cfg.instructions = [];
|
|
@@ -7390,16 +8630,23 @@ var plugin = async (input, _options) => {
|
|
|
7390
8630
|
"context-generator": contextGeneratorTool,
|
|
7391
8631
|
"create-skill": createSkillTool,
|
|
7392
8632
|
reflect: reflectTool,
|
|
7393
|
-
codegraph: codegraphTool
|
|
8633
|
+
codegraph: codegraphTool,
|
|
8634
|
+
"load-rules": loadRulesTool,
|
|
8635
|
+
"list-rules": listRulesTool,
|
|
8636
|
+
"rtk-setup": rtkSetupTool
|
|
7394
8637
|
},
|
|
7395
8638
|
"shell.env": shellEnvHook,
|
|
7396
8639
|
"todo.updated": todoHook,
|
|
7397
8640
|
"file.edited": fileEdited,
|
|
7398
8641
|
"file.watcher.updated": fileWatcherUpdated,
|
|
7399
8642
|
"experimental.session.compacting": compactionHook,
|
|
7400
|
-
"command.execute.before": async (
|
|
8643
|
+
"command.execute.before": async (input2, _output) => {
|
|
8644
|
+
activityReporter.reportCommandStarted(input2.command);
|
|
8645
|
+
lastExecutedCommand = input2.command;
|
|
8646
|
+
},
|
|
7401
8647
|
"permission.ask": async (input2, _output) => {
|
|
7402
8648
|
notifyPermissionNeeded(input2.title);
|
|
8649
|
+
activityReporter.reportWaitingForApproval(input2.title);
|
|
7403
8650
|
},
|
|
7404
8651
|
event: async ({ event }) => {
|
|
7405
8652
|
const type = event?.type ?? "";
|
|
@@ -7416,6 +8663,10 @@ var plugin = async (input, _options) => {
|
|
|
7416
8663
|
orchestratorGuard.onEvent(event);
|
|
7417
8664
|
if (type === "session.idle") {
|
|
7418
8665
|
const hasEdits = fileTracker.getEditedPaths().length > 0;
|
|
8666
|
+
if (lastExecutedCommand) {
|
|
8667
|
+
activityReporter.reportCommandCompleted(lastExecutedCommand, hasEdits);
|
|
8668
|
+
lastExecutedCommand = null;
|
|
8669
|
+
}
|
|
7419
8670
|
notifCtrl.onSessionIdle(hasEdits);
|
|
7420
8671
|
try {
|
|
7421
8672
|
await sessionIdleHook();
|
|
@@ -7425,6 +8676,7 @@ var plugin = async (input, _options) => {
|
|
|
7425
8676
|
}
|
|
7426
8677
|
}
|
|
7427
8678
|
if (type === "session.error") {
|
|
8679
|
+
lastExecutedCommand = null;
|
|
7428
8680
|
const err = event?.properties?.error;
|
|
7429
8681
|
const errorMsg = (err && typeof err === "object" && "message" in err ? String(err.message) : undefined) ?? (typeof err === "string" ? err : undefined) ?? "An unexpected error occurred";
|
|
7430
8682
|
notifCtrl.onSessionError(errorMsg);
|
|
@@ -7469,7 +8721,7 @@ var plugin = async (input, _options) => {
|
|
|
7469
8721
|
}
|
|
7470
8722
|
}
|
|
7471
8723
|
}
|
|
7472
|
-
await telemetryHook({ directory }, toolInput, toolOutput);
|
|
8724
|
+
await telemetryHook({ directory }, toolInput, toolOutput, activityReporter);
|
|
7473
8725
|
await approvalHook({ directory }, toolInput, toolOutput);
|
|
7474
8726
|
await guardRailsHook({ directory }, toolInput, toolOutput);
|
|
7475
8727
|
await toolGuardHook({ directory }, toolInput, toolOutput);
|
|
@@ -7477,7 +8729,7 @@ var plugin = async (input, _options) => {
|
|
|
7477
8729
|
await decisionTraceHook({ directory }, toolInput, toolOutput);
|
|
7478
8730
|
},
|
|
7479
8731
|
"tool.execute.after": async (toolInput, toolOutput) => {
|
|
7480
|
-
await telemetryAfterHook({ directory }, toolInput, toolOutput);
|
|
8732
|
+
await telemetryAfterHook({ directory }, toolInput, toolOutput, activityReporter);
|
|
7481
8733
|
const afterToolName = toolInput.tool ?? toolInput.name ?? "";
|
|
7482
8734
|
if (afterToolName === "delegate" || afterToolName === "run-pipeline") {
|
|
7483
8735
|
try {
|