codecartographer-pi 0.12.3 → 0.12.9

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.
@@ -0,0 +1,80 @@
1
+ ---
2
+ name: guided-vision-interview
3
+ description: Conduct a structured product discovery interview to extract a rich vision brief from the user before the synthesis pipeline's vision-capture phase.
4
+ ---
5
+
6
+ # Guided Vision Interview
7
+
8
+ Your job is to conduct a conversational product discovery interview that draws out the user's product idea and writes it into `inputs/vision.md` as a rich brief. The synthesis pipeline's vision-capture phase will then structure this brief into a testable vision document.
9
+
10
+ ## Interview structure
11
+
12
+ Ask questions in this order. Do not dump all questions at once — ask one or two at a time, wait for the answer, then probe deeper based on what the user said. Skip questions the user has already answered unprompted.
13
+
14
+ ### 1. Audience and problem (2-3 questions)
15
+ - Who specifically is this for? (Not "everyone" — get them to name a concrete persona or team)
16
+ - What problem does this solve for them? What are they doing today, and why is it painful?
17
+ - Why are existing solutions insufficient? What's the gap?
18
+
19
+ ### 2. Desired outcomes (2-3 questions)
20
+ - What should the user be able to do that they can't today?
21
+ - What does success look like? How would they know it worked?
22
+ - What's the most important outcome — the one thing that must work?
23
+
24
+ ### 3. Scope and non-goals (2-3 questions)
25
+ - What's explicitly in scope for this version?
26
+ - What are you deliberately NOT building? What's a non-goal?
27
+ - Is there anything you're tempted to include but know you should defer?
28
+
29
+ ### 4. Constraints (2-3 questions)
30
+ - Any hard technical constraints? (language, platform, integration, performance)
31
+ - Any operational constraints? (self-hosted, cloud, offline, privacy, latency)
32
+ - Any timeline or team-size constraints?
33
+
34
+ ### 5. Success measures and acceptance (1-2 questions)
35
+ - How would you measure success? What metric or observation would tell you it's working?
36
+ - Can you describe one specific scenario where someone uses this and has a good experience?
37
+
38
+ ## Writing the brief
39
+
40
+ After the interview, synthesize the user's answers into `inputs/vision.md`. Use this structure:
41
+
42
+ ```markdown
43
+ # Vision brief
44
+
45
+ ## Audience
46
+ - Who: [specific persona]
47
+ - Current approach: [what they do today]
48
+ - Pain: [why it's painful]
49
+
50
+ ## Problem
51
+ [2-3 sentences describing the core problem]
52
+
53
+ ## Desired outcomes
54
+ - [outcome 1]
55
+ - [outcome 2]
56
+ - [outcome 3, if any]
57
+
58
+ ## Scope
59
+ - In scope: [list]
60
+ - Non-goals: [list]
61
+
62
+ ## Constraints
63
+ - [constraint 1]
64
+ - [constraint 2, if any]
65
+
66
+ ## Success measures
67
+ - [measure 1]
68
+ - [acceptance scenario, if provided]
69
+
70
+ ## Assumptions and open decisions
71
+ - [anything the user was unsure about — mark as assumption or needs-decision]
72
+ ```
73
+
74
+ ## Rules
75
+
76
+ - Do not invent product decisions the user didn't make. If something is unclear, ask. If they're unsure, record it as an assumption.
77
+ - Keep the interview conversational — not a form. Follow up on vague answers with a specific probe.
78
+ - If the user gives a very detailed answer up front, skip the questions they already covered and go deeper on the gaps.
79
+ - Do not select library entries or discuss implementation in this interview. That comes later in the synthesis pipeline.
80
+ - After writing the brief, tell the user they can now run `/codecarto-init synthesis` followed by `/codecarto-next` to start the synthesis pipeline.
package/README.md CHANGED
@@ -222,7 +222,7 @@ The default is a 7-phase run that splits the defect scan into a mechanical early
222
222
  | **Architecture only** | 1 | Quick structural overview |
223
223
  | **Synthesis** | 4 | Turn a product vision and confirmed library specifications into a provenance-backed implementation plan |
224
224
 
225
- Set the active pipeline by editing `workflow/status.yaml`'s `pipeline:` field, or pass it as the argument to `/codecarto-init`.
225
+ Switch the active pipeline with `/codecarto-switch-pipeline <variant>` (Pi) or `codecarto_switch_pipeline` (MCP). This rewrites `status.yaml` in-place without deleting findings, handoffs, usage data, or closeouts. Phases that exist in both the old and new pipelines preserve their completion status.
226
226
 
227
227
  **On disk:**
228
228
 
@@ -287,6 +287,8 @@ Beyond the slash commands, the Pi extension layers on:
287
287
  |---|---|
288
288
  | `/codecarto-init [variant]` | Copy `.codecarto/` into the current repository, select pipeline variant |
289
289
  | `/codecarto-open` | Activate an existing `.codecarto/` workspace in a new Pi session without resetting durable state |
290
+ | `/codecarto-vision` | Run a guided product discovery interview to produce `inputs/vision.md` for the synthesis pipeline |
291
+ | `/codecarto-switch-pipeline <variant>` | Switch the active pipeline in-place without losing findings or progress |
290
292
  | `/codecarto-status` | Current phase, progress, open questions |
291
293
  | `/codecarto-next [--auto [--strict]] [--llm-steer \| --no-llm-steer]` | Spawn the next eligible phase as a sub-agent. After the sub-agent finishes, auto-validates and auto-completes the phase so `status.yaml` advances without manual steps. `--auto` walks the full pipeline end-to-end (same validate + complete + advance loop, repeated); `--strict` flips the `PASS WITH GAPS` rule from "advance" to "pause". |
292
294
  | `/codecarto-phase <id>` | Force a specific phase, even out of pipeline order |
@@ -294,6 +296,8 @@ Beyond the slash commands, the Pi extension layers on:
294
296
  | `/codecarto-complete [phase]` | Validate and atomically apply the phase handoff, canonical status, closeout, and log entry |
295
297
  | `/codecarto-skill <name>` | Run a post-pipeline skill once all phases are complete |
296
298
  | `/codecarto-publish` | Publish the reimplementation spec to the configured library after reviewing an explicit confirmation preview |
299
+ | `/codecarto-library-init <path> [--namespace <name>]` | Create a library directory with marker and write the config — fixes the first-publish dead end |
300
+ | `/codecarto-config` | Show the effective merged configuration (global + workspace) and library marker status |
297
301
  | `/codecarto-usage` | Cumulative + per-phase token usage |
298
302
  | `/codecarto-dashboard [--narrate]` | Regenerate `.codecarto/dashboard.html`; `--narrate` for the LLM executive summary |
299
303
 
