codecartographer-pi 0.12.11 → 0.14.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 (43) hide show
  1. package/.codecarto/CONTRIBUTING.md +1 -1
  2. package/.codecarto/GUIDE.md +4 -3
  3. package/.codecarto/NEW_THREAD_BLURB.md +9 -10
  4. package/.codecarto/findings/defect-scan-mechanical/SKILL.md +1 -1
  5. package/.codecarto/skills/spec-delta-application/SKILL.md +1 -1
  6. package/.codecarto/templates/architecture-map.md +2 -1
  7. package/.codecarto/templates/behavioral-contracts.md +2 -1
  8. package/.codecarto/templates/closeout-template.md +5 -2
  9. package/.codecarto/templates/mechanical-defects.md +4 -2
  10. package/.codecarto/templates/protocols-and-state.md +2 -1
  11. package/.codecarto/templates/reverse-engineering-bundle.md +2 -1
  12. package/.codecarto/templates/semantic-defects.md +3 -1
  13. package/.codecarto/templates/thread-log-entry-template.md +11 -13
  14. package/.codecarto/workflow/VALIDATE.md +17 -14
  15. package/.codecarto/workflow/pipeline-architecture-only.yaml +2 -2
  16. package/.codecarto/workflow/pipeline-defect-scan.yaml +4 -4
  17. package/.codecarto/workflow/pipeline-full-with-audit.yaml +12 -12
  18. package/.codecarto/workflow/pipeline-full-with-deep-audit.yaml +15 -15
  19. package/.codecarto/workflow/pipeline-lite.yaml +6 -6
  20. package/.codecarto/workflow/pipeline.yaml +10 -10
  21. package/.codecarto/workflow/scaffold-version.yaml +6 -0
  22. package/README.md +25 -1
  23. package/agent-skill/codecartographer/SKILL.md +143 -0
  24. package/agent-skill/codecartographer/references/carrying-results-forward.md +53 -0
  25. package/agent-skill/codecartographer/references/deep-audit-synthesis.md +54 -0
  26. package/agent-skill/codecartographer/references/executors.md +75 -0
  27. package/agent-skill/codecartographer/references/handoff-contract.md +80 -0
  28. package/agent-skill/codecartographer/references/kernel-first-rewrite.md +56 -0
  29. package/agent-skill/codecartographer/references/phase-recovery.md +48 -0
  30. package/agent-skill/codecartographer/references/pipeline-selection.md +38 -0
  31. package/dist/core/completion.js +13 -0
  32. package/dist/core/guide.d.ts +18 -0
  33. package/dist/core/guide.js +50 -0
  34. package/dist/core/index.d.ts +1 -0
  35. package/dist/core/index.js +1 -0
  36. package/dist/core/prompts.js +20 -1
  37. package/dist/core/types.d.ts +2 -0
  38. package/dist/core/workspace.d.ts +11 -0
  39. package/dist/core/workspace.js +56 -1
  40. package/dist/extensions/codecarto/index.js +4 -1
  41. package/dist/mcp-server/server.d.ts +14 -0
  42. package/dist/mcp-server/server.js +47 -10
  43. package/package.json +12 -3
