fifony 0.1.42 → 0.1.43

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 (58) hide show
  1. package/app/dist/assets/{CommandPalette-DNR5umI1.js → CommandPalette-M4VAMxCU.js} +1 -1
  2. package/app/dist/assets/{KeyboardShortcutsHelp-Dpl19F20.js → KeyboardShortcutsHelp-DkvPUXQq.js} +1 -1
  3. package/app/dist/assets/OnboardingWizard-B7V9hoCR.js +1 -0
  4. package/app/dist/assets/analytics.lazy-zVJdF880.js +1 -0
  5. package/app/dist/assets/{api-ChEctgc5.js → api-CkVfYg_m.js} +1 -1
  6. package/app/dist/assets/{createLucideIcon-R47sXufx.js → createLucideIcon-Dfk_Hxud.js} +1 -1
  7. package/app/dist/assets/index-BpiCi7Ew.css +1 -0
  8. package/app/dist/assets/index-D2INW0zc.js +47 -0
  9. package/app/dist/assets/vendor-BEoYbFV1.js +9 -0
  10. package/app/dist/index.html +5 -5
  11. package/app/dist/service-worker.js +9 -4
  12. package/bin/fifony.js +3 -0
  13. package/dist/agent/pty-daemon.js +177 -0
  14. package/dist/agent/run-local.js +177 -43
  15. package/dist/{agent-NNGZEKZH.js → agent-RMQTTUEC.js} +37 -16
  16. package/dist/analytics-broadcaster-O6YBP66L.js +145 -0
  17. package/dist/chunk-3NE23NYW.js +82 -0
  18. package/dist/chunk-42AMQAJG.js +404 -0
  19. package/dist/{chunk-H5N7O5NP.js → chunk-AILXZ2TD.js} +79 -147
  20. package/dist/{chunk-I2UHVKHS.js → chunk-BRSR26VK.js} +2 -2
  21. package/dist/chunk-E2EWEYA4.js +1302 -0
  22. package/dist/chunk-ESWHDHH6.js +102 -0
  23. package/dist/{chunk-NB44PCD2.js → chunk-FJNH3G2Z.js} +1061 -1138
  24. package/dist/chunk-MVTGAKQK.js +493 -0
  25. package/dist/chunk-QQQLP3PL.js +155 -0
  26. package/dist/chunk-SOBLO4YZ.js +2016 -0
  27. package/dist/chunk-YRSH2CLW.js +13784 -0
  28. package/dist/cli.js +335 -44
  29. package/dist/{issue-state-machine-GPQNZYUZ.js → fsm-issue-YGGF7SIL.js} +9 -5
  30. package/dist/helpers-L7NYO5XS.js +53 -0
  31. package/dist/issue-log-broadcaster-WZAHISYB.js +84 -0
  32. package/dist/{issues-MZLRSXD6.js → issues-3QRR7KM6.js} +10 -8
  33. package/dist/log-analyzer-K7MXQB4T.js +287 -0
  34. package/dist/mcp/server.js +109 -137
  35. package/dist/parallel-executor-6INE6NDO.js +118 -0
  36. package/dist/pid-manager-UBWXVSMD.js +21 -0
  37. package/dist/queue-workers-XFZK3TT5.js +32 -0
  38. package/dist/replan-issue.command-4UCWYHGZ.js +15 -0
  39. package/dist/scheduler-ZP7GOZDW.js +26 -0
  40. package/dist/{settings-NGY33WQE.js → settings-ZAWDCFP2.js} +32 -8
  41. package/dist/settings.resource-5CW456AZ.js +24 -0
  42. package/dist/store-M6NCKMZY.js +97 -0
  43. package/dist/{web-push-CRVDJKWR.js → web-push-AX5IIK3P.js} +2 -2
  44. package/dist/{workspace-D3F3XGSI.js → workspace-CJTWFWTJ.js} +5 -4
  45. package/package.json +8 -7
  46. package/app/dist/assets/OnboardingWizard-CijMhJDW.js +0 -1
  47. package/app/dist/assets/analytics.lazy-Dq90a756.js +0 -1
  48. package/app/dist/assets/index-Dy_fM427.js +0 -54
  49. package/app/dist/assets/index-Q9jBP0Pz.css +0 -1
  50. package/app/dist/assets/vendor-DkWeBvNl.js +0 -9
  51. package/dist/chunk-2CVTK5F2.js +0 -288
  52. package/dist/chunk-37N5OFHM.js +0 -125
  53. package/dist/chunk-JTKUWIQD.js +0 -8406
  54. package/dist/chunk-RBDBGU2C.js +0 -303
  55. package/dist/issue-runner-CMZPSVC7.js +0 -16
  56. package/dist/queue-workers-XZ6DGH4W.js +0 -23
  57. package/dist/scheduler-NVE6L3P7.js +0 -22
  58. package/dist/store-4HCGBN4L.js +0 -65
