fifony 0.1.21 → 0.1.22

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 (37) hide show
  1. package/README.md +7 -0
  2. package/app/dist/assets/{KeyboardShortcutsHelp-BTjiQe_Y.js → KeyboardShortcutsHelp-DFstgyXD.js} +1 -1
  3. package/app/dist/assets/OnboardingWizard-Daehu2Uj.js +1 -0
  4. package/app/dist/assets/analytics.lazy-C1-iSRM_.js +1 -0
  5. package/app/dist/assets/{createLucideIcon-DtZs0TX0.js → createLucideIcon-BWC-guQt.js} +1 -1
  6. package/app/dist/assets/index-DbIrs0MK.css +1 -0
  7. package/app/dist/assets/index-O_FDwkw6.js +43 -0
  8. package/app/dist/assets/vendor-BTlTWMUF.js +9 -0
  9. package/app/dist/index.html +4 -5
  10. package/app/dist/service-worker.js +1 -1
  11. package/bin/fifony-wrap.js +53 -0
  12. package/dist/agent/cli-wrapper.js +78 -0
  13. package/dist/agent/cli-wrapper.js.map +1 -0
  14. package/dist/agent/run-local.js +93 -7889
  15. package/dist/agent/run-local.js.map +1 -1
  16. package/dist/chunk-F6JEQIP2.js +449 -0
  17. package/dist/chunk-F6JEQIP2.js.map +1 -0
  18. package/dist/{chunk-SMGXYOWU.js → chunk-O665NS5E.js} +411 -9
  19. package/dist/chunk-O665NS5E.js.map +1 -0
  20. package/dist/chunk-VP6TGOMT.js +8915 -0
  21. package/dist/chunk-VP6TGOMT.js.map +1 -0
  22. package/dist/cli.js +182 -0
  23. package/dist/cli.js.map +1 -1
  24. package/dist/issue-runner-MDCJ4G26.js +11 -0
  25. package/dist/issue-runner-MDCJ4G26.js.map +1 -0
  26. package/dist/mcp/server.js +589 -595
  27. package/dist/mcp/server.js.map +1 -1
  28. package/dist/queue-workers-LAYOT4E5.js +21 -0
  29. package/dist/queue-workers-LAYOT4E5.js.map +1 -0
  30. package/package.json +10 -9
  31. package/app/dist/assets/OnboardingWizard-BALlquG0.js +0 -1
  32. package/app/dist/assets/analytics.lazy-DjSzXIey.js +0 -1
  33. package/app/dist/assets/index-BV11ScVl.js +0 -42
  34. package/app/dist/assets/index-DWbxgKSd.css +0 -1
  35. package/app/dist/assets/vendor-BoGBoEwT.js +0 -9
  36. package/app/dist/assets/zap-DpjdVd1i.js +0 -1
  37. package/dist/chunk-SMGXYOWU.js.map +0 -1
@@ -1,105 +1,29 @@
1
1
  import {
2
2
  inferCapabilityPaths,
3
+ parseIssueState,
3
4
  renderPrompt,
4
5
  resolveTaskCapabilities
5
- } from "../chunk-SMGXYOWU.js";
6
+ } from "../chunk-O665NS5E.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");
20
+ var DATABASE_PATH = join(STATE_ROOT, "s3db");
97
21
  var STORAGE_BUCKET = env.FIFONY_STORAGE_BUCKET ?? "fifony";
98
22
  var STORAGE_KEY_PREFIX = env.FIFONY_STORAGE_KEY_PREFIX ?? "state";
99
23
  var DEBUG_BOOT = env.FIFONY_DEBUG_BOOT === "1";
100
24
  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");
25
+ const resolved = value.startsWith("file://") ? fileURLToPath(value) : value.startsWith("~/") ? resolve(homedir(), value.slice(2)) : resolve(value);
26
+ return basename(resolved) === ".fifony" ? resolved : join(resolved, ".fifony");
103
27
  }
104
28
  var RUNTIME_RESOURCE = "runtime_state";
105
29
  var ISSUE_RESOURCE = "issues";
@@ -128,7 +52,7 @@ function nowIso() {
128
52
  return (/* @__PURE__ */ new Date()).toISOString();
129
53
  }
130
54
  function safeRead(path) {
131
- return existsSync2(path) ? readFileSync2(path, "utf8") : "";
55
+ return existsSync(path) ? readFileSync(path, "utf8") : "";
132
56
  }
133
57
  async function loadS3dbModule() {
134
58
  try {
@@ -315,23 +239,134 @@ async function appendEvent(level, message, payload = {}, issueId) {
315
239
  });
316
240
  }
317
241
 