@@ -0,0 +1,18 @@
1
+ /** Packaged agent-skill directory. Wrappers serve its contents through codecarto_guide. */
2
+ export declare const packagedAgentSkillDir: string;
3
+ /** A guide document: the main skill or one of its topic references. */
4
+ export type GuideDocument = {
5
+ /** `overview` for SKILL.md, otherwise the reference's basename without `.md`. */
6
+ topic: string;
7
+ /** Markdown body, with SKILL.md's installer frontmatter stripped. */
8
+ content: string;
9
+ };
10
+ /** Topic names available to {@link readGuide}, `overview` first. */
11
+ export declare function listGuideTopics(): Promise<string[]>;
12
+ /**
13
+ * Read one guide document.
14
+ * @param topic - `overview` (default) for the main skill, or a reference name from {@link listGuideTopics}.
15
+ * @returns the requested document.
16
+ * @throws when the packaged skill is missing, or the topic is not one of {@link listGuideTopics}.
17
+ */
18
+ export declare function readGuide(topic?: string): Promise<GuideDocument>;
@@ -0,0 +1,50 @@
1
+ // Serves the packaged agent skill — the instructions for driving this server —
2
+ // so an MCP client can read them without installing the skill files. The shipped
3
+ // markdown under agent-skill/ is the single source: nothing here duplicates its
4
+ // prose, and a drift test would have nothing to compare.
5
+ import { readdir, readFile } from "node:fs/promises";
6
+ import { basename, join } from "node:path";
7
+ import { pathExists } from "./utils.js";
8
+ import { packageRoot } from "./workspace.js";
9
+ /** Packaged agent-skill directory. Wrappers serve its contents through codecarto_guide. */
10
+ export const packagedAgentSkillDir = join(packageRoot, "agent-skill", "codecartographer");
11
+ /**
12
+ * Strip a leading YAML frontmatter block. The frontmatter is skill-installer
13
+ * metadata (name, version, license) that carries no instruction value for an
14
+ * agent reading the guide through the tool.
15
+ */
16
+ function stripFrontmatter(markdown) {
17
+ const match = /^---\r?\n[\s\S]*?\r?\n---\r?\n/.exec(markdown);
18
+ return match ? markdown.slice(match[0].length).trimStart() : markdown;
19
+ }
20
+ /** Topic names available to {@link readGuide}, `overview` first. */
21
+ export async function listGuideTopics() {
22
+ const referencesDir = join(packagedAgentSkillDir, "references");
23
+ if (!(await pathExists(referencesDir)))
24
+ return ["overview"];
25
+ const names = (await readdir(referencesDir))
26
+ .filter((name) => name.endsWith(".md"))
27
+ .map((name) => basename(name, ".md"))
28
+ .sort();
29
+ return ["overview", ...names];
30
+ }
31
+ /**
32
+ * Read one guide document.
33
+ * @param topic - `overview` (default) for the main skill, or a reference name from {@link listGuideTopics}.
34
+ * @returns the requested document.
35
+ * @throws when the packaged skill is missing, or the topic is not one of {@link listGuideTopics}.
36
+ */
37
+ export async function readGuide(topic = "overview") {
38
+ const requested = topic.trim() || "overview";
39
+ const available = await listGuideTopics();
40
+ if (!available.includes(requested)) {
41
+ throw new Error(`Unknown guide topic ${requested}. Available: ${available.join(", ")}.`);
42
+ }
43
+ const path = requested === "overview"
44
+ ? join(packagedAgentSkillDir, "SKILL.md")
45
+ : join(packagedAgentSkillDir, "references", `${requested}.md`);
46
+ if (!(await pathExists(path))) {
47
+ throw new Error(`Packaged agent skill is missing at ${path}. Reinstall codecartographer-pi.`);
48
+ }
49
+ return { topic: requested, content: stripFrontmatter(await readFile(path, "utf8")) };
50
+ }
@@ -8,6 +8,7 @@ export * from "./workspace.ts";
8
8
  export * from "./completion.ts";
9
9
  export * from "./orchestrator-config.ts";
10
10
  export * from "./usage.ts";
11
+ export * from "./guide.ts";
11
12
  export * from "./dashboard.ts";
12
13
  export * from "./library.ts";
13
14
  export * from "./synthesis.ts";
@@ -11,6 +11,7 @@ export * from "./workspace.js";
11
11
  export * from "./completion.js";
12
12
  export * from "./orchestrator-config.js";
13
13
  export * from "./usage.js";
14
+ export * from "./guide.js";
14
15
  export * from "./dashboard.js";
15
16
  export * from "./library.js";
16
17
  export * from "./synthesis.js";
@@ -4,6 +4,7 @@
4
4
  import { readdir } from "node:fs/promises";
5
5
  import { join } from "node:path";
6
6
  import { pathExists } from "./utils.js";
7
+ import { describeScaffoldStaleness } from "./workspace.js";
7
8
  import { runPhasePreflight } from "./synthesis.js";
