fifony 0.1.21 → 0.1.22-next.4ab1d2e

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 (51) hide show
  1. package/README.md +40 -9
  2. package/app/dist/assets/KeyboardShortcutsHelp-BB5jLK_E.js +1 -0
  3. package/app/dist/assets/OnboardingWizard-xyM3Okjv.js +1 -0
  4. package/app/dist/assets/analytics.lazy-CfJXsh6r.js +1 -0
  5. package/app/dist/assets/{createLucideIcon-DtZs0TX0.js → createLucideIcon-BWC-guQt.js} +1 -1
  6. package/app/dist/assets/index-C1QEwHZG.js +43 -0
  7. package/app/dist/assets/index-DjmUHXd1.css +1 -0
  8. package/app/dist/assets/vendor-BTlTWMUF.js +9 -0
  9. package/app/dist/dinofffaur.png +0 -0
  10. package/app/dist/index.html +4 -5
  11. package/app/dist/service-worker.js +1 -1
  12. package/app/public/dinofffaur.png +0 -0
  13. package/bin/fifony-wrap.js +53 -0
  14. package/dist/agent/cli-wrapper.js +78 -0
  15. package/dist/agent/cli-wrapper.js.map +1 -0
  16. package/dist/agent/run-local.js +228 -7894
  17. package/dist/agent/run-local.js.map +1 -1
  18. package/dist/chunk-3QSBGJMT.js +2190 -0
  19. package/dist/chunk-3QSBGJMT.js.map +1 -0
  20. package/dist/chunk-4OLABTVH.js +7083 -0
  21. package/dist/chunk-4OLABTVH.js.map +1 -0
  22. package/dist/chunk-D564G33G.js +91 -0
  23. package/dist/chunk-D564G33G.js.map +1 -0
  24. package/dist/{chunk-SMGXYOWU.js → chunk-DD5BE2W6.js} +430 -31
  25. package/dist/chunk-DD5BE2W6.js.map +1 -0
  26. package/dist/chunk-DVU3CXWA.js +75 -0
  27. package/dist/chunk-DVU3CXWA.js.map +1 -0
  28. package/dist/cli.js +187 -1
  29. package/dist/cli.js.map +1 -1
  30. package/dist/issue-runner-4WL4EK6R.js +13 -0
  31. package/dist/issue-runner-4WL4EK6R.js.map +1 -0
  32. package/dist/issue-state-machine-IWLKOTPI.js +39 -0
  33. package/dist/issue-state-machine-IWLKOTPI.js.map +1 -0
  34. package/dist/mcp/server.js +592 -605
  35. package/dist/mcp/server.js.map +1 -1
  36. package/dist/queue-workers-2I7VRZA7.js +20 -0
  37. package/dist/queue-workers-2I7VRZA7.js.map +1 -0
  38. package/dist/store-3JLC6EXY.js +56 -0
  39. package/dist/store-3JLC6EXY.js.map +1 -0
  40. package/package.json +10 -9
  41. package/FIFONY.md +0 -173
  42. package/app/dist/assets/KeyboardShortcutsHelp-BTjiQe_Y.js +0 -1
  43. package/app/dist/assets/OnboardingWizard-BALlquG0.js +0 -1
  44. package/app/dist/assets/analytics.lazy-DjSzXIey.js +0 -1
  45. package/app/dist/assets/index-BV11ScVl.js +0 -42
  46. package/app/dist/assets/index-DWbxgKSd.css +0 -1
  47. package/app/dist/assets/vendor-BoGBoEwT.js +0 -9
  48. package/app/dist/assets/zap-DpjdVd1i.js +0 -1
  49. package/dist/chunk-SMGXYOWU.js.map +0 -1
  50. package/src/fixtures/agent-catalog.json +0 -208
  51. package/src/fixtures/skill-catalog.json +0 -67
@@ -1,105 +1,27 @@
1
1
  import {
2
2
  inferCapabilityPaths,
3
+ parseIssueState,
3
4
  renderPrompt,
4
5
  resolveTaskCapabilities
5
- } from "../chunk-SMGXYOWU.js";
6
+ } from "../chunk-DD5BE2W6.js";
6
7
 
7
8
  // src/mcp/server.ts
8
- import { createHash } from "crypto";
9
- import { existsSync as existsSync3 } from "fs";
10
- import { dirname, join as join3, resolve as resolve3 } from "path";
11
- import { env as env2, stdin, stdout } from "process";
12
- import { fileURLToPath as fileURLToPath2 } from "url";
13
-
14
- // src/integrations/catalog.ts
15
- import { existsSync, readdirSync, readFileSync } from "fs";
16
- import { homedir } from "os";
17
- import { join, resolve } from "path";
18
- function listNames(basePath) {
19
- if (!existsSync(basePath)) {
20
- return [];
21
- }
22
- return readdirSync(basePath, { withFileTypes: true }).filter((entry) => entry.isDirectory() || entry.isFile()).map((entry) => entry.name).sort((left, right) => left.localeCompare(right));
23
- }
24
- function readSkillSummary(skillPath) {
25
- try {
26
- const skillFile = join(skillPath, "SKILL.md");
27
- if (!existsSync(skillFile)) {
28
- return "";
29
- }
30
- const contents = readFileSync(skillFile, "utf8");
31
- const firstParagraph = contents.split("\n").map((line) => line.trim()).filter(Boolean).find((line) => !line.startsWith("#"));
32
- return firstParagraph ?? "";
33
- } catch {
34
- return "";
35
- }
36
- }
37
- function discoverIntegrations(workspaceRoot) {
38
- const home = homedir();
39
- const agentLocations = [
40
- resolve(workspaceRoot, ".codex", "agents"),
41
- resolve(workspaceRoot, "agents"),
42
- join(home, ".codex", "agents"),
43
- join(home, ".claude", "agents")
44
- ];
45
- const skillLocations = [
46
- resolve(workspaceRoot, ".codex", "skills"),
47
- resolve(workspaceRoot, ".claude", "skills"),
48
- join(home, ".codex", "skills"),
49
- join(home, ".claude", "skills")
50
- ];
51
- const agencyItems = agentLocations.flatMap((location) => listNames(location).map((name) => ({ location, name }))).filter(({ name }) => name.startsWith("agency-")).map(({ location, name }) => `${name} @ ${location}`);
52
- const impeccableItems = skillLocations.flatMap((location) => listNames(location).map((name) => ({ location, name }))).filter(
53
- ({ name }) => name === "teach-impeccable" || name === "frontend-design" || name === "polish" || name === "audit" || name === "critique" || name.includes("impeccable")
54
- ).map(({ location, name }) => {
55
- const summary = readSkillSummary(join(location, name));
56
- return summary ? `${name} @ ${location} \u2014 ${summary}` : `${name} @ ${location}`;
57
- });
58
- return [
59
- {
60
- id: "agency-agents",
61
- kind: "agents",
62
- installed: agencyItems.length > 0,
63
- locations: agentLocations.filter((location) => existsSync(location)),
64
- items: agencyItems,
65
- summary: agencyItems.length > 0 ? "Local specialized agent profiles are available for planner/executor/reviewer roles." : "No agency agent profiles were detected in the standard local locations."
66
- },
67
- {
68
- id: "impeccable",
69
- kind: "skills",
70
- installed: impeccableItems.length > 0,
71
- locations: skillLocations.filter((location) => existsSync(location)),
72
- items: impeccableItems,
73
- summary: impeccableItems.length > 0 ? "Frontend and design-oriented skills are available for review and polish workflows." : "No impeccable-related skills were detected in the standard local skill directories."
74
- }
75
- ];
76
- }
77
- async function buildIntegrationSnippet(integrationId, workspaceRoot) {
78
- if (integrationId === "agency-agents") {
79
- return renderPrompt("integrations-agency-agents", { workspaceRoot });
80
- }
81
- if (integrationId === "impeccable") {
82
- return renderPrompt("integrations-impeccable");
83
- }
84
- return "Unknown integration.";
85
- }
9
+ import { env as env3, stdin } from "process";
86
10
 
87
11
  // src/mcp/database.ts
88
- import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
89
- import { basename, join as join2, resolve as resolve2 } from "path";
12
+ import { existsSync, readFileSync } from "fs";
13
+ import { basename, join, resolve } from "path";
90
14
  import { env } from "process";
91
- import { homedir as homedir2 } from "os";
15
+ import { homedir } from "os";
92
16
  import { fileURLToPath } from "url";
93
17
  var WORKSPACE_ROOT = env.FIFONY_WORKSPACE_ROOT ?? process.cwd();
94
18
  var PERSISTENCE_ROOT = env.FIFONY_PERSISTENCE ?? WORKSPACE_ROOT;
95
19
  var STATE_ROOT = resolvePersistenceRoot(PERSISTENCE_ROOT);
96
- var DATABASE_PATH = join2(STATE_ROOT, "s3db");
97
- var STORAGE_BUCKET = env.FIFONY_STORAGE_BUCKET ?? "fifony";
98
- var STORAGE_KEY_PREFIX = env.FIFONY_STORAGE_KEY_PREFIX ?? "state";
20
+ var DATABASE_PATH = join(STATE_ROOT, "fifony.sqlite");
99
21
  var DEBUG_BOOT = env.FIFONY_DEBUG_BOOT === "1";
100
22
  function resolvePersistenceRoot(value) {
101
- const resolved = value.startsWith("file://") ? fileURLToPath(value) : value.startsWith("~/") ? resolve2(homedir2(), value.slice(2)) : resolve2(value);
102
- return basename(resolved) === ".fifony" ? resolved : join2(resolved, ".fifony");
23
+ const resolved = value.startsWith("file://") ? fileURLToPath(value) : value.startsWith("~/") ? resolve(homedir(), value.slice(2)) : resolve(value);
24
+ return basename(resolved) === ".fifony" ? resolved : join(resolved, ".fifony");
103
25
  }
104
26
  var RUNTIME_RESOURCE = "runtime_state";
105
27
  var ISSUE_RESOURCE = "issues";
@@ -128,14 +50,14 @@ function nowIso() {
128
50
  return (/* @__PURE__ */ new Date()).toISOString();
129
51
  }
130
52
  function safeRead(path) {
131
- return existsSync2(path) ? readFileSync2(path, "utf8") : "";
53
+ return existsSync(path) ? readFileSync(path, "utf8") : "";
132
54
  }
