@dv.nghiem/flowdeck 0.4.2 → 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 (100) 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 +1236 -318
  11. package/dist/services/artifact-store.d.ts +39 -0
  12. package/dist/services/artifact-store.d.ts.map +1 -0
  13. package/dist/services/artifact-store.test.d.ts +2 -0
  14. package/dist/services/artifact-store.test.d.ts.map +1 -0
  15. package/dist/services/context-assembler.d.ts +29 -0
  16. package/dist/services/context-assembler.d.ts.map +1 -0
  17. package/dist/services/context-assembler.test.d.ts +2 -0
  18. package/dist/services/context-assembler.test.d.ts.map +1 -0
  19. package/dist/services/cost-budget.d.ts +53 -0
  20. package/dist/services/cost-budget.d.ts.map +1 -0
  21. package/dist/services/cost-budget.test.d.ts +2 -0
  22. package/dist/services/cost-budget.test.d.ts.map +1 -0
  23. package/dist/services/cost-estimator.d.ts +103 -0
  24. package/dist/services/cost-estimator.d.ts.map +1 -0
  25. package/dist/services/cost-estimator.test.d.ts +2 -0
  26. package/dist/services/cost-estimator.test.d.ts.map +1 -0
  27. package/dist/services/draft-verifier.d.ts +48 -0
  28. package/dist/services/draft-verifier.d.ts.map +1 -0
  29. package/dist/services/draft-verifier.test.d.ts +2 -0
  30. package/dist/services/draft-verifier.test.d.ts.map +1 -0
  31. package/dist/services/index.d.ts +13 -0
  32. package/dist/services/index.d.ts.map +1 -1
  33. package/dist/services/lazy-rule-loader.d.ts +104 -0
  34. package/dist/services/lazy-rule-loader.d.ts.map +1 -0
  35. package/dist/services/lazy-rule-loader.test.d.ts +23 -0
  36. package/dist/services/lazy-rule-loader.test.d.ts.map +1 -0
  37. package/dist/services/model-router-ext.test.d.ts +2 -0
  38. package/dist/services/model-router-ext.test.d.ts.map +1 -0
  39. package/dist/services/model-router.d.ts +52 -0
  40. package/dist/services/model-router.d.ts.map +1 -0
  41. package/dist/services/model-router.test.d.ts +2 -0
  42. package/dist/services/model-router.test.d.ts.map +1 -0
  43. package/dist/services/prompt-cache-ext.test.d.ts +2 -0
  44. package/dist/services/prompt-cache-ext.test.d.ts.map +1 -0
  45. package/dist/services/prompt-cache.d.ts +61 -0
  46. package/dist/services/prompt-cache.d.ts.map +1 -0
  47. package/dist/services/prompt-cache.test.d.ts +2 -0
  48. package/dist/services/prompt-cache.test.d.ts.map +1 -0
  49. package/dist/services/rtk-manager.d.ts +80 -0
  50. package/dist/services/rtk-manager.d.ts.map +1 -0
  51. package/dist/services/rtk-manager.test.d.ts +2 -0
  52. package/dist/services/rtk-manager.test.d.ts.map +1 -0
  53. package/dist/services/rtk-policy.d.ts +26 -0
  54. package/dist/services/rtk-policy.d.ts.map +1 -0
  55. package/dist/services/rtk-policy.test.d.ts +2 -0
  56. package/dist/services/rtk-policy.test.d.ts.map +1 -0
  57. package/dist/services/rule-engine.d.ts +29 -0
  58. package/dist/services/rule-engine.d.ts.map +1 -0
  59. package/dist/services/rule-engine.test.d.ts +2 -0
  60. package/dist/services/rule-engine.test.d.ts.map +1 -0
  61. package/dist/services/task-batcher.d.ts +48 -0
  62. package/dist/services/task-batcher.d.ts.map +1 -0
  63. package/dist/services/task-batcher.test.d.ts +2 -0
  64. package/dist/services/task-batcher.test.d.ts.map +1 -0
  65. package/dist/services/telemetry.d.ts +6 -0
  66. package/dist/services/telemetry.d.ts.map +1 -1
  67. package/dist/services/token-budget.d.ts +44 -0
  68. package/dist/services/token-budget.d.ts.map +1 -0
  69. package/dist/services/token-budget.test.d.ts +2 -0
  70. package/dist/services/token-budget.test.d.ts.map +1 -0
  71. package/dist/services/token-metrics-ext.test.d.ts +2 -0
  72. package/dist/services/token-metrics-ext.test.d.ts.map +1 -0
  73. package/dist/services/token-metrics.d.ts +97 -0
  74. package/dist/services/token-metrics.d.ts.map +1 -0
  75. package/dist/services/token-metrics.test.d.ts +2 -0
  76. package/dist/services/token-metrics.test.d.ts.map +1 -0
  77. package/dist/tools/council.d.ts.map +1 -1
  78. package/dist/tools/delegate.d.ts.map +1 -1
  79. package/dist/tools/load-rules.d.ts +25 -0
  80. package/dist/tools/load-rules.d.ts.map +1 -0
  81. package/dist/tools/rtk-setup.d.ts +22 -0
  82. package/dist/tools/rtk-setup.d.ts.map +1 -0
  83. package/dist/tools/run-pipeline.d.ts.map +1 -1
  84. package/docs/commands/fd-map-codebase.md +2 -1
  85. package/docs/configuration/index.md +26 -0
  86. package/docs/getting-started/installation.md +20 -0
  87. package/docs/reference/hooks.md +16 -1
  88. package/docs/reference/rtk.md +162 -0
  89. package/package.json +1 -1
  90. package/src/rules/common/agent-orchestration.md +7 -0
  91. package/src/rules/common/behavioral.md +7 -0
  92. package/src/rules/common/coding-style.md +7 -0
  93. package/src/rules/common/git-workflow.md +7 -0
  94. package/src/rules/common/security.md +7 -0
  95. package/src/rules/common/testing.md +7 -0
  96. package/src/rules/golang/patterns.md +7 -0
  97. package/src/rules/java/patterns.md +7 -0
  98. package/src/rules/python/patterns.md +7 -0
  99. package/src/rules/rust/patterns.md +7 -0
  100. 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) {
@@ -2137,59 +2782,298 @@ var codegraphTool = tool16({
2137
2782
  }
2138
2783
  });
