codecartographer-pi 0.19.5 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/.codecarto/GUIDE.md +1 -1
  2. package/.codecarto/broadside/SKILL.md +14 -0
  3. package/.codecarto/broadside/config.yaml +18 -0
  4. package/.codecarto/templates/gitignore +55 -0
  5. package/.codecarto/workflow/VALIDATE.md +2 -1
  6. package/.codecarto/workflow/scaffold-version.yaml +1 -1
  7. package/README.md +10 -6
  8. package/dist/core/amendment.js +28 -23
  9. package/dist/core/broadside.d.ts +72 -2
  10. package/dist/core/broadside.js +351 -68
  11. package/dist/core/completion.js +95 -26
  12. package/dist/core/dashboard-writer.d.ts +8 -0
  13. package/dist/core/dashboard-writer.js +159 -0
  14. package/dist/core/index.d.ts +2 -0
  15. package/dist/core/index.js +2 -0
  16. package/dist/core/library.js +115 -107
  17. package/dist/core/orchestrator-config.d.ts +32 -7
  18. package/dist/core/orchestrator-config.js +124 -44
  19. package/dist/core/pipeline.d.ts +37 -0
  20. package/dist/core/pipeline.js +80 -10
  21. package/dist/core/prompts.d.ts +20 -0
  22. package/dist/core/prompts.js +43 -10
  23. package/dist/core/secrets.d.ts +16 -0
  24. package/dist/core/secrets.js +98 -0
  25. package/dist/core/status.d.ts +30 -2
  26. package/dist/core/status.js +54 -8
  27. package/dist/core/synthesis.js +5 -2
  28. package/dist/core/usage.d.ts +8 -0
  29. package/dist/core/usage.js +35 -7
  30. package/dist/core/utils.d.ts +32 -5
  31. package/dist/core/utils.js +81 -19
  32. package/dist/core/workspace.d.ts +99 -18
  33. package/dist/core/workspace.js +275 -36
  34. package/dist/core/yaml.js +173 -14
  35. package/dist/extensions/codecarto/agent-rewriter.js +21 -14
  36. package/dist/extensions/codecarto/agent-runner.d.ts +6 -2
  37. package/dist/extensions/codecarto/agent-runner.js +27 -9
  38. package/dist/extensions/codecarto/agent-state.d.ts +0 -2
  39. package/dist/extensions/codecarto/auto-runner.js +10 -6
  40. package/dist/extensions/codecarto/dashboard-narrator.js +12 -8
  41. package/dist/extensions/codecarto/dashboard-writer.d.ts +1 -8
  42. package/dist/extensions/codecarto/dashboard-writer.js +5 -157
  43. package/dist/extensions/codecarto/index.js +73 -21
  44. package/dist/extensions/codecarto/phase-compaction.js +7 -7
  45. package/dist/mcp-server/server.js +111 -50
  46. 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, getNextEligiblePhase, getPipelineLabel, getWorkspaceState, isValidSlug, isWithinPathResolved, listBatchModels, listEntries, listGuideTopics, loadBroadsideConfig, modelsText, readGuide, listSkillNames, 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, } from "../core/index.js";