8
9
  export function describeEntry(entry) {
9
10
  const parts = [];
@@ -30,6 +31,7 @@ export function collectRoutedCarryForward(state, targetPhaseId) {
30
31
  export async function buildPhasePrompt(state, phase, forced, options = {}) {
31
32
  const preflight = options.preflight ?? await runPhasePreflight(state, phase);
32
33
  const synthesisWorkflow = state.pipeline.workflow_name === "evidence-backed-project-synthesis";
34
+ const handoffTemplateExists = await pathExists(join(state.workspaceDir, "templates", "phase-handoff.yaml"));
33
35
  const lines = [
34
36
  `Read .codecarto/GUIDE.md and continue the CodeCartographer workflow for the phase \`${phase.id}\`.`,
35
37
  synthesisWorkflow
@@ -39,8 +41,10 @@ export async function buildPhasePrompt(state, phase, forced, options = {}) {
39
41
  "Required reads before analysis:",
40
42
  "- .codecarto/GUIDE.md",
41
43
  "- .codecarto/workflow/status.yaml",
42
- "- .codecarto/templates/phase-handoff.yaml",
43
44
  ];
45
+ if (handoffTemplateExists) {
46
+ lines.push("- .codecarto/templates/phase-handoff.yaml");
47
+ }
44
48
  const primaryOutput = phase.primary_output ? `.codecarto/${phase.primary_output}` : undefined;
45
49
  if (primaryOutput) {
46
50
  lines.push(`- ${primaryOutput} if it already exists (continue instead of duplicating work)`);
@@ -69,6 +73,21 @@ export async function buildPhasePrompt(state, phase, forced, options = {}) {
69
73
  if (await pathExists(decisionsPath)) {
70
74
  lines.push("- .codecarto/DECISIONS.md (numbered project decisions; new entries are appended in your closeout)");
71
75
  }
76
+ // Stale-scaffold warnings (issue #85): a workspace copied from an old
77
+ // template can contradict the runtime contract. Both surfaces render this
78
+ // prompt, so warn here rather than per-host.
79
+ const scaffoldWarnings = [];
80
+ if (!handoffTemplateExists) {
81
+ scaffoldWarnings.push(`.codecarto/templates/phase-handoff.yaml is missing — this scaffold predates the v0.12.0 handoff contract. Completion still requires a phase handoff at .codecarto/scratch/handoffs/${phase.id}.yaml; refresh the framework-owned files (GUIDE.md, templates/, workflow/VALIDATE.md, workflow/pipeline*.yaml) from the current CodeCartographer template, and trust this prompt over the workspace GUIDE.md where they disagree.`);
82
+ }
83
+ const staleness = describeScaffoldStaleness(state);
84
+ if (staleness)
85
+ scaffoldWarnings.push(staleness);
86
+ if (scaffoldWarnings.length > 0) {
87
+ lines.push("");
88
+ for (const warning of scaffoldWarnings)
89
+ lines.push(`WARNING: ${warning}`);
90
+ }
72
91
  const routed = collectRoutedCarryForward(state, phase.id);
73
92
  if (routed.length > 0) {
74
93
  lines.push("", `Items routed to \`${phase.id}\` for closure (carry_forward from earlier phases):`);
@@ -67,6 +67,8 @@ export type WorkspaceState = {
67
67
  pipelinePath: string;
68
68
  status: NormalizedStatus;
69
69
  pipeline: PipelineFile;
70
+ /** From workflow/scaffold-version.yaml; undefined for scaffolds that predate the marker. */
71
+ scaffoldVersion?: string;
70
72
  };
71
73
  export type ValidationOverall = "PASS" | "PASS WITH GAPS" | "FAIL" | "MISSING";
72
74
  export type ValidationResult = {
@@ -1,7 +1,18 @@
1
1
  import type { PhaseHandoff, WorkspaceState } from "./types.ts";
2
+ /** Installed package root. Anchors packaged assets served to clients (template, agent skill). */
3
+ export declare const packageRoot: string;
2
4
  export declare const packagedWorkspaceDir: string;
3
5
  export declare const PACKAGE_VERSION: string;
4
6
  export declare function getWorkspaceState(cwd: string): Promise<WorkspaceState | null>;
7
+ /**
8
+ * Human-readable staleness notice for the workspace's .codecarto/ scaffold,
9
+ * or null when the scaffold matches the running framework. A missing marker
10
+ * means the scaffold was copied from a release that predates it — those
11
+ * scaffolds may also predate the v0.12.0 handoff contract, whose GUIDE and
12
+ * pipelines instruct the exact opposite completion protocol. Warn, never
13
+ * fail: unversioned workspaces must keep working.
14
+ */
15
+ export declare function describeScaffoldStaleness(state: WorkspaceState): string | null;
5
16
  export declare function updateStatusAtomically(cwd: string, updater: (state: WorkspaceState) => Promise<{
6
17
  state: WorkspaceState;
7
18
  handoff?: PhaseHandoff;
@@ -26,7 +26,8 @@ function findPackageRoot(start) {
26
26
  }
27
27
  }
28
28
  const coreDir = dirname(fileURLToPath(import.meta.url));
29
- const packageRoot = findPackageRoot(coreDir);
29
+ /** Installed package root. Anchors packaged assets served to clients (template, agent skill). */
30
+ export const packageRoot = findPackageRoot(coreDir);
30
31
  // Path to the packaged framework template directory. Wrappers copy this on
31
32
  // /codecarto-init.
32
33
  export const packagedWorkspaceDir = join(packageRoot, ".codecarto");
@@ -76,6 +77,17 @@ export async function getWorkspaceState(cwd) {
76
77
  }
77
78
  const pipeline = await loadYamlFile(pipelinePath);
78
79
  const status = normalizeStatus(rawStatus, pipeline, pipelineRelativePath, cwd);
80
+ const scaffoldVersionPath = join(workspaceDir, "workflow", "scaffold-version.yaml");
81
+ let scaffoldVersion;
82
+ if (await pathExists(scaffoldVersionPath)) {
83
+ const marker = await loadYamlFile(scaffoldVersionPath);
84
+ if (typeof marker.scaffold_version === "string" && marker.scaffold_version.trim()) {
85
+ scaffoldVersion = marker.scaffold_version.trim();
86
+ }
87
+ else if (typeof marker.scaffold_version === "number") {
88
+ scaffoldVersion = String(marker.scaffold_version);
89
+ }
90
+ }
79
91
  return {
80
92
  cwd,
81
93
  workspaceDir,
@@ -83,8 +95,51 @@ export async function getWorkspaceState(cwd) {
83
95
  pipelinePath,
84
96
  pipeline,
85
97
  status,
98
+ ...(scaffoldVersion !== undefined && { scaffoldVersion }),
86
99
  };
87
100
  }
101
+ // Numeric x.y.z comparison; null when either side is not a plain dotted triple.
102
+ function compareDottedVersions(a, b) {
103
+ const parse = (version) => {
104
+ const match = /^(\d+)\.(\d+)\.(\d+)$/.exec(version.trim());
105
+ return match ? [Number(match[1]), Number(match[2]), Number(match[3])] : null;
106
+ };
107
+ const left = parse(a);
108
+ const right = parse(b);
109
+ if (!left || !right)
110
+ return null;
111
+ for (let i = 0; i < 3; i++) {
112
+ if (left[i] !== right[i])
113
+ return left[i] < right[i] ? -1 : 1;
114
+ }
115
+ return 0;
116
+ }
117
+ /**
118
+ * Human-readable staleness notice for the workspace's .codecarto/ scaffold,
119
+ * or null when the scaffold matches the running framework. A missing marker
120
+ * means the scaffold was copied from a release that predates it — those
121
+ * scaffolds may also predate the v0.12.0 handoff contract, whose GUIDE and
122
+ * pipelines instruct the exact opposite completion protocol. Warn, never
123
+ * fail: unversioned workspaces must keep working.
124
+ */
125
+ export function describeScaffoldStaleness(state) {
126
+ const scaffold = state.scaffoldVersion;
127
+ if (!scaffold) {
128
+ return "This workspace's .codecarto/ scaffold has no workflow/scaffold-version.yaml marker (introduced after v0.12.11), so its framework-owned files (GUIDE.md, templates/, workflow/ pipelines and VALIDATE.md) may predate the v0.12.0 handoff contract. Refresh them from the packaged CodeCartographer template.";
129
+ }
130
+ const comparison = compareDottedVersions(scaffold, PACKAGE_VERSION);
131
+ if (comparison === 0)
132
+ return null;
133
+ if (comparison === null) {
134
+ return scaffold === PACKAGE_VERSION
135
+ ? null
136
+ : `This workspace's scaffold version (${scaffold}) does not match the running framework (${PACKAGE_VERSION}). Refresh the framework-owned files (GUIDE.md, templates/, workflow/) from the packaged template.`;
137
+ }
138
+ if (comparison < 0) {
139
+ return `This workspace's scaffold (v${scaffold}) is older than the running framework (v${PACKAGE_VERSION}). Refresh the framework-owned files (GUIDE.md, templates/, workflow/) from the packaged template to pick up pipeline and template fixes.`;
140
+ }
141
+ return `This workspace's scaffold (v${scaffold}) is newer than the running framework (v${PACKAGE_VERSION}). Upgrade CodeCartographer to at least v${scaffold}.`;
142
+ }
88
143
  export async function updateStatusAtomically(cwd, updater) {
89
144
  const workspaceDir = join(cwd, ".codecarto");
90
145
  const statusPath = join(workspaceDir, "workflow", "status.yaml");
@@ -8,7 +8,7 @@ import { narrateDashboard } from "./dashboard-narrator.js";
8
8
  import { writeDashboard } from "./dashboard-writer.js";
9
9
  import { parseNextFlags } from "./next-flags.js";
10
10
  import { phaseCompactionExtension } from "./phase-compaction.js";
11
- import { buildPhasePrompt, buildSkillPrompt, buildValidationSummary, canonicalPath, computePerPhaseTotals, computeTotals, createEmptyStatus, DEFAULT_PIPELINE_PATH, deriveSlug, discoverLibrary, getNextEligiblePhase, getPipelineLabel, getWorkspaceState, isWithinPathResolved, listSkillNames, loadCodecartoConfig, loadUsage, loadYamlFile, normalizeForComparison, packagedWorkspaceDir, pathExists, PACKAGE_VERSION, PhasePreflightError, PIPELINE_ALIASES, publishEntry, resolvePhase, resolvePipelineChoice, runPhasePreflight, stringifySimpleYaml, switchPipeline, validatePhaseOutput, writeLibraryConfig, } from "../../core/index.js";
11
+ import { buildPhasePrompt, buildSkillPrompt, buildValidationSummary, canonicalPath, computePerPhaseTotals, computeTotals, createEmptyStatus, DEFAULT_PIPELINE_PATH, describeScaffoldStaleness, deriveSlug, discoverLibrary, getNextEligiblePhase, getPipelineLabel, getWorkspaceState, isWithinPathResolved, listSkillNames, loadCodecartoConfig, loadUsage, loadYamlFile, normalizeForComparison, packagedWorkspaceDir, pathExists, PACKAGE_VERSION, PhasePreflightError, PIPELINE_ALIASES, publishEntry, resolvePhase, resolvePipelineChoice, runPhasePreflight, stringifySimpleYaml, switchPipeline, validatePhaseOutput, writeLibraryConfig, } from "../../core/index.js";
12
12
  import { initLibrary } from "../../core/library.js";
13
13
  import { resolveUserConfigPath } from "../../core/orchestrator-config.js";
14
14
  const STATUS_WIDGET_ID = "codecarto-widget";
@@ -71,6 +71,9 @@ function buildStatusLines(state, extraLines = []) {
71
71
  `Post-pipeline work: ${postPipelinePending} pending`,
72
72
  `Next: ${nextAction}`,
73
73
  ];
74
+ const scaffoldNotice = describeScaffoldStaleness(state);
75
+ if (scaffoldNotice)
76
+ lines.push(`Scaffold: ${scaffoldNotice}`);
74
77
  if (extraLines.length > 0) {
75
78
  lines.push("", ...extraLines);
76
79
  }
@@ -78,6 +78,11 @@ export declare function handleSkill(args: {
78
78
  }>;
79
79
  structuredContent?: Record<string, unknown>;
80
80
  }>;
81
+ export declare function readSpecArg(args: {
82
+ spec?: unknown;
83
+ spec_path?: unknown;
84
+ cwd?: unknown;
85
+ }, allowedRoots: string[]): Promise<string>;
81
86
  export declare function handlePublish(args: Record<string, unknown>): Promise<{
82
87
  content: Array<{
83
88
  type: "text";
@@ -166,6 +171,15 @@ export declare function handleListSkills(args: {
166
171
  }>;
167
172
  structuredContent?: Record<string, unknown>;
168
173
  }>;
174
+ export declare function handleGuide(args: {
175
+ topic?: string;
176
+ }): Promise<{
177
+ content: Array<{
178
+ type: "text";
179
+ text: string;
180
+ }>;
181
+ structuredContent?: Record<string, unknown>;
182
+ }>;
169
183
  export declare function buildServer(): Server<{
170
184
  method: string;
171
185
  params?: {
@@ -15,7 +15,7 @@ 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, getNextEligiblePhase, getPipelineLabel, getWorkspaceState, isValidSlug, isWithinPathResolved, listEntries, 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, resolvePhase, resolvePipelineChoice, stringifySimpleYaml, switchPipeline, validatePhaseOutput, writeLibraryConfig, } from "../core/index.js";
19
19
  import { initLibrary } from "../core/library.js";
20
20
  import { loadUserConfig, resolveUserConfigPath } from "../core/orchestrator-config.js";
21
21
  import { writeDashboard } from "../extensions/codecarto/dashboard-writer.js";
@@ -118,7 +118,8 @@ export async function handleStatus(args) {
118
118
  const currentOpenQuestions = currentPhase === "complete" ? 0 : state.status.phases[currentPhase]?.open_questions.length ?? 0;
119
119
  const terminalOpenQuestions = Object.values(state.status.phases).reduce((sum, phase) => sum + (phase.open_questions?.length ?? 0), 0);
120
120
  const postPipelinePending = state.status.post_pipeline.filter((entry) => entry.status !== "resolved").length;
121
- const summary = [
121
+ const scaffoldNotice = describeScaffoldStaleness(state);
122
+ const summaryLines = [
122
123
  `Phase: ${currentPhase}`,
123
124
  `Pipeline state: ${currentPhase === "complete" ? "complete" : "in progress"}`,
124
125
  `Pipeline: ${getPipelineLabel(state.status.pipeline)} (${state.status.pipeline})`,
@@ -127,8 +128,12 @@ export async function handleStatus(args) {
127
128
  `Carry-forward (pipeline phases): ${totalCarryForward}`,
128
129
  `Post-pipeline work: ${postPipelinePending} pending`,
129
130
  `Next: ${state.status.next_actions[0] ?? (nextPhase ? `Begin ${nextPhase.id}` : "All phases complete.")}`,
130
- ].join("\n");
131
+ ];
132
+ if (scaffoldNotice)
133
+ summaryLines.push(`Scaffold: ${scaffoldNotice}`);
134
+ const summary = summaryLines.join("\n");
131
135
  return textResult(summary, {
136
+ ...(scaffoldNotice ? { scaffoldNotice } : {}),
132
137
  currentPhase,
133
138
  pipeline: state.status.pipeline,
134
139
  pipelineLabel: getPipelineLabel(state.status.pipeline),
@@ -325,25 +330,32 @@ function buildGenerationFromArg(model_metadata) {
325
330
  out.notes = m.notes;
326
331
  return out;
327
332
  }
328
- async function readSpecArg(args, allowedRoots = []) {
333
+ // `allowedRoots` is deliberately required and must be non-empty whenever
334
+ // spec_path is used. It previously defaulted to `[]`, which made containment
335
+ // opt-in: a caller that omitted it would read any absolute path the client
336
+ // asked for, silently reopening the arbitrary-file-read class of bug fixed in
337
+ // v0.12.11. Containment is now the default posture and an empty root set is a
338
+ // programming error rather than a bypass.
339
+ export async function readSpecArg(args, allowedRoots) {
329
340
  if (typeof args.spec === "string" && args.spec.length > 0)
330
341
  return args.spec;
331
342
  if (typeof args.spec_path === "string" && args.spec_path.length > 0) {
332
343
  if (!isAbsolute(args.spec_path)) {
333
344
  throw new McpError(ErrorCode.InvalidParams, `spec_path must be absolute, got: ${args.spec_path}`);
334
345
  }
346
+ if (!Array.isArray(allowedRoots) || allowedRoots.length === 0) {
347
+ throw new McpError(ErrorCode.InternalError, "refusing to read spec_path without a containment root — this is a caller bug, not a client error");
348
+ }
335
349
  if (!(await pathExists(args.spec_path))) {
336
350
  throw new McpError(ErrorCode.InvalidParams, `spec_path does not exist: ${args.spec_path}`);
337
351
  }
338
352
  // Enforce path containment: spec_path must be within an allowed root
339
353
  // (cwd's .codecarto/ or the configured library path) to prevent
340
354
  // arbitrary file reads.
341
- if (allowedRoots.length > 0) {
342
- const resolvedSpecPath = await canonicalPath(args.spec_path);
343
- const withinAllowed = await Promise.all(allowedRoots.map((root) => isWithinPathResolved(resolvedSpecPath, root)));
344
- if (!withinAllowed.some((result) => result)) {
345
- throw new McpError(ErrorCode.InvalidParams, `spec_path must be within the workspace (.codecarto/) or the configured library path. Got: ${args.spec_path}`);
346
- }
355
+ const resolvedSpecPath = await canonicalPath(args.spec_path);
356
+ const withinAllowed = await Promise.all(allowedRoots.map((root) => isWithinPathResolved(resolvedSpecPath, root)));
357
+ if (!withinAllowed.some((result) => result)) {
358
+ throw new McpError(ErrorCode.InvalidParams, `spec_path must be within the workspace (.codecarto/) or the configured library path. Got: ${args.spec_path}`);
347
359
  }
348
360
  return readFile(args.spec_path, "utf8");
349
361
  }
@@ -864,6 +876,19 @@ const TOOLS = [
864
876
  required: ["cwd"],
865
877
  },
866
878
  },
879
+ {
880
+ name: "codecarto_guide",
881
+ description: "Return the instructions for driving this server: the status/next/execute/validate/complete loop, the phase-handoff contract, pipeline selection, executor choice, and recovery. Call this first when you have not run a CodeCartographer pipeline before. Takes no workspace.",
882
+ inputSchema: {
883
+ type: "object",
884
+ properties: {
885
+ topic: {
886
+ type: "string",
887
+ description: "Guide topic. Omit for the overview; other topics are listed in every response.",
888
+ },
889
+ },
890
+ },
891
+ },
867
892
  {
868
893
  name: "codecarto_list_skills",
869
894
  description: "List available post-pipeline skills installed in the workspace.",
@@ -893,7 +918,19 @@ const HANDLERS = {
893
918
  codecarto_usage: handleUsage,
894
919
  codecarto_dashboard: handleDashboard,
895
920
  codecarto_list_skills: handleListSkills,
921
+ codecarto_guide: handleGuide,
896
922
  };
923
+ export async function handleGuide(args) {
924
+ const topics = await listGuideTopics();
925
+ const document = await readGuide(args.topic).catch((error) => {
926
+ throw new McpError(ErrorCode.InvalidParams, error instanceof Error ? error.message : String(error));
927
+ });
928
+ const other = topics.filter((name) => name !== document.topic);
929
+ const footer = other.length > 0
930
+ ? `\n\n---\nOther guide topics: ${other.join(", ")} (call codecarto_guide with topic).`
931
+ : "";
932
+ return textResult(`${document.content}${footer}`, { topic: document.topic, topics });
933
+ }
897
934
  // ---------- server bootstrap ----------
898
935
  export function buildServer() {
899
936
  const server = new Server({ name: "codecartographer", version: PACKAGE_VERSION }, { capabilities: { tools: {} } });
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "codecartographer-pi",
3
- "version": "0.12.11",
3
+ "version": "0.14.0",
4
4
  "mcpName": "io.github.HuginnIndustries/codecartographer",
5
- "description": "Evidence-backed reverse engineering and human-gated software planning for Pi and MCP coding agents.",
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",
7
7
  "keywords": [
8
8
  "pi-package",
@@ -13,7 +13,12 @@
13
13
  "mcp",
14
14
  "software-planning",
15
15
  "code-analysis",
16
- "synthesis"
16
+ "synthesis",
17
+ "codebase",
18
+ "ai-agent",
19
+ "context-engineering",
20
+ "code-understanding",
21
+ "spec-driven"
17
22
  ],
18
23
  "license": "MIT",
19
24
  "author": "James Sesler",
@@ -30,6 +35,7 @@
30
35
  },
31
36
  "files": [
32
37
  ".codecarto/**/*",
38
+ "agent-skill/**/*",
33
39
  "dist/**/*",
34
40
  "assets/logo.svg",
35
41
  "README.md",
@@ -59,5 +65,8 @@
59
65
  },
60
66
  "devDependencies": {
61
67
  "typescript": "^5.9.3"
68
+ },
69
+ "overrides": {
70
+ "undici": "^8.10.0"
62
71
  }
63
72
  }