@harness-engineering/cli 1.8.2 → 1.10.0

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 (92) hide show
  1. package/dist/agents/skills/claude-code/cleanup-dead-code/SKILL.md +3 -3
  2. package/dist/agents/skills/claude-code/harness-autopilot/SKILL.md +20 -3
  3. package/dist/agents/skills/claude-code/harness-brainstorming/SKILL.md +55 -5
  4. package/dist/agents/skills/claude-code/harness-code-review/SKILL.md +36 -15
  5. package/dist/agents/skills/claude-code/harness-codebase-cleanup/SKILL.md +1 -1
  6. package/dist/agents/skills/claude-code/harness-execution/SKILL.md +70 -13
  7. package/dist/agents/skills/claude-code/harness-planning/SKILL.md +41 -3
  8. package/dist/agents/skills/claude-code/harness-pre-commit-review/SKILL.md +28 -3
  9. package/dist/agents/skills/claude-code/harness-release-readiness/SKILL.md +14 -2
  10. package/dist/agents/skills/claude-code/harness-verification/SKILL.md +18 -2
  11. package/dist/agents/skills/gemini-cli/cleanup-dead-code/SKILL.md +3 -3
  12. package/dist/agents/skills/gemini-cli/harness-autopilot/SKILL.md +20 -3
  13. package/dist/agents/skills/gemini-cli/harness-brainstorming/SKILL.md +55 -5
  14. package/dist/agents/skills/gemini-cli/harness-code-review/SKILL.md +36 -15
  15. package/dist/agents/skills/gemini-cli/harness-codebase-cleanup/SKILL.md +1 -1
  16. package/dist/agents/skills/gemini-cli/harness-execution/SKILL.md +70 -13
  17. package/dist/agents/skills/gemini-cli/harness-planning/SKILL.md +41 -3
  18. package/dist/agents/skills/gemini-cli/harness-pre-commit-review/SKILL.md +28 -3
  19. package/dist/agents/skills/gemini-cli/harness-release-readiness/SKILL.md +14 -2
  20. package/dist/agents/skills/gemini-cli/harness-verification/SKILL.md +18 -2
  21. package/dist/agents-md-EMRFLNBC.js +8 -0
  22. package/dist/architecture-5JNN5L3M.js +13 -0
  23. package/dist/bin/harness-mcp.d.ts +1 -0
  24. package/dist/bin/harness-mcp.js +28 -0
  25. package/dist/bin/harness.js +42 -8
  26. package/dist/check-phase-gate-WOKIYGAM.js +12 -0
  27. package/dist/chunk-46YA6FI3.js +293 -0
  28. package/dist/chunk-4PFMY3H7.js +248 -0
  29. package/dist/{chunk-LB4GRDDV.js → chunk-72GHBOL2.js} +1 -1
  30. package/dist/chunk-7X7ZAYMY.js +373 -0
  31. package/dist/chunk-B7HFEHWP.js +35 -0
  32. package/dist/chunk-BM3PWGXQ.js +14 -0
  33. package/dist/chunk-C2ERUR3L.js +255 -0
  34. package/dist/chunk-CWZ4Y2PO.js +189 -0
  35. package/dist/{chunk-ULSRSP53.js → chunk-ECUJQS3B.js} +11 -112
  36. package/dist/chunk-EOLRW32Q.js +72 -0
  37. package/dist/chunk-F3YDAJFQ.js +125 -0
  38. package/dist/chunk-F4PTVZWA.js +116 -0
  39. package/dist/chunk-FPIPT36X.js +187 -0
  40. package/dist/chunk-FX7SQHGD.js +103 -0
  41. package/dist/chunk-HIOXKZYF.js +15 -0
  42. package/dist/chunk-IDZNPTYD.js +16 -0
  43. package/dist/chunk-JSTQ3AWB.js +31 -0
  44. package/dist/chunk-K6XAPGML.js +27 -0
  45. package/dist/chunk-KET4QQZB.js +8 -0
  46. package/dist/chunk-LXU5M77O.js +4028 -0
  47. package/dist/chunk-MDUK2J2O.js +67 -0
  48. package/dist/chunk-MHBMTPW7.js +29 -0
  49. package/dist/chunk-MO4YQOMB.js +85 -0
  50. package/dist/chunk-NKDM3FMH.js +52 -0
  51. package/dist/{chunk-SAB3VXOW.js → chunk-NX6DSZSM.js} +144 -111
  52. package/dist/chunk-OPXH4CQN.js +62 -0
  53. package/dist/{chunk-Y7U5AYAL.js → chunk-PAHHT2IK.js} +471 -2719
  54. package/dist/chunk-PMTFPOCT.js +122 -0
  55. package/dist/chunk-PSXF277V.js +89 -0
  56. package/dist/chunk-Q6AB7W5Z.js +135 -0
  57. package/dist/chunk-QPEH2QPG.js +347 -0
  58. package/dist/chunk-TEFCFC4H.js +15 -0
  59. package/dist/chunk-TRAPF4IX.js +185 -0
  60. package/dist/chunk-VUCPTQ6G.js +67 -0
  61. package/dist/chunk-W6Y7ZW3Y.js +13 -0
  62. package/dist/chunk-ZOAWBDWU.js +72 -0
  63. package/dist/ci-workflow-ZBBUNTHQ.js +8 -0
  64. package/dist/constants-5JGUXPEK.js +6 -0
  65. package/dist/create-skill-LUWO46WF.js +11 -0
  66. package/dist/dist-D4RYGUZE.js +14 -0
  67. package/dist/{dist-K6KTTN3I.js → dist-I7DB5VKB.js} +237 -0
  68. package/dist/dist-L7LAAQAS.js +18 -0
  69. package/dist/{dist-ZODQVGC4.js → dist-PBTNVK6K.js} +8 -6
  70. package/dist/docs-PTJGD6XI.js +12 -0
  71. package/dist/engine-SCMZ3G3E.js +8 -0
  72. package/dist/entropy-YIUBGKY7.js +12 -0
  73. package/dist/feedback-WEVQSLAA.js +18 -0
  74. package/dist/generate-agent-definitions-BU5LOJTI.js +15 -0
  75. package/dist/glob-helper-5OHBUQAI.js +52 -0
  76. package/dist/graph-loader-RLO3KRIX.js +8 -0
  77. package/dist/index.d.ts +11 -1
  78. package/dist/index.js +84 -33
  79. package/dist/loader-6S6PVGSF.js +10 -0
  80. package/dist/mcp-BNLBTCXZ.js +34 -0
  81. package/dist/performance-5TVW6SA6.js +24 -0
  82. package/dist/review-pipeline-4JTQAWKW.js +9 -0
  83. package/dist/runner-VMYLHWOC.js +6 -0
  84. package/dist/runtime-PXIM7UV6.js +9 -0
  85. package/dist/security-URYTKLGK.js +9 -0
  86. package/dist/skill-executor-KVS47DAU.js +8 -0
  87. package/dist/validate-KSDUUK2M.js +12 -0
  88. package/dist/validate-cross-check-WZAX357V.js +8 -0
  89. package/dist/version-KFFPOQAX.js +6 -0
  90. package/package.json +7 -5
  91. package/dist/create-skill-UZOHMXRU.js +0 -8
  92. package/dist/validate-cross-check-DLNK423G.js +0 -7
