@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.
Files changed (108) hide show
  1. package/README.md +3 -0
  2. package/dist/agents/index.d.ts +13 -0
  3. package/dist/agents/index.d.ts.map +1 -1
  4. package/dist/config/schema.d.ts +19 -0
  5. package/dist/config/schema.d.ts.map +1 -1
  6. package/dist/hooks/compaction-hook.d.ts +6 -1
  7. package/dist/hooks/compaction-hook.d.ts.map +1 -1
  8. package/dist/hooks/shell-env-hook.d.ts.map +1 -1
  9. package/dist/hooks/telemetry-hook.d.ts +3 -2
  10. package/dist/hooks/telemetry-hook.d.ts.map +1 -1
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +1588 -336
  13. package/dist/services/activity-reporter.d.ts +96 -0
  14. package/dist/services/activity-reporter.d.ts.map +1 -0
  15. package/dist/services/activity-reporter.test.d.ts +2 -0
  16. package/dist/services/activity-reporter.test.d.ts.map +1 -0
  17. package/dist/services/artifact-store.d.ts +39 -0
  18. package/dist/services/artifact-store.d.ts.map +1 -0
  19. package/dist/services/artifact-store.test.d.ts +2 -0
  20. package/dist/services/artifact-store.test.d.ts.map +1 -0
  21. package/dist/services/context-assembler.d.ts +29 -0
  22. package/dist/services/context-assembler.d.ts.map +1 -0
  23. package/dist/services/context-assembler.test.d.ts +2 -0
  24. package/dist/services/context-assembler.test.d.ts.map +1 -0
  25. package/dist/services/cost-budget.d.ts +53 -0
  26. package/dist/services/cost-budget.d.ts.map +1 -0
  27. package/dist/services/cost-budget.test.d.ts +2 -0
  28. package/dist/services/cost-budget.test.d.ts.map +1 -0
  29. package/dist/services/cost-estimator.d.ts +103 -0
  30. package/dist/services/cost-estimator.d.ts.map +1 -0
  31. package/dist/services/cost-estimator.test.d.ts +2 -0
  32. package/dist/services/cost-estimator.test.d.ts.map +1 -0
  33. package/dist/services/draft-verifier.d.ts +48 -0
  34. package/dist/services/draft-verifier.d.ts.map +1 -0
  35. package/dist/services/draft-verifier.test.d.ts +2 -0
  36. package/dist/services/draft-verifier.test.d.ts.map +1 -0
  37. package/dist/services/index.d.ts +13 -0
  38. package/dist/services/index.d.ts.map +1 -1
  39. package/dist/services/lazy-rule-loader.d.ts +104 -0
  40. package/dist/services/lazy-rule-loader.d.ts.map +1 -0
  41. package/dist/services/lazy-rule-loader.test.d.ts +23 -0
  42. package/dist/services/lazy-rule-loader.test.d.ts.map +1 -0
  43. package/dist/services/model-router-ext.test.d.ts +2 -0
  44. package/dist/services/model-router-ext.test.d.ts.map +1 -0
  45. package/dist/services/model-router.d.ts +52 -0
  46. package/dist/services/model-router.d.ts.map +1 -0
  47. package/dist/services/model-router.test.d.ts +2 -0
  48. package/dist/services/model-router.test.d.ts.map +1 -0
  49. package/dist/services/prompt-cache-ext.test.d.ts +2 -0
  50. package/dist/services/prompt-cache-ext.test.d.ts.map +1 -0
  51. package/dist/services/prompt-cache.d.ts +61 -0
  52. package/dist/services/prompt-cache.d.ts.map +1 -0
  53. package/dist/services/prompt-cache.test.d.ts +2 -0
  54. package/dist/services/prompt-cache.test.d.ts.map +1 -0
  55. package/dist/services/rtk-manager.d.ts +80 -0
  56. package/dist/services/rtk-manager.d.ts.map +1 -0
  57. package/dist/services/rtk-manager.test.d.ts +2 -0
  58. package/dist/services/rtk-manager.test.d.ts.map +1 -0
  59. package/dist/services/rtk-policy.d.ts +26 -0
  60. package/dist/services/rtk-policy.d.ts.map +1 -0
  61. package/dist/services/rtk-policy.test.d.ts +2 -0
  62. package/dist/services/rtk-policy.test.d.ts.map +1 -0
  63. package/dist/services/rule-engine.d.ts +29 -0
  64. package/dist/services/rule-engine.d.ts.map +1 -0
  65. package/dist/services/rule-engine.test.d.ts +2 -0
  66. package/dist/services/rule-engine.test.d.ts.map +1 -0
  67. package/dist/services/task-batcher.d.ts +48 -0
  68. package/dist/services/task-batcher.d.ts.map +1 -0
  69. package/dist/services/task-batcher.test.d.ts +2 -0
  70. package/dist/services/task-batcher.test.d.ts.map +1 -0
  71. package/dist/services/telemetry.d.ts +15 -1
  72. package/dist/services/telemetry.d.ts.map +1 -1
  73. package/dist/services/token-budget.d.ts +44 -0
  74. package/dist/services/token-budget.d.ts.map +1 -0
  75. package/dist/services/token-budget.test.d.ts +2 -0
  76. package/dist/services/token-budget.test.d.ts.map +1 -0
  77. package/dist/services/token-metrics-ext.test.d.ts +2 -0
  78. package/dist/services/token-metrics-ext.test.d.ts.map +1 -0
  79. package/dist/services/token-metrics.d.ts +97 -0
  80. package/dist/services/token-metrics.d.ts.map +1 -0
  81. package/dist/services/token-metrics.test.d.ts +2 -0
  82. package/dist/services/token-metrics.test.d.ts.map +1 -0
  83. package/dist/tools/council.d.ts.map +1 -1
  84. package/dist/tools/delegate.d.ts +2 -1
  85. package/dist/tools/delegate.d.ts.map +1 -1
  86. package/dist/tools/load-rules.d.ts +25 -0
  87. package/dist/tools/load-rules.d.ts.map +1 -0
  88. package/dist/tools/rtk-setup.d.ts +22 -0
  89. package/dist/tools/rtk-setup.d.ts.map +1 -0
  90. package/dist/tools/run-pipeline.d.ts +2 -1
  91. package/dist/tools/run-pipeline.d.ts.map +1 -1
  92. package/docs/commands/fd-map-codebase.md +2 -1
  93. package/docs/configuration/index.md +26 -0
  94. package/docs/getting-started/installation.md +20 -0
  95. package/docs/reference/hooks.md +16 -1
  96. package/docs/reference/rtk.md +162 -0
  97. package/package.json +1 -1
  98. package/src/rules/common/agent-orchestration.md +7 -0
  99. package/src/rules/common/behavioral.md +7 -0
  100. package/src/rules/common/coding-style.md +7 -0
  101. package/src/rules/common/git-workflow.md +7 -0
  102. package/src/rules/common/security.md +7 -0
  103. package/src/rules/common/testing.md +7 -0
  104. package/src/rules/golang/patterns.md +7 -0
  105. package/src/rules/java/patterns.md +7 -0
  106. package/src/rules/python/patterns.md +7 -0
  107. package/src/rules/rust/patterns.md +7 -0
  108. 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 { readdirSync as readdirSync3, readFileSync as readFileSync24, existsSync as existsSync26 } from "fs";