133
55
  async function loadS3dbModule() {
134
56
  try {
135
- const imported = await import("s3db.js/lite");
57
+ const imported = await import("s3db.js");
136
58
  return {
137
59
  default: imported.default,
138
- FileSystemClient: imported.FileSystemClient
60
+ SqliteClient: imported.SqliteClient
139
61
  };
140
62
  } catch (error) {
141
63
  throw new Error(`Unable to load s3db.js: ${String(error)}`);
@@ -146,12 +68,7 @@ async function initDatabase() {
146
68
  debugBoot("mcp:getDatabase:start");
147
69
  const s3db = await loadS3dbModule();
148
70
  debugBoot("mcp:getDatabase:module-loaded");
149
- const client = new s3db.FileSystemClient({
150
- basePath: DATABASE_PATH,
151
- bucket: STORAGE_BUCKET,
152
- keyPrefix: STORAGE_KEY_PREFIX,
153
- verbose: false
154
- });
71
+ const client = new s3db.SqliteClient({ basePath: DATABASE_PATH });
155
72
  database = new s3db.default({ client, verbose: false });
156
73
  await database.connect();
157
74
  debugBoot("mcp:getDatabase:connected");
@@ -315,23 +232,134 @@ async function appendEvent(level, message, payload = {}, issueId) {
315
232
  });
316
233
  }
317
234
 
318
- // src/mcp/server.ts
235
+ // src/mcp/jsonrpc-transport.ts
236
+ import { stdout } from "process";
237
+
238
+ // src/agents/integrations/catalog.ts
239
+ import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync2 } from "fs";
240
+ import { homedir as homedir2 } from "os";
241
+ import { join as join2, resolve as resolve2 } from "path";
242
+ function listNames(basePath) {
243
+ if (!existsSync2(basePath)) {
244
+ return [];
245
+ }
246
+ return readdirSync(basePath, { withFileTypes: true }).filter((entry) => entry.isDirectory() || entry.isFile()).map((entry) => entry.name).sort((left, right) => left.localeCompare(right));
247
+ }
248
+ function readSkillSummary(skillPath) {
249
+ try {
250
+ const skillFile = join2(skillPath, "SKILL.md");
251
+ if (!existsSync2(skillFile)) {
252
+ return "";
253
+ }
254
+ const contents = readFileSync2(skillFile, "utf8");
255
+ const firstParagraph = contents.split("\n").map((line) => line.trim()).filter(Boolean).find((line) => !line.startsWith("#"));
256
+ return firstParagraph ?? "";
257
+ } catch {
258
+ return "";
259
+ }
260
+ }
261
+ function discoverIntegrations(workspaceRoot) {
262
+ const home = homedir2();
263
+ const agentLocations = [
264
+ resolve2(workspaceRoot, ".codex", "agents"),
265
+ resolve2(workspaceRoot, "agents"),
266
+ join2(home, ".codex", "agents"),
267
+ join2(home, ".claude", "agents")
268
+ ];
269
+ const skillLocations = [
270
+ resolve2(workspaceRoot, ".codex", "skills"),
271
+ resolve2(workspaceRoot, ".claude", "skills"),
272
+ join2(home, ".codex", "skills"),
273
+ join2(home, ".claude", "skills")
274
+ ];
275
+ const agencyItems = agentLocations.flatMap((location) => listNames(location).map((name) => ({ location, name }))).filter(({ name }) => name.startsWith("agency-")).map(({ location, name }) => `${name} @ ${location}`);
276
+ const impeccableItems = skillLocations.flatMap((location) => listNames(location).map((name) => ({ location, name }))).filter(
277
+ ({ name }) => name === "teach-impeccable" || name === "frontend-design" || name === "polish" || name === "audit" || name === "critique" || name.includes("impeccable")
278
+ ).map(({ location, name }) => {
279
+ const summary = readSkillSummary(join2(location, name));
280
+ return summary ? `${name} @ ${location} \u2014 ${summary}` : `${name} @ ${location}`;
281
+ });
282
+ return [
283
+ {
284
+ id: "agency-agents",
285
+ kind: "agents",
286
+ installed: agencyItems.length > 0,
287
+ locations: agentLocations.filter((location) => existsSync2(location)),
288
+ items: agencyItems,
289
+ summary: agencyItems.length > 0 ? "Local specialized agent profiles are available for planner/executor/reviewer roles." : "No agency agent profiles were detected in the standard local locations."
290
+ },
291
+ {
292
+ id: "impeccable",
293
+ kind: "skills",
294
+ installed: impeccableItems.length > 0,
295
+ locations: skillLocations.filter((location) => existsSync2(location)),
296
+ items: impeccableItems,
297
+ summary: impeccableItems.length > 0 ? "Frontend and design-oriented skills are available for review and polish workflows." : "No impeccable-related skills were detected in the standard local skill directories."
298
+ }
299
+ ];
300
+ }
301
+ async function buildIntegrationSnippet(integrationId, workspaceRoot) {
302
+ if (integrationId === "agency-agents") {
303
+ return renderPrompt("integrations-agency-agents", { workspaceRoot });
304
+ }
305
+ if (integrationId === "impeccable") {
306
+ return renderPrompt("integrations-impeccable");
307
+ }
308
+ return "Unknown integration.";
309
+ }
310
+
311
+ // src/mcp/api-client.ts
312
+ import { env as env2 } from "process";
313
+ async function resolveApiBaseUrl() {
314
+ const envPort = env2.FIFONY_API_PORT;
315
+ if (envPort) return `http://localhost:${envPort}`;
316
+ const runtime = await getRuntimeSnapshot();
317
+ const config = runtime.config;
318
+ const port = config?.dashboardPort;
319
+ if (port) return `http://localhost:${port}`;
320
+ for (const candidate of [4e3, 3e3, 8080]) {
321
+ try {
322
+ const res = await fetch(`http://localhost:${candidate}/health`, { signal: AbortSignal.timeout(1e3) });
323
+ if (res.ok) return `http://localhost:${candidate}`;
324
+ } catch {
325
+ }
326
+ }
327
+ throw new Error("Fifony runtime API is not reachable. Start the runtime with --port to enable plan/refine/approve/analytics tools.");
328
+ }
329
+ async function apiPost(path, body = {}) {
330
+ const base = await resolveApiBaseUrl();
331
+ const res = await fetch(`${base}${path}`, {
332
+ method: "POST",
333
+ headers: { "content-type": "application/json" },
334
+ body: JSON.stringify(body),
335
+ signal: AbortSignal.timeout(12e4)
336
+ });
337
+ const json = await res.json();
338
+ if (!res.ok || json.ok === false) {
339
+ throw new Error(typeof json.error === "string" ? json.error : `API request failed: ${res.status}`);
340
+ }
341
+ return json;
342
+ }
343
+ async function apiGet(path) {
344
+ const base = await resolveApiBaseUrl();
345
+ const res = await fetch(`${base}${path}`, {
346
+ signal: AbortSignal.timeout(3e4)
347
+ });
348
+ const json = await res.json();
349
+ if (!res.ok || json.ok === false) {
350
+ throw new Error(typeof json.error === "string" ? json.error : `API request failed: ${res.status}`);
351
+ }
352
+ return json;
353
+ }
354
+
355
+ // src/mcp/resources/resource-builder.ts
356
+ import { dirname, join as join3, resolve as resolve3 } from "path";
357
+ import { fileURLToPath as fileURLToPath2 } from "url";
319
358
  var __filename = fileURLToPath2(import.meta.url);
320
359
  var __dirname = dirname(__filename);
321
- var PACKAGE_ROOT = resolve3(__dirname, "../..");
322
- var WORKFLOW_PATH = join3(WORKSPACE_ROOT, "WORKFLOW.md");
360
+ var PACKAGE_ROOT = resolve3(__dirname, "../../..");
323
361
  var README_PATH = join3(PACKAGE_ROOT, "README.md");
324
362
  var FIFONY_GUIDE_PATH = join3(PACKAGE_ROOT, "FIFONY.md");
