codecartographer-pi 0.14.0 → 0.15.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 (33) hide show
  1. package/.codecarto/BACKLOG.md +4 -12
  2. package/.codecarto/GUIDE.md +35 -36
  3. package/.codecarto/NEW_THREAD_BLURB.md +3 -3
  4. package/.codecarto/skills/spec-delta-application/SKILL.md +2 -2
  5. package/.codecarto/templates/amendment.yaml +17 -0
  6. package/.codecarto/templates/conventions-template.md +6 -5
  7. package/.codecarto/templates/decisions-template.md +13 -10
  8. package/.codecarto/templates/phase-handoff.yaml +7 -0
  9. package/.codecarto/templates/spike-report.md +51 -0
  10. package/.codecarto/workflow/scaffold-version.yaml +1 -1
  11. package/agent-skill/codecartographer/SKILL.md +4 -1
  12. package/agent-skill/codecartographer/references/handoff-contract.md +12 -1
  13. package/agent-skill/codecartographer/references/orchestration.md +45 -0
  14. package/dist/core/amendment.d.ts +41 -0
  15. package/dist/core/amendment.js +140 -0
  16. package/dist/core/completion.d.ts +16 -0
  17. package/dist/core/completion.js +175 -3
  18. package/dist/core/index.d.ts +1 -0
  19. package/dist/core/index.js +1 -0
  20. package/dist/core/pipeline.js +18 -0
  21. package/dist/core/prompts.js +53 -0
  22. package/dist/core/status.d.ts +8 -1
  23. package/dist/core/status.js +27 -1
  24. package/dist/core/types.d.ts +24 -0
  25. package/dist/core/usage.d.ts +7 -0
  26. package/dist/core/usage.js +2 -1
  27. package/dist/core/workspace.d.ts +39 -0
  28. package/dist/core/workspace.js +92 -4
  29. package/dist/extensions/codecarto/auto-runner.js +1 -0
  30. package/dist/extensions/codecarto/index.js +4 -1
  31. package/dist/mcp-server/server.d.ts +118 -57
  32. package/dist/mcp-server/server.js +124 -9
  33. package/package.json +1 -1
@@ -15,7 +15,9 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
15
15
  import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } from "@modelcontextprotocol/sdk/types.js";
16
16
  import { cp, mkdir, readFile, rename, writeFile } from "node:fs/promises";
17
17
  import { basename, isAbsolute, join } from "node:path";
18
- import { buildPhasePrompt, buildSkillPrompt, buildValidationSummary, canonicalPath, completeValidatedPhase, computePerPhaseTotals, computeTotals, createEmptyStatus, DEFAULT_PIPELINE_PATH, deriveSlug, discoverLibrary, describeScaffoldStaleness, getNextEligiblePhase, getPipelineLabel, getWorkspaceState, isValidSlug, isWithinPathResolved, listEntries, listGuideTopics, readGuide, listSkillNames, loadCodecartoConfig, loadUsage, loadYamlFile, normalizeForComparison, PACKAGE_VERSION, packagedWorkspaceDir, pathExists, PhasePreflightError, publishEntry, reindex as libraryReindex, resolvePhase, resolvePipelineChoice, stringifySimpleYaml, switchPipeline, validatePhaseOutput, writeLibraryConfig, } from "../core/index.js";
18
+ import { buildPhasePrompt, buildSkillPrompt, buildValidationSummary, canonicalPath, completeValidatedPhase, computePerPhaseTotals, computeTotals, createEmptyStatus, DEFAULT_PIPELINE_PATH, deriveSlug, discoverLibrary, describeScaffoldStaleness, getNextEligiblePhase, getPipelineLabel, getWorkspaceState, isValidSlug, isWithinPathResolved, listEntries, listGuideTopics, readGuide, listSkillNames, loadCodecartoConfig, loadUsage, loadYamlFile, normalizeForComparison, PACKAGE_VERSION, packagedWorkspaceDir, pathExists, PhasePreflightError, publishEntry, reindex as libraryReindex, refreshScaffold, resolvePhase, resolvePipelineChoice, seedOrchestratorFiles, stringifySimpleYaml, switchPipeline, validatePhaseOutput, writeLibraryConfig, } from "../core/index.js";
19
+ import { applyAmendment } from "../core/amendment.js";
20
+ import { appendUsageRun } from "../core/usage.js";
19
21
  import { initLibrary } from "../core/library.js";