20
+ import { buildPhasePrompt, buildSkillPrompt, buildValidationSummary, BROADSIDE_DIR, broadsideDirFor, BROADSIDE_LENS_IDS, BROADSIDE_SKILL_NAME, backupWorkspaceState, canonicalPath, copyPackagedWorkspace, collectResultText, completeValidatedPhase, computePerPhaseTotals, computeTotals, createEmptyStatus, DEFAULT_PIPELINE_PATH, deriveSlug, discoverLibrary, describeScaffoldStaleness, detectProvenanceConflicts, estimateSubmitText, getLens, describeDanglingCarryForward, describeMissingCompletedOutputs, getNextEligiblePhase, getPipelineLabel, getWorkspaceState, isValidSlug, isWithinPathResolved, listBatchModels, listEntries, listGuideTopics, loadBroadsideConfig, modelsText, readGuide, listMissingCompletedOutputs, listSkillNames, 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,6 +49,25 @@ 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) {
54
72
  const state = await getWorkspaceState(cwd).catch((error) => {
55
73
  throw new McpError(ErrorCode.InternalError, error instanceof Error ? error.message : String(error));
@@ -109,12 +127,21 @@ export async function handleInit(args) {
109
127
  const entries = (await readdir(targetWorkspaceDir)).filter((entry) => entry !== BROADSIDE_DIR);
110
128
  broadsideOnly = entries.length === 0 && (await pathExists(join(targetWorkspaceDir, BROADSIDE_DIR)));
111
129
  }
112
- if (targetExists && !sameWorkspace && !broadsideOnly) {
130
+ if (targetExists && !broadsideOnly) {
113
131
  if (!args.force) {
114
- throw new McpError(ErrorCode.InvalidRequest, `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.`);
132
+ throw new McpError(ErrorCode.InvalidRequest, sameWorkspace
133
+ ? `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.`
134
+ : `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
135
  }
116
136
  const backupDir = join(cwd, `.codecarto-backup-${new Date().toISOString().replace(/[:.]/g, "-")}`);
117
- await rename(targetWorkspaceDir, backupDir);
137
+ if (sameWorkspace) {
138
+ // The template cannot be renamed away — it is what init copies from —
139
+ // so its session state is moved out file by file instead (#245).
140
+ await backupWorkspaceState(targetWorkspaceDir, backupDir);
141
+ }
142
+ else {
143
+ await rename(targetWorkspaceDir, backupDir);
144
+ }
118
145
  }
119
146
  if (!(await pathExists(targetWorkspaceDir))) {
120
147
  await mkdir(cwd, { recursive: true });
@@ -125,8 +152,11 @@ export async function handleInit(args) {
125
152
  // broadside state and results already on disk.
126
153
  await copyPackagedWorkspace(targetWorkspaceDir);
127
154
  }
155
+ // The template no longer carries a status.yaml (it is this repository's
156
+ // own project state); one is only present when init is re-run over an
157
+ // existing workspace, where its pipeline stays the default.
128
158
  const statusPath = join(targetWorkspaceDir, "workflow", "status.yaml");
129
- const rawStatus = (await loadYamlFile(statusPath)) ?? {};
159
+ const rawStatus = (await pathExists(statusPath)) ? ((await loadYamlFile(statusPath)) ?? {}) : {};
130
160
  const selectedPipelinePath = pipelineChoice ?? rawStatus.pipeline?.trim() ?? DEFAULT_PIPELINE_PATH;
131
161
  const resolvedPipelinePath = join(targetWorkspaceDir, selectedPipelinePath);
132
162
  if (!(await pathExists(resolvedPipelinePath))) {
@@ -166,6 +196,7 @@ export async function handleStatus(args) {
166
196
  const terminalOpenQuestions = Object.values(state.status.phases).reduce((sum, phase) => sum + (phase.open_questions?.length ?? 0), 0);
167
197
  const postPipelinePending = state.status.post_pipeline.filter((entry) => entry.status !== "resolved").length;
168
198
  const scaffoldNotice = describeScaffoldStaleness(state);
199
+ const missingOutputs = await listMissingCompletedOutputs(state);
169
200
  const summaryLines = [
170
201
  `Phase: ${currentPhase}`,
171
202
  `Pipeline state: ${currentPhase === "complete" ? "complete" : "in progress"}`,
@@ -183,9 +214,11 @@ export async function handleStatus(args) {
183
214
  ];
184
215
  if (scaffoldNotice)
185
216
  summaryLines.push(`Scaffold: ${scaffoldNotice}`);
217
+ summaryLines.push(...describeMissingCompletedOutputs(missingOutputs));
186
218
  const summary = summaryLines.join("\n");
187
219
  return textResult(summary, {
188
220
  ...(scaffoldNotice ? { scaffoldNotice } : {}),
221
+ ...(missingOutputs.length > 0 ? { missingOutputs } : {}),
189
222
  currentPhase,
190
223
  pipeline: state.status.pipeline,
191
224
  pipelineLabel: getPipelineLabel(state.status.pipeline),
@@ -209,6 +242,11 @@ export async function handleSwitchPipeline(args) {
209
242
  return textResult(`Already on pipeline: ${getPipelineLabel(pipelineChoice)}`, { pipeline: getPipelineLabel(pipelineChoice) });
210
243
  }
211
244
  const result = await switchPipeline(cwd, pipelineChoice);
245
+ // The dashboard is rendered from the pipeline the switch just replaced;
246
+ // Pi re-rendered here and MCP did not, so the file showed the old phase
247
+ // list until the next completion (#254). Same best-effort call as
248
+ // completion and amendment make.
249
+ const dashboardPath = (await writeDashboard(cwd, PACKAGE_VERSION)) ? ".codecarto/dashboard.html" : undefined;
212
250
  const lines = [`Switched pipeline: ${getPipelineLabel(pipelineChoice)}`];
213
251
  if (result.carried.length > 0)
214
252
  lines.push(`Phases preserved (completed): ${result.carried.join(", ")}`);
@@ -216,11 +254,18 @@ export async function handleSwitchPipeline(args) {
216
254
  lines.push(`New phases: ${result.newPhases.join(", ")}`);
217
255
  if (result.dropped.length > 0)
218
256
  lines.push(`Phases not in new pipeline: ${result.dropped.join(", ")} (findings remain on disk)`);
257
+ lines.push(...describeDanglingCarryForward(result.dangling));
258
+ lines.push(`Current phase: ${result.state.status.current_phase}`);
259
+ if (dashboardPath)
260
+ lines.push(`Dashboard refreshed: ${dashboardPath}`);
219
261
  return textResult(lines.join("\n"), {
220
262
  pipeline: getPipelineLabel(pipelineChoice),
263
+ currentPhase: result.state.status.current_phase,
221
264
  carried: result.carried,
222
265
  newPhases: result.newPhases,
223
266
  dropped: result.dropped,
267
+ dangling: result.dangling,
268
+ dashboardPath,
224
269
  });
225
270
  }
226
271
  export async function handleNext(args) {
@@ -350,17 +395,20 @@ export async function handleSkill(args) {
350
395
  if (nextPhase) {
351
396
  throw new McpError(ErrorCode.InvalidRequest, `Cannot run skill: pipeline is not complete (next phase: ${nextPhase.id}). Finish the pipeline first.`);
352
397
  }
353
- const skillFile = join(state.workspaceDir, "skills", args.name, "SKILL.md");
354
- if (!(await pathExists(skillFile))) {
398
+ // Resolve against the installed list only: the name is never joined onto a
399
+ // path, so a traversal like `../findings/architecture` cannot splice a
400
+ // phase SKILL.md (or anything else) into the post-pipeline prompt.
401
+ const skillName = await resolveSkillName(state.workspaceDir, args.name);
402
+ if (!skillName) {
355
403
  const available = await listSkillNames(state.workspaceDir);
356
404
  const hint = available.length > 0 ? ` Available: ${available.join(", ")}.` : " No skills installed.";
357
- 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.`);
405
+ 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.`);
358
406
  }
359
- const prompt = await buildSkillPrompt(state, args.name);
360
- return textResult(prompt, { skill: args.name });
407
+ const prompt = await buildSkillPrompt(state, skillName);
408
+ return textResult(prompt, { skill: skillName });
361
409
  }
362
410
  // ---------- library helpers ----------
363
- async function resolveLibraryPath(args) {
411
+ function resolveLibraryPath(args, config) {
364
412
  const explicit = typeof args.library_path === "string" && args.library_path.trim() !== ""
365
413
  ? args.library_path.trim()
366
414
  : null;
@@ -370,18 +418,8 @@ async function resolveLibraryPath(args) {
370
418
  }
371
419
  return explicit;
372
420
  }
373
- if (typeof args.cwd === "string" && args.cwd.trim() !== "") {
374
- const cwd = args.cwd.trim();
375
- if (!isAbsolute(cwd)) {
376
- throw new McpError(ErrorCode.InvalidParams, `cwd must be absolute, got: ${cwd}`);
377
- }
378
- // loadCodecartoConfig merges user-global under per-workspace and tolerates
379
- // a missing workspace file, so a single call covers both cases.
380
- const workspaceDir = join(cwd, ".codecarto");
381
- const config = await loadCodecartoConfig(workspaceDir);
382
- if (config.library.path)
383
- return config.library.path;
384
- }
421
+ if (config.library.path)
422
+ return config.library.path;
385
423
  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).");
386
424
  }
387
425
  /**
@@ -391,17 +429,22 @@ async function resolveLibraryPath(args) {
391
429
  * the publish tool enforces.
392
430
  */
393
431
  async function loadEffectiveConfig(cwd) {
394
- if (typeof cwd !== "string" || cwd.trim() === "")
432
+ // This config decides whether publish_confirm gates the write, so the cwd
433
+ // it is read through is the one optionalCwd validated, never a raw argument.
434
+ if (!cwd)
395
435
  return loadUserConfig();
396
- // A relative path here resolves against the server process's working
397
- // directory, not the caller's, so it would quietly read some other
398
- // workspace's config — and this config decides whether publish_confirm
399
- // gates the write. Refuse rather than answer from the wrong file.
400
- const trimmed = cwd.trim();
401
- if (!isAbsolute(trimmed)) {
402
- throw new McpError(ErrorCode.InvalidParams, `cwd must be an absolute path, got: ${trimmed}`);
403
- }
404
- return loadCodecartoConfig(join(trimmed, ".codecarto"));
436
+ return loadCodecartoConfig(join(cwd, ".codecarto"));
437
+ }
438
+ /**
439
+ * The library tools answer from the config — its path, its namespace, its
440
+ * confirm gate — so a config file that could not be used in full is a
441
+ * refusal, not a silent fallback to whatever the other layer says (#242).
442
+ * The message names each file and key; codecarto_config shows the same list.
443
+ */
444
+ function refuseOnConfigProblems(config, tool) {
445
+ if (config.problems.length === 0)
446
+ return;
447
+ throw new McpError(ErrorCode.InvalidRequest, [`${tool} refused: the configuration has problems. Fix or remove the offending file, then retry.`, ...describeConfigProblems(config)].join("\n"));
405
448
  }
406
449
  function asStringArray(value, fieldName) {
407
450
  if (!Array.isArray(value)) {
@@ -488,12 +531,12 @@ async function resolveDefaultsFromWorkspace(cwd, overrides) {
488
531
  let namespace = typeof overrides.namespace === "string" && overrides.namespace.trim() !== ""
489
532
  ? overrides.namespace.trim()
490
533
  : null;
491
- if (typeof cwd === "string" && cwd.trim() !== "" && isAbsolute(cwd)) {
492
- const workspaceDir = join(cwd.trim(), ".codecarto");
534
+ if (cwd) {
535
+ const workspaceDir = join(cwd, ".codecarto");
493
536
  if (await pathExists(workspaceDir)) {
494
537
  if (pipeline === "unknown") {
495
538
  try {
496
- const state = await getWorkspaceState(cwd.trim());
539
+ const state = await getWorkspaceState(cwd);
497
540
  if (state?.status.pipeline)
498
541
  pipeline = state.status.pipeline;
499
542
  }
@@ -539,16 +582,20 @@ function provenanceConflictLines(conflicts) {
539
582
  }
540
583
  // ---------- library handlers ----------
541
584
  export async function handlePublish(args) {
542
- const libraryPath = await resolveLibraryPath(args);
585
+ // First, before anything is read through it: cwd is a containment root
586
+ // for spec_path below and the source of the config that gates the write.
587
+ const cwd = await optionalCwd(args.cwd);
588
+ const config = await loadEffectiveConfig(cwd);
589
+ refuseOnConfigProblems(config, "codecarto_publish");
590
+ const libraryPath = resolveLibraryPath(args, config);
543
591
  const marker = await discoverLibrary(libraryPath);
544
592
  if (!marker) {
545
593
  throw new McpError(ErrorCode.InvalidParams, `No CodeCartographer library at ${libraryPath} (missing .codecarto-library marker). Create one before publishing.`);
546
594
  }
547
595
  // Build allowed roots for spec_path containment: workspace .codecarto/ and library path
548
596
  const allowedRoots = [libraryPath];
549
- if (typeof args.cwd === "string" && args.cwd.trim() !== "") {
550
- allowedRoots.push(join(args.cwd.trim(), ".codecarto"));
551
- }
597
+ if (cwd)
598
+ allowedRoots.push(join(cwd, ".codecarto"));
552
599
  const spec = await readSpecArg(args, allowedRoots);
553
600
  if (typeof args.source_repo !== "string" || args.source_repo.trim() === "") {
554
601
  throw new McpError(ErrorCode.InvalidParams, "source_repo is required");
@@ -564,7 +611,7 @@ export async function handlePublish(args) {
564
611
  if (!isValidSlug(slug)) {
565
612
  throw new McpError(ErrorCode.InvalidParams, `Resolved slug "${slug}" is invalid. Provide an explicit slug (lowercase ASCII, starts with a letter, max 64 chars).`);
566
613
  }
567
- const defaults = await resolveDefaultsFromWorkspace(args.cwd, {
614
+ const defaults = await resolveDefaultsFromWorkspace(cwd, {
568
615
  pipeline: args.pipeline,
569
616
  namespace: args.namespace,
570
617
  });
@@ -593,7 +640,6 @@ export async function handlePublish(args) {
593
640
  // opt-out, and a host that never configured the key keeps the behavior it
594
641
  // had. Runs after every argument check so the preview names the resolved
595
642
  // slug and namespace, and before publishEntry so nothing is written.
596
- const config = await loadEffectiveConfig(args.cwd);
597
643
  if (config.library.publish_confirm && config.library.publish_confirm_configured && args.confirm !== true) {
598
644
  const preview = await previewPublishVersion(libraryPath, spec, { slug, namespace }, { forceNewVersion });
599
645
  const label = `${namespace ? `${namespace}/` : ""}${slug}`;
@@ -662,7 +708,9 @@ export async function handlePublish(args) {
662
708
  });
663
709
  }
664
710
  export async function handleLibraryList(args) {
665
- const libraryPath = await resolveLibraryPath(args);
711
+ const config = await loadEffectiveConfig(await optionalCwd(args.cwd));
712
+ refuseOnConfigProblems(config, "codecarto_library_list");
713
+ const libraryPath = resolveLibraryPath(args, config);
666
714
  const marker = await discoverLibrary(libraryPath);
667
715
  if (!marker) {
668
716
  throw new McpError(ErrorCode.InvalidParams, `No CodeCartographer library at ${libraryPath} (missing .codecarto-library marker).`);
@@ -703,7 +751,9 @@ export async function handleLibraryList(args) {
703
751
  });
704
752
  }
705
753
  export async function handleLibraryReindex(args) {
706
- const libraryPath = await resolveLibraryPath(args);
754
+ const config = await loadEffectiveConfig(await optionalCwd(args.cwd));
755
+ refuseOnConfigProblems(config, "codecarto_library_reindex");
756
+ const libraryPath = resolveLibraryPath(args, config);
707
757
  const marker = await discoverLibrary(libraryPath);
708
758
  if (!marker) {
709
759
  throw new McpError(ErrorCode.InvalidParams, `No CodeCartographer library at ${libraryPath} (missing .codecarto-library marker).`);
@@ -739,10 +789,17 @@ export async function handleLibraryInit(args) {
739
789
  });
740
790
  // Write config to user-global location
741
791
  const configPath = resolveUserConfigPath();
742
- await writeLibraryConfig(configPath, libraryPath, args.namespace ?? null);
792
+ // Writes library.path (and library.namespace when given) and nothing else:
793
+ // in particular it does not set publish_confirm, so initializing a library
794
+ // does not switch the codecarto_publish confirm gate on (#244). A config
795
+ // file that cannot be parsed is left alone and reported.
796
+ await writeLibraryConfig(configPath, libraryPath, args.namespace ?? null).catch((error) => {
797
+ throw new McpError(ErrorCode.InvalidRequest, error instanceof Error ? error.message : String(error));
798
+ });
799
+ const written = args.namespace ? "library.path and library.namespace" : "library.path";
743
800
  const msg = result.alreadyExisted
744
- ? `Library already exists at ${libraryPath} (marker preserved). Config written to ${configPath}.`
745
- : `Created library at ${libraryPath} with marker "${result.marker.name}". Config written to ${configPath}.`;
801
+ ? `Library already exists at ${libraryPath} (marker preserved). Wrote ${written} to ${configPath}; other keys untouched.`
802
+ : `Created library at ${libraryPath} with marker "${result.marker.name}". Wrote ${written} to ${configPath}; other keys untouched.`;
746
803
  return textResult(msg, {
747
804
  libraryPath,
748
805
  markerName: result.marker.name,
@@ -805,6 +862,9 @@ export async function handleConfig(args) {
805
862
  ` Library marker: ${markerStatus}`,
806
863
  ` User-global config: ${userConfigPath}`,
807
864
  ` Workspace config: ${workspaceConfigPath ?? "(no cwd provided)"}`,
865
+ // A file that could not be used is the one thing this tool exists
866
+ // to surface; the library tools refuse while any is listed (#242).
867
+ ...describeConfigProblems(config),
808
868
  ].join("\n"), {
809
869
  libraryPath: config.library.path,
810
870
  libraryNamespace: config.library.namespace,
@@ -812,6 +872,7 @@ export async function handleConfig(args) {
812
872
  llmSteerNextPhase: config.orchestrator.llm_steer_next_phase,
813
873
  userConfigPath,
814
874
  workspaceConfigPath,
875
+ problems: config.problems,
815
876
  });
816
877
  }
817
878
  // ---------- MCP parity handlers: open, usage, dashboard, list_skills ----------
@@ -1113,7 +1174,7 @@ const TOOLS = [
1113
1174
  },
1114
1175
  {
1115
1176
  name: "codecarto_validate",
1116
- description: "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.",
1177
+ 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.",
1117
1178
  inputSchema: {
1118
1179
  type: "object",
1119
1180
  properties: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codecartographer-pi",
3
- "version": "0.19.5",
3
+ "version": "0.20.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
  }