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