318
- // src/mcp/server.ts
242
+ // src/mcp/jsonrpc-transport.ts
243
+ import { stdout } from "process";
244
+
245
+ // src/integrations/catalog.ts
246
+ import { existsSync as existsSync2, readdirSync, readFileSync as readFileSync2 } from "fs";
247
+ import { homedir as homedir2 } from "os";
248
+ import { join as join2, resolve as resolve2 } from "path";
249
+ function listNames(basePath) {
250
+ if (!existsSync2(basePath)) {
251
+ return [];
252
+ }
253
+ return readdirSync(basePath, { withFileTypes: true }).filter((entry) => entry.isDirectory() || entry.isFile()).map((entry) => entry.name).sort((left, right) => left.localeCompare(right));
254
+ }
255
+ function readSkillSummary(skillPath) {
256
+ try {
257
+ const skillFile = join2(skillPath, "SKILL.md");
258
+ if (!existsSync2(skillFile)) {
259
+ return "";
260
+ }
261
+ const contents = readFileSync2(skillFile, "utf8");
262
+ const firstParagraph = contents.split("\n").map((line) => line.trim()).filter(Boolean).find((line) => !line.startsWith("#"));
263
+ return firstParagraph ?? "";
264
+ } catch {
265
+ return "";
266
+ }
267
+ }
268
+ function discoverIntegrations(workspaceRoot) {
269
+ const home = homedir2();
270
+ const agentLocations = [
271
+ resolve2(workspaceRoot, ".codex", "agents"),
272
+ resolve2(workspaceRoot, "agents"),
273
+ join2(home, ".codex", "agents"),
274
+ join2(home, ".claude", "agents")
275
+ ];
276
+ const skillLocations = [
277
+ resolve2(workspaceRoot, ".codex", "skills"),
278
+ resolve2(workspaceRoot, ".claude", "skills"),
279
+ join2(home, ".codex", "skills"),
280
+ join2(home, ".claude", "skills")
281
+ ];
282
+ const agencyItems = agentLocations.flatMap((location) => listNames(location).map((name) => ({ location, name }))).filter(({ name }) => name.startsWith("agency-")).map(({ location, name }) => `${name} @ ${location}`);
283
+ const impeccableItems = skillLocations.flatMap((location) => listNames(location).map((name) => ({ location, name }))).filter(
284
+ ({ name }) => name === "teach-impeccable" || name === "frontend-design" || name === "polish" || name === "audit" || name === "critique" || name.includes("impeccable")
285
+ ).map(({ location, name }) => {
286
+ const summary = readSkillSummary(join2(location, name));
287
+ return summary ? `${name} @ ${location} \u2014 ${summary}` : `${name} @ ${location}`;
288
+ });
289
+ return [
290
+ {
291
+ id: "agency-agents",
292
+ kind: "agents",
293
+ installed: agencyItems.length > 0,
294
+ locations: agentLocations.filter((location) => existsSync2(location)),
295
+ items: agencyItems,
296
+ 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."
297
+ },
298
+ {
299
+ id: "impeccable",
300
+ kind: "skills",
301
+ installed: impeccableItems.length > 0,
302
+ locations: skillLocations.filter((location) => existsSync2(location)),
303
+ items: impeccableItems,
304
+ 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."
305
+ }
306
+ ];
307
+ }
308
+ async function buildIntegrationSnippet(integrationId, workspaceRoot) {
309
+ if (integrationId === "agency-agents") {
310
+ return renderPrompt("integrations-agency-agents", { workspaceRoot });
311
+ }
312
+ if (integrationId === "impeccable") {
313
+ return renderPrompt("integrations-impeccable");
314
+ }
315
+ return "Unknown integration.";
316
+ }
317
+
318
+ // src/mcp/api-client.ts
319
+ import { env as env2 } from "process";
320
+ async function resolveApiBaseUrl() {
321
+ const envPort = env2.FIFONY_API_PORT;
322
+ if (envPort) return `http://localhost:${envPort}`;
323
+ const runtime = await getRuntimeSnapshot();
324
+ const config = runtime.config;
325
+ const port = config?.dashboardPort;
326
+ if (port) return `http://localhost:${port}`;
327
+ for (const candidate of [4e3, 3e3, 8080]) {
328
+ try {
329
+ const res = await fetch(`http://localhost:${candidate}/health`, { signal: AbortSignal.timeout(1e3) });
330
+ if (res.ok) return `http://localhost:${candidate}`;
331
+ } catch {
332
+ }
333
+ }
334
+ throw new Error("Fifony runtime API is not reachable. Start the runtime with --port to enable plan/refine/approve/analytics tools.");
335
+ }
336
+ async function apiPost(path, body = {}) {
337
+ const base = await resolveApiBaseUrl();
338
+ const res = await fetch(`${base}${path}`, {
339
+ method: "POST",
340
+ headers: { "content-type": "application/json" },
341
+ body: JSON.stringify(body),
342
+ signal: AbortSignal.timeout(12e4)
343
+ });
344
+ const json = await res.json();
345
+ if (!res.ok || json.ok === false) {
346
+ throw new Error(typeof json.error === "string" ? json.error : `API request failed: ${res.status}`);
347
+ }
348
+ return json;
349
+ }
350
+ async function apiGet(path) {
351
+ const base = await resolveApiBaseUrl();
352
+ const res = await fetch(`${base}${path}`, {
353
+ signal: AbortSignal.timeout(3e4)
354
+ });
355
+ const json = await res.json();
356
+ if (!res.ok || json.ok === false) {
357
+ throw new Error(typeof json.error === "string" ? json.error : `API request failed: ${res.status}`);
358
+ }
359
+ return json;
360
+ }
361
+
362
+ // src/mcp/resources/resource-builder.ts
363
+ import { dirname, join as join3, resolve as resolve3 } from "path";
364
+ import { fileURLToPath as fileURLToPath2 } from "url";
319
365
  var __filename = fileURLToPath2(import.meta.url);
320
366
  var __dirname = dirname(__filename);
321
- var PACKAGE_ROOT = resolve3(__dirname, "../..");
322
- var WORKFLOW_PATH = join3(WORKSPACE_ROOT, "WORKFLOW.md");
367
+ var PACKAGE_ROOT = resolve3(__dirname, "../../..");
323
368
  var README_PATH = join3(PACKAGE_ROOT, "README.md");
