codecartographer-pi 0.19.6 → 0.21.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/GUIDE.md +2 -2
- package/.codecarto/broadside/SKILL.md +21 -3
- package/.codecarto/broadside/config.yaml +35 -9
- package/.codecarto/findings/contracts/SKILL.md +4 -1
- package/.codecarto/findings/defect-scan/SKILL.md +10 -0
- package/.codecarto/findings/defect-scan-mechanical/SKILL.md +6 -0
- package/.codecarto/findings/defect-scan-semantic/SKILL.md +8 -1
- package/.codecarto/findings/porting/SKILL.md +4 -0
- package/.codecarto/findings/protocols/SKILL.md +4 -0
- package/.codecarto/templates/mechanical-defects.md +15 -0
- package/.codecarto/templates/reimplementation-spec.md +5 -3
- package/.codecarto/templates/reverse-engineering-bundle.md +10 -1
- package/.codecarto/templates/semantic-defects.md +15 -0
- package/.codecarto/workflow/VALIDATE.md +3 -2
- package/.codecarto/workflow/scaffold-version.yaml +1 -1
- package/README.md +13 -9
- package/dist/core/amendment.js +9 -4
- package/dist/core/broadside.d.ts +121 -2
- package/dist/core/broadside.js +478 -92
- package/dist/core/completion.js +88 -23
- package/dist/core/dashboard-writer.d.ts +8 -0
- package/dist/core/dashboard-writer.js +159 -0
- package/dist/core/index.d.ts +2 -0
- package/dist/core/index.js +2 -0
- package/dist/core/library.js +9 -4
- package/dist/core/orchestrator-config.d.ts +32 -7
- package/dist/core/orchestrator-config.js +124 -44
- package/dist/core/pipeline.d.ts +73 -0
- package/dist/core/pipeline.js +134 -10
- package/dist/core/prompts.d.ts +20 -0
- package/dist/core/prompts.js +53 -13
- package/dist/core/secrets.d.ts +16 -0
- package/dist/core/secrets.js +98 -0
- package/dist/core/status.d.ts +16 -0
- package/dist/core/status.js +47 -20
- package/dist/core/synthesis.js +5 -2
- package/dist/core/utils.d.ts +7 -0
- package/dist/core/utils.js +7 -0
- package/dist/core/workspace.d.ts +55 -8
- package/dist/core/workspace.js +116 -8
- package/dist/core/yaml.js +181 -15
- package/dist/extensions/codecarto/agent-rewriter.js +21 -14
- package/dist/extensions/codecarto/agent-runner.d.ts +6 -2
- package/dist/extensions/codecarto/agent-runner.js +27 -9
- package/dist/extensions/codecarto/agent-state.d.ts +0 -2
- package/dist/extensions/codecarto/auto-runner.d.ts +1 -1
- package/dist/extensions/codecarto/auto-runner.js +27 -8
- package/dist/extensions/codecarto/broadside-flags.d.ts +3 -1
- package/dist/extensions/codecarto/broadside-flags.js +12 -0
- package/dist/extensions/codecarto/dashboard-narrator.js +9 -2
- package/dist/extensions/codecarto/dashboard-writer.d.ts +1 -8
- package/dist/extensions/codecarto/dashboard-writer.js +5 -154
- package/dist/extensions/codecarto/index.js +158 -47
- package/dist/extensions/codecarto/phase-compaction.js +5 -1
- package/dist/mcp-server/server.d.ts +3 -1
- package/dist/mcp-server/server.js +205 -77
- package/package.json +3 -2
|
@@ -17,12 +17,11 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
17
17
|
import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } from "@modelcontextprotocol/sdk/types.js";
|
|
18
18
|
import { mkdir, readFile, readdir, rename, writeFile } from "node:fs/promises";
|
|
19
19
|
import { basename, isAbsolute, join } from "node:path";
|
|
20
|
-
import { buildPhasePrompt, buildSkillPrompt, buildValidationSummary, BROADSIDE_DIR, broadsideDirFor, BROADSIDE_LENS_IDS, BROADSIDE_SKILL_NAME, canonicalPath, copyPackagedWorkspace, collectResultText, completeValidatedPhase, computePerPhaseTotals, computeTotals, createEmptyStatus, DEFAULT_PIPELINE_PATH, deriveSlug, discoverLibrary, describeScaffoldStaleness, detectProvenanceConflicts, estimateSubmitText, getLens,
|
|
20
|
+
import { buildPhasePrompt, buildSkillPrompt, buildValidationSummary, BROADSIDE_DIR, broadsideDirFor, BROADSIDE_LENS_IDS, BROADSIDE_SKILL_NAME, BroadsideConfigError, defaultBroadsideConfig, backupWorkspaceState, canonicalPath, copyPackagedWorkspace, collectResultText, completeValidatedPhase, computePerPhaseTotals, computeTotals, createEmptyStatus, DEFAULT_PIPELINE_PATH, deriveSlug, discoverLibrary, describeScaffoldStaleness, detectProvenanceConflicts, estimateSubmitText, getLens, describeDanglingCarryForward, describeMissingCompletedOutputs, describeStuckPipeline, getPipelineLabel, getWorkspaceState, isValidSlug, isWithinPathResolved, listBatchModels, listEntries, listGuideTopics, loadBroadsideConfig, modelsText, readGuide, listMissingCompletedOutputs, listSkillNames, resolvePipelineOutcome, resolveSkillName, loadCodecartoConfig, loadUsage, loadYamlFile, normalizeForComparison, PACKAGE_VERSION, packagedWorkspaceDir, pathExists, PhasePreflightError, previewPublishVersion, publishEntry, reindex as libraryReindex, refreshScaffold, resolvePhase, resolvePipelineChoice, readBroadsideSkill, runBroadsideCollect, runBroadsideStatus, runBroadsideSubmit, seedOrchestratorFiles, statusText, stringifySimpleYaml, switchPipeline, validatePhaseOutput, writeLibraryConfig, writeDashboard, } from "../core/index.js";
|
|
21
21
|
import { applyAmendment } from "../core/amendment.js";
|
|
22
22
|
import { appendUsageRun } from "../core/usage.js";
|
|
23
23
|
import { initLibrary } from "../core/library.js";
|
|
24
|
-
import { loadUserConfig, resolveUserConfigPath } from "../core/orchestrator-config.js";
|
|
25
|
-
import { writeDashboard } from "../extensions/codecarto/dashboard-writer.js";
|
|
24
|
+
import { describeConfigProblems, loadUserConfig, resolveUserConfigPath } from "../core/orchestrator-config.js";
|
|
26
25
|
// ---------- input helpers ----------
|
|
27
26
|
/**
|
|
28
27
|
* Normalize an optional `phase` argument. A client can send any JSON, and
|
|
@@ -50,9 +49,32 @@ async function validateCwd(cwd) {
|
|
|
50
49
|
}
|
|
51
50
|
return cwd;
|
|
52
51
|
}
|
|
52
|
+
/**
|
|
53
|
+
* The library tools take `cwd` optionally: absent means "no workspace, use
|
|
54
|
+
* the user-global config alone". When it is given it becomes a containment
|
|
55
|
+
* root for spec_path and the source of the workspace config that gates the
|
|
56
|
+
* write, so it is validated exactly as a required cwd is — absolute and
|
|
57
|
+
* existing — before anything reads through it (#241). A relative path would
|
|
58
|
+
* resolve against the MCP server process's working directory, not the
|
|
59
|
+
* caller's, and widen the root to whatever `.codecarto/` sits there.
|
|
60
|
+
*/
|
|
61
|
+
async function optionalCwd(cwd) {
|
|
62
|
+
if (cwd === undefined || cwd === null)
|
|
63
|
+
return null;
|
|
64
|
+
if (typeof cwd !== "string") {
|
|
65
|
+
throw new McpError(ErrorCode.InvalidParams, "cwd must be a string when given");
|
|
66
|
+
}
|
|
67
|
+
if (cwd.trim() === "")
|
|
68
|
+
return null;
|
|
69
|
+
return validateCwd(cwd.trim());
|
|
70
|
+
}
|
|
53
71
|
async function requireWorkspace(cwd) {
|
|
72
|
+
// What getWorkspaceState throws is the user's to fix — a status.yaml that
|
|
73
|
+
// does not parse, a missing `pipeline:`, a pipeline file that is not there
|
|
74
|
+
// — so it is InvalidRequest, not the InternalError a host would retry or
|
|
75
|
+
// report as a server bug (self-audit mech 2.8).
|
|
54
76
|
const state = await getWorkspaceState(cwd).catch((error) => {
|
|
55
|
-
throw new McpError(ErrorCode.
|
|
77
|
+
throw new McpError(ErrorCode.InvalidRequest, error instanceof Error ? error.message : String(error));
|
|
56
78
|
});
|
|
57
79
|
if (!state) {
|
|
58
80
|
throw new McpError(ErrorCode.InvalidRequest, `No CodeCartographer workspace at ${cwd}. Call codecarto_init first.`);
|
|
@@ -71,9 +93,9 @@ function textResult(text, structured) {
|
|
|
71
93
|
structuredContent: { ...structured, text },
|
|
72
94
|
};
|
|
73
95
|
}
|
|
74
|
-
async function buildMcpPhasePrompt(state, phase, forced) {
|
|
96
|
+
async function buildMcpPhasePrompt(state, phase, forced, auto = false) {
|
|
75
97
|
try {
|
|
76
|
-
return await buildPhasePrompt(state, phase, forced);
|
|
98
|
+
return await buildPhasePrompt(state, phase, forced, { auto });
|
|
77
99
|
}
|
|
78
100
|
catch (error) {
|
|
79
101
|
if (error instanceof PhasePreflightError) {
|
|
@@ -109,12 +131,21 @@ export async function handleInit(args) {
|
|
|
109
131
|
const entries = (await readdir(targetWorkspaceDir)).filter((entry) => entry !== BROADSIDE_DIR);
|
|
110
132
|
broadsideOnly = entries.length === 0 && (await pathExists(join(targetWorkspaceDir, BROADSIDE_DIR)));
|
|
111
133
|
}
|
|
112
|
-
if (targetExists && !
|
|
134
|
+
if (targetExists && !broadsideOnly) {
|
|
113
135
|
if (!args.force) {
|
|
114
|
-
throw new McpError(ErrorCode.InvalidRequest,
|
|
136
|
+
throw new McpError(ErrorCode.InvalidRequest, sameWorkspace
|
|
137
|
+
? `The .codecarto/ at ${targetWorkspaceDir} is CodeCartographer's own packaged template (a checkout install), and it holds workspace state. Pass force: true to move that state — status, findings, handoffs, usage data, closeouts, dashboard — to a .codecarto-backup-TIMESTAMP/ directory and reinitialize; the framework files stay in place. Consider codecarto_open to reattach without resetting.`
|
|
138
|
+
: `A .codecarto/ directory already exists at ${targetWorkspaceDir}. Pass force: true to back it up and reinitialize. Warning: this moves all existing findings, handoffs, usage data, closeouts, and phase progress to a .codecarto-backup-TIMESTAMP/ directory.`);
|
|
115
139
|
}
|
|
116
140
|
const backupDir = join(cwd, `.codecarto-backup-${new Date().toISOString().replace(/[:.]/g, "-")}`);
|
|
117
|
-
|
|
141
|
+
if (sameWorkspace) {
|
|
142
|
+
// The template cannot be renamed away — it is what init copies from —
|
|
143
|
+
// so its session state is moved out file by file instead (#245).
|
|
144
|
+
await backupWorkspaceState(targetWorkspaceDir, backupDir);
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
await rename(targetWorkspaceDir, backupDir);
|
|
148
|
+
}
|
|
118
149
|
}
|
|
119
150
|
if (!(await pathExists(targetWorkspaceDir))) {
|
|
120
151
|
await mkdir(cwd, { recursive: true });
|
|
@@ -161,17 +192,21 @@ export async function handleInit(args) {
|
|
|
161
192
|
export async function handleStatus(args) {
|
|
162
193
|
const cwd = await validateCwd(args.cwd);
|
|
163
194
|
const state = await requireWorkspace(cwd);
|
|
164
|
-
const
|
|
165
|
-
const
|
|
195
|
+
const outcome = resolvePipelineOutcome(state);
|
|
196
|
+
const nextPhase = outcome.kind === "eligible" ? outcome.phase : null;
|
|
197
|
+
// "complete" is the terminal marker; a stuck pipeline sits on its first
|
|
198
|
+
// blocked phase and says so below (#228).
|
|
199
|
+
const currentPhase = outcome.kind === "eligible" ? outcome.phase.id : outcome.kind === "stuck" ? outcome.blocked[0].phaseId : "complete";
|
|
166
200
|
const completed = state.pipeline.phase_order.filter((id) => state.status.phases[id]?.status === "complete").length;
|
|
167
201
|
const totalCarryForward = Object.values(state.status.phases).reduce((sum, phase) => sum + (phase.carry_forward?.length ?? 0), 0);
|
|
168
202
|
const currentOpenQuestions = currentPhase === "complete" ? 0 : state.status.phases[currentPhase]?.open_questions?.length ?? 0;
|
|
169
203
|
const terminalOpenQuestions = Object.values(state.status.phases).reduce((sum, phase) => sum + (phase.open_questions?.length ?? 0), 0);
|
|
170
204
|
const postPipelinePending = state.status.post_pipeline.filter((entry) => entry.status !== "resolved").length;
|
|
171
205
|
const scaffoldNotice = describeScaffoldStaleness(state);
|
|
206
|
+
const missingOutputs = await listMissingCompletedOutputs(state);
|
|
172
207
|
const summaryLines = [
|
|
173
208
|
`Phase: ${currentPhase}`,
|
|
174
|
-
`Pipeline state: ${
|
|
209
|
+
`Pipeline state: ${outcome.kind === "eligible" ? "in progress" : outcome.kind}`,
|
|
175
210
|
`Pipeline: ${getPipelineLabel(state.status.pipeline)} (${state.status.pipeline})`,
|
|
176
211
|
`Progress: ${completed}/${state.pipeline.phase_order.length} complete`,
|
|
177
212
|
`Open questions (terminal unresolved): ${terminalOpenQuestions}`,
|
|
@@ -184,11 +219,17 @@ export async function handleStatus(args) {
|
|
|
184
219
|
? state.status.next_actions.map((action, index) => `${index === 0 ? "Next: " : " "}${action}`)
|
|
185
220
|
: [`Next: ${nextPhase ? `Begin ${nextPhase.id}` : "All phases complete."}`]),
|
|
186
221
|
];
|
|
222
|
+
if (outcome.kind === "stuck")
|
|
223
|
+
summaryLines.push(describeStuckPipeline(outcome.blocked));
|
|
187
224
|
if (scaffoldNotice)
|
|
188
225
|
summaryLines.push(`Scaffold: ${scaffoldNotice}`);
|
|
226
|
+
summaryLines.push(...describeMissingCompletedOutputs(missingOutputs));
|
|
189
227
|
const summary = summaryLines.join("\n");
|
|
190
228
|
return textResult(summary, {
|
|
191
229
|
...(scaffoldNotice ? { scaffoldNotice } : {}),
|
|
230
|
+
...(missingOutputs.length > 0 ? { missingOutputs } : {}),
|
|
231
|
+
...(outcome.kind === "stuck" ? { stuck: outcome.blocked } : {}),
|
|
232
|
+
pipelineState: outcome.kind === "eligible" ? "in progress" : outcome.kind,
|
|
192
233
|
currentPhase,
|
|
193
234
|
pipeline: state.status.pipeline,
|
|
194
235
|
pipelineLabel: getPipelineLabel(state.status.pipeline),
|
|
@@ -212,6 +253,11 @@ export async function handleSwitchPipeline(args) {
|
|
|
212
253
|
return textResult(`Already on pipeline: ${getPipelineLabel(pipelineChoice)}`, { pipeline: getPipelineLabel(pipelineChoice) });
|
|
213
254
|
}
|
|
214
255
|
const result = await switchPipeline(cwd, pipelineChoice);
|
|
256
|
+
// The dashboard is rendered from the pipeline the switch just replaced;
|
|
257
|
+
// Pi re-rendered here and MCP did not, so the file showed the old phase
|
|
258
|
+
// list until the next completion (#254). Same best-effort call as
|
|
259
|
+
// completion and amendment make.
|
|
260
|
+
const dashboardPath = (await writeDashboard(cwd, PACKAGE_VERSION)) ? ".codecarto/dashboard.html" : undefined;
|
|
215
261
|
const lines = [`Switched pipeline: ${getPipelineLabel(pipelineChoice)}`];
|
|
216
262
|
if (result.carried.length > 0)
|
|
217
263
|
lines.push(`Phases preserved (completed): ${result.carried.join(", ")}`);
|
|
@@ -219,24 +265,41 @@ export async function handleSwitchPipeline(args) {
|
|
|
219
265
|
lines.push(`New phases: ${result.newPhases.join(", ")}`);
|
|
220
266
|
if (result.dropped.length > 0)
|
|
221
267
|
lines.push(`Phases not in new pipeline: ${result.dropped.join(", ")} (findings remain on disk)`);
|
|
268
|
+
lines.push(...describeDanglingCarryForward(result.dangling));
|
|
269
|
+
lines.push(`Current phase: ${result.state.status.current_phase}`);
|
|
270
|
+
if (dashboardPath)
|
|
271
|
+
lines.push(`Dashboard refreshed: ${dashboardPath}`);
|
|
222
272
|
return textResult(lines.join("\n"), {
|
|
223
273
|
pipeline: getPipelineLabel(pipelineChoice),
|
|
274
|
+
currentPhase: result.state.status.current_phase,
|
|
224
275
|
carried: result.carried,
|
|
225
276
|
newPhases: result.newPhases,
|
|
226
277
|
dropped: result.dropped,
|
|
278
|
+
dangling: result.dangling,
|
|
279
|
+
dashboardPath,
|
|
227
280
|
});
|
|
228
281
|
}
|
|
229
282
|
export async function handleNext(args) {
|
|
230
283
|
const cwd = await validateCwd(args.cwd);
|
|
231
284
|
const state = await requireWorkspace(cwd);
|
|
232
|
-
const
|
|
233
|
-
if (
|
|
285
|
+
const outcome = resolvePipelineOutcome(state);
|
|
286
|
+
if (outcome.kind === "stuck") {
|
|
287
|
+
// Not a result: a host looping on codecarto_next would read a text
|
|
288
|
+
// answer as "done" (#228). There is no prompt to hand out until the
|
|
289
|
+
// pipeline file is fixed.
|
|
290
|
+
throw new McpError(ErrorCode.InvalidRequest, describeStuckPipeline(outcome.blocked));
|
|
291
|
+
}
|
|
292
|
+
if (outcome.kind === "complete") {
|
|
234
293
|
return textResult("All CodeCartographer phases are complete. Run codecarto_skill for post-pipeline work.", {
|
|
235
294
|
complete: true,
|
|
236
295
|
});
|
|
237
296
|
}
|
|
238
|
-
|
|
239
|
-
|
|
297
|
+
// `unattended` is the MCP spelling of Pi's --auto: nobody is there to
|
|
298
|
+
// answer the reimplementation-spec phase's Strategic Alignment Hook, so
|
|
299
|
+
// the prompt carries the auto-default instruction instead (#270).
|
|
300
|
+
const unattended = args.unattended === true;
|
|
301
|
+
const prompt = await buildMcpPhasePrompt(state, outcome.phase, false, unattended);
|
|
302
|
+
return textResult(prompt, { phase: outcome.phase.id, forced: false, unattended });
|
|
240
303
|
}
|
|
241
304
|
export async function handlePhase(args) {
|
|
242
305
|
if (typeof args.phase !== "string" || !args.phase.trim()) {
|
|
@@ -248,8 +311,9 @@ export async function handlePhase(args) {
|
|
|
248
311
|
if (!phase) {
|
|
249
312
|
throw new McpError(ErrorCode.InvalidParams, `Unknown phase: ${args.phase}`);
|
|
250
313
|
}
|
|
251
|
-
const
|
|
252
|
-
|
|
314
|
+
const unattended = args.unattended === true;
|
|
315
|
+
const prompt = await buildMcpPhasePrompt(state, phase, true, unattended);
|
|
316
|
+
return textResult(prompt, { phase: phase.id, forced: true, unattended });
|
|
253
317
|
}
|
|
254
318
|
export async function handleValidate(args) {
|
|
255
319
|
const cwd = await validateCwd(args.cwd);
|
|
@@ -349,21 +413,27 @@ export async function handleSkill(args) {
|
|
|
349
413
|
return textResult(skill.content, { skill: BROADSIDE_SKILL_NAME, path: skill.path, postPipeline: false });
|
|
350
414
|
}
|
|
351
415
|
const state = await requireWorkspace(cwd);
|
|
352
|
-
const
|
|
353
|
-
if (
|
|
354
|
-
throw new McpError(ErrorCode.InvalidRequest, `Cannot run skill: pipeline is not complete (next phase: ${
|
|
416
|
+
const outcome = resolvePipelineOutcome(state);
|
|
417
|
+
if (outcome.kind === "eligible") {
|
|
418
|
+
throw new McpError(ErrorCode.InvalidRequest, `Cannot run skill: pipeline is not complete (next phase: ${outcome.phase.id}). Finish the pipeline first.`);
|
|
355
419
|
}
|
|
356
|
-
|
|
357
|
-
|
|
420
|
+
if (outcome.kind === "stuck") {
|
|
421
|
+
throw new McpError(ErrorCode.InvalidRequest, `Cannot run skill: the pipeline is not complete. ${describeStuckPipeline(outcome.blocked)}`);
|
|
422
|
+
}
|
|
423
|
+
// Resolve against the installed list only: the name is never joined onto a
|
|
424
|
+
// path, so a traversal like `../findings/architecture` cannot splice a
|
|
425
|
+
// phase SKILL.md (or anything else) into the post-pipeline prompt.
|
|
426
|
+
const skillName = await resolveSkillName(state.workspaceDir, args.name);
|
|
427
|
+
if (!skillName) {
|
|
358
428
|
const available = await listSkillNames(state.workspaceDir);
|
|
359
429
|
const hint = available.length > 0 ? ` Available: ${available.join(", ")}.` : " No skills installed.";
|
|
360
|
-
throw new McpError(ErrorCode.InvalidParams, `Unknown skill: ${args.name}.${hint} The Broad-Side reading guide is served as \`${BROADSIDE_SKILL_NAME}\` and is not pipeline-gated.`);
|
|
430
|
+
throw new McpError(ErrorCode.InvalidParams, `Unknown skill: ${args.name.trim()}.${hint} The Broad-Side reading guide is served as \`${BROADSIDE_SKILL_NAME}\` and is not pipeline-gated.`);
|
|
361
431
|
}
|
|
362
|
-
const prompt = await buildSkillPrompt(state,
|
|
363
|
-
return textResult(prompt, { skill:
|
|
432
|
+
const prompt = await buildSkillPrompt(state, skillName);
|
|
433
|
+
return textResult(prompt, { skill: skillName });
|
|
364
434
|
}
|
|
365
435
|
// ---------- library helpers ----------
|
|
366
|
-
|
|
436
|
+
function resolveLibraryPath(args, config) {
|
|
367
437
|
const explicit = typeof args.library_path === "string" && args.library_path.trim() !== ""
|
|
368
438
|
? args.library_path.trim()
|
|
369
439
|
: null;
|
|
@@ -373,18 +443,8 @@ async function resolveLibraryPath(args) {
|
|
|
373
443
|
}
|
|
374
444
|
return explicit;
|
|
375
445
|
}
|
|
376
|
-
if (
|
|
377
|
-
|
|
378
|
-
if (!isAbsolute(cwd)) {
|
|
379
|
-
throw new McpError(ErrorCode.InvalidParams, `cwd must be absolute, got: ${cwd}`);
|
|
380
|
-
}
|
|
381
|
-
// loadCodecartoConfig merges user-global under per-workspace and tolerates
|
|
382
|
-
// a missing workspace file, so a single call covers both cases.
|
|
383
|
-
const workspaceDir = join(cwd, ".codecarto");
|
|
384
|
-
const config = await loadCodecartoConfig(workspaceDir);
|
|
385
|
-
if (config.library.path)
|
|
386
|
-
return config.library.path;
|
|
387
|
-
}
|
|
446
|
+
if (config.library.path)
|
|
447
|
+
return config.library.path;
|
|
388
448
|
throw new McpError(ErrorCode.InvalidParams, "library_path is required (pass it explicitly, or pass cwd and configure library.path in ~/.codecarto/config.yaml or .codecarto/workflow/config.yaml).");
|
|
389
449
|
}
|
|
390
450
|
/**
|
|
@@ -394,17 +454,22 @@ async function resolveLibraryPath(args) {
|
|
|
394
454
|
* the publish tool enforces.
|
|
395
455
|
*/
|
|
396
456
|
async function loadEffectiveConfig(cwd) {
|
|
397
|
-
|
|
457
|
+
// This config decides whether publish_confirm gates the write, so the cwd
|
|
458
|
+
// it is read through is the one optionalCwd validated, never a raw argument.
|
|
459
|
+
if (!cwd)
|
|
398
460
|
return loadUserConfig();
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
461
|
+
return loadCodecartoConfig(join(cwd, ".codecarto"));
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
464
|
+
* The library tools answer from the config — its path, its namespace, its
|
|
465
|
+
* confirm gate — so a config file that could not be used in full is a
|
|
466
|
+
* refusal, not a silent fallback to whatever the other layer says (#242).
|
|
467
|
+
* The message names each file and key; codecarto_config shows the same list.
|
|
468
|
+
*/
|
|
469
|
+
function refuseOnConfigProblems(config, tool) {
|
|
470
|
+
if (config.problems.length === 0)
|
|
471
|
+
return;
|
|
472
|
+
throw new McpError(ErrorCode.InvalidRequest, [`${tool} refused: the configuration has problems. Fix or remove the offending file, then retry.`, ...describeConfigProblems(config)].join("\n"));
|
|
408
473
|
}
|
|
409
474
|
function asStringArray(value, fieldName) {
|
|
410
475
|
if (!Array.isArray(value)) {
|
|
@@ -491,12 +556,12 @@ async function resolveDefaultsFromWorkspace(cwd, overrides) {
|
|
|
491
556
|
let namespace = typeof overrides.namespace === "string" && overrides.namespace.trim() !== ""
|
|
492
557
|
? overrides.namespace.trim()
|
|
493
558
|
: null;
|
|
494
|
-
if (
|
|
495
|
-
const workspaceDir = join(cwd
|
|
559
|
+
if (cwd) {
|
|
560
|
+
const workspaceDir = join(cwd, ".codecarto");
|
|
496
561
|
if (await pathExists(workspaceDir)) {
|
|
497
562
|
if (pipeline === "unknown") {
|
|
498
563
|
try {
|
|
499
|
-
const state = await getWorkspaceState(cwd
|
|
564
|
+
const state = await getWorkspaceState(cwd);
|
|
500
565
|
if (state?.status.pipeline)
|
|
501
566
|
pipeline = state.status.pipeline;
|
|
502
567
|
}
|
|
@@ -542,16 +607,20 @@ function provenanceConflictLines(conflicts) {
|
|
|
542
607
|
}
|
|
543
608
|
// ---------- library handlers ----------
|
|
544
609
|
export async function handlePublish(args) {
|
|
545
|
-
|
|
610
|
+
// First, before anything is read through it: cwd is a containment root
|
|
611
|
+
// for spec_path below and the source of the config that gates the write.
|
|
612
|
+
const cwd = await optionalCwd(args.cwd);
|
|
613
|
+
const config = await loadEffectiveConfig(cwd);
|
|
614
|
+
refuseOnConfigProblems(config, "codecarto_publish");
|
|
615
|
+
const libraryPath = resolveLibraryPath(args, config);
|
|
546
616
|
const marker = await discoverLibrary(libraryPath);
|
|
547
617
|
if (!marker) {
|
|
548
618
|
throw new McpError(ErrorCode.InvalidParams, `No CodeCartographer library at ${libraryPath} (missing .codecarto-library marker). Create one before publishing.`);
|
|
549
619
|
}
|
|
550
620
|
// Build allowed roots for spec_path containment: workspace .codecarto/ and library path
|
|
551
621
|
const allowedRoots = [libraryPath];
|
|
552
|
-
if (
|
|
553
|
-
allowedRoots.push(join(
|
|
554
|
-
}
|
|
622
|
+
if (cwd)
|
|
623
|
+
allowedRoots.push(join(cwd, ".codecarto"));
|
|
555
624
|
const spec = await readSpecArg(args, allowedRoots);
|
|
556
625
|
if (typeof args.source_repo !== "string" || args.source_repo.trim() === "") {
|
|
557
626
|
throw new McpError(ErrorCode.InvalidParams, "source_repo is required");
|
|
@@ -567,7 +636,7 @@ export async function handlePublish(args) {
|
|
|
567
636
|
if (!isValidSlug(slug)) {
|
|
568
637
|
throw new McpError(ErrorCode.InvalidParams, `Resolved slug "${slug}" is invalid. Provide an explicit slug (lowercase ASCII, starts with a letter, max 64 chars).`);
|
|
569
638
|
}
|
|
570
|
-
const defaults = await resolveDefaultsFromWorkspace(
|
|
639
|
+
const defaults = await resolveDefaultsFromWorkspace(cwd, {
|
|
571
640
|
pipeline: args.pipeline,
|
|
572
641
|
namespace: args.namespace,
|
|
573
642
|
});
|
|
@@ -596,7 +665,6 @@ export async function handlePublish(args) {
|
|
|
596
665
|
// opt-out, and a host that never configured the key keeps the behavior it
|
|
597
666
|
// had. Runs after every argument check so the preview names the resolved
|
|
598
667
|
// slug and namespace, and before publishEntry so nothing is written.
|
|
599
|
-
const config = await loadEffectiveConfig(args.cwd);
|
|
600
668
|
if (config.library.publish_confirm && config.library.publish_confirm_configured && args.confirm !== true) {
|
|
601
669
|
const preview = await previewPublishVersion(libraryPath, spec, { slug, namespace }, { forceNewVersion });
|
|
602
670
|
const label = `${namespace ? `${namespace}/` : ""}${slug}`;
|
|
@@ -665,7 +733,9 @@ export async function handlePublish(args) {
|
|
|
665
733
|
});
|
|
666
734
|
}
|
|
667
735
|
export async function handleLibraryList(args) {
|
|
668
|
-
const
|
|
736
|
+
const config = await loadEffectiveConfig(await optionalCwd(args.cwd));
|
|
737
|
+
refuseOnConfigProblems(config, "codecarto_library_list");
|
|
738
|
+
const libraryPath = resolveLibraryPath(args, config);
|
|
669
739
|
const marker = await discoverLibrary(libraryPath);
|
|
670
740
|
if (!marker) {
|
|
671
741
|
throw new McpError(ErrorCode.InvalidParams, `No CodeCartographer library at ${libraryPath} (missing .codecarto-library marker).`);
|
|
@@ -706,7 +776,9 @@ export async function handleLibraryList(args) {
|
|
|
706
776
|
});
|
|
707
777
|
}
|
|
708
778
|
export async function handleLibraryReindex(args) {
|
|
709
|
-
const
|
|
779
|
+
const config = await loadEffectiveConfig(await optionalCwd(args.cwd));
|
|
780
|
+
refuseOnConfigProblems(config, "codecarto_library_reindex");
|
|
781
|
+
const libraryPath = resolveLibraryPath(args, config);
|
|
710
782
|
const marker = await discoverLibrary(libraryPath);
|
|
711
783
|
if (!marker) {
|
|
712
784
|
throw new McpError(ErrorCode.InvalidParams, `No CodeCartographer library at ${libraryPath} (missing .codecarto-library marker).`);
|
|
@@ -742,10 +814,17 @@ export async function handleLibraryInit(args) {
|
|
|
742
814
|
});
|
|
743
815
|
// Write config to user-global location
|
|
744
816
|
const configPath = resolveUserConfigPath();
|
|
745
|
-
|
|
817
|
+
// Writes library.path (and library.namespace when given) and nothing else:
|
|
818
|
+
// in particular it does not set publish_confirm, so initializing a library
|
|
819
|
+
// does not switch the codecarto_publish confirm gate on (#244). A config
|
|
820
|
+
// file that cannot be parsed is left alone and reported.
|
|
821
|
+
await writeLibraryConfig(configPath, libraryPath, args.namespace ?? null).catch((error) => {
|
|
822
|
+
throw new McpError(ErrorCode.InvalidRequest, error instanceof Error ? error.message : String(error));
|
|
823
|
+
});
|
|
824
|
+
const written = args.namespace ? "library.path and library.namespace" : "library.path";
|
|
746
825
|
const msg = result.alreadyExisted
|
|
747
|
-
? `Library already exists at ${libraryPath} (marker preserved).
|
|
748
|
-
: `Created library at ${libraryPath} with marker "${result.marker.name}".
|
|
826
|
+
? `Library already exists at ${libraryPath} (marker preserved). Wrote ${written} to ${configPath}; other keys untouched.`
|
|
827
|
+
: `Created library at ${libraryPath} with marker "${result.marker.name}". Wrote ${written} to ${configPath}; other keys untouched.`;
|
|
749
828
|
return textResult(msg, {
|
|
750
829
|
libraryPath,
|
|
751
830
|
markerName: result.marker.name,
|
|
@@ -808,6 +887,9 @@ export async function handleConfig(args) {
|
|
|
808
887
|
` Library marker: ${markerStatus}`,
|
|
809
888
|
` User-global config: ${userConfigPath}`,
|
|
810
889
|
` Workspace config: ${workspaceConfigPath ?? "(no cwd provided)"}`,
|
|
890
|
+
// A file that could not be used is the one thing this tool exists
|
|
891
|
+
// to surface; the library tools refuse while any is listed (#242).
|
|
892
|
+
...describeConfigProblems(config),
|
|
811
893
|
].join("\n"), {
|
|
812
894
|
libraryPath: config.library.path,
|
|
813
895
|
libraryNamespace: config.library.namespace,
|
|
@@ -815,6 +897,7 @@ export async function handleConfig(args) {
|
|
|
815
897
|
llmSteerNextPhase: config.orchestrator.llm_steer_next_phase,
|
|
816
898
|
userConfigPath,
|
|
817
899
|
workspaceConfigPath,
|
|
900
|
+
problems: config.problems,
|
|
818
901
|
});
|
|
819
902
|
}
|
|
820
903
|
// ---------- MCP parity handlers: open, usage, dashboard, list_skills ----------
|
|
@@ -825,8 +908,16 @@ export async function handleOpen(args) {
|
|
|
825
908
|
throw new McpError(ErrorCode.InvalidRequest, "No existing CodeCartographer workspace found. Run codecarto_init first.");
|
|
826
909
|
}
|
|
827
910
|
const state = await requireWorkspace(cwd);
|
|
828
|
-
const
|
|
829
|
-
|
|
911
|
+
const outcome = resolvePipelineOutcome(state);
|
|
912
|
+
const nextPhase = outcome.kind === "eligible" ? outcome.phase.id : outcome.kind === "stuck" ? `${outcome.blocked[0].phaseId} (stuck)` : "complete";
|
|
913
|
+
const lines = [`Opened existing CodeCartographer workspace: ${getPipelineLabel(state.status.pipeline)}. Current phase: ${nextPhase}.`];
|
|
914
|
+
if (outcome.kind === "stuck")
|
|
915
|
+
lines.push(describeStuckPipeline(outcome.blocked));
|
|
916
|
+
return textResult(lines.join("\n"), {
|
|
917
|
+
pipeline: getPipelineLabel(state.status.pipeline),
|
|
918
|
+
currentPhase: nextPhase,
|
|
919
|
+
...(outcome.kind === "stuck" ? { stuck: outcome.blocked } : {}),
|
|
920
|
+
});
|
|
830
921
|
}
|
|
831
922
|
export async function handleUsage(args) {
|
|
832
923
|
const cwd = await validateCwd(args.cwd);
|
|
@@ -949,18 +1040,40 @@ export async function handleBroadside(args) {
|
|
|
949
1040
|
if (!["submit", "collect", "status", "models"].includes(action)) {
|
|
950
1041
|
throw new McpError(ErrorCode.InvalidParams, `Unknown action: ${action}. Valid actions: submit, collect, status, models.`);
|
|
951
1042
|
}
|
|
952
|
-
|
|
1043
|
+
// A config.yaml that exists but cannot be read refuses every action that
|
|
1044
|
+
// would act on it (#232); status only reads recorded runs, so it answers
|
|
1045
|
+
// and says the file is unreadable. A corrupt state.json refuses even that:
|
|
1046
|
+
// there is nothing trustworthy to report and nothing may write over it (#233).
|
|
1047
|
+
let config;
|
|
1048
|
+
let configWarning = null;
|
|
1049
|
+
try {
|
|
1050
|
+
config = await loadBroadsideConfig(broadsideDirFor(cwd));
|
|
1051
|
+
}
|
|
1052
|
+
catch (error) {
|
|
1053
|
+
if (!(error instanceof BroadsideConfigError) || action !== "status") {
|
|
1054
|
+
throw new McpError(ErrorCode.InvalidRequest, error instanceof Error ? error.message : String(error));
|
|
1055
|
+
}
|
|
1056
|
+
config = defaultBroadsideConfig();
|
|
1057
|
+
configWarning = error.message;
|
|
1058
|
+
}
|
|
953
1059
|
if (action === "status") {
|
|
954
|
-
const { state } = await runBroadsideStatus(cwd)
|
|
955
|
-
|
|
1060
|
+
const { state } = await runBroadsideStatus(cwd).catch((error) => {
|
|
1061
|
+
throw new McpError(ErrorCode.InvalidRequest, error instanceof Error ? error.message : String(error));
|
|
1062
|
+
});
|
|
1063
|
+
const lines = [statusText(state)];
|
|
1064
|
+
if (configWarning)
|
|
1065
|
+
lines.push(`Warning: ${configWarning}`);
|
|
1066
|
+
return textResult(lines.join("\n"), { state, ...(configWarning ? { configWarning } : {}) });
|
|
956
1067
|
}
|
|
957
1068
|
const apiKey = resolveBroadsideApiKey(args.api_key, config);
|
|
958
1069
|
// Every run knob resolves the same way: explicit parameter, else the repo's
|
|
959
1070
|
// config.yaml default, else the shipped default baked into loadBroadsideConfig.
|
|
960
|
-
|
|
1071
|
+
// An explicit 0 means "poll once and return": it used to fall through to
|
|
1072
|
+
// the config default and, as undefined, to the 25-minute budget (#230).
|
|
1073
|
+
const waitSeconds = typeof args.wait_seconds === "number" && args.wait_seconds >= 0
|
|
961
1074
|
? args.wait_seconds
|
|
962
1075
|
: config.waitSeconds;
|
|
963
|
-
const waitMs = waitSeconds
|
|
1076
|
+
const waitMs = waitSeconds * 1000;
|
|
964
1077
|
const includeSynthesis = args.include_synthesis ?? config.includeSynthesis;
|
|
965
1078
|
const includeTriage = args.include_triage ?? config.includeTriage;
|
|
966
1079
|
const retryTruncated = args.retry_truncated ?? config.retryTruncated;
|
|
@@ -989,7 +1102,9 @@ export async function handleBroadside(args) {
|
|
|
989
1102
|
else {
|
|
990
1103
|
lenses = config.defaultLenses;
|
|
991
1104
|
}
|
|
992
|
-
|
|
1105
|
+
// An explicit 0 is "no limit" (#231); absent falls back to config.yaml,
|
|
1106
|
+
// whose own default is BROADSIDE_DEFAULT_MAX_COST.
|
|
1107
|
+
const maxCost = typeof args.max_cost === "number" && args.max_cost >= 0 ? args.max_cost : config.maxCost;
|
|
993
1108
|
const result = await runBroadsideSubmit(cwd, apiKey, {
|
|
994
1109
|
lenses,
|
|
995
1110
|
model: config.model,
|
|
@@ -1026,11 +1141,13 @@ export async function handleBroadside(args) {
|
|
|
1026
1141
|
});
|
|
1027
1142
|
}
|
|
1028
1143
|
// action === "collect"
|
|
1144
|
+
const runId = typeof args.run_id === "string" && args.run_id.trim() ? args.run_id.trim() : undefined;
|
|
1029
1145
|
const collect = await runBroadsideCollect(cwd, apiKey, {
|
|
1030
1146
|
waitMs,
|
|
1031
1147
|
includeSynthesis,
|
|
1032
1148
|
includeTriage,
|
|
1033
1149
|
retryTruncated,
|
|
1150
|
+
...(runId && { runId }),
|
|
1034
1151
|
}).catch((error) => {
|
|
1035
1152
|
throw new McpError(ErrorCode.InvalidRequest, error instanceof Error ? error.message : String(error));
|
|
1036
1153
|
});
|
|
@@ -1098,7 +1215,13 @@ const TOOLS = [
|
|
|
1098
1215
|
description: "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.",
|
|
1099
1216
|
inputSchema: {
|
|
1100
1217
|
type: "object",
|
|
1101
|
-
properties: {
|
|
1218
|
+
properties: {
|
|
1219
|
+
cwd: { type: "string", description: "Absolute path to the target repository." },
|
|
1220
|
+
unattended: {
|
|
1221
|
+
type: "boolean",
|
|
1222
|
+
description: "Set when no user is in the loop to answer the prompt's questions (an autonomous run). The reimplementation-spec prompt then defaults to the language-agnostic variant and records selection: auto-default instead of asking which variant to build, and every interactive hook is suppressed. Same as Pi's /codecarto-next --auto.",
|
|
1223
|
+
},
|
|
1224
|
+
},
|
|
1102
1225
|
required: ["cwd"],
|
|
1103
1226
|
},
|
|
1104
1227
|
},
|
|
@@ -1110,13 +1233,14 @@ const TOOLS = [
|
|
|
1110
1233
|
properties: {
|
|
1111
1234
|
cwd: { type: "string", description: "Absolute path to the target repository." },
|
|
1112
1235
|
phase: { type: "string", description: "Phase id from the active pipeline." },
|
|
1236
|
+
unattended: { type: "boolean", description: "As for codecarto_next: no user is in the loop, so interactive hooks default and record their choice instead of asking." },
|
|
1113
1237
|
},
|
|
1114
1238
|
required: ["cwd", "phase"],
|
|
1115
1239
|
},
|
|
1116
1240
|
},
|
|
1117
1241
|
{
|
|
1118
1242
|
name: "codecarto_validate",
|
|
1119
|
-
description: "
|
|
1243
|
+
description: "Read the ## Validation table a phase wrote at the end of its primary output — the phase's own PASS/PARTIAL/FAIL per completion criterion — apply two cross-checks (findings evidence/action pairing; declared secondary outputs present), and return PASS/PASS WITH GAPS/FAIL/MISSING plus the parsed rows. It does not judge the criteria itself. If phase is omitted, validates the next eligible phase.",
|
|
1120
1244
|
inputSchema: {
|
|
1121
1245
|
type: "object",
|
|
1122
1246
|
properties: {
|
|
@@ -1128,7 +1252,7 @@ const TOOLS = [
|
|
|
1128
1252
|
},
|
|
1129
1253
|
{
|
|
1130
1254
|
name: "codecarto_complete",
|
|
1131
|
-
description: "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
|
|
1255
|
+
description: "Mark a phase complete. Requires the phase output's validation to be PASS or PASS WITH GAPS and applies the phase handoff at scratch/handoffs/<phase>.yaml. Atomically updates status.yaml under a file lock, appends one THREAD_LOG.md entry, and writes the phase's closeout: the handoff's closeout_content (plus its decisions) overwrites the canonical <date>-<phase>.md when supplied; otherwise a stub is copied from the template only if no closeout exists yet. If phase is omitted, completes the next eligible phase.",
|
|
1132
1256
|
inputSchema: {
|
|
1133
1257
|
type: "object",
|
|
1134
1258
|
properties: {
|
|
@@ -1354,11 +1478,15 @@ const TOOLS = [
|
|
|
1354
1478
|
},
|
|
1355
1479
|
api_key: {
|
|
1356
1480
|
type: "string",
|
|
1357
|
-
description: "OpenRouter API key.
|
|
1481
|
+
description: "OpenRouter API key. A value passed here is recorded wherever the host logs tool calls; prefer the OPENROUTER_API_KEY environment variable of the server process. (api_key in .codecarto/broadside/config.yaml also works, but that file is tracked, so a key there is committed with it.)",
|
|
1482
|
+
},
|
|
1483
|
+
run_id: {
|
|
1484
|
+
type: "string",
|
|
1485
|
+
description: "For collect: the run to collect, as listed by the status action. Defaults to the most recent run; pass this to collect an older run that is still in flight after a newer submit.",
|
|
1358
1486
|
},
|
|
1359
1487
|
wait_seconds: {
|
|
1360
1488
|
type: "number",
|
|
1361
|
-
description: "For submit: after submitting, poll up to this many seconds before returning. For collect: poll up to this many seconds before returning with partial state. Falls back to wait_seconds in .codecarto/broadside/config.yaml.",
|
|
1489
|
+
description: "For submit: after submitting, poll up to this many seconds before returning. For collect: poll up to this many seconds before returning with partial state. 0 polls each in-flight batch once and returns without waiting. Falls back to wait_seconds in .codecarto/broadside/config.yaml (default 0).",
|
|
1362
1490
|
},
|
|
1363
1491
|
include_synthesis: {
|
|
1364
1492
|
type: "boolean",
|
|
@@ -1374,7 +1502,7 @@ const TOOLS = [
|
|
|
1374
1502
|
},
|
|
1375
1503
|
max_cost: {
|
|
1376
1504
|
type: "number",
|
|
1377
|
-
description: "Approximate run expense limit in USD. The submit action estimates the run cost from slice sizes and the configured model's per-token pricing (live OpenRouter lookup, cached 24h) and refuses to submit when the estimate exceeds the limit unless force is true. Falls back to max_cost in .codecarto/broadside/config.yaml.",
|
|
1505
|
+
description: "Approximate run expense limit in USD. The submit action estimates the run cost from slice sizes and the configured model's per-token pricing (live OpenRouter lookup, cached 24h) and refuses to submit when the estimate exceeds the limit unless force is true. Falls back to max_cost in .codecarto/broadside/config.yaml, whose default is $1.00; pass 0 for no limit.",
|
|
1378
1506
|
},
|
|
1379
1507
|
force: {
|
|
1380
1508
|
type: "boolean",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codecartographer-pi",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.0",
|
|
4
4
|
"mcpName": "io.github.HuginnIndustries/codecartographer",
|
|
5
5
|
"description": "Turn an unfamiliar codebase into a validated reimplementation spec, then synthesize confirmed specs and a product vision into a traceable plan.",
|
|
6
6
|
"type": "module",
|
|
@@ -76,6 +76,7 @@
|
|
|
76
76
|
"typescript": "^5.9.3"
|
|
77
77
|
},
|
|
78
78
|
"overrides": {
|
|
79
|
-
"undici": "^8.10.0"
|
|
79
|
+
"undici": "^8.10.0",
|
|
80
|
+
"hono": "^4.13.5"
|
|
80
81
|
}
|
|
81
82
|
}
|