20
22
  import { loadUserConfig, resolveUserConfigPath } from "../core/orchestrator-config.js";
21
23
  import { writeDashboard } from "../extensions/codecarto/dashboard-writer.js";
@@ -41,13 +43,17 @@ async function requireWorkspace(cwd) {
41
43
  }
42
44
  return state;
43
45
  }
46
+ // MCP clients may read structuredContent in preference to content when both are
47
+ // present. The tools whose payload IS prose — the phase prompts, the guide —
48
+ // therefore have to expose it structurally too, or such a client receives labels
49
+ // and no payload: codecarto_next returned no phase prompt at all (issue #94).
50
+ // Carrying the rendered text under a stable key keeps both client styles whole,
51
+ // and no tool declares an outputSchema that this could violate.
44
52
  function textResult(text, structured) {
45
- const result = {
53
+ return {
46
54
  content: [{ type: "text", text }],
55
+ structuredContent: { ...structured, text },
47
56
  };
48
- if (structured)
49
- result.structuredContent = structured;
50
- return result;
51
57
  }
52
58
  async function buildMcpPhasePrompt(state, phase, forced) {
53
59
  try {
@@ -100,12 +106,23 @@ export async function handleInit(args) {
100
106
  const normalizedStatus = createEmptyStatus(basename(cwd), selectedPipelinePath, pipeline);
101
107
  normalizedStatus.last_updated = new Date().toISOString();
102
108
  await writeFile(statusPath, `${stringifySimpleYaml(normalizedStatus)}\n`, "utf8");
109
+ // Orchestration is on by default (issue #97/#98): the driving chat holds the
110
+ // duties, so the files those duties maintain exist from the first phase.
111
+ const seeded = await seedOrchestratorFiles(targetWorkspaceDir);
103
112
  const label = getPipelineLabel(selectedPipelinePath);
104
- return textResult(`Initialized CodeCartographer workspace at ${targetWorkspaceDir}.\nPipeline: ${label} (${selectedPipelinePath})\nFirst phase: ${normalizedStatus.current_phase}`, {
113
+ const initLines = [
114
+ `Initialized CodeCartographer workspace at ${targetWorkspaceDir}.`,
115
+ `Pipeline: ${label} (${selectedPipelinePath})`,
116
+ `First phase: ${normalizedStatus.current_phase}`,
117
+ ];
118
+ if (seeded.length > 0)
119
+ initLines.push(`Seeded orchestrator files: ${seeded.join(", ")} (the driving chat holds the orchestrator duties — see GUIDE.md §Roles).`);
120
+ return textResult(initLines.join("\n"), {
105
121
  workspaceDir: targetWorkspaceDir,
106
122
  pipeline: selectedPipelinePath,
107
123
  pipelineLabel: label,
108
124
  firstPhase: normalizedStatus.current_phase,
125
+ seededOrchestratorFiles: seeded,
109
126
  });
110
127
  }
111
128
  export async function handleStatus(args) {
@@ -212,6 +229,7 @@ export async function handleValidate(args) {
212
229
  rows: validation.rows,
213
230
  gaps: validation.gaps,
214
231
  errors: validation.errors,
232
+ secondaryOutputs: validation.secondaryOutputs ?? [],
215
233
  });
216
234
  }
217
235
  export async function handleComplete(args) {
@@ -223,20 +241,46 @@ export async function handleComplete(args) {
223
241
  if (validation.overall === "FAIL" || validation.overall === "MISSING") {
224
242
  throw new McpError(ErrorCode.InvalidRequest, `Cannot complete ${validation.phaseId}: validation is ${validation.overall}.\n${buildValidationSummary(validation).join("\n")}`);
225
243
  }
226
- const { updatedState, closeoutNotice } = await completeValidatedPhase(cwd, validation, "codecarto_complete").catch((error) => {
244
+ const { updatedState, closeoutNotice, orchestratorCheckpoint } = await completeValidatedPhase(cwd, validation, "codecarto_complete").catch((error) => {
227
245
  throw new McpError(ErrorCode.InvalidParams, error instanceof Error ? error.message : String(error));
228
246
  });
247
+ // Record the run in the usage log (issue #100). MCP hosts execute phases in
248
+ // their own context, so tokens and activity are unknowable here — this is a
249
+ // completion receipt, marked recorded_by so totals can tell it apart from
250
+ // the Pi runner's token-bearing entries. Recording lives in this handler,
251
+ // not core completion, because Pi-driven runs already record with real
252
+ // telemetry and must not double-count.
253
+ try {
254
+ await appendUsageRun(updatedState.workspaceDir, {
255
+ timestamp: new Date().toISOString(),
256
+ phase: validation.phaseId,
257
+ status: "completed",
258
+ turn_count: 0,
259
+ tool_uses: 0,
260
+ duration_ms: 0,
261
+ tokens: { input: 0, output: 0, cache_write: 0 },
262
+ recorded_by: "mcp-complete",
263
+ });
264
+ }
265
+ catch {
266
+ // Usage is best-effort telemetry: the phase completed and canonical
267
+ // state is already written, so a usage-log write failure must not fail
268
+ // the completion result. Nothing else can act on the error here.
269
+ }
229
270
  const lines = [
230
271
  `Marked ${validation.phaseId} complete (validation: ${validation.overall}).`,
231
272
  `Next phase: ${updatedState.status.current_phase}`,
232
273
  ];
233
274
  if (closeoutNotice)
234
275
  lines.push(closeoutNotice);
276
+ if (orchestratorCheckpoint)
277
+ lines.push(orchestratorCheckpoint);
235
278
  return textResult(lines.join("\n"), {
236
279
  completedPhase: validation.phaseId,
237
280
  validation: validation.overall,
238
281
  nextPhase: updatedState.status.current_phase,
239
282
  closeoutNotice,
283
+ orchestratorCheckpoint,
240
284
  });
241
285
  }
242
286
  export async function handleSkill(args) {
@@ -615,18 +659,22 @@ export async function handleUsage(args) {
615
659
  }
616
660
  const totals = computeTotals(usage);
617
661
  const perPhase = computePerPhaseTotals(usage);
662
+ const receiptRuns = usage.runs.filter((run) => run.recorded_by === "mcp-complete").length;
618
663
  const lines = [
619
664
  `Total runs: ${totals.runs}`,
620
665
  `Total tokens: ${totals.tokens.input} in / ${totals.tokens.output} out / ${totals.tokens.cache_write} cache-write`,
621
666
  `Total duration: ${totals.duration_ms}ms / ${totals.tool_uses} tool uses`,
622
- "",
623
- "Per-phase totals:",
624
667
  ];
668
+ if (receiptRuns > 0) {
669
+ lines.push(`Note: ${receiptRuns} run(s) recorded via codecarto_complete carry no token or activity data (MCP hosts execute phases in their own context) — zeros above are unknowns, not free runs.`);
670
+ }
671
+ lines.push("", "Per-phase totals:");
625
672
  for (const [phaseId, t] of perPhase) {
626
673
  lines.push(` ${phaseId}: ${t.runs} run(s), ${t.tokens.input + t.tokens.output} tokens, ${t.tool_uses} tool uses, ${t.duration_ms}ms`);
627
674
  }
628
675
  return textResult(lines.join("\n"), {
629
676
  runs: totals.runs,
677
+ receiptRuns,
630
678
  tokens: totals.tokens,
631
679
  toolUses: totals.tool_uses,
632
680
  durationMs: totals.duration_ms,
@@ -648,6 +696,50 @@ export async function handleListSkills(args) {
648
696
  : ["No skills installed."];
649
697
  return textResult(lines.join("\n"), { skills });
650
698
  }
699
+ export async function handleRefreshScaffold(args) {
700
+ const cwd = await validateCwd(args.cwd);
701
+ await requireWorkspace(cwd);
702
+ const result = await refreshScaffold(cwd).catch((error) => {
703
+ throw new McpError(ErrorCode.InvalidRequest, error instanceof Error ? error.message : String(error));
704
+ });
705
+ const shown = result.written.slice(0, 20);
706
+ const lines = [
707
+ `Refreshed ${result.written.length} framework-owned file(s) from the packaged template (${result.scaffoldVersionBefore ?? "unversioned"} → ${result.scaffoldVersionAfter}).`,
708
+ "Project state, user config, findings outputs, scratch, closeouts, and orchestrator files were not touched.",
709
+ ...shown.map((path) => ` - .codecarto/${path}`),
710
+ ];
711
+ if (result.written.length > shown.length)
712
+ lines.push(` … +${result.written.length - shown.length} more`);
713
+ return textResult(lines.join("\n"), {
714
+ written: result.written,
715
+ scaffoldVersionBefore: result.scaffoldVersionBefore,
716
+ scaffoldVersionAfter: result.scaffoldVersionAfter,
717
+ });
718
+ }
719
+ export async function handleAmend(args) {
720
+ if (typeof args.name !== "string" || !args.name.trim()) {
721
+ throw new McpError(ErrorCode.InvalidParams, "name is required (the amendment file's slug under .codecarto/scratch/amendments/)");
722
+ }
723
+ const cwd = await validateCwd(args.cwd);
724
+ await requireWorkspace(cwd);
725
+ const { applied, closeoutNotice } = await applyAmendment(cwd, args.name).catch((error) => {
726
+ throw new McpError(ErrorCode.InvalidRequest, error instanceof Error ? error.message : String(error));
727
+ });
728
+ const lines = [
729
+ `Amendment applied.`,
730
+ `Open questions closed: ${applied.openQuestionsClosed.length > 0 ? applied.openQuestionsClosed.join(", ") : "none"}`,
731
+ `Post-pipeline items closed: ${applied.postPipelineClosed.length > 0 ? applied.postPipelineClosed.join(", ") : "none"}`,
732
+ ];
733
+ if (applied.unknownIds.length > 0)
734
+ lines.push(`Ids that matched nothing (already closed or unknown): ${applied.unknownIds.join(", ")}`);
735
+ lines.push(closeoutNotice);
736
+ return textResult(lines.join("\n"), {
737
+ openQuestionsClosed: applied.openQuestionsClosed,
738
+ postPipelineClosed: applied.postPipelineClosed,
739
+ unknownIds: applied.unknownIds,
740
+ closeoutNotice,
741
+ });
742
+ }
651
743
  // ---------- tool registry ----------
652
744
  const TOOLS = [
653
745
  {
@@ -898,9 +990,32 @@ const TOOLS = [
898
990
  required: ["cwd"],
899
991
  },
900
992
  },
993
+ {
994
+ name: "codecarto_amend",
995
+ description: "Apply a post-pipeline amendment from .codecarto/scratch/amendments/<name>.yaml to workflow/status.yaml: close open questions resolved on evidence after the pipeline completed and retire finished post-pipeline backlog items, under the same lock completion uses. Writes an amendment closeout and THREAD_LOG entry. Refused while the pipeline is incomplete — mid-pipeline resolutions belong in the phase handoff. Idempotent: ids that no longer match are reported, not fatal.",
996
+ inputSchema: {
997
+ type: "object",
998
+ properties: {
999
+ cwd: { type: "string", description: "Absolute path to the target repository." },
1000
+ name: { type: "string", description: "Amendment file slug under .codecarto/scratch/amendments/ (with or without .yaml)." },
1001
+ },
1002
+ required: ["cwd", "name"],
1003
+ },
1004
+ },
1005
+ {
1006
+ name: "codecarto_refresh_scaffold",
1007
+ description: "Refresh a workspace's framework-owned files (GUIDE.md, templates/, workflow pipelines and VALIDATE.md, skills/, findings SKILL and README stubs) from the packaged template — the action every scaffold-staleness warning instructs. Never touches project state (status.yaml), user config (workflow/config.yaml, usage log), findings outputs, scratch/, closeouts/, or the orchestrator files (CONVENTIONS.md, DECISIONS.md, BACKLOG.md, THREAD_LOG.md). Appends one THREAD_LOG entry naming the version transition. Unlike codecarto_init force:true, nothing is backed up or lost.",
1008
+ inputSchema: {
1009
+ type: "object",
1010
+ properties: { cwd: { type: "string", description: "Absolute path to the target repository." } },
1011
+ required: ["cwd"],
1012
+ },
1013
+ },
901
1014
  ];
902
1015
  const HANDLERS = {
1016
+ codecarto_amend: handleAmend,
903
1017
  codecarto_init: handleInit,
1018
+ codecarto_refresh_scaffold: handleRefreshScaffold,
904
1019
  codecarto_status: handleStatus,
905
1020
  codecarto_switch_pipeline: handleSwitchPipeline,
906
1021
  codecarto_next: handleNext,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codecartographer-pi",
3
- "version": "0.14.0",
3
+ "version": "0.15.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",