324
369
  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
370
  async function buildIntegrationGuide() {
336
371
  return renderPrompt("mcp-integration-guide", {
337
372
  workspaceRoot: WORKSPACE_ROOT,
@@ -363,7 +398,6 @@ async function buildStateSummary() {
363
398
  workspaceRoot: WORKSPACE_ROOT,
364
399
  persistenceRoot: PERSISTENCE_ROOT,
365
400
  stateRoot: STATE_ROOT,
366
- workflowPresent: existsSync3(WORKFLOW_PATH),
367
401
  runtimeUpdatedAt: runtime.updatedAt ?? null,
368
402
  issueCount: issues.length,
369
403
  issuesByState: byState,
@@ -387,13 +421,15 @@ async function buildIssuePrompt(issue, provider, role) {
387
421
  provider,
388
422
  id: issue.id,
389
423
  title: issue.title,
390
- state: issue.state ?? "Todo",
424
+ state: issue.state ?? "Planning",
391
425
  capabilityCategory: resolution.category,
392
426
  overlays: resolution.overlays,
393
427
  paths: Array.isArray(issue.paths) ? issue.paths.filter((value) => typeof value === "string") : [],
394
428
  description: issue.description || "No description provided."
395
429
  });
396
430
  }
431
+
432
+ // src/mcp/resources/resource-handlers.ts
397
433
  async function listResourcesMcp() {
398
434
  const issues = await getIssues();
399
435
  const resources = [
@@ -405,9 +441,6 @@ async function listResourcesMcp() {
405
441
  { uri: "fifony://integrations", name: "Fifony integrations", description: "Discovered local integrations such as agency-agents and impeccable skills.", mimeType: "application/json" },
406
442
  { uri: "fifony://capabilities", name: "Fifony capability routing", description: "How Fifony would route current issues to providers, profiles, and overlays.", mimeType: "application/json" }
407
443
  ];
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
444
  resources.push(
412
445
  { uri: "fifony://analytics", name: "Token usage analytics", description: "Token usage analytics snapshot including totals, cost estimates, and per-model breakdown.", mimeType: "application/json" },
413
446
  { uri: "fifony://workflow/config", name: "Workflow config", description: "Current pipeline workflow configuration (plan/execute/review providers, models, and effort).", mimeType: "application/json" },
@@ -466,7 +499,6 @@ async function readResource(uri) {
466
499
  )
467
500
  }];
468
501
  }
469
- if (uri === "fifony://workspace/workflow") return [{ uri, mimeType: "text/markdown", text: safeRead(WORKFLOW_PATH) }];
470
502
  if (uri === "fifony://analytics") {
471
503
  try {
472
504
  const result = await apiGet("/api/analytics/tokens");
@@ -548,407 +580,52 @@ async function readResource(uri) {
548
580
  }
549
581
  throw new Error(`Unknown resource: ${uri}`);
550
582
  }
551
- function listPrompts() {
583
+
584
+ // src/mcp/tools/tool-list.ts
585
+ function listTools() {
552
586
  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 }] }
587
+ { name: "fifony.status", description: "Return a compact status summary for the current Fifony workspace.", inputSchema: { type: "object", properties: {}, additionalProperties: false } },
588
+ { 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 } },
589
+ { 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 } },
590
+ { 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 } },
591
+ { 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 } },
592
+ { 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 } },
593
+ { 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 } },
594
+ { 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 } },
595
+ { 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 } },
596
+ { 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 } },
597
+ { name: "fifony.list_integrations", description: "List discovered local integrations such as agency-agents profiles and impeccable skills.", inputSchema: { type: "object", properties: {}, additionalProperties: false } },
598
+ { 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 } },
599
+ { 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 } },
600
+ { 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 } },
601
+ { 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 } },
602
+ { 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 } },
603
+ { 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 } },
604
+ { 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 } },
605
+ { 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 } },
606
+ { 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 } },
607
+ { 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 } },
608
+ { 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 } },
609
+ { name: "fifony.scan_project", description: "Scan the target project structure, returning files, directories, and detected technologies.", inputSchema: { type: "object", properties: {}, additionalProperties: false } },
610
+ { 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 } },
611
+ { 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
612
  ];