@@ -0,0 +1,287 @@
1
+ import {
2
+ resolvePlanStageConfig
3
+ } from "./chunk-YRSH2CLW.js";
4
+ import "./chunk-BRSR26VK.js";
5
+ import "./chunk-QQQLP3PL.js";
6
+ import "./chunk-AILXZ2TD.js";
7
+ import "./chunk-SOBLO4YZ.js";
8
+ import "./chunk-E2EWEYA4.js";
9
+ import "./chunk-MVTGAKQK.js";
10
+ import {
11
+ ADAPTERS,
12
+ detectAvailableProviders,
13
+ resolveProviderCapabilities
14
+ } from "./chunk-FJNH3G2Z.js";
15
+ import {
16
+ logger
17
+ } from "./chunk-DVU3CXWA.js";
18
+ import "./chunk-ESWHDHH6.js";
19
+ import {
20
+ TARGET_ROOT,
21
+ appendFileTail,
22
+ extractJsonObjects
23
+ } from "./chunk-42AMQAJG.js";
24
+ import "./chunk-3NE23NYW.js";
25
+
26
+ // src/agents/planning/log-analyzer.ts
27
+ import { env } from "process";
28
+ import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "fs";
29
+ import { spawn } from "child_process";
30
+ import { tmpdir } from "os";
31
+ import { join } from "path";
32
+ var HEALTHCHECK_JSON_SCHEMA = JSON.stringify({
33
+ type: "object",
34
+ properties: {
35
+ host: { type: "string" },
36
+ port: { type: "number" },
37
+ protocol: { type: "string", enum: ["http", "https", "tcp"] }
38
+ },
39
+ required: ["host", "port", "protocol"],
40
+ additionalProperties: false
41
+ });
42
+ var FIX_JSON_SCHEMA = JSON.stringify({
43
+ type: "object",
44
+ properties: {
45
+ hasProblem: { type: "boolean" },
46
+ title: { type: "string" },
47
+ description: { type: "string" },
48
+ issueType: { type: "string", enum: ["bug", "chore", "feature"] }
49
+ },
50
+ required: ["hasProblem"],
51
+ additionalProperties: false
52
+ });
53
+ function buildHealthcheckPrompt(logTail, serviceName) {
54
+ return `You are analyzing the startup log of a service called "${serviceName}".
55
+
56
+ Your task: identify the host (IP address or hostname) and port where this service is listening for incoming connections.
57
+
58
+ Look for patterns like:
59
+ - "Listening on http://localhost:3000"
60
+ - "Server started on port 3000"
61
+ - "running on 0.0.0.0:8080"
62
+ - "started on 127.0.0.1:4000"
63
+ - Any URL or address/port combination that indicates where the service is reachable
64
+
65
+ Return ONLY a JSON object with this exact structure:
66
+ {"host": "localhost", "port": 3000, "protocol": "http"}
67
+
68
+ Use protocol "http" unless you clearly see "https" or it's a raw TCP service (then use "tcp").
69
+ If you cannot find a clear host+port in the log, return: {"host": "localhost", "port": 0, "protocol": "tcp"}
70
+
71
+ SERVICE LOG (last lines):
72
+ \`\`\`
73
+ ${logTail}
74
+ \`\`\``;
75
+ }
76
+ function buildFixPrompt(logTail, serviceName) {
77
+ return `You are analyzing the log of a service called "${serviceName}".
78
+
79
+ Step 1 \u2014 decide if there is a real actionable problem:
80
+ - Set hasProblem=true if the log contains errors, crashes, unresolved dependencies, misconfigurations, or anything preventing the service from working correctly.
81
+ - Set hasProblem=false if the service started and is running normally (no errors, just startup output or healthy traffic logs).
82
+
83
+ Step 2 \u2014 if hasProblem=true, fill in title, description, and issueType to create a clear issue report a developer can act on:
84
+ - title: short (max 80 chars), specific to the actual error
85
+ - description: root cause, relevant error messages, file paths or commands from the log, and what to investigate
86
+ - issueType: "bug" for crashes/errors, "chore" for config/dependency/tooling issues, "feature" for missing functionality
87
+
88
+ Return ONLY a JSON object:
89
+ { "hasProblem": true, "title": "...", "description": "...", "issueType": "bug" }
90
+ or
91
+ { "hasProblem": false }
92
+
93
+ SERVICE LOG (last lines):
94
+ \`\`\`
95
+ ${logTail}
96
+ \`\`\``;
97
+ }
98
+ function readProviderOutput(resultFile, fallback) {
99
+ if (existsSync(resultFile)) {
100
+ try {
101
+ return readFileSync(resultFile, "utf8").trim();
102
+ } catch {
103
+ }
104
+ }
105
+ return fallback;
106
+ }
107
+ async function runOneShot(command, provider, prompt, timeoutMs) {
108
+ const tempDir = mkdtempSync(join(tmpdir(), "fifony-log-analyze-"));
109
+ const promptFile = join(tempDir, "prompt.md");
110
+ const resultFile = join(tempDir, "result.txt");
111
+ writeFileSync(promptFile, `${prompt}
112
+ `, "utf8");
113
+ const spawnEnv = {
114
+ ...env,
115
+ FIFONY_PROMPT_FILE: promptFile,
116
+ FIFONY_PROMPT: prompt,
117
+ FIFONY_AGENT_PROVIDER: provider,
118
+ FIFONY_RESULT_FILE: resultFile
119
+ };
120
+ return await new Promise((resolve, reject) => {
121
+ const startedAt = Date.now();
122
+ let output = "";
123
+ let timedOut = false;
124
+ const child = spawn(command, { shell: true, cwd: TARGET_ROOT, env: spawnEnv });
125
+ if (child.stdin) child.stdin.end();
126
+ child.stdout?.on("data", (chunk) => {
127
+ output = appendFileTail(output, String(chunk), 12e3);
128
+ });
129
+ child.stderr?.on("data", (chunk) => {
130
+ output = appendFileTail(output, String(chunk), 12e3);
131
+ });
132
+ const timer = setTimeout(() => {
133
+ timedOut = true;
134
+ child.kill("SIGTERM");
135
+ }, Math.max(timeoutMs, 1e3));
136
+ child.on("error", () => {
137
+ clearTimeout(timer);
138
+ rmSync(tempDir, { recursive: true, force: true });
139
+ reject(new Error("Could not execute AI command."));
140
+ });
141
+ child.on("close", (code) => {
142
+ clearTimeout(timer);
143
+ const commandOutput = readProviderOutput(resultFile, output);
144
+ rmSync(tempDir, { recursive: true, force: true });
145
+ if (timedOut) {
146
+ reject(new Error(`Log analysis timed out after ${Date.now() - startedAt}ms.`));
147
+ return;
148
+ }
149
+ if (code !== 0 && !commandOutput.trim()) {
150
+ reject(new Error(`Log analysis command failed (exit ${code ?? "unknown"}) with no output.`));
151
+ return;
152
+ }
153
+ if (code !== 0) {
154
+ logger.warn({ exitCode: code, provider }, "[LogAnalyzer] Provider exited non-zero but produced output \u2014 attempting to use it");
155
+ }
156
+ resolve(commandOutput);
157
+ });
158
+ });
159
+ }
160
+ function extractJsonFromOutput(raw) {
161
+ const text = raw.trim();
162
+ if (!text) return null;
163
+ const codeBlocks = [...text.matchAll(/```(?:json)?\s*([\s\S]*?)\s*```/gi)].map((m) => m[1].trim()).reverse();
164
+ for (const block of codeBlocks) {
165
+ for (const candidate of extractJsonObjects(block)) {
166
+ try {
167
+ return JSON.parse(candidate);
168
+ } catch {
169
+ }
170
+ }
171
+ }
172
+ const candidates = extractJsonObjects(text).reverse();
173
+ for (const candidate of candidates) {
174
+ try {
175
+ const parsed = JSON.parse(candidate);
176
+ if (parsed && typeof parsed === "object") {
177
+ const p = parsed;
178
+ for (const key of ["structured_output", "result", "response", "output"]) {
179
+ const r = p[key];
180
+ if (!r) continue;
181
+ if (typeof r === "object") return r;
182
+ if (typeof r === "string") {
183
+ const clean = r.trim().replace(/^```(?:json)?\s*|\s*```$/g, "").trim();
184
+ for (const inner of extractJsonObjects(clean)) {
185
+ try {
186
+ return JSON.parse(inner);
187
+ } catch {
188
+ }
189
+ }
190
+ try {
191
+ return JSON.parse(r);
192
+ } catch {
193
+ }
194
+ }
195
+ }
196
+ }
197
+ return parsed;
198
+ } catch {
199
+ }
200
+ }
201
+ return null;
202
+ }
203
+ async function resolveProvider(config) {
204
+ const { provider: selectedProvider, model: selectedModel } = await resolvePlanStageConfig(config);
205
+ const providers = detectAvailableProviders();
206
+ const isAvailable = providers.some((p) => p.name === selectedProvider && p.available);
207
+ if (!isAvailable) {
208
+ const known = providers.map((e) => `${e.name}:${e.available ? "ok" : "missing"}`).join(", ");
209
+ throw new Error(`Plan provider "${selectedProvider}" is not available. Detected: ${known}`);
210
+ }
211
+ const adapter = ADAPTERS[selectedProvider];
212
+ if (!adapter) throw new Error(`No adapter for provider "${selectedProvider}".`);
213
+ return { provider: selectedProvider, model: selectedModel, adapter };
214
+ }
215
+ function truncateLog(log, maxLines) {
216
+ const lines = log.split("\n");
217
+ return lines.length > maxLines ? lines.slice(-maxLines).join("\n") : log;
218
+ }
219
+ async function analyzeLogForHealthcheck(logTail, serviceName, config) {
220
+ const { provider, model, adapter } = await resolveProvider(config);
221
+ const caps = resolveProviderCapabilities(provider);
222
+ const command = adapter.buildCommand({
223
+ model,
224
+ readOnly: true,
225
+ jsonSchema: caps.structuredOutput.mode !== "none" ? HEALTHCHECK_JSON_SCHEMA : void 0
226
+ });
227
+ const prompt = buildHealthcheckPrompt(truncateLog(logTail, 150), serviceName);
228
+ const timeoutMs = config.commandTimeoutMs ?? 6e4;
229
+ logger.debug({ provider, serviceName }, "[LogAnalyzer] Detecting healthcheck config from log");
230
+ const raw = await runOneShot(command, provider, prompt, timeoutMs);
231
+ const result = extractJsonFromOutput(raw);
232
+ if (!result?.port || result.port <= 0) {
233
+ logger.debug({ provider, serviceName }, "[LogAnalyzer] Could not extract host/port from log");
234
+ return null;
235
+ }
236
+ const protocol = result.protocol === "https" ? "https" : result.protocol === "tcp" ? "tcp" : "http";
237
+ const host = result.host || "localhost";
238
+ const port = result.port;
239
+ const healthcheck = protocol === "tcp" ? { type: "tcp", port } : { type: "http", endpoint: `${protocol}://${host}:${port}/health`, port };
240
+ logger.info({ provider, serviceName, healthcheck }, "[LogAnalyzer] Healthcheck config detected");
241
+ return healthcheck;
242
+ }
243
+ async function analyzeLogForFix(logTail, serviceName, config) {
244
+ const { provider, model, adapter } = await resolveProvider(config);
245
+ const caps = resolveProviderCapabilities(provider);
246
+ const command = adapter.buildCommand({
247
+ model,
248
+ readOnly: true,
249
+ jsonSchema: caps.structuredOutput.mode !== "none" ? FIX_JSON_SCHEMA : void 0
250
+ });
251
+ const prompt = buildFixPrompt(truncateLog(logTail, 100), serviceName);
252
+ const timeoutMs = config.commandTimeoutMs ?? 6e4;
253
+ logger.debug({ provider, serviceName }, "[LogAnalyzer] Analyzing log for fix suggestion");
254
+ const raw = await runOneShot(command, provider, prompt, timeoutMs);
255
+ const result = extractJsonFromOutput(raw);
256
+ if (result === null) {
257
+ let envelopeKeys = [];
258
+ try {
259
+ const { extractJsonObjects: extractJsonObjects2 } = await import("./helpers-L7NYO5XS.js");
260
+ const candidates = extractJsonObjects2(raw.trim());
261
+ if (candidates.length > 0) envelopeKeys = Object.keys(JSON.parse(candidates[candidates.length - 1]));
262
+ } catch {
263
+ }
264
+ logger.warn({ provider, serviceName, rawLength: raw.length, envelopeKeys, rawTail: raw.slice(-400) }, "[LogAnalyzer] Could not extract fix suggestion from log");
265
+ return null;
266
+ }
267
+ if (!result.hasProblem) {
268
+ logger.debug({ provider, serviceName }, "[LogAnalyzer] No problem detected in log");
269
+ return { hasProblem: false };
270
+ }
271
+ if (!result.title) {
272
+ logger.warn({ provider, serviceName }, "[LogAnalyzer] hasProblem=true but no title in result");
273
+ return null;
274
+ }
275
+ const issueType = ["bug", "chore", "feature"].includes(result.issueType ?? "") ? result.issueType : "bug";
276
+ return {
277
+ hasProblem: true,
278
+ title: result.title.slice(0, 120),
279
+ description: result.description ?? "",
280
+ issueType
281
+ };
282
+ }
283
+ export {
284
+ analyzeLogForFix,
285
+ analyzeLogForHealthcheck
286
+ };
287
+ //# sourceMappingURL=log-analyzer-K7MXQB4T.js.map
@@ -1,8 +1,9 @@
1
1
  import {
2
- parseIssueState,
3
2
  renderPrompt
4
- } from "../chunk-2CVTK5F2.js";
5
- import "../chunk-37N5OFHM.js";
3
+ } from "../chunk-ESWHDHH6.js";
4
+ import {
5
+ parseIssueState
6
+ } from "../chunk-42AMQAJG.js";
6
7
 