325
- var DEBUG_BOOT2 = env2.FIFONY_DEBUG_BOOT === "1";
326
- var incomingBuffer = Buffer.alloc(0);
327
- function debugBoot2(message) {
328
- if (!DEBUG_BOOT2) return;
329
- process.stderr.write(`[FIFONY_DEBUG_BOOT] ${message}
330
- `);
331
- }
332
- function hashInput(value) {
333
- return createHash("sha1").update(value).digest("hex").slice(0, 10);
334
- }
335
363
  async function buildIntegrationGuide() {
336
364
  return renderPrompt("mcp-integration-guide", {
337
365
  workspaceRoot: WORKSPACE_ROOT,
@@ -363,7 +391,6 @@ async function buildStateSummary() {
363
391
  workspaceRoot: WORKSPACE_ROOT,
364
392
  persistenceRoot: PERSISTENCE_ROOT,
365
393
  stateRoot: STATE_ROOT,
366
- workflowPresent: existsSync3(WORKFLOW_PATH),
367
394
  runtimeUpdatedAt: runtime.updatedAt ?? null,
368
395
  issueCount: issues.length,
369
396
  issuesByState: byState,
@@ -387,13 +414,15 @@ async function buildIssuePrompt(issue, provider, role) {
387
414
  provider,
388
415
  id: issue.id,
389
416
  title: issue.title,
390
- state: issue.state ?? "Todo",
417
+ state: issue.state ?? "Planning",
391
418
  capabilityCategory: resolution.category,
392
419
  overlays: resolution.overlays,
393
420
  paths: Array.isArray(issue.paths) ? issue.paths.filter((value) => typeof value === "string") : [],
394
421
  description: issue.description || "No description provided."
395
422
  });
396
423
  }
424
+
425
+ // src/mcp/resources/resource-handlers.ts
397
426
  async function listResourcesMcp() {
398
427
  const issues = await getIssues();
399
428
  const resources = [
@@ -405,9 +434,6 @@ async function listResourcesMcp() {
405
434
  { uri: "fifony://integrations", name: "Fifony integrations", description: "Discovered local integrations such as agency-agents and impeccable skills.", mimeType: "application/json" },
406
435
  { uri: "fifony://capabilities", name: "Fifony capability routing", description: "How Fifony would route current issues to providers, profiles, and overlays.", mimeType: "application/json" }
407
436
  ];
408
- if (existsSync3(WORKFLOW_PATH)) {
409
- resources.push({ uri: "fifony://workspace/workflow", name: "Workspace workflow", description: "The active WORKFLOW.md from the target workspace.", mimeType: "text/markdown" });
410
- }
411
437
  resources.push(
412
438
  { uri: "fifony://analytics", name: "Token usage analytics", description: "Token usage analytics snapshot including totals, cost estimates, and per-model breakdown.", mimeType: "application/json" },
413
439
  { uri: "fifony://workflow/config", name: "Workflow config", description: "Current pipeline workflow configuration (plan/execute/review providers, models, and effort).", mimeType: "application/json" },
@@ -466,7 +492,6 @@ async function readResource(uri) {
466
492
  )
467
493
  }];
468
494
  }
469
- if (uri === "fifony://workspace/workflow") return [{ uri, mimeType: "text/markdown", text: safeRead(WORKFLOW_PATH) }];
470
495
  if (uri === "fifony://analytics") {
471
496
  try {
472
497
  const result = await apiGet("/api/analytics/tokens");
@@ -548,407 +573,52 @@ async function readResource(uri) {
548
573
  }
549
574
  throw new Error(`Unknown resource: ${uri}`);
550
575
  }
551
- function listPrompts() {
576
+
577
+ // src/mcp/tools/tool-list.ts
578
+ function listTools() {
552
579
  return [
553
- { name: "fifony-integrate-client", description: "Generate setup instructions for connecting an MCP-capable client to Fifony.", arguments: [{ name: "client", description: "Client name, e.g. codex or claude.", required: true }, { name: "goal", description: "What the client should do with Fifony.", required: false }] },
554
- { name: "fifony-plan-issue", description: "Generate a planning prompt for a specific issue in the Fifony store.", arguments: [{ name: "issueId", description: "Issue identifier.", required: true }, { name: "provider", description: "Agent provider name.", required: false }] },
555
- { name: "fifony-review-workflow", description: "Review the current WORKFLOW.md and propose improvements for orchestration quality.", arguments: [{ name: "provider", description: "Reviewing model or client.", required: false }] },
556
- { name: "fifony-use-integration", description: "Generate a concrete integration prompt for agency-agents or impeccable.", arguments: [{ name: "integration", description: "Integration id: agency-agents or impeccable.", required: true }] },
557
- { name: "fifony-route-task", description: "Explain which providers, profiles, and overlays Fifony would choose for a task.", arguments: [{ name: "title", description: "Task title.", required: true }, { name: "description", description: "Task description.", required: false }, { name: "labels", description: "Comma-separated labels.", required: false }, { name: "paths", description: "Comma-separated target paths or files.", required: false }] },
558
- { name: "fifony-diagnose-blocked", description: "Help diagnose why an issue is blocked or failing, analyzing the issue plan, last error, history, and events.", arguments: [{ name: "issueId", description: "Issue identifier to diagnose.", required: true }] },
559
- { name: "fifony-weekly-summary", description: "Generate a weekly progress summary including issues created, completed, blocked, and token usage.", arguments: [] },
560
- { name: "fifony-refine-plan", description: "Guided plan refinement prompt that shows the current plan and helps provide specific feedback.", arguments: [{ name: "issueId", description: "Issue identifier whose plan to refine.", required: true }, { name: "concern", description: "Optional specific concern to address in refinement.", required: false }] },
561
- { name: "fifony-code-review", description: "Review code changes for an issue by analyzing its git diff.", arguments: [{ name: "issueId", description: "Issue identifier to review.", required: true }] }
580
+ { name: "fifony.status", description: "Return a compact status summary for the current Fifony workspace.", inputSchema: { type: "object", properties: {}, additionalProperties: false } },
581
+ { name: "fifony.list_issues", description: "List issues from the Fifony durable store.", inputSchema: { type: "object", properties: { state: { type: "string" }, capabilityCategory: { type: "string" }, category: { type: "string" } }, additionalProperties: false } },
582
+ { name: "fifony.create_issue", description: "Create a new issue directly in the Fifony durable store.", inputSchema: { type: "object", properties: { id: { type: "string" }, title: { type: "string" }, description: { type: "string" }, priority: { type: "number" }, state: { type: "string" }, labels: { type: "array", items: { type: "string" } }, paths: { type: "array", items: { type: "string" } } }, required: ["title"], additionalProperties: false } },
583
+ { name: "fifony.update_issue_state", description: "Update an issue state in the Fifony store and append an event.", inputSchema: { type: "object", properties: { issueId: { type: "string" }, state: { type: "string" }, note: { type: "string" } }, required: ["issueId", "state"], additionalProperties: false } },
584
+ { name: "fifony.plan", description: "Generate an AI plan for an issue. The issue must be in Planning state. Returns the plan summary and step count.", inputSchema: { type: "object", properties: { issueId: { type: "string", description: "The issue identifier to plan." }, fast: { type: "boolean", description: "Use fast planning mode (less thorough but quicker)." } }, required: ["issueId"], additionalProperties: false } },
585
+ { name: "fifony.refine", description: "Refine an existing plan with feedback. The issue must already have a plan.", inputSchema: { type: "object", properties: { issueId: { type: "string", description: "The issue identifier whose plan to refine." }, feedback: { type: "string", description: "Feedback to guide the plan refinement." } }, required: ["issueId", "feedback"], additionalProperties: false } },
586
+ { name: "fifony.approve", description: "Approve a plan and move the issue to Planned for execution.", inputSchema: { type: "object", properties: { issueId: { type: "string", description: "The issue identifier to approve." } }, required: ["issueId"], additionalProperties: false } },
587
+ { name: "fifony.merge", description: "Merge workspace changes back into the project root. Copies new/modified files from the issue workspace to TARGET_ROOT and removes files that were deleted. Skips fifony internal files, node_modules, .git, and dist.", inputSchema: { type: "object", properties: { issueId: { type: "string", description: "The issue identifier whose workspace to merge." } }, required: ["issueId"], additionalProperties: false } },
588
+ { name: "fifony.analytics", description: "Get token usage analytics including overall totals, cost estimates, and top issues by token consumption.", inputSchema: { type: "object", properties: {}, additionalProperties: false } },
589
+ { name: "fifony.integration_config", description: "Generate a ready-to-paste MCP client configuration snippet for this Fifony workspace.", inputSchema: { type: "object", properties: { client: { type: "string" } }, additionalProperties: false } },
590
+ { name: "fifony.list_integrations", description: "List discovered local integrations such as agency-agents profiles and impeccable skills.", inputSchema: { type: "object", properties: {}, additionalProperties: false } },
591
+ { 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 } },
592
+ { name: "fifony.resolve_capabilities", description: "Resolve which providers, roles, profiles, and overlays Fifony should use for a task.", inputSchema: { type: "object", properties: { title: { type: "string" }, description: { type: "string" }, labels: { type: "array", items: { type: "string" } }, paths: { type: "array", items: { type: "string" } } }, required: ["title"], additionalProperties: false } },
593
+ { 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 } },
594
+ { 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 } },
595
+ { name: "fifony.retry_issue", description: "Retry a failed or blocked issue, resetting it to Planned state.", inputSchema: { type: "object", properties: { issueId: { type: "string", description: "The issue identifier to retry." } }, required: ["issueId"], additionalProperties: false } },
596
+ { 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 } },
597
+ { name: "fifony.get_diff", description: "Get git diff for an issue's workspace, including per-file summary and full diff text.", inputSchema: { type: "object", properties: { issueId: { type: "string", description: "The issue identifier." } }, required: ["issueId"], additionalProperties: false } },
598
+ { name: "fifony.get_live", description: "Get live agent output for a running issue, including log tail, PID, elapsed time, and status.", inputSchema: { type: "object", properties: { issueId: { type: "string", description: "The issue identifier." } }, required: ["issueId"], additionalProperties: false } },
599
+ { name: "fifony.get_events", description: "Get event feed, optionally filtered by issue, kind, or limited.", inputSchema: { type: "object", properties: { issueId: { type: "string", description: "Filter events by issue identifier." }, kind: { type: "string", description: "Filter events by kind (info, error, state, manual, progress)." }, limit: { type: "number", description: "Maximum number of events to return (default 50)." } }, additionalProperties: false } },
600
+ { name: "fifony.get_workflow", description: "Get the current pipeline workflow configuration including providers, models, and effort for plan/execute/review stages.", inputSchema: { type: "object", properties: {}, additionalProperties: false } },
601
+ { name: "fifony.set_workflow", description: "Update the pipeline workflow configuration. Each stage (plan, execute, review) needs provider, model, and effort.", inputSchema: { type: "object", properties: { plan: { type: "object", properties: { provider: { type: "string" }, model: { type: "string" }, effort: { type: "string" } }, required: ["provider", "model", "effort"] }, execute: { type: "object", properties: { provider: { type: "string" }, model: { type: "string" }, effort: { type: "string" } }, required: ["provider", "model", "effort"] }, review: { type: "object", properties: { provider: { type: "string" }, model: { type: "string" }, effort: { type: "string" } }, required: ["provider", "model", "effort"] } }, required: ["plan", "execute", "review"], additionalProperties: false } },
602
+ { name: "fifony.scan_project", description: "Scan the target project structure, returning files, directories, and detected technologies.", inputSchema: { type: "object", properties: {}, additionalProperties: false } },
603
+ { name: "fifony.install_agents", description: "Install agents from the Fifony catalog into the target workspace.", inputSchema: { type: "object", properties: { agents: { type: "array", items: { type: "string" }, description: "List of agent names to install." } }, required: ["agents"], additionalProperties: false } },
604
+ { name: "fifony.install_skills", description: "Install skills from the Fifony catalog into the target workspace.", inputSchema: { type: "object", properties: { skills: { type: "array", items: { type: "string" }, description: "List of skill names to install." } }, required: ["skills"], additionalProperties: false } }
562
605
  ];
563
606
  }