563
613
  }
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));
614
+
615
+ // src/mcp/tools/tool-executor.ts
616
+ import { createHash } from "crypto";
617
+ function hashInput(value) {
618
+ return createHash("sha1").update(value).digest("hex").slice(0, 10);
619
+ }
620
+ function toolText(text) {
621
+ return { content: [{ type: "text", text }] };
622
+ }
623
+ async function callTool(name, args = {}) {
624
+ if (name === "fifony.status") return toolText(await buildStateSummary());
625
+ if (name === "fifony.list_issues") {
626
+ const stateFilter = typeof args.state === "string" && args.state.trim() ? args.state.trim() : "";
627
+ const capabilityCategory = typeof args.capabilityCategory === "string" && args.capabilityCategory.trim() ? args.capabilityCategory.trim() : typeof args.category === "string" && args.category.trim() ? args.category.trim() : "";
628
+ return toolText(JSON.stringify(await listIssues({ state: stateFilter || void 0, capabilityCategory: capabilityCategory || void 0 }), null, 2));
952
629
  }
953
630
  if (name === "fifony.create_issue") {
954
631
  await initDatabase();
@@ -959,7 +636,7 @@ async function callTool(name, args = {}) {
959
636
  const issueId = explicitId || `LOCAL-${hashInput(`${title}:${nowIso()}`)}`.toUpperCase();
960
637
  const description = typeof args.description === "string" ? args.description : "";
961
638
  const priority = typeof args.priority === "number" ? args.priority : 2;
962
- const state = typeof args.state === "string" && args.state.trim() ? args.state.trim() : "Todo";
639
+ const state = parseIssueState(args.state) ?? "Planning";
963
640
  const baseLabels = Array.isArray(args.labels) ? args.labels.filter((value) => typeof value === "string") : ["fifony", "mcp"];
964
641
  const paths = Array.isArray(args.paths) ? args.paths.filter((value) => typeof value === "string") : [];
965
642
  const inferredPaths = inferCapabilityPaths({ id: issueId, identifier: issueId, title, description, labels: baseLabels, paths });
@@ -994,9 +671,9 @@ async function callTool(name, args = {}) {
994
671
  await initDatabase();
995
672
  const { issueResource: issueResource2 } = getResources();
996
673
  const issueId = typeof args.issueId === "string" ? args.issueId.trim() : "";
997
- const state = typeof args.state === "string" ? args.state.trim() : "";
674
+ const state = parseIssueState(args.state);
998
675
  const note = typeof args.note === "string" ? args.note : "";
999
- if (!issueId || !state) throw new Error("issueId and state are required");
676
+ if (!issueId || !state) throw new Error("issueId and a valid canonical state are required");
1000
677
  const current = await issueResource2?.get(issueId);
1001
678
  if (!current) throw new Error(`Issue not found: ${issueId}`);
1002
679
  const updated = await issueResource2?.update(issueId, { state, updatedAt: nowIso() });
@@ -1044,8 +721,8 @@ async function callTool(name, args = {}) {
1044
721
  const issue = result.issue;
1045
722
  return toolText(JSON.stringify({
1046
723
  issueId,
1047
- state: issue?.state ?? "Todo",
1048
- message: `Plan approved for ${issueId}. Issue moved to Todo and is ready for execution.`
724
+ state: issue?.state ?? "Planned",
725
+ message: `Plan approved for ${issueId}. Issue moved to Planned and is ready for execution.`
1049
726
  }, null, 2));
1050
727
  }
1051
728
  if (name === "fifony.merge") {
@@ -1073,12 +750,7 @@ async function callTool(name, args = {}) {
1073
750
  overall: { inputTokens, outputTokens, totalTokens },
1074
751
  estimatedCostUsd: Math.round(estimatedCost * 100) / 100,
1075
752
  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
- }))
753
+ topIssues: (topIssues ?? []).slice(0, 10).map((issue) => ({ id: issue.id, totalTokens: issue.totalTokens, inputTokens: issue.inputTokens, outputTokens: issue.outputTokens }))
1082
754
  }, null, 2));
1083
755
  }
1084
756
  if (name === "fifony.integration_config") {
@@ -1139,7 +811,7 @@ async function callTool(name, args = {}) {
1139
811
  try {
1140
812
  const result = await apiPost(`/api/issues/${encodeURIComponent(issueId)}/retry`);
1141
813
  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));
814
+ return toolText(JSON.stringify({ issueId, state: issue?.state ?? "Planned", message: `Issue ${issueId} has been retried and reset to Planned.` }, null, 2));
1143
815
  } catch (error) {
1144
816
  throw new Error(`Failed to retry issue ${issueId}: ${String(error)}`);
1145
817
  }
@@ -1163,97 +835,411 @@ async function callTool(name, args = {}) {
1163
835
  throw new Error(`Failed to enhance ${field}: ${String(error)}`);
1164
836
  }
