codecartographer-pi 0.12.4 → 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
+ }
@@ -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
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) => {
@@ -268,6 +301,57 @@ export default function codeCartographerExtension(pi) {
268
301
  ctx.ui.notify(`CodeCartographer phase: ${nextPhase}`, "info");
269
302
  },
270
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
+ });
271
355
  pi.registerCommand("codecarto-next", {
272
356
  description: "Run the next eligible CodeCartographer phase as a sub-agent. Flags: --llm-steer / --no-llm-steer / --auto [--strict]",
273
357
  getArgumentCompletions: (prefix) => {
@@ -600,6 +684,66 @@ export default function codeCartographerExtension(pi) {
600
684
  }
601
685
  },
602
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
+ });
603
747
  pi.registerCommand("codecarto-usage", {
604
748
  description: "Show cumulative + per-phase token usage from local phase runs",
605
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?: {
@@ -15,7 +15,10 @@ 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, 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()) {
@@ -138,6 +141,31 @@ export async function handleStatus(args) {
138
141
  nextActions: state.status.next_actions,
139
142
  });
140
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
+ }
141
169
  export async function handleNext(args) {
142
170
  const cwd = await validateCwd(args.cwd);
143
171
  const state = await requireWorkspace(cwd);
@@ -460,6 +488,139 @@ export async function handleLibraryReindex(args) {
460
488
  namespaces: index.namespaces,
461
489
  });
462
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
+ }
463
624
  // ---------- tool registry ----------
464
625
  const TOOLS = [
465
626
  {
@@ -481,6 +642,21 @@ const TOOLS = [
481
642
  required: ["cwd"],
482
643
  },
483
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
+ },
484
660
  {
485
661
  name: "codecarto_status",
486
662
  description: "Show the current CodeCartographer phase, active pipeline, and progress for a target repository.",
@@ -611,10 +787,82 @@ const TOOLS = [
611
787
  },
612
788
  },
613
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
+ },
614
861
  ];
615
862
  const HANDLERS = {
616
863
  codecarto_init: handleInit,
617
864
  codecarto_status: handleStatus,
865
+ codecarto_switch_pipeline: handleSwitchPipeline,
618
866
  codecarto_next: handleNext,
619
867
  codecarto_phase: handlePhase,
620
868
  codecarto_validate: handleValidate,
@@ -623,6 +871,13 @@ const HANDLERS = {
623
871
  codecarto_publish: handlePublish,
624
872
  codecarto_library_list: handleLibraryList,
625
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,
626
881
  };
627
882
  // ---------- server bootstrap ----------
628
883
  export function buildServer() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codecartographer-pi",
3
- "version": "0.12.4",
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",