2139
2784
 
2140
- // src/hooks/guard-rails.ts
2141
- import { existsSync as existsSync17, readFileSync as readFileSync16 } from "fs";
2142
- import { join as join17 } from "path";
2143
-
2144
- // src/config/loader.ts
2145
- import { existsSync as existsSync16, readFileSync as readFileSync15 } from "fs";
2146
- import { join as join16 } from "path";
2147
- import { homedir } from "os";
2148
- var CONFIG_FILENAME = "flowdeck.json";
2149
- function getGlobalConfigDir() {
2150
- return process.env.OPENCODE_CONFIG_DIR || (process.env.XDG_CONFIG_HOME ? join16(process.env.XDG_CONFIG_HOME, "opencode") : join16(homedir(), ".config", "opencode"));
2151
- }
2152
- function loadFlowDeckConfig(directory) {
2153
- const candidates = [];
2154
- if (directory) {
2155
- candidates.push(join16(directory, ".opencode", CONFIG_FILENAME));
2156
- }
2157
- candidates.push(join16(getGlobalConfigDir(), CONFIG_FILENAME));
2158
- for (const configPath of candidates) {
2159
- if (existsSync16(configPath)) {
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
+ }
2160
2825
  try {
2161
- const content = readFileSync15(configPath, "utf-8");
2162
- 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);
2163
2832
  } catch {
2164
- console.warn(`[flowdeck] Failed to load config from ${configPath}`);
2833
+ loaded.push(`${name} (read error)`);
2165
2834
  }
2166
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
+ }
2167
2950
  }