1165
837
  }
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");
838
+ if (name === "fifony.get_diff") {
839
+ const issueId = typeof args.issueId === "string" ? args.issueId.trim() : "";
840
+ if (!issueId) throw new Error("issueId is required");
841
+ try {
842
+ const result = await apiGet(`/api/diff/${encodeURIComponent(issueId)}`);
843
+ return toolText(JSON.stringify(result, null, 2));
844
+ } catch (error) {
845
+ throw new Error(`Failed to get diff for ${issueId}: ${String(error)}`);
846
+ }
847
+ }
848
+ if (name === "fifony.get_live") {
849
+ const issueId = typeof args.issueId === "string" ? args.issueId.trim() : "";
850
+ if (!issueId) throw new Error("issueId is required");
851
+ try {
852
+ const result = await apiGet(`/api/live/${encodeURIComponent(issueId)}`);
853
+ return toolText(JSON.stringify(result, null, 2));
854
+ } catch (error) {
855
+ throw new Error(`Failed to get live output for ${issueId}: ${String(error)}`);
856
+ }
857
+ }
858
+ if (name === "fifony.get_events") {
859
+ const issueId = typeof args.issueId === "string" ? args.issueId.trim() : "";
860
+ const kind = typeof args.kind === "string" ? args.kind.trim() : "";
861
+ const limit = typeof args.limit === "number" ? args.limit : 50;
862
+ const params = new URLSearchParams();
863
+ if (issueId) params.set("issueId", issueId);
864
+ if (kind) params.set("kind", kind);
865
+ const query = params.toString();
866
+ try {
867
+ const result = await apiGet(`/api/events/feed${query ? `?${query}` : ""}`);
868
+ const events = Array.isArray(result.events) ? result.events.slice(0, limit) : [];
869
+ return toolText(JSON.stringify({ events, count: events.length }, null, 2));
870
+ } catch (error) {
871
+ const events = await listEvents({ limit });
872
+ const filtered = events.filter((event) => {
873
+ if (issueId && event.issueId !== issueId) return false;
874
+ if (kind && event.kind !== kind) return false;
875
+ return true;
876
+ }).slice(0, limit);
877
+ return toolText(JSON.stringify({ events: filtered, count: filtered.length }, null, 2));
878
+ }
879
+ }
880
+ if (name === "fifony.get_workflow") {
881
+ try {
882
+ const result = await apiGet("/api/config/workflow");
883
+ return toolText(JSON.stringify(result, null, 2));
884
+ } catch (error) {
885
+ throw new Error(`Failed to get workflow config: ${String(error)}`);
886
+ }
887
+ }
888
+ if (name === "fifony.set_workflow") {
889
+ const plan = args.plan;
890
+ const execute = args.execute;
891
+ const review = args.review;
892
+ if (!plan || !execute || !review) throw new Error("plan, execute, and review are all required");
893
+ try {
894
+ const result = await apiPost("/api/config/workflow", { workflow: { plan, execute, review } });
895
+ return toolText(JSON.stringify({ message: "Workflow configuration updated successfully.", workflow: result.workflow }, null, 2));
896
+ } catch (error) {
897
+ throw new Error(`Failed to set workflow config: ${String(error)}`);
898
+ }
899
+ }
900
+ if (name === "fifony.scan_project") {
901
+ try {
902
+ const result = await apiGet("/api/scan/project");
903
+ return toolText(JSON.stringify(result, null, 2));
904
+ } catch (error) {
905
+ throw new Error(`Failed to scan project: ${String(error)}`);
906
+ }
907
+ }
908
+ if (name === "fifony.install_agents") {
909
+ const agents = Array.isArray(args.agents) ? args.agents.filter((value) => typeof value === "string") : [];
910
+ if (agents.length === 0) throw new Error("At least one agent name is required");
911
+ try {
912
+ const result = await apiPost("/api/install/agents", { agents });
913
+ return toolText(JSON.stringify(result, null, 2));
914
+ } catch (error) {
915
+ throw new Error(`Failed to install agents: ${String(error)}`);
916
+ }
917
+ }
918
+ if (name === "fifony.install_skills") {
919
+ const skills = Array.isArray(args.skills) ? args.skills.filter((value) => typeof value === "string") : [];
920
+ if (skills.length === 0) throw new Error("At least one skill name is required");
921
+ try {
922
+ const result = await apiPost("/api/install/skills", { skills });
923
+ return toolText(JSON.stringify(result, null, 2));
924
+ } catch (error) {
925
+ throw new Error(`Failed to install skills: ${String(error)}`);
926
+ }
927
+ }
928
+ throw new Error(`Unknown tool: ${name}`);
929
+ }
930
+
931
+ // src/mcp/prompts/prompt-list.ts
932
+ function listPrompts() {
933
+ return [
934
+ { 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 }] },
935
+ { 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 }] },
936
+ { 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 }] },
937
+ { 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 }] },
938
+ { 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 }] },
939
+ { name: "fifony-weekly-summary", description: "Generate a weekly progress summary including issues created, completed, blocked, and token usage.", arguments: [] },
940
+ { 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 }] },
941
+ { 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 }] }
942
+ ];
943
+ }
944
+
945
+ // src/mcp/prompts/prompt-handler.ts
946
+ async function getPrompt(name, args = {}) {
947
+ if (name === "fifony-integrate-client") {
948
+ const client = typeof args.client === "string" && args.client.trim() ? args.client.trim() : "mcp-client";
949
+ const goal = typeof args.goal === "string" && args.goal.trim() ? args.goal.trim() : "integrate with the local Fifony workspace";
950
+ const integrationGuide = await buildIntegrationGuide();
951
+ return {
952
+ description: "Client integration prompt for Fifony.",
953
+ messages: [{
954
+ role: "user",
955
+ content: {
956
+ type: "text",
957
+ text: await renderPrompt("mcp-integrate-client", { client, goal, integrationGuide })
958
+ }
959
+ }]
960
+ };
961
+ }
962
+ if (name === "fifony-plan-issue") {
963
+ const issueId = typeof args.issueId === "string" ? args.issueId : "";
964
+ const provider = typeof args.provider === "string" && args.provider.trim() ? args.provider.trim() : "codex";
965
+ const issue = issueId ? await getIssue(issueId) : null;
966
+ if (!issue) throw new Error(`Issue not found: ${issueId}`);
967
+ return {
968
+ description: "Issue planning prompt grounded in the Fifony issue store.",
969
+ messages: [{
970
+ role: "user",
971
+ content: { type: "text", text: await buildIssuePrompt(issue, provider, "planner") }
972
+ }]
973
+ };
974
+ }
975
+ if (name === "fifony-use-integration") {
976
+ const integration = typeof args.integration === "string" ? args.integration : "";
977
+ return {
978
+ description: "Integration guidance for a discovered Fifony extension.",
979
+ messages: [{
980
+ role: "user",
981
+ content: { type: "text", text: await buildIntegrationSnippet(integration, WORKSPACE_ROOT) }
982
+ }]
983
+ };
984
+ }
985
+ if (name === "fifony-route-task") {
986
+ const title = typeof args.title === "string" ? args.title : "";
987
+ const description = typeof args.description === "string" ? args.description : "";
988
+ const labels = typeof args.labels === "string" ? args.labels.split(",").map((label) => label.trim()).filter(Boolean) : [];
989
+ const paths = typeof args.paths === "string" ? args.paths.split(",").map((value) => value.trim()).filter(Boolean) : [];
990
+ const resolution = resolveTaskCapabilities({ title, description, labels, paths });
991
+ return {
992
+ description: "Task routing prompt produced by the Fifony capability resolver.",
993
+ messages: [{
994
+ role: "user",
995
+ content: {
996
+ type: "text",
997
+ text: await renderPrompt("mcp-route-task", {
998
+ resolutionJson: JSON.stringify(resolution, null, 2)
999
+ })
1000
+ }
1001
+ }]
1002
+ };
1003
+ }
1004
+ if (name === "fifony-diagnose-blocked") {
1005
+ const issueId = typeof args.issueId === "string" ? args.issueId.trim() : "";
1006
+ if (!issueId) throw new Error("issueId is required");
1007
+ const issue = await getIssue(issueId);
1008
+ if (!issue) throw new Error(`Issue not found: ${issueId}`);
1009
+ const issueData = issue;
1010
+ let events = [];
1011
+ try {
1012
+ const evResult = await apiGet(`/api/events/feed?issueId=${encodeURIComponent(issueId)}`);
1013
+ events = Array.isArray(evResult.events) ? evResult.events.slice(0, 30) : [];
1014
+ } catch {
1015
+ const localEvents = await listEvents({ limit: 100 });
1016
+ events = localEvents.filter((event) => event.issueId === issueId).slice(0, 30);
1017
+ }
1018
+ const plan = issueData.plan ?? null;
1019
+ const history = Array.isArray(issueData.history) ? issueData.history : [];
1020
+ const lastError = issueData.lastError ?? null;
1021
+ const state = issueData.state ?? "Unknown";
1022
+ const attempts = issueData.attempts ?? 0;
1023
+ const maxAttempts = issueData.maxAttempts ?? 3;
1024
+ const diagnosticText = [
1025
+ `# Diagnostic Report for Issue ${issueId}`,
1026
+ ``,
1027
+ `## Issue Details`,
1028
+ `- **Title**: ${issueData.title ?? "Unknown"}`,
1029
+ `- **State**: ${state}`,
1030
+ `- **Attempts**: ${attempts} / ${maxAttempts}`,
1031
+ `- **Last Error**: ${lastError ?? "None"}`,
1032
+ `- **Updated At**: ${issueData.updatedAt ?? "Unknown"}`,
1033
+ ``,
1034
+ `## Plan`,
1035
+ plan ? `- **Summary**: ${plan.summary ?? plan.title ?? "No summary"}` : "No plan generated.",
1036
+ plan?.steps ? `- **Steps**: ${plan.steps.length} step(s)` : "",
1037
+ plan?.estimatedComplexity ? `- **Estimated Complexity**: ${plan.estimatedComplexity}` : "",
1038
+ ``,
1039
+ `## History`,
1040
+ ...history.length > 0 ? history.slice(-15).map((entry) => `- ${entry}`) : ["No history entries."],
1041
+ ``,
1042
+ `## Recent Events`,
1043
+ ...events.length > 0 ? events.slice(0, 15).map((event) => `- [${event.kind ?? "info"}] ${event.at ?? ""}: ${event.message ?? ""}`) : ["No events found."],
1044
+ ``,
1045
+ `## Diagnostic Questions`,
1046
+ `Based on the information above, please analyze:`,
1047
+ `1. What is the root cause of the issue being in "${state}" state?`,
1048
+ `2. Is the error recoverable? If so, what steps should be taken?`,
1049
+ `3. Does the plan need modification before retrying?`,
1050
+ `4. Are there any dependency or configuration issues that need resolution?`,
1051
+ `5. What is the recommended next action?`
1052
+ ].filter((line) => line !== void 0).join("\n");
1053
+ return {
1054
+ description: `Diagnostic prompt for blocked/failed issue ${issueId}.`,
1055
+ messages: [{
1056
+ role: "user",
1057
+ content: { type: "text", text: diagnosticText }
1058
+ }]
1059
+ };
1060
+ }
1061
+ if (name === "fifony-weekly-summary") {
1062
+ const issues = await getIssues();
1063
+ let analytics = {};
1169
1064
  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)}`);
1065
+ analytics = await apiGet("/api/analytics/tokens");
1066
+ } catch {
1174
1067
  }
1068
+ const overall = analytics.overall ?? {};
1069
+ const byState = issues.reduce((accumulator, issue) => {
1070
+ const key = issue.state ?? "Unknown";
1071
+ accumulator[key] = (accumulator[key] ?? 0) + 1;
1072
+ return accumulator;
1073
+ }, {});
1074
+ const totalIssues = issues.length;
1075
+ const completed = byState["Done"] ?? 0;
1076
+ const blocked = (byState["Blocked"] ?? 0) + (byState["Failed"] ?? 0);
1077
+ const inProgress = (byState["Running"] ?? 0) + (byState["Reviewing"] ?? 0) + (byState["Reviewed"] ?? 0) + (byState["Queued"] ?? 0);
1078
+ const planned = byState["Planned"] ?? 0;
1079
+ const planning = byState["Planning"] ?? 0;
1080
+ const cancelled = byState["Cancelled"] ?? 0;
1081
+ const inputTokens = typeof overall.inputTokens === "number" ? overall.inputTokens : 0;
1082
+ const outputTokens = typeof overall.outputTokens === "number" ? overall.outputTokens : 0;
1083
+ const totalTokens = typeof overall.totalTokens === "number" ? overall.totalTokens : 0;
1084
+ const estimatedCost = inputTokens / 1e6 * 3 + outputTokens / 1e6 * 15;
1085
+ const summaryText = [
1086
+ `# Fifony Weekly Progress Summary`,
1087
+ ``,
1088
+ `## Issue Statistics`,
1089
+ `| Status | Count |`,
1090
+ `|--------|-------|`,
1091
+ `| Total Issues | ${totalIssues} |`,
1092
+ `| Completed (Done) | ${completed} |`,
1093
+ `| In Progress | ${inProgress} |`,
1094
+ `| Planned | ${planned} |`,
1095
+ `| Planning | ${planning} |`,
1096
+ `| Blocked/Failed | ${blocked} |`,
1097
+ `| Cancelled | ${cancelled} |`,
1098
+ ``,
1099
+ `## Token Usage`,
1100
+ `- **Total Tokens**: ${totalTokens.toLocaleString()}`,
1101
+ `- **Input Tokens**: ${inputTokens.toLocaleString()}`,
1102
+ `- **Output Tokens**: ${outputTokens.toLocaleString()}`,
1103
+ `- **Estimated Cost**: $${(Math.round(estimatedCost * 100) / 100).toFixed(2)}`,
1104
+ ``,
1105
+ `## Analysis Request`,
1106
+ `Based on these metrics, please provide:`,
1107
+ `1. A brief summary of overall progress this week`,
1108
+ `2. Identification of any bottlenecks (blocked/failed issues)`,
1109
+ `3. Token usage efficiency assessment`,
1110
+ `4. Recommendations for improving throughput`,
1111
+ `5. Priority items for next week`
1112
+ ].join("\n");
1113
+ return {
1114
+ description: "Weekly progress summary prompt for the Fifony workspace.",
1115
+ messages: [{
1116
+ role: "user",
1117
+ content: { type: "text", text: summaryText }
1118
+ }]
1119
+ };
1175
1120
  }
