codecartographer-pi 0.1.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/.codecarto/BACKLOG.md +192 -0
- package/.codecarto/CHANGELOG-2026-05-02-feedback-pass.md +118 -0
- package/.codecarto/CONTRIBUTING.md +56 -0
- package/.codecarto/GUIDE.md +298 -0
- package/.codecarto/LICENSE +21 -0
- package/.codecarto/NEW_THREAD_BLURB.md +47 -0
- package/.codecarto/README.md +39 -0
- package/.codecarto/THREAD_LOG.md +39 -0
- package/.codecarto/closeouts/2026-05-02-framework-feedback-pass.md +111 -0
- package/.codecarto/findings/architecture/README.md +3 -0
- package/.codecarto/findings/architecture/SKILL.md +102 -0
- package/.codecarto/findings/build-and-deploy/.gitkeep +0 -0
- package/.codecarto/findings/config-model/.gitkeep +0 -0
- package/.codecarto/findings/contracts/README.md +3 -0
- package/.codecarto/findings/contracts/SKILL.md +89 -0
- package/.codecarto/findings/defect-scan/README.md +18 -0
- package/.codecarto/findings/defect-scan/SKILL.md +87 -0
- package/.codecarto/findings/defect-scan/passes/01-logic-and-correctness.md +50 -0
- package/.codecarto/findings/defect-scan/passes/02-error-handling.md +55 -0
- package/.codecarto/findings/defect-scan/passes/03-concurrency-and-resources.md +54 -0
- package/.codecarto/findings/defect-scan/passes/04-security-and-trust.md +62 -0
- package/.codecarto/findings/defect-scan/passes/05-api-contract-violations.md +57 -0
- package/.codecarto/findings/defect-scan/passes/06-config-and-environment.md +58 -0
- package/.codecarto/findings/defect-scan-mechanical/README.md +17 -0
- package/.codecarto/findings/defect-scan-mechanical/SKILL.md +60 -0
- package/.codecarto/findings/defect-scan-semantic/README.md +17 -0
- package/.codecarto/findings/defect-scan-semantic/SKILL.md +54 -0
- package/.codecarto/findings/porting/README.md +3 -0
- package/.codecarto/findings/porting/SKILL.md +52 -0
- package/.codecarto/findings/protocols/README.md +3 -0
- package/.codecarto/findings/protocols/SKILL.md +87 -0
- package/.codecarto/findings/public-surfaces/README.md +3 -0
- package/.codecarto/findings/reimplementation-spec/README.md +3 -0
- package/.codecarto/findings/reimplementation-spec/SKILL.md +66 -0
- package/.codecarto/findings/runtime-lifecycle/README.md +3 -0
- package/.codecarto/findings/state-and-storage/README.md +3 -0
- package/.codecarto/scratch/.gitkeep +0 -0
- package/.codecarto/skills/spec-delta-application/SKILL.md +102 -0
- package/.codecarto/templates/architecture-map.md +143 -0
- package/.codecarto/templates/behavioral-contracts.md +134 -0
- package/.codecarto/templates/closeout-template.md +85 -0
- package/.codecarto/templates/conventions-template.md +65 -0
- package/.codecarto/templates/decisions-template.md +82 -0
- package/.codecarto/templates/defect-fix-tracker.md +77 -0
- package/.codecarto/templates/defect-report.md +116 -0
- package/.codecarto/templates/deltas-applied.md +71 -0
- package/.codecarto/templates/mechanical-defects.md +104 -0
- package/.codecarto/templates/protocols-and-state.md +126 -0
- package/.codecarto/templates/reimplementation-spec-opinionated.md +183 -0
- package/.codecarto/templates/reimplementation-spec.md +148 -0
- package/.codecarto/templates/reverse-engineering-bundle.md +141 -0
- package/.codecarto/templates/semantic-defects.md +109 -0
- package/.codecarto/templates/thread-log-entry-template.md +27 -0
- package/.codecarto/workflow/VALIDATE.md +81 -0
- package/.codecarto/workflow/pipeline-architecture-only.yaml +38 -0
- package/.codecarto/workflow/pipeline-defect-scan.yaml +61 -0
- package/.codecarto/workflow/pipeline-full-with-audit.yaml +188 -0
- package/.codecarto/workflow/pipeline-full-with-deep-audit.yaml +227 -0
- package/.codecarto/workflow/pipeline-lite.yaml +100 -0
- package/.codecarto/workflow/pipeline.yaml +163 -0
- package/.codecarto/workflow/status.yaml +64 -0
- package/LICENSE +21 -0
- package/README.md +356 -0
- package/core/index.ts +11 -0
- package/core/pipeline.ts +175 -0
- package/core/prompts.ts +183 -0
- package/core/status.ts +155 -0
- package/core/types.ts +96 -0
- package/core/utils.ts +52 -0
- package/core/workspace.ts +81 -0
- package/core/yaml.ts +256 -0
- package/extensions/codecarto/index.ts +446 -0
- package/mcp-server/bin.mjs +7 -0
- package/mcp-server/server.ts +497 -0
- package/package.json +52 -0
|
@@ -0,0 +1,497 @@
|
|
|
1
|
+
// CodeCartographer MCP server. Exposes the framework as seven JSON-RPC tools
|
|
2
|
+
// equivalent to the seven /codecarto-* commands the Pi extension registers.
|
|
3
|
+
// Both wrappers import their primitives from ../core/index.ts so phase prompts,
|
|
4
|
+
// status normalization, validation, and atomic completion are byte-identical
|
|
5
|
+
// across surfaces.
|
|
6
|
+
//
|
|
7
|
+
// Transport: stdio (the default for MCP servers spawned as subprocesses by
|
|
8
|
+
// hosts like Claude Code or Claude Desktop).
|
|
9
|
+
//
|
|
10
|
+
// Each tool takes an absolute `cwd` (the project the host wants to analyze).
|
|
11
|
+
// Tools that produce phase or skill text return it inline as the tool result;
|
|
12
|
+
// the host decides how to surface it (display, feed to the agent, etc.).
|
|
13
|
+
|
|
14
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
15
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
16
|
+
import {
|
|
17
|
+
CallToolRequestSchema,
|
|
18
|
+
ErrorCode,
|
|
19
|
+
ListToolsRequestSchema,
|
|
20
|
+
McpError,
|
|
21
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
22
|
+
import { cp, mkdir, rm, writeFile } from "node:fs/promises";
|
|
23
|
+
import { basename, isAbsolute, join } from "node:path";
|
|
24
|
+
|
|
25
|
+
import {
|
|
26
|
+
buildPhasePrompt,
|
|
27
|
+
buildSkillPrompt,
|
|
28
|
+
buildThreadLogEntry,
|
|
29
|
+
buildValidationSummary,
|
|
30
|
+
canonicalPath,
|
|
31
|
+
closeoutFileName,
|
|
32
|
+
createEmptyStatus,
|
|
33
|
+
dateOnly,
|
|
34
|
+
DEFAULT_PIPELINE_PATH,
|
|
35
|
+
ensureCloseoutStub,
|
|
36
|
+
getNextEligiblePhase,
|
|
37
|
+
getPipelineLabel,
|
|
38
|
+
getWorkspaceState,
|
|
39
|
+
listSkillNames,
|
|
40
|
+
loadYamlFile,
|
|
41
|
+
normalizeForComparison,
|
|
42
|
+
normalizeStatus,
|
|
43
|
+
type OpenQuestionEntry,
|
|
44
|
+
packagedWorkspaceDir,
|
|
45
|
+
pathExists,
|
|
46
|
+
PIPELINE_ALIASES,
|
|
47
|
+
type PipelineFile,
|
|
48
|
+
resolvePhase,
|
|
49
|
+
resolvePipelineChoice,
|
|
50
|
+
type StatusFile,
|
|
51
|
+
stringifySimpleYaml,
|
|
52
|
+
uniqueStrings,
|
|
53
|
+
updateStatusAtomically,
|
|
54
|
+
validatePhaseOutput,
|
|
55
|
+
type WorkspaceState,
|
|
56
|
+
} from "../core/index.ts";
|
|
57
|
+
|
|
58
|
+
// ---------- input helpers ----------
|
|
59
|
+
|
|
60
|
+
async function validateCwd(cwd: unknown): Promise<string> {
|
|
61
|
+
if (typeof cwd !== "string" || !cwd.trim()) {
|
|
62
|
+
throw new McpError(ErrorCode.InvalidParams, "cwd is required");
|
|
63
|
+
}
|
|
64
|
+
if (!isAbsolute(cwd)) {
|
|
65
|
+
throw new McpError(ErrorCode.InvalidParams, `cwd must be an absolute path, got: ${cwd}`);
|
|
66
|
+
}
|
|
67
|
+
if (!(await pathExists(cwd))) {
|
|
68
|
+
throw new McpError(ErrorCode.InvalidParams, `cwd does not exist: ${cwd}`);
|
|
69
|
+
}
|
|
70
|
+
return cwd;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function requireWorkspace(cwd: string): Promise<WorkspaceState> {
|
|
74
|
+
const state = await getWorkspaceState(cwd).catch((error) => {
|
|
75
|
+
throw new McpError(ErrorCode.InternalError, error instanceof Error ? error.message : String(error));
|
|
76
|
+
});
|
|
77
|
+
if (!state) {
|
|
78
|
+
throw new McpError(
|
|
79
|
+
ErrorCode.InvalidRequest,
|
|
80
|
+
`No CodeCartographer workspace at ${cwd}. Call codecarto_init first.`,
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
return state;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function textResult(text: string, structured?: Record<string, unknown>) {
|
|
87
|
+
const result: { content: Array<{ type: "text"; text: string }>; structuredContent?: Record<string, unknown> } = {
|
|
88
|
+
content: [{ type: "text", text }],
|
|
89
|
+
};
|
|
90
|
+
if (structured) result.structuredContent = structured;
|
|
91
|
+
return result;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ---------- handlers ----------
|
|
95
|
+
|
|
96
|
+
export async function handleInit(args: { cwd: string; pipeline?: string; force?: boolean }) {
|
|
97
|
+
const cwd = await validateCwd(args.cwd);
|
|
98
|
+
const pipelineChoice = args.pipeline ? resolvePipelineChoice(args.pipeline) : null;
|
|
99
|
+
if (args.pipeline && !pipelineChoice) {
|
|
100
|
+
throw new McpError(ErrorCode.InvalidParams, `Unknown pipeline: ${args.pipeline}`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (!(await pathExists(packagedWorkspaceDir))) {
|
|
104
|
+
throw new McpError(ErrorCode.InternalError, "Packaged .codecarto assets are missing on the MCP server.");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const targetWorkspaceDir = join(cwd, ".codecarto");
|
|
108
|
+
const targetExists = await pathExists(targetWorkspaceDir);
|
|
109
|
+
let sameWorkspace = false;
|
|
110
|
+
if (targetExists) {
|
|
111
|
+
sameWorkspace =
|
|
112
|
+
normalizeForComparison(await canonicalPath(targetWorkspaceDir)) ===
|
|
113
|
+
normalizeForComparison(await canonicalPath(packagedWorkspaceDir));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (targetExists && !sameWorkspace) {
|
|
117
|
+
if (!args.force) {
|
|
118
|
+
throw new McpError(
|
|
119
|
+
ErrorCode.InvalidRequest,
|
|
120
|
+
`A .codecarto/ directory already exists at ${targetWorkspaceDir}. Pass force: true to overwrite it.`,
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
await rm(targetWorkspaceDir, { recursive: true, force: true });
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (!(await pathExists(targetWorkspaceDir))) {
|
|
127
|
+
await mkdir(cwd, { recursive: true });
|
|
128
|
+
await cp(packagedWorkspaceDir, targetWorkspaceDir, { recursive: true });
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const statusPath = join(targetWorkspaceDir, "workflow", "status.yaml");
|
|
132
|
+
const rawStatus = (await loadYamlFile<StatusFile>(statusPath)) ?? {};
|
|
133
|
+
const selectedPipelinePath = pipelineChoice ?? rawStatus.pipeline?.trim() ?? DEFAULT_PIPELINE_PATH;
|
|
134
|
+
const resolvedPipelinePath = join(targetWorkspaceDir, selectedPipelinePath);
|
|
135
|
+
|
|
136
|
+
if (!(await pathExists(resolvedPipelinePath))) {
|
|
137
|
+
throw new McpError(ErrorCode.InvalidParams, `Pipeline file not found: ${selectedPipelinePath}`);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const pipeline = await loadYamlFile<PipelineFile>(resolvedPipelinePath);
|
|
141
|
+
const normalizedStatus = createEmptyStatus(basename(cwd), selectedPipelinePath, pipeline);
|
|
142
|
+
normalizedStatus.last_updated = new Date().toISOString();
|
|
143
|
+
await writeFile(statusPath, `${stringifySimpleYaml(normalizedStatus)}\n`, "utf8");
|
|
144
|
+
|
|
145
|
+
const label = getPipelineLabel(selectedPipelinePath);
|
|
146
|
+
return textResult(
|
|
147
|
+
`Initialized CodeCartographer workspace at ${targetWorkspaceDir}.\nPipeline: ${label} (${selectedPipelinePath})\nFirst phase: ${normalizedStatus.current_phase}`,
|
|
148
|
+
{
|
|
149
|
+
workspaceDir: targetWorkspaceDir,
|
|
150
|
+
pipeline: selectedPipelinePath,
|
|
151
|
+
pipelineLabel: label,
|
|
152
|
+
firstPhase: normalizedStatus.current_phase,
|
|
153
|
+
},
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export async function handleStatus(args: { cwd: string }) {
|
|
158
|
+
const cwd = await validateCwd(args.cwd);
|
|
159
|
+
const state = await requireWorkspace(cwd);
|
|
160
|
+
const nextPhase = getNextEligiblePhase(state);
|
|
161
|
+
const currentPhase = nextPhase?.id ?? state.status.current_phase ?? "complete";
|
|
162
|
+
const completed = state.pipeline.phase_order.filter((id) => state.status.phases[id]?.status === "complete").length;
|
|
163
|
+
const totalCarryForward = Object.values(state.status.phases).reduce(
|
|
164
|
+
(sum, phase) => sum + (phase.carry_forward?.length ?? 0),
|
|
165
|
+
0,
|
|
166
|
+
);
|
|
167
|
+
const currentOpenQuestions =
|
|
168
|
+
currentPhase === "complete" ? 0 : state.status.phases[currentPhase]?.open_questions.length ?? 0;
|
|
169
|
+
const summary = [
|
|
170
|
+
`Phase: ${currentPhase}`,
|
|
171
|
+
`Pipeline: ${getPipelineLabel(state.status.pipeline)} (${state.status.pipeline})`,
|
|
172
|
+
`Progress: ${completed}/${state.pipeline.phase_order.length} complete`,
|
|
173
|
+
`Open questions (current phase): ${currentOpenQuestions}`,
|
|
174
|
+
`Carry-forward (all phases): ${totalCarryForward}`,
|
|
175
|
+
`Next: ${state.status.next_actions[0] ?? (nextPhase ? `Begin ${nextPhase.id}` : "All phases complete.")}`,
|
|
176
|
+
].join("\n");
|
|
177
|
+
return textResult(summary, {
|
|
178
|
+
currentPhase,
|
|
179
|
+
pipeline: state.status.pipeline,
|
|
180
|
+
pipelineLabel: getPipelineLabel(state.status.pipeline),
|
|
181
|
+
completed,
|
|
182
|
+
total: state.pipeline.phase_order.length,
|
|
183
|
+
openQuestionsCurrentPhase: currentOpenQuestions,
|
|
184
|
+
carryForwardTotal: totalCarryForward,
|
|
185
|
+
nextActions: state.status.next_actions,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export async function handleNext(args: { cwd: string }) {
|
|
190
|
+
const cwd = await validateCwd(args.cwd);
|
|
191
|
+
const state = await requireWorkspace(cwd);
|
|
192
|
+
const phase = getNextEligiblePhase(state);
|
|
193
|
+
if (!phase) {
|
|
194
|
+
return textResult("All CodeCartographer phases are complete. Run codecarto_skill for post-pipeline work.", {
|
|
195
|
+
complete: true,
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
const prompt = await buildPhasePrompt(state, phase, false);
|
|
199
|
+
return textResult(prompt, { phase: phase.id, forced: false });
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export async function handlePhase(args: { cwd: string; phase: string }) {
|
|
203
|
+
if (typeof args.phase !== "string" || !args.phase.trim()) {
|
|
204
|
+
throw new McpError(ErrorCode.InvalidParams, "phase is required");
|
|
205
|
+
}
|
|
206
|
+
const cwd = await validateCwd(args.cwd);
|
|
207
|
+
const state = await requireWorkspace(cwd);
|
|
208
|
+
const phase = resolvePhase(state, args.phase);
|
|
209
|
+
if (!phase) {
|
|
210
|
+
throw new McpError(ErrorCode.InvalidParams, `Unknown phase: ${args.phase}`);
|
|
211
|
+
}
|
|
212
|
+
const prompt = await buildPhasePrompt(state, phase, true);
|
|
213
|
+
return textResult(prompt, { phase: phase.id, forced: true });
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export async function handleValidate(args: { cwd: string; phase?: string }) {
|
|
217
|
+
const cwd = await validateCwd(args.cwd);
|
|
218
|
+
const state = await requireWorkspace(cwd);
|
|
219
|
+
const validation = await validatePhaseOutput(state, args.phase?.trim() || undefined).catch((error) => {
|
|
220
|
+
throw new McpError(ErrorCode.InvalidParams, error instanceof Error ? error.message : String(error));
|
|
221
|
+
});
|
|
222
|
+
const summary = buildValidationSummary(validation).join("\n");
|
|
223
|
+
return textResult(summary, {
|
|
224
|
+
phaseId: validation.phaseId,
|
|
225
|
+
overall: validation.overall,
|
|
226
|
+
exists: validation.exists,
|
|
227
|
+
hasValidationBlock: validation.hasValidationBlock,
|
|
228
|
+
primaryOutput: validation.primaryOutput,
|
|
229
|
+
rows: validation.rows,
|
|
230
|
+
gaps: validation.gaps,
|
|
231
|
+
errors: validation.errors,
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export async function handleComplete(args: { cwd: string; phase?: string }) {
|
|
236
|
+
const cwd = await validateCwd(args.cwd);
|
|
237
|
+
const initialState = await requireWorkspace(cwd);
|
|
238
|
+
const validation = await validatePhaseOutput(initialState, args.phase?.trim() || undefined).catch((error) => {
|
|
239
|
+
throw new McpError(ErrorCode.InvalidParams, error instanceof Error ? error.message : String(error));
|
|
240
|
+
});
|
|
241
|
+
if (validation.overall === "FAIL" || validation.overall === "MISSING") {
|
|
242
|
+
throw new McpError(
|
|
243
|
+
ErrorCode.InvalidRequest,
|
|
244
|
+
`Cannot complete ${validation.phaseId}: validation is ${validation.overall}.\n${buildValidationSummary(validation).join("\n")}`,
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const completionTimestamp = new Date().toISOString();
|
|
249
|
+
const updatedState = await updateStatusAtomically(cwd, (lockedState) => {
|
|
250
|
+
const phase = resolvePhase(lockedState, validation.phaseId);
|
|
251
|
+
if (!phase?.primary_output) {
|
|
252
|
+
throw new Error(`Phase ${validation.phaseId} is missing primary_output.`);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const nextStatus = normalizeStatus(lockedState.status, lockedState.pipeline, lockedState.status.pipeline, lockedState.cwd);
|
|
256
|
+
const existingPhase = nextStatus.phases[validation.phaseId] ?? {
|
|
257
|
+
status: "pending",
|
|
258
|
+
owner_notes: [],
|
|
259
|
+
outputs_present: [],
|
|
260
|
+
open_questions: [],
|
|
261
|
+
carry_forward: [],
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
const gapEntries: OpenQuestionEntry[] = validation.rows
|
|
265
|
+
.filter((row) => row.result.toUpperCase().includes("PARTIAL"))
|
|
266
|
+
.map((row) => ({
|
|
267
|
+
kind: "needs-maintainer-decision",
|
|
268
|
+
description: row.criterion || "Partial validation gap",
|
|
269
|
+
deferred_reason: row.evidence || "Marked PARTIAL by validation",
|
|
270
|
+
}));
|
|
271
|
+
|
|
272
|
+
const mergedOpenQuestions: OpenQuestionEntry[] = [...existingPhase.open_questions];
|
|
273
|
+
for (const candidate of gapEntries) {
|
|
274
|
+
const dupe = mergedOpenQuestions.some(
|
|
275
|
+
(entry) => entry.description === candidate.description && entry.deferred_reason === candidate.deferred_reason,
|
|
276
|
+
);
|
|
277
|
+
if (!dupe) mergedOpenQuestions.push(candidate);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
nextStatus.phases[validation.phaseId] = {
|
|
281
|
+
status: "complete",
|
|
282
|
+
owner_notes: uniqueStrings([
|
|
283
|
+
...existingPhase.owner_notes,
|
|
284
|
+
`Completed via codecarto_complete on ${completionTimestamp}.`,
|
|
285
|
+
`Primary output: .codecarto/${validation.primaryOutput}`,
|
|
286
|
+
`Validation: ${validation.overall}`,
|
|
287
|
+
]).slice(-3),
|
|
288
|
+
outputs_present: uniqueStrings([...existingPhase.outputs_present, validation.primaryOutput]),
|
|
289
|
+
open_questions: mergedOpenQuestions,
|
|
290
|
+
carry_forward: existingPhase.carry_forward ?? [],
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
nextStatus.last_updated = completionTimestamp;
|
|
294
|
+
const updatedWorkspaceState: WorkspaceState = {
|
|
295
|
+
...lockedState,
|
|
296
|
+
status: nextStatus,
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
const nextEligible = getNextEligiblePhase(updatedWorkspaceState);
|
|
300
|
+
nextStatus.current_phase = nextEligible?.id ?? "complete";
|
|
301
|
+
nextStatus.next_actions = nextEligible
|
|
302
|
+
? [`Begin ${nextEligible.id} phase by producing ${nextEligible.primary_output ?? `findings/${nextEligible.id}/`}`]
|
|
303
|
+
: ["All phases complete. Review findings, open questions, and downstream implementation notes."];
|
|
304
|
+
|
|
305
|
+
return {
|
|
306
|
+
state: { ...updatedWorkspaceState, status: nextStatus },
|
|
307
|
+
threadLogEntry: buildThreadLogEntry(validation.phaseId, validation, completionTimestamp),
|
|
308
|
+
};
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
let closeoutNotice: string | undefined;
|
|
312
|
+
try {
|
|
313
|
+
const created = await ensureCloseoutStub(updatedState.workspaceDir, validation.phaseId, completionTimestamp);
|
|
314
|
+
if (created) {
|
|
315
|
+
closeoutNotice = `Closeout stub created: .codecarto/closeouts/${closeoutFileName(dateOnly(completionTimestamp), validation.phaseId)} (fill it in)`;
|
|
316
|
+
}
|
|
317
|
+
} catch (error) {
|
|
318
|
+
closeoutNotice = `Closeout stub not created: ${error instanceof Error ? error.message : String(error)}`;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const lines = [
|
|
322
|
+
`Marked ${validation.phaseId} complete (validation: ${validation.overall}).`,
|
|
323
|
+
`Next phase: ${updatedState.status.current_phase}`,
|
|
324
|
+
];
|
|
325
|
+
if (closeoutNotice) lines.push(closeoutNotice);
|
|
326
|
+
|
|
327
|
+
return textResult(lines.join("\n"), {
|
|
328
|
+
completedPhase: validation.phaseId,
|
|
329
|
+
validation: validation.overall,
|
|
330
|
+
nextPhase: updatedState.status.current_phase,
|
|
331
|
+
closeoutNotice,
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
export async function handleSkill(args: { cwd: string; name: string }) {
|
|
336
|
+
if (typeof args.name !== "string" || !args.name.trim()) {
|
|
337
|
+
throw new McpError(ErrorCode.InvalidParams, "name is required");
|
|
338
|
+
}
|
|
339
|
+
const cwd = await validateCwd(args.cwd);
|
|
340
|
+
const state = await requireWorkspace(cwd);
|
|
341
|
+
const nextPhase = getNextEligiblePhase(state);
|
|
342
|
+
if (nextPhase) {
|
|
343
|
+
throw new McpError(
|
|
344
|
+
ErrorCode.InvalidRequest,
|
|
345
|
+
`Cannot run skill: pipeline is not complete (next phase: ${nextPhase.id}). Finish the pipeline first.`,
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
const skillFile = join(state.workspaceDir, "skills", args.name, "SKILL.md");
|
|
349
|
+
if (!(await pathExists(skillFile))) {
|
|
350
|
+
const available = await listSkillNames(state.workspaceDir);
|
|
351
|
+
const hint = available.length > 0 ? ` Available: ${available.join(", ")}.` : " No skills installed.";
|
|
352
|
+
throw new McpError(ErrorCode.InvalidParams, `Unknown skill: ${args.name}.${hint}`);
|
|
353
|
+
}
|
|
354
|
+
const prompt = await buildSkillPrompt(state, args.name);
|
|
355
|
+
return textResult(prompt, { skill: args.name });
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// ---------- tool registry ----------
|
|
359
|
+
|
|
360
|
+
const TOOLS = [
|
|
361
|
+
{
|
|
362
|
+
name: "codecarto_init",
|
|
363
|
+
description:
|
|
364
|
+
"Initialize a CodeCartographer workspace (.codecarto/) in a target repository. Copies the packaged framework template and writes a fresh status.yaml for the chosen pipeline. Errors if .codecarto/ already exists unless force is true.",
|
|
365
|
+
inputSchema: {
|
|
366
|
+
type: "object",
|
|
367
|
+
properties: {
|
|
368
|
+
cwd: { type: "string", description: "Absolute path to the target repository." },
|
|
369
|
+
pipeline: {
|
|
370
|
+
type: "string",
|
|
371
|
+
description: `Pipeline alias (one of ${Object.keys(PIPELINE_ALIASES).join(", ")}) or workflow/*.yaml path. Defaults to the framework's default pipeline.`,
|
|
372
|
+
},
|
|
373
|
+
force: {
|
|
374
|
+
type: "boolean",
|
|
375
|
+
description: "Overwrite an existing .codecarto/ directory if present (default false).",
|
|
376
|
+
},
|
|
377
|
+
},
|
|
378
|
+
required: ["cwd"],
|
|
379
|
+
},
|
|
380
|
+
},
|
|
381
|
+
{
|
|
382
|
+
name: "codecarto_status",
|
|
383
|
+
description: "Show the current CodeCartographer phase, active pipeline, and progress for a target repository.",
|
|
384
|
+
inputSchema: {
|
|
385
|
+
type: "object",
|
|
386
|
+
properties: { cwd: { type: "string", description: "Absolute path to the target repository." } },
|
|
387
|
+
required: ["cwd"],
|
|
388
|
+
},
|
|
389
|
+
},
|
|
390
|
+
{
|
|
391
|
+
name: "codecarto_next",
|
|
392
|
+
description:
|
|
393
|
+
"Return the prompt text for the next eligible CodeCartographer phase. The host should feed this prompt back to the agent or display it to the user.",
|
|
394
|
+
inputSchema: {
|
|
395
|
+
type: "object",
|
|
396
|
+
properties: { cwd: { type: "string", description: "Absolute path to the target repository." } },
|
|
397
|
+
required: ["cwd"],
|
|
398
|
+
},
|
|
399
|
+
},
|
|
400
|
+
{
|
|
401
|
+
name: "codecarto_phase",
|
|
402
|
+
description:
|
|
403
|
+
"Return the prompt text for a specific CodeCartographer phase, even if it is not the next eligible phase. Used to revisit a phase or to bypass DAG order intentionally.",
|
|
404
|
+
inputSchema: {
|
|
405
|
+
type: "object",
|
|
406
|
+
properties: {
|
|
407
|
+
cwd: { type: "string", description: "Absolute path to the target repository." },
|
|
408
|
+
phase: { type: "string", description: "Phase id from the active pipeline." },
|
|
409
|
+
},
|
|
410
|
+
required: ["cwd", "phase"],
|
|
411
|
+
},
|
|
412
|
+
},
|
|
413
|
+
{
|
|
414
|
+
name: "codecarto_validate",
|
|
415
|
+
description:
|
|
416
|
+
"Validate a phase's primary output against the validation block in the produced markdown. Returns overall PASS/PASS WITH GAPS/FAIL/MISSING plus the parsed criteria rows. If phase is omitted, validates the next eligible phase.",
|
|
417
|
+
inputSchema: {
|
|
418
|
+
type: "object",
|
|
419
|
+
properties: {
|
|
420
|
+
cwd: { type: "string", description: "Absolute path to the target repository." },
|
|
421
|
+
phase: { type: "string", description: "Phase id (optional)." },
|
|
422
|
+
},
|
|
423
|
+
required: ["cwd"],
|
|
424
|
+
},
|
|
425
|
+
},
|
|
426
|
+
{
|
|
427
|
+
name: "codecarto_complete",
|
|
428
|
+
description:
|
|
429
|
+
"Mark a phase complete. Requires the phase output's validation to be PASS or PASS WITH GAPS. Atomically updates status.yaml under a file lock, appends to THREAD_LOG.md, and creates a closeout stub from the template if one does not yet exist. If phase is omitted, completes the next eligible phase.",
|
|
430
|
+
inputSchema: {
|
|
431
|
+
type: "object",
|
|
432
|
+
properties: {
|
|
433
|
+
cwd: { type: "string", description: "Absolute path to the target repository." },
|
|
434
|
+
phase: { type: "string", description: "Phase id (optional)." },
|
|
435
|
+
},
|
|
436
|
+
required: ["cwd"],
|
|
437
|
+
},
|
|
438
|
+
},
|
|
439
|
+
{
|
|
440
|
+
name: "codecarto_skill",
|
|
441
|
+
description:
|
|
442
|
+
"Return the prompt text for a post-pipeline skill (only callable after all phases are complete). Use codecarto_status to confirm completion first.",
|
|
443
|
+
inputSchema: {
|
|
444
|
+
type: "object",
|
|
445
|
+
properties: {
|
|
446
|
+
cwd: { type: "string", description: "Absolute path to the target repository." },
|
|
447
|
+
name: { type: "string", description: "Skill name (a directory under .codecarto/skills/)." },
|
|
448
|
+
},
|
|
449
|
+
required: ["cwd", "name"],
|
|
450
|
+
},
|
|
451
|
+
},
|
|
452
|
+
] as const;
|
|
453
|
+
|
|
454
|
+
const HANDLERS: Record<string, (args: any) => Promise<unknown>> = {
|
|
455
|
+
codecarto_init: handleInit,
|
|
456
|
+
codecarto_status: handleStatus,
|
|
457
|
+
codecarto_next: handleNext,
|
|
458
|
+
codecarto_phase: handlePhase,
|
|
459
|
+
codecarto_validate: handleValidate,
|
|
460
|
+
codecarto_complete: handleComplete,
|
|
461
|
+
codecarto_skill: handleSkill,
|
|
462
|
+
};
|
|
463
|
+
|
|
464
|
+
// ---------- server bootstrap ----------
|
|
465
|
+
|
|
466
|
+
export function buildServer() {
|
|
467
|
+
const server = new Server(
|
|
468
|
+
{ name: "codecartographer", version: "0.1.0" },
|
|
469
|
+
{ capabilities: { tools: {} } },
|
|
470
|
+
);
|
|
471
|
+
|
|
472
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS as unknown as typeof TOOLS[number][] }));
|
|
473
|
+
|
|
474
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
475
|
+
const handler = HANDLERS[request.params.name];
|
|
476
|
+
if (!handler) {
|
|
477
|
+
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}`);
|
|
478
|
+
}
|
|
479
|
+
try {
|
|
480
|
+
return (await handler(request.params.arguments ?? {})) as Awaited<ReturnType<typeof handleStatus>>;
|
|
481
|
+
} catch (error) {
|
|
482
|
+
if (error instanceof McpError) throw error;
|
|
483
|
+
throw new McpError(
|
|
484
|
+
ErrorCode.InternalError,
|
|
485
|
+
error instanceof Error ? error.message : String(error),
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
return server;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
export async function startStdioServer() {
|
|
494
|
+
const server = buildServer();
|
|
495
|
+
const transport = new StdioServerTransport();
|
|
496
|
+
await server.connect(transport);
|
|
497
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "codecartographer-pi",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "CodeCartographer packaged for Pi as an extension-driven workflow wrapper.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"pi-package",
|
|
8
|
+
"pi",
|
|
9
|
+
"codecartographer",
|
|
10
|
+
"reverse-engineering",
|
|
11
|
+
"llm"
|
|
12
|
+
],
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"author": "James Sesler",
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/HuginnIndustries/CodeCartographer.git"
|
|
18
|
+
},
|
|
19
|
+
"bugs": {
|
|
20
|
+
"url": "https://github.com/HuginnIndustries/CodeCartographer/issues"
|
|
21
|
+
},
|
|
22
|
+
"homepage": "https://github.com/HuginnIndustries/CodeCartographer#readme",
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=22.6.0"
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
".codecarto/**/*",
|
|
28
|
+
"core/**/*",
|
|
29
|
+
"extensions/**/*",
|
|
30
|
+
"mcp-server/**/*",
|
|
31
|
+
"README.md",
|
|
32
|
+
"LICENSE"
|
|
33
|
+
],
|
|
34
|
+
"bin": {
|
|
35
|
+
"codecarto-mcp": "./mcp-server/bin.mjs"
|
|
36
|
+
},
|
|
37
|
+
"scripts": {
|
|
38
|
+
"test": "node --experimental-strip-types --disable-warning=ExperimentalWarning --test tests/*.test.mjs"
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"@modelcontextprotocol/sdk": "*"
|
|
42
|
+
},
|
|
43
|
+
"peerDependencies": {
|
|
44
|
+
"@mariozechner/pi-coding-agent": "*",
|
|
45
|
+
"@sinclair/typebox": "*"
|
|
46
|
+
},
|
|
47
|
+
"pi": {
|
|
48
|
+
"extensions": [
|
|
49
|
+
"./extensions"
|
|
50
|
+
]
|
|
51
|
+
}
|
|
52
|
+
}
|