2168
- return {};
2169
- }
2170
- function resolveDesignFirstConfig(config) {
2171
2951
  return {
2172
- enabled: config.designFirst?.enabled ?? true,
2173
- enforcement: config.designFirst?.enforcement ?? "strict",
2174
- requireApprovalBeforeImplementation: config.designFirst?.requireApprovalBeforeImplementation ?? true,
2175
- modelOverrides: config.designFirst?.modelOverrides ?? {},
2176
- defaultSkillsByTaskType: config.designFirst?.defaultSkillsByTaskType ?? {
2177
- "landing-page": ["landing-page-design", "wireframe-planning", "design-system-definition", "frontend-handoff"],
2178
- dashboard: ["dashboard-design", "ui-ux-planning", "wireframe-planning", "responsive-review"],
2179
- "admin-panel": ["ui-ux-planning", "wireframe-planning", "design-system-definition", "frontend-handoff"],
2180
- "app-screen": ["app-shell-design", "ui-ux-planning", "wireframe-planning", "responsive-review"],
2181
- "general-ui": ["ui-ux-planning", "wireframe-planning", "design-system-definition", "frontend-handoff"]
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
+ };
2182
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
2183
3016
  };
2184
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
+
2185
3067
  // src/hooks/guard-rails.ts
3068
+ import { existsSync as existsSync24, readFileSync as readFileSync23 } from "fs";
3069
+ import { join as join23 } from "path";
2186
3070
  var PLANNING_DIR2 = ".planning";
2187
3071
  var CONFIG_FILE = "config.json";
2188
3072
  var STATE_FILE2 = "STATE.md";
2189
3073
  function resolveExecutionMode(configPath, trustScore, volatility) {
2190
- if (existsSync17(configPath)) {
3074
+ if (existsSync24(configPath)) {
2191
3075
  try {
2192
- const config = JSON.parse(readFileSync16(configPath, "utf-8"));
3076
+ const config = JSON.parse(readFileSync23(configPath, "utf-8"));
2193
3077
  if (config.execution_mode === "review-only")
2194
3078
  return "review-only";
2195
3079
  if (config.execution_mode === "guarded")
@@ -2243,22 +3127,22 @@ async function guardRailsHook(ctx, input, _output) {
2243
3127
  if (!ENABLED)
2244
3128
  return;
2245
3129
  const dir = ctx.directory;
2246
- const planningDirPath = join17(dir, PLANNING_DIR2);
3130
+ const planningDirPath = join23(dir, PLANNING_DIR2);
2247
3131
  const codebaseDirectory = codebaseDir(dir);
2248
- const configPath = join17(planningDirPath, CONFIG_FILE);
2249
- const statePath2 = join17(planningDirPath, STATE_FILE2);
3132
+ const configPath = join23(planningDirPath, CONFIG_FILE);
3133
+ const statePath2 = join23(planningDirPath, STATE_FILE2);
2250
3134
  const workspaceRoot = findWorkspaceRoot(dir);
2251
3135
  if (workspaceRoot && dir !== workspaceRoot) {
2252
3136
  const config = getWorkspaceConfig(dir);
2253
- if (config && config.workspace_mode === "shared" && !existsSync17(planningDirPath)) {
3137
+ if (config && config.workspace_mode === "shared" && !existsSync24(planningDirPath)) {
2254
3138
  const msg = `No .planning/ in this sub-repo. Switch to workspace root: cd ${workspaceRoot}`;
2255
3139
  throw new Error(`[flowdeck] BLOCK: ${msg}`);
2256
3140
  }
2257
3141
  }
2258
3142
  if (input.tool === "write" || input.tool === "edit") {
2259
- if (!existsSync17(planningDirPath))
3143
+ if (!existsSync24(planningDirPath))
2260
3144
  return;
2261
- if (!existsSync17(codebaseDirectory)) {
3145
+ if (!existsSync24(codebaseDirectory)) {
2262
3146
  throw new Error(`[flowdeck] WARNING: .codebase/ not found. Run /fd-map-codebase to map the codebase.`);
2263
3147
  }
2264
3148
  const execMode = resolveExecutionMode(configPath, null);
@@ -2314,15 +3198,15 @@ function getDesignGateMessage(dir) {
2314
3198
  }
2315
3199
  function planSuggestsUiHeavy(dir, phase) {
2316
3200
  const planPath = phasePlanPath(dir, phase);
2317
- if (!existsSync17(planPath))
3201
+ if (!existsSync24(planPath))
2318
3202
  return false;
2319
- const planContent = readFileSync16(planPath, "utf-8");
3203
+ const planContent = readFileSync23(planPath, "utf-8");
2320
3204
  return isUiHeavyTask(planContent);
2321
3205
  }
2322
3206
  function effectiveSeverity(configPath, statePath2) {
2323
- if (existsSync17(configPath)) {
3207
+ if (existsSync24(configPath)) {
2324
3208
  try {
2325
- const configContent = readFileSync16(configPath, "utf-8");
3209
+ const configContent = readFileSync23(configPath, "utf-8");
2326
3210
  const config = JSON.parse(configContent);
2327
3211
  if (config.guard_enforcement === "warn")
2328
3212
  return "warn";
@@ -2338,10 +3222,10 @@ function getEffectiveSeverity(configPath, statePath2) {
2338
3222
  return effectiveSeverity(configPath, statePath2);
2339
3223
  }
2340
3224
  function getPlanConfirmed(statePath2) {
2341
- if (!existsSync17(statePath2))
3225
+ if (!existsSync24(statePath2))
2342
3226
  return false;
2343
3227
  try {
2344
- const content = readFileSync16(statePath2, "utf-8");
3228
+ const content = readFileSync23(statePath2, "utf-8");
2345
3229
  const match = content.match(/plan_confirmed:\s*(true|false)/i);
2346
3230
  return match ? match[1].toLowerCase() === "true" : false;
2347
3231
  } catch {
@@ -2349,32 +3233,32 @@ function getPlanConfirmed(statePath2) {
2349
3233
  }
2350
3234
  }
2351
3235
  function getWarningMessage(planningDir2) {
2352
- if (!existsSync17(join17(planningDir2, STATE_FILE2))) {
3236
+ if (!existsSync24(join23(planningDir2, STATE_FILE2))) {
2353
3237
  return "No STATE.md found. Run /fd-map-codebase then /fd-new-feature to start a feature.";
2354
3238
  }
2355
3239
  return "Plan not confirmed. Run /fd-plan and confirm to enable execution.";
2356
3240
  }
2357
3241
  function getBlockMessage(planningDir2) {
2358
- if (!existsSync17(join17(planningDir2, STATE_FILE2))) {
3242
+ if (!existsSync24(join23(planningDir2, STATE_FILE2))) {
2359
3243
  return "No STATE.md found. Run /fd-map-codebase then /fd-new-feature to start a feature.";
2360
3244
  }
2361
3245
  return "Plan not confirmed. Run /fd-plan and confirm to enable execution.";
2362
3246
  }
2363
3247
 
2364
3248
  // src/hooks/tool-guard.ts
2365
- import { existsSync as existsSync18, readFileSync as readFileSync17 } from "fs";
2366
- import { join as join18 } from "path";
3249
+ import { existsSync as existsSync25, readFileSync as readFileSync24 } from "fs";
3250
+ import { join as join24 } from "path";
2367
3251
  var IS_ENABLED = () => process.env.FLOWDECK_TOOL_GUARD_ENABLED === "on";
2368
3252
  var BLOCKED_PATTERNS = {
2369
3253
  read: [".env", ".pem", ".key", ".secret"],
2370
3254
  write: ["node_modules"],
2371
3255
  bash: ["rm -rf"]
2372
3256
  };
2373
- function isBlocked(tool17, args) {
2374
- const patterns = BLOCKED_PATTERNS[tool17];
3257
+ function isBlocked(tool19, args) {
3258
+ const patterns = BLOCKED_PATTERNS[tool19];
2375
3259
  if (!patterns)
2376
3260
  return null;
2377
- if (tool17 === "bash") {
3261
+ if (tool19 === "bash") {
2378
3262
  const cmd = args.command;
2379
3263
  if (!cmd)
2380
3264
  return null;
@@ -2385,7 +3269,7 @@ function isBlocked(tool17, args) {
2385
3269
  }
2386
3270
  return null;
2387
3271
  }
2388
- if (tool17 === "read") {
3272
+ if (tool19 === "read") {
2389
3273
  const filePath = args.filePath;
2390
3274
  if (!filePath)
2391
3275
  return null;
@@ -2396,7 +3280,7 @@ function isBlocked(tool17, args) {
2396
3280
  }
2397
3281
  return null;
2398
3282
  }
2399
- if (tool17 === "write") {
3283
+ if (tool19 === "write") {
2400
3284
  const filePath = args.filePath;
2401
3285
  if (!filePath)
2402
3286
  return null;
@@ -2410,11 +3294,11 @@ function isBlocked(tool17, args) {
2410
3294
  return null;
2411
3295
  }
2412
3296
  function checkArchConstraint(directory, filePath) {
2413
- const constraintsPath = join18(codebaseDir(directory), "CONSTRAINTS.md");
2414
- if (!existsSync18(constraintsPath))
3297
+ const constraintsPath = join24(codebaseDir(directory), "CONSTRAINTS.md");
3298
+ if (!existsSync25(constraintsPath))
2415
3299
  return null;
2416
3300
  try {
2417
- const content = readFileSync17(constraintsPath, "utf-8");
3301
+ const content = readFileSync24(constraintsPath, "utf-8");
2418
3302
  const match = content.match(/## Forbidden Paths\n([\s\S]*?)(?:\n##|$)/);
2419
3303
  if (!match)
2420
3304
  return null;
@@ -2455,9 +3339,9 @@ function isUiDesignApprovalRequired(directory) {
2455
3339
  return !(state.design_stage === "handoff_complete" && state.design_approved);
2456
3340
  }
2457
3341
  const planPath = phasePlanPath(directory, state.phase || 1);
2458
- if (!existsSync18(planPath))
3342
+ if (!existsSync25(planPath))
2459
3343
  return false;
2460
- const planContent = readFileSync17(planPath, "utf-8");
3344
+ const planContent = readFileSync24(planPath, "utf-8");
2461
3345
  if (!isUiHeavyTask(planContent))
2462
3346
  return false;
2463
3347
  return !(state.design_stage === "handoff_complete" && state.design_approved);
@@ -2486,18 +3370,18 @@ async function toolGuardHook(ctx, input, output) {
2486
3370
  }
2487
3371
 
2488
3372
  // src/hooks/session-start.ts
2489
- import { existsSync as existsSync19, readFileSync as readFileSync18 } from "fs";
3373
+ import { existsSync as existsSync26, readFileSync as readFileSync25 } from "fs";
2490
3374
  async function sessionStartHook(ctx) {
2491
3375
  const planningDir2 = ctx.directory + "/.planning";
2492
3376
  const codebaseDirectory = codebaseDir(ctx.directory);
2493
3377
  const workspaceRoot = findWorkspaceRoot(ctx.directory);
2494
3378
  const config = workspaceRoot ? getWorkspaceConfig(ctx.directory) : null;
2495
- if (!existsSync19(planningDir2)) {
3379
+ if (!existsSync26(planningDir2)) {
2496
3380
  return {
2497
3381
  flowdeck_phase: null,
2498
3382
  flowdeck_status: "no_plan",
2499
3383
  flowdeck_warning: "Run /fd-map-codebase to index the codebase, then /fd-new-feature to start a feature.",
2500
- flowdeck_has_codebase: existsSync19(codebaseDirectory),
3384
+ flowdeck_has_codebase: existsSync26(codebaseDirectory),
2501
3385
  ...workspaceRoot && config?.sub_repos ? {
2502
3386
  flowdeck_workspace_root: workspaceRoot,
2503
3387
  flowdeck_sub_repos: config.sub_repos,
@@ -2508,7 +3392,7 @@ async function sessionStartHook(ctx) {
2508
3392
  }
2509
3393
  try {
2510
3394
  const stateFilePath = statePath(ctx.directory);
2511
- const content = readFileSync18(stateFilePath, "utf-8");
3395
+ const content = readFileSync25(stateFilePath, "utf-8");
2512
3396
  const state = parseState(content);
2513
3397
  const currentPhase = state["current_phase"] || {};
2514
3398
  const result = {
@@ -2516,7 +3400,7 @@ async function sessionStartHook(ctx) {
2516
3400
  flowdeck_status: currentPhase["status"] ?? null,
2517
3401
  flowdeck_steps_pending: currentPhase["steps_pending"] ?? null,
2518
3402
  flowdeck_last_action: currentPhase["last_action"] ?? null,
2519
- flowdeck_has_codebase: existsSync19(codebaseDirectory)
3403
+ flowdeck_has_codebase: existsSync26(codebaseDirectory)
2520
3404
  };
2521
3405
  if (workspaceRoot && config?.sub_repos && config.sub_repos.length > 0) {
2522
3406
  result.flowdeck_workspace_root = workspaceRoot;
@@ -2531,7 +3415,7 @@ async function sessionStartHook(ctx) {
2531
3415
  flowdeck_phase: null,
2532
3416
  flowdeck_status: "error",
2533
3417
  flowdeck_warning: "State file unreadable. Continuing without flowdeck context.",
2534
- flowdeck_has_codebase: existsSync19(codebaseDirectory)
3418
+ flowdeck_has_codebase: existsSync26(codebaseDirectory)
2535
3419
  };
2536
3420
  if (workspaceRoot && config?.sub_repos && config.sub_repos.length > 0) {
2537
3421
  result.flowdeck_workspace_root = workspaceRoot;
@@ -2670,13 +3554,13 @@ class NotificationController {
2670
3554
  return this.lastNotifiedKey;
2671
3555
  }
2672
3556
  }
2673
- function notifyPermissionNeeded(tool17) {
2674
- 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");
2675
3559
  }
2676
3560
 
2677
3561
  // src/hooks/patch-trust.ts
2678
- import { existsSync as existsSync20, readFileSync as readFileSync19 } from "fs";
2679
- import { join as join19 } from "path";
3562
+ import { existsSync as existsSync27, readFileSync as readFileSync26 } from "fs";
3563
+ import { join as join25 } from "path";
2680
3564
  var HIGH_RISK_KEYWORDS = [
2681
3565
  "password",
2682
3566
  "secret",
@@ -2698,11 +3582,11 @@ var HIGH_RISK_KEYWORDS = [
2698
3582
  "privilege"
2699
3583
  ];
2700
3584
  function loadVolatility(directory) {
2701
- const p = join19(codebaseDir(directory), "VOLATILITY.json");
2702
- if (!existsSync20(p))
3585
+ const p = join25(codebaseDir(directory), "VOLATILITY.json");
3586
+ if (!existsSync27(p))
2703
3587
  return {};
2704
3588
  try {
2705
- const data = JSON.parse(readFileSync19(p, "utf-8"));
3589
+ const data = JSON.parse(readFileSync26(p, "utf-8"));
2706
3590
  const map = {};
2707
3591
  for (const entry of data.entries ?? [])
2708
3592
  map[entry.path] = entry.stability;
@@ -2712,11 +3596,11 @@ function loadVolatility(directory) {
2712
3596
  }
2713
3597
  }
2714
3598
  function loadFailedPaths(directory) {
2715
- const p = join19(codebaseDir(directory), "FAILURES.json");
2716
- if (!existsSync20(p))
3599
+ const p = join25(codebaseDir(directory), "FAILURES.json");
3600
+ if (!existsSync27(p))
2717
3601
  return [];
2718
3602
  try {
2719
- const data = JSON.parse(readFileSync19(p, "utf-8"));
3603
+ const data = JSON.parse(readFileSync26(p, "utf-8"));
2720
3604
  return (data.entries ?? []).flatMap((e) => e.affected_paths ?? []);
2721
3605
  } catch {
2722
3606
  return [];
@@ -2781,8 +3665,8 @@ async function patchTrustHook(ctx, input, output) {
2781
3665
  }
2782
3666
 
2783
3667
  // src/hooks/decision-trace-hook.ts
2784
- import { existsSync as existsSync21, mkdirSync as mkdirSync11, appendFileSync as appendFileSync3 } from "fs";
2785
- import { join as join20 } from "path";
3668
+ import { existsSync as existsSync28, mkdirSync as mkdirSync14, appendFileSync as appendFileSync4 } from "fs";
3669
+ import { join as join26 } from "path";
2786
3670
  async function decisionTraceHook(ctx, input, output) {
2787
3671
  if (input.tool !== "write" && input.tool !== "edit")
2788
3672
  return;
@@ -2791,8 +3675,8 @@ async function decisionTraceHook(ctx, input, output) {
2791
3675
  return;
2792
3676
  const base = codebaseDir(ctx.directory);
2793
3677
  try {
2794
- if (!existsSync21(base))
2795
- mkdirSync11(base, { recursive: true });
3678
+ if (!existsSync28(base))
3679
+ mkdirSync14(base, { recursive: true });
2796
3680
  const entry = {
2797
3681
  timestamp: new Date().toISOString(),
2798
3682
  file_path: filePath,
@@ -2804,30 +3688,30 @@ async function decisionTraceHook(ctx, input, output) {
2804
3688
  risk_level: "unknown",
2805
3689
  auto_recorded: true
2806
3690
  };
2807
- appendFileSync3(join20(base, "DECISIONS.jsonl"), JSON.stringify(entry) + `
3691
+ appendFileSync4(join26(base, "DECISIONS.jsonl"), JSON.stringify(entry) + `
2808
3692
  `, "utf-8");
2809
3693
  } catch {}
2810
3694
  }
2811
3695
 
2812
3696
  // src/services/telemetry.ts
2813
- import { existsSync as existsSync22, readFileSync as readFileSync20, appendFileSync as appendFileSync4, mkdirSync as mkdirSync12 } from "fs";
2814
- import { join as join21 } from "path";
3697
+ import { existsSync as existsSync29, readFileSync as readFileSync27, appendFileSync as appendFileSync5, mkdirSync as mkdirSync15 } from "fs";
3698
+ import { join as join27 } from "path";
2815
3699
  import { randomUUID } from "crypto";
2816
3700
  function telemetryPath(dir) {
2817
- return join21(codebaseDir(dir), "TELEMETRY.jsonl");
3701
+ return join27(codebaseDir(dir), "TELEMETRY.jsonl");
2818
3702
  }
2819
- function appendEvent(dir, partial) {
3703
+ function appendEvent2(dir, partial) {
2820
3704
  if (process.env.TELEMETRY_ENABLED !== "true")
2821
3705
  return null;
2822
3706
  const cd = codebaseDir(dir);
2823
- if (!existsSync22(cd))
2824
- mkdirSync12(cd, { recursive: true });
3707
+ if (!existsSync29(cd))
3708
+ mkdirSync15(cd, { recursive: true });
2825
3709
  const event = {
2826
3710
  id: randomUUID(),
2827
3711
  ts: new Date().toISOString(),
2828
3712
  ...partial
2829
3713
  };
2830
- appendFileSync4(telemetryPath(dir), JSON.stringify(event) + `
3714
+ appendFileSync5(telemetryPath(dir), JSON.stringify(event) + `
2831
3715
  `, "utf-8");
2832
3716
  return event;
2833
3717
  }
@@ -2857,34 +3741,34 @@ function inferStatus(output) {
2857
3741
  }
2858
3742
  async function telemetryHook(context, toolInput, output) {
2859
3743
  const dir = context.directory ?? process.cwd();
2860
- const tool17 = toolInput.name ?? toolInput.tool ?? "unknown";
3744
+ const tool19 = toolInput.name ?? toolInput.tool ?? "unknown";
2861
3745
  const ids = resolveIds(toolInput);
2862
- appendEvent(dir, {
3746
+ appendEvent2(dir, {
2863
3747
  session_id: ids.session_id,
2864
3748
  run_id: ids.run_id,
2865
3749
  event: "tool.call",
2866
- tool: tool17,
3750
+ tool: tool19,
2867
3751
  status: "ok",
2868
3752
  meta: { parameters: output.args ?? {} }
2869
3753
  });
2870
3754
  }
2871
3755
  async function telemetryAfterHook(context, toolInput, output) {
2872
3756
  const dir = context.directory ?? process.cwd();
2873
- const tool17 = toolInput.name ?? toolInput.tool ?? "unknown";
3757
+ const tool19 = toolInput.name ?? toolInput.tool ?? "unknown";
2874
3758
  const ids = resolveIds(toolInput);
2875
3759
  const status = inferStatus(output);
2876
- appendEvent(dir, {
3760
+ appendEvent2(dir, {
2877
3761
  session_id: ids.session_id,
2878
3762
  run_id: ids.run_id,
2879
3763
  event: "tool.complete",
2880
- tool: tool17,
3764
+ tool: tool19,
2881
3765
  status
2882
3766
  });
2883
3767
  }
2884
3768
 
2885
3769
  // src/services/approval-manager.ts
2886
- import { existsSync as existsSync23, readFileSync as readFileSync21, writeFileSync as writeFileSync15, mkdirSync as mkdirSync13 } from "fs";
2887
- import { join as join22 } from "path";
3770
+ import { existsSync as existsSync30, readFileSync as readFileSync28, writeFileSync as writeFileSync17, mkdirSync as mkdirSync16 } from "fs";
3771
+ import { join as join28 } from "path";
2888
3772
  var APPROVAL_TTL_MS = 30 * 60 * 1000;
2889
3773
  var SENSITIVE_PATTERNS = [
2890
3774
  /auth/i,
@@ -2921,14 +3805,14 @@ function isSensitivePath(filePath) {
2921
3805
  return SENSITIVE_PATTERNS.some((p) => p.test(filePath));
2922
3806
  }
2923
3807
  function approvalsPath(dir) {
2924
- return join22(codebaseDir(dir), "APPROVALS.json");
3808
+ return join28(codebaseDir(dir), "APPROVALS.json");
2925
3809
  }
2926
3810
  function loadStore2(dir) {
2927
3811
  const p = approvalsPath(dir);
2928
- if (!existsSync23(p))
3812
+ if (!existsSync30(p))
2929
3813
  return { requests: [] };
2930
3814
  try {
2931
- return JSON.parse(readFileSync21(p, "utf-8"));
3815
+ return JSON.parse(readFileSync28(p, "utf-8"));
2932
3816
  } catch {
2933
3817
  return { requests: [] };
2934
3818
  }
@@ -2946,8 +3830,8 @@ async function approvalHook(context, toolInput, output) {
2946
3830
  if (!ENABLED2)
2947
3831
  return;
2948
3832
  const dir = context.directory ?? process.cwd();
2949
- const tool17 = toolInput.name ?? toolInput.tool ?? "";
2950
- if (!WRITE_TOOLS.has(tool17))
3833
+ const tool19 = toolInput.name ?? toolInput.tool ?? "";
3834
+ if (!WRITE_TOOLS.has(tool19))
2951
3835
  return;
2952
3836
  const args = output.args ?? {};
2953
3837
  const filePath = String(args.path ?? args.file_path ?? args.filename ?? "");
@@ -2958,11 +3842,11 @@ async function approvalHook(context, toolInput, output) {
2958
3842
  const approval = checkApproval(dir, filePath, "");
2959
3843
  if (approval)
2960
3844
  return;
2961
- appendEvent(dir, {
3845
+ appendEvent2(dir, {
2962
3846
  session_id: process.env.OPENCODE_SESSION_ID ?? "session-0",
2963
3847
  run_id: process.env.OPENCODE_RUN_ID ?? "run-0",
2964
3848
  event: "approval.request",
2965
- tool: tool17,
3849
+ tool: tool19,
2966
3850
  status: "blocked",
2967
3851
  files: [filePath],
2968
3852
  meta: { trigger: "sensitive_file", file: filePath }
@@ -3023,15 +3907,15 @@ function createContextWindowMonitorHook() {
3023
3907
  }
3024
3908
 
3025
3909
  // src/hooks/shell-env-hook.ts
3026
- import { existsSync as existsSync24, readFileSync as readFileSync22 } from "fs";
3027
- import { join as join23 } from "path";
3028
- import { createRequire } from "module";
3910
+ import { existsSync as existsSync31, readFileSync as readFileSync29 } from "fs";
3911
+ import { join as join29 } from "path";
3912
+ import { createRequire as createRequire2 } from "module";
3029
3913
  var _version;
3030
3914
  function getVersion() {
3031
3915
  if (_version)
3032
3916
  return _version;
3033
3917
  try {
3034
- const require2 = createRequire(import.meta.url);
3918
+ const require2 = createRequire2(import.meta.url);
3035
3919
  const pkg = require2("../../package.json");
3036
3920
  _version = pkg.version ?? "0.0.0";
3037
3921
  } catch {
@@ -3060,7 +3944,7 @@ var MARKER_TO_LANG = {
3060
3944
  };
3061
3945
  function detectPackageManager(root) {
3062
3946
  for (const [lockfile, pm] of Object.entries(LOCKFILE_TO_PM)) {
3063
- if (existsSync24(join23(root, lockfile)))
3947
+ if (existsSync31(join29(root, lockfile)))
3064
3948
  return pm;
3065
3949
  }
3066
3950
  return;
@@ -3069,7 +3953,7 @@ function detectLanguages(root) {
3069
3953
  const langs = [];
3070
3954
  const seen = new Set;
3071
3955
  for (const [marker, lang] of Object.entries(MARKER_TO_LANG)) {
3072
- if (!seen.has(lang) && existsSync24(join23(root, marker))) {
3956
+ if (!seen.has(lang) && existsSync31(join29(root, marker))) {
3073
3957
  langs.push(lang);
3074
3958
  seen.add(lang);
3075
3959
  }
@@ -3077,17 +3961,29 @@ function detectLanguages(root) {
3077
3961
  return langs;
3078
3962
  }
3079
3963
  function readCurrentPhase(root) {
3080
- const statePath2 = join23(root, ".planning", "STATE.md");
3081
- if (!existsSync24(statePath2))
3964
+ const statePath2 = join29(root, ".planning", "STATE.md");
3965
+ if (!existsSync31(statePath2))
3082
3966
  return;
3083
3967
  try {
3084
- const content = readFileSync22(statePath2, "utf-8");
3968
+ const content = readFileSync29(statePath2, "utf-8");
3085
3969
  const match = content.match(/phase:\s*(\S+)/i);
3086
3970
  return match?.[1];
3087
3971
  } catch {
3088
3972
  return;
3089
3973
  }
3090
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
+ }
3091
3987
  function createShellEnvHook(ctx) {
3092
3988
  const root = ctx.worktree || ctx.directory;
3093
3989
  return async (_input, output) => {
@@ -3105,6 +4001,14 @@ function createShellEnvHook(ctx) {
3105
4001
  const phase = readCurrentPhase(root);
3106
4002
  if (phase)
3107
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
+ }
3108
4012
  };
3109
4013
  }
3110
4014
 
@@ -3186,8 +4090,8 @@ function createSessionIdleHook(client, tracker) {
3186
4090
  }
3187
4091
 
3188
4092
  // src/hooks/compaction-hook.ts
3189
- import { existsSync as existsSync25, readFileSync as readFileSync23 } from "fs";
3190
- import { join as join24 } from "path";
4093
+ import { existsSync as existsSync32, readFileSync as readFileSync30 } from "fs";
4094
+ import { join as join30 } from "path";
3191
4095
  var STRUCTURED_SUMMARY_PROMPT = `
3192
4096
  When summarizing this session, you MUST include the following sections:
3193
4097
 
@@ -3225,40 +4129,58 @@ List all background agent tasks spawned this session.
3225
4129
  For each: agent name, status, description, session_id.
3226
4130
  **RESUME, DON'T RESTART.** Use session_id to continue existing sessions.
3227
4131
  `;
4132
+ var _lastInjected = new Map;
3228
4133
  function readPlanningState2(directory) {
3229
- const statePath2 = join24(directory, ".planning", "STATE.md");
3230
- if (!existsSync25(statePath2))
4134
+ const sp = statePath(directory);
4135
+ if (!existsSync32(sp))
3231
4136
  return null;
3232
4137
  try {
3233
- const content = readFileSync23(statePath2, "utf-8");
3234
- 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 };
3235
4142
  } catch {
3236
4143
  return null;
3237
4144
  }
3238
4145
  }
3239
4146
  function createCompactionHook(ctx, tracker) {
3240
- return async (_input, output) => {
4147
+ return async (input, output) => {
3241
4148
  const sections = ["# FlowDeck Context (preserve across compaction)", ""];
3242
- const state = readPlanningState2(ctx.directory);
3243
- 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) {
3244
4157
  sections.push("## Planning State");
3245
4158
  sections.push("```");
3246
- sections.push(state.trim());
4159
+ sections.push(stateData.content.trim());
3247
4160
  sections.push("```");
3248
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("");
3249
4166
  }
3250
- const indexPath = join24(ctx.directory, ".planning", "CODEBASE_INDEX.md");
3251
- let indexSummary = "";
3252
- if (existsSync25(indexPath)) {
4167
+ const indexPath2 = join30(ctx.directory, ".planning", "CODEBASE_INDEX.md");
4168
+ if (indexChanged && existsSync32(indexPath2)) {
3253
4169
  try {
3254
- const indexContent = readFileSync23(indexPath, "utf-8");
3255
- 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("");
3256
4174
  } catch {}
3257
- }
3258
- if (indexSummary) {
3259
- 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}_`);
3260
4178
  sections.push("");
3261
4179
  }
4180
+ _lastInjected.set(input.sessionID, {
4181
+ stateVersion: currentStateVersion,
4182
+ indexVersion: currentIndexVersion
4183
+ });
3262
4184
  const edited = tracker.getEditedPaths();
3263
4185
  if (edited.length > 0) {
3264
4186
  sections.push("## Recently Edited Files");
@@ -7215,7 +8137,7 @@ function shouldProceed(decision, mode, canBlock) {
7215
8137
  }
7216
8138
  function _emitTelemetry(directory, decision, ctx) {
7217
8139
  try {
7218
- appendEvent(directory, {
8140
+ appendEvent2(directory, {
7219
8141
  session_id: ctx.session_id ?? "session-0",
7220
8142
  run_id: ctx.run_id ?? "unknown",
7221
8143
  event: "supervisor.review",
@@ -7236,37 +8158,29 @@ function _emitTelemetry(directory, decision, ctx) {
7236
8158
  }
7237
8159
 
7238
8160
  // src/index.ts
7239
- function loadRulePaths() {
7240
- const __dir = dirname4(fileURLToPath2(import.meta.url));
7241
- const rulesDir = join25(__dir, "..", "src", "rules");
7242
- if (!existsSync26(rulesDir))
7243
- return [];
7244
- const paths = [];
7245
- function walk(dir) {
7246
- for (const entry of readdirSync3(dir, { withFileTypes: true })) {
7247
- const full = join25(dir, entry.name);
7248
- if (entry.isDirectory()) {
7249
- walk(full);
7250
- } else if (entry.isFile() && entry.name.endsWith(".md") && entry.name !== "README.md") {
7251
- paths.push(full);
7252
- }
7253
- }
7254
- }
7255
- walk(rulesDir);
7256
- return paths;
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 };
7257
8171
  }
7258
8172
  function loadCommands() {
7259
- const __dir = dirname4(fileURLToPath2(import.meta.url));
7260
- const commandsDir = join25(__dir, "..", "src", "commands");
7261
- if (!existsSync26(commandsDir))
8173
+ const __dir = dirname5(fileURLToPath3(import.meta.url));
8174
+ const commandsDir = join31(__dir, "..", "src", "commands");
8175
+ if (!existsSync33(commandsDir))
7262
8176
  return {};
7263
8177
  const commands = {};
7264
8178
  try {
7265
- for (const file of readdirSync3(commandsDir)) {
8179
+ for (const file of readdirSync5(commandsDir)) {
7266
8180
  if (!file.endsWith(".md"))
7267
8181
  continue;
7268
- const name = basename(file, ".md");
7269
- const raw = readFileSync24(join25(commandsDir, file), "utf-8");
8182
+ const name = basename2(file, ".md");
8183
+ const raw = readFileSync31(join31(commandsDir, file), "utf-8");
7270
8184
  let description;
7271
8185
  let template = raw;
7272
8186
  const fmMatch = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/);
@@ -7348,8 +8262,8 @@ var plugin = async (input, _options) => {
7348
8262
  }
7349
8263
  }
7350
8264
  }
7351
- const skillsDir = join25(dirname4(fileURLToPath2(import.meta.url)), "..", "src", "skills");
7352
- if (existsSync26(skillsDir)) {
8265
+ const skillsDir = join31(dirname5(fileURLToPath3(import.meta.url)), "..", "src", "skills");
8266
+ if (existsSync33(skillsDir)) {
7353
8267
  const cfgAny = cfg;
7354
8268
  if (!cfgAny.skills || typeof cfgAny.skills !== "object") {
7355
8269
  cfgAny.skills = { paths: [] };
@@ -7361,7 +8275,8 @@ var plugin = async (input, _options) => {
7361
8275
  cfgSkills.paths.push(skillsDir);
7362
8276
  }
7363
8277
  }
7364
- const rulePaths = loadRulePaths();
8278
+ const { paths: rulePaths, diagnostics: rulesDiag } = lazyLoadRulePaths(directory);
8279
+ appLog(rulesDiag);
7365
8280
  if (rulePaths.length > 0) {
7366
8281
  if (!Array.isArray(cfg.instructions)) {
7367
8282
  cfg.instructions = [];
@@ -7390,7 +8305,10 @@ var plugin = async (input, _options) => {
7390
8305
  "context-generator": contextGeneratorTool,
7391
8306
  "create-skill": createSkillTool,
7392
8307
  reflect: reflectTool,
7393
- codegraph: codegraphTool
8308
+ codegraph: codegraphTool,
8309
+ "load-rules": loadRulesTool,
8310
+ "list-rules": listRulesTool,
8311
+ "rtk-setup": rtkSetupTool
7394
8312
  },
7395
8313
  "shell.env": shellEnvHook,
7396
8314
  "todo.updated": todoHook,