1176
- if (name === "fifony.get_live") {
1121
+ if (name === "fifony-refine-plan") {
1177
1122
  const issueId = typeof args.issueId === "string" ? args.issueId.trim() : "";
1123
+ const concern = typeof args.concern === "string" ? args.concern.trim() : "";
1178
1124
  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
- }
1125
+ const issue = await getIssue(issueId);
1126
+ if (!issue) throw new Error(`Issue not found: ${issueId}`);
1127
+ const issueData = issue;
1128
+ const plan = issueData.plan ?? null;
1129
+ const steps = plan?.steps ?? [];
1130
+ const stepsText = steps.length > 0 ? steps.map((step, index) => `${index + 1}. **${step.title ?? step.description ?? "Step"}**
1131
+ ${step.description ?? step.detail ?? ""}`).join("\n") : "No steps defined.";
1132
+ const refinementText = [
1133
+ `# Plan Refinement for Issue ${issueId}`,
1134
+ ``,
1135
+ `## Issue`,
1136
+ `- **Title**: ${issueData.title ?? "Unknown"}`,
1137
+ `- **Description**: ${issueData.description ?? "No description"}`,
1138
+ ``,
1139
+ `## Current Plan`,
1140
+ plan ? `- **Summary**: ${plan.summary ?? plan.title ?? "No summary"}` : "No plan exists yet.",
1141
+ plan?.estimatedComplexity ? `- **Complexity**: ${plan.estimatedComplexity}` : "",
1142
+ ``,
1143
+ `### Steps`,
1144
+ stepsText,
1145
+ ``,
1146
+ concern ? `## Specific Concern
1147
+ ${concern}
1148
+ ` : "",
1149
+ `## Refinement Guidance`,
1150
+ `Please review the current plan and provide specific, actionable feedback:`,
1151
+ `1. Are the steps correctly ordered and complete?`,
1152
+ `2. Are there missing edge cases or error handling steps?`,
1153
+ `3. Is the complexity estimate accurate?`,
1154
+ `4. Are the file paths and affected areas correct?`,
1155
+ `5. Should any steps be split, merged, or removed?`,
1156
+ ``,
1157
+ `Provide your feedback, and it will be used to refine the plan via \`fifony.refine\`.`
1158
+ ].filter((line) => line !== void 0).join("\n");
1159
+ return {
1160
+ description: `Plan refinement prompt for issue ${issueId}.`,
1161
+ messages: [{
1162
+ role: "user",
1163
+ content: { type: "text", text: refinementText }
1164
+ }]
1165
+ };
1185
1166
  }