7
8
  // src/mcp/server.ts
8
9
  import { env as env3, stdin } from "process";
@@ -210,7 +211,7 @@ async function getIssue(issueId) {
210
211
  const issue = await issueResource?.get(issueId);
211
212
  return issue ?? null;
212
213
  }
213
- async function appendEvent(level, message, payload = {}, issueId) {
214
+ async function appendEvent(level, message, _payload = {}, issueId) {
214
215
  await initDatabase();
215
216
  const { randomUUID } = await import("crypto");
216
217
  await eventResource?.insert({
@@ -488,7 +489,7 @@ async function readResource(uri) {
488
489
  if (diffMatch) {
489
490
  const issueId2 = decodeURIComponent(diffMatch[1]);
490
491
  try {
491
- const result = await apiGet(`/api/diff/${encodeURIComponent(issueId2)}`);
492
+ const result = await apiGet(`/api/issues/${encodeURIComponent(issueId2)}/diff`);
492
493
  return [{ uri, mimeType: "application/json", text: JSON.stringify(result, null, 2) }];
493
494
  } catch (error) {
494
495
  return [{ uri, mimeType: "application/json", text: JSON.stringify({ error: String(error) }, null, 2) }];
@@ -530,6 +531,7 @@ function listTools() {
530
531
  { name: "fifony.list_integrations", description: "List discovered local integrations such as agency-agents profiles and impeccable skills.", inputSchema: { type: "object", properties: {}, additionalProperties: false } },
531
532
  { name: "fifony.integration_snippet", description: "Generate a workflow or prompt snippet for a discovered integration.", inputSchema: { type: "object", properties: { integration: { type: "string" } }, required: ["integration"], additionalProperties: false } },
532
533
  { name: "fifony.get_issue", description: "Get full detail of a single issue including plan, history, events, and diff status.", inputSchema: { type: "object", properties: { issueId: { type: "string", description: "The issue identifier." } }, required: ["issueId"], additionalProperties: false } },
534
+ { name: "fifony.build_context", description: "Build the same ranked context pack used internally by Fifony for planner/executor/reviewer prompts.", inputSchema: { type: "object", properties: { issueId: { type: "string", description: "The issue identifier." }, role: { type: "string", enum: ["planner", "executor", "reviewer"], description: "Optional role override for context building." } }, required: ["issueId"], additionalProperties: false } },
533
535
  { name: "fifony.cancel_issue", description: "Cancel an issue, moving it to Cancelled state.", inputSchema: { type: "object", properties: { issueId: { type: "string", description: "The issue identifier to cancel." } }, required: ["issueId"], additionalProperties: false } },
534
536
  { name: "fifony.retry_issue", description: "Retry a failed or blocked issue, re-queuing it for execution.", inputSchema: { type: "object", properties: { issueId: { type: "string", description: "The issue identifier to retry." } }, required: ["issueId"], additionalProperties: false } },
535
537
  { name: "fifony.enhance", description: "AI-enhance an issue title or description. Provide either an issueId to enhance an existing issue, or title+description for standalone enhancement.", inputSchema: { type: "object", properties: { issueId: { type: "string", description: "Issue identifier (optional, for existing issues)." }, title: { type: "string", description: "Issue title (for standalone enhancement)." }, description: { type: "string", description: "Issue description (for standalone enhancement)." }, field: { type: "string", enum: ["title", "description"], description: "Which field to enhance." } }, required: ["field"], additionalProperties: false } },
@@ -692,7 +694,7 @@ async function callTool(name, args = {}) {
692
694
  let diff = null;
693
695
  let events = [];
694
696
  try {
695
- diff = await apiGet(`/api/diff/${encodeURIComponent(issueId)}`);
697
+ diff = await apiGet(`/api/issues/${encodeURIComponent(issueId)}/diff`);
696
698
  } catch {
697
699
  }
698
700
  try {
@@ -709,6 +711,18 @@ async function callTool(name, args = {}) {
709
711
  recentEvents: events.slice(0, 20)
710
712
  }, null, 2));
711
713
  }
714
+ if (name === "fifony.build_context") {
715
+ const issueId = typeof args.issueId === "string" ? args.issueId.trim() : "";
716
+ const role = typeof args.role === "string" ? args.role.trim() : "";
717
+ if (!issueId) throw new Error("issueId is required");
718
+ try {
719
+ const suffix = role ? `?role=${encodeURIComponent(role)}` : "";
720
+ const result = await apiGet(`/api/issues/${encodeURIComponent(issueId)}/context${suffix}`);
721
+ return toolText(JSON.stringify(result, null, 2));
722
+ } catch (error) {
723
+ throw new Error(`Failed to build context for ${issueId}: ${String(error)}`);
724
+ }
725
+ }
712
726
  if (name === "fifony.cancel_issue") {
713
727
  const issueId = typeof args.issueId === "string" ? args.issueId.trim() : "";
714
728
  if (!issueId) throw new Error("issueId is required");
@@ -754,7 +768,7 @@ async function callTool(name, args = {}) {
754
768
  const issueId = typeof args.issueId === "string" ? args.issueId.trim() : "";
755
769
  if (!issueId) throw new Error("issueId is required");
756
770
  try {
757
- const result = await apiGet(`/api/diff/${encodeURIComponent(issueId)}`);
771
+ const result = await apiGet(`/api/issues/${encodeURIComponent(issueId)}/diff`);
758
772
  return toolText(JSON.stringify(result, null, 2));
759
773
  } catch (error) {
760
774
  throw new Error(`Failed to get diff for ${issueId}: ${String(error)}`);
@@ -764,7 +778,7 @@ async function callTool(name, args = {}) {
764
778
  const issueId = typeof args.issueId === "string" ? args.issueId.trim() : "";
765
779
  if (!issueId) throw new Error("issueId is required");
766
780
  try {
767
- const result = await apiGet(`/api/live/${encodeURIComponent(issueId)}`);
781
+ const result = await apiGet(`/api/issues/${encodeURIComponent(issueId)}/live`);
768
782
  return toolText(JSON.stringify(result, null, 2));
769
783
  } catch (error) {
770
784
  throw new Error(`Failed to get live output for ${issueId}: ${String(error)}`);
@@ -857,6 +871,11 @@ function listPrompts() {
857
871
  }
858
872
 
859
873
  // src/mcp/prompts/prompt-handler.ts
874
+ function stringifyValue(value, fallback) {
875
+ if (typeof value === "string" && value.trim()) return value.trim();
876
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
877
+ return fallback;
878
+ }
860
879
  async function getPrompt(name, args = {}) {
861
880
  if (name === "fifony-integrate-client") {
862
881
  const client = typeof args.client === "string" && args.client.trim() ? args.client.trim() : "mcp-client";
@@ -916,40 +935,33 @@ async function getPrompt(name, args = {}) {
916
935
  const state = issueData.state ?? "Unknown";
917
936
  const attempts = issueData.attempts ?? 0;
918
937
  const maxAttempts = issueData.maxAttempts ?? 3;
919
- const diagnosticText = [
920
- `# Diagnostic Report for Issue ${issueId}`,
921
- ``,
922
- `## Issue Details`,
923
- `- **Title**: ${issueData.title ?? "Unknown"}`,
924
- `- **State**: ${state}`,
925
- `- **Attempts**: ${attempts} / ${maxAttempts}`,
926
- `- **Last Error**: ${lastError ?? "None"}`,
927
- `- **Updated At**: ${issueData.updatedAt ?? "Unknown"}`,
928
- ``,
929
- `## Plan`,
930
- plan ? `- **Summary**: ${plan.summary ?? plan.title ?? "No summary"}` : "No plan generated.",
931
- plan?.steps ? `- **Steps**: ${plan.steps.length} step(s)` : "",
932
- plan?.estimatedComplexity ? `- **Estimated Complexity**: ${plan.estimatedComplexity}` : "",
933
- ``,
934
- `## History`,
935
- ...history.length > 0 ? history.slice(-15).map((entry) => `- ${entry}`) : ["No history entries."],
936
- ``,
937
- `## Recent Events`,
938
- ...events.length > 0 ? events.slice(0, 15).map((event) => `- [${event.kind ?? "info"}] ${event.at ?? ""}: ${event.message ?? ""}`) : ["No events found."],
939
- ``,
940
- `## Diagnostic Questions`,
941
- `Based on the information above, please analyze:`,
942
- `1. What is the root cause of the issue being in "${state}" state?`,
943
- `2. Is the error recoverable? If so, what steps should be taken?`,
944
- `3. Does the plan need modification before retrying?`,
945
- `4. Are there any dependency or configuration issues that need resolution?`,
946
- `5. What is the recommended next action?`
947
- ].filter((line) => line !== void 0).join("\n");
948
938
  return {
949
939
  description: `Diagnostic prompt for blocked/failed issue ${issueId}.`,
950
940
  messages: [{
951
941
  role: "user",
952
- content: { type: "text", text: diagnosticText }
942
+ content: {
943
+ type: "text",
944
+ text: await renderPrompt("mcp-diagnose-blocked", {
945
+ issueId,
946
+ title: stringifyValue(issueData.title, "Unknown"),
947
+ state,
948
+ attempts,
949
+ maxAttempts,
950
+ lastError: stringifyValue(lastError, "None"),
951
+ updatedAt: stringifyValue(issueData.updatedAt, "Unknown"),
952
+ hasPlan: !!plan,
953
+ planSummary: stringifyValue(plan?.summary ?? plan?.title, "No summary"),
954
+ hasPlanSteps: Array.isArray(plan?.steps) && plan.steps.length > 0,
955
+ planStepsCount: Array.isArray(plan?.steps) ? plan.steps.length : 0,
956
+ planComplexity: stringifyValue(plan?.estimatedComplexity, ""),
957
+ history: history.length > 0 ? history.slice(-15).map((entry) => stringifyValue(entry, "Unknown history entry")) : [],
958
+ recentEvents: events.length > 0 ? events.slice(0, 15).map((event) => ({
959
+ kind: stringifyValue(event?.kind, "info"),
960
+ at: stringifyValue(event?.at, "unknown time"),
961
+ message: stringifyValue(event?.message, "")
962
+ })) : []
963
+ })
964
+ }
953
965
  }]
954
966
  };
955
967
  }
@@ -977,39 +989,26 @@ async function getPrompt(name, args = {}) {
977
989
  const outputTokens = typeof overall.outputTokens === "number" ? overall.outputTokens : 0;
978
990
  const totalTokens = typeof overall.totalTokens === "number" ? overall.totalTokens : 0;
979
991
  const estimatedCost = inputTokens / 1e6 * 3 + outputTokens / 1e6 * 15;
980
- const summaryText = [
981
- `# Fifony Weekly Progress Summary`,
982
- ``,
983
- `## Issue Statistics`,
984
- `| Status | Count |`,
985
- `|--------|-------|`,
986
- `| Total Issues | ${totalIssues} |`,
987
- `| Completed (Done) | ${completed} |`,
988
- `| In Progress | ${inProgress} |`,
989
- `| Planned | ${planned} |`,
990
- `| Planning | ${planning} |`,
991
- `| Blocked/Failed | ${blocked} |`,
992
- `| Cancelled | ${cancelled} |`,
993
- ``,
994
- `## Token Usage`,
995
- `- **Total Tokens**: ${totalTokens.toLocaleString()}`,
996
- `- **Input Tokens**: ${inputTokens.toLocaleString()}`,
997
- `- **Output Tokens**: ${outputTokens.toLocaleString()}`,
998
- `- **Estimated Cost**: $${(Math.round(estimatedCost * 100) / 100).toFixed(2)}`,
999
- ``,
1000
- `## Analysis Request`,
1001
- `Based on these metrics, please provide:`,
1002
- `1. A brief summary of overall progress this week`,
1003
- `2. Identification of any bottlenecks (blocked/failed issues)`,
1004
- `3. Token usage efficiency assessment`,
1005
- `4. Recommendations for improving throughput`,
1006
- `5. Priority items for next week`
1007
- ].join("\n");
1008
992
  return {
1009
993
  description: "Weekly progress summary prompt for the Fifony workspace.",
1010
994
  messages: [{
1011
995
  role: "user",
1012
- content: { type: "text", text: summaryText }
996
+ content: {
997
+ type: "text",
998
+ text: await renderPrompt("mcp-weekly-summary", {
999
+ totalIssues,
1000
+ completed,
1001
+ inProgress,
1002
+ planned,
1003
+ planning,
1004
+ blocked,
1005
+ cancelled,
1006
+ totalTokensFormatted: totalTokens.toLocaleString(),
1007
+ inputTokensFormatted: inputTokens.toLocaleString(),
1008
+ outputTokensFormatted: outputTokens.toLocaleString(),
1009
+ estimatedCostFormatted: (Math.round(estimatedCost * 100) / 100).toFixed(2)
1010
+ })
1011
+ }
1013
1012
  }]
1014
1013
  };
1015
1014
  }
@@ -1021,41 +1020,28 @@ async function getPrompt(name, args = {}) {
1021
1020
  if (!issue) throw new Error(`Issue not found: ${issueId}`);
1022
1021
  const issueData = issue;
1023
1022
  const plan = issueData.plan ?? null;
1024
- const steps = plan?.steps ?? [];
1025
- const stepsText = steps.length > 0 ? steps.map((step, index) => `${index + 1}. **${step.title ?? step.description ?? "Step"}**
1026
- ${step.description ?? step.detail ?? ""}`).join("\n") : "No steps defined.";
1027
- const refinementText = [
1028
- `# Plan Refinement for Issue ${issueId}`,
1029
- ``,
1030
- `## Issue`,
1031
- `- **Title**: ${issueData.title ?? "Unknown"}`,
1032
- `- **Description**: ${issueData.description ?? "No description"}`,
1033
- ``,
1034
- `## Current Plan`,
1035
- plan ? `- **Summary**: ${plan.summary ?? plan.title ?? "No summary"}` : "No plan exists yet.",
1036
- plan?.estimatedComplexity ? `- **Complexity**: ${plan.estimatedComplexity}` : "",
1037
- ``,
1038
- `### Steps`,
1039
- stepsText,
1040
- ``,
1041
- concern ? `## Specific Concern
1042
- ${concern}
1043
- ` : "",
1044
- `## Refinement Guidance`,
1045
- `Please review the current plan and provide specific, actionable feedback:`,
1046
- `1. Are the steps correctly ordered and complete?`,
1047
- `2. Are there missing edge cases or error handling steps?`,
1048
- `3. Is the complexity estimate accurate?`,
1049
- `4. Are the file paths and affected areas correct?`,
1050
- `5. Should any steps be split, merged, or removed?`,
1051
- ``,
1052
- `Provide your feedback, and it will be used to refine the plan via \`fifony.refine\`.`
1053
- ].filter((line) => line !== void 0).join("\n");
1023
+ const steps = Array.isArray(plan?.steps) ? plan.steps : [];
1054
1024
  return {
1055
1025
  description: `Plan refinement prompt for issue ${issueId}.`,
1056
1026
  messages: [{
1057
1027
  role: "user",
1058
- content: { type: "text", text: refinementText }
1028
+ content: {
1029
+ type: "text",
1030
+ text: await renderPrompt("mcp-refine-plan", {
1031
+ issueId,
1032
+ title: stringifyValue(issueData.title, "Unknown"),
1033
+ description: stringifyValue(issueData.description, "No description"),
1034
+ hasPlan: !!plan,
1035
+ planSummary: stringifyValue(plan?.summary ?? plan?.title, "No summary"),
1036
+ planComplexity: stringifyValue(plan?.estimatedComplexity, ""),
1037
+ steps: steps.map((step, index) => ({
1038
+ index: index + 1,
1039
+ title: stringifyValue(step?.title ?? step?.description, "Step"),
1040
+ description: stringifyValue(step?.description ?? step?.detail, "")
1041
+ })),
1042
+ concern
1043
+ })
1044
+ }
1059
1045
  }]
1060
1046
  };
1061
1047
  }
@@ -1067,7 +1053,7 @@ ${concern}
1067
1053
  const issueData = issue;
1068
1054
  let diffData = {};
1069
1055
  try {
1070
- diffData = await apiGet(`/api/diff/${encodeURIComponent(issueId)}`);
1056
+ diffData = await apiGet(`/api/issues/${encodeURIComponent(issueId)}/diff`);
1071
1057
  } catch (error) {
1072
1058
  throw new Error(`Cannot fetch diff for issue ${issueId}. Is the runtime running? ${String(error)}`);
1073
1059
  }
@@ -1080,51 +1066,37 @@ ${concern}
1080
1066
  description: `Code review prompt for issue ${issueId} (no changes).`,
1081
1067
  messages: [{
1082
1068
  role: "user",
1083
- content: { type: "text", text: `# Code Review for ${issueId}
1084
-
1085
- No code changes found for this issue. The workspace may not have been created yet or no modifications were made.` }
1069
+ content: {
1070
+ type: "text",
1071
+ text: await renderPrompt("mcp-code-review-empty", { issueId })
1072
+ }
1086
1073
  }]
1087
1074
  };
1088
1075
  }
1089
- const filesTable = files.map((file) => `| ${file.path} | ${file.status} | +${file.additions} | -${file.deletions} |`).join("\n");
1090
- const reviewText = [
1091
- `# Code Review for Issue ${issueId}`,
1092
- ``,
1093
- `## Issue Context`,
1094
- `- **Title**: ${issueData.title ?? "Unknown"}`,
1095
- `- **Description**: ${issueData.description ?? "No description"}`,
1096
- `- **State**: ${issueData.state ?? "Unknown"}`,
1097
- ``,
1098
- `## Change Summary`,
1099
- `- **Files Changed**: ${files.length}`,
1100
- `- **Total Additions**: +${totalAdditions}`,
1101
- `- **Total Deletions**: -${totalDeletions}`,
1102
- ``,
1103
- `### Files`,
1104
- `| Path | Status | Additions | Deletions |`,
1105
- `|------|--------|-----------|-----------|`,
1106
- filesTable,
1107
- ``,
1108
- `## Diff`,
1109
- "```diff",
1110
- diff.length > 5e4 ? diff.substring(0, 5e4) + "\n... (diff truncated at 50KB)" : diff,
1111
- "```",
1112
- ``,
1113
- `## Review Checklist`,
1114
- `Please review the changes and evaluate:`,
1115
- `1. **Correctness**: Do the changes correctly implement what the issue describes?`,
1116
- `2. **Code Quality**: Is the code clean, readable, and follows project conventions?`,
1117
- `3. **Error Handling**: Are edge cases and errors properly handled?`,
1118
- `4. **Security**: Are there any security concerns (hardcoded secrets, SQL injection, XSS)?`,
1119
- `5. **Performance**: Are there any performance concerns or inefficiencies?`,
1120
- `6. **Tests**: Are changes adequately covered by tests?`,
1121
- `7. **Breaking Changes**: Do any changes break backward compatibility?`
1122
- ].join("\n");
1123
1076
  return {
1124
1077
  description: `Code review prompt for issue ${issueId}.`,
1125
1078
  messages: [{
1126
1079
  role: "user",
1127
- content: { type: "text", text: reviewText }
1080
+ content: {
1081
+ type: "text",
1082
+ text: await renderPrompt("mcp-code-review", {
1083
+ issueId,
1084
+ title: stringifyValue(issueData.title, "Unknown"),
1085
+ description: stringifyValue(issueData.description, "No description"),
1086
+ state: stringifyValue(issueData.state, "Unknown"),
1087
+ filesChanged: files.length,
1088
+ totalAdditions,
1089
+ totalDeletions,
1090
+ files: files.map((file) => ({
1091
+ path: stringifyValue(file?.path, "(unknown)"),
1092
+ status: stringifyValue(file?.status, "modified"),
1093
+ additions: typeof file?.additions === "number" ? file.additions : 0,
1094
+ deletions: typeof file?.deletions === "number" ? file.deletions : 0
1095
+ })),
1096
+ diff: diff.length > 5e4 ? `${diff.substring(0, 5e4)}
1097
+ ... (diff truncated at 50KB)` : diff
1098
+ })
1099
+ }
1128
1100
  }]
1129
1101
  };
1130
1102
  }