@dv.nghiem/flowdeck 0.4.1 → 0.4.3

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