1186
- if (name === "fifony.get_events") {
1167
+ if (name === "fifony-code-review") {
1187
1168
  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");
1169
+ if (!issueId) throw new Error("issueId is required");
1170
+ const issue = await getIssue(issueId);
1171
+ if (!issue) throw new Error(`Issue not found: ${issueId}`);
1172
+ const issueData = issue;
1173
+ let diffData = {};
1239
1174
  try {
1240
- const result = await apiPost("/api/install/agents", { agents });
1241
- return toolText(JSON.stringify(result, null, 2));
1175
+ diffData = await apiGet(`/api/diff/${encodeURIComponent(issueId)}`);
1242
1176
  } catch (error) {
1243
- throw new Error(`Failed to install agents: ${String(error)}`);
1177
+ throw new Error(`Cannot fetch diff for issue ${issueId}. Is the runtime running? ${String(error)}`);
1244
1178
  }
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)}`);
1179
+ const files = Array.isArray(diffData.files) ? diffData.files : [];
1180
+ const diff = typeof diffData.diff === "string" ? diffData.diff : "";
1181
+ const totalAdditions = typeof diffData.totalAdditions === "number" ? diffData.totalAdditions : 0;
1182
+ const totalDeletions = typeof diffData.totalDeletions === "number" ? diffData.totalDeletions : 0;
1183
+ if (!diff.trim()) {
1184
+ return {
1185
+ description: `Code review prompt for issue ${issueId} (no changes).`,
1186
+ messages: [{
1187
+ role: "user",
1188
+ content: { type: "text", text: `# Code Review for ${issueId}
1189
+
1190
+ No code changes found for this issue. The workspace may not have been created yet or no modifications were made.` }
1191
+ }]
1192
+ };
1254
1193
  }
1194
+ const filesTable = files.map((file) => `| ${file.path} | ${file.status} | +${file.additions} | -${file.deletions} |`).join("\n");
1195
+ const reviewText = [
1196
+ `# Code Review for Issue ${issueId}`,
1197
+ ``,
1198
+ `## Issue Context`,
1199
+ `- **Title**: ${issueData.title ?? "Unknown"}`,
1200
+ `- **Description**: ${issueData.description ?? "No description"}`,
1201
+ `- **State**: ${issueData.state ?? "Unknown"}`,
1202
+ ``,
1203
+ `## Change Summary`,
1204
+ `- **Files Changed**: ${files.length}`,
1205
+ `- **Total Additions**: +${totalAdditions}`,
1206
+ `- **Total Deletions**: -${totalDeletions}`,
1207
+ ``,
1208
+ `### Files`,
1209
+ `| Path | Status | Additions | Deletions |`,
1210
+ `|------|--------|-----------|-----------|`,
1211
+ filesTable,
1212
+ ``,
1213
+ `## Diff`,
1214
+ "```diff",
1215
+ diff.length > 5e4 ? diff.substring(0, 5e4) + "\n... (diff truncated at 50KB)" : diff,
1216
+ "```",
1217
+ ``,
1218
+ `## Review Checklist`,
1219
+ `Please review the changes and evaluate:`,
1220
+ `1. **Correctness**: Do the changes correctly implement what the issue describes?`,
1221
+ `2. **Code Quality**: Is the code clean, readable, and follows project conventions?`,
1222
+ `3. **Error Handling**: Are edge cases and errors properly handled?`,
1223
+ `4. **Security**: Are there any security concerns (hardcoded secrets, SQL injection, XSS)?`,
1224
+ `5. **Performance**: Are there any performance concerns or inefficiencies?`,
1225
+ `6. **Tests**: Are changes adequately covered by tests?`,
1226
+ `7. **Breaking Changes**: Do any changes break backward compatibility?`
1227
+ ].join("\n");
1228
+ return {
1229
+ description: `Code review prompt for issue ${issueId}.`,
1230
+ messages: [{
1231
+ role: "user",
1232
+ content: { type: "text", text: reviewText }
1233
+ }]
1234
+ };
1255
1235
  }