@@ -0,0 +1,4028 @@
1
+ import {
2
+ detectEntropyDefinition,
3
+ handleDetectEntropy
4
+ } from "./chunk-CWZ4Y2PO.js";
5
+ import {
6
+ checkPerformanceDefinition,
7
+ getCriticalPathsDefinition,
8
+ getPerfBaselinesDefinition,
9
+ handleCheckPerformance,
10
+ handleGetCriticalPaths,
11
+ handleGetPerfBaselines,
12
+ handleUpdatePerfBaselines,
13
+ updatePerfBaselinesDefinition
14
+ } from "./chunk-FPIPT36X.js";
15
+ import {
16
+ analyzeDiffDefinition,
17
+ createSelfReviewDefinition,
18
+ handleAnalyzeDiff,
19
+ handleCreateSelfReview,
20
+ handleRequestPeerReview,
21
+ requestPeerReviewDefinition
22
+ } from "./chunk-4PFMY3H7.js";
23
+ import {
24
+ handleRunSecurityScan,
25
+ runSecurityScanDefinition
26
+ } from "./chunk-PSXF277V.js";
27
+ import {
28
+ handleRunCodeReview,
29
+ runCodeReviewDefinition
30
+ } from "./chunk-PMTFPOCT.js";
31
+ import {
32
+ GENERATED_HEADER_CLAUDE,
33
+ GENERATED_HEADER_GEMINI,
34
+ VALID_PLATFORMS,
35
+ applySyncPlan,
36
+ computeSyncPlan
37
+ } from "./chunk-ZOAWBDWU.js";
38
+ import {
39
+ handleValidateProject,
40
+ validateToolDefinition
41
+ } from "./chunk-FX7SQHGD.js";
42
+ import {
43
+ loadGraphStore
44
+ } from "./chunk-OPXH4CQN.js";
45
+ import {
46
+ checkDependenciesDefinition,
47
+ handleCheckDependencies
48
+ } from "./chunk-MO4YQOMB.js";
49
+ import {
50
+ resolveProjectConfig
51
+ } from "./chunk-K6XAPGML.js";
52
+ import {
53
+ checkDocsDefinition,
54
+ handleCheckDocs
55
+ } from "./chunk-F4PTVZWA.js";
56
+ import {
57
+ resultToMcpResponse
58
+ } from "./chunk-IDZNPTYD.js";
59
+ import {
60
+ sanitizePath
61
+ } from "./chunk-W6Y7ZW3Y.js";
62
+ import {
63
+ resolveGlobalSkillsDir,
64
+ resolvePersonasDir,
65
+ resolveProjectSkillsDir,
66
+ resolveSkillsDir,
67
+ resolveTemplatesDir
68
+ } from "./chunk-EOLRW32Q.js";
69
+ import {
70
+ CLIError,
71
+ ExitCode,
72
+ handleError
73
+ } from "./chunk-B7HFEHWP.js";
74
+ import {
75
+ SkillMetadataSchema
76
+ } from "./chunk-MDUK2J2O.js";
77
+ import {
78
+ Err,
79
+ Ok
80
+ } from "./chunk-MHBMTPW7.js";
81
+
82
+ // src/mcp/server.ts
83
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
84
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
85
+ import {
86
+ CallToolRequestSchema,
87
+ ListToolsRequestSchema,
88
+ ListResourcesRequestSchema,
89
+ ReadResourceRequestSchema
90
+ } from "@modelcontextprotocol/sdk/types.js";
91
+
92
+ // src/mcp/tools/linter.ts
93
+ var generateLinterDefinition = {
94
+ name: "generate_linter",
95
+ description: "Generate an ESLint rule from YAML configuration",
96
+ inputSchema: {
97
+ type: "object",
98
+ properties: {
99
+ configPath: { type: "string", description: "Path to harness-linter.yml" },
100
+ outputDir: { type: "string", description: "Output directory for generated rule" }
101
+ },
102
+ required: ["configPath"]
103
+ }
104
+ };
105
+ async function handleGenerateLinter(input) {
106
+ try {
107
+ const { generate } = await import("./dist-L7LAAQAS.js");
108
+ const result = await generate({
109
+ configPath: sanitizePath(input.configPath),
110
+ ...input.outputDir !== void 0 && { outputDir: sanitizePath(input.outputDir) }
111
+ });
112
+ if ("success" in result && result.success) {
113
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
114
+ }
115
+ return { content: [{ type: "text", text: JSON.stringify(result) }], isError: true };
116
+ } catch (error) {
117
+ return {
118
+ content: [
119
+ {
120
+ type: "text",
121
+ text: `Error: ${error instanceof Error ? error.message : String(error)}`
122
+ }
123
+ ],
124
+ isError: true
125
+ };
126
+ }
127
+ }
128
+ var validateLinterConfigDefinition = {
129
+ name: "validate_linter_config",
130
+ description: "Validate a harness-linter.yml configuration file",
131
+ inputSchema: {
132
+ type: "object",
133
+ properties: {
134
+ configPath: { type: "string", description: "Path to harness-linter.yml" }
135
+ },
136
+ required: ["configPath"]
137
+ }
138
+ };
139
+ async function handleValidateLinterConfig(input) {
140
+ try {
141
+ const { validate } = await import("./dist-L7LAAQAS.js");
142
+ const result = await validate({ configPath: sanitizePath(input.configPath) });
143
+ if ("success" in result && result.success) {
144
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
145
+ }
146
+ return { content: [{ type: "text", text: JSON.stringify(result) }], isError: true };
147
+ } catch (error) {
148
+ return {
149
+ content: [
150
+ {
151
+ type: "text",
152
+ text: `Error: ${error instanceof Error ? error.message : String(error)}`
153
+ }
154
+ ],
155
+ isError: true
156
+ };
157
+ }
158
+ }
159
+
160
+ // src/mcp/tools/init.ts
161
+ import * as path from "path";
162
+ var initProjectDefinition = {
163
+ name: "init_project",
164
+ description: "Scaffold a new harness engineering project from a template",
165
+ inputSchema: {
166
+ type: "object",
167
+ properties: {
168
+ path: { type: "string", description: "Target directory" },
169
+ name: { type: "string", description: "Project name" },
170
+ level: {
171
+ type: "string",
172
+ enum: ["basic", "intermediate", "advanced"],
173
+ description: "Adoption level"
174
+ },
175
+ framework: { type: "string", description: "Framework overlay (e.g., nextjs)" }
176
+ },
177
+ required: ["path"]
178
+ }
179
+ };
180
+ async function handleInitProject(input) {
181
+ try {
182
+ const { TemplateEngine } = await import("./engine-SCMZ3G3E.js");
183
+ const templatesDir = resolveTemplatesDir();
184
+ const engine = new TemplateEngine(templatesDir);
185
+ const level = input.level ?? "basic";
186
+ const resolveResult = engine.resolveTemplate(level, input.framework);
187
+ if (!resolveResult.ok) return resultToMcpResponse(resolveResult);
188
+ const safePath = sanitizePath(input.path);
189
+ const renderResult = engine.render(resolveResult.value, {
190
+ projectName: input.name ?? path.basename(safePath),
191
+ level,
192
+ ...input.framework !== void 0 && { framework: input.framework }
193
+ });
194
+ if (!renderResult.ok) return resultToMcpResponse(renderResult);
195
+ const writeResult = engine.write(renderResult.value, safePath, {
196
+ overwrite: false
197
+ });
198
+ return resultToMcpResponse(writeResult);
199
+ } catch (error) {
200
+ return {
201
+ content: [
202
+ {
203
+ type: "text",
204
+ text: `Init failed: ${error instanceof Error ? error.message : String(error)}`
205
+ }
206
+ ],
207
+ isError: true
208
+ };
209
+ }
210
+ }
211
+
212
+ // src/mcp/tools/persona.ts
213
+ import * as path2 from "path";
214
+ var listPersonasDefinition = {
215
+ name: "list_personas",
216
+ description: "List available agent personas",
217
+ inputSchema: { type: "object", properties: {} }
218
+ };
219
+ async function handleListPersonas() {
220
+ const { listPersonas } = await import("./loader-6S6PVGSF.js");
221
+ const result = listPersonas(resolvePersonasDir());
222
+ return resultToMcpResponse(result);
223
+ }
224
+ var generatePersonaArtifactsDefinition = {
225
+ name: "generate_persona_artifacts",
226
+ description: "Generate runtime config, AGENTS.md fragment, and CI workflow from a persona",
227
+ inputSchema: {
228
+ type: "object",
229
+ properties: {
230
+ name: { type: "string", description: "Persona name (e.g., architecture-enforcer)" },
231
+ only: {
232
+ type: "string",
233
+ enum: ["runtime", "agents-md", "ci"],
234
+ description: "Generate only a specific artifact type"
235
+ }
236
+ },
237
+ required: ["name"]
238
+ }
239
+ };
240
+ async function handleGeneratePersonaArtifacts(input) {
241
+ if (!/^[a-z0-9][a-z0-9._-]*$/i.test(input.name)) {
242
+ return resultToMcpResponse(Err(new Error(`Invalid persona name: ${input.name}`)));
243
+ }
244
+ const { loadPersona } = await import("./loader-6S6PVGSF.js");
245
+ const { generateRuntime } = await import("./runtime-PXIM7UV6.js");
246
+ const { generateAgentsMd } = await import("./agents-md-EMRFLNBC.js");
247
+ const { generateCIWorkflow } = await import("./ci-workflow-ZBBUNTHQ.js");
248
+ const personasDir = resolvePersonasDir();
249
+ const filePath = path2.join(personasDir, `${input.name}.yaml`);
250
+ if (!filePath.startsWith(personasDir)) {
251
+ return resultToMcpResponse(Err(new Error(`Invalid persona path: ${input.name}`)));
252
+ }
253
+ const personaResult = loadPersona(filePath);
254
+ if (!personaResult.ok) return resultToMcpResponse(personaResult);
255
+ const persona = personaResult.value;
256
+ const artifacts = {};
257
+ if (!input.only || input.only === "runtime") {
258
+ const r = generateRuntime(persona);
259
+ if (r.ok) artifacts.runtime = r.value;
260
+ }
261
+ if (!input.only || input.only === "agents-md") {
262
+ const r = generateAgentsMd(persona);
263
+ if (r.ok) artifacts.agentsMd = r.value;
264
+ }
265
+ if (!input.only || input.only === "ci") {
266
+ const r = generateCIWorkflow(persona, "github");
267
+ if (r.ok) artifacts.ciWorkflow = r.value;
268
+ }
269
+ return resultToMcpResponse(Ok(artifacts));
270
+ }
271
+ var runPersonaDefinition = {
272
+ name: "run_persona",
273
+ description: "Execute all steps defined in a persona and return aggregated results",
274
+ inputSchema: {
275
+ type: "object",
276
+ properties: {
277
+ persona: { type: "string", description: "Persona name (e.g., architecture-enforcer)" },
278
+ path: { type: "string", description: "Path to project root" },
279
+ trigger: {
280
+ type: "string",
281
+ enum: [
282
+ "always",
283
+ "on_pr",
284
+ "on_commit",
285
+ "on_review",
286
+ "scheduled",
287
+ "manual",
288
+ "on_plan_approved",
289
+ "auto"
290
+ ],
291
+ description: "Trigger context for step filtering (default: auto)"
292
+ },
293
+ dryRun: { type: "boolean", description: "Preview without side effects" }
294
+ },
295
+ required: ["persona"]
296
+ }
297
+ };
298
+ async function handleRunPersona(input) {
299
+ if (!/^[a-z0-9][a-z0-9._-]*$/i.test(input.persona)) {
300
+ return resultToMcpResponse(Err(new Error(`Invalid persona name: ${input.persona}`)));
301
+ }
302
+ const { loadPersona } = await import("./loader-6S6PVGSF.js");
303
+ const { runPersona } = await import("./runner-VMYLHWOC.js");
304
+ const { executeSkill } = await import("./skill-executor-KVS47DAU.js");
305
+ const personasDir = resolvePersonasDir();
306
+ const filePath = path2.join(personasDir, `${input.persona}.yaml`);
307
+ if (!filePath.startsWith(personasDir)) {
308
+ return resultToMcpResponse(Err(new Error(`Invalid persona path: ${input.persona}`)));
309
+ }
310
+ const personaResult = loadPersona(filePath);
311
+ if (!personaResult.ok) return resultToMcpResponse(personaResult);
312
+ const projectPath = input.path ? sanitizePath(input.path) : process.cwd();
313
+ const trigger = input.trigger ?? "auto";
314
+ const { ALLOWED_PERSONA_COMMANDS } = await import("./constants-5JGUXPEK.js");
315
+ const commandExecutor = async (command) => {
316
+ if (!ALLOWED_PERSONA_COMMANDS.has(command)) {
317
+ return Err(new Error(`Unknown harness command: ${command}`));
318
+ }
319
+ try {
320
+ const { execFileSync } = await import("child_process");
321
+ const args = ["harness", command];
322
+ if (input.dryRun) args.push("--dry-run");
323
+ const output = execFileSync("npx", args, {
324
+ cwd: projectPath,
325
+ stdio: "pipe",
326
+ timeout: personaResult.value.config.timeout
327
+ });
328
+ return Ok(output.toString());
329
+ } catch (error) {
330
+ return Err(
331
+ new Error(`${command} failed: ${error instanceof Error ? error.message : String(error)}`)
332
+ );
333
+ }
334
+ };
335
+ const report = await runPersona(personaResult.value, {
336
+ trigger,
337
+ commandExecutor,
338
+ skillExecutor: executeSkill,
339
+ projectPath
340
+ });
341
+ return resultToMcpResponse(Ok(report));
342
+ }
343
+
344
+ // src/mcp/tools/agent.ts
345
+ var addComponentDefinition = {
346
+ name: "add_component",
347
+ description: "Add a component (layer, doc, or component type) to the project using the harness CLI",
348
+ inputSchema: {
349
+ type: "object",
350
+ properties: {
351
+ path: { type: "string", description: "Path to project root directory" },
352
+ type: {
353
+ type: "string",
354
+ enum: ["layer", "doc", "component"],
355
+ description: "Type of component to add"
356
+ },
357
+ name: { type: "string", description: "Name of the component to add" }
358
+ },
359
+ required: ["path", "type", "name"]
360
+ }
361
+ };
362
+ var COMPONENT_NAME_REGEX = /^[a-z0-9][a-z0-9._-]{0,64}$/i;
363
+ async function handleAddComponent(input) {
364
+ const projectPath = sanitizePath(input.path);
365
+ const ALLOWED_TYPES = /* @__PURE__ */ new Set(["layer", "doc", "component"]);
366
+ if (!ALLOWED_TYPES.has(input.type)) {
367
+ return {
368
+ content: [
369
+ {
370
+ type: "text",
371
+ text: JSON.stringify({ error: `Invalid component type: ${input.type}` })
372
+ }
373
+ ],
374
+ isError: true
375
+ };
376
+ }
377
+ if (!COMPONENT_NAME_REGEX.test(input.name)) {
378
+ return {
379
+ content: [
380
+ {
381
+ type: "text",
382
+ text: JSON.stringify({
383
+ error: `Invalid component name: must match ${COMPONENT_NAME_REGEX} (lowercase alphanumeric, dots, hyphens, underscores, max 65 chars)`
384
+ })
385
+ }
386
+ ],
387
+ isError: true
388
+ };
389
+ }
390
+ try {
391
+ const { execFileSync } = await import("child_process");
392
+ const output = execFileSync("npx", ["harness", "add", input.type, input.name], {
393
+ cwd: projectPath,
394
+ stdio: "pipe"
395
+ });
396
+ return {
397
+ content: [{ type: "text", text: output.toString() }]
398
+ };
399
+ } catch (error) {
400
+ return {
401
+ content: [
402
+ {
403
+ type: "text",
404
+ text: JSON.stringify({
405
+ error: `add_component failed: ${error instanceof Error ? error.message : String(error)}`
406
+ })
407
+ }
408
+ ],
409
+ isError: true
410
+ };
411
+ }
412
+ }
413
+ var runAgentTaskDefinition = {
414
+ name: "run_agent_task",
415
+ description: "Run an agent task using the harness CLI",
416
+ inputSchema: {
417
+ type: "object",
418
+ properties: {
419
+ task: { type: "string", description: "Task to run" },
420
+ path: { type: "string", description: "Path to project root directory" },
421
+ timeout: { type: "number", description: "Timeout in milliseconds" }
422
+ },
423
+ required: ["task"]
424
+ }
425
+ };
426
+ var ALLOWED_AGENT_TASKS = /* @__PURE__ */ new Set(["review", "doc-review", "test-review"]);
427
+ async function handleRunAgentTask(input) {
428
+ if (!ALLOWED_AGENT_TASKS.has(input.task)) {
429
+ return {
430
+ content: [
431
+ {
432
+ type: "text",
433
+ text: JSON.stringify({
434
+ error: `Invalid task: "${input.task}". Allowed tasks: ${[...ALLOWED_AGENT_TASKS].join(", ")}`
435
+ })
436
+ }
437
+ ],
438
+ isError: true
439
+ };
440
+ }
441
+ const projectPath = input.path ? sanitizePath(input.path) : process.cwd();
442
+ try {
443
+ const { execFileSync } = await import("child_process");
444
+ const output = execFileSync("npx", ["harness", "agent", "run", input.task], {
445
+ cwd: projectPath,
446
+ stdio: "pipe",
447
+ timeout: input.timeout
448
+ });
449
+ return {
450
+ content: [{ type: "text", text: output.toString() }]
451
+ };
452
+ } catch (error) {
453
+ return {
454
+ content: [
455
+ {
456
+ type: "text",
457
+ text: JSON.stringify({
458
+ error: `run_agent_task failed: ${error instanceof Error ? error.message : String(error)}`
459
+ })
460
+ }
461
+ ],
462
+ isError: true
463
+ };
464
+ }
465
+ }
466
+
467
+ // src/mcp/tools/skill.ts
468
+ import * as fs from "fs";
469
+ import * as path3 from "path";
470
+ var runSkillDefinition = {
471
+ name: "run_skill",
472
+ description: "Load and return the content of a skill (SKILL.md), optionally with project state context",
473
+ inputSchema: {
474
+ type: "object",
475
+ properties: {
476
+ skill: { type: "string", description: "Skill name (e.g., harness-tdd)" },
477
+ path: { type: "string", description: "Path to project root for state context injection" },
478
+ complexity: {
479
+ type: "string",
480
+ enum: ["auto", "light", "full"],
481
+ description: "Complexity level for scale-adaptive rigor"
482
+ },
483
+ phase: { type: "string", description: "Start at a specific phase (re-entry)" },
484
+ party: { type: "boolean", description: "Enable multi-perspective evaluation" }
485
+ },
486
+ required: ["skill"]
487
+ }
488
+ };
489
+ async function handleRunSkill(input) {
490
+ const skillsDir = resolveSkillsDir();
491
+ if (!/^[a-z0-9][a-z0-9._-]*$/i.test(input.skill)) {
492
+ return resultToMcpResponse(Err(new Error(`Invalid skill name: ${input.skill}`)));
493
+ }
494
+ const skillDir = path3.join(skillsDir, input.skill);
495
+ if (!skillDir.startsWith(skillsDir)) {
496
+ return resultToMcpResponse(Err(new Error(`Invalid skill path: ${input.skill}`)));
497
+ }
498
+ if (!fs.existsSync(skillDir)) {
499
+ return resultToMcpResponse(Err(new Error(`Skill not found: ${input.skill}`)));
500
+ }
501
+ const skillMdPath = path3.join(skillDir, "SKILL.md");
502
+ if (!fs.existsSync(skillMdPath)) {
503
+ return resultToMcpResponse(Err(new Error(`SKILL.md not found for skill: ${input.skill}`)));
504
+ }
505
+ let content = fs.readFileSync(skillMdPath, "utf-8");
506
+ if (input.path) {
507
+ const projectPath = sanitizePath(input.path);
508
+ const stateFile = path3.join(projectPath, ".harness", "state.json");
509
+ if (fs.existsSync(stateFile)) {
510
+ const stateContent = fs.readFileSync(stateFile, "utf-8");
511
+ content += `
512
+
513
+ ---
514
+ ## Project State
515
+ \`\`\`json
516
+ ${stateContent}
517
+ \`\`\`
518
+ `;
519
+ }
520
+ }
521
+ return resultToMcpResponse(Ok(content));
522
+ }
523
+ var createSkillDefinition = {
524
+ name: "create_skill",
525
+ description: "Scaffold a new harness skill with skill.yaml and SKILL.md",
526
+ inputSchema: {
527
+ type: "object",
528
+ properties: {
529
+ path: { type: "string", description: "Path to project root directory" },
530
+ name: { type: "string", description: "Skill name in kebab-case (e.g., my-new-skill)" },
531
+ description: { type: "string", description: "Skill description" },
532
+ cognitiveMode: {
533
+ type: "string",
534
+ enum: [
535
+ "adversarial-reviewer",
536
+ "constructive-architect",
537
+ "meticulous-implementer",
538
+ "diagnostic-investigator",
539
+ "advisory-guide",
540
+ "meticulous-verifier"
541
+ ],
542
+ description: "Cognitive mode (default: constructive-architect)"
543
+ }
544
+ },
545
+ required: ["path", "name", "description"]
546
+ }
547
+ };
548
+ async function handleCreateSkill(input) {
549
+ try {
550
+ const { generateSkillFiles } = await import("./create-skill-LUWO46WF.js");
551
+ const result = generateSkillFiles({
552
+ name: input.name,
553
+ description: input.description,
554
+ cognitiveMode: input.cognitiveMode ?? "constructive-architect",
555
+ outputDir: sanitizePath(input.path)
556
+ });
557
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
558
+ } catch (error) {
559
+ return {
560
+ content: [
561
+ {
562
+ type: "text",
563
+ text: `Error: ${error instanceof Error ? error.message : String(error)}`
564
+ }
565
+ ],
566
+ isError: true
567
+ };
568
+ }
569
+ }
570
+
571
+ // src/mcp/resources/skills.ts
572
+ import * as fs2 from "fs";
573
+ import * as path4 from "path";
574
+ import * as yaml from "yaml";
575
+ async function getSkillsResource(projectRoot) {
576
+ const skillsDir = path4.join(projectRoot, "agents", "skills", "claude-code");
577
+ const skills = [];
578
+ if (!fs2.existsSync(skillsDir)) {
579
+ return JSON.stringify(skills, null, 2);
580
+ }
581
+ const entries = fs2.readdirSync(skillsDir, { withFileTypes: true });
582
+ for (const entry of entries) {
583
+ if (!entry.isDirectory()) continue;
584
+ const skillYamlPath = path4.join(skillsDir, entry.name, "skill.yaml");
585
+ if (!fs2.existsSync(skillYamlPath)) continue;
586
+ try {
587
+ const content = fs2.readFileSync(skillYamlPath, "utf-8");
588
+ const parsed = yaml.parse(content);
589
+ skills.push({
590
+ name: parsed.name,
591
+ description: parsed.description,
592
+ cognitive_mode: parsed.cognitive_mode,
593
+ type: parsed.type,
594
+ triggers: parsed.triggers
595
+ });
596
+ } catch {
597
+ }
598
+ }
599
+ return JSON.stringify(skills, null, 2);
600
+ }
601
+
602
+ // src/mcp/resources/rules.ts
603
+ import * as fs3 from "fs";
604
+ import * as path5 from "path";
605
+ async function getRulesResource(projectRoot) {
606
+ const rules = [];
607
+ const configPath = path5.join(projectRoot, "harness.config.json");
608
+ if (fs3.existsSync(configPath)) {
609
+ try {
610
+ const config = JSON.parse(fs3.readFileSync(configPath, "utf-8"));
611
+ if (config.layers) {
612
+ rules.push({ type: "layer-enforcement", config: config.layers });
613
+ }
614
+ if (config.phaseGates) {
615
+ rules.push({ type: "phase-gates", config: config.phaseGates });
616
+ }
617
+ if (config.rules) {
618
+ rules.push({ type: "custom-rules", config: config.rules });
619
+ }
620
+ } catch {
621
+ }
622
+ }
623
+ const linterPath = path5.join(projectRoot, ".harness", "linter.json");
624
+ if (fs3.existsSync(linterPath)) {
625
+ try {
626
+ const linterConfig = JSON.parse(fs3.readFileSync(linterPath, "utf-8"));
627
+ rules.push({ type: "linter", config: linterConfig });
628
+ } catch {
629
+ }
630
+ }
631
+ return JSON.stringify(rules, null, 2);
632
+ }
633
+
634
+ // src/mcp/resources/project.ts
635
+ import * as fs4 from "fs";
636
+ import * as path6 from "path";
637
+ async function getProjectResource(projectRoot) {
638
+ const agentsPath = path6.join(projectRoot, "AGENTS.md");
639
+ if (fs4.existsSync(agentsPath)) {
640
+ return fs4.readFileSync(agentsPath, "utf-8");
641
+ }
642
+ return "# No AGENTS.md found";
643
+ }
644
+
645
+ // src/mcp/resources/learnings.ts
646
+ import * as fs5 from "fs";
647
+ import * as path7 from "path";
648
+ async function getLearningsResource(projectRoot) {
649
+ const sections = [];
650
+ const reviewPath = path7.join(projectRoot, ".harness", "review-learnings.md");
651
+ if (fs5.existsSync(reviewPath)) {
652
+ sections.push("## Review Learnings\n\n" + fs5.readFileSync(reviewPath, "utf-8"));
653
+ }
654
+ const antiPath = path7.join(projectRoot, ".harness", "anti-patterns.md");
655
+ if (fs5.existsSync(antiPath)) {
656
+ sections.push("## Anti-Pattern Log\n\n" + fs5.readFileSync(antiPath, "utf-8"));
657
+ }
658
+ return sections.length > 0 ? sections.join("\n\n---\n\n") : "No learnings files found.";
659
+ }
660
+
661
+ // src/mcp/tools/state.ts
662
+ var manageStateDefinition = {
663
+ name: "manage_state",
664
+ description: "Manage harness project state: show current state, record learnings/failures, archive failures, reset state, run mechanical gate checks, or save/load session handoff",
665
+ inputSchema: {
666
+ type: "object",
667
+ properties: {
668
+ path: { type: "string", description: "Path to project root" },
669
+ action: {
670
+ type: "string",
671
+ enum: [
672
+ "show",
673
+ "learn",
674
+ "failure",
675
+ "archive",
676
+ "reset",
677
+ "gate",
678
+ "save-handoff",
679
+ "load-handoff"
680
+ ],
681
+ description: "Action to perform"
682
+ },
683
+ learning: { type: "string", description: "Learning text to record (required for learn)" },
684
+ skillName: { type: "string", description: "Skill name associated with the entry" },
685
+ outcome: { type: "string", description: "Outcome associated with the learning" },
686
+ description: { type: "string", description: "Failure description (required for failure)" },
687
+ failureType: { type: "string", description: "Type of failure (required for failure)" },
688
+ handoff: { type: "object", description: "Handoff data to save (required for save-handoff)" },
689
+ stream: {
690
+ type: "string",
691
+ description: "Stream name to target (auto-resolves from branch if omitted)"
692
+ }
693
+ },
694
+ required: ["path", "action"]
695
+ }
696
+ };
697
+ async function handleManageState(input) {
698
+ try {
699
+ const {
700
+ loadState,
701
+ saveState,
702
+ appendLearning,
703
+ appendFailure,
704
+ archiveFailures,
705
+ runMechanicalGate,
706
+ DEFAULT_STATE
707
+ } = await import("./dist-PBTNVK6K.js");
708
+ const projectPath = sanitizePath(input.path);
709
+ switch (input.action) {
710
+ case "show": {
711
+ const result = await loadState(projectPath, input.stream);
712
+ return resultToMcpResponse(result);
713
+ }
714
+ case "learn": {
715
+ if (!input.learning) {
716
+ return {
717
+ content: [
718
+ { type: "text", text: "Error: learning is required for learn action" }
719
+ ],
720
+ isError: true
721
+ };
722
+ }
723
+ const result = await appendLearning(
724
+ projectPath,
725
+ input.learning,
726
+ input.skillName,
727
+ input.outcome,
728
+ input.stream
729
+ );
730
+ if (!result.ok) return resultToMcpResponse(result);
731
+ return resultToMcpResponse(Ok({ recorded: true }));
732
+ }
733
+ case "failure": {
734
+ if (!input.description) {
735
+ return {
736
+ content: [
737
+ { type: "text", text: "Error: description is required for failure action" }
738
+ ],
739
+ isError: true
740
+ };
741
+ }
742
+ if (!input.failureType) {
743
+ return {
744
+ content: [
745
+ {
746
+ type: "text",
747
+ text: "Error: failureType is required for failure action"
748
+ }
749
+ ],
750
+ isError: true
751
+ };
752
+ }
753
+ const result = await appendFailure(
754
+ projectPath,
755
+ input.description,
756
+ input.skillName ?? "unknown",
757
+ input.failureType,
758
+ input.stream
759
+ );
760
+ if (!result.ok) return resultToMcpResponse(result);
761
+ return resultToMcpResponse(Ok({ recorded: true }));
762
+ }
763
+ case "archive": {
764
+ const result = await archiveFailures(projectPath, input.stream);
765
+ if (!result.ok) return resultToMcpResponse(result);
766
+ return resultToMcpResponse(Ok({ archived: true }));
767
+ }
768
+ case "reset": {
769
+ const result = await saveState(projectPath, { ...DEFAULT_STATE }, input.stream);
770
+ if (!result.ok) return resultToMcpResponse(result);
771
+ return resultToMcpResponse(Ok({ reset: true }));
772
+ }
773
+ case "gate": {
774
+ const result = await runMechanicalGate(projectPath);
775
+ return resultToMcpResponse(result);
776
+ }
777
+ case "save-handoff": {
778
+ if (!input.handoff) {
779
+ return {
780
+ content: [
781
+ { type: "text", text: "Error: handoff is required for save-handoff action" }
782
+ ],
783
+ isError: true
784
+ };
785
+ }
786
+ const { saveHandoff } = await import("./dist-PBTNVK6K.js");
787
+ const result = await saveHandoff(
788
+ projectPath,
789
+ input.handoff,
790
+ input.stream
791
+ );
792
+ return resultToMcpResponse(result.ok ? Ok({ saved: true }) : result);
793
+ }
794
+ case "load-handoff": {
795
+ const { loadHandoff } = await import("./dist-PBTNVK6K.js");
796
+ const result = await loadHandoff(projectPath, input.stream);
797
+ return resultToMcpResponse(result);
798
+ }
799
+ default: {
800
+ return {
801
+ content: [{ type: "text", text: `Error: unknown action` }],
802
+ isError: true
803
+ };
804
+ }
805
+ }
806
+ } catch (error) {
807
+ return {
808
+ content: [
809
+ {
810
+ type: "text",
811
+ text: `Error: ${error instanceof Error ? error.message : String(error)}`
812
+ }
813
+ ],
814
+ isError: true
815
+ };
816
+ }
817
+ }
818
+ var listStreamsDefinition = {
819
+ name: "list_streams",
820
+ description: "List known state streams with branch associations and last-active timestamps",
821
+ inputSchema: {
822
+ type: "object",
823
+ properties: {
824
+ path: { type: "string", description: "Path to project root" }
825
+ },
826
+ required: ["path"]
827
+ }
828
+ };
829
+ async function handleListStreams(input) {
830
+ try {
831
+ const { listStreams, loadStreamIndex } = await import("./dist-PBTNVK6K.js");
832
+ const projectPath = sanitizePath(input.path);
833
+ const indexResult = await loadStreamIndex(projectPath);
834
+ const streamsResult = await listStreams(projectPath);
835
+ if (!streamsResult.ok) return resultToMcpResponse(streamsResult);
836
+ return resultToMcpResponse(
837
+ Ok({
838
+ activeStream: indexResult.ok ? indexResult.value.activeStream : null,
839
+ streams: streamsResult.value
840
+ })
841
+ );
842
+ } catch (error) {
843
+ return {
844
+ content: [
845
+ {
846
+ type: "text",
847
+ text: `Error: ${error instanceof Error ? error.message : String(error)}`
848
+ }
849
+ ],
850
+ isError: true
851
+ };
852
+ }
853
+ }
854
+
855
+ // src/mcp/tools/phase-gate.ts
856
+ var checkPhaseGateDefinition = {
857
+ name: "check_phase_gate",
858
+ description: "Verify implementation-to-spec mappings: checks that each implementation file has a corresponding spec document",
859
+ inputSchema: {
860
+ type: "object",
861
+ properties: {
862
+ path: { type: "string", description: "Path to project root directory" }
863
+ },
864
+ required: ["path"]
865
+ }
866
+ };
867
+ async function handleCheckPhaseGate(input) {
868
+ try {
869
+ const { runCheckPhaseGate } = await import("./check-phase-gate-WOKIYGAM.js");
870
+ const result = await runCheckPhaseGate({ cwd: sanitizePath(input.path) });
871
+ if (result.ok) {
872
+ return { content: [{ type: "text", text: JSON.stringify(result.value) }] };
873
+ }
874
+ return { content: [{ type: "text", text: result.error.message }], isError: true };
875
+ } catch (error) {
876
+ return {
877
+ content: [
878
+ {
879
+ type: "text",
880
+ text: `Error: ${error instanceof Error ? error.message : String(error)}`
881
+ }
882
+ ],
883
+ isError: true
884
+ };
885
+ }
886
+ }
887
+
888
+ // src/mcp/tools/cross-check.ts
889
+ import * as path8 from "path";
890
+ var validateCrossCheckDefinition = {
891
+ name: "validate_cross_check",
892
+ description: "Validate plan-to-implementation coverage: checks that specs have plans and plans have implementations, detects staleness",
893
+ inputSchema: {
894
+ type: "object",
895
+ properties: {
896
+ path: { type: "string", description: "Path to project root directory" },
897
+ specsDir: {
898
+ type: "string",
899
+ description: "Specs directory relative to project root (default: docs/specs)"
900
+ },
901
+ plansDir: {
902
+ type: "string",
903
+ description: "Plans directory relative to project root (default: docs/plans)"
904
+ }
905
+ },
906
+ required: ["path"]
907
+ }
908
+ };
909
+ async function handleValidateCrossCheck(input) {
910
+ let projectPath;
911
+ try {
912
+ projectPath = sanitizePath(input.path);
913
+ } catch (error) {
914
+ return {
915
+ content: [
916
+ {
917
+ type: "text",
918
+ text: `Error: ${error instanceof Error ? error.message : String(error)}`
919
+ }
920
+ ],
921
+ isError: true
922
+ };
923
+ }
924
+ try {
925
+ const { runCrossCheck } = await import("./validate-cross-check-WZAX357V.js");
926
+ const specsDir = path8.resolve(projectPath, input.specsDir ?? "docs/specs");
927
+ if (!specsDir.startsWith(projectPath)) {
928
+ return {
929
+ content: [{ type: "text", text: "Error: specsDir escapes project root" }],
930
+ isError: true
931
+ };
932
+ }
933
+ const plansDir = path8.resolve(projectPath, input.plansDir ?? "docs/plans");
934
+ if (!plansDir.startsWith(projectPath)) {
935
+ return {
936
+ content: [{ type: "text", text: "Error: plansDir escapes project root" }],
937
+ isError: true
938
+ };
939
+ }
940
+ const result = await runCrossCheck({
941
+ projectPath,
942
+ specsDir,
943
+ plansDir
944
+ });
945
+ if (result.ok) {
946
+ return { content: [{ type: "text", text: JSON.stringify(result.value) }] };
947
+ }
948
+ return { content: [{ type: "text", text: result.error.message }], isError: true };
949
+ } catch (error) {
950
+ return {
951
+ content: [
952
+ {
953
+ type: "text",
954
+ text: `Error: ${error instanceof Error ? error.message : String(error)}`
955
+ }
956
+ ],
957
+ isError: true
958
+ };
959
+ }
960
+ }
961
+
962
+ // src/commands/generate-slash-commands.ts
963
+ import { Command } from "commander";
964
+ import fs7 from "fs";
965
+ import path10 from "path";
966
+ import os from "os";
967
+ import readline from "readline";
968
+
969
+ // src/slash-commands/normalize.ts
970
+ import fs6 from "fs";
971
+ import path9 from "path";
972
+ import { parse as parse2 } from "yaml";
973
+
974
+ // src/slash-commands/normalize-name.ts
975
+ function normalizeName(skillName) {
976
+ let name = skillName;
977
+ if (name.startsWith("harness-")) {
978
+ name = name.slice("harness-".length);
979
+ }
980
+ name = name.replace(/-harness-/g, "-");
981
+ if (name.endsWith("-harness")) {
982
+ name = name.slice(0, -"-harness".length);
983
+ }
984
+ return name;
985
+ }
986
+
987
+ // src/slash-commands/normalize.ts
988
+ function normalizeSkills(skillSources, platforms) {
989
+ const specs = [];
990
+ const nameMap = /* @__PURE__ */ new Map();
991
+ for (const { dir: skillsDir, source } of skillSources) {
992
+ if (!fs6.existsSync(skillsDir)) continue;
993
+ const entries = fs6.readdirSync(skillsDir, { withFileTypes: true }).filter((d) => d.isDirectory());
994
+ for (const entry of entries) {
995
+ const yamlPath = path9.join(skillsDir, entry.name, "skill.yaml");
996
+ if (!fs6.existsSync(yamlPath)) continue;
997
+ let raw;
998
+ try {
999
+ raw = fs6.readFileSync(yamlPath, "utf-8");
1000
+ } catch {
1001
+ continue;
1002
+ }
1003
+ const parsed = parse2(raw);
1004
+ const result = SkillMetadataSchema.safeParse(parsed);
1005
+ if (!result.success) {
1006
+ console.warn(`Skipping ${entry.name}: invalid skill.yaml`);
1007
+ continue;
1008
+ }
1009
+ const meta = result.data;
1010
+ const matchesPlatform = platforms.some((p) => meta.platforms.includes(p));
1011
+ if (!matchesPlatform) continue;
1012
+ const normalized = normalizeName(meta.name);
1013
+ const existing = nameMap.get(normalized);
1014
+ if (existing) {
1015
+ if (existing.source === source) {
1016
+ throw new Error(
1017
+ `Name collision: skills "${existing.skillName}" and "${meta.name}" both normalize to "${normalized}"`
1018
+ );
1019
+ }
1020
+ continue;
1021
+ }
1022
+ nameMap.set(normalized, { skillName: meta.name, source });
1023
+ const skillMdPath = path9.join(skillsDir, entry.name, "SKILL.md");
1024
+ const skillMdContent = fs6.existsSync(skillMdPath) ? fs6.readFileSync(skillMdPath, "utf-8") : "";
1025
+ const skillMdRelative = path9.relative(
1026
+ process.cwd(),
1027
+ path9.join(skillsDir, entry.name, "SKILL.md")
1028
+ );
1029
+ const skillYamlRelative = path9.relative(
1030
+ process.cwd(),
1031
+ path9.join(skillsDir, entry.name, "skill.yaml")
1032
+ );
1033
+ const args = (meta.cli?.args ?? []).map((a) => ({
1034
+ name: a.name,
1035
+ description: a.description ?? "",
1036
+ required: a.required ?? false
1037
+ }));
1038
+ const tools = [...meta.tools ?? []];
1039
+ if (!tools.includes("Read")) {
1040
+ tools.push("Read");
1041
+ }
1042
+ const contextLines = [];
1043
+ if (meta.cognitive_mode) {
1044
+ contextLines.push(`Cognitive mode: ${meta.cognitive_mode}`);
1045
+ }
1046
+ if (meta.type) {
1047
+ contextLines.push(`Type: ${meta.type}`);
1048
+ }
1049
+ if (meta.state?.persistent) {
1050
+ const files = meta.state.files?.join(", ") ?? "";
1051
+ contextLines.push(`State: persistent${files ? ` (files: ${files})` : ""}`);
1052
+ }
1053
+ const objectiveLines = [meta.description];
1054
+ if (meta.phases && meta.phases.length > 0) {
1055
+ objectiveLines.push("");
1056
+ objectiveLines.push("Phases:");
1057
+ for (const phase of meta.phases) {
1058
+ const req = phase.required !== false ? "" : " (optional)";
1059
+ objectiveLines.push(`- ${phase.name}: ${phase.description}${req}`);
1060
+ }
1061
+ }
1062
+ const executionContextLines = [];
1063
+ if (skillMdContent) {
1064
+ executionContextLines.push(`@${skillMdRelative}`);
1065
+ executionContextLines.push(`@${skillYamlRelative}`);
1066
+ }
1067
+ const processLines = [];
1068
+ if (meta.mcp?.tool) {
1069
+ processLines.push(
1070
+ `1. Try: invoke mcp__harness__${meta.mcp.tool} with skill: "${meta.name}"`
1071
+ );
1072
+ processLines.push(`2. If MCP unavailable: read SKILL.md and follow its workflow directly`);
1073
+ processLines.push(`3. Pass through any arguments provided by the user`);
1074
+ } else {
1075
+ processLines.push(`1. Read SKILL.md and follow its workflow directly`);
1076
+ processLines.push(`2. Pass through any arguments provided by the user`);
1077
+ }
1078
+ specs.push({
1079
+ name: normalized,
1080
+ namespace: "harness",
1081
+ fullName: `harness:${normalized}`,
1082
+ description: meta.description,
1083
+ version: meta.version,
1084
+ ...meta.cognitive_mode ? { cognitiveMode: meta.cognitive_mode } : {},
1085
+ tools,
1086
+ args,
1087
+ skillYamlName: meta.name,
1088
+ sourceDir: entry.name,
1089
+ skillsBaseDir: skillsDir,
1090
+ source,
1091
+ prompt: {
1092
+ context: contextLines.join("\n"),
1093
+ objective: objectiveLines.join("\n"),
1094
+ executionContext: executionContextLines.join("\n"),
1095
+ process: processLines.join("\n")
1096
+ }
1097
+ });
1098
+ }
1099
+ }
1100
+ return specs;
1101
+ }
1102
+
1103
+ // src/slash-commands/argument-hint.ts
1104
+ function buildArgumentHint(args) {
1105
+ return args.map((arg) => arg.required ? `--${arg.name} <${arg.name}>` : `[--${arg.name} <${arg.name}>]`).join(" ");
1106
+ }
1107
+
1108
+ // src/slash-commands/render-claude-code.ts
1109
+ function renderClaudeCode(spec) {
1110
+ const lines = ["---"];
1111
+ lines.push(`name: ${spec.fullName}`);
1112
+ lines.push(`description: ${spec.description}`);
1113
+ const hint = buildArgumentHint(spec.args);
1114
+ if (hint) {
1115
+ lines.push(`argument-hint: "${hint}"`);
1116
+ }
1117
+ if (spec.tools.length > 0) {
1118
+ lines.push("allowed-tools:");
1119
+ for (const tool of spec.tools) {
1120
+ lines.push(` - ${tool}`);
1121
+ }
1122
+ }
1123
+ lines.push("---");
1124
+ lines.push("");
1125
+ lines.push(GENERATED_HEADER_CLAUDE);
1126
+ lines.push("");
1127
+ lines.push("<context>");
1128
+ lines.push(spec.prompt.context);
1129
+ lines.push("</context>");
1130
+ lines.push("");
1131
+ lines.push("<objective>");
1132
+ lines.push(spec.prompt.objective);
1133
+ lines.push("</objective>");
1134
+ lines.push("");
1135
+ if (spec.prompt.executionContext) {
1136
+ lines.push("<execution_context>");
1137
+ lines.push(spec.prompt.executionContext);
1138
+ lines.push("</execution_context>");
1139
+ lines.push("");
1140
+ }
1141
+ lines.push("<process>");
1142
+ lines.push(spec.prompt.process);
1143
+ lines.push("</process>");
1144
+ lines.push("");
1145
+ return lines.join("\n");
1146
+ }
1147
+
1148
+ // src/slash-commands/render-gemini.ts
1149
+ function escapeTomlLiteral(content) {
1150
+ return content.replace(/'''/g, "''\\'''");
1151
+ }
1152
+ function renderGemini(spec, skillMdContent, skillYamlContent) {
1153
+ const lines = [GENERATED_HEADER_GEMINI];
1154
+ const safeDesc = spec.description.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
1155
+ lines.push(`description = "${safeDesc}"`);
1156
+ lines.push("prompt = '''");
1157
+ lines.push("<context>");
1158
+ lines.push(spec.prompt.context);
1159
+ lines.push("</context>");
1160
+ lines.push("");
1161
+ lines.push("<objective>");
1162
+ lines.push(spec.prompt.objective);
1163
+ lines.push("</objective>");
1164
+ lines.push("");
1165
+ if (skillMdContent || skillYamlContent) {
1166
+ lines.push("<execution_context>");
1167
+ if (skillMdContent) {
1168
+ const mdPath = spec.prompt.executionContext.split("\n")[0]?.replace(/^@/, "") ?? "";
1169
+ lines.push(`--- SKILL.md (${mdPath}) ---`);
1170
+ lines.push(escapeTomlLiteral(skillMdContent));
1171
+ lines.push("");
1172
+ }
1173
+ if (skillYamlContent) {
1174
+ const refs = spec.prompt.executionContext.split("\n");
1175
+ const yamlPath = (refs[1] ?? refs[0] ?? "").replace(/^@/, "");
1176
+ lines.push(`--- skill.yaml (${yamlPath}) ---`);
1177
+ lines.push(escapeTomlLiteral(skillYamlContent));
1178
+ }
1179
+ lines.push("</execution_context>");
1180
+ lines.push("");
1181
+ }
1182
+ const geminiProcess = spec.prompt.process.replace(
1183
+ "read SKILL.md and follow its workflow directly",
1184
+ "follow the SKILL.md workflow provided above directly"
1185
+ );
1186
+ lines.push("<process>");
1187
+ lines.push(geminiProcess);
1188
+ lines.push("</process>");
1189
+ lines.push("'''");
1190
+ lines.push("");
1191
+ return lines.join("\n");
1192
+ }
1193
+
1194
+ // src/commands/generate-slash-commands.ts
1195
+ function resolveOutputDir(platform, opts) {
1196
+ if (opts.output) {
1197
+ return path10.join(opts.output, "harness");
1198
+ }
1199
+ if (opts.global) {
1200
+ const home = os.homedir();
1201
+ return platform === "claude-code" ? path10.join(home, ".claude", "commands", "harness") : path10.join(home, ".gemini", "commands", "harness");
1202
+ }
1203
+ return platform === "claude-code" ? path10.join("agents", "commands", "claude-code", "harness") : path10.join("agents", "commands", "gemini-cli", "harness");
1204
+ }
1205
+ function fileExtension(platform) {
1206
+ return platform === "claude-code" ? ".md" : ".toml";
1207
+ }
1208
+ async function confirmDeletion(files) {
1209
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
1210
+ return new Promise((resolve2) => {
1211
+ rl.question(`
1212
+ Remove ${files.length} orphaned command(s)? (y/N) `, (answer) => {
1213
+ rl.close();
1214
+ resolve2(answer.toLowerCase() === "y");
1215
+ });
1216
+ });
1217
+ }
1218
+ function generateSlashCommands(opts) {
1219
+ const skillSources = [];
1220
+ if (opts.skillsDir) {
1221
+ skillSources.push({ dir: opts.skillsDir, source: "project" });
1222
+ } else {
1223
+ const projectDir = resolveProjectSkillsDir();
1224
+ if (projectDir) {
1225
+ skillSources.push({ dir: projectDir, source: "project" });
1226
+ }
1227
+ if (opts.includeGlobal || skillSources.length === 0) {
1228
+ const globalDir = resolveGlobalSkillsDir();
1229
+ if (!projectDir || path10.resolve(globalDir) !== path10.resolve(projectDir)) {
1230
+ skillSources.push({ dir: globalDir, source: "global" });
1231
+ }
1232
+ }
1233
+ }
1234
+ const specs = normalizeSkills(skillSources, opts.platforms);
1235
+ const results = [];
1236
+ for (const platform of opts.platforms) {
1237
+ const outputDir = resolveOutputDir(platform, opts);
1238
+ const ext = fileExtension(platform);
1239
+ const useAbsolutePaths = opts.global;
1240
+ const rendered = /* @__PURE__ */ new Map();
1241
+ for (const spec of specs) {
1242
+ const filename = `${spec.name}${ext}`;
1243
+ if (platform === "claude-code") {
1244
+ const renderSpec = useAbsolutePaths ? {
1245
+ ...spec,
1246
+ prompt: {
1247
+ ...spec.prompt,
1248
+ executionContext: spec.prompt.executionContext.split("\n").map((line) => {
1249
+ if (line.startsWith("@")) {
1250
+ const relPath = line.slice(1);
1251
+ return `@${path10.resolve(relPath)}`;
1252
+ }
1253
+ return line;
1254
+ }).join("\n")
1255
+ }
1256
+ } : spec;
1257
+ rendered.set(filename, renderClaudeCode(renderSpec));
1258
+ } else {
1259
+ const mdPath = path10.join(spec.skillsBaseDir, spec.sourceDir, "SKILL.md");
1260
+ const yamlPath = path10.join(spec.skillsBaseDir, spec.sourceDir, "skill.yaml");
1261
+ const mdContent = fs7.existsSync(mdPath) ? fs7.readFileSync(mdPath, "utf-8") : "";
1262
+ const yamlContent = fs7.existsSync(yamlPath) ? fs7.readFileSync(yamlPath, "utf-8") : "";
1263
+ rendered.set(filename, renderGemini(spec, mdContent, yamlContent));
1264
+ }
1265
+ }
1266
+ const plan = computeSyncPlan(outputDir, rendered);
1267
+ if (!opts.dryRun) {
1268
+ applySyncPlan(outputDir, rendered, plan, false);
1269
+ }
1270
+ results.push({
1271
+ platform,
1272
+ added: plan.added,
1273
+ updated: plan.updated,
1274
+ removed: plan.removed,
1275
+ unchanged: plan.unchanged,
1276
+ outputDir
1277
+ });
1278
+ }
1279
+ return results;
1280
+ }
1281
+ async function handleOrphanDeletion(results, opts) {
1282
+ if (opts.dryRun) return;
1283
+ for (const result of results) {
1284
+ if (result.removed.length === 0) continue;
1285
+ const shouldDelete = opts.yes || await confirmDeletion(result.removed);
1286
+ if (shouldDelete) {
1287
+ for (const filename of result.removed) {
1288
+ const filePath = path10.join(result.outputDir, filename);
1289
+ if (fs7.existsSync(filePath)) {
1290
+ fs7.unlinkSync(filePath);
1291
+ }
1292
+ }
1293
+ }
1294
+ }
1295
+ }
1296
+ function createGenerateSlashCommandsCommand() {
1297
+ return new Command("generate-slash-commands").description(
1298
+ "Generate native slash commands for Claude Code and Gemini CLI from skill metadata"
1299
+ ).option("--platforms <list>", "Target platforms (comma-separated)", "claude-code,gemini-cli").option("--global", "Write to global config directories", false).option("--include-global", "Include built-in global skills alongside project skills", false).option("--output <dir>", "Custom output directory").option("--skills-dir <path>", "Skills directory to scan").option("--dry-run", "Show what would change without writing", false).option("--yes", "Skip deletion confirmation prompts", false).action(async (opts, cmd) => {
1300
+ const globalOpts = cmd.optsWithGlobals();
1301
+ const platforms = opts.platforms.split(",").map((p) => p.trim());
1302
+ for (const p of platforms) {
1303
+ if (!VALID_PLATFORMS.includes(p)) {
1304
+ throw new CLIError(
1305
+ `Invalid platform "${p}". Valid platforms: ${VALID_PLATFORMS.join(", ")}`,
1306
+ ExitCode.VALIDATION_FAILED
1307
+ );
1308
+ }
1309
+ }
1310
+ const generateOpts = {
1311
+ platforms,
1312
+ global: opts.global,
1313
+ includeGlobal: opts.includeGlobal,
1314
+ output: opts.output,
1315
+ skillsDir: opts.skillsDir ?? "",
1316
+ dryRun: opts.dryRun,
1317
+ yes: opts.yes
1318
+ };
1319
+ try {
1320
+ const results = generateSlashCommands(generateOpts);
1321
+ if (globalOpts.json) {
1322
+ console.log(JSON.stringify(results, null, 2));
1323
+ return;
1324
+ }
1325
+ const totalCommands = results.reduce(
1326
+ (sum, r) => sum + r.added.length + r.updated.length + r.unchanged.length,
1327
+ 0
1328
+ );
1329
+ if (totalCommands === 0) {
1330
+ console.log(
1331
+ "\nNo skills found. Use --include-global to include built-in skills, or create a skill with: harness create-skill"
1332
+ );
1333
+ return;
1334
+ }
1335
+ for (const result of results) {
1336
+ console.log(`
1337
+ ${result.platform} \u2192 ${result.outputDir}`);
1338
+ if (result.added.length > 0) {
1339
+ console.log(` + ${result.added.length} new: ${result.added.join(", ")}`);
1340
+ }
1341
+ if (result.updated.length > 0) {
1342
+ console.log(` ~ ${result.updated.length} updated: ${result.updated.join(", ")}`);
1343
+ }
1344
+ if (result.removed.length > 0) {
1345
+ console.log(` - ${result.removed.length} removed: ${result.removed.join(", ")}`);
1346
+ }
1347
+ if (result.unchanged.length > 0) {
1348
+ console.log(` = ${result.unchanged.length} unchanged`);
1349
+ }
1350
+ if (opts.dryRun) {
1351
+ console.log(" (dry run \u2014 no files written)");
1352
+ }
1353
+ }
1354
+ await handleOrphanDeletion(results, { yes: opts.yes, dryRun: opts.dryRun });
1355
+ } catch (error) {
1356
+ handleError(error);
1357
+ }
1358
+ });
1359
+ }
1360
+
1361
+ // src/mcp/tools/generate-slash-commands.ts
1362
+ var generateSlashCommandsDefinition = {
1363
+ name: "generate_slash_commands",
1364
+ description: "Generate native slash commands for Claude Code and Gemini CLI from harness skill metadata",
1365
+ inputSchema: {
1366
+ type: "object",
1367
+ properties: {
1368
+ platforms: {
1369
+ type: "string",
1370
+ description: "Comma-separated platforms: claude-code,gemini-cli (default: both)"
1371
+ },
1372
+ global: {
1373
+ type: "boolean",
1374
+ description: "Write to global config directories (~/.claude/commands/, ~/.gemini/commands/)"
1375
+ },
1376
+ output: {
1377
+ type: "string",
1378
+ description: "Custom output directory"
1379
+ },
1380
+ skillsDir: {
1381
+ type: "string",
1382
+ description: "Skills directory to scan"
1383
+ },
1384
+ includeGlobal: {
1385
+ type: "boolean",
1386
+ description: "Include built-in global skills alongside project skills"
1387
+ },
1388
+ dryRun: {
1389
+ type: "boolean",
1390
+ description: "Show what would change without writing files"
1391
+ }
1392
+ }
1393
+ }
1394
+ };
1395
+ async function handleGenerateSlashCommands(input) {
1396
+ try {
1397
+ const platforms = (input.platforms ?? "claude-code,gemini-cli").split(",").map((p) => p.trim());
1398
+ const results = generateSlashCommands({
1399
+ platforms,
1400
+ global: input.global ?? false,
1401
+ includeGlobal: input.includeGlobal ?? false,
1402
+ ...input.output !== void 0 && { output: sanitizePath(input.output) },
1403
+ skillsDir: input.skillsDir ? sanitizePath(input.skillsDir) : "",
1404
+ dryRun: input.dryRun ?? false,
1405
+ yes: true
1406
+ });
1407
+ return resultToMcpResponse(Ok(JSON.stringify(results, null, 2)));
1408
+ } catch (error) {
1409
+ return resultToMcpResponse(Err(error instanceof Error ? error : new Error(String(error))));
1410
+ }
1411
+ }
1412
+
1413
+ // src/mcp/resources/state.ts
1414
+ async function getStateResource(projectRoot) {
1415
+ try {
1416
+ const { loadState, migrateToStreams } = await import("./dist-PBTNVK6K.js");
1417
+ await migrateToStreams(projectRoot);
1418
+ const result = await loadState(projectRoot);
1419
+ if (result.ok) {
1420
+ return JSON.stringify(result.value, null, 2);
1421
+ }
1422
+ return JSON.stringify({
1423
+ schemaVersion: 1,
1424
+ position: {},
1425
+ decisions: [],
1426
+ blockers: [],
1427
+ progress: {}
1428
+ });
1429
+ } catch {
1430
+ return JSON.stringify({
1431
+ schemaVersion: 1,
1432
+ position: {},
1433
+ decisions: [],
1434
+ blockers: [],
1435
+ progress: {}
1436
+ });
1437
+ }
1438
+ }
1439
+
1440
+ // src/mcp/tools/graph.ts
1441
+ import * as path11 from "path";
1442
+ function graphNotFoundError() {
1443
+ return {
1444
+ content: [
1445
+ {
1446
+ type: "text",
1447
+ text: "No graph found. Run `harness scan` or use `ingest_source` tool first."
1448
+ }
1449
+ ],
1450
+ isError: true
1451
+ };
1452
+ }
1453
+ var queryGraphDefinition = {
1454
+ name: "query_graph",
1455
+ description: "Query the project knowledge graph using ContextQL. Traverses from root nodes outward, filtering by node/edge types.",
1456
+ inputSchema: {
1457
+ type: "object",
1458
+ properties: {
1459
+ path: { type: "string", description: "Path to project root" },
1460
+ rootNodeIds: {
1461
+ type: "array",
1462
+ items: { type: "string" },
1463
+ description: "Node IDs to start traversal from"
1464
+ },
1465
+ maxDepth: { type: "number", description: "Maximum traversal depth (default 3)" },
1466
+ includeTypes: {
1467
+ type: "array",
1468
+ items: { type: "string" },
1469
+ description: "Only include nodes of these types"
1470
+ },
1471
+ excludeTypes: {
1472
+ type: "array",
1473
+ items: { type: "string" },
1474
+ description: "Exclude nodes of these types"
1475
+ },
1476
+ includeEdges: {
1477
+ type: "array",
1478
+ items: { type: "string" },
1479
+ description: "Only traverse edges of these types"
1480
+ },
1481
+ bidirectional: {
1482
+ type: "boolean",
1483
+ description: "Traverse edges in both directions (default false)"
1484
+ },
1485
+ pruneObservability: {
1486
+ type: "boolean",
1487
+ description: "Prune observability nodes like spans/metrics/logs (default true)"
1488
+ },
1489
+ mode: {
1490
+ type: "string",
1491
+ enum: ["summary", "detailed"],
1492
+ description: "Response density: summary returns node/edge counts by type + top 10 nodes by connectivity, detailed returns full arrays. Default: detailed"
1493
+ }
1494
+ },
1495
+ required: ["path", "rootNodeIds"]
1496
+ }
1497
+ };
1498
+ async function handleQueryGraph(input) {
1499
+ try {
1500
+ const projectPath = sanitizePath(input.path);
1501
+ const store = await loadGraphStore(projectPath);
1502
+ if (!store) return graphNotFoundError();
1503
+ const { ContextQL } = await import("./dist-I7DB5VKB.js");
1504
+ const cql = new ContextQL(store);
1505
+ const result = cql.execute({
1506
+ rootNodeIds: input.rootNodeIds,
1507
+ ...input.maxDepth !== void 0 && { maxDepth: input.maxDepth },
1508
+ ...input.includeTypes !== void 0 && {
1509
+ includeTypes: input.includeTypes
1510
+ },
1511
+ ...input.excludeTypes !== void 0 && {
1512
+ excludeTypes: input.excludeTypes
1513
+ },
1514
+ ...input.includeEdges !== void 0 && {
1515
+ includeEdges: input.includeEdges
1516
+ },
1517
+ ...input.bidirectional !== void 0 && { bidirectional: input.bidirectional },
1518
+ ...input.pruneObservability !== void 0 && {
1519
+ pruneObservability: input.pruneObservability
1520
+ }
1521
+ });
1522
+ if (input.mode === "summary") {
1523
+ const nodesByType = {};
1524
+ for (const node of result.nodes) {
1525
+ nodesByType[node.type] = (nodesByType[node.type] ?? 0) + 1;
1526
+ }
1527
+ const edgesByType = {};
1528
+ for (const edge of result.edges) {
1529
+ edgesByType[edge.type] = (edgesByType[edge.type] ?? 0) + 1;
1530
+ }
1531
+ const edgeCounts = /* @__PURE__ */ new Map();
1532
+ for (const edge of result.edges) {
1533
+ edgeCounts.set(edge.from, (edgeCounts.get(edge.from) ?? 0) + 1);
1534
+ edgeCounts.set(edge.to, (edgeCounts.get(edge.to) ?? 0) + 1);
1535
+ }
1536
+ const topNodes = [...edgeCounts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 10).map(([id, connections]) => ({ id, connections }));
1537
+ return {
1538
+ content: [
1539
+ {
1540
+ type: "text",
1541
+ text: JSON.stringify({
1542
+ mode: "summary",
1543
+ totalNodes: result.nodes.length,
1544
+ totalEdges: result.edges.length,
1545
+ nodesByType,
1546
+ edgesByType,
1547
+ topNodes,
1548
+ stats: result.stats
1549
+ })
1550
+ }
1551
+ ]
1552
+ };
1553
+ }
1554
+ return {
1555
+ content: [{ type: "text", text: JSON.stringify(result) }]
1556
+ };
1557
+ } catch (error) {
1558
+ return {
1559
+ content: [
1560
+ {
1561
+ type: "text",
1562
+ text: `Error: ${error instanceof Error ? error.message : String(error)}`
1563
+ }
1564
+ ],
1565
+ isError: true
1566
+ };
1567
+ }
1568
+ }
1569
+ var searchSimilarDefinition = {
1570
+ name: "search_similar",
1571
+ description: "Search the knowledge graph for nodes similar to a query string using keyword and semantic fusion.",
1572
+ inputSchema: {
1573
+ type: "object",
1574
+ properties: {
1575
+ path: { type: "string", description: "Path to project root" },
1576
+ query: { type: "string", description: "Search query string" },
1577
+ topK: { type: "number", description: "Maximum number of results to return (default 10)" },
1578
+ mode: {
1579
+ type: "string",
1580
+ enum: ["summary", "detailed"],
1581
+ description: "Response density: summary returns top 5 results with scores only, detailed returns top 10+ with full metadata. Default: detailed"
1582
+ }
1583
+ },
1584
+ required: ["path", "query"]
1585
+ }
1586
+ };
1587
+ async function handleSearchSimilar(input) {
1588
+ try {
1589
+ const projectPath = sanitizePath(input.path);
1590
+ const store = await loadGraphStore(projectPath);
1591
+ if (!store) return graphNotFoundError();
1592
+ const { FusionLayer } = await import("./dist-I7DB5VKB.js");
1593
+ const fusion = new FusionLayer(store);
1594
+ const results = fusion.search(input.query, input.topK ?? 10);
1595
+ if (input.mode === "summary") {
1596
+ const summaryResults = results.slice(0, 5).map((r) => ({
1597
+ nodeId: r.nodeId,
1598
+ score: r.score
1599
+ }));
1600
+ return {
1601
+ content: [
1602
+ {
1603
+ type: "text",
1604
+ text: JSON.stringify({ mode: "summary", results: summaryResults })
1605
+ }
1606
+ ]
1607
+ };
1608
+ }
1609
+ return {
1610
+ content: [{ type: "text", text: JSON.stringify(results) }]
1611
+ };
1612
+ } catch (error) {
1613
+ return {
1614
+ content: [
1615
+ {
1616
+ type: "text",
1617
+ text: `Error: ${error instanceof Error ? error.message : String(error)}`
1618
+ }
1619
+ ],
1620
+ isError: true
1621
+ };
1622
+ }
1623
+ }
1624
+ var findContextForDefinition = {
1625
+ name: "find_context_for",
1626
+ description: "Find relevant context for a given intent by searching the graph and expanding around top results. Returns assembled context within a token budget.",
1627
+ inputSchema: {
1628
+ type: "object",
1629
+ properties: {
1630
+ path: { type: "string", description: "Path to project root" },
1631
+ intent: { type: "string", description: "Description of what context is needed for" },
1632
+ tokenBudget: {
1633
+ type: "number",
1634
+ description: "Approximate token budget for results (default 4000)"
1635
+ }
1636
+ },
1637
+ required: ["path", "intent"]
1638
+ }
1639
+ };
1640
+ async function handleFindContextFor(input) {
1641
+ try {
1642
+ const projectPath = sanitizePath(input.path);
1643
+ const store = await loadGraphStore(projectPath);
1644
+ if (!store) return graphNotFoundError();
1645
+ const { FusionLayer, ContextQL } = await import("./dist-I7DB5VKB.js");
1646
+ const fusion = new FusionLayer(store);
1647
+ const cql = new ContextQL(store);
1648
+ const tokenBudget = input.tokenBudget ?? 4e3;
1649
+ const charBudget = tokenBudget * 4;
1650
+ const searchResults = fusion.search(input.intent, 10);
1651
+ if (searchResults.length === 0) {
1652
+ return {
1653
+ content: [
1654
+ {
1655
+ type: "text",
1656
+ text: JSON.stringify({ context: [], message: "No relevant nodes found." })
1657
+ }
1658
+ ]
1659
+ };
1660
+ }
1661
+ const contextBlocks = [];
1662
+ let totalChars = 0;
1663
+ for (const result of searchResults) {
1664
+ if (totalChars >= charBudget) break;
1665
+ const expanded = cql.execute({
1666
+ rootNodeIds: [result.nodeId],
1667
+ maxDepth: 2
1668
+ });
1669
+ const blockJson = JSON.stringify({
1670
+ rootNode: result.nodeId,
1671
+ score: result.score,
1672
+ nodes: expanded.nodes,
1673
+ edges: expanded.edges
1674
+ });
1675
+ if (totalChars + blockJson.length > charBudget && contextBlocks.length > 0) {
1676
+ break;
1677
+ }
1678
+ contextBlocks.push({
1679
+ rootNode: result.nodeId,
1680
+ score: result.score,
1681
+ nodes: expanded.nodes,
1682
+ edges: expanded.edges
1683
+ });
1684
+ totalChars += blockJson.length;
1685
+ }
1686
+ return {
1687
+ content: [
1688
+ {
1689
+ type: "text",
1690
+ text: JSON.stringify({
1691
+ intent: input.intent,
1692
+ tokenBudget,
1693
+ blocksReturned: contextBlocks.length,
1694
+ context: contextBlocks
1695
+ })
1696
+ }
1697
+ ]
1698
+ };
1699
+ } catch (error) {
1700
+ return {
1701
+ content: [
1702
+ {
1703
+ type: "text",
1704
+ text: `Error: ${error instanceof Error ? error.message : String(error)}`
1705
+ }
1706
+ ],
1707
+ isError: true
1708
+ };
1709
+ }
1710
+ }
1711
+ var getRelationshipsDefinition = {
1712
+ name: "get_relationships",
1713
+ description: "Get relationships for a specific node in the knowledge graph, with configurable direction and depth.",
1714
+ inputSchema: {
1715
+ type: "object",
1716
+ properties: {
1717
+ path: { type: "string", description: "Path to project root" },
1718
+ nodeId: { type: "string", description: "ID of the node to get relationships for" },
1719
+ direction: {
1720
+ type: "string",
1721
+ enum: ["outbound", "inbound", "both"],
1722
+ description: "Direction of relationships to include (default both)"
1723
+ },
1724
+ depth: { type: "number", description: "Traversal depth (default 1)" },
1725
+ mode: {
1726
+ type: "string",
1727
+ enum: ["summary", "detailed"],
1728
+ description: "Response density: summary returns neighbor counts by type + direct neighbors only, detailed returns full traversal. Default: detailed"
1729
+ }
1730
+ },
1731
+ required: ["path", "nodeId"]
1732
+ }
1733
+ };
1734
+ async function handleGetRelationships(input) {
1735
+ try {
1736
+ const projectPath = sanitizePath(input.path);
1737
+ const store = await loadGraphStore(projectPath);
1738
+ if (!store) return graphNotFoundError();
1739
+ const { ContextQL } = await import("./dist-I7DB5VKB.js");
1740
+ const cql = new ContextQL(store);
1741
+ const direction = input.direction ?? "both";
1742
+ const bidirectional = direction === "both" || direction === "inbound";
1743
+ const result = cql.execute({
1744
+ rootNodeIds: [input.nodeId],
1745
+ maxDepth: input.depth ?? 1,
1746
+ bidirectional
1747
+ });
1748
+ let filteredNodes = result.nodes;
1749
+ let filteredEdges = result.edges;
1750
+ if (direction === "inbound") {
1751
+ filteredEdges = result.edges.filter((e) => e.from !== input.nodeId);
1752
+ const reachableNodeIds = new Set(filteredEdges.map((e) => e.from));
1753
+ reachableNodeIds.add(input.nodeId);
1754
+ filteredNodes = result.nodes.filter((n) => reachableNodeIds.has(n.id));
1755
+ }
1756
+ if (input.mode === "summary") {
1757
+ const neighborsByType = {};
1758
+ for (const node of filteredNodes) {
1759
+ if (node.id === input.nodeId) continue;
1760
+ neighborsByType[node.type] = (neighborsByType[node.type] ?? 0) + 1;
1761
+ }
1762
+ return {
1763
+ content: [
1764
+ {
1765
+ type: "text",
1766
+ text: JSON.stringify({
1767
+ mode: "summary",
1768
+ nodeId: input.nodeId,
1769
+ direction,
1770
+ totalNeighbors: filteredNodes.length - 1,
1771
+ neighborsByType,
1772
+ totalEdges: filteredEdges.length,
1773
+ stats: result.stats
1774
+ })
1775
+ }
1776
+ ]
1777
+ };
1778
+ }
1779
+ return {
1780
+ content: [
1781
+ {
1782
+ type: "text",
1783
+ text: JSON.stringify({
1784
+ nodeId: input.nodeId,
1785
+ direction,
1786
+ depth: input.depth ?? 1,
1787
+ nodes: filteredNodes,
1788
+ edges: filteredEdges,
1789
+ stats: result.stats
1790
+ })
1791
+ }
1792
+ ]
1793
+ };
1794
+ } catch (error) {
1795
+ return {
1796
+ content: [
1797
+ {
1798
+ type: "text",
1799
+ text: `Error: ${error instanceof Error ? error.message : String(error)}`
1800
+ }
1801
+ ],
1802
+ isError: true
1803
+ };
1804
+ }
1805
+ }
1806
+ var getImpactDefinition = {
1807
+ name: "get_impact",
1808
+ description: "Analyze the impact of changing a node or file. Returns affected tests, docs, code, and other nodes grouped by type.",
1809
+ inputSchema: {
1810
+ type: "object",
1811
+ properties: {
1812
+ path: { type: "string", description: "Path to project root" },
1813
+ nodeId: { type: "string", description: "ID of the node to analyze impact for" },
1814
+ filePath: {
1815
+ type: "string",
1816
+ description: "File path (relative to project root) to analyze impact for"
1817
+ },
1818
+ mode: {
1819
+ type: "string",
1820
+ enum: ["summary", "detailed"],
1821
+ description: "Response density: summary returns impacted file count by category + highest-risk items, detailed returns full impact tree. Default: detailed"
1822
+ }
1823
+ },
1824
+ required: ["path"]
1825
+ }
1826
+ };
1827
+ async function handleGetImpact(input) {
1828
+ try {
1829
+ if (!input.nodeId && !input.filePath) {
1830
+ return {
1831
+ content: [
1832
+ {
1833
+ type: "text",
1834
+ text: "Error: either nodeId or filePath is required"
1835
+ }
1836
+ ],
1837
+ isError: true
1838
+ };
1839
+ }
1840
+ const projectPath = sanitizePath(input.path);
1841
+ const store = await loadGraphStore(projectPath);
1842
+ if (!store) return graphNotFoundError();
1843
+ const { ContextQL } = await import("./dist-I7DB5VKB.js");
1844
+ let targetNodeId = input.nodeId;
1845
+ if (!targetNodeId && input.filePath) {
1846
+ const fileNodes = store.findNodes({ type: "file" });
1847
+ const match = fileNodes.find(
1848
+ (n) => n.path === input.filePath || n.id === `file:${input.filePath}`
1849
+ );
1850
+ if (!match) {
1851
+ return {
1852
+ content: [
1853
+ {
1854
+ type: "text",
1855
+ text: `Error: no file node found matching path "${input.filePath}"`
1856
+ }
1857
+ ],
1858
+ isError: true
1859
+ };
1860
+ }
1861
+ targetNodeId = match.id;
1862
+ }
1863
+ const cql = new ContextQL(store);
1864
+ const result = cql.execute({
1865
+ rootNodeIds: [targetNodeId],
1866
+ bidirectional: true,
1867
+ maxDepth: 3
1868
+ });
1869
+ const groups = {
1870
+ tests: [],
1871
+ docs: [],
1872
+ code: [],
1873
+ other: []
1874
+ };
1875
+ const testTypes = /* @__PURE__ */ new Set(["test_result"]);
1876
+ const docTypes = /* @__PURE__ */ new Set(["adr", "decision", "document", "learning"]);
1877
+ const codeTypes = /* @__PURE__ */ new Set([
1878
+ "file",
1879
+ "module",
1880
+ "class",
1881
+ "interface",
1882
+ "function",
1883
+ "method",
1884
+ "variable"
1885
+ ]);
1886
+ for (const node of result.nodes) {
1887
+ if (node.id === targetNodeId) continue;
1888
+ if (testTypes.has(node.type)) {
1889
+ groups["tests"].push(node);
1890
+ } else if (docTypes.has(node.type)) {
1891
+ groups["docs"].push(node);
1892
+ } else if (codeTypes.has(node.type)) {
1893
+ groups["code"].push(node);
1894
+ } else {
1895
+ groups["other"].push(node);
1896
+ }
1897
+ }
1898
+ if (input.mode === "summary") {
1899
+ const highestRiskItems = [
1900
+ ...groups["tests"].slice(0, 2),
1901
+ ...groups["code"].slice(0, 2),
1902
+ ...groups["docs"].slice(0, 2)
1903
+ ].map((n) => {
1904
+ const node = n;
1905
+ return { id: node.id, type: node.type };
1906
+ });
1907
+ return {
1908
+ content: [
1909
+ {
1910
+ type: "text",
1911
+ text: JSON.stringify({
1912
+ mode: "summary",
1913
+ targetNodeId,
1914
+ impactCounts: {
1915
+ tests: groups["tests"].length,
1916
+ docs: groups["docs"].length,
1917
+ code: groups["code"].length,
1918
+ other: groups["other"].length
1919
+ },
1920
+ highestRiskItems,
1921
+ stats: result.stats
1922
+ })
1923
+ }
1924
+ ]
1925
+ };
1926
+ }
1927
+ return {
1928
+ content: [
1929
+ {
1930
+ type: "text",
1931
+ text: JSON.stringify({
1932
+ targetNodeId,
1933
+ impact: groups,
1934
+ stats: result.stats,
1935
+ edges: result.edges
1936
+ })
1937
+ }
1938
+ ]
1939
+ };
1940
+ } catch (error) {
1941
+ return {
1942
+ content: [
1943
+ {
1944
+ type: "text",
1945
+ text: `Error: ${error instanceof Error ? error.message : String(error)}`
1946
+ }
1947
+ ],
1948
+ isError: true
1949
+ };
1950
+ }
1951
+ }
1952
+ var ingestSourceDefinition = {
1953
+ name: "ingest_source",
1954
+ description: "Ingest sources into the project knowledge graph. Supports code analysis, knowledge documents, git history, or all at once.",
1955
+ inputSchema: {
1956
+ type: "object",
1957
+ properties: {
1958
+ path: { type: "string", description: "Path to project root" },
1959
+ source: {
1960
+ type: "string",
1961
+ enum: ["code", "knowledge", "git", "all"],
1962
+ description: "Type of source to ingest"
1963
+ }
1964
+ },
1965
+ required: ["path", "source"]
1966
+ }
1967
+ };
1968
+ async function handleIngestSource(input) {
1969
+ try {
1970
+ const projectPath = sanitizePath(input.path);
1971
+ const graphDir = path11.join(projectPath, ".harness", "graph");
1972
+ const { GraphStore, CodeIngestor, TopologicalLinker, KnowledgeIngestor, GitIngestor } = await import("./dist-I7DB5VKB.js");
1973
+ const fs10 = await import("fs/promises");
1974
+ await fs10.mkdir(graphDir, { recursive: true });
1975
+ const store = new GraphStore();
1976
+ await store.load(graphDir);
1977
+ const results = [];
1978
+ if (input.source === "code" || input.source === "all") {
1979
+ const codeIngestor = new CodeIngestor(store);
1980
+ const codeResult = await codeIngestor.ingest(projectPath);
1981
+ results.push(codeResult);
1982
+ const linker = new TopologicalLinker(store);
1983
+ linker.link();
1984
+ }
1985
+ if (input.source === "knowledge" || input.source === "all") {
1986
+ const knowledgeIngestor = new KnowledgeIngestor(store);
1987
+ const knowledgeResult = await knowledgeIngestor.ingestAll(projectPath);
1988
+ results.push(knowledgeResult);
1989
+ }
1990
+ if (input.source === "git" || input.source === "all") {
1991
+ const gitIngestor = new GitIngestor(store);
1992
+ const gitResult = await gitIngestor.ingest(projectPath);
1993
+ results.push(gitResult);
1994
+ }
1995
+ await store.save(graphDir);
1996
+ const combined = {
1997
+ nodesAdded: results.reduce((s, r) => s + r.nodesAdded, 0),
1998
+ nodesUpdated: results.reduce((s, r) => s + r.nodesUpdated, 0),
1999
+ edgesAdded: results.reduce((s, r) => s + r.edgesAdded, 0),
2000
+ edgesUpdated: results.reduce((s, r) => s + r.edgesUpdated, 0),
2001
+ errors: results.flatMap((r) => r.errors),
2002
+ durationMs: results.reduce((s, r) => s + r.durationMs, 0),
2003
+ graphStats: {
2004
+ totalNodes: store.nodeCount,
2005
+ totalEdges: store.edgeCount
2006
+ }
2007
+ };
2008
+ return {
2009
+ content: [{ type: "text", text: JSON.stringify(combined) }]
2010
+ };
2011
+ } catch (error) {
2012
+ return {
2013
+ content: [
2014
+ {
2015
+ type: "text",
2016
+ text: `Error: ${error instanceof Error ? error.message : String(error)}`
2017
+ }
2018
+ ],
2019
+ isError: true
2020
+ };
2021
+ }
2022
+ }
2023
+ var detectAnomaliesDefinition = {
2024
+ name: "detect_anomalies",
2025
+ description: "Detect structural anomalies \u2014 statistical outliers across code metrics and topological single points of failure in the import graph",
2026
+ inputSchema: {
2027
+ type: "object",
2028
+ properties: {
2029
+ path: { type: "string", description: "Path to project root" },
2030
+ threshold: { type: "number", description: "Z-score threshold (default 2.0)" },
2031
+ metrics: {
2032
+ type: "array",
2033
+ items: { type: "string" },
2034
+ description: "Metrics to analyze (default: cyclomaticComplexity, fanIn, fanOut, hotspotScore, transitiveDepth)"
2035
+ }
2036
+ },
2037
+ required: ["path"]
2038
+ }
2039
+ };
2040
+ async function handleDetectAnomalies(input) {
2041
+ try {
2042
+ const projectPath = sanitizePath(input.path);
2043
+ const store = await loadGraphStore(projectPath);
2044
+ if (!store) return graphNotFoundError();
2045
+ const { GraphAnomalyAdapter } = await import("./dist-I7DB5VKB.js");
2046
+ const adapter = new GraphAnomalyAdapter(store);
2047
+ const report = adapter.detect({
2048
+ ...input.threshold !== void 0 && { threshold: input.threshold },
2049
+ ...input.metrics !== void 0 && { metrics: input.metrics }
2050
+ });
2051
+ return {
2052
+ content: [{ type: "text", text: JSON.stringify(report) }]
2053
+ };
2054
+ } catch (error) {
2055
+ return {
2056
+ content: [
2057
+ {
2058
+ type: "text",
2059
+ text: `Error: ${error instanceof Error ? error.message : String(error)}`
2060
+ }
2061
+ ],
2062
+ isError: true
2063
+ };
2064
+ }
2065
+ }
2066
+
2067
+ // src/mcp/resources/graph.ts
2068
+ import * as fs8 from "fs/promises";
2069
+ import * as path12 from "path";
2070
+ var MAX_ITEMS = 5e3;
2071
+ function formatStaleness(isoTimestamp) {
2072
+ const then = new Date(isoTimestamp).getTime();
2073
+ const now = Date.now();
2074
+ const diffMs = now - then;
2075
+ const seconds = Math.floor(diffMs / 1e3);
2076
+ const minutes = Math.floor(seconds / 60);
2077
+ const hours = Math.floor(minutes / 60);
2078
+ const days = Math.floor(hours / 24);
2079
+ if (days > 0) return `${days} day${days === 1 ? "" : "s"} ago`;
2080
+ if (hours > 0) return `${hours} hour${hours === 1 ? "" : "s"} ago`;
2081
+ if (minutes > 0) return `${minutes} minute${minutes === 1 ? "" : "s"} ago`;
2082
+ return "just now";
2083
+ }
2084
+ async function getGraphResource(projectRoot) {
2085
+ const store = await loadGraphStore(projectRoot);
2086
+ if (!store) {
2087
+ return JSON.stringify({
2088
+ status: "no_graph",
2089
+ message: "No knowledge graph found. Run harness scan to build one."
2090
+ });
2091
+ }
2092
+ const graphDir = path12.join(projectRoot, ".harness", "graph");
2093
+ const metadataPath = path12.join(graphDir, "metadata.json");
2094
+ let lastScanTimestamp = null;
2095
+ try {
2096
+ const raw = JSON.parse(await fs8.readFile(metadataPath, "utf-8"));
2097
+ lastScanTimestamp = raw.lastScanTimestamp ?? null;
2098
+ } catch {
2099
+ }
2100
+ const allNodes = store.findNodes({});
2101
+ const allEdges = store.getEdges({});
2102
+ const nodesByType = {};
2103
+ for (const node of allNodes) {
2104
+ nodesByType[node.type] = (nodesByType[node.type] ?? 0) + 1;
2105
+ }
2106
+ const edgesByType = {};
2107
+ for (const edge of allEdges) {
2108
+ edgesByType[edge.type] = (edgesByType[edge.type] ?? 0) + 1;
2109
+ }
2110
+ let status = "ok";
2111
+ let staleness = "unknown";
2112
+ if (lastScanTimestamp) {
2113
+ const ageMs = Date.now() - new Date(lastScanTimestamp).getTime();
2114
+ const twentyFourHoursMs = 24 * 60 * 60 * 1e3;
2115
+ if (ageMs > twentyFourHoursMs) {
2116
+ status = "stale";
2117
+ }
2118
+ staleness = formatStaleness(lastScanTimestamp);
2119
+ }
2120
+ return JSON.stringify({
2121
+ status,
2122
+ nodeCount: store.nodeCount,
2123
+ edgeCount: store.edgeCount,
2124
+ nodesByType,
2125
+ edgesByType,
2126
+ lastScanTimestamp,
2127
+ staleness
2128
+ });
2129
+ }
2130
+ async function getEntitiesResource(projectRoot) {
2131
+ const store = await loadGraphStore(projectRoot);
2132
+ if (!store) {
2133
+ return "[]";
2134
+ }
2135
+ const nodes = store.findNodes({});
2136
+ const entities = nodes.slice(0, MAX_ITEMS).map((n) => ({
2137
+ id: n.id,
2138
+ type: n.type,
2139
+ name: n.name,
2140
+ path: n.path,
2141
+ metadata: n.metadata
2142
+ }));
2143
+ if (nodes.length > MAX_ITEMS) {
2144
+ return JSON.stringify({ entities, _truncated: true, _total: nodes.length }, null, 2);
2145
+ }
2146
+ return JSON.stringify(entities);
2147
+ }
2148
+ async function getRelationshipsResource(projectRoot) {
2149
+ const store = await loadGraphStore(projectRoot);
2150
+ if (!store) {
2151
+ return "[]";
2152
+ }
2153
+ const edges = store.getEdges({});
2154
+ const relationships = edges.slice(0, MAX_ITEMS).map((e) => ({
2155
+ from: e.from,
2156
+ to: e.to,
2157
+ type: e.type,
2158
+ confidence: e.confidence,
2159
+ metadata: e.metadata
2160
+ }));
2161
+ if (edges.length > MAX_ITEMS) {
2162
+ return JSON.stringify({ relationships, _truncated: true, _total: edges.length }, null, 2);
2163
+ }
2164
+ return JSON.stringify(relationships);
2165
+ }
2166
+
2167
+ // src/mcp/tools/agent-definitions.ts
2168
+ var generateAgentDefinitionsDefinition = {
2169
+ name: "generate_agent_definitions",
2170
+ description: "Generate agent definition files from personas for Claude Code and Gemini CLI",
2171
+ inputSchema: {
2172
+ type: "object",
2173
+ properties: {
2174
+ global: { type: "boolean", description: "Write to global agent directory" },
2175
+ platform: {
2176
+ type: "string",
2177
+ enum: ["claude-code", "gemini-cli", "all"],
2178
+ description: "Target platform (default: all)"
2179
+ },
2180
+ dryRun: { type: "boolean", description: "Preview without writing" }
2181
+ }
2182
+ }
2183
+ };
2184
+ async function handleGenerateAgentDefinitions(input) {
2185
+ const { generateAgentDefinitions } = await import("./generate-agent-definitions-BU5LOJTI.js");
2186
+ const platforms = input.platform === "all" || !input.platform ? ["claude-code", "gemini-cli"] : [input.platform];
2187
+ const results = generateAgentDefinitions({
2188
+ platforms: [...platforms],
2189
+ global: input.global ?? false,
2190
+ dryRun: input.dryRun ?? false
2191
+ });
2192
+ return resultToMcpResponse(Ok(results));
2193
+ }
2194
+
2195
+ // src/mcp/tools/roadmap.ts
2196
+ import * as fs9 from "fs";
2197
+ import * as path13 from "path";
2198
+ var manageRoadmapDefinition = {
2199
+ name: "manage_roadmap",
2200
+ description: "Manage the project roadmap: show, add, update, remove, sync features, or query by filter. Reads and writes docs/roadmap.md.",
2201
+ inputSchema: {
2202
+ type: "object",
2203
+ properties: {
2204
+ path: { type: "string", description: "Path to project root" },
2205
+ action: {
2206
+ type: "string",
2207
+ enum: ["show", "add", "update", "remove", "query", "sync"],
2208
+ description: "Action to perform"
2209
+ },
2210
+ feature: { type: "string", description: "Feature name (required for add, update, remove)" },
2211
+ milestone: {
2212
+ type: "string",
2213
+ description: "Milestone name (required for add; optional filter for show)"
2214
+ },
2215
+ status: {
2216
+ type: "string",
2217
+ enum: ["backlog", "planned", "in-progress", "done", "blocked"],
2218
+ description: "Feature status (required for add; optional for update; optional filter for show)"
2219
+ },
2220
+ summary: {
2221
+ type: "string",
2222
+ description: "Feature summary (required for add; optional for update)"
2223
+ },
2224
+ spec: { type: "string", description: "Spec file path (optional for add/update)" },
2225
+ plans: {
2226
+ type: "array",
2227
+ items: { type: "string" },
2228
+ description: "Plan file paths (optional for add/update)"
2229
+ },
2230
+ blocked_by: {
2231
+ type: "array",
2232
+ items: { type: "string" },
2233
+ description: "Blocking feature names (optional for add/update)"
2234
+ },
2235
+ filter: {
2236
+ type: "string",
2237
+ description: 'Query filter: "blocked", "in-progress", "done", "planned", "backlog", or "milestone:<name>" (required for query)'
2238
+ },
2239
+ apply: {
2240
+ type: "boolean",
2241
+ description: "For sync action: apply proposed changes (default: false, preview only)"
2242
+ },
2243
+ force_sync: {
2244
+ type: "boolean",
2245
+ description: "For sync action: override human-always-wins rule"
2246
+ }
2247
+ },
2248
+ required: ["path", "action"]
2249
+ }
2250
+ };
2251
+ function roadmapPath(projectRoot) {
2252
+ return path13.join(projectRoot, "docs", "roadmap.md");
2253
+ }
2254
+ function readRoadmapFile(projectRoot) {
2255
+ const filePath = roadmapPath(projectRoot);
2256
+ try {
2257
+ return fs9.readFileSync(filePath, "utf-8");
2258
+ } catch {
2259
+ return null;
2260
+ }
2261
+ }
2262
+ function writeRoadmapFile(projectRoot, content) {
2263
+ const filePath = roadmapPath(projectRoot);
2264
+ const dir = path13.dirname(filePath);
2265
+ fs9.mkdirSync(dir, { recursive: true });
2266
+ fs9.writeFileSync(filePath, content, "utf-8");
2267
+ }
2268
+ async function handleManageRoadmap(input) {
2269
+ try {
2270
+ const { parseRoadmap, serializeRoadmap, syncRoadmap } = await import("./dist-PBTNVK6K.js");
2271
+ const { Ok: Ok2 } = await import("./dist-D4RYGUZE.js");
2272
+ const projectPath = sanitizePath(input.path);
2273
+ switch (input.action) {
2274
+ case "show": {
2275
+ const raw = readRoadmapFile(projectPath);
2276
+ if (raw === null) {
2277
+ return {
2278
+ content: [
2279
+ {
2280
+ type: "text",
2281
+ text: "Error: docs/roadmap.md not found. Create a roadmap first."
2282
+ }
2283
+ ],
2284
+ isError: true
2285
+ };
2286
+ }
2287
+ const result = parseRoadmap(raw);
2288
+ if (!result.ok) return resultToMcpResponse(result);
2289
+ let roadmap = result.value;
2290
+ if (input.milestone) {
2291
+ const milestoneFilter = input.milestone;
2292
+ roadmap = {
2293
+ ...roadmap,
2294
+ milestones: roadmap.milestones.filter(
2295
+ (m) => m.name.toLowerCase() === milestoneFilter.toLowerCase()
2296
+ )
2297
+ };
2298
+ }
2299
+ if (input.status) {
2300
+ const statusFilter = input.status;
2301
+ roadmap = {
2302
+ ...roadmap,
2303
+ milestones: roadmap.milestones.map((m) => ({
2304
+ ...m,
2305
+ features: m.features.filter((f) => f.status === statusFilter)
2306
+ })).filter((m) => m.features.length > 0)
2307
+ };
2308
+ }
2309
+ return resultToMcpResponse(Ok2(roadmap));
2310
+ }
2311
+ case "add": {
2312
+ if (!input.feature) {
2313
+ return {
2314
+ content: [{ type: "text", text: "Error: feature is required for add action" }],
2315
+ isError: true
2316
+ };
2317
+ }
2318
+ if (!input.milestone) {
2319
+ return {
2320
+ content: [
2321
+ { type: "text", text: "Error: milestone is required for add action" }
2322
+ ],
2323
+ isError: true
2324
+ };
2325
+ }
2326
+ if (!input.status) {
2327
+ return {
2328
+ content: [{ type: "text", text: "Error: status is required for add action" }],
2329
+ isError: true
2330
+ };
2331
+ }
2332
+ if (!input.summary) {
2333
+ return {
2334
+ content: [{ type: "text", text: "Error: summary is required for add action" }],
2335
+ isError: true
2336
+ };
2337
+ }
2338
+ const raw = readRoadmapFile(projectPath);
2339
+ if (raw === null) {
2340
+ return {
2341
+ content: [
2342
+ {
2343
+ type: "text",
2344
+ text: "Error: docs/roadmap.md not found. Create a roadmap first."
2345
+ }
2346
+ ],
2347
+ isError: true
2348
+ };
2349
+ }
2350
+ const result = parseRoadmap(raw);
2351
+ if (!result.ok) return resultToMcpResponse(result);
2352
+ const roadmap = result.value;
2353
+ const milestone = roadmap.milestones.find(
2354
+ (m) => m.name.toLowerCase() === input.milestone.toLowerCase()
2355
+ );
2356
+ if (!milestone) {
2357
+ return {
2358
+ content: [
2359
+ { type: "text", text: `Error: milestone "${input.milestone}" not found` }
2360
+ ],
2361
+ isError: true
2362
+ };
2363
+ }
2364
+ milestone.features.push({
2365
+ name: input.feature,
2366
+ status: input.status,
2367
+ spec: input.spec ?? null,
2368
+ plans: input.plans ?? [],
2369
+ blockedBy: input.blocked_by ?? [],
2370
+ summary: input.summary
2371
+ });
2372
+ roadmap.frontmatter.lastManualEdit = (/* @__PURE__ */ new Date()).toISOString();
2373
+ writeRoadmapFile(projectPath, serializeRoadmap(roadmap));
2374
+ return resultToMcpResponse(Ok2(roadmap));
2375
+ }
2376
+ case "update": {
2377
+ if (!input.feature) {
2378
+ return {
2379
+ content: [
2380
+ { type: "text", text: "Error: feature is required for update action" }
2381
+ ],
2382
+ isError: true
2383
+ };
2384
+ }
2385
+ const raw = readRoadmapFile(projectPath);
2386
+ if (raw === null) {
2387
+ return {
2388
+ content: [
2389
+ {
2390
+ type: "text",
2391
+ text: "Error: docs/roadmap.md not found. Create a roadmap first."
2392
+ }
2393
+ ],
2394
+ isError: true
2395
+ };
2396
+ }
2397
+ const result = parseRoadmap(raw);
2398
+ if (!result.ok) return resultToMcpResponse(result);
2399
+ const roadmap = result.value;
2400
+ let found = false;
2401
+ for (const m of roadmap.milestones) {
2402
+ const feature = m.features.find(
2403
+ (f) => f.name.toLowerCase() === input.feature.toLowerCase()
2404
+ );
2405
+ if (feature) {
2406
+ if (input.status) feature.status = input.status;
2407
+ if (input.summary !== void 0) feature.summary = input.summary;
2408
+ if (input.spec !== void 0) feature.spec = input.spec || null;
2409
+ if (input.plans !== void 0) feature.plans = input.plans;
2410
+ if (input.blocked_by !== void 0) feature.blockedBy = input.blocked_by;
2411
+ found = true;
2412
+ break;
2413
+ }
2414
+ }
2415
+ if (!found) {
2416
+ return {
2417
+ content: [
2418
+ { type: "text", text: `Error: feature "${input.feature}" not found` }
2419
+ ],
2420
+ isError: true
2421
+ };
2422
+ }
2423
+ roadmap.frontmatter.lastManualEdit = (/* @__PURE__ */ new Date()).toISOString();
2424
+ writeRoadmapFile(projectPath, serializeRoadmap(roadmap));
2425
+ return resultToMcpResponse(Ok2(roadmap));
2426
+ }
2427
+ case "remove": {
2428
+ if (!input.feature) {
2429
+ return {
2430
+ content: [
2431
+ { type: "text", text: "Error: feature is required for remove action" }
2432
+ ],
2433
+ isError: true
2434
+ };
2435
+ }
2436
+ const raw = readRoadmapFile(projectPath);
2437
+ if (raw === null) {
2438
+ return {
2439
+ content: [
2440
+ {
2441
+ type: "text",
2442
+ text: "Error: docs/roadmap.md not found. Create a roadmap first."
2443
+ }
2444
+ ],
2445
+ isError: true
2446
+ };
2447
+ }
2448
+ const result = parseRoadmap(raw);
2449
+ if (!result.ok) return resultToMcpResponse(result);
2450
+ const roadmap = result.value;
2451
+ let found = false;
2452
+ for (const m of roadmap.milestones) {
2453
+ const idx = m.features.findIndex(
2454
+ (f) => f.name.toLowerCase() === input.feature.toLowerCase()
2455
+ );
2456
+ if (idx !== -1) {
2457
+ m.features.splice(idx, 1);
2458
+ found = true;
2459
+ break;
2460
+ }
2461
+ }
2462
+ if (!found) {
2463
+ return {
2464
+ content: [
2465
+ { type: "text", text: `Error: feature "${input.feature}" not found` }
2466
+ ],
2467
+ isError: true
2468
+ };
2469
+ }
2470
+ roadmap.frontmatter.lastManualEdit = (/* @__PURE__ */ new Date()).toISOString();
2471
+ writeRoadmapFile(projectPath, serializeRoadmap(roadmap));
2472
+ return resultToMcpResponse(Ok2(roadmap));
2473
+ }
2474
+ case "query": {
2475
+ if (!input.filter) {
2476
+ return {
2477
+ content: [
2478
+ { type: "text", text: "Error: filter is required for query action" }
2479
+ ],
2480
+ isError: true
2481
+ };
2482
+ }
2483
+ const raw = readRoadmapFile(projectPath);
2484
+ if (raw === null) {
2485
+ return {
2486
+ content: [
2487
+ {
2488
+ type: "text",
2489
+ text: "Error: docs/roadmap.md not found. Create a roadmap first."
2490
+ }
2491
+ ],
2492
+ isError: true
2493
+ };
2494
+ }
2495
+ const result = parseRoadmap(raw);
2496
+ if (!result.ok) return resultToMcpResponse(result);
2497
+ const roadmap = result.value;
2498
+ const allFeatures = roadmap.milestones.flatMap(
2499
+ (m) => m.features.map((f) => ({ ...f, milestone: m.name }))
2500
+ );
2501
+ const filter = input.filter.toLowerCase();
2502
+ let filtered;
2503
+ if (filter.startsWith("milestone:")) {
2504
+ const milestoneName = filter.slice("milestone:".length).trim();
2505
+ filtered = allFeatures.filter((f) => f.milestone.toLowerCase().includes(milestoneName));
2506
+ } else {
2507
+ filtered = allFeatures.filter((f) => f.status === filter);
2508
+ }
2509
+ return resultToMcpResponse(Ok2(filtered));
2510
+ }
2511
+ case "sync": {
2512
+ const raw = readRoadmapFile(projectPath);
2513
+ if (raw === null) {
2514
+ return {
2515
+ content: [
2516
+ {
2517
+ type: "text",
2518
+ text: "Error: docs/roadmap.md not found. Create a roadmap first."
2519
+ }
2520
+ ],
2521
+ isError: true
2522
+ };
2523
+ }
2524
+ const result = parseRoadmap(raw);
2525
+ if (!result.ok) return resultToMcpResponse(result);
2526
+ const roadmap = result.value;
2527
+ const syncResult = syncRoadmap({
2528
+ projectPath,
2529
+ roadmap,
2530
+ forceSync: input.force_sync ?? false
2531
+ });
2532
+ if (!syncResult.ok) return resultToMcpResponse(syncResult);
2533
+ const changes = syncResult.value;
2534
+ if (changes.length === 0) {
2535
+ return resultToMcpResponse(Ok2({ changes: [], message: "Roadmap is up to date." }));
2536
+ }
2537
+ if (input.apply) {
2538
+ for (const change of changes) {
2539
+ for (const m of roadmap.milestones) {
2540
+ const feature = m.features.find(
2541
+ (f) => f.name.toLowerCase() === change.feature.toLowerCase()
2542
+ );
2543
+ if (feature) {
2544
+ feature.status = change.to;
2545
+ break;
2546
+ }
2547
+ }
2548
+ }
2549
+ roadmap.frontmatter.lastSynced = (/* @__PURE__ */ new Date()).toISOString();
2550
+ writeRoadmapFile(projectPath, serializeRoadmap(roadmap));
2551
+ return resultToMcpResponse(Ok2({ changes, applied: true, roadmap }));
2552
+ }
2553
+ return resultToMcpResponse(Ok2({ changes, applied: false }));
2554
+ }
2555
+ default: {
2556
+ return {
2557
+ content: [{ type: "text", text: `Error: unknown action` }],
2558
+ isError: true
2559
+ };
2560
+ }
2561
+ }
2562
+ } catch (error) {
2563
+ return {
2564
+ content: [
2565
+ {
2566
+ type: "text",
2567
+ text: `Error: ${error instanceof Error ? error.message : String(error)}`
2568
+ }
2569
+ ],
2570
+ isError: true
2571
+ };
2572
+ }
2573
+ }
2574
+
2575
+ // src/mcp/tools/interaction.ts
2576
+ import { randomUUID } from "crypto";
2577
+
2578
+ // src/mcp/tools/interaction-schemas.ts
2579
+ import { z } from "zod";
2580
+ var RiskLevel = z.enum(["low", "medium", "high"]);
2581
+ var EffortLevel = z.enum(["low", "medium", "high"]);
2582
+ var ConfidenceLevel = z.enum(["low", "medium", "high"]);
2583
+ var InteractionOptionSchema = z.object({
2584
+ label: z.string().min(1),
2585
+ pros: z.array(z.string().min(1)).min(1),
2586
+ cons: z.array(z.string().min(1)).min(1),
2587
+ risk: RiskLevel.optional(),
2588
+ effort: EffortLevel.optional()
2589
+ });
2590
+ var InteractionQuestionSchema = z.object({
2591
+ text: z.string().min(1),
2592
+ options: z.array(InteractionOptionSchema).min(2).max(10).optional(),
2593
+ recommendation: z.object({
2594
+ optionIndex: z.number().int().min(0),
2595
+ reason: z.string().min(1),
2596
+ confidence: ConfidenceLevel
2597
+ }).optional(),
2598
+ default: z.number().int().min(0).optional()
2599
+ });
2600
+ var InteractionQuestionWithOptionsSchema = InteractionQuestionSchema.refine(
2601
+ (data) => {
2602
+ if (data.options && data.options.length > 0) {
2603
+ return data.recommendation !== void 0;
2604
+ }
2605
+ return true;
2606
+ },
2607
+ { message: "recommendation is required when options are provided" }
2608
+ ).refine(
2609
+ (data) => {
2610
+ if (data.recommendation && data.options) {
2611
+ return data.recommendation.optionIndex < data.options.length;
2612
+ }
2613
+ return true;
2614
+ },
2615
+ { message: "recommendation.optionIndex must reference a valid option" }
2616
+ ).refine(
2617
+ (data) => {
2618
+ if (data.default !== void 0 && data.options) {
2619
+ return data.default < data.options.length;
2620
+ }
2621
+ return true;
2622
+ },
2623
+ { message: "default must reference a valid option index" }
2624
+ );
2625
+ var InteractionConfirmationSchema = z.object({
2626
+ text: z.string().min(1),
2627
+ context: z.string().min(1),
2628
+ impact: z.string().optional(),
2629
+ risk: RiskLevel.optional()
2630
+ });
2631
+ var QualityGateCheckSchema = z.object({
2632
+ name: z.string().min(1),
2633
+ passed: z.boolean(),
2634
+ detail: z.string().optional()
2635
+ });
2636
+ var QualityGateSchema = z.object({
2637
+ checks: z.array(QualityGateCheckSchema).min(1),
2638
+ allPassed: z.boolean()
2639
+ });
2640
+ var InteractionTransitionSchema = z.object({
2641
+ completedPhase: z.string().min(1),
2642
+ suggestedNext: z.string().min(1),
2643
+ reason: z.string().min(1),
2644
+ artifacts: z.array(z.string()),
2645
+ requiresConfirmation: z.boolean(),
2646
+ summary: z.string().min(1),
2647
+ qualityGate: QualityGateSchema.optional()
2648
+ });
2649
+ var BatchDecisionSchema = z.object({
2650
+ label: z.string().min(1),
2651
+ recommendation: z.string().min(1),
2652
+ risk: z.literal("low")
2653
+ });
2654
+ var InteractionBatchSchema = z.object({
2655
+ text: z.string().min(1),
2656
+ decisions: z.array(BatchDecisionSchema).min(1)
2657
+ });
2658
+ var InteractionTypeSchema = z.enum(["question", "confirmation", "transition", "batch"]);
2659
+ var EmitInteractionInputSchema = z.object({
2660
+ path: z.string().min(1),
2661
+ type: InteractionTypeSchema,
2662
+ stream: z.string().optional(),
2663
+ // Uses base schema here; refined validation (recommendation required with options)
2664
+ // is applied in the handler's question branch via InteractionQuestionWithOptionsSchema.
2665
+ // Refined schemas with .refine() cannot be nested inside z.object().optional() reliably.
2666
+ question: InteractionQuestionSchema.optional(),
2667
+ confirmation: InteractionConfirmationSchema.optional(),
2668
+ transition: InteractionTransitionSchema.optional(),
2669
+ batch: InteractionBatchSchema.optional()
2670
+ });
2671
+
2672
+ // src/mcp/tools/interaction-renderer.ts
2673
+ function columnLabel(index) {
2674
+ return String.fromCharCode(65 + index);
2675
+ }
2676
+ function renderQuestion(question) {
2677
+ const { text, options, recommendation } = question;
2678
+ if (!options || options.length === 0) {
2679
+ return text;
2680
+ }
2681
+ const headers = options.map(
2682
+ (opt, i) => `${columnLabel(i)}) ${escapeCell(opt.label)}`
2683
+ );
2684
+ const headerRow = `| | ${headers.join(" | ")} |`;
2685
+ const separatorRow = `|---|${options.map(() => "---").join("|")}|`;
2686
+ const prosRow = `| **Pros** | ${options.map((opt) => opt.pros.map(escapeCell).join("; ")).join(" | ")} |`;
2687
+ const consRow = `| **Cons** | ${options.map((opt) => opt.cons.map(escapeCell).join("; ")).join(" | ")} |`;
2688
+ const rows = [headerRow, separatorRow, prosRow, consRow];
2689
+ if (options.some((opt) => opt.risk)) {
2690
+ const riskRow = `| **Risk** | ${options.map((opt) => opt.risk ? capitalize(opt.risk) : "-").join(" | ")} |`;
2691
+ rows.push(riskRow);
2692
+ }
2693
+ if (options.some((opt) => opt.effort)) {
2694
+ const effortRow = `| **Effort** | ${options.map((opt) => opt.effort ? capitalize(opt.effort) : "-").join(" | ")} |`;
2695
+ rows.push(effortRow);
2696
+ }
2697
+ let prompt = `### Decision needed: ${text}
2698
+
2699
+ ${rows.join("\n")}`;
2700
+ if (recommendation) {
2701
+ const opt = options[recommendation.optionIndex];
2702
+ if (opt) {
2703
+ const recLabel = `${columnLabel(recommendation.optionIndex)}) ${opt.label}`;
2704
+ prompt += `
2705
+
2706
+ **Recommendation:** ${recLabel} (confidence: ${recommendation.confidence})`;
2707
+ prompt += `
2708
+ > ${recommendation.reason}`;
2709
+ }
2710
+ }
2711
+ return prompt;
2712
+ }
2713
+ function renderConfirmation(confirmation) {
2714
+ let prompt = `${confirmation.text}
2715
+
2716
+ Context: ${confirmation.context}`;
2717
+ if (confirmation.impact) {
2718
+ prompt += `
2719
+
2720
+ Impact: ${confirmation.impact}`;
2721
+ }
2722
+ if (confirmation.risk) {
2723
+ prompt += `
2724
+ Risk: ${capitalize(confirmation.risk)}`;
2725
+ }
2726
+ prompt += "\n\nProceed? (yes/no)";
2727
+ return prompt;
2728
+ }
2729
+ function renderTransition(transition) {
2730
+ let prompt = `Phase "${transition.completedPhase}" complete. ${transition.reason}
2731
+
2732
+ ${transition.summary}
2733
+
2734
+ Artifacts produced:
2735
+ ${transition.artifacts.map((a) => ` - ${a}`).join("\n")}`;
2736
+ if (transition.qualityGate) {
2737
+ prompt += "\n\n**Quality Gate:**\n";
2738
+ for (const check of transition.qualityGate.checks) {
2739
+ const icon = check.passed ? "PASS" : "FAIL";
2740
+ prompt += ` - [${icon}] ${check.name}`;
2741
+ if (check.detail) {
2742
+ prompt += ` -- ${check.detail}`;
2743
+ }
2744
+ prompt += "\n";
2745
+ }
2746
+ prompt += transition.qualityGate.allPassed ? " All checks passed." : " **Some checks failed.**";
2747
+ }
2748
+ prompt += "\n\n";
2749
+ prompt += transition.requiresConfirmation ? `Suggested next: "${transition.suggestedNext}". Proceed?` : `Proceeding to ${transition.suggestedNext}...`;
2750
+ return prompt;
2751
+ }
2752
+ function renderBatch(batch) {
2753
+ let prompt = `${batch.text}
2754
+
2755
+ `;
2756
+ batch.decisions.forEach((d, i) => {
2757
+ prompt += `${i + 1}. **${d.label}** -- Recommendation: ${d.recommendation} (risk: low)
2758
+ `;
2759
+ });
2760
+ prompt += "\nApprove all? (yes/no)";
2761
+ return prompt;
2762
+ }
2763
+ function capitalize(s) {
2764
+ return s.charAt(0).toUpperCase() + s.slice(1);
2765
+ }
2766
+ function escapeCell(s) {
2767
+ return s.replace(/\|/g, "\\|");
2768
+ }
2769
+
2770
+ // src/mcp/tools/interaction.ts
2771
+ var emitInteractionDefinition = {
2772
+ name: "emit_interaction",
2773
+ description: "Emit a structured interaction (question, confirmation, phase transition, or batch decision) for round-trip communication with the user",
2774
+ inputSchema: {
2775
+ type: "object",
2776
+ properties: {
2777
+ path: { type: "string", description: "Path to project root" },
2778
+ type: {
2779
+ type: "string",
2780
+ enum: ["question", "confirmation", "transition", "batch"],
2781
+ description: "Type of interaction"
2782
+ },
2783
+ stream: {
2784
+ type: "string",
2785
+ description: "State stream for recording (auto-resolves from branch if omitted)"
2786
+ },
2787
+ question: {
2788
+ type: "object",
2789
+ description: "Question payload (required when type is question)",
2790
+ properties: {
2791
+ text: { type: "string", description: "The question text" },
2792
+ options: {
2793
+ type: "array",
2794
+ items: {
2795
+ type: "object",
2796
+ properties: {
2797
+ label: { type: "string" },
2798
+ pros: { type: "array", items: { type: "string" } },
2799
+ cons: { type: "array", items: { type: "string" } },
2800
+ risk: { type: "string", enum: ["low", "medium", "high"] },
2801
+ effort: { type: "string", enum: ["low", "medium", "high"] }
2802
+ },
2803
+ required: ["label", "pros", "cons"]
2804
+ },
2805
+ description: "Structured options with pros/cons (omit for free-form)"
2806
+ },
2807
+ recommendation: {
2808
+ type: "object",
2809
+ properties: {
2810
+ optionIndex: { type: "number", description: "Index of recommended option" },
2811
+ reason: { type: "string", description: "Why this option is recommended" },
2812
+ confidence: { type: "string", enum: ["low", "medium", "high"] }
2813
+ },
2814
+ required: ["optionIndex", "reason", "confidence"],
2815
+ description: "Required when options are provided"
2816
+ },
2817
+ default: { type: "number", description: "Default option index" }
2818
+ },
2819
+ required: ["text"]
2820
+ },
2821
+ confirmation: {
2822
+ type: "object",
2823
+ description: "Confirmation payload (required when type is confirmation)",
2824
+ properties: {
2825
+ text: { type: "string", description: "What to confirm" },
2826
+ context: {
2827
+ type: "string",
2828
+ description: "Why confirmation is needed"
2829
+ },
2830
+ impact: { type: "string", description: "Impact description" },
2831
+ risk: { type: "string", enum: ["low", "medium", "high"], description: "Risk level" }
2832
+ },
2833
+ required: ["text", "context"]
2834
+ },
2835
+ transition: {
2836
+ type: "object",
2837
+ description: "Transition payload (required when type is transition)",
2838
+ properties: {
2839
+ completedPhase: {
2840
+ type: "string",
2841
+ description: "Phase that was completed"
2842
+ },
2843
+ suggestedNext: {
2844
+ type: "string",
2845
+ description: "Suggested next phase"
2846
+ },
2847
+ reason: {
2848
+ type: "string",
2849
+ description: "Why the transition is happening"
2850
+ },
2851
+ artifacts: {
2852
+ type: "array",
2853
+ items: { type: "string" },
2854
+ description: "File paths produced during the completed phase"
2855
+ },
2856
+ requiresConfirmation: {
2857
+ type: "boolean",
2858
+ description: "true = wait for user confirmation, false = proceed immediately"
2859
+ },
2860
+ summary: {
2861
+ type: "string",
2862
+ description: "1-2 sentence rich summary with key metrics"
2863
+ },
2864
+ qualityGate: {
2865
+ type: "object",
2866
+ properties: {
2867
+ checks: {
2868
+ type: "array",
2869
+ items: {
2870
+ type: "object",
2871
+ properties: {
2872
+ name: { type: "string" },
2873
+ passed: { type: "boolean" },
2874
+ detail: { type: "string" }
2875
+ },
2876
+ required: ["name", "passed"]
2877
+ }
2878
+ },
2879
+ allPassed: { type: "boolean" }
2880
+ },
2881
+ required: ["checks", "allPassed"],
2882
+ description: "Quality gate results for the completed phase"
2883
+ }
2884
+ },
2885
+ required: [
2886
+ "completedPhase",
2887
+ "suggestedNext",
2888
+ "reason",
2889
+ "artifacts",
2890
+ "requiresConfirmation",
2891
+ "summary"
2892
+ ]
2893
+ },
2894
+ batch: {
2895
+ type: "object",
2896
+ description: "Batch decision payload (required when type is batch)",
2897
+ properties: {
2898
+ text: { type: "string", description: "Batch description" },
2899
+ decisions: {
2900
+ type: "array",
2901
+ items: {
2902
+ type: "object",
2903
+ properties: {
2904
+ label: { type: "string" },
2905
+ recommendation: { type: "string" },
2906
+ risk: { type: "string", enum: ["low"] }
2907
+ },
2908
+ required: ["label", "recommendation", "risk"]
2909
+ },
2910
+ description: "Low-risk decisions to approve in batch"
2911
+ }
2912
+ },
2913
+ required: ["text", "decisions"]
2914
+ }
2915
+ },
2916
+ required: ["path", "type"]
2917
+ }
2918
+ };
2919
+ async function handleEmitInteraction(input) {
2920
+ try {
2921
+ const parseResult = EmitInteractionInputSchema.safeParse(input);
2922
+ if (!parseResult.success) {
2923
+ return {
2924
+ content: [
2925
+ {
2926
+ type: "text",
2927
+ text: `Error: ${parseResult.error.issues.map((i) => i.path.length > 0 ? `${i.path.join(".")}: ${i.message}` : i.message).join("; ")}`
2928
+ }
2929
+ ],
2930
+ isError: true
2931
+ };
2932
+ }
2933
+ const validInput = parseResult.data;
2934
+ const projectPath = sanitizePath(validInput.path);
2935
+ const id = randomUUID();
2936
+ switch (validInput.type) {
2937
+ case "question": {
2938
+ if (!validInput.question) {
2939
+ return {
2940
+ content: [
2941
+ {
2942
+ type: "text",
2943
+ text: "Error: question payload is required when type is question"
2944
+ }
2945
+ ],
2946
+ isError: true
2947
+ };
2948
+ }
2949
+ const questionResult = InteractionQuestionWithOptionsSchema.safeParse(validInput.question);
2950
+ if (!questionResult.success) {
2951
+ return {
2952
+ content: [
2953
+ {
2954
+ type: "text",
2955
+ text: `Error: ${questionResult.error.issues.map((i) => i.path.length > 0 ? `${i.path.join(".")}: ${i.message}` : i.message).join("; ")}`
2956
+ }
2957
+ ],
2958
+ isError: true
2959
+ };
2960
+ }
2961
+ const prompt = renderQuestion(questionResult.data);
2962
+ await recordInteraction(
2963
+ projectPath,
2964
+ id,
2965
+ "question",
2966
+ questionResult.data.text,
2967
+ validInput.stream
2968
+ );
2969
+ return {
2970
+ content: [{ type: "text", text: JSON.stringify({ id, prompt }) }]
2971
+ };
2972
+ }
2973
+ case "confirmation": {
2974
+ if (!validInput.confirmation) {
2975
+ return {
2976
+ content: [
2977
+ {
2978
+ type: "text",
2979
+ text: "Error: confirmation payload is required when type is confirmation"
2980
+ }
2981
+ ],
2982
+ isError: true
2983
+ };
2984
+ }
2985
+ const confirmResult = InteractionConfirmationSchema.safeParse(validInput.confirmation);
2986
+ if (!confirmResult.success) {
2987
+ return {
2988
+ content: [
2989
+ {
2990
+ type: "text",
2991
+ text: `Error: ${confirmResult.error.issues.map((i) => i.message).join("; ")}`
2992
+ }
2993
+ ],
2994
+ isError: true
2995
+ };
2996
+ }
2997
+ const prompt = renderConfirmation(confirmResult.data);
2998
+ await recordInteraction(
2999
+ projectPath,
3000
+ id,
3001
+ "confirmation",
3002
+ confirmResult.data.text,
3003
+ validInput.stream
3004
+ );
3005
+ return {
3006
+ content: [{ type: "text", text: JSON.stringify({ id, prompt }) }]
3007
+ };
3008
+ }
3009
+ case "transition": {
3010
+ if (!validInput.transition) {
3011
+ return {
3012
+ content: [
3013
+ {
3014
+ type: "text",
3015
+ text: "Error: transition payload is required when type is transition"
3016
+ }
3017
+ ],
3018
+ isError: true
3019
+ };
3020
+ }
3021
+ const transitionResult = InteractionTransitionSchema.safeParse(validInput.transition);
3022
+ if (!transitionResult.success) {
3023
+ return {
3024
+ content: [
3025
+ {
3026
+ type: "text",
3027
+ text: `Error: ${transitionResult.error.issues.map((i) => i.message).join("; ")}`
3028
+ }
3029
+ ],
3030
+ isError: true
3031
+ };
3032
+ }
3033
+ const transition = transitionResult.data;
3034
+ const prompt = renderTransition(transition);
3035
+ try {
3036
+ const { saveHandoff } = await import("./dist-PBTNVK6K.js");
3037
+ await saveHandoff(
3038
+ projectPath,
3039
+ {
3040
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
3041
+ fromSkill: "emit_interaction",
3042
+ phase: transition.completedPhase,
3043
+ summary: transition.reason,
3044
+ completed: [transition.completedPhase],
3045
+ pending: [transition.suggestedNext],
3046
+ concerns: [],
3047
+ decisions: [],
3048
+ blockers: [],
3049
+ contextKeywords: []
3050
+ },
3051
+ validInput.stream
3052
+ );
3053
+ } catch {
3054
+ }
3055
+ await recordInteraction(
3056
+ projectPath,
3057
+ id,
3058
+ "transition",
3059
+ `${transition.completedPhase} -> ${transition.suggestedNext}`,
3060
+ validInput.stream
3061
+ );
3062
+ const responsePayload = { id, prompt, handoffWritten: true };
3063
+ if (!transition.requiresConfirmation) {
3064
+ responsePayload.autoTransition = true;
3065
+ responsePayload.nextAction = `Invoke harness-${transition.suggestedNext} skill now`;
3066
+ }
3067
+ return {
3068
+ content: [
3069
+ {
3070
+ type: "text",
3071
+ text: JSON.stringify(responsePayload)
3072
+ }
3073
+ ]
3074
+ };
3075
+ }
3076
+ case "batch": {
3077
+ if (!validInput.batch) {
3078
+ return {
3079
+ content: [
3080
+ {
3081
+ type: "text",
3082
+ text: "Error: batch payload is required when type is batch"
3083
+ }
3084
+ ],
3085
+ isError: true
3086
+ };
3087
+ }
3088
+ const batchResult = InteractionBatchSchema.safeParse(validInput.batch);
3089
+ if (!batchResult.success) {
3090
+ return {
3091
+ content: [
3092
+ {
3093
+ type: "text",
3094
+ text: `Error: ${batchResult.error.issues.map((i) => i.message).join("; ")}`
3095
+ }
3096
+ ],
3097
+ isError: true
3098
+ };
3099
+ }
3100
+ const prompt = renderBatch(batchResult.data);
3101
+ await recordInteraction(projectPath, id, "batch", batchResult.data.text, validInput.stream);
3102
+ return {
3103
+ content: [
3104
+ {
3105
+ type: "text",
3106
+ text: JSON.stringify({ id, prompt, batchMode: true })
3107
+ }
3108
+ ]
3109
+ };
3110
+ }
3111
+ default: {
3112
+ return {
3113
+ content: [
3114
+ {
3115
+ type: "text",
3116
+ text: `Error: unknown interaction type: ${String(validInput.type)}`
3117
+ }
3118
+ ],
3119
+ isError: true
3120
+ };
3121
+ }
3122
+ }
3123
+ } catch (error) {
3124
+ return {
3125
+ content: [
3126
+ {
3127
+ type: "text",
3128
+ text: `Error: ${error instanceof Error ? error.message : String(error)}`
3129
+ }
3130
+ ],
3131
+ isError: true
3132
+ };
3133
+ }
3134
+ }
3135
+ async function recordInteraction(projectPath, id, type, decision, stream) {
3136
+ try {
3137
+ const { loadState, saveState } = await import("./dist-PBTNVK6K.js");
3138
+ const stateResult = await loadState(projectPath, stream);
3139
+ if (stateResult.ok) {
3140
+ const state = stateResult.value;
3141
+ state.decisions.push({
3142
+ date: (/* @__PURE__ */ new Date()).toISOString(),
3143
+ decision: `[${type}:${id}] ${decision}`,
3144
+ context: "pending user response"
3145
+ });
3146
+ await saveState(projectPath, state, stream);
3147
+ }
3148
+ } catch {
3149
+ }
3150
+ }
3151
+
3152
+ // src/mcp/tools/gather-context.ts
3153
+ var gatherContextDefinition = {
3154
+ name: "gather_context",
3155
+ description: "Assemble all working context an agent needs in a single call: state, learnings, handoff, graph context, and project validation. Runs constituents in parallel.",
3156
+ inputSchema: {
3157
+ type: "object",
3158
+ properties: {
3159
+ path: { type: "string", description: "Path to project root" },
3160
+ intent: {
3161
+ type: "string",
3162
+ description: "What the agent is about to do (used for graph context search)"
3163
+ },
3164
+ skill: {
3165
+ type: "string",
3166
+ description: "Current skill name (filters learnings by skill)"
3167
+ },
3168
+ tokenBudget: {
3169
+ type: "number",
3170
+ description: "Approximate token budget for graph context (default 4000)"
3171
+ },
3172
+ include: {
3173
+ type: "array",
3174
+ items: {
3175
+ type: "string",
3176
+ enum: ["state", "learnings", "handoff", "graph", "validation"]
3177
+ },
3178
+ description: "Which constituents to include (default: all)"
3179
+ },
3180
+ mode: {
3181
+ type: "string",
3182
+ enum: ["summary", "detailed"],
3183
+ description: "Response density. Default: summary"
3184
+ }
3185
+ },
3186
+ required: ["path", "intent"]
3187
+ }
3188
+ };
3189
+ async function handleGatherContext(input) {
3190
+ const start = Date.now();
3191
+ let projectPath;
3192
+ try {
3193
+ projectPath = sanitizePath(input.path);
3194
+ } catch (error) {
3195
+ return {
3196
+ content: [
3197
+ {
3198
+ type: "text",
3199
+ text: `Error: ${error instanceof Error ? error.message : String(error)}`
3200
+ }
3201
+ ],
3202
+ isError: true
3203
+ };
3204
+ }
3205
+ const includeSet = new Set(
3206
+ input.include ?? ["state", "learnings", "handoff", "graph", "validation"]
3207
+ );
3208
+ const errors = [];
3209
+ const statePromise = includeSet.has("state") ? import("./dist-PBTNVK6K.js").then((core) => core.loadState(projectPath)) : Promise.resolve(null);
3210
+ const learningsPromise = includeSet.has("learnings") ? import("./dist-PBTNVK6K.js").then(
3211
+ (core) => core.loadRelevantLearnings(projectPath, input.skill)
3212
+ ) : Promise.resolve(null);
3213
+ const handoffPromise = includeSet.has("handoff") ? import("./dist-PBTNVK6K.js").then((core) => core.loadHandoff(projectPath)) : Promise.resolve(null);
3214
+ const graphPromise = includeSet.has("graph") ? (async () => {
3215
+ const { loadGraphStore: loadGraphStore2 } = await import("./graph-loader-RLO3KRIX.js");
3216
+ const store = await loadGraphStore2(projectPath);
3217
+ if (!store) return null;
3218
+ const { FusionLayer, ContextQL } = await import("./dist-I7DB5VKB.js");
3219
+ const fusion = new FusionLayer(store);
3220
+ const cql = new ContextQL(store);
3221
+ const tokenBudget = input.tokenBudget ?? 4e3;
3222
+ const charBudget = tokenBudget * 4;
3223
+ const searchResults = fusion.search(input.intent, 10);
3224
+ if (searchResults.length === 0) return { context: [], tokenBudget };
3225
+ const contextBlocks = [];
3226
+ let totalChars = 0;
3227
+ for (const result of searchResults) {
3228
+ if (totalChars >= charBudget) break;
3229
+ const expanded = cql.execute({
3230
+ rootNodeIds: [result.nodeId],
3231
+ maxDepth: 2
3232
+ });
3233
+ const blockJson = JSON.stringify({
3234
+ rootNode: result.nodeId,
3235
+ score: result.score,
3236
+ nodes: expanded.nodes,
3237
+ edges: expanded.edges
3238
+ });
3239
+ if (totalChars + blockJson.length > charBudget && contextBlocks.length > 0) break;
3240
+ contextBlocks.push({
3241
+ rootNode: result.nodeId,
3242
+ score: result.score,
3243
+ nodes: expanded.nodes,
3244
+ edges: expanded.edges
3245
+ });
3246
+ totalChars += blockJson.length;
3247
+ }
3248
+ return {
3249
+ intent: input.intent,
3250
+ tokenBudget,
3251
+ blocksReturned: contextBlocks.length,
3252
+ context: contextBlocks
3253
+ };
3254
+ })() : Promise.resolve(null);
3255
+ const validationPromise = includeSet.has("validation") ? (async () => {
3256
+ const { handleValidateProject: handleValidateProject2 } = await import("./validate-KSDUUK2M.js");
3257
+ const result = await handleValidateProject2({ path: projectPath });
3258
+ const first = result.content[0];
3259
+ return first ? JSON.parse(first.text) : null;
3260
+ })() : Promise.resolve(null);
3261
+ const [stateResult, learningsResult, handoffResult, graphResult, validationResult] = await Promise.allSettled([
3262
+ statePromise,
3263
+ learningsPromise,
3264
+ handoffPromise,
3265
+ graphPromise,
3266
+ validationPromise
3267
+ ]);
3268
+ function extract(settled, name) {
3269
+ if (settled.status === "rejected") {
3270
+ errors.push(`${name}: ${String(settled.reason)}`);
3271
+ return null;
3272
+ }
3273
+ return settled.value;
3274
+ }
3275
+ const stateRaw = extract(stateResult, "state");
3276
+ const learningsRaw = extract(learningsResult, "learnings");
3277
+ const handoffRaw = extract(handoffResult, "handoff");
3278
+ const graphContextRaw = extract(graphResult, "graph");
3279
+ const validationRaw = extract(validationResult, "validation");
3280
+ const state = stateRaw && typeof stateRaw === "object" && "ok" in stateRaw ? stateRaw.ok ? stateRaw.value : (() => {
3281
+ errors.push(`state: ${stateRaw.error.message}`);
3282
+ return null;
3283
+ })() : stateRaw;
3284
+ const learnings = learningsRaw && typeof learningsRaw === "object" && "ok" in learningsRaw ? learningsRaw.ok ? learningsRaw.value : (() => {
3285
+ errors.push(
3286
+ `learnings: ${learningsRaw.error.message}`
3287
+ );
3288
+ return [];
3289
+ })() : learningsRaw ?? [];
3290
+ const handoff = handoffRaw && typeof handoffRaw === "object" && "ok" in handoffRaw ? handoffRaw.ok ? handoffRaw.value : (() => {
3291
+ errors.push(`handoff: ${handoffRaw.error.message}`);
3292
+ return null;
3293
+ })() : handoffRaw;
3294
+ const graphContext = graphContextRaw;
3295
+ const validation = validationRaw;
3296
+ const assembledIn = Date.now() - start;
3297
+ const mode = input.mode ?? "summary";
3298
+ const outputState = state ?? null;
3299
+ const outputLearnings = learnings ?? [];
3300
+ const outputHandoff = handoff ?? null;
3301
+ const outputGraphContext = graphContext == null ? null : mode === "summary" ? {
3302
+ blocksReturned: graphContext.blocksReturned ?? 0,
3303
+ nodeCount: (graphContext.context ?? []).reduce(
3304
+ (sum, b) => sum + (Array.isArray(b.nodes) ? b.nodes.length : 0),
3305
+ 0
3306
+ ),
3307
+ edgeCount: (graphContext.context ?? []).reduce(
3308
+ (sum, b) => sum + (Array.isArray(b.edges) ? b.edges.length : 0),
3309
+ 0
3310
+ ),
3311
+ intent: graphContext.intent ?? null
3312
+ } : graphContext;
3313
+ const outputValidation = validation ?? null;
3314
+ const output = {
3315
+ state: outputState,
3316
+ learnings: outputLearnings,
3317
+ handoff: outputHandoff,
3318
+ graphContext: outputGraphContext,
3319
+ validation: outputValidation,
3320
+ meta: {
3321
+ assembledIn,
3322
+ graphAvailable: graphContext !== null,
3323
+ tokenEstimate: 0,
3324
+ // set below from final serialization
3325
+ errors
3326
+ }
3327
+ };
3328
+ const outputText = JSON.stringify(output);
3329
+ const tokenEstimate = Math.ceil(outputText.length / 4);
3330
+ output.meta.tokenEstimate = tokenEstimate;
3331
+ return {
3332
+ content: [
3333
+ {
3334
+ type: "text",
3335
+ text: JSON.stringify(output)
3336
+ }
3337
+ ]
3338
+ };
3339
+ }
3340
+
3341
+ // src/mcp/tools/assess-project.ts
3342
+ var assessProjectDefinition = {
3343
+ name: "assess_project",
3344
+ description: "Run all project health checks in parallel and return a unified report. Checks: validate, dependencies, docs, entropy, security, performance, lint.",
3345
+ inputSchema: {
3346
+ type: "object",
3347
+ properties: {
3348
+ path: { type: "string", description: "Path to project root" },
3349
+ checks: {
3350
+ type: "array",
3351
+ items: {
3352
+ type: "string",
3353
+ enum: ["validate", "deps", "docs", "entropy", "security", "perf", "lint"]
3354
+ },
3355
+ description: "Which checks to run (default: all)"
3356
+ },
3357
+ mode: {
3358
+ type: "string",
3359
+ enum: ["summary", "detailed"],
3360
+ description: "Response density. Default: summary"
3361
+ }
3362
+ },
3363
+ required: ["path"]
3364
+ }
3365
+ };
3366
+ async function handleAssessProject(input) {
3367
+ const start = Date.now();
3368
+ let projectPath;
3369
+ try {
3370
+ projectPath = sanitizePath(input.path);
3371
+ } catch (error) {
3372
+ return {
3373
+ content: [
3374
+ {
3375
+ type: "text",
3376
+ text: `Error: ${error instanceof Error ? error.message : String(error)}`
3377
+ }
3378
+ ],
3379
+ isError: true
3380
+ };
3381
+ }
3382
+ const checksToRun = new Set(
3383
+ input.checks ?? ["validate", "deps", "docs", "entropy", "security", "perf", "lint"]
3384
+ );
3385
+ const mode = input.mode ?? "summary";
3386
+ let validateResult = null;
3387
+ if (checksToRun.has("validate")) {
3388
+ try {
3389
+ const { handleValidateProject: handleValidateProject2 } = await import("./validate-KSDUUK2M.js");
3390
+ const result = await handleValidateProject2({ path: projectPath });
3391
+ const first = result.content[0];
3392
+ const parsed = first ? JSON.parse(first.text) : {};
3393
+ validateResult = {
3394
+ name: "validate",
3395
+ passed: parsed.valid === true,
3396
+ issueCount: parsed.errors?.length ?? 0,
3397
+ ...parsed.errors?.length > 0 ? { topIssue: parsed.errors[0] } : {},
3398
+ ...mode === "detailed" ? { detailed: parsed } : {}
3399
+ };
3400
+ } catch (error) {
3401
+ validateResult = {
3402
+ name: "validate",
3403
+ passed: false,
3404
+ issueCount: 1,
3405
+ topIssue: error instanceof Error ? error.message : String(error)
3406
+ };
3407
+ }
3408
+ }
3409
+ const parallelChecks = [];
3410
+ if (checksToRun.has("deps")) {
3411
+ parallelChecks.push(
3412
+ (async () => {
3413
+ try {
3414
+ const { handleCheckDependencies: handleCheckDependencies2 } = await import("./architecture-5JNN5L3M.js");
3415
+ const result = await handleCheckDependencies2({ path: projectPath });
3416
+ const first = result.content[0];
3417
+ const parsed = first ? JSON.parse(first.text) : {};
3418
+ const violations = parsed.violations ?? [];
3419
+ return {
3420
+ name: "deps",
3421
+ passed: !result.isError && violations.length === 0,
3422
+ issueCount: violations.length,
3423
+ ...violations.length > 0 ? { topIssue: violations[0]?.message ?? String(violations[0]) } : {},
3424
+ ...mode === "detailed" ? { detailed: parsed } : {}
3425
+ };
3426
+ } catch (error) {
3427
+ return {
3428
+ name: "deps",
3429
+ passed: false,
3430
+ issueCount: 1,
3431
+ topIssue: error instanceof Error ? error.message : String(error)
3432
+ };
3433
+ }
3434
+ })()
3435
+ );
3436
+ }
3437
+ if (checksToRun.has("docs")) {
3438
+ parallelChecks.push(
3439
+ (async () => {
3440
+ try {
3441
+ const { handleCheckDocs: handleCheckDocs2 } = await import("./docs-PTJGD6XI.js");
3442
+ const result = await handleCheckDocs2({ path: projectPath, scope: "coverage" });
3443
+ const first = result.content[0];
3444
+ const parsed = first ? JSON.parse(first.text) : {};
3445
+ const undocumented = parsed.undocumented ?? parsed.files?.undocumented ?? [];
3446
+ return {
3447
+ name: "docs",
3448
+ passed: !result.isError,
3449
+ issueCount: Array.isArray(undocumented) ? undocumented.length : 0,
3450
+ ...Array.isArray(undocumented) && undocumented.length > 0 ? { topIssue: `Undocumented: ${undocumented[0]}` } : {},
3451
+ ...mode === "detailed" ? { detailed: parsed } : {}
3452
+ };
3453
+ } catch (error) {
3454
+ return {
3455
+ name: "docs",
3456
+ passed: false,
3457
+ issueCount: 1,
3458
+ topIssue: error instanceof Error ? error.message : String(error)
3459
+ };
3460
+ }
3461
+ })()
3462
+ );
3463
+ }
3464
+ if (checksToRun.has("entropy")) {
3465
+ parallelChecks.push(
3466
+ (async () => {
3467
+ try {
3468
+ const { handleDetectEntropy: handleDetectEntropy2 } = await import("./entropy-YIUBGKY7.js");
3469
+ const result = await handleDetectEntropy2({ path: projectPath, type: "all" });
3470
+ const first = result.content[0];
3471
+ const parsed = first ? JSON.parse(first.text) : {};
3472
+ const issues = (parsed.drift?.staleReferences?.length ?? 0) + (parsed.drift?.missingTargets?.length ?? 0) + (parsed.deadCode?.unusedImports?.length ?? 0) + (parsed.deadCode?.unusedExports?.length ?? 0) + (parsed.patterns?.violations?.length ?? 0);
3473
+ return {
3474
+ name: "entropy",
3475
+ passed: !result.isError && issues === 0,
3476
+ issueCount: issues,
3477
+ ...issues > 0 ? { topIssue: "Entropy detected -- run detect_entropy for details" } : {},
3478
+ ...mode === "detailed" ? { detailed: parsed } : {}
3479
+ };
3480
+ } catch (error) {
3481
+ return {
3482
+ name: "entropy",
3483
+ passed: false,
3484
+ issueCount: 1,
3485
+ topIssue: error instanceof Error ? error.message : String(error)
3486
+ };
3487
+ }
3488
+ })()
3489
+ );
3490
+ }
3491
+ if (checksToRun.has("security")) {
3492
+ parallelChecks.push(
3493
+ (async () => {
3494
+ try {
3495
+ const { handleRunSecurityScan: handleRunSecurityScan2 } = await import("./security-URYTKLGK.js");
3496
+ const result = await handleRunSecurityScan2({ path: projectPath });
3497
+ const first = result.content[0];
3498
+ const parsed = first ? JSON.parse(first.text) : {};
3499
+ const findings = parsed.findings ?? [];
3500
+ const errorCount = findings.filter(
3501
+ (f) => f.severity === "error"
3502
+ ).length;
3503
+ return {
3504
+ name: "security",
3505
+ passed: !result.isError && errorCount === 0,
3506
+ issueCount: findings.length,
3507
+ ...findings.length > 0 ? {
3508
+ topIssue: `${findings[0]?.rule ?? findings[0]?.type ?? "finding"}: ${findings[0]?.message ?? ""}`
3509
+ } : {},
3510
+ ...mode === "detailed" ? { detailed: parsed } : {}
3511
+ };
3512
+ } catch (error) {
3513
+ return {
3514
+ name: "security",
3515
+ passed: false,
3516
+ issueCount: 1,
3517
+ topIssue: error instanceof Error ? error.message : String(error)
3518
+ };
3519
+ }
3520
+ })()
3521
+ );
3522
+ }
3523
+ if (checksToRun.has("perf")) {
3524
+ parallelChecks.push(
3525
+ (async () => {
3526
+ try {
3527
+ const { handleCheckPerformance: handleCheckPerformance2 } = await import("./performance-5TVW6SA6.js");
3528
+ const result = await handleCheckPerformance2({ path: projectPath });
3529
+ const first = result.content[0];
3530
+ const parsed = first ? JSON.parse(first.text) : {};
3531
+ const issues = parsed.violations?.length ?? parsed.issues?.length ?? 0;
3532
+ return {
3533
+ name: "perf",
3534
+ passed: !result.isError && issues === 0,
3535
+ issueCount: issues,
3536
+ ...issues > 0 ? { topIssue: "Performance issues detected" } : {},
3537
+ ...mode === "detailed" ? { detailed: parsed } : {}
3538
+ };
3539
+ } catch (error) {
3540
+ return {
3541
+ name: "perf",
3542
+ passed: false,
3543
+ issueCount: 1,
3544
+ topIssue: error instanceof Error ? error.message : String(error)
3545
+ };
3546
+ }
3547
+ })()
3548
+ );
3549
+ }
3550
+ if (checksToRun.has("lint")) {
3551
+ parallelChecks.push(
3552
+ (async () => {
3553
+ try {
3554
+ const { execFileSync } = await import("child_process");
3555
+ const output = execFileSync("npx", ["turbo", "run", "lint", "--force"], {
3556
+ cwd: projectPath,
3557
+ encoding: "utf-8",
3558
+ timeout: 6e4,
3559
+ stdio: ["pipe", "pipe", "pipe"]
3560
+ });
3561
+ return {
3562
+ name: "lint",
3563
+ passed: true,
3564
+ issueCount: 0,
3565
+ ...mode === "detailed" ? { detailed: output } : {}
3566
+ };
3567
+ } catch (error) {
3568
+ const stderr = error && typeof error === "object" && "stderr" in error ? String(error.stderr) : "";
3569
+ const stdout = error && typeof error === "object" && "stdout" in error ? String(error.stdout) : "";
3570
+ const combined = (stderr + "\n" + stdout).trim();
3571
+ const errorMatch = combined.match(/(\d+) error/);
3572
+ const issueCount = errorMatch?.[1] ? parseInt(errorMatch[1], 10) : 1;
3573
+ const firstError = combined.split("\n").find((line) => line.includes("error"));
3574
+ return {
3575
+ name: "lint",
3576
+ passed: false,
3577
+ issueCount,
3578
+ topIssue: firstError?.trim() ?? (error instanceof Error ? error.message : String(error)),
3579
+ ...mode === "detailed" ? { detailed: combined } : {}
3580
+ };
3581
+ }
3582
+ })()
3583
+ );
3584
+ }
3585
+ const parallelResults = await Promise.all(parallelChecks);
3586
+ const allChecks = [];
3587
+ if (validateResult) allChecks.push(validateResult);
3588
+ allChecks.push(...parallelResults);
3589
+ const healthy = allChecks.every((c) => c.passed);
3590
+ const assessedIn = Date.now() - start;
3591
+ if (mode === "summary") {
3592
+ const summaryChecks = allChecks.map(({ detailed: _d, ...rest }) => rest);
3593
+ return {
3594
+ content: [
3595
+ {
3596
+ type: "text",
3597
+ text: JSON.stringify({ healthy, checks: summaryChecks, assessedIn })
3598
+ }
3599
+ ]
3600
+ };
3601
+ }
3602
+ return {
3603
+ content: [
3604
+ {
3605
+ type: "text",
3606
+ text: JSON.stringify({ healthy, checks: allChecks, assessedIn })
3607
+ }
3608
+ ]
3609
+ };
3610
+ }
3611
+
3612
+ // src/mcp/tools/review-changes.ts
3613
+ var SIZE_GATE_LINES = 1e4;
3614
+ var reviewChangesDefinition = {
3615
+ name: "review_changes",
3616
+ description: "Review code changes at configurable depth: quick (diff analysis), standard (+ self-review), deep (full 7-phase pipeline). Auto-downgrades deep to standard for diffs > 10k lines.",
3617
+ inputSchema: {
3618
+ type: "object",
3619
+ properties: {
3620
+ path: { type: "string", description: "Path to project root" },
3621
+ diff: {
3622
+ type: "string",
3623
+ description: "Raw git diff string. If omitted, auto-detects from git."
3624
+ },
3625
+ depth: {
3626
+ type: "string",
3627
+ enum: ["quick", "standard", "deep"],
3628
+ description: "Review depth: quick, standard, or deep"
3629
+ },
3630
+ mode: {
3631
+ type: "string",
3632
+ enum: ["summary", "detailed"],
3633
+ description: "Response density. Default: summary"
3634
+ }
3635
+ },
3636
+ required: ["path", "depth"]
3637
+ }
3638
+ };
3639
+ async function getDiff(projectPath, providedDiff) {
3640
+ if (providedDiff) return providedDiff;
3641
+ const { execFileSync } = await import("child_process");
3642
+ try {
3643
+ const staged = execFileSync("git", ["diff", "--cached"], {
3644
+ cwd: projectPath,
3645
+ encoding: "utf-8",
3646
+ timeout: 1e4
3647
+ });
3648
+ if (staged.trim().length > 0) return staged;
3649
+ const unstaged = execFileSync("git", ["diff"], {
3650
+ cwd: projectPath,
3651
+ encoding: "utf-8",
3652
+ timeout: 1e4
3653
+ });
3654
+ if (unstaged.trim().length > 0) return unstaged;
3655
+ throw new Error("No diff found -- provide a diff string or have uncommitted changes");
3656
+ } catch (error) {
3657
+ if (error instanceof Error && error.message.includes("No diff found")) throw error;
3658
+ throw new Error(
3659
+ `Failed to get diff from git: ${error instanceof Error ? error.message : String(error)}`,
3660
+ { cause: error }
3661
+ );
3662
+ }
3663
+ }
3664
+ async function handleReviewChanges(input) {
3665
+ let projectPath;
3666
+ try {
3667
+ projectPath = sanitizePath(input.path);
3668
+ } catch (error) {
3669
+ return {
3670
+ content: [
3671
+ {
3672
+ type: "text",
3673
+ text: `Error: ${error instanceof Error ? error.message : String(error)}`
3674
+ }
3675
+ ],
3676
+ isError: true
3677
+ };
3678
+ }
3679
+ let diff;
3680
+ try {
3681
+ diff = await getDiff(projectPath, input.diff);
3682
+ } catch (error) {
3683
+ return {
3684
+ content: [
3685
+ {
3686
+ type: "text",
3687
+ text: `Error: ${error instanceof Error ? error.message : String(error)}`
3688
+ }
3689
+ ],
3690
+ isError: true
3691
+ };
3692
+ }
3693
+ const diffLines = diff.split("\n").length;
3694
+ let effectiveDepth = input.depth;
3695
+ let downgraded = false;
3696
+ if (effectiveDepth === "deep" && diffLines > SIZE_GATE_LINES) {
3697
+ effectiveDepth = "standard";
3698
+ downgraded = true;
3699
+ }
3700
+ try {
3701
+ if (effectiveDepth === "quick") {
3702
+ const { handleAnalyzeDiff: handleAnalyzeDiff2 } = await import("./feedback-WEVQSLAA.js");
3703
+ const result2 = await handleAnalyzeDiff2({ diff, path: projectPath });
3704
+ const firstContent = result2.content[0];
3705
+ if (!firstContent) throw new Error("Empty analyze_diff response");
3706
+ const parsed2 = JSON.parse(firstContent.text);
3707
+ return {
3708
+ content: [
3709
+ {
3710
+ type: "text",
3711
+ text: JSON.stringify({
3712
+ depth: "quick",
3713
+ downgraded,
3714
+ findings: parsed2.findings ?? parsed2.warnings ?? [],
3715
+ fileCount: parsed2.summary?.filesChanged ?? parsed2.files?.length ?? 0,
3716
+ lineCount: diffLines,
3717
+ ...result2.isError ? { error: parsed2 } : {}
3718
+ })
3719
+ }
3720
+ ]
3721
+ };
3722
+ }
3723
+ if (effectiveDepth === "standard") {
3724
+ const { handleAnalyzeDiff: handleAnalyzeDiff2, handleCreateSelfReview: handleCreateSelfReview2 } = await import("./feedback-WEVQSLAA.js");
3725
+ const [diffResult, reviewResult] = await Promise.all([
3726
+ handleAnalyzeDiff2({ diff, path: projectPath }),
3727
+ handleCreateSelfReview2({ path: projectPath, diff })
3728
+ ]);
3729
+ const diffContent = diffResult.content[0];
3730
+ const reviewContent = reviewResult.content[0];
3731
+ if (!diffContent || !reviewContent) throw new Error("Empty review response");
3732
+ const diffParsed = JSON.parse(diffContent.text);
3733
+ const reviewParsed = JSON.parse(reviewContent.text);
3734
+ const findings = [
3735
+ ...diffParsed.findings ?? diffParsed.warnings ?? [],
3736
+ ...reviewParsed.findings ?? reviewParsed.items ?? []
3737
+ ];
3738
+ return {
3739
+ content: [
3740
+ {
3741
+ type: "text",
3742
+ text: JSON.stringify({
3743
+ depth: "standard",
3744
+ downgraded,
3745
+ findings,
3746
+ diffAnalysis: diffParsed,
3747
+ selfReview: reviewParsed,
3748
+ fileCount: diffParsed.summary?.filesChanged ?? diffParsed.files?.length ?? 0,
3749
+ lineCount: diffLines
3750
+ })
3751
+ }
3752
+ ]
3753
+ };
3754
+ }
3755
+ const { handleRunCodeReview: handleRunCodeReview2 } = await import("./review-pipeline-4JTQAWKW.js");
3756
+ const result = await handleRunCodeReview2({ path: projectPath, diff });
3757
+ const deepContent = result.content[0];
3758
+ if (!deepContent) throw new Error("Empty code review response");
3759
+ const parsed = JSON.parse(deepContent.text);
3760
+ return {
3761
+ content: [
3762
+ {
3763
+ type: "text",
3764
+ text: JSON.stringify({
3765
+ depth: "deep",
3766
+ downgraded: false,
3767
+ findings: parsed.findings ?? [],
3768
+ assessment: parsed.assessment,
3769
+ findingCount: parsed.findingCount,
3770
+ lineCount: diffLines,
3771
+ pipeline: parsed
3772
+ })
3773
+ }
3774
+ ]
3775
+ };
3776
+ } catch (error) {
3777
+ return {
3778
+ content: [
3779
+ {
3780
+ type: "text",
3781
+ text: `Error: ${error instanceof Error ? error.message : String(error)}`
3782
+ }
3783
+ ],
3784
+ isError: true
3785
+ };
3786
+ }
3787
+ }
3788
+
3789
+ // src/mcp/server.ts
3790
+ var TOOL_DEFINITIONS = [
3791
+ validateToolDefinition,
3792
+ checkDependenciesDefinition,
3793
+ checkDocsDefinition,
3794
+ detectEntropyDefinition,
3795
+ generateLinterDefinition,
3796
+ validateLinterConfigDefinition,
3797
+ initProjectDefinition,
3798
+ listPersonasDefinition,
3799
+ generatePersonaArtifactsDefinition,
3800
+ runPersonaDefinition,
3801
+ addComponentDefinition,
3802
+ runAgentTaskDefinition,
3803
+ runSkillDefinition,
3804
+ manageStateDefinition,
3805
+ createSelfReviewDefinition,
3806
+ analyzeDiffDefinition,
3807
+ requestPeerReviewDefinition,
3808
+ checkPhaseGateDefinition,
3809
+ validateCrossCheckDefinition,
3810
+ createSkillDefinition,
3811
+ generateSlashCommandsDefinition,
3812
+ queryGraphDefinition,
3813
+ searchSimilarDefinition,
3814
+ findContextForDefinition,
3815
+ getRelationshipsDefinition,
3816
+ getImpactDefinition,
3817
+ ingestSourceDefinition,
3818
+ generateAgentDefinitionsDefinition,
3819
+ runSecurityScanDefinition,
3820
+ checkPerformanceDefinition,
3821
+ getPerfBaselinesDefinition,
3822
+ updatePerfBaselinesDefinition,
3823
+ getCriticalPathsDefinition,
3824
+ listStreamsDefinition,
3825
+ manageRoadmapDefinition,
3826
+ emitInteractionDefinition,
3827
+ runCodeReviewDefinition,
3828
+ gatherContextDefinition,
3829
+ assessProjectDefinition,
3830
+ reviewChangesDefinition,
3831
+ detectAnomaliesDefinition
3832
+ ];
3833
+ var TOOL_HANDLERS = {
3834
+ validate_project: handleValidateProject,
3835
+ check_dependencies: handleCheckDependencies,
3836
+ check_docs: handleCheckDocs,
3837
+ detect_entropy: handleDetectEntropy,
3838
+ generate_linter: handleGenerateLinter,
3839
+ validate_linter_config: handleValidateLinterConfig,
3840
+ init_project: handleInitProject,
3841
+ list_personas: handleListPersonas,
3842
+ generate_persona_artifacts: handleGeneratePersonaArtifacts,
3843
+ run_persona: handleRunPersona,
3844
+ add_component: handleAddComponent,
3845
+ run_agent_task: handleRunAgentTask,
3846
+ run_skill: handleRunSkill,
3847
+ manage_state: handleManageState,
3848
+ create_self_review: handleCreateSelfReview,
3849
+ analyze_diff: handleAnalyzeDiff,
3850
+ request_peer_review: handleRequestPeerReview,
3851
+ check_phase_gate: handleCheckPhaseGate,
3852
+ validate_cross_check: handleValidateCrossCheck,
3853
+ create_skill: handleCreateSkill,
3854
+ generate_slash_commands: handleGenerateSlashCommands,
3855
+ query_graph: handleQueryGraph,
3856
+ search_similar: handleSearchSimilar,
3857
+ find_context_for: handleFindContextFor,
3858
+ get_relationships: handleGetRelationships,
3859
+ get_impact: handleGetImpact,
3860
+ ingest_source: handleIngestSource,
3861
+ generate_agent_definitions: handleGenerateAgentDefinitions,
3862
+ run_security_scan: handleRunSecurityScan,
3863
+ check_performance: handleCheckPerformance,
3864
+ get_perf_baselines: handleGetPerfBaselines,
3865
+ update_perf_baselines: handleUpdatePerfBaselines,
3866
+ get_critical_paths: handleGetCriticalPaths,
3867
+ list_streams: handleListStreams,
3868
+ manage_roadmap: handleManageRoadmap,
3869
+ emit_interaction: handleEmitInteraction,
3870
+ run_code_review: handleRunCodeReview,
3871
+ gather_context: handleGatherContext,
3872
+ assess_project: handleAssessProject,
3873
+ review_changes: handleReviewChanges,
3874
+ detect_anomalies: handleDetectAnomalies
3875
+ };
3876
+ var RESOURCE_DEFINITIONS = [
3877
+ {
3878
+ uri: "harness://skills",
3879
+ name: "Harness Skills",
3880
+ description: "Available skills with metadata (name, description, cognitive_mode, type, triggers)",
3881
+ mimeType: "application/json"
3882
+ },
3883
+ {
3884
+ uri: "harness://rules",
3885
+ name: "Harness Rules",
3886
+ description: "Active linter rules and constraints from harness config",
3887
+ mimeType: "application/json"
3888
+ },
3889
+ {
3890
+ uri: "harness://project",
3891
+ name: "Project Context",
3892
+ description: "Project structure and agent instructions from AGENTS.md",
3893
+ mimeType: "text/markdown"
3894
+ },
3895
+ {
3896
+ uri: "harness://learnings",
3897
+ name: "Learnings",
3898
+ description: "Review learnings and anti-pattern log from .harness/",
3899
+ mimeType: "text/markdown"
3900
+ },
3901
+ {
3902
+ uri: "harness://state",
3903
+ name: "Project State",
3904
+ description: "Current harness state including position, progress, decisions, and blockers",
3905
+ mimeType: "application/json"
3906
+ },
3907
+ {
3908
+ uri: "harness://graph",
3909
+ name: "Knowledge Graph",
3910
+ description: "Graph statistics, node/edge counts by type, staleness",
3911
+ mimeType: "application/json"
3912
+ },
3913
+ {
3914
+ uri: "harness://entities",
3915
+ name: "Graph Entities",
3916
+ description: "All entity nodes with types and metadata",
3917
+ mimeType: "application/json"
3918
+ },
3919
+ {
3920
+ uri: "harness://relationships",
3921
+ name: "Graph Relationships",
3922
+ description: "All edges with types, confidence scores, and timestamps",
3923
+ mimeType: "application/json"
3924
+ }
3925
+ ];
3926
+ var RESOURCE_HANDLERS = {
3927
+ "harness://skills": getSkillsResource,
3928
+ "harness://rules": getRulesResource,
3929
+ "harness://project": getProjectResource,
3930
+ "harness://learnings": getLearningsResource,
3931
+ "harness://state": getStateResource,
3932
+ "harness://graph": getGraphResource,
3933
+ "harness://entities": getEntitiesResource,
3934
+ "harness://relationships": getRelationshipsResource
3935
+ };
3936
+ function getToolDefinitions() {
3937
+ return TOOL_DEFINITIONS;
3938
+ }
3939
+ function createHarnessServer(projectRoot) {
3940
+ const resolvedRoot = projectRoot ?? process.cwd();
3941
+ let sessionChecked = false;
3942
+ const server = new Server(
3943
+ { name: "harness-engineering", version: "0.1.0" },
3944
+ { capabilities: { tools: {}, resources: {} } }
3945
+ );
3946
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
3947
+ tools: TOOL_DEFINITIONS
3948
+ }));
3949
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
3950
+ const { name, arguments: args } = request.params;
3951
+ const handler = TOOL_HANDLERS[name];
3952
+ if (!handler) {
3953
+ return { content: [{ type: "text", text: `Unknown tool: ${name}` }], isError: true };
3954
+ }
3955
+ const result = await handler(args ?? {});
3956
+ if (!sessionChecked) {
3957
+ sessionChecked = true;
3958
+ try {
3959
+ const {
3960
+ getUpdateNotification,
3961
+ isUpdateCheckEnabled,
3962
+ shouldRunCheck,
3963
+ readCheckState,
3964
+ spawnBackgroundCheck
3965
+ } = await import("./dist-PBTNVK6K.js");
3966
+ const { CLI_VERSION: version } = await import("./version-KFFPOQAX.js");
3967
+ let CLI_VERSION = version;
3968
+ let configInterval;
3969
+ try {
3970
+ const configResult = resolveProjectConfig(resolvedRoot);
3971
+ if (configResult.ok) {
3972
+ const raw = configResult.value.updateCheckInterval;
3973
+ if (typeof raw === "number" && Number.isInteger(raw) && raw >= 0) {
3974
+ configInterval = raw;
3975
+ }
3976
+ }
3977
+ } catch {
3978
+ }
3979
+ const DEFAULT_INTERVAL = 864e5;
3980
+ if (isUpdateCheckEnabled(configInterval)) {
3981
+ const state = readCheckState();
3982
+ if (shouldRunCheck(state, configInterval ?? DEFAULT_INTERVAL)) {
3983
+ spawnBackgroundCheck(CLI_VERSION);
3984
+ }
3985
+ const notification = getUpdateNotification(CLI_VERSION);
3986
+ if (notification) {
3987
+ result.content.push({ type: "text", text: `
3988
+ ---
3989
+ ${notification}` });
3990
+ }
3991
+ }
3992
+ } catch {
3993
+ }
3994
+ }
3995
+ return result;
3996
+ });
3997
+ server.setRequestHandler(ListResourcesRequestSchema, async () => ({
3998
+ resources: RESOURCE_DEFINITIONS
3999
+ }));
4000
+ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
4001
+ const uri = request.params.uri;
4002
+ const handler = RESOURCE_HANDLERS[uri];
4003
+ if (!handler) {
4004
+ throw new Error(`Unknown resource: ${uri}`);
4005
+ }
4006
+ const content = await handler(resolvedRoot);
4007
+ const resourceDef = RESOURCE_DEFINITIONS.find((r) => r.uri === uri);
4008
+ const mimeType = resourceDef?.mimeType ?? "text/plain";
4009
+ return {
4010
+ contents: [{ uri, text: content, mimeType }]
4011
+ };
4012
+ });
4013
+ return server;
4014
+ }
4015
+ async function startServer() {
4016
+ const server = createHarnessServer();
4017
+ const transport = new StdioServerTransport();
4018
+ await server.connect(transport);
4019
+ }
4020
+
4021
+ export {
4022
+ generateSlashCommands,
4023
+ handleOrphanDeletion,
4024
+ createGenerateSlashCommandsCommand,
4025
+ getToolDefinitions,
4026
+ createHarnessServer,
4027
+ startServer
4028
+ };