3
- import { join as join25, basename } from "path";
4
- import { dirname as dirname4 } from "path";
5
- import { fileURLToPath as fileURLToPath2 } from "url";
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 join3 } from "path";
159
+ import { join as join4 } from "path";
9
160
  import { tool as tool2 } from "@opencode-ai/plugin";
10
- import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, existsSync as existsSync3 } from "fs";
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 join2, dirname, resolve } from "path";
14
- import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync2 } from "fs";
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 join(directory, CODEBASE_DIR);
173
+ return join2(directory, CODEBASE_DIR);
23
174
  }
24
175
  function codebaseFilePath(directory, filename) {
25
- return join(codebaseDir(directory), filename);
176
+ return join2(codebaseDir(directory), filename);
26
177
  }
27
178
  function listCodebaseFiles(directory) {
28
179
  const base = codebaseDir(directory);
29
- if (!existsSync(base))
180
+ if (!existsSync2(base))
30
181
  return [];
31
- return readdirSync(base).filter((f) => f.endsWith(".md") || f.endsWith(".json"));
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 (!existsSync(filePath)) {
188
+ if (!existsSync2(filePath)) {
38
189
  results[file] = { error: `File not found: ${file}` };
39
190
  continue;
40
191
  }
41
- results[file] = readFileSync(filePath, "utf-8");
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 (!existsSync(base)) {
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 (!existsSync(base)) {
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 join2(directory, PLANNING_DIR);
245
+ return join3(directory, PLANNING_DIR);
95
246
  }
96
247
  function statePath(directory) {
97
- return join2(planningDir(directory), STATE_FILE);
248
+ return join3(planningDir(directory), STATE_FILE);
98
249
  }
99
250
  function phasePlanPath(directory, phase) {
100
- return join2(planningDir(directory), "phases", `phase-${phase}`, PLAN_FILE);
251
+ return join3(planningDir(directory), "phases", `phase-${phase}`, PLAN_FILE);
101
252
  }
102
253
  function resultPath(directory, phase) {
103
- return join2(planningDir(directory), "phases", `phase-${phase}`, RESULT_FILE);
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 (!existsSync2(sp)) {
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 = readFileSync2(sp, "utf-8");
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 = join2(current, ".planning", "config.json");
230
- if (existsSync2(configPath)) {
380
+ const configPath = join3(current, ".planning", "config.json");
381
+ if (existsSync3(configPath)) {
231
382
  try {
232
- const config = JSON.parse(readFileSync2(configPath, "utf-8"));
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 = join2(root, ".planning", "config.json");
258
- if (!existsSync2(configPath))
408
+ const configPath = join3(root, ".planning", "config.json");
409
+ if (!existsSync3(configPath))
259
410
  return null;
260
411
  try {
261
- const config = JSON.parse(readFileSync2(configPath, "utf-8"));
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 (!existsSync3(sp)) {
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 = readFileSync3(sp, "utf-8");
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 = readFileSync3(sp, "utf-8");
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 = readFileSync3(sp, "utf-8");
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() : join3(planningDir(dir), "phases", `phase-${phase}`, PLAN_FILE2);
376
- if (!existsSync3(planFile))
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: readFileSync3(planFile, "utf-8") });
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 = readFileSync3(sp, "utf-8");
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 (existsSync3(planFile)) {
393
- let planContent = readFileSync3(planFile, "utf-8");
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 (existsSync3(resultFile)) {
400
- writeFileSync3(resultFile, readFileSync3(resultFile, "utf-8") + entry, "utf-8");
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 readFileSync4, writeFileSync as writeFileSync4, existsSync as existsSync4 } from "fs";
432
- import { join as join4 } from "path";
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 = join4(planningDir(workspaceRoot), "STATE.md");
438
- if (!existsSync4(sp))
588
+ const sp = join5(planningDir(workspaceRoot), "STATE.md");
589
+ if (!existsSync5(sp))
439
590
  return null;
440
- return readFileSync4(sp, "utf-8");
591
+ return readFileSync5(sp, "utf-8");
441
592
  }
442
593
  function writeWorkspaceStateFile(workspaceRoot, content) {
443
- const sp = join4(planningDir(workspaceRoot), "STATE.md");
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(join4(workspaceRoot, ".planning", "config.json"), subRepos);
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 = existsSync4(planningPath);
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(join4(workspaceRoot, ".planning", "config.json"), subRepos);
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 (!existsSync4(planningPath)) {
672
+ if (!existsSync5(planningPath)) {
522
673
  return { error: "not_found", path: targetPath };
523
674
  }
524
675
  const sp = statePath(targetPath);
525
- if (!existsSync4(sp)) {
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 = readFileSync4(sp, "utf-8");
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 existsSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync5, mkdirSync as mkdirSync2 } from "fs";
577
- import { join as join5 } from "path";
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 join5(codebaseDir(dir), "AGENT_PERF.json");
730
+ return join6(codebaseDir(dir), "AGENT_PERF.json");
580
731
  }
581
732
  function loadStore(dir) {
582
733
  const p = perfPath(dir);
583
- if (!existsSync5(p))
734
+ if (!existsSync6(p))
584
735
  return { entries: [], updated_at: new Date().toISOString() };
585
736
  try {
586
- return JSON.parse(readFileSync5(p, "utf-8"));
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 (!existsSync5(cd))
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 (const step of args.steps) {
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
- carryContext = output;
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: Date.now() - startTime,
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 fullPrompt = args.context ? `${args.context}
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: fullPrompt }],
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: `Prompt failed: ${promptRes?.error?.detail ?? "unknown"}`,
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: `Agent error: ${JSON.stringify(info.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 readFileSync6, writeFileSync as writeFileSync6, existsSync as existsSync6, mkdirSync as mkdirSync3 } from "fs";
944
- import { join as join6 } from "path";
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 join6(codebaseDir(directory), MEMORY_FILE);
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 (!existsSync6(p))
1803
+ if (!existsSync12(p))
955
1804
  return emptyMemory();
956
1805
  try {
957
- return JSON.parse(readFileSync6(p, "utf-8"));
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 (!existsSync6(base))
965
- mkdirSync3(base, { recursive: true });
1813
+ if (!existsSync12(base))
1814
+ mkdirSync6(base, { recursive: true });
966
1815
  memory.last_updated = new Date().toISOString();
967
- writeFileSync6(memoryPath(directory), JSON.stringify(memory, null, 2), "utf-8");
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 readFileSync7, writeFileSync as writeFileSync7, existsSync as existsSync7, mkdirSync as mkdirSync4 } from "fs";
1045
- import { join as join7 } from "path";
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 join7(codebaseDir(directory), FAILURES_FILE);
1897
+ return join12(codebaseDir(directory), FAILURES_FILE);
1049
1898
  }
1050
1899
  function readStore(directory) {
1051
1900
  const p = failuresPath(directory);
1052
- if (!existsSync7(p))
1901
+ if (!existsSync13(p))
1053
1902
  return { version: "1.0", last_updated: new Date().toISOString(), entries: [] };
1054
1903
  try {
1055
- return JSON.parse(readFileSync7(p, "utf-8"));
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 (!existsSync7(base))
1063
- mkdirSync4(base, { recursive: true });
1911
+ if (!existsSync13(base))
1912
+ mkdirSync7(base, { recursive: true });
1064
1913
  store.last_updated = new Date().toISOString();
1065
- writeFileSync7(failuresPath(directory), JSON.stringify(store, null, 2), "utf-8");
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 readFileSync8, existsSync as existsSync8, mkdirSync as mkdirSync5, appendFileSync } from "fs";
1150
- import { join as join8 } from "path";
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 join8(codebaseDir(directory), DECISIONS_FILE);
2002
+ return join13(codebaseDir(directory), DECISIONS_FILE);
1154
2003
  }
1155
2004
  function readDecisions(directory) {
1156
2005
  const p = decisionsPath(directory);
1157
- if (!existsSync8(p))
2006
+ if (!existsSync14(p))
1158
2007
  return [];
1159
- return readFileSync8(p, "utf-8").split(`
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 (!existsSync8(base))
1200
- mkdirSync5(base, { recursive: true });
2048
+ if (!existsSync14(base))
2049
+ mkdirSync8(base, { recursive: true });
1201
2050
  const entry = { ...args.entry, timestamp: new Date().toISOString() };
1202
- appendFileSync(decisionsPath(dir), JSON.stringify(entry) + `
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 readFileSync9, writeFileSync as writeFileSync9, existsSync as existsSync9, mkdirSync as mkdirSync6 } from "fs";
1235
- import { join as join9 } from "path";
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 join9(codebaseDir(directory), VOLATILITY_FILE);
2087
+ return join14(codebaseDir(directory), VOLATILITY_FILE);
1239
2088
  }
1240
2089
  function readStore2(directory) {
1241
2090
  const p = volatilityPath(directory);
1242
- if (!existsSync9(p))
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(readFileSync9(p, "utf-8"));
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 (!existsSync9(base))
1253
- mkdirSync6(base, { recursive: true });
2101
+ if (!existsSync15(base))
2102
+ mkdirSync9(base, { recursive: true });
1254
2103
  store.last_updated = new Date().toISOString();
1255
- writeFileSync9(volatilityPath(directory), JSON.stringify(store, null, 2), "utf-8");
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 readFileSync10, writeFileSync as writeFileSync10, existsSync as existsSync10, mkdirSync as mkdirSync7 } from "fs";
1343
- import { join as join10 } from "path";
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 join10(codebaseDir(directory), POLICIES_FILE);
2195
+ return join15(codebaseDir(directory), POLICIES_FILE);
1347
2196
  }
1348
2197
  function readStore3(directory) {
1349
2198
  const p = policiesPath(directory);
1350
- if (!existsSync10(p))
2199
+ if (!existsSync16(p))
1351
2200
  return { version: "1.0", last_updated: new Date().toISOString(), policies: [] };
1352
2201
  try {
1353
- return JSON.parse(readFileSync10(p, "utf-8"));
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 (!existsSync10(base))
1361
- mkdirSync7(base, { recursive: true });
2209
+ if (!existsSync16(base))
2210
+ mkdirSync10(base, { recursive: true });
1362
2211
  store.last_updated = new Date().toISOString();
1363
- writeFileSync10(policiesPath(directory), JSON.stringify(store, null, 2), "utf-8");
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 readFileSync11, writeFileSync as writeFileSync11 } from "fs";
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 = readFileSync11(fullPath, "utf-8");
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 = createHash("md5").update(args.targetContent).digest("hex");
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
- writeFileSync11(fullPath, newContent, "utf-8");
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 appendFileSync2, existsSync as existsSync11, mkdirSync as mkdirSync8 } from "fs";
1481
- import { join as join11 } from "path";
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 3 specialized agents in parallel and returns their synthesized outputs.",
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 Promise.all(tasks.map(async (task) => {
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 (!existsSync11(base))
1553
- mkdirSync8(base, { recursive: true });
1554
- const path = join11(base, "COUNCILS.jsonl");
1555
- appendFileSync2(path, JSON.stringify(payload) + `
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 writeFileSync12, existsSync as existsSync12, readFileSync as readFileSync12, readdirSync as readdirSync2, statSync } from "fs";
1563
- import { join as join12 } from "path";
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 ? join12(root, args.targetDir) : root;
1573
- if (!existsSync12(target)) {
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 = join12(target, "AGENTS.md");
1577
- if (existsSync12(agentsMdPath) && !args.force) {
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 = join12(root, "package.json");
2470
+ const pkgPath = join17(root, "package.json");
1581
2471
  let projectName = "Project";
1582
2472
  let techStack = "Unknown";
1583
- if (existsSync12(pkgPath)) {
2473
+ if (existsSync18(pkgPath)) {
1584
2474
  try {
1585
- const pkg = JSON.parse(readFileSync12(pkgPath, "utf-8"));
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
- ${readdirSync2(target).slice(0, 10).map((f) => {
1603
- const s = statSync(join12(target, f));
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
- writeFileSync12(agentsMdPath, content, "utf-8");
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 mkdirSync9, writeFileSync as writeFileSync13, existsSync as existsSync13 } from "fs";
1619
- import { join as join13, dirname as dirname3 } from "path";
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 = join13(dirname3(fileURLToPath(import.meta.url)), "..", "skills");
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 = join13(SKILLS_DIR, args.name);
1632
- const skillFile = join13(skillDir, "SKILL.md");
1633
- if (existsSync13(skillFile)) {
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
- mkdirSync9(skillDir, { recursive: true });
1649
- writeFileSync13(skillFile, fullContent, "utf-8");
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 existsSync14, readFileSync as readFileSync13 } from "fs";
1662
- import { join as join14 } from "path";
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 = join14(root, rel);
1692
- if (!existsSync14(full))
2581
+ const full = join19(root, rel);
2582
+ if (!existsSync20(full))
1693
2583
  continue;
1694
2584
  try {
1695
- const raw = readFileSync13(full, "utf-8").trim();
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 existsSync15, readFileSync as readFileSync14, writeFileSync as writeFileSync14, mkdirSync as mkdirSync10 } from "fs";
1720
- import { join as join15 } from "path";
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 join15(codebaseDir(dir), CODEGRAPH_META_FILE);
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 existsSync15(join15(dir, ".codegraph", "codegraph.db"));
2629
+ return existsSync21(join20(dir, ".codegraph", "codegraph.db"));
1740
2630
  }
1741
2631
  function readCodegraphMeta(dir) {
1742
2632
  const path = metaPath(dir);
1743
- if (!existsSync15(path)) {
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 = readFileSync14(path, "utf-8");
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 (!existsSync15(base))
1824
- mkdirSync10(base, { recursive: true });
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
- writeFileSync14(metaPath(dir), lines.join(`
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/hooks/guard-rails.ts
2141
- import { existsSync as existsSync17, readFileSync as readFileSync16 } from "fs";
2142
- import { join as join17 } from "path";
2143
-
2144
- // src/config/loader.ts
2145
- import { existsSync as existsSync16, readFileSync as readFileSync15 } from "fs";
2146
- import { join as join16 } from "path";
2147
- import { homedir } from "os";
2148
- var CONFIG_FILENAME = "flowdeck.json";
2149
- function getGlobalConfigDir() {
2150
- return process.env.OPENCODE_CONFIG_DIR || (process.env.XDG_CONFIG_HOME ? join16(process.env.XDG_CONFIG_HOME, "opencode") : join16(homedir(), ".config", "opencode"));
2151
- }
2152
- function loadFlowDeckConfig(directory) {
2153
- const candidates = [];
2154
- if (directory) {
2155
- candidates.push(join16(directory, ".opencode", CONFIG_FILENAME));
2156
- }
2157
- candidates.push(join16(getGlobalConfigDir(), CONFIG_FILENAME));
2158
- for (const configPath of candidates) {
2159
- if (existsSync16(configPath)) {
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 content = readFileSync15(configPath, "utf-8");
2162
- return JSON.parse(content);
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
- console.warn(`[flowdeck] Failed to load config from ${configPath}`);
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
- enabled: config.designFirst?.enabled ?? true,
2173
- enforcement: config.designFirst?.enforcement ?? "strict",
2174
- requireApprovalBeforeImplementation: config.designFirst?.requireApprovalBeforeImplementation ?? true,
2175
- modelOverrides: config.designFirst?.modelOverrides ?? {},
2176
- defaultSkillsByTaskType: config.designFirst?.defaultSkillsByTaskType ?? {
2177
- "landing-page": ["landing-page-design", "wireframe-planning", "design-system-definition", "frontend-handoff"],
2178
- dashboard: ["dashboard-design", "ui-ux-planning", "wireframe-planning", "responsive-review"],
2179
- "admin-panel": ["ui-ux-planning", "wireframe-planning", "design-system-definition", "frontend-handoff"],
2180
- "app-screen": ["app-shell-design", "ui-ux-planning", "wireframe-planning", "responsive-review"],
2181
- "general-ui": ["ui-ux-planning", "wireframe-planning", "design-system-definition", "frontend-handoff"]
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 (existsSync17(configPath)) {
3319
+ if (existsSync24(configPath)) {
2191
3320
  try {
2192
- const config = JSON.parse(readFileSync16(configPath, "utf-8"));
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 = join17(dir, PLANNING_DIR2);
3375
+ const planningDirPath = join23(dir, PLANNING_DIR2);
2247
3376
  const codebaseDirectory = codebaseDir(dir);
2248
- const configPath = join17(planningDirPath, CONFIG_FILE);
2249
- const statePath2 = join17(planningDirPath, STATE_FILE2);
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" && !existsSync17(planningDirPath)) {
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 (!existsSync17(planningDirPath))
3388
+ if (!existsSync24(planningDirPath))
2260
3389
  return;
2261
- if (!existsSync17(codebaseDirectory)) {
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 (!existsSync17(planPath))
3446
+ if (!existsSync24(planPath))
2318
3447
  return false;
2319
- const planContent = readFileSync16(planPath, "utf-8");
3448
+ const planContent = readFileSync23(planPath, "utf-8");
2320
3449
  return isUiHeavyTask(planContent);
2321
3450
  }
2322
3451
  function effectiveSeverity(configPath, statePath2) {
2323
- if (existsSync17(configPath)) {
3452
+ if (existsSync24(configPath)) {
2324
3453
  try {
2325
- const configContent = readFileSync16(configPath, "utf-8");
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 (!existsSync17(statePath2))
3470
+ if (!existsSync24(statePath2))
2342
3471
  return false;
2343
3472
  try {
2344
- const content = readFileSync16(statePath2, "utf-8");
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 (!existsSync17(join17(planningDir2, STATE_FILE2))) {
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 (!existsSync17(join17(planningDir2, STATE_FILE2))) {
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 existsSync18, readFileSync as readFileSync17 } from "fs";
2366
- import { join as join18 } from "path";
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(tool17, args) {
2374
- const patterns = BLOCKED_PATTERNS[tool17];
3502
+ function isBlocked(tool19, args) {
3503
+ const patterns = BLOCKED_PATTERNS[tool19];
2375
3504
  if (!patterns)
2376
3505
  return null;
2377
- if (tool17 === "bash") {
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 (tool17 === "read") {
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 (tool17 === "write") {
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 = join18(codebaseDir(directory), "CONSTRAINTS.md");
2414
- if (!existsSync18(constraintsPath))
3542
+ const constraintsPath = join24(codebaseDir(directory), "CONSTRAINTS.md");
3543
+ if (!existsSync25(constraintsPath))
2415
3544
  return null;
2416
3545
  try {
2417
- const content = readFileSync17(constraintsPath, "utf-8");
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 (!existsSync18(planPath))
3587
+ if (!existsSync25(planPath))
2459
3588
  return false;
2460
- const planContent = readFileSync17(planPath, "utf-8");
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 existsSync19, readFileSync as readFileSync18 } from "fs";
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 (!existsSync19(planningDir2)) {
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: existsSync19(codebaseDirectory),
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 = readFileSync18(stateFilePath, "utf-8");
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: existsSync19(codebaseDirectory)
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: existsSync19(codebaseDirectory)
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 normalizeCommandName(raw) {
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 = normalizeCommandName(rawCommand);
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(tool17) {
2674
- notify("FlowDeck Permission Required", `Agent needs approval to use tool: ${tool17}`, "critical");
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 existsSync20, readFileSync as readFileSync19 } from "fs";
2679
- import { join as join19 } from "path";
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 = join19(codebaseDir(directory), "VOLATILITY.json");
2702
- if (!existsSync20(p))
3830
+ const p = join25(codebaseDir(directory), "VOLATILITY.json");
3831
+ if (!existsSync27(p))
2703
3832
  return {};
2704
3833
  try {
2705
- const data = JSON.parse(readFileSync19(p, "utf-8"));
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 = join19(codebaseDir(directory), "FAILURES.json");
2716
- if (!existsSync20(p))
3844
+ const p = join25(codebaseDir(directory), "FAILURES.json");
3845
+ if (!existsSync27(p))
2717
3846
  return [];
2718
3847
  try {
2719
- const data = JSON.parse(readFileSync19(p, "utf-8"));
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 existsSync21, mkdirSync as mkdirSync11, appendFileSync as appendFileSync3 } from "fs";
2785
- import { join as join20 } from "path";
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 (!existsSync21(base))
2795
- mkdirSync11(base, { recursive: true });
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
- appendFileSync3(join20(base, "DECISIONS.jsonl"), JSON.stringify(entry) + `
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 existsSync22, readFileSync as readFileSync20, appendFileSync as appendFileSync4, mkdirSync as mkdirSync12 } from "fs";
2814
- import { join as join21 } from "path";
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 join21(codebaseDir(dir), "TELEMETRY.jsonl");
3946
+ return join27(codebaseDir(dir), "TELEMETRY.jsonl");
2818
3947
  }
2819
- function appendEvent(dir, partial) {
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 (!existsSync22(cd))
2824
- mkdirSync12(cd, { recursive: true });
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
- appendFileSync4(telemetryPath(dir), JSON.stringify(event) + `
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
- async function telemetryHook(context, toolInput, output) {
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 tool17 = toolInput.name ?? toolInput.tool ?? "unknown";
4027
+ const tool19 = toolInput.name ?? toolInput.tool ?? "unknown";
2861
4028
  const ids = resolveIds(toolInput);
2862
- appendEvent(dir, {
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: tool17,
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 tool17 = toolInput.name ?? toolInput.tool ?? "unknown";
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
- appendEvent(dir, {
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: tool17,
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 existsSync23, readFileSync as readFileSync21, writeFileSync as writeFileSync15, mkdirSync as mkdirSync13 } from "fs";
2887
- import { join as join22 } from "path";
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 join22(codebaseDir(dir), "APPROVALS.json");
4130
+ return join28(codebaseDir(dir), "APPROVALS.json");
2925
4131
  }
2926
4132
  function loadStore2(dir) {
2927
4133
  const p = approvalsPath(dir);
2928
- if (!existsSync23(p))
4134
+ if (!existsSync30(p))
2929
4135
  return { requests: [] };
2930
4136
  try {
2931
- return JSON.parse(readFileSync21(p, "utf-8"));
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 tool17 = toolInput.name ?? toolInput.tool ?? "";
2950
- if (!WRITE_TOOLS.has(tool17))
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
- appendEvent(dir, {
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: tool17,
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 existsSync24, readFileSync as readFileSync22 } from "fs";
3027
- import { join as join23 } from "path";
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 = createRequire(import.meta.url);
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 (existsSync24(join23(root, lockfile)))
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) && existsSync24(join23(root, marker))) {
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 = join23(root, ".planning", "STATE.md");
3081
- if (!existsSync24(statePath2))
4286
+ const statePath2 = join29(root, ".planning", "STATE.md");
4287
+ if (!existsSync31(statePath2))
3082
4288
  return;
3083
4289
  try {
3084
- const content = readFileSync22(statePath2, "utf-8");
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 existsSync25, readFileSync as readFileSync23 } from "fs";
3190
- import { join as join24 } from "path";
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 statePath2 = join24(directory, ".planning", "STATE.md");
3230
- if (!existsSync25(statePath2))
4456
+ const sp = statePath(directory);
4457
+ if (!existsSync32(sp))
3231
4458
  return null;
3232
4459
  try {
3233
- const content = readFileSync23(statePath2, "utf-8");
3234
- return content.slice(0, 1500);
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 (_input, output) => {
4469
+ return async (input, output) => {
3241
4470
  const sections = ["# FlowDeck Context (preserve across compaction)", ""];
3242
- const state = readPlanningState2(ctx.directory);
3243
- if (state) {
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(state.trim());
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 indexPath = join24(ctx.directory, ".planning", "CODEBASE_INDEX.md");
3251
- let indexSummary = "";
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 = readFileSync23(indexPath, "utf-8");
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
- if (indexSummary) {
3259
- sections.push(indexSummary);
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
- appendEvent(directory, {
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 loadRulePaths() {
7240
- const __dir = dirname4(fileURLToPath2(import.meta.url));
7241
- const rulesDir = join25(__dir, "..", "src", "rules");
7242
- if (!existsSync26(rulesDir))
7243
- return [];
7244
- const paths = [];
7245
- function walk(dir) {
7246
- for (const entry of readdirSync3(dir, { withFileTypes: true })) {
7247
- const full = join25(dir, entry.name);
7248
- if (entry.isDirectory()) {
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 = dirname4(fileURLToPath2(import.meta.url));
7260
- const commandsDir = join25(__dir, "..", "src", "commands");
7261
- if (!existsSync26(commandsDir))
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 readdirSync3(commandsDir)) {
8501
+ for (const file of readdirSync5(commandsDir)) {
7266
8502
  if (!file.endsWith(".md"))
7267
8503
  continue;
7268
- const name = basename(file, ".md");
7269
- const raw = readFileSync24(join25(commandsDir, file), "utf-8");
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 runPipelineTool = createRunPipelineTool(client);
7287
- const delegateTool = createDelegateTool(client);
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 = join25(dirname4(fileURLToPath2(import.meta.url)), "..", "src", "skills");
7352
- if (existsSync26(skillsDir)) {
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 = loadRulePaths();
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 (_input, _output) => {},
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 {