1256
- throw new Error(`Unknown tool: ${name}`);
1236
+ throw new Error(`Unknown prompt: ${name}`);
1237
+ }
1238
+
1239
+ // src/mcp/jsonrpc-transport.ts
1240
+ var incomingBuffer = Buffer.alloc(0);
1241
+ function setIncomingBuffer(buffer) {
1242
+ incomingBuffer = buffer;
1257
1243
  }
1258
1244
  function sendMessage(message) {
1259
1245
  const payload = Buffer.from(JSON.stringify(message), "utf8");
@@ -1331,13 +1317,21 @@ function processIncomingBuffer() {
1331
1317
  void handleRequest(request);
1332
1318
  }
1333
1319
  }
1320
+
1321
+ // src/mcp/server.ts
1322
+ var DEBUG_BOOT2 = env3.FIFONY_DEBUG_BOOT === "1";
1323
+ function debugBoot2(message) {
1324
+ if (!DEBUG_BOOT2) return;
1325
+ process.stderr.write(`[FIFONY_DEBUG_BOOT] ${message}
1326
+ `);
1327
+ }
1334
1328
  async function bootstrap() {
1335
1329
  debugBoot2("mcp:bootstrap:start");
1336
1330
  await initDatabase();
1337
1331
  debugBoot2("mcp:bootstrap:database-ready");
1338
1332
  await appendEvent("info", "Fifony MCP server started.", { workspaceRoot: WORKSPACE_ROOT, persistenceRoot: PERSISTENCE_ROOT });
1339
1333
  stdin.on("data", (chunk) => {
1340
- incomingBuffer = Buffer.concat([incomingBuffer, chunk]);
1334
+ setIncomingBuffer(Buffer.concat([incomingBuffer, chunk]));
1341
1335
  processIncomingBuffer();
1342
1336
  });
1343
1337
  stdin.resume();