@@ -78,6 +78,27 @@ export interface LibraryIndex {
78
78
  export declare function discoverLibrary(libraryPath: string): Promise<LibraryMarker | null>;
79
79
  export declare function readMarker(libraryRoot: string): Promise<LibraryMarker | null>;
80
80
  export declare function writeMarker(libraryRoot: string, marker: LibraryMarker): Promise<void>;
81
+ export interface InitLibraryOptions {
82
+ /** Library name (defaults to basename of the path). */
83
+ name?: string;
84
+ /** Visibility level. Default "internal". */
85
+ visibility?: LibraryVisibility;
86
+ /** Whether this is a namespaced (shared) library. Default false. */
87
+ namespaced?: boolean;
88
+ }
89
+ export interface InitLibraryResult {
90
+ libraryPath: string;
91
+ marker: LibraryMarker;
92
+ /** True if the marker already existed (idempotent re-run). */
93
+ alreadyExisted: boolean;
94
+ }
95
+ /**
96
+ * Initialize a CodeCartographer library at the given path: create the
97
+ * directory if needed, write the `.codecarto-library` marker if missing,
98
+ * and return the marker. Idempotent — re-running on an existing library
99
+ * is safe and preserves the existing marker.
100
+ */
101
+ export declare function initLibrary(libraryPath: string, options?: InitLibraryOptions): Promise<InitLibraryResult>;
81
102
  export declare function isValidSlug(slug: string): boolean;
82
103
  /**
83
104
  * Derive a slug from a source repo URL or path. The last meaningful path
@@ -25,7 +25,7 @@
25
25
  import { createHash } from "node:crypto";
26
26
  import { spawn } from "node:child_process";
27
27
  import { mkdir, readFile, readdir, rename, rm, writeFile } from "node:fs/promises";
28
- import { join, resolve } from "node:path";
28
+ import { basename, join, resolve } from "node:path";
29
29
  import { isPlainObject, pathExists } from "./utils.js";
30
30
  import { parseSimpleYaml, stringifySimpleYaml } from "./yaml.js";
31
31
  // ─── Constants ──────────────────────────────────────────────────────────────
@@ -86,6 +86,28 @@ function normalizeMarker(raw) {
86
86
  function isVisibility(v) {
87
87
  return v === "internal" || v === "shared" || v === "public";
88
88
  }
89
+ /**
90
+ * Initialize a CodeCartographer library at the given path: create the
91
+ * directory if needed, write the `.codecarto-library` marker if missing,
92
+ * and return the marker. Idempotent — re-running on an existing library
93
+ * is safe and preserves the existing marker.
94
+ */
95
+ export async function initLibrary(libraryPath, options = {}) {
96
+ const existing = await discoverLibrary(libraryPath);
97
+ if (existing) {
98
+ return { libraryPath, marker: existing, alreadyExisted: true };
99
+ }
100
+ const name = options.name?.trim() || basename(libraryPath);
101
+ const marker = {
102
+ schema_version: MARKER_SCHEMA_VERSION,
103
+ name,
104
+ namespaced: options.namespaced ?? false,
105
+ visibility: options.visibility ?? "internal",
106
+ created_at: new Date().toISOString(),
107
+ };
108
+ await writeMarker(libraryPath, marker);
109
+ return { libraryPath, marker, alreadyExisted: false };
110
+ }
89
111
  // ─── Slug helpers ───────────────────────────────────────────────────────────
90
112
  export function isValidSlug(slug) {
91
113
  if (typeof slug !== "string")
@@ -55,4 +55,10 @@ export declare function loadUserConfig(): Promise<CodecartoConfig>;
55
55
  * a layer in memory (e.g. "what if library_path were X").
56
56
  */
57
57
  export declare function mergeConfig(raw: RawConfig | null | undefined): CodecartoConfig;
58
+ /**
59
+ * Write a `library:` block into a config file (user-global or workspace).
60
+ * Creates the file and parent directories if needed. Preserves any existing
61
+ * `orchestrator:` block. Overwrites the `library:` block if present.
62
+ */
63
+ export declare function writeLibraryConfig(configPath: string, libraryPath: string, namespace?: string | null, publishConfirm?: boolean): Promise<void>;
58
64
  export {};
@@ -15,8 +15,9 @@
15
15
  // don't have to expand themselves.
16
16
  import { homedir } from "node:os";
17
17
  import { join, resolve } from "node:path";
18
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
18
19
  import { expandTilde, pathExists } from "./utils.js";
19
- import { loadYamlFile } from "./yaml.js";
20
+ import { loadYamlFile, parseSimpleYaml, stringifySimpleYaml } from "./yaml.js";
20
21
  export const CONFIG_RELATIVE_PATH = "workflow/config.yaml";
21
22
  export const USER_CONFIG_DIR = join(homedir(), ".codecarto");
22
23
  export const USER_CONFIG_PATH = join(USER_CONFIG_DIR, "config.yaml");
@@ -110,3 +111,27 @@ function cloneDefault() {
110
111
  library: { ...DEFAULT_CONFIG.library },
111
112
  };
112
113
  }
114
+ /**
115
+ * Write a `library:` block into a config file (user-global or workspace).
116
+ * Creates the file and parent directories if needed. Preserves any existing
117
+ * `orchestrator:` block. Overwrites the `library:` block if present.
118
+ */
119
+ export async function writeLibraryConfig(configPath, libraryPath, namespace = null, publishConfirm = true) {
120
+ let existing = {};
121
+ if (await pathExists(configPath)) {
122
+ try {
123
+ const raw = await readFile(configPath, "utf8");
124
+ existing = parseSimpleYaml(raw);
125
+ }
126
+ catch {
127
+ // Malformed file — start fresh
128
+ }
129
+ }
130
+ const library = { path: libraryPath, publish_confirm: publishConfirm };
131
+ if (namespace)
132
+ library.namespace = namespace;
133
+ const updated = { ...existing, library };
134
+ const dir = configPath.includes("/") ? configPath.slice(0, configPath.lastIndexOf("/")) : ".";
135
+ await mkdir(dir, { recursive: true });
136
+ await writeFile(configPath, `${stringifySimpleYaml(updated)}\n`, "utf8");
137
+ }
@@ -78,25 +78,25 @@ export async function runPhasePreflight(state, phase) {
78
78
  if (checks.has("requires-vision-input")) {
79
79
  const visionPath = join(state.workspaceDir, SYNTHESIS_VISION_INPUT_PATH);
80
80
  if (!(await pathExists(visionPath))) {
81
- throw new PhasePreflightError(phase.id, `the vision brief is missing at .codecarto/${SYNTHESIS_VISION_INPUT_PATH}. Create it before starting synthesis.`);
81
+ throw new PhasePreflightError(phase.id, `the vision brief is missing at .codecarto/${SYNTHESIS_VISION_INPUT_PATH}. Create that file and describe your product intent (audience, problem, desired outcome, constraints, non-goals). See .codecarto/templates/vision.md for the expected structure.`);
82
82
  }
83
83
  const rawVision = await readFile(visionPath, "utf8");
84
84
  if (!hasMeaningfulVisionContent(rawVision)) {
85
- throw new PhasePreflightError(phase.id, `the vision brief at .codecarto/${SYNTHESIS_VISION_INPUT_PATH} is still empty. Describe the audience, problem, and desired outcome, then retry.`);
85
+ throw new PhasePreflightError(phase.id, `the vision brief at .codecarto/${SYNTHESIS_VISION_INPUT_PATH} appears to be empty or only contains comments. Write your product intent into that file — at minimum: who the product is for, what problem it solves, and what outcome you want. See .codecarto/templates/vision.md for the full structure.`);
86
86
  }
87
87
  }
88
88
  if (checks.has("requires-library")) {
89
89
  const config = await loadCodecartoConfig(state.workspaceDir);
90
90
  if (!config.library.path) {
91
- throw new PhasePreflightError(phase.id, "no library.path is configured. Set it in ~/.codecarto/config.yaml or .codecarto/workflow/config.yaml.");
91
+ throw new PhasePreflightError(phase.id, "no library.path is configured. Create a library directory with a .codecarto-library marker file, then set library.path in ~/.codecarto/config.yaml or .codecarto/workflow/config.yaml. Example config:\n library:\n path: ~/codecarto-library\n publish_confirm: true");
92
92
  }
93
93
  const marker = await discoverLibrary(config.library.path);
94
94
  if (!marker) {
95
- throw new PhasePreflightError(phase.id, `no CodeCartographer library was found at ${config.library.path} (missing .codecarto-library).`);
95
+ throw new PhasePreflightError(phase.id, `no CodeCartographer library was found at ${config.library.path} (missing .codecarto-library). Create a .codecarto-library marker file in that directory with: {"schema_version": 1, "name": "personal-library", "visibility": "internal", "namespaced": false}`);
96
96
  }
97
97
  const entries = await listEntries(config.library.path);
98
98
  if (entries.length === 0) {
99
- throw new PhasePreflightError(phase.id, `the configured library at ${config.library.path} has no entries. Publish at least one reimplementation spec first.`);
99
+ throw new PhasePreflightError(phase.id, `the configured library at ${config.library.path} has no entries. Run a reverse-engineering pipeline (e.g. /codecarto-init full-with-deep-audit) on a source repository, then use /codecarto-publish to publish at least one reimplementation spec into the library before starting synthesis.`);
100
100
  }
101
101
  result.libraryPath = config.library.path;
102
102
  result.libraryName = marker.name;
@@ -110,13 +110,13 @@ export async function runPhasePreflight(state, phase) {
110
110
  result.confirmedSelections = parseConfirmedProposalSelections(await readFile(proposalPath, "utf8"));
111
111
  result.confirmedEntries = result.confirmedSelections.map((selection) => selection.ref);
112
112
  if (result.confirmedSelections.length === 0) {
113
- throw new PhasePreflightError(phase.id, `no library entries are confirmed in .codecarto/${SYNTHESIS_PROPOSAL_PATH}. Change at least one [ ] checkbox to [x], then retry.`);
113
+ throw new PhasePreflightError(phase.id, `no library entries are confirmed in .codecarto/${SYNTHESIS_PROPOSAL_PATH}. Open that file and change at least one [ ] checkbox to [x] for the entry you want to include, then retry.`);
114
114
  }
115
115
  const available = new Map(result.libraryEntries.map((entry) => [entry.ref, entry]));
116
116
  for (const selection of result.confirmedSelections) {
117
117
  const entry = available.get(selection.ref);
118
118
  if (!entry || !entry.versions.includes(selection.version)) {
119
- throw new PhasePreflightError(phase.id, `confirmed selection ${selection.ref}@v${selection.version} is not present in the configured library. Re-run the proposal phase or correct the checked row.`);
119
+ throw new PhasePreflightError(phase.id, `confirmed selection ${selection.ref}@v${selection.version} is not present in the configured library. Edit .codecarto/${SYNTHESIS_PROPOSAL_PATH} to correct the checked row (change [x] back to [ ] or update the version), or re-run the goal-synthesis-propose phase to regenerate the proposal.`);
120
120
  }
121
121
  selection.specPath = specPathForVersion(result.libraryPath, selection.ref, selection.version);
122
122
  }
@@ -11,3 +11,17 @@ export declare function updateStatusAtomically(cwd: string, updater: (state: Wor
11
11
  handoff?: PhaseHandoff;
12
12
  threadLogEntry?: string;
13
13
  }): Promise<WorkspaceState>;
14
+ /**
15
+ * Switch the active pipeline in-place without deleting findings, handoffs,
16
+ * usage data, closeouts, or checkpoints. Phases that exist in both the old
17
+ * and new pipelines preserve their completion status, owner notes, open
18
+ * questions, and carry-forward entries. Phases unique to the new pipeline
19
+ * start as pending. Phases unique to the old pipeline are dropped from
20
+ * status.yaml (but their findings remain on disk under findings/).
21
+ */
22
+ export declare function switchPipeline(cwd: string, newPipelinePath: string): Promise<{
23
+ state: WorkspaceState;
24
+ carried: string[];
25
+ dropped: string[];
26
+ newPhases: string[];
27
+ }>;
@@ -4,9 +4,9 @@
4
4
  // atomic status-update primitive used by /codecarto-complete.
5
5
  import { existsSync, readFileSync } from "node:fs";
6
6
  import { appendFile, readFile, rename, writeFile } from "node:fs/promises";
7
- import { dirname, join, relative } from "node:path";
7
+ import { basename, dirname, join, relative } from "node:path";
8
8
  import { fileURLToPath } from "node:url";
9
- import { acquireLock, applyHandoff, normalizeStatus, parseHandoff } from "./status.js";
9
+ import { acquireLock, applyHandoff, createEmptyStatus, normalizeStatus, parseHandoff } from "./status.js";
10
10
  import { pathExists } from "./utils.js";
11
11
  import { loadYamlFile, stringifySimpleYaml } from "./yaml.js";
12
12
  // Walk up from the current file to find the package root. Needed because the
@@ -129,3 +129,58 @@ export async function updateStatusAtomically(cwd, updater) {
129
129
  await lock.release();
130
130
  }
131
131
  }
132
+ /**
133
+ * Switch the active pipeline in-place without deleting findings, handoffs,
134
+ * usage data, closeouts, or checkpoints. Phases that exist in both the old
135
+ * and new pipelines preserve their completion status, owner notes, open
136
+ * questions, and carry-forward entries. Phases unique to the new pipeline
137
+ * start as pending. Phases unique to the old pipeline are dropped from
138
+ * status.yaml (but their findings remain on disk under findings/).
139
+ */
140
+ export async function switchPipeline(cwd, newPipelinePath) {
141
+ const workspaceDir = join(cwd, ".codecarto");
142
+ const statusPath = join(workspaceDir, "workflow", "status.yaml");
143
+ const lockPath = `${statusPath}.lock`;
144
+ const lock = await acquireLock(lockPath);
145
+ try {
146
+ const currentState = await getWorkspaceState(cwd);
147
+ if (!currentState) {
148
+ throw new Error("CodeCartographer workspace not found. Run /codecarto-init first.");
149
+ }
150
+ const resolvedPipelinePath = join(workspaceDir, newPipelinePath);
151
+ if (!(await pathExists(resolvedPipelinePath))) {
152
+ throw new Error(`Pipeline not found: ${newPipelinePath}`);
153
+ }
154
+ const newPipeline = await loadYamlFile(resolvedPipelinePath);
155
+ const freshStatus = createEmptyStatus(basename(cwd), newPipelinePath, newPipeline);
156
+ // Preserve phase data for phases that exist in both old and new pipelines.
157
+ const carried = [];
158
+ const oldPhases = currentState.status.phases;
159
+ for (const phaseId of newPipeline.phase_order) {
160
+ if (oldPhases[phaseId]) {
161
+ freshStatus.phases[phaseId] = { ...oldPhases[phaseId] };
162
+ if (oldPhases[phaseId].status === "complete") {
163
+ carried.push(phaseId);
164
+ }
165
+ }
166
+ }
167
+ // Track phases that were in the old pipeline but not the new one.
168
+ const dropped = currentState.pipeline.phase_order.filter((phaseId) => !newPipeline.phase_order.includes(phaseId));
169
+ const newPhases = newPipeline.phase_order.filter((phaseId) => !currentState.pipeline.phase_order.includes(phaseId));
170
+ // Preserve post_pipeline entries from the old status.
171
+ freshStatus.post_pipeline = currentState.status.post_pipeline;
172
+ freshStatus.last_updated = new Date().toISOString();
173
+ assertCanonicalStatus(freshStatus);
174
+ const serialized = `${stringifySimpleYaml(freshStatus)}\n`;
175
+ const tempPath = `${statusPath}.${process.pid}.${Date.now()}.tmp`;
176
+ await writeFile(tempPath, serialized, "utf8");
177
+ await rename(tempPath, statusPath);
178
+ const state = await getWorkspaceState(cwd);
179
+ if (!state)
180
+ throw new Error("Failed to reload workspace state after pipeline switch.");
181
+ return { state, carried, dropped, newPhases };
182
+ }
183
+ finally {
184
+ await lock.release();
185
+ }
186
+ }
@@ -1,4 +1,5 @@
1
- import { cp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
1
+ import { cp, mkdir, readFile, rename, writeFile } from "node:fs/promises";
2
+ import { homedir } from "node:os";
2
3
  import { basename, join, resolve } from "node:path";
3
4
  import { autoCompletePhase, buildAutoSummary, isPhaseRunning, runAuto, runSinglePhase } from "./auto-runner.js";
4
5
  import { disposeAgentsWidget } from "./agent-widget.js";
@@ -7,7 +8,9 @@ import { narrateDashboard } from "./dashboard-narrator.js";
7
8
  import { writeDashboard } from "./dashboard-writer.js";
8
9
  import { parseNextFlags } from "./next-flags.js";
9
10
  import { phaseCompactionExtension } from "./phase-compaction.js";
10
- import { buildPhasePrompt, buildSkillPrompt, buildValidationSummary, canonicalPath, computePerPhaseTotals, computeTotals, createEmptyStatus, DEFAULT_PIPELINE_PATH, deriveSlug, discoverLibrary, getNextEligiblePhase, getPipelineLabel, getWorkspaceState, isWithinPath, listSkillNames, loadCodecartoConfig, loadUsage, loadYamlFile, normalizeForComparison, packagedWorkspaceDir, pathExists, PACKAGE_VERSION, PhasePreflightError, PIPELINE_ALIASES, publishEntry, resolvePhase, resolvePipelineChoice, runPhasePreflight, stringifySimpleYaml, validatePhaseOutput, } from "../../core/index.js";
11
+ import { buildPhasePrompt, buildSkillPrompt, buildValidationSummary, canonicalPath, computePerPhaseTotals, computeTotals, createEmptyStatus, DEFAULT_PIPELINE_PATH, deriveSlug, discoverLibrary, getNextEligiblePhase, getPipelineLabel, getWorkspaceState, isWithinPath, 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
+ import { initLibrary } from "../../core/library.js";
13
+ import { resolveUserConfigPath } from "../../core/orchestrator-config.js";
11
14
  const STATUS_WIDGET_ID = "codecarto-widget";
12
15
  const STATUS_LINE_ID = "codecarto-status";
13
16
  const SAFE_TOOL_NAMES = ["read", "grep", "find", "ls", "edit", "write"];
@@ -195,6 +198,36 @@ export default function codeCartographerExtension(pi) {
195
198
  }
196
199
  },
197
200
  });
201
+ pi.registerCommand("codecarto-vision", {
202
+ description: "Run a guided product discovery interview to produce inputs/vision.md for the synthesis pipeline",
203
+ handler: async (_args, ctx) => {
204
+ const interviewPath = join(ctx.cwd, ".codecarto", "findings", "vision-capture", "INTERVIEW.md");
205
+ if (!(await pathExists(interviewPath))) {
206
+ ctx.ui.notify("Vision interview skill not found. Run /codecarto-init synthesis first.", "warning");
207
+ return;
208
+ }
209
+ const interviewSkill = await readFile(interviewPath, "utf8");
210
+ const inputsDir = join(ctx.cwd, ".codecarto", "inputs");
211
+ const visionPath = join(inputsDir, "vision.md");
212
+ const visionExists = await pathExists(visionPath);
213
+ const prompt = [
214
+ "Read the interview skill below and conduct a guided product discovery interview with the user.",
215
+ "",
216
+ interviewSkill,
217
+ "",
218
+ `The vision brief should be written to .codecarto/inputs/vision.md${visionExists ? " (it already exists — review and improve it based on the interview)" : " (it does not exist yet — create it)"}.`,
219
+ "After the interview, write the synthesized brief and tell the user to run /codecarto-init synthesis followed by /codecarto-next to start the pipeline.",
220
+ ].join("\n");
221
+ if (ctx.isIdle()) {
222
+ pi.sendUserMessage(prompt);
223
+ }
224
+ else {
225
+ pi.sendUserMessage(prompt, { deliverAs: "followUp" });
226
+ }
227
+ lastFeedbackLines = ["Vision interview started — answer the questions in chat."];
228
+ ctx.ui.notify("Vision interview queued — answer the questions in the chat.", "info");
229
+ },
230
+ });
198
231
  pi.registerCommand("codecarto-init", {
199
232
  description: "Initialize .codecarto/ in the current repository",
200
233
  getArgumentCompletions: (prefix) => {
@@ -220,10 +253,13 @@ export default function codeCartographerExtension(pi) {
220
253
  if (targetExists) {
221
254
  const sameWorkspace = normalizeForComparison(await canonicalPath(targetWorkspaceDir)) === normalizeForComparison(await canonicalPath(sourceWorkspaceDir));
222
255
  if (!sameWorkspace) {
223
- const overwrite = await ctx.ui.confirm("CodeCartographer already exists", "A .codecarto/ directory already exists in this repository. Overwrite it?");
256
+ const overwrite = await ctx.ui.confirm("CodeCartographer already exists — data will be lost", "A .codecarto/ directory already exists in this repository. Re-initializing will back up the existing workspace to .codecarto-backup-TIMESTAMP/ and create a fresh one. All phase findings, handoffs, usage data, closeouts, and progress will be moved to the backup. Consider /codecarto-open to reattach without resetting. Continue?");
224
257
  if (!overwrite)
225
258
  return;
226
- await rm(targetWorkspaceDir, { recursive: true, force: true });
259
+ const backupDir = join(ctx.cwd, `.codecarto-backup-${new Date().toISOString().replace(/[:.]/g, "-")}`);
260
+ await rename(targetWorkspaceDir, backupDir);
261
+ if (ctx.hasUI)
262
+ ctx.ui.notify(`Backed up existing workspace to ${basename(backupDir)}/`, "info");
227
263
  }
228
264
  }
229
265
  if (!(await pathExists(targetWorkspaceDir))) {
@@ -265,6 +301,57 @@ export default function codeCartographerExtension(pi) {
265
301
  ctx.ui.notify(`CodeCartographer phase: ${nextPhase}`, "info");
266
302
  },
267
303
  });
304
+ pi.registerCommand("codecarto-switch-pipeline", {
305
+ description: "Switch the active pipeline without losing findings or progress: /codecarto-switch-pipeline <variant>",
306
+ getArgumentCompletions: (prefix) => {
307
+ const items = Object.keys(PIPELINE_ALIASES)
308
+ .filter((value) => value.startsWith(prefix))
309
+ .map((value) => ({ value, label: value }));
310
+ return items.length > 0 ? items : null;
311
+ },
312
+ handler: async (args, ctx) => {
313
+ const trimmedArgs = args.trim();
314
+ if (!trimmedArgs) {
315
+ ctx.ui.notify("Usage: /codecarto-switch-pipeline <variant> (e.g. lite, full, synthesis)", "warning");
316
+ return;
317
+ }
318
+ const pipelineChoice = resolvePipelineChoice(trimmedArgs);
319
+ if (!pipelineChoice) {
320
+ ctx.ui.notify(`Unknown pipeline: ${trimmedArgs}`, "error");
321
+ return;
322
+ }
323
+ const state = await ensureWorkspaceState(ctx);
324
+ if (!state)
325
+ return;
326
+ const currentPipeline = state.status.pipeline;
327
+ if (currentPipeline === pipelineChoice) {
328
+ ctx.ui.notify(`Already on pipeline: ${getPipelineLabel(pipelineChoice)}`, "info");
329
+ return;
330
+ }
331
+ try {
332
+ const result = await switchPipeline(ctx.cwd, pipelineChoice);
333
+ const lines = [
334
+ `Switched pipeline: ${getPipelineLabel(pipelineChoice)}`,
335
+ ];
336
+ if (result.carried.length > 0)
337
+ lines.push(`Phases preserved (completed): ${result.carried.join(", ")}`);
338
+ if (result.newPhases.length > 0)
339
+ lines.push(`New phases: ${result.newPhases.join(", ")}`);
340
+ if (result.dropped.length > 0)
341
+ lines.push(`Phases not in new pipeline: ${result.dropped.join(", ")} (findings remain on disk)`);
342
+ lastFeedbackLines = lines;
343
+ await refreshWorkspaceUi(ctx, lastFeedbackLines);
344
+ ctx.ui.notify(`Switched to pipeline: ${getPipelineLabel(pipelineChoice)}`, "info");
345
+ void writeDashboard(ctx.cwd, PACKAGE_VERSION);
346
+ }
347
+ catch (error) {
348
+ const message = error instanceof Error ? error.message : String(error);
349
+ lastFeedbackLines = [message];
350
+ setUiState(ctx, state, lastFeedbackLines);
351
+ ctx.ui.notify(message, "error");
352
+ }
353
+ },
354
+ });
268
355
  pi.registerCommand("codecarto-next", {
269
356
  description: "Run the next eligible CodeCartographer phase as a sub-agent. Flags: --llm-steer / --no-llm-steer / --auto [--strict]",
270
357
  getArgumentCompletions: (prefix) => {
@@ -538,12 +625,12 @@ export default function codeCartographerExtension(pi) {
538
625
  return;
539
626
  const config = await loadCodecartoConfig(state.workspaceDir);
540
627
  if (!config.library.path) {
541
- ctx.ui.notify("No library.path is configured. Set it in ~/.codecarto/config.yaml or .codecarto/workflow/config.yaml.", "error");
628
+ ctx.ui.notify("No library.path is configured. Create a library directory with a .codecarto-library marker, then set library.path in ~/.codecarto/config.yaml or .codecarto/workflow/config.yaml.", "error");
542
629
  return;
543
630
  }
544
631
  const marker = await discoverLibrary(config.library.path);
545
632
  if (!marker) {
546
- ctx.ui.notify(`No CodeCartographer library at ${config.library.path} (missing .codecarto-library).`, "error");
633
+ ctx.ui.notify(`No CodeCartographer library at ${config.library.path} (missing .codecarto-library). Create a .codecarto-library marker file in that directory.`, "error");
547
634
  return;
548
635
  }
549
636
  const phase = resolvePhase(state, "reimplementation-spec");
@@ -597,6 +684,66 @@ export default function codeCartographerExtension(pi) {
597
684
  }
598
685
  },
599
686
  });
687
+ pi.registerCommand("codecarto-library-init", {
688
+ description: "Initialize a CodeCartographer library and configure it: /codecarto-library-init <path> [--namespace <name>]",
689
+ handler: async (args, ctx) => {
690
+ const parts = args.trim().split(/\s+/);
691
+ const pathArg = parts[0];
692
+ const namespaceIdx = parts.indexOf("--namespace");
693
+ const namespace = namespaceIdx >= 0 ? parts[namespaceIdx + 1] : null;
694
+ if (!pathArg) {
695
+ ctx.ui.notify("Usage: /codecarto-library-init <path> [--namespace <name>]", "warning");
696
+ return;
697
+ }
698
+ const libraryPath = pathArg.startsWith("~") ? join(homedir(), pathArg.slice(1)) : resolve(pathArg);
699
+ try {
700
+ const result = await initLibrary(libraryPath, {
701
+ namespaced: !!namespace,
702
+ ...(namespace ? {} : {}),
703
+ });
704
+ // Write the config to the user-global location
705
+ const configPath = resolveUserConfigPath();
706
+ await writeLibraryConfig(configPath, libraryPath, namespace);
707
+ const msg = result.alreadyExisted
708
+ ? `Library already exists at ${libraryPath} (marker preserved). Config updated.`
709
+ : `Created library at ${libraryPath} with marker "${result.marker.name}".`;
710
+ lastFeedbackLines = [msg, `Config written to ${configPath}`];
711
+ if (ctx.hasUI) {
712
+ ctx.ui.notify(msg, "info");
713
+ ctx.ui.notify(`Config written to ${configPath}`, "info");
714
+ }
715
+ }
716
+ catch (error) {
717
+ const message = error instanceof Error ? error.message : String(error);
718
+ ctx.ui.notify(`Library init failed: ${message}`, "error");
719
+ }
720
+ },
721
+ });
722
+ pi.registerCommand("codecarto-config", {
723
+ description: "Show the effective merged CodeCartographer configuration (global + workspace)",
724
+ handler: async (_args, ctx) => {
725
+ const state = await ensureWorkspaceState(ctx);
726
+ const config = await loadCodecartoConfig(state ? state.workspaceDir : join(ctx.cwd, ".codecarto"));
727
+ const lines = [
728
+ "Effective CodeCartographer configuration:",
729
+ ` library.path: ${config.library.path ?? "(not set)"}`,
730
+ ` library.namespace: ${config.library.namespace ?? "(not set)"}`,
731
+ ` library.publish_confirm: ${config.library.publish_confirm}`,
732
+ ` orchestrator.llm_steer_next_phase: ${config.orchestrator.llm_steer_next_phase}`,
733
+ "",
734
+ ` User-global config: ${resolveUserConfigPath()}`,
735
+ ` Workspace config: ${state ? join(state.workspaceDir, "workflow/config.yaml") : "(no workspace)"}`,
736
+ ];
737
+ if (config.library.path) {
738
+ const marker = await discoverLibrary(config.library.path);
739
+ lines.push(` Library marker: ${marker ? `found ("${marker.name}", namespaced: ${marker.namespaced})` : "MISSING — run /codecarto-library-init"}`);
740
+ }
741
+ lastFeedbackLines = lines;
742
+ if (state)
743
+ setUiState(ctx, state, lastFeedbackLines);
744
+ ctx.ui.notify("Configuration shown in status widget.", "info");
745
+ },
746
+ });
600
747
  pi.registerCommand("codecarto-usage", {
601
748
  description: "Show cumulative + per-phase token usage from local phase runs",
602
749
  handler: async (_args, ctx) => {
@@ -19,6 +19,16 @@ export declare function handleStatus(args: {
19
19
  }>;
20
20
  structuredContent?: Record<string, unknown>;
21
21
  }>;
22
+ export declare function handleSwitchPipeline(args: {
23
+ cwd: string;
24
+ pipeline: string;
25
+ }): Promise<{
26
+ content: Array<{
27
+ type: "text";
28
+ text: string;
29
+ }>;
30
+ structuredContent?: Record<string, unknown>;
31
+ }>;
22
32
  export declare function handleNext(args: {
23
33
  cwd: string;
24
34
  }): Promise<{
@@ -89,6 +99,73 @@ export declare function handleLibraryReindex(args: Record<string, unknown>): Pro
89
99
  }>;
90
100
  structuredContent?: Record<string, unknown>;
91
101
  }>;
102
+ export declare function handleLibraryInit(args: {
103
+ library_path: string;
104
+ name?: string;
105
+ namespace?: string;
106
+ cwd?: string;
107
+ }): Promise<{
108
+ content: Array<{
109
+ type: "text";
110
+ text: string;
111
+ }>;
112
+ structuredContent?: Record<string, unknown>;
113
+ }>;
114
+ export declare function handleVision(args: {
115
+ cwd: string;
116
+ raw_text: string;
117
+ }): Promise<{
118
+ content: Array<{
119
+ type: "text";
120
+ text: string;
121
+ }>;
122
+ structuredContent?: Record<string, unknown>;
123
+ }>;
124
+ export declare function handleConfig(args: {
125
+ cwd?: string;
126
+ }): Promise<{
127
+ content: Array<{
128
+ type: "text";
129
+ text: string;
130
+ }>;
131
+ structuredContent?: Record<string, unknown>;
132
+ }>;
133
+ export declare function handleOpen(args: {
134
+ cwd: string;
135
+ }): Promise<{
136
+ content: Array<{
137
+ type: "text";
138
+ text: string;
139
+ }>;
140
+ structuredContent?: Record<string, unknown>;
141
+ }>;
142
+ export declare function handleUsage(args: {
143
+ cwd: string;
144
+ }): Promise<{
145
+ content: Array<{
146
+ type: "text";
147
+ text: string;
148
+ }>;
149
+ structuredContent?: Record<string, unknown>;
150
+ }>;
151
+ export declare function handleDashboard(args: {
152
+ cwd: string;
153
+ }): Promise<{
154
+ content: Array<{
155
+ type: "text";
156
+ text: string;
157
+ }>;
158
+ structuredContent?: Record<string, unknown>;
159
+ }>;
160
+ export declare function handleListSkills(args: {
161
+ cwd: string;
162
+ }): Promise<{
163
+ content: Array<{
164
+ type: "text";
165
+ text: string;
166
+ }>;
167
+ structuredContent?: Record<string, unknown>;
168
+ }>;
92
169
  export declare function buildServer(): Server<{
93
170
  method: string;
94
171
  params?: {
@@ -13,9 +13,12 @@
13
13
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
14
14
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
15
15
  import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } from "@modelcontextprotocol/sdk/types.js";
16
- import { cp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
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, createEmptyStatus, DEFAULT_PIPELINE_PATH, deriveSlug, discoverLibrary, getNextEligiblePhase, getPipelineLabel, getWorkspaceState, isValidSlug, listEntries, listSkillNames, loadCodecartoConfig, loadYamlFile, normalizeForComparison, PACKAGE_VERSION, packagedWorkspaceDir, pathExists, PhasePreflightError, publishEntry, reindex as libraryReindex, resolvePhase, resolvePipelineChoice, stringifySimpleYaml, validatePhaseOutput, } from "../core/index.js";
18
+ import { buildPhasePrompt, buildSkillPrompt, buildValidationSummary, canonicalPath, completeValidatedPhase, computePerPhaseTotals, computeTotals, createEmptyStatus, DEFAULT_PIPELINE_PATH, deriveSlug, discoverLibrary, getNextEligiblePhase, getPipelineLabel, getWorkspaceState, isValidSlug, 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";
19
+ import { initLibrary } from "../core/library.js";
20
+ import { loadUserConfig, resolveUserConfigPath } from "../core/orchestrator-config.js";
21
+ import { writeDashboard } from "../extensions/codecarto/dashboard-writer.js";
19
22
  // ---------- input helpers ----------
20
23
  async function validateCwd(cwd) {
21
24
  if (typeof cwd !== "string" || !cwd.trim()) {
@@ -77,9 +80,10 @@ export async function handleInit(args) {
77
80
  }
78
81
  if (targetExists && !sameWorkspace) {
79
82
  if (!args.force) {
80
- throw new McpError(ErrorCode.InvalidRequest, `A .codecarto/ directory already exists at ${targetWorkspaceDir}. Pass force: true to overwrite it.`);
83
+ 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.`);
81
84
  }
82
- await rm(targetWorkspaceDir, { recursive: true, force: true });
85
+ const backupDir = join(cwd, `.codecarto-backup-${new Date().toISOString().replace(/[:.]/g, "-")}`);
86
+ await rename(targetWorkspaceDir, backupDir);
83
87
  }
84
88
  if (!(await pathExists(targetWorkspaceDir))) {
85
89
  await mkdir(cwd, { recursive: true });
@@ -137,6 +141,31 @@ export async function handleStatus(args) {
137
141
  nextActions: state.status.next_actions,
138
142
  });
139
143
  }
144
+ export async function handleSwitchPipeline(args) {
145
+ const cwd = await validateCwd(args.cwd);
146
+ const state = await requireWorkspace(cwd);
147
+ const pipelineChoice = resolvePipelineChoice(args.pipeline);
148
+ if (!pipelineChoice) {
149
+ throw new McpError(ErrorCode.InvalidRequest, `Unknown pipeline: ${args.pipeline}`);
150
+ }
151
+ if (state.status.pipeline === pipelineChoice) {
152
+ return textResult(`Already on pipeline: ${getPipelineLabel(pipelineChoice)}`, { pipeline: getPipelineLabel(pipelineChoice) });
153
+ }
154
+ const result = await switchPipeline(cwd, pipelineChoice);
155
+ const lines = [`Switched pipeline: ${getPipelineLabel(pipelineChoice)}`];
156
+ if (result.carried.length > 0)
157
+ lines.push(`Phases preserved (completed): ${result.carried.join(", ")}`);
158
+ if (result.newPhases.length > 0)
159
+ lines.push(`New phases: ${result.newPhases.join(", ")}`);
160
+ if (result.dropped.length > 0)
161
+ lines.push(`Phases not in new pipeline: ${result.dropped.join(", ")} (findings remain on disk)`);
162
+ return textResult(lines.join("\n"), {
163
+ pipeline: getPipelineLabel(pipelineChoice),
164
+ carried: result.carried,
165
+ newPhases: result.newPhases,
166
+ dropped: result.dropped,
167
+ });
168
+ }
140
169
  export async function handleNext(args) {
141
170
  const cwd = await validateCwd(args.cwd);
142
171
  const state = await requireWorkspace(cwd);
@@ -459,11 +488,144 @@ export async function handleLibraryReindex(args) {
459
488
  namespaces: index.namespaces,
460
489
  });
461
490
  }
491
+ export async function handleLibraryInit(args) {
492
+ if (!args.library_path || typeof args.library_path !== "string") {
493
+ throw new McpError(ErrorCode.InvalidParams, "library_path is required.");
494
+ }
495
+ const libraryPath = args.library_path;
496
+ const namespaced = !!args.namespace;
497
+ const result = await initLibrary(libraryPath, {
498
+ name: args.name,
499
+ namespaced,
500
+ });
501
+ // Write config to user-global location
502
+ const configPath = resolveUserConfigPath();
503
+ await writeLibraryConfig(configPath, libraryPath, args.namespace ?? null);
504
+ const msg = result.alreadyExisted
505
+ ? `Library already exists at ${libraryPath} (marker preserved). Config written to ${configPath}.`
506
+ : `Created library at ${libraryPath} with marker "${result.marker.name}". Config written to ${configPath}.`;
507
+ return textResult(msg, {
508
+ libraryPath,
509
+ markerName: result.marker.name,
510
+ namespaced: result.marker.namespaced,
511
+ alreadyExisted: result.alreadyExisted,
512
+ configPath,
513
+ });
514
+ }
515
+ export async function handleVision(args) {
516
+ const cwd = await validateCwd(args.cwd);
517
+ const workspaceDir = join(cwd, ".codecarto");
518
+ const interviewPath = join(workspaceDir, "findings", "vision-capture", "INTERVIEW.md");
519
+ const visionPath = join(workspaceDir, "inputs", "vision.md");
520
+ if (!(await pathExists(interviewPath))) {
521
+ throw new McpError(ErrorCode.InvalidRequest, "Vision interview skill not found. Run codecarto_init with the synthesis pipeline first.");
522
+ }
523
+ const interviewSkill = await readFile(interviewPath, "utf8");
524
+ const prompt = [
525
+ "Read the interview skill below and use it to structure the user's raw product text into a vision brief.",
526
+ "",
527
+ interviewSkill,
528
+ "",
529
+ "The user's raw product text is:",
530
+ "",
531
+ args.raw_text,
532
+ "",
533
+ `Write the synthesized brief to ${visionPath}.`,
534
+ ].join("\n");
535
+ return textResult(prompt, {
536
+ cwd,
537
+ visionPath,
538
+ interviewPath,
539
+ note: "Feed this prompt to your agent. The agent will write the structured vision brief to inputs/vision.md.",
540
+ });
541
+ }
542
+ export async function handleConfig(args) {
543
+ const config = args.cwd
544
+ ? await loadCodecartoConfig(join(args.cwd, ".codecarto"))
545
+ : await loadUserConfig();
546
+ const userConfigPath = resolveUserConfigPath();
547
+ const workspaceConfigPath = args.cwd ? join(args.cwd, ".codecarto", "workflow", "config.yaml") : null;
548
+ let markerStatus = "not configured";
549
+ if (config.library.path) {
550
+ const marker = await discoverLibrary(config.library.path);
551
+ markerStatus = marker ? `found ("${marker.name}", namespaced: ${marker.namespaced})` : "MISSING";
552
+ }
553
+ return textResult([
554
+ "Effective CodeCartographer configuration:",
555
+ ` library.path: ${config.library.path ?? "(not set)"}`,
556
+ ` library.namespace: ${config.library.namespace ?? "(not set)"}`,
557
+ ` library.publish_confirm: ${config.library.publish_confirm}`,
558
+ ` orchestrator.llm_steer_next_phase: ${config.orchestrator.llm_steer_next_phase}`,
559
+ ` Library marker: ${markerStatus}`,
560
+ ` User-global config: ${userConfigPath}`,
561
+ ` Workspace config: ${workspaceConfigPath ?? "(no cwd provided)"}`,
562
+ ].join("\n"), {
563
+ libraryPath: config.library.path,
564
+ libraryNamespace: config.library.namespace,
565
+ publishConfirm: config.library.publish_confirm,
566
+ llmSteerNextPhase: config.orchestrator.llm_steer_next_phase,
567
+ userConfigPath,
568
+ workspaceConfigPath,
569
+ });
570
+ }
571
+ // ---------- MCP parity handlers: open, usage, dashboard, list_skills ----------
572
+ export async function handleOpen(args) {
573
+ const cwd = await validateCwd(args.cwd);
574
+ const workspaceDir = join(cwd, ".codecarto");
575
+ if (!(await pathExists(join(workspaceDir, "workflow", "status.yaml")))) {
576
+ throw new McpError(ErrorCode.InvalidRequest, "No existing CodeCartographer workspace found. Run codecarto_init first.");
577
+ }
578
+ const state = await requireWorkspace(cwd);
579
+ const nextPhase = getNextEligiblePhase(state)?.id ?? "complete";
580
+ return textResult(`Opened existing CodeCartographer workspace: ${getPipelineLabel(state.status.pipeline)}. Current phase: ${nextPhase}.`, { pipeline: getPipelineLabel(state.status.pipeline), currentPhase: nextPhase });
581
+ }
582
+ export async function handleUsage(args) {
583
+ const cwd = await validateCwd(args.cwd);
584
+ const state = await requireWorkspace(cwd);
585
+ const usage = await loadUsage(state.workspaceDir);
586
+ if (usage.runs.length === 0) {
587
+ return textResult("No phase runs recorded yet.", { runs: 0 });
588
+ }
589
+ const totals = computeTotals(usage);
590
+ const perPhase = computePerPhaseTotals(usage);
591
+ const lines = [
592
+ `Total runs: ${totals.runs}`,
593
+ `Total tokens: ${totals.tokens.input} in / ${totals.tokens.output} out / ${totals.tokens.cache_write} cache-write`,
594
+ `Total duration: ${totals.duration_ms}ms / ${totals.tool_uses} tool uses`,
595
+ "",
596
+ "Per-phase totals:",
597
+ ];
598
+ for (const [phaseId, t] of perPhase) {
599
+ lines.push(` ${phaseId}: ${t.runs} run(s), ${t.tokens.input + t.tokens.output} tokens, ${t.tool_uses} tool uses, ${t.duration_ms}ms`);
600
+ }
601
+ return textResult(lines.join("\n"), {
602
+ runs: totals.runs,
603
+ tokens: totals.tokens,
604
+ toolUses: totals.tool_uses,
605
+ durationMs: totals.duration_ms,
606
+ perPhase: Object.fromEntries(perPhase),
607
+ });
608
+ }
609
+ export async function handleDashboard(args) {
610
+ const cwd = await validateCwd(args.cwd);
611
+ await requireWorkspace(cwd);
612
+ await writeDashboard(cwd, PACKAGE_VERSION);
613
+ return textResult("Dashboard regenerated: .codecarto/dashboard.html", { path: ".codecarto/dashboard.html" });
614
+ }
615
+ export async function handleListSkills(args) {
616
+ const cwd = await validateCwd(args.cwd);
617
+ const state = await requireWorkspace(cwd);
618
+ const skills = await listSkillNames(state.workspaceDir);
619
+ const lines = skills.length > 0
620
+ ? [`Available skills (${skills.length}):`, ...skills.map((s) => ` - ${s}`)]
621
+ : ["No skills installed."];
622
+ return textResult(lines.join("\n"), { skills });
623
+ }
462
624
  // ---------- tool registry ----------
463
625
  const TOOLS = [
464
626
  {
465
627
  name: "codecarto_init",
466
- description: "Initialize a CodeCartographer workspace (.codecarto/) in a target repository. Copies the packaged framework template and writes a fresh status.yaml for the chosen pipeline. Errors if .codecarto/ already exists unless force is true.",
628
+ description: "Initialize a CodeCartographer workspace (.codecarto/) in a target repository. Copies the packaged framework template and writes a fresh status.yaml for the chosen pipeline. If .codecarto/ already exists, pass force: true to back up the existing workspace to .codecarto-backup-TIMESTAMP/ and create a fresh one. Warning: backing up moves all existing findings, handoffs, usage data, closeouts, and phase progress to the backup directory.",
467
629
  inputSchema: {
468
630
  type: "object",
469
631
  properties: {
@@ -474,12 +636,27 @@ const TOOLS = [
474
636
  },
475
637
  force: {
476
638
  type: "boolean",
477
- description: "Overwrite an existing .codecarto/ directory if present (default false).",
639
+ description: "Back up and overwrite an existing .codecarto/ directory if present (default false).",
478
640
  },
479
641
  },
480
642
  required: ["cwd"],
481
643
  },
482
644
  },
645
+ {
646
+ name: "codecarto_switch_pipeline",
647
+ description: "Switch the active pipeline in-place without losing findings, handoffs, usage data, or phase progress. Phases that exist in both the old and new pipelines preserve their completion status. Phases unique to the new pipeline start as pending. Pass a pipeline alias (e.g. lite, full, synthesis) or a workflow/*.yaml path.",
648
+ inputSchema: {
649
+ type: "object",
650
+ properties: {
651
+ cwd: { type: "string", description: "Absolute path to the target repository." },
652
+ pipeline: {
653
+ type: "string",
654
+ description: "Pipeline alias (e.g. lite, full, synthesis, architecture-only, defect-scan) or workflow/*.yaml path.",
655
+ },
656
+ },
657
+ required: ["cwd", "pipeline"],
658
+ },
659
+ },
483
660
  {
484
661
  name: "codecarto_status",
485
662
  description: "Show the current CodeCartographer phase, active pipeline, and progress for a target repository.",
@@ -610,10 +787,82 @@ const TOOLS = [
610
787
  },
611
788
  },
612
789
  },
790
+ {
791
+ name: "codecarto_vision",
792
+ description: "Generate a structured vision brief from raw product text using the guided interview skill. Returns a prompt the host should feed to its agent to write inputs/vision.md. Requires a synthesis workspace (run codecarto_init with the synthesis pipeline first).",
793
+ inputSchema: {
794
+ type: "object",
795
+ properties: {
796
+ cwd: { type: "string", description: "Absolute path to the target repository with a .codecarto/ synthesis workspace." },
797
+ raw_text: { type: "string", description: "The user's raw product text — audience, problem, desired outcomes, constraints, non-goals." },
798
+ },
799
+ required: ["cwd", "raw_text"],
800
+ },
801
+ },
802
+ {
803
+ name: "codecarto_library_init",
804
+ description: "Initialize a CodeCartographer library at the given path: create the directory, write the .codecarto-library marker, and write the library.path into the user-global config. Idempotent — safe to re-run on an existing library. Pass a namespace to create a namespaced (shared) library.",
805
+ inputSchema: {
806
+ type: "object",
807
+ properties: {
808
+ library_path: { type: "string", description: "Absolute path for the library directory." },
809
+ name: { type: "string", description: "Library name (defaults to the directory basename)." },
810
+ namespace: { type: "string", description: "Default namespace for a namespaced (shared) library." },
811
+ },
812
+ required: ["library_path"],
813
+ },
814
+ },
815
+ {
816
+ name: "codecarto_config",
817
+ description: "Show the effective merged CodeCartographer configuration (library.path, library.namespace, publish_confirm, llm_steer_next_phase) and whether the library marker was found. Pass cwd to include workspace-level config in the merge.",
818
+ inputSchema: {
819
+ type: "object",
820
+ properties: {
821
+ cwd: { type: "string", description: "Absolute path to a repository with a .codecarto/ workspace (optional)." },
822
+ },
823
+ },
824
+ },
825
+ {
826
+ name: "codecarto_open",
827
+ description: "Activate an existing CodeCartographer workspace without resetting state. Returns the current pipeline and phase.",
828
+ inputSchema: {
829
+ type: "object",
830
+ properties: { cwd: { type: "string", description: "Absolute path to the target repository." } },
831
+ required: ["cwd"],
832
+ },
833
+ },
834
+ {
835
+ name: "codecarto_usage",
836
+ description: "Show cumulative and per-phase token usage from local phase runs.",
837
+ inputSchema: {
838
+ type: "object",
839
+ properties: { cwd: { type: "string", description: "Absolute path to the target repository." } },
840
+ required: ["cwd"],
841
+ },
842
+ },
843
+ {
844
+ name: "codecarto_dashboard",
845
+ description: "Regenerate .codecarto/dashboard.html from the current workspace state.",
846
+ inputSchema: {
847
+ type: "object",
848
+ properties: { cwd: { type: "string", description: "Absolute path to the target repository." } },
849
+ required: ["cwd"],
850
+ },
851
+ },
852
+ {
853
+ name: "codecarto_list_skills",
854
+ description: "List available post-pipeline skills installed in the workspace.",
855
+ inputSchema: {
856
+ type: "object",
857
+ properties: { cwd: { type: "string", description: "Absolute path to the target repository." } },
858
+ required: ["cwd"],
859
+ },
860
+ },
613
861
  ];
614
862
  const HANDLERS = {
615
863
  codecarto_init: handleInit,
616
864
  codecarto_status: handleStatus,
865
+ codecarto_switch_pipeline: handleSwitchPipeline,
617
866
  codecarto_next: handleNext,
618
867
  codecarto_phase: handlePhase,
619
868
  codecarto_validate: handleValidate,
@@ -622,10 +871,17 @@ const HANDLERS = {
622
871
  codecarto_publish: handlePublish,
623
872
  codecarto_library_list: handleLibraryList,
624
873
  codecarto_library_reindex: handleLibraryReindex,
874
+ codecarto_library_init: handleLibraryInit,
875
+ codecarto_config: handleConfig,
876
+ codecarto_vision: handleVision,
877
+ codecarto_open: handleOpen,
878
+ codecarto_usage: handleUsage,
879
+ codecarto_dashboard: handleDashboard,
880
+ codecarto_list_skills: handleListSkills,
625
881
  };
626
882
  // ---------- server bootstrap ----------
627
883
  export function buildServer() {
628
- const server = new Server({ name: "codecartographer", version: "0.2.0" }, { capabilities: { tools: {} } });
884
+ const server = new Server({ name: "codecartographer", version: PACKAGE_VERSION }, { capabilities: { tools: {} } });
629
885
  server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
630
886
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
631
887
  const handler = HANDLERS[request.params.name];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codecartographer-pi",
3
- "version": "0.12.3",
3
+ "version": "0.12.9",
4
4
  "mcpName": "io.github.HuginnIndustries/codecartographer",
5
5
  "description": "Evidence-backed reverse engineering and human-gated software planning for Pi and MCP coding agents.",
6
6
  "type": "module",