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.
- package/.codecarto/findings/vision-capture/INTERVIEW.md +80 -0
- package/README.md +5 -1
- package/dist/core/library.d.ts +21 -0
- package/dist/core/library.js +23 -1
- package/dist/core/orchestrator-config.d.ts +6 -0
- package/dist/core/orchestrator-config.js +26 -1
- package/dist/core/synthesis.js +7 -7
- package/dist/core/workspace.d.ts +14 -0
- package/dist/core/workspace.js +57 -2
- package/dist/extensions/codecarto/index.js +153 -6
- package/dist/mcp-server/server.d.ts +77 -0
- package/dist/mcp-server/server.js +263 -7
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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
|
|
package/dist/core/library.d.ts
CHANGED
|
@@ -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
|
package/dist/core/library.js
CHANGED
|
@@ -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
|
+
}
|
package/dist/core/synthesis.js
CHANGED
|
@@ -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
|
|
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}
|
|
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.
|
|
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.
|
|
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}.
|
|
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.
|
|
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
|
}
|
package/dist/core/workspace.d.ts
CHANGED
|
@@ -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
|
+
}>;
|
package/dist/core/workspace.js
CHANGED
|
@@ -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,
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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,
|
|
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
|
|
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
|
-
|
|
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.
|
|
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: "
|
|
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:
|
|
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
|
+
"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",
|