564
- async function getPrompt(name, args = {}) {
565
- if (name === "fifony-integrate-client") {
566
- const client = typeof args.client === "string" && args.client.trim() ? args.client.trim() : "mcp-client";
567
- const goal = typeof args.goal === "string" && args.goal.trim() ? args.goal.trim() : "integrate with the local Fifony workspace";
568
- const integrationGuide = await buildIntegrationGuide();
569
- return {
570
- description: "Client integration prompt for Fifony.",
571
- messages: [{
572
- role: "user",
573
- content: {
574
- type: "text",
575
- text: await renderPrompt("mcp-integrate-client", { client, goal, integrationGuide })
576
- }
577
- }]
578
- };
579
- }
580
- if (name === "fifony-plan-issue") {
581
- const issueId = typeof args.issueId === "string" ? args.issueId : "";
582
- const provider = typeof args.provider === "string" && args.provider.trim() ? args.provider.trim() : "codex";
583
- const issue = issueId ? await getIssue(issueId) : null;
584
- if (!issue) throw new Error(`Issue not found: ${issueId}`);
585
- return {
586
- description: "Issue planning prompt grounded in the Fifony issue store.",
587
- messages: [{
588
- role: "user",
589
- content: { type: "text", text: await buildIssuePrompt(issue, provider, "planner") }
590
- }]
591
- };
592
- }
593
- if (name === "fifony-review-workflow") {
594
- const provider = typeof args.provider === "string" && args.provider.trim() ? args.provider.trim() : "claude";
595
- return {
596
- description: "Workflow review prompt for Fifony orchestration.",
597
- messages: [{
598
- role: "user",
599
- content: {
600
- type: "text",
601
- text: await renderPrompt("mcp-review-workflow", {
602
- provider,
603
- workspaceRoot: WORKSPACE_ROOT,
604
- workflowPresent: existsSync3(WORKFLOW_PATH) ? "yes" : "no"
605
- })
606
- }
607
- }]
608
- };
609
- }
610
- if (name === "fifony-use-integration") {
611
- const integration = typeof args.integration === "string" ? args.integration : "";
612
- return {
613
- description: "Integration guidance for a discovered Fifony extension.",
614
- messages: [{
615
- role: "user",
616
- content: { type: "text", text: await buildIntegrationSnippet(integration, WORKSPACE_ROOT) }
617
- }]
618
- };
619
- }
620
- if (name === "fifony-route-task") {
621
- const title = typeof args.title === "string" ? args.title : "";
622
- const description = typeof args.description === "string" ? args.description : "";
623
- const labels = typeof args.labels === "string" ? args.labels.split(",").map((label) => label.trim()).filter(Boolean) : [];
624
- const paths = typeof args.paths === "string" ? args.paths.split(",").map((value) => value.trim()).filter(Boolean) : [];
625
- const resolution = resolveTaskCapabilities({ title, description, labels, paths });
626
- return {
627
- description: "Task routing prompt produced by the Fifony capability resolver.",
628
- messages: [{
629
- role: "user",
630
- content: {
631
- type: "text",
632
- text: await renderPrompt("mcp-route-task", {
633
- resolutionJson: JSON.stringify(resolution, null, 2)
634
- })
635
- }
636
- }]
637
- };
638
- }
639
- if (name === "fifony-diagnose-blocked") {
640
- const issueId = typeof args.issueId === "string" ? args.issueId.trim() : "";
641
- if (!issueId) throw new Error("issueId is required");
642
- const issue = await getIssue(issueId);
643
- if (!issue) throw new Error(`Issue not found: ${issueId}`);
644
- const issueData = issue;
645
- let events = [];
646
- try {
647
- const evResult = await apiGet(`/api/events/feed?issueId=${encodeURIComponent(issueId)}`);
648
- events = Array.isArray(evResult.events) ? evResult.events.slice(0, 30) : [];
649
- } catch {
650
- const localEvents = await listEvents({ limit: 100 });
651
- events = localEvents.filter((event) => event.issueId === issueId).slice(0, 30);
652
- }
653
- const plan = issueData.plan ?? null;
654
- const history = Array.isArray(issueData.history) ? issueData.history : [];
655
- const lastError = issueData.lastError ?? null;
656
- const state = issueData.state ?? "Unknown";
657
- const attempts = issueData.attempts ?? 0;
658
- const maxAttempts = issueData.maxAttempts ?? 3;
659
- const diagnosticText = [
660
- `# Diagnostic Report for Issue ${issueId}`,
661
- ``,
662
- `## Issue Details`,
663
- `- **Title**: ${issueData.title ?? "Unknown"}`,
664
- `- **State**: ${state}`,
665
- `- **Attempts**: ${attempts} / ${maxAttempts}`,
666
- `- **Last Error**: ${lastError ?? "None"}`,
667
- `- **Updated At**: ${issueData.updatedAt ?? "Unknown"}`,
668
- ``,
669
- `## Plan`,
670
- plan ? `- **Summary**: ${plan.summary ?? plan.title ?? "No summary"}` : "No plan generated.",
671
- plan?.steps ? `- **Steps**: ${plan.steps.length} step(s)` : "",
672
- plan?.estimatedComplexity ? `- **Estimated Complexity**: ${plan.estimatedComplexity}` : "",
673
- ``,
674
- `## History`,
675
- ...history.length > 0 ? history.slice(-15).map((entry) => `- ${entry}`) : ["No history entries."],
676
- ``,
677
- `## Recent Events`,
678
- ...events.length > 0 ? events.slice(0, 15).map((event) => `- [${event.kind ?? "info"}] ${event.at ?? ""}: ${event.message ?? ""}`) : ["No events found."],
679
- ``,
680
- `## Diagnostic Questions`,
681
- `Based on the information above, please analyze:`,
682
- `1. What is the root cause of the issue being in "${state}" state?`,
683
- `2. Is the error recoverable? If so, what steps should be taken?`,
684
- `3. Does the plan need modification before retrying?`,
685
- `4. Are there any dependency or configuration issues that need resolution?`,
686
- `5. What is the recommended next action?`
687
- ].filter((line) => line !== void 0).join("\n");
688
- return {
689
- description: `Diagnostic prompt for blocked/failed issue ${issueId}.`,
690
- messages: [{
691
- role: "user",
692
- content: { type: "text", text: diagnosticText }
693
- }]
694
- };
695
- }
696
- if (name === "fifony-weekly-summary") {
697
- const issues = await getIssues();
698
- let analytics = {};
699
- try {
700
- analytics = await apiGet("/api/analytics/tokens");
701
- } catch {
702
- }
703
- const overall = analytics.overall ?? {};
704
- const byState = issues.reduce((accumulator, issue) => {
705
- const key = issue.state ?? "Unknown";
706
- accumulator[key] = (accumulator[key] ?? 0) + 1;
707
- return accumulator;
708
- }, {});
709
- const totalIssues = issues.length;
710
- const completed = byState["Done"] ?? 0;
711
- const blocked = (byState["Blocked"] ?? 0) + (byState["Failed"] ?? 0);
712
- const inProgress = (byState["Running"] ?? 0) + (byState["In Review"] ?? 0) + (byState["Queued"] ?? 0);
713
- const todo = byState["Todo"] ?? 0;
714
- const planning = byState["Planning"] ?? 0;
715
- const cancelled = byState["Cancelled"] ?? 0;
716
- const inputTokens = typeof overall.inputTokens === "number" ? overall.inputTokens : 0;
717
- const outputTokens = typeof overall.outputTokens === "number" ? overall.outputTokens : 0;
718
- const totalTokens = typeof overall.totalTokens === "number" ? overall.totalTokens : 0;
719
- const estimatedCost = inputTokens / 1e6 * 3 + outputTokens / 1e6 * 15;
720
- const summaryText = [
721
- `# Fifony Weekly Progress Summary`,
722
- ``,
723
- `## Issue Statistics`,
724
- `| Status | Count |`,
725
- `|--------|-------|`,
726
- `| Total Issues | ${totalIssues} |`,
727
- `| Completed (Done) | ${completed} |`,
728
- `| In Progress | ${inProgress} |`,
729
- `| Todo | ${todo} |`,
730
- `| Planning | ${planning} |`,
731
- `| Blocked/Failed | ${blocked} |`,
732
- `| Cancelled | ${cancelled} |`,
733
- ``,
734
- `## Token Usage`,
735
- `- **Total Tokens**: ${totalTokens.toLocaleString()}`,
736
- `- **Input Tokens**: ${inputTokens.toLocaleString()}`,
737
- `- **Output Tokens**: ${outputTokens.toLocaleString()}`,
738
- `- **Estimated Cost**: $${(Math.round(estimatedCost * 100) / 100).toFixed(2)}`,
739
- ``,
740
- `## Analysis Request`,
741
- `Based on these metrics, please provide:`,
742
- `1. A brief summary of overall progress this week`,
743
- `2. Identification of any bottlenecks (blocked/failed issues)`,
744
- `3. Token usage efficiency assessment`,
745
- `4. Recommendations for improving throughput`,
746
- `5. Priority items for next week`
747
- ].join("\n");
748
- return {
749
- description: "Weekly progress summary prompt for the Fifony workspace.",
750
- messages: [{
751
- role: "user",
752
- content: { type: "text", text: summaryText }
753
- }]
754
- };
755
- }
756
- if (name === "fifony-refine-plan") {
757
- const issueId = typeof args.issueId === "string" ? args.issueId.trim() : "";
758
- const concern = typeof args.concern === "string" ? args.concern.trim() : "";
759
- if (!issueId) throw new Error("issueId is required");
760
- const issue = await getIssue(issueId);
761
- if (!issue) throw new Error(`Issue not found: ${issueId}`);
762
- const issueData = issue;
763
- const plan = issueData.plan ?? null;
764
- const steps = plan?.steps ?? [];
765
- const stepsText = steps.length > 0 ? steps.map((step, index) => `${index + 1}. **${step.title ?? step.description ?? "Step"}**
766
- ${step.description ?? step.detail ?? ""}`).join("\n") : "No steps defined.";
767
- const refinementText = [
768
- `# Plan Refinement for Issue ${issueId}`,
769
- ``,
770
- `## Issue`,
771
- `- **Title**: ${issueData.title ?? "Unknown"}`,
772
- `- **Description**: ${issueData.description ?? "No description"}`,
773
- ``,
774
- `## Current Plan`,
775
- plan ? `- **Summary**: ${plan.summary ?? plan.title ?? "No summary"}` : "No plan exists yet.",
776
- plan?.estimatedComplexity ? `- **Complexity**: ${plan.estimatedComplexity}` : "",
777
- ``,
778
- `### Steps`,
779
- stepsText,
780
- ``,
781
- concern ? `## Specific Concern
782
- ${concern}
783
- ` : "",
784
- `## Refinement Guidance`,
785
- `Please review the current plan and provide specific, actionable feedback:`,
786
- `1. Are the steps correctly ordered and complete?`,
787
- `2. Are there missing edge cases or error handling steps?`,
788
- `3. Is the complexity estimate accurate?`,
789
- `4. Are the file paths and affected areas correct?`,
790
- `5. Should any steps be split, merged, or removed?`,
791
- ``,
792
- `Provide your feedback, and it will be used to refine the plan via \`fifony.refine\`.`
793
- ].filter((line) => line !== void 0).join("\n");
794
- return {
795
- description: `Plan refinement prompt for issue ${issueId}.`,
796
- messages: [{
797
- role: "user",
798
- content: { type: "text", text: refinementText }
799
- }]
800
- };
801
- }
802
- if (name === "fifony-code-review") {
803
- const issueId = typeof args.issueId === "string" ? args.issueId.trim() : "";
804
- if (!issueId) throw new Error("issueId is required");
805
- const issue = await getIssue(issueId);
806
- if (!issue) throw new Error(`Issue not found: ${issueId}`);
807
- const issueData = issue;
808
- let diffData = {};
809
- try {
810
- diffData = await apiGet(`/api/diff/${encodeURIComponent(issueId)}`);
811
- } catch (error) {
812
- throw new Error(`Cannot fetch diff for issue ${issueId}. Is the runtime running? ${String(error)}`);
813
- }
814
- const files = Array.isArray(diffData.files) ? diffData.files : [];
815
- const diff = typeof diffData.diff === "string" ? diffData.diff : "";
816
- const totalAdditions = typeof diffData.totalAdditions === "number" ? diffData.totalAdditions : 0;
817
- const totalDeletions = typeof diffData.totalDeletions === "number" ? diffData.totalDeletions : 0;
818
- if (!diff.trim()) {
819
- return {
820
- description: `Code review prompt for issue ${issueId} (no changes).`,
821
- messages: [{
822
- role: "user",
823
- content: { type: "text", text: `# Code Review for ${issueId}
824
-
825
- No code changes found for this issue. The workspace may not have been created yet or no modifications were made.` }
826
- }]
827
- };
828
- }
829
- const filesTable = files.map((file) => `| ${file.path} | ${file.status} | +${file.additions} | -${file.deletions} |`).join("\n");
830
- const reviewText = [
831
- `# Code Review for Issue ${issueId}`,
832
- ``,
833
- `## Issue Context`,
834
- `- **Title**: ${issueData.title ?? "Unknown"}`,
835
- `- **Description**: ${issueData.description ?? "No description"}`,
836
- `- **State**: ${issueData.state ?? "Unknown"}`,
837
- ``,
838
- `## Change Summary`,
839
- `- **Files Changed**: ${files.length}`,
840
- `- **Total Additions**: +${totalAdditions}`,
841
- `- **Total Deletions**: -${totalDeletions}`,
842
- ``,
843
- `### Files`,
844
- `| Path | Status | Additions | Deletions |`,
845
- `|------|--------|-----------|-----------|`,
846
- filesTable,
847
- ``,
848
- `## Diff`,
849
- "```diff",
850
- diff.length > 5e4 ? diff.substring(0, 5e4) + "\n... (diff truncated at 50KB)" : diff,
851
- "```",
852
- ``,
853
- `## Review Checklist`,
854
- `Please review the changes and evaluate:`,
855
- `1. **Correctness**: Do the changes correctly implement what the issue describes?`,
856
- `2. **Code Quality**: Is the code clean, readable, and follows project conventions?`,
857
- `3. **Error Handling**: Are edge cases and errors properly handled?`,
858
- `4. **Security**: Are there any security concerns (hardcoded secrets, SQL injection, XSS)?`,
859
- `5. **Performance**: Are there any performance concerns or inefficiencies?`,
860
- `6. **Tests**: Are changes adequately covered by tests?`,
861
- `7. **Breaking Changes**: Do any changes break backward compatibility?`
862
- ].join("\n");
863
- return {
864
- description: `Code review prompt for issue ${issueId}.`,
865
- messages: [{
866
- role: "user",
867
- content: { type: "text", text: reviewText }
868
- }]
869
- };
870
- }
871
- throw new Error(`Unknown prompt: ${name}`);
872
- }
873
- function listTools() {
874
- return [
875
- { name: "fifony.status", description: "Return a compact status summary for the current Fifony workspace.", inputSchema: { type: "object", properties: {}, additionalProperties: false } },
876
- { name: "fifony.list_issues", description: "List issues from the Fifony durable store.", inputSchema: { type: "object", properties: { state: { type: "string" }, capabilityCategory: { type: "string" }, category: { type: "string" } }, additionalProperties: false } },
877
- { name: "fifony.create_issue", description: "Create a new issue directly in the Fifony durable store.", inputSchema: { type: "object", properties: { id: { type: "string" }, title: { type: "string" }, description: { type: "string" }, priority: { type: "number" }, state: { type: "string" }, labels: { type: "array", items: { type: "string" } }, paths: { type: "array", items: { type: "string" } } }, required: ["title"], additionalProperties: false } },
878
- { name: "fifony.update_issue_state", description: "Update an issue state in the Fifony store and append an event.", inputSchema: { type: "object", properties: { issueId: { type: "string" }, state: { type: "string" }, note: { type: "string" } }, required: ["issueId", "state"], additionalProperties: false } },
879
- { name: "fifony.plan", description: "Generate an AI plan for an issue. The issue must be in Planning state. Returns the plan summary and step count.", inputSchema: { type: "object", properties: { issueId: { type: "string", description: "The issue identifier to plan." }, fast: { type: "boolean", description: "Use fast planning mode (less thorough but quicker)." } }, required: ["issueId"], additionalProperties: false } },
880
- { name: "fifony.refine", description: "Refine an existing plan with feedback. The issue must already have a plan.", inputSchema: { type: "object", properties: { issueId: { type: "string", description: "The issue identifier whose plan to refine." }, feedback: { type: "string", description: "Feedback to guide the plan refinement." } }, required: ["issueId", "feedback"], additionalProperties: false } },
881
- { name: "fifony.approve", description: "Approve a plan and move the issue to Todo for execution.", inputSchema: { type: "object", properties: { issueId: { type: "string", description: "The issue identifier to approve." } }, required: ["issueId"], additionalProperties: false } },
882
- { name: "fifony.merge", description: "Merge workspace changes back into the project root. Copies new/modified files from the issue workspace to TARGET_ROOT and removes files that were deleted. Skips fifony internal files, node_modules, .git, and dist.", inputSchema: { type: "object", properties: { issueId: { type: "string", description: "The issue identifier whose workspace to merge." } }, required: ["issueId"], additionalProperties: false } },
883
- { name: "fifony.analytics", description: "Get token usage analytics including overall totals, cost estimates, and top issues by token consumption.", inputSchema: { type: "object", properties: {}, additionalProperties: false } },
884
- { name: "fifony.integration_config", description: "Generate a ready-to-paste MCP client configuration snippet for this Fifony workspace.", inputSchema: { type: "object", properties: { client: { type: "string" } }, additionalProperties: false } },
885
- { name: "fifony.list_integrations", description: "List discovered local integrations such as agency-agents profiles and impeccable skills.", inputSchema: { type: "object", properties: {}, additionalProperties: false } },
886
- { 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 } },
887
- { name: "fifony.resolve_capabilities", description: "Resolve which providers, roles, profiles, and overlays Fifony should use for a task.", inputSchema: { type: "object", properties: { title: { type: "string" }, description: { type: "string" }, labels: { type: "array", items: { type: "string" } }, paths: { type: "array", items: { type: "string" } } }, required: ["title"], additionalProperties: false } },
888
- { 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 } },
889
- { 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 } },
890
- { name: "fifony.retry_issue", description: "Retry a failed or blocked issue, resetting it to Todo state.", inputSchema: { type: "object", properties: { issueId: { type: "string", description: "The issue identifier to retry." } }, required: ["issueId"], additionalProperties: false } },
891
- { 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 } },
892
- { name: "fifony.get_diff", description: "Get git diff for an issue's workspace, including per-file summary and full diff text.", inputSchema: { type: "object", properties: { issueId: { type: "string", description: "The issue identifier." } }, required: ["issueId"], additionalProperties: false } },
893
- { name: "fifony.get_live", description: "Get live agent output for a running issue, including log tail, PID, elapsed time, and status.", inputSchema: { type: "object", properties: { issueId: { type: "string", description: "The issue identifier." } }, required: ["issueId"], additionalProperties: false } },
894
- { name: "fifony.get_events", description: "Get event feed, optionally filtered by issue, kind, or limited.", inputSchema: { type: "object", properties: { issueId: { type: "string", description: "Filter events by issue identifier." }, kind: { type: "string", description: "Filter events by kind (info, error, state, manual, progress)." }, limit: { type: "number", description: "Maximum number of events to return (default 50)." } }, additionalProperties: false } },
895
- { name: "fifony.get_workflow", description: "Get the current pipeline workflow configuration including providers, models, and effort for plan/execute/review stages.", inputSchema: { type: "object", properties: {}, additionalProperties: false } },
896
- { name: "fifony.set_workflow", description: "Update the pipeline workflow configuration. Each stage (plan, execute, review) needs provider, model, and effort.", inputSchema: { type: "object", properties: { plan: { type: "object", properties: { provider: { type: "string" }, model: { type: "string" }, effort: { type: "string" } }, required: ["provider", "model", "effort"] }, execute: { type: "object", properties: { provider: { type: "string" }, model: { type: "string" }, effort: { type: "string" } }, required: ["provider", "model", "effort"] }, review: { type: "object", properties: { provider: { type: "string" }, model: { type: "string" }, effort: { type: "string" } }, required: ["provider", "model", "effort"] } }, required: ["plan", "execute", "review"], additionalProperties: false } },
897
- { name: "fifony.scan_project", description: "Scan the target project structure, returning files, directories, and detected technologies.", inputSchema: { type: "object", properties: {}, additionalProperties: false } },
898
- { name: "fifony.install_agents", description: "Install agents from the Fifony catalog into the target workspace.", inputSchema: { type: "object", properties: { agents: { type: "array", items: { type: "string" }, description: "List of agent names to install." } }, required: ["agents"], additionalProperties: false } },
899
- { name: "fifony.install_skills", description: "Install skills from the Fifony catalog into the target workspace.", inputSchema: { type: "object", properties: { skills: { type: "array", items: { type: "string" }, description: "List of skill names to install." } }, required: ["skills"], additionalProperties: false } }
900
- ];
901
- }
902
- function toolText(text) {
903
- return { content: [{ type: "text", text }] };
904
- }
905
- async function resolveApiBaseUrl() {
906
- const envPort = env2.FIFONY_API_PORT;
907
- if (envPort) return `http://localhost:${envPort}`;
908
- const runtime = await getRuntimeSnapshot();
909
- const config = runtime.config;
910
- const port = config?.dashboardPort;
911
- if (port) return `http://localhost:${port}`;
912
- for (const candidate of [4e3, 3e3, 8080]) {
913
- try {
914
- const res = await fetch(`http://localhost:${candidate}/health`, { signal: AbortSignal.timeout(1e3) });
915
- if (res.ok) return `http://localhost:${candidate}`;
916
- } catch {
917
- }
918
- }
919
- throw new Error("Fifony runtime API is not reachable. Start the runtime with --port to enable plan/refine/approve/analytics tools.");
920
- }
921
- async function apiPost(path, body = {}) {
922
- const base = await resolveApiBaseUrl();
923
- const res = await fetch(`${base}${path}`, {
924
- method: "POST",
925
- headers: { "content-type": "application/json" },
926
- body: JSON.stringify(body),
927
- signal: AbortSignal.timeout(12e4)
928
- });
929
- const json = await res.json();
930
- if (!res.ok || json.ok === false) {
931
- throw new Error(typeof json.error === "string" ? json.error : `API request failed: ${res.status}`);
932
- }
933
- return json;
934
- }
935
- async function apiGet(path) {
936
- const base = await resolveApiBaseUrl();
937
- const res = await fetch(`${base}${path}`, {
938
- signal: AbortSignal.timeout(3e4)
939
- });
940
- const json = await res.json();
941
- if (!res.ok || json.ok === false) {
942
- throw new Error(typeof json.error === "string" ? json.error : `API request failed: ${res.status}`);
943
- }
944
- return json;
945
- }
946
- async function callTool(name, args = {}) {
947
- if (name === "fifony.status") return toolText(await buildStateSummary());
948
- if (name === "fifony.list_issues") {
949
- const stateFilter = typeof args.state === "string" && args.state.trim() ? args.state.trim() : "";
950
- const capabilityCategory = typeof args.capabilityCategory === "string" && args.capabilityCategory.trim() ? args.capabilityCategory.trim() : typeof args.category === "string" && args.category.trim() ? args.category.trim() : "";
951
- return toolText(JSON.stringify(await listIssues({ state: stateFilter || void 0, capabilityCategory: capabilityCategory || void 0 }), null, 2));
607
+
608
+ // src/mcp/tools/tool-executor.ts
609
+ import { createHash } from "crypto";
610
+ function hashInput(value) {
611
+ return createHash("sha1").update(value).digest("hex").slice(0, 10);
612
+ }
613
+ function toolText(text) {
614
+ return { content: [{ type: "text", text }] };
615
+ }
616
+ async function callTool(name, args = {}) {
617
+ if (name === "fifony.status") return toolText(await buildStateSummary());
618
+ if (name === "fifony.list_issues") {
619
+ const stateFilter = typeof args.state === "string" && args.state.trim() ? args.state.trim() : "";
620
+ const capabilityCategory = typeof args.capabilityCategory === "string" && args.capabilityCategory.trim() ? args.capabilityCategory.trim() : typeof args.category === "string" && args.category.trim() ? args.category.trim() : "";
621
+ return toolText(JSON.stringify(await listIssues({ state: stateFilter || void 0, capabilityCategory: capabilityCategory || void 0 }), null, 2));
952
622
  }
953
623
  if (name === "fifony.create_issue") {
954
624
  await initDatabase();
@@ -959,7 +629,7 @@ async function callTool(name, args = {}) {
959
629
  const issueId = explicitId || `LOCAL-${hashInput(`${title}:${nowIso()}`)}`.toUpperCase();
960
630
  const description = typeof args.description === "string" ? args.description : "";
961
631
  const priority = typeof args.priority === "number" ? args.priority : 2;
962
- const state = typeof args.state === "string" && args.state.trim() ? args.state.trim() : "Todo";
632
+ const state = parseIssueState(args.state) ?? "Planning";
963
633
  const baseLabels = Array.isArray(args.labels) ? args.labels.filter((value) => typeof value === "string") : ["fifony", "mcp"];
964
634
  const paths = Array.isArray(args.paths) ? args.paths.filter((value) => typeof value === "string") : [];
965
635
  const inferredPaths = inferCapabilityPaths({ id: issueId, identifier: issueId, title, description, labels: baseLabels, paths });
@@ -994,9 +664,9 @@ async function callTool(name, args = {}) {
994
664
  await initDatabase();
995
665
  const { issueResource: issueResource2 } = getResources();
996
666
  const issueId = typeof args.issueId === "string" ? args.issueId.trim() : "";
997
- const state = typeof args.state === "string" ? args.state.trim() : "";
667
+ const state = parseIssueState(args.state);
998
668
  const note = typeof args.note === "string" ? args.note : "";
999
- if (!issueId || !state) throw new Error("issueId and state are required");
669
+ if (!issueId || !state) throw new Error("issueId and a valid canonical state are required");
1000
670
  const current = await issueResource2?.get(issueId);
1001
671
  if (!current) throw new Error(`Issue not found: ${issueId}`);
1002
672
  const updated = await issueResource2?.update(issueId, { state, updatedAt: nowIso() });
@@ -1044,8 +714,8 @@ async function callTool(name, args = {}) {
1044
714
  const issue = result.issue;
1045
715
  return toolText(JSON.stringify({
1046
716
  issueId,
1047
- state: issue?.state ?? "Todo",
1048
- message: `Plan approved for ${issueId}. Issue moved to Todo and is ready for execution.`
717
+ state: issue?.state ?? "Planned",
718
+ message: `Plan approved for ${issueId}. Issue moved to Planned and is ready for execution.`
1049
719
  }, null, 2));
1050
720
  }
1051
721
  if (name === "fifony.merge") {
@@ -1073,12 +743,7 @@ async function callTool(name, args = {}) {
1073
743
  overall: { inputTokens, outputTokens, totalTokens },
1074
744
  estimatedCostUsd: Math.round(estimatedCost * 100) / 100,
1075
745
  modelBreakdown: byModel ?? {},
1076
- topIssues: (topIssues ?? []).slice(0, 10).map((issue) => ({
1077
- id: issue.id,
1078
- totalTokens: issue.totalTokens,
1079
- inputTokens: issue.inputTokens,
1080
- outputTokens: issue.outputTokens
1081
- }))
746
+ topIssues: (topIssues ?? []).slice(0, 10).map((issue) => ({ id: issue.id, totalTokens: issue.totalTokens, inputTokens: issue.inputTokens, outputTokens: issue.outputTokens }))
1082
747
  }, null, 2));
1083
748
  }
1084
749
  if (name === "fifony.integration_config") {
@@ -1139,7 +804,7 @@ async function callTool(name, args = {}) {
1139
804
  try {
1140
805
  const result = await apiPost(`/api/issues/${encodeURIComponent(issueId)}/retry`);
1141
806
  const issue = result.issue;
1142
- return toolText(JSON.stringify({ issueId, state: issue?.state ?? "Todo", message: `Issue ${issueId} has been retried and reset to Todo.` }, null, 2));
807
+ return toolText(JSON.stringify({ issueId, state: issue?.state ?? "Planned", message: `Issue ${issueId} has been retried and reset to Planned.` }, null, 2));
1143
808
  } catch (error) {
1144
809
  throw new Error(`Failed to retry issue ${issueId}: ${String(error)}`);
1145
810
  }
@@ -1163,97 +828,411 @@ async function callTool(name, args = {}) {
1163
828
  throw new Error(`Failed to enhance ${field}: ${String(error)}`);
1164
829
  }
1165
830
  }
1166
- if (name === "fifony.get_diff") {
1167
- const issueId = typeof args.issueId === "string" ? args.issueId.trim() : "";
1168
- if (!issueId) throw new Error("issueId is required");
831
+ if (name === "fifony.get_diff") {
832
+ const issueId = typeof args.issueId === "string" ? args.issueId.trim() : "";
833
+ if (!issueId) throw new Error("issueId is required");
834
+ try {
835
+ const result = await apiGet(`/api/diff/${encodeURIComponent(issueId)}`);
836
+ return toolText(JSON.stringify(result, null, 2));
837
+ } catch (error) {
838
+ throw new Error(`Failed to get diff for ${issueId}: ${String(error)}`);
839
+ }
840
+ }
841
+ if (name === "fifony.get_live") {
842
+ const issueId = typeof args.issueId === "string" ? args.issueId.trim() : "";
843
+ if (!issueId) throw new Error("issueId is required");
844
+ try {
845
+ const result = await apiGet(`/api/live/${encodeURIComponent(issueId)}`);
846
+ return toolText(JSON.stringify(result, null, 2));
847
+ } catch (error) {
848
+ throw new Error(`Failed to get live output for ${issueId}: ${String(error)}`);
849
+ }
850
+ }
851
+ if (name === "fifony.get_events") {
852
+ const issueId = typeof args.issueId === "string" ? args.issueId.trim() : "";
853
+ const kind = typeof args.kind === "string" ? args.kind.trim() : "";
854
+ const limit = typeof args.limit === "number" ? args.limit : 50;
855
+ const params = new URLSearchParams();
856
+ if (issueId) params.set("issueId", issueId);
857
+ if (kind) params.set("kind", kind);
858
+ const query = params.toString();
859
+ try {
860
+ const result = await apiGet(`/api/events/feed${query ? `?${query}` : ""}`);
861
+ const events = Array.isArray(result.events) ? result.events.slice(0, limit) : [];
862
+ return toolText(JSON.stringify({ events, count: events.length }, null, 2));
863
+ } catch (error) {
864
+ const events = await listEvents({ limit });
865
+ const filtered = events.filter((event) => {
866
+ if (issueId && event.issueId !== issueId) return false;
867
+ if (kind && event.kind !== kind) return false;
868
+ return true;
869
+ }).slice(0, limit);
870
+ return toolText(JSON.stringify({ events: filtered, count: filtered.length }, null, 2));
871
+ }
872
+ }
873
+ if (name === "fifony.get_workflow") {
874
+ try {
875
+ const result = await apiGet("/api/config/workflow");
876
+ return toolText(JSON.stringify(result, null, 2));
877
+ } catch (error) {
878
+ throw new Error(`Failed to get workflow config: ${String(error)}`);
879
+ }
880
+ }
881
+ if (name === "fifony.set_workflow") {
882
+ const plan = args.plan;
883
+ const execute = args.execute;
884
+ const review = args.review;
885
+ if (!plan || !execute || !review) throw new Error("plan, execute, and review are all required");
886
+ try {
887
+ const result = await apiPost("/api/config/workflow", { workflow: { plan, execute, review } });
888
+ return toolText(JSON.stringify({ message: "Workflow configuration updated successfully.", workflow: result.workflow }, null, 2));
889
+ } catch (error) {
890
+ throw new Error(`Failed to set workflow config: ${String(error)}`);
891
+ }
892
+ }
893
+ if (name === "fifony.scan_project") {
894
+ try {
895
+ const result = await apiGet("/api/scan/project");
896
+ return toolText(JSON.stringify(result, null, 2));
897
+ } catch (error) {
898
+ throw new Error(`Failed to scan project: ${String(error)}`);
899
+ }
900
+ }
901
+ if (name === "fifony.install_agents") {
902
+ const agents = Array.isArray(args.agents) ? args.agents.filter((value) => typeof value === "string") : [];
903
+ if (agents.length === 0) throw new Error("At least one agent name is required");
904
+ try {
905
+ const result = await apiPost("/api/install/agents", { agents });
906
+ return toolText(JSON.stringify(result, null, 2));
907
+ } catch (error) {
908
+ throw new Error(`Failed to install agents: ${String(error)}`);
909
+ }
910
+ }
911
+ if (name === "fifony.install_skills") {
912
+ const skills = Array.isArray(args.skills) ? args.skills.filter((value) => typeof value === "string") : [];
913
+ if (skills.length === 0) throw new Error("At least one skill name is required");
914
+ try {
915
+ const result = await apiPost("/api/install/skills", { skills });
916
+ return toolText(JSON.stringify(result, null, 2));
917
+ } catch (error) {
918
+ throw new Error(`Failed to install skills: ${String(error)}`);
919
+ }
920
+ }
921
+ throw new Error(`Unknown tool: ${name}`);
922
+ }
923
+
924
+ // src/mcp/prompts/prompt-list.ts
925
+ function listPrompts() {
926
+ return [
927
+ { name: "fifony-integrate-client", description: "Generate setup instructions for connecting an MCP-capable client to Fifony.", arguments: [{ name: "client", description: "Client name, e.g. codex or claude.", required: true }, { name: "goal", description: "What the client should do with Fifony.", required: false }] },
928
+ { name: "fifony-plan-issue", description: "Generate a planning prompt for a specific issue in the Fifony store.", arguments: [{ name: "issueId", description: "Issue identifier.", required: true }, { name: "provider", description: "Agent provider name.", required: false }] },
929
+ { name: "fifony-use-integration", description: "Generate a concrete integration prompt for agency-agents or impeccable.", arguments: [{ name: "integration", description: "Integration id: agency-agents or impeccable.", required: true }] },
930
+ { name: "fifony-route-task", description: "Explain which providers, profiles, and overlays Fifony would choose for a task.", arguments: [{ name: "title", description: "Task title.", required: true }, { name: "description", description: "Task description.", required: false }, { name: "labels", description: "Comma-separated labels.", required: false }, { name: "paths", description: "Comma-separated target paths or files.", required: false }] },
931
+ { name: "fifony-diagnose-blocked", description: "Help diagnose why an issue is blocked or failing, analyzing the issue plan, last error, history, and events.", arguments: [{ name: "issueId", description: "Issue identifier to diagnose.", required: true }] },
932
+ { name: "fifony-weekly-summary", description: "Generate a weekly progress summary including issues created, completed, blocked, and token usage.", arguments: [] },
933
+ { name: "fifony-refine-plan", description: "Guided plan refinement prompt that shows the current plan and helps provide specific feedback.", arguments: [{ name: "issueId", description: "Issue identifier whose plan to refine.", required: true }, { name: "concern", description: "Optional specific concern to address in refinement.", required: false }] },
934
+ { name: "fifony-code-review", description: "Review code changes for an issue by analyzing its git diff.", arguments: [{ name: "issueId", description: "Issue identifier to review.", required: true }] }
935
+ ];
936
+ }
937
+
938
+ // src/mcp/prompts/prompt-handler.ts
939
+ async function getPrompt(name, args = {}) {
940
+ if (name === "fifony-integrate-client") {
941
+ const client = typeof args.client === "string" && args.client.trim() ? args.client.trim() : "mcp-client";
942
+ const goal = typeof args.goal === "string" && args.goal.trim() ? args.goal.trim() : "integrate with the local Fifony workspace";
943
+ const integrationGuide = await buildIntegrationGuide();
944
+ return {
945
+ description: "Client integration prompt for Fifony.",
946
+ messages: [{
947
+ role: "user",
948
+ content: {
949
+ type: "text",
950
+ text: await renderPrompt("mcp-integrate-client", { client, goal, integrationGuide })
951
+ }
952
+ }]
953
+ };
954
+ }
955
+ if (name === "fifony-plan-issue") {
956
+ const issueId = typeof args.issueId === "string" ? args.issueId : "";
957
+ const provider = typeof args.provider === "string" && args.provider.trim() ? args.provider.trim() : "codex";
958
+ const issue = issueId ? await getIssue(issueId) : null;
959
+ if (!issue) throw new Error(`Issue not found: ${issueId}`);
960
+ return {
961
+ description: "Issue planning prompt grounded in the Fifony issue store.",
962
+ messages: [{
963
+ role: "user",
964
+ content: { type: "text", text: await buildIssuePrompt(issue, provider, "planner") }
965
+ }]
966
+ };
967
+ }
968
+ if (name === "fifony-use-integration") {
969
+ const integration = typeof args.integration === "string" ? args.integration : "";
970
+ return {
971
+ description: "Integration guidance for a discovered Fifony extension.",
972
+ messages: [{
973
+ role: "user",
974
+ content: { type: "text", text: await buildIntegrationSnippet(integration, WORKSPACE_ROOT) }
975
+ }]
976
+ };
977
+ }
978
+ if (name === "fifony-route-task") {
979
+ const title = typeof args.title === "string" ? args.title : "";
980
+ const description = typeof args.description === "string" ? args.description : "";
981
+ const labels = typeof args.labels === "string" ? args.labels.split(",").map((label) => label.trim()).filter(Boolean) : [];
982
+ const paths = typeof args.paths === "string" ? args.paths.split(",").map((value) => value.trim()).filter(Boolean) : [];
983
+ const resolution = resolveTaskCapabilities({ title, description, labels, paths });
984
+ return {
985
+ description: "Task routing prompt produced by the Fifony capability resolver.",
986
+ messages: [{
987
+ role: "user",
988
+ content: {
989
+ type: "text",
990
+ text: await renderPrompt("mcp-route-task", {
991
+ resolutionJson: JSON.stringify(resolution, null, 2)
992
+ })
993
+ }
994
+ }]
995
+ };
996
+ }
997
+ if (name === "fifony-diagnose-blocked") {
998
+ const issueId = typeof args.issueId === "string" ? args.issueId.trim() : "";
999
+ if (!issueId) throw new Error("issueId is required");
1000
+ const issue = await getIssue(issueId);
1001
+ if (!issue) throw new Error(`Issue not found: ${issueId}`);
1002
+ const issueData = issue;
1003
+ let events = [];
1004
+ try {
1005
+ const evResult = await apiGet(`/api/events/feed?issueId=${encodeURIComponent(issueId)}`);
1006
+ events = Array.isArray(evResult.events) ? evResult.events.slice(0, 30) : [];
1007
+ } catch {
1008
+ const localEvents = await listEvents({ limit: 100 });
1009
+ events = localEvents.filter((event) => event.issueId === issueId).slice(0, 30);
1010
+ }
1011
+ const plan = issueData.plan ?? null;
1012
+ const history = Array.isArray(issueData.history) ? issueData.history : [];
1013
+ const lastError = issueData.lastError ?? null;
1014
+ const state = issueData.state ?? "Unknown";
1015
+ const attempts = issueData.attempts ?? 0;
1016
+ const maxAttempts = issueData.maxAttempts ?? 3;
1017
+ const diagnosticText = [
1018
+ `# Diagnostic Report for Issue ${issueId}`,
1019
+ ``,
1020
+ `## Issue Details`,
1021
+ `- **Title**: ${issueData.title ?? "Unknown"}`,
1022
+ `- **State**: ${state}`,
1023
+ `- **Attempts**: ${attempts} / ${maxAttempts}`,
1024
+ `- **Last Error**: ${lastError ?? "None"}`,
1025
+ `- **Updated At**: ${issueData.updatedAt ?? "Unknown"}`,
1026
+ ``,
1027
+ `## Plan`,
1028
+ plan ? `- **Summary**: ${plan.summary ?? plan.title ?? "No summary"}` : "No plan generated.",
1029
+ plan?.steps ? `- **Steps**: ${plan.steps.length} step(s)` : "",
1030
+ plan?.estimatedComplexity ? `- **Estimated Complexity**: ${plan.estimatedComplexity}` : "",
1031
+ ``,
1032
+ `## History`,
1033
+ ...history.length > 0 ? history.slice(-15).map((entry) => `- ${entry}`) : ["No history entries."],
1034
+ ``,
1035
+ `## Recent Events`,
1036
+ ...events.length > 0 ? events.slice(0, 15).map((event) => `- [${event.kind ?? "info"}] ${event.at ?? ""}: ${event.message ?? ""}`) : ["No events found."],
1037
+ ``,
1038
+ `## Diagnostic Questions`,
1039
+ `Based on the information above, please analyze:`,
1040
+ `1. What is the root cause of the issue being in "${state}" state?`,
1041
+ `2. Is the error recoverable? If so, what steps should be taken?`,
1042
+ `3. Does the plan need modification before retrying?`,
1043
+ `4. Are there any dependency or configuration issues that need resolution?`,
1044
+ `5. What is the recommended next action?`
1045
+ ].filter((line) => line !== void 0).join("\n");
1046
+ return {
1047
+ description: `Diagnostic prompt for blocked/failed issue ${issueId}.`,
1048
+ messages: [{
1049
+ role: "user",
1050
+ content: { type: "text", text: diagnosticText }
1051
+ }]
1052
+ };
1053
+ }
1054
+ if (name === "fifony-weekly-summary") {
1055
+ const issues = await getIssues();
1056
+ let analytics = {};
1169
1057
  try {
1170
- const result = await apiGet(`/api/diff/${encodeURIComponent(issueId)}`);
1171
- return toolText(JSON.stringify(result, null, 2));
1172
- } catch (error) {
1173
- throw new Error(`Failed to get diff for ${issueId}: ${String(error)}`);
1058
+ analytics = await apiGet("/api/analytics/tokens");
1059
+ } catch {
1174
1060
  }
1061
+ const overall = analytics.overall ?? {};
1062
+ const byState = issues.reduce((accumulator, issue) => {
1063
+ const key = issue.state ?? "Unknown";
1064
+ accumulator[key] = (accumulator[key] ?? 0) + 1;
1065
+ return accumulator;
1066
+ }, {});
1067
+ const totalIssues = issues.length;
1068
+ const completed = byState["Done"] ?? 0;
1069
+ const blocked = (byState["Blocked"] ?? 0) + (byState["Failed"] ?? 0);
1070
+ const inProgress = (byState["Running"] ?? 0) + (byState["Reviewing"] ?? 0) + (byState["Reviewed"] ?? 0) + (byState["Queued"] ?? 0);
1071
+ const planned = byState["Planned"] ?? 0;
1072
+ const planning = byState["Planning"] ?? 0;
1073
+ const cancelled = byState["Cancelled"] ?? 0;
1074
+ const inputTokens = typeof overall.inputTokens === "number" ? overall.inputTokens : 0;
1075
+ const outputTokens = typeof overall.outputTokens === "number" ? overall.outputTokens : 0;
1076
+ const totalTokens = typeof overall.totalTokens === "number" ? overall.totalTokens : 0;
1077
+ const estimatedCost = inputTokens / 1e6 * 3 + outputTokens / 1e6 * 15;
1078
+ const summaryText = [
1079
+ `# Fifony Weekly Progress Summary`,
1080
+ ``,
1081
+ `## Issue Statistics`,
1082
+ `| Status | Count |`,
1083
+ `|--------|-------|`,
1084
+ `| Total Issues | ${totalIssues} |`,
1085
+ `| Completed (Done) | ${completed} |`,
1086
+ `| In Progress | ${inProgress} |`,
1087
+ `| Planned | ${planned} |`,
1088
+ `| Planning | ${planning} |`,
1089
+ `| Blocked/Failed | ${blocked} |`,
1090
+ `| Cancelled | ${cancelled} |`,
1091
+ ``,
1092
+ `## Token Usage`,
1093
+ `- **Total Tokens**: ${totalTokens.toLocaleString()}`,
1094
+ `- **Input Tokens**: ${inputTokens.toLocaleString()}`,
1095
+ `- **Output Tokens**: ${outputTokens.toLocaleString()}`,
1096
+ `- **Estimated Cost**: $${(Math.round(estimatedCost * 100) / 100).toFixed(2)}`,
1097
+ ``,
1098
+ `## Analysis Request`,
1099
+ `Based on these metrics, please provide:`,
1100
+ `1. A brief summary of overall progress this week`,
1101
+ `2. Identification of any bottlenecks (blocked/failed issues)`,
1102
+ `3. Token usage efficiency assessment`,
1103
+ `4. Recommendations for improving throughput`,
1104
+ `5. Priority items for next week`
1105
+ ].join("\n");
1106
+ return {
1107
+ description: "Weekly progress summary prompt for the Fifony workspace.",
1108
+ messages: [{
1109
+ role: "user",
1110
+ content: { type: "text", text: summaryText }
1111
+ }]
1112
+ };
1175
1113
  }
1176
- if (name === "fifony.get_live") {
1114
+ if (name === "fifony-refine-plan") {
1177
1115
  const issueId = typeof args.issueId === "string" ? args.issueId.trim() : "";
1116
+ const concern = typeof args.concern === "string" ? args.concern.trim() : "";
1178
1117
  if (!issueId) throw new Error("issueId is required");
1179
- try {
1180
- const result = await apiGet(`/api/live/${encodeURIComponent(issueId)}`);
1181
- return toolText(JSON.stringify(result, null, 2));
1182
- } catch (error) {
1183
- throw new Error(`Failed to get live output for ${issueId}: ${String(error)}`);
1184
- }
1118
+ const issue = await getIssue(issueId);
1119
+ if (!issue) throw new Error(`Issue not found: ${issueId}`);
1120
+ const issueData = issue;
1121
+ const plan = issueData.plan ?? null;
1122
+ const steps = plan?.steps ?? [];
1123
+ const stepsText = steps.length > 0 ? steps.map((step, index) => `${index + 1}. **${step.title ?? step.description ?? "Step"}**
1124
+ ${step.description ?? step.detail ?? ""}`).join("\n") : "No steps defined.";
1125
+ const refinementText = [
1126
+ `# Plan Refinement for Issue ${issueId}`,
1127
+ ``,
1128
+ `## Issue`,
1129
+ `- **Title**: ${issueData.title ?? "Unknown"}`,
1130
+ `- **Description**: ${issueData.description ?? "No description"}`,
1131
+ ``,
1132
+ `## Current Plan`,
1133
+ plan ? `- **Summary**: ${plan.summary ?? plan.title ?? "No summary"}` : "No plan exists yet.",
1134
+ plan?.estimatedComplexity ? `- **Complexity**: ${plan.estimatedComplexity}` : "",
1135
+ ``,
1136
+ `### Steps`,
1137
+ stepsText,
1138
+ ``,
1139
+ concern ? `## Specific Concern
1140
+ ${concern}
1141
+ ` : "",
1142
+ `## Refinement Guidance`,
1143
+ `Please review the current plan and provide specific, actionable feedback:`,
1144
+ `1. Are the steps correctly ordered and complete?`,
1145
+ `2. Are there missing edge cases or error handling steps?`,
1146
+ `3. Is the complexity estimate accurate?`,
1147
+ `4. Are the file paths and affected areas correct?`,
1148
+ `5. Should any steps be split, merged, or removed?`,
1149
+ ``,
1150
+ `Provide your feedback, and it will be used to refine the plan via \`fifony.refine\`.`
1151
+ ].filter((line) => line !== void 0).join("\n");
1152
+ return {
1153
+ description: `Plan refinement prompt for issue ${issueId}.`,
1154
+ messages: [{
1155
+ role: "user",
1156
+ content: { type: "text", text: refinementText }
1157
+ }]
1158
+ };
1185
1159
  }
1186
- if (name === "fifony.get_events") {
1160
+ if (name === "fifony-code-review") {
1187
1161
  const issueId = typeof args.issueId === "string" ? args.issueId.trim() : "";
1188
- const kind = typeof args.kind === "string" ? args.kind.trim() : "";
1189
- const limit = typeof args.limit === "number" ? args.limit : 50;
1190
- const params = new URLSearchParams();
1191
- if (issueId) params.set("issueId", issueId);
1192
- if (kind) params.set("kind", kind);
1193
- const query = params.toString();
1194
- try {
1195
- const result = await apiGet(`/api/events/feed${query ? `?${query}` : ""}`);
1196
- const events = Array.isArray(result.events) ? result.events.slice(0, limit) : [];
1197
- return toolText(JSON.stringify({ events, count: events.length }, null, 2));
1198
- } catch (error) {
1199
- const events = await listEvents({ limit });
1200
- const filtered = events.filter((event) => {
1201
- if (issueId && event.issueId !== issueId) return false;
1202
- if (kind && event.kind !== kind) return false;
1203
- return true;
1204
- }).slice(0, limit);
1205
- return toolText(JSON.stringify({ events: filtered, count: filtered.length }, null, 2));
1206
- }
1207
- }
1208
- if (name === "fifony.get_workflow") {
1209
- try {
1210
- const result = await apiGet("/api/config/workflow");
1211
- return toolText(JSON.stringify(result, null, 2));
1212
- } catch (error) {
1213
- throw new Error(`Failed to get workflow config: ${String(error)}`);
1214
- }
1215
- }
1216
- if (name === "fifony.set_workflow") {
1217
- const plan = args.plan;
1218
- const execute = args.execute;
1219
- const review = args.review;
1220
- if (!plan || !execute || !review) throw new Error("plan, execute, and review are all required");
1221
- try {
1222
- const result = await apiPost("/api/config/workflow", { workflow: { plan, execute, review } });
1223
- return toolText(JSON.stringify({ message: "Workflow configuration updated successfully.", workflow: result.workflow }, null, 2));
1224
- } catch (error) {
1225
- throw new Error(`Failed to set workflow config: ${String(error)}`);
1226
- }
1227
- }
1228
- if (name === "fifony.scan_project") {
1229
- try {
1230
- const result = await apiGet("/api/scan/project");
1231
- return toolText(JSON.stringify(result, null, 2));
1232
- } catch (error) {
1233
- throw new Error(`Failed to scan project: ${String(error)}`);
1234
- }
1235
- }
1236
- if (name === "fifony.install_agents") {
1237
- const agents = Array.isArray(args.agents) ? args.agents.filter((value) => typeof value === "string") : [];
1238
- if (agents.length === 0) throw new Error("At least one agent name is required");
1162
+ if (!issueId) throw new Error("issueId is required");
1163
+ const issue = await getIssue(issueId);
1164
+ if (!issue) throw new Error(`Issue not found: ${issueId}`);
1165
+ const issueData = issue;
1166
+ let diffData = {};
1239
1167
  try {
1240
- const result = await apiPost("/api/install/agents", { agents });
1241
- return toolText(JSON.stringify(result, null, 2));
1168
+ diffData = await apiGet(`/api/diff/${encodeURIComponent(issueId)}`);
1242
1169
  } catch (error) {
1243
- throw new Error(`Failed to install agents: ${String(error)}`);
1170
+ throw new Error(`Cannot fetch diff for issue ${issueId}. Is the runtime running? ${String(error)}`);
1244
1171
  }
1245
- }
1246
- if (name === "fifony.install_skills") {
1247
- const skills = Array.isArray(args.skills) ? args.skills.filter((value) => typeof value === "string") : [];
1248
- if (skills.length === 0) throw new Error("At least one skill name is required");
1249
- try {
1250
- const result = await apiPost("/api/install/skills", { skills });
1251
- return toolText(JSON.stringify(result, null, 2));
1252
- } catch (error) {
1253
- throw new Error(`Failed to install skills: ${String(error)}`);
1172
+ const files = Array.isArray(diffData.files) ? diffData.files : [];
1173
+ const diff = typeof diffData.diff === "string" ? diffData.diff : "";
1174
+ const totalAdditions = typeof diffData.totalAdditions === "number" ? diffData.totalAdditions : 0;
1175
+ const totalDeletions = typeof diffData.totalDeletions === "number" ? diffData.totalDeletions : 0;
1176
+ if (!diff.trim()) {
1177
+ return {
1178
+ description: `Code review prompt for issue ${issueId} (no changes).`,
1179
+ messages: [{
1180
+ role: "user",
1181
+ content: { type: "text", text: `# Code Review for ${issueId}
1182
+
1183
+ No code changes found for this issue. The workspace may not have been created yet or no modifications were made.` }
1184
+ }]
1185
+ };
1254
1186
  }
1187
+ const filesTable = files.map((file) => `| ${file.path} | ${file.status} | +${file.additions} | -${file.deletions} |`).join("\n");
1188
+ const reviewText = [
1189
+ `# Code Review for Issue ${issueId}`,
1190
+ ``,
1191
+ `## Issue Context`,
1192
+ `- **Title**: ${issueData.title ?? "Unknown"}`,
1193
+ `- **Description**: ${issueData.description ?? "No description"}`,
1194
+ `- **State**: ${issueData.state ?? "Unknown"}`,
1195
+ ``,
1196
+ `## Change Summary`,
1197
+ `- **Files Changed**: ${files.length}`,
1198
+ `- **Total Additions**: +${totalAdditions}`,
1199
+ `- **Total Deletions**: -${totalDeletions}`,
1200
+ ``,
1201
+ `### Files`,
1202
+ `| Path | Status | Additions | Deletions |`,
1203
+ `|------|--------|-----------|-----------|`,
1204
+ filesTable,
1205
+ ``,
1206
+ `## Diff`,
1207
+ "```diff",
1208
+ diff.length > 5e4 ? diff.substring(0, 5e4) + "\n... (diff truncated at 50KB)" : diff,
1209
+ "```",
1210
+ ``,
1211
+ `## Review Checklist`,
1212
+ `Please review the changes and evaluate:`,
1213
+ `1. **Correctness**: Do the changes correctly implement what the issue describes?`,
1214
+ `2. **Code Quality**: Is the code clean, readable, and follows project conventions?`,
1215
+ `3. **Error Handling**: Are edge cases and errors properly handled?`,
1216
+ `4. **Security**: Are there any security concerns (hardcoded secrets, SQL injection, XSS)?`,
1217
+ `5. **Performance**: Are there any performance concerns or inefficiencies?`,
1218
+ `6. **Tests**: Are changes adequately covered by tests?`,
1219
+ `7. **Breaking Changes**: Do any changes break backward compatibility?`
1220
+ ].join("\n");
1221
+ return {
1222
+ description: `Code review prompt for issue ${issueId}.`,
1223
+ messages: [{
1224
+ role: "user",
1225
+ content: { type: "text", text: reviewText }
1226
+ }]
1227
+ };
1255
1228
  }
1256
- throw new Error(`Unknown tool: ${name}`);
1229
+ throw new Error(`Unknown prompt: ${name}`);
1230
+ }
1231
+
1232
+ // src/mcp/jsonrpc-transport.ts
1233
+ var incomingBuffer = Buffer.alloc(0);
1234
+ function setIncomingBuffer(buffer) {
1235
+ incomingBuffer = buffer;
1257
1236
  }
1258
1237
  function sendMessage(message) {
1259
1238
  const payload = Buffer.from(JSON.stringify(message), "utf8");
@@ -1331,13 +1310,21 @@ function processIncomingBuffer() {
1331
1310
  void handleRequest(request);
1332
1311
  }
1333
1312
  }
1313
+
1314
+ // src/mcp/server.ts
1315
+ var DEBUG_BOOT2 = env3.FIFONY_DEBUG_BOOT === "1";
1316
+ function debugBoot2(message) {
1317
+ if (!DEBUG_BOOT2) return;
1318
+ process.stderr.write(`[FIFONY_DEBUG_BOOT] ${message}
1319
+ `);
1320
+ }
1334
1321
  async function bootstrap() {
1335
1322
  debugBoot2("mcp:bootstrap:start");
1336
1323
  await initDatabase();
1337
1324
  debugBoot2("mcp:bootstrap:database-ready");
1338
1325
  await appendEvent("info", "Fifony MCP server started.", { workspaceRoot: WORKSPACE_ROOT, persistenceRoot: PERSISTENCE_ROOT });
1339
1326
  stdin.on("data", (chunk) => {
1340
- incomingBuffer = Buffer.concat([incomingBuffer, chunk]);
1327
+ setIncomingBuffer(Buffer.concat([incomingBuffer, chunk]));
1341
1328
  processIncomingBuffer();
1342
1329
  });
1343
1330
  stdin.resume();