codecartographer-pi 0.1.1 → 0.1.3

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/README.md CHANGED
@@ -44,14 +44,18 @@ That's it. The LLM reads the guide, checks `workflow/status.yaml` for progress,
44
44
 
45
45
  This branch also packages CodeCartographer for [Pi](https://github.com/badlogic/pi-mono/tree/main/packages/coding-agent) without changing `.codecarto/` itself. Pi is an **optional peer dependency** — if you only want the template or the MCP server, you don't need Pi installed.
46
46
 
47
- Install from a local checkout or git URL:
47
+ Install from npm, a local checkout, or a git URL:
48
48
 
49
49
  ```bash
50
+ pi install npm:codecartographer-pi
51
+ # or, from a local checkout:
50
52
  pi install /absolute/path/to/CodeCartographer
51
- # or
53
+ # or, from a git URL:
52
54
  pi install git:github.com/your-user/CodeCartographer
53
55
  ```
54
56
 
57
+ > **Don't run `npm install codecartographer-pi` for the Pi use case.** Plain `npm install` puts the package on disk but doesn't register it with Pi, so it never appears in the TUI. Use `pi install npm:codecartographer-pi` instead — Pi handles the npm install internally and writes the package into its own `settings.json` (`~/.pi/agent/settings.json` by default). Plain `npm install` is the right command only for the MCP-server use case described below.
58
+
55
59
  For extension development, you can also point Pi directly at the extension entrypoint or place it in an auto-discovered extensions directory and use `/reload`:
56
60
 
57
61
  ```bash
@@ -73,17 +77,30 @@ If you install the whole repository as a Pi package, Pi may still run package in
73
77
  What the Pi extension adds:
74
78
 
75
79
  - `/codecarto-init` to copy `.codecarto/` into the current repository
76
- - `/codecarto-next` to queue the next eligible phase prompt
80
+ - `/codecarto-next` to queue the next eligible phase prompt (or, in sub-agent mode, spawn the phase as a child session)
77
81
  - `/codecarto-status` to show current phase progress
78
82
  - `/codecarto-validate` and `/codecarto-complete` for validation-gated status updates
79
83
  - a footer/widget showing the active CodeCartographer phase
80
84
  - tool interception that blocks `edit` and `write` outside `.codecarto/`
81
85
  - direct phase prompts that tell Pi exactly which `.codecarto/findings/<phase>/SKILL.md` file to read, without registering those internal files as global Pi skills
82
86
 
87
+ ### Orchestrator / phase sub-agent mode
88
+
89
+ When `/codecarto-init` runs from the Pi extension (0.1.3+), the current Pi session is recorded as the **orchestrator** for that workspace. Each subsequent `/codecarto-next`:
90
+
91
+ - **Run from the orchestrator** — spawns a child Pi session pre-seeded with the phase prompt. The TUI switches to the child; phase tool calls and reasoning land in the child's context window, leaving the orchestrator clean.
92
+ - **Run from inside a phase child** — switches the TUI back to the orchestrator and chains the next phase as another child, atomically.
93
+
94
+ The orchestrator pointer is stored in `.codecarto/workflow/.orchestrator.local.yaml` (gitignored — the file holds an absolute path into the user's Pi session storage, which is machine-local). Workspaces created by 0.1.0 – 0.1.2 don't have this file; the extension falls back to in-place phase prompts (the legacy behavior). Re-run `/codecarto-init` to opt in.
95
+
96
+ The MCP-server path is unaffected — it has no session concept; the host (Claude Desktop / Claude Code / etc.) is the orchestrator.
97
+
83
98
  ## MCP Server
84
99
 
85
100
  The same framework is also packaged as a [Model Context Protocol](https://modelcontextprotocol.io) server, so any MCP-compatible host (Claude Code, Claude Desktop, etc.) can drive a CodeCartographer workflow without the Pi runtime. The server imports the same `core/` primitives the Pi extension uses, so phase prompts and validation are byte-identical across both surfaces.
86
101
 
102
+ Implements MCP spec revision [`2025-11-25`](https://modelcontextprotocol.io/specification/2025-11-25) via `@modelcontextprotocol/sdk` ≥ 1.29.0. The negotiated `protocolVersion` reflects whatever the connecting client requests; the server accepts every revision the SDK supports (currently `2025-11-25`, `2025-06-18`, `2025-03-26`, `2024-11-05`, `2024-10-07`).
103
+
87
104
  Install and wire it up:
88
105
 
89
106
  ```bash
@@ -5,3 +5,4 @@ export * from "./status.ts";
5
5
  export * from "./pipeline.ts";
6
6
  export * from "./prompts.ts";
7
7
  export * from "./workspace.ts";
8
+ export * from "./orchestrator.ts";
@@ -8,3 +8,4 @@ export * from "./status.js";
8
8
  export * from "./pipeline.js";
9
9
  export * from "./prompts.js";
10
10
  export * from "./workspace.js";
11
+ export * from "./orchestrator.js";
@@ -0,0 +1,6 @@
1
+ export interface OrchestratorState {
2
+ sessionFile: string;
3
+ sessionId: string;
4
+ }
5
+ export declare function loadOrchestratorState(cwd: string): Promise<OrchestratorState | null>;
6
+ export declare function writeOrchestratorState(cwd: string, state: OrchestratorState): Promise<void>;
@@ -0,0 +1,36 @@
1
+ // Per-machine pointer to the Pi session that runs as the CodeCartographer
2
+ // orchestrator. Stored in `.codecarto/workflow/.orchestrator.local.yaml`
3
+ // (gitignored) so committed `status.yaml` doesn't leak machine-specific
4
+ // session file paths to collaborators.
5
+ //
6
+ // Only the Pi extension writes/reads this — the MCP path has no session
7
+ // concept. When the file is missing or malformed, `/codecarto-next` falls
8
+ // back to in-place phase prompts (legacy 0.1.0–0.1.2 behavior).
9
+ import { mkdir, writeFile } from "node:fs/promises";
10
+ import { dirname, join } from "node:path";
11
+ import { pathExists } from "./utils.js";
12
+ import { loadYamlFile, stringifySimpleYaml } from "./yaml.js";
13
+ const ORCHESTRATOR_STATE_RELATIVE = "workflow/.orchestrator.local.yaml";
14
+ function orchestratorStatePath(cwd) {
15
+ return join(cwd, ".codecarto", ORCHESTRATOR_STATE_RELATIVE);
16
+ }
17
+ export async function loadOrchestratorState(cwd) {
18
+ const path = orchestratorStatePath(cwd);
19
+ if (!(await pathExists(path)))
20
+ return null;
21
+ let data;
22
+ try {
23
+ data = await loadYamlFile(path);
24
+ }
25
+ catch {
26
+ return null;
27
+ }
28
+ if (!data?.sessionFile || !data?.sessionId)
29
+ return null;
30
+ return { sessionFile: data.sessionFile, sessionId: data.sessionId };
31
+ }
32
+ export async function writeOrchestratorState(cwd, state) {
33
+ const path = orchestratorStatePath(cwd);
34
+ await mkdir(dirname(path), { recursive: true });
35
+ await writeFile(path, `${stringifySimpleYaml(state)}\n`, "utf8");
36
+ }
@@ -2,14 +2,31 @@
2
2
  // (so the MCP server and Pi can both copy from it on /codecarto-init), loads
3
3
  // + normalizes the per-project workspace state from disk, and provides the
4
4
  // atomic status-update primitive used by /codecarto-complete.
5
+ import { existsSync } from "node:fs";
5
6
  import { appendFile, rename, writeFile } from "node:fs/promises";
6
- import { dirname, join, relative, resolve } from "node:path";
7
+ import { dirname, join, relative } from "node:path";
7
8
  import { fileURLToPath } from "node:url";
8
9
  import { acquireLock, normalizeStatus } from "./status.js";
9
10
  import { pathExists } from "./utils.js";
10
11
  import { loadYamlFile, stringifySimpleYaml } from "./yaml.js";
12
+ // Walk up from the current file to find the package root. Needed because the
13
+ // source lives at <root>/core/workspace.ts (one level below the package root)
14
+ // but compiles to <root>/dist/core/workspace.js (two levels below). A fixed
15
+ // `..` only works in one of those layouts, so resolve `package.json` instead.
16
+ function findPackageRoot(start) {
17
+ let dir = start;
18
+ while (true) {
19
+ if (existsSync(join(dir, "package.json")))
20
+ return dir;
21
+ const parent = dirname(dir);
22
+ if (parent === dir) {
23
+ throw new Error(`Could not locate package.json starting from ${start}`);
24
+ }
25
+ dir = parent;
26
+ }
27
+ }
11
28
  const coreDir = dirname(fileURLToPath(import.meta.url));
12
- const packageRoot = resolve(coreDir, "..");
29
+ const packageRoot = findPackageRoot(coreDir);
13
30
  // Path to the packaged framework template directory. Wrappers copy this on
14
31
  // /codecarto-init.
15
32
  export const packagedWorkspaceDir = join(packageRoot, ".codecarto");
@@ -1,2 +1,2 @@
1
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  export default function codeCartographerExtension(pi: ExtensionAPI): void;
@@ -1,6 +1,6 @@
1
1
  import { cp, mkdir, rm, writeFile } from "node:fs/promises";
2
2
  import { basename, join, resolve } from "node:path";
3
- import { buildPhasePrompt, buildSkillPrompt, buildThreadLogEntry, buildValidationSummary, canonicalPath, closeoutFileName, createEmptyStatus, dateOnly, DEFAULT_PIPELINE_PATH, ensureCloseoutStub, getNextEligiblePhase, getPipelineLabel, getWorkspaceState, isWithinPath, listSkillNames, loadYamlFile, normalizeForComparison, normalizeStatus, packagedWorkspaceDir, pathExists, PIPELINE_ALIASES, resolvePhase, resolvePipelineChoice, stringifySimpleYaml, uniqueStrings, updateStatusAtomically, validatePhaseOutput, } from "../../core/index.js";
3
+ import { buildPhasePrompt, buildSkillPrompt, buildThreadLogEntry, buildValidationSummary, canonicalPath, closeoutFileName, createEmptyStatus, dateOnly, DEFAULT_PIPELINE_PATH, ensureCloseoutStub, getNextEligiblePhase, getPipelineLabel, getWorkspaceState, isWithinPath, listSkillNames, loadOrchestratorState, loadYamlFile, normalizeForComparison, normalizeStatus, packagedWorkspaceDir, pathExists, PIPELINE_ALIASES, resolvePhase, resolvePipelineChoice, stringifySimpleYaml, uniqueStrings, updateStatusAtomically, validatePhaseOutput, writeOrchestratorState, } from "../../core/index.js";
4
4
  const STATUS_WIDGET_ID = "codecarto-widget";
5
5
  const STATUS_LINE_ID = "codecarto-status";
6
6
  const SAFE_TOOL_NAMES = ["read", "grep", "find", "ls", "edit", "write"];
@@ -153,6 +153,21 @@ export default function codeCartographerExtension(pi) {
153
153
  normalizedStatus.last_updated = new Date().toISOString();
154
154
  await writeFile(rawStatusPath, `${stringifySimpleYaml(normalizedStatus)}\n`, "utf8");
155
155
  lastFeedbackLines = [`Initialized workspace with pipeline: ${getPipelineLabel(selectedPipelinePath)}`];
156
+ // Claim the current Pi session as the orchestrator for this workspace.
157
+ // /codecarto-next will then spawn each phase as a child session, keeping
158
+ // the orchestrator's context window clean. The pointer is gitignored
159
+ // (workflow/.orchestrator.local.yaml) so it's machine-local. If we have
160
+ // no session file (rare; Pi running headless), skip silently — handlers
161
+ // fall back to in-place phase prompts.
162
+ const orchestratorSessionFile = ctx.sessionManager.getSessionFile();
163
+ const orchestratorSessionId = ctx.sessionManager.getSessionId();
164
+ if (orchestratorSessionFile && orchestratorSessionId) {
165
+ await writeOrchestratorState(ctx.cwd, {
166
+ sessionFile: orchestratorSessionFile,
167
+ sessionId: orchestratorSessionId,
168
+ });
169
+ lastFeedbackLines.push(`Claimed this session as the orchestrator (${orchestratorSessionId.slice(0, 8)}…).`);
170
+ }
156
171
  ctx.ui.notify(`Initialized CodeCartographer (${getPipelineLabel(selectedPipelinePath)})`, "info");
157
172
  await ctx.reload();
158
173
  return;
@@ -184,15 +199,69 @@ export default function codeCartographerExtension(pi) {
184
199
  return;
185
200
  }
186
201
  const prompt = await buildPhasePrompt(state, phase, false);
187
- if (ctx.isIdle()) {
188
- pi.sendUserMessage(prompt);
202
+ const orchestrator = await loadOrchestratorState(ctx.cwd);
203
+ const currentSessionFile = ctx.sessionManager.getSessionFile();
204
+ // Legacy path: workspace has no orchestrator pointer (created by 0.1.2
205
+ // or earlier, or Pi has no session file). Queue the prompt in-place
206
+ // exactly as we did before sub-agent mode existed.
207
+ if (!orchestrator || !currentSessionFile) {
208
+ if (ctx.isIdle())
209
+ pi.sendUserMessage(prompt);
210
+ else
211
+ pi.sendUserMessage(prompt, { deliverAs: "followUp" });
212
+ lastFeedbackLines = [`Queued phase prompt for ${phase.id} (in-place; run /codecarto-init to enable sub-agent mode)`];
213
+ setUiState(ctx, state, lastFeedbackLines);
214
+ ctx.ui.notify(`Queued CodeCartographer phase: ${phase.id}`, "info");
215
+ return;
189
216
  }
190
- else {
191
- pi.sendUserMessage(prompt, { deliverAs: "followUp" });
217
+ // Orchestrator path: we're the parent. Spawn a child session for the
218
+ // phase so its tool calls and reasoning land in an isolated context.
219
+ if (currentSessionFile === orchestrator.sessionFile) {
220
+ const result = await ctx.newSession({
221
+ parentSession: orchestrator.sessionFile,
222
+ withSession: async (childCtx) => {
223
+ childCtx.sendUserMessage(prompt);
224
+ },
225
+ });
226
+ if (result.cancelled) {
227
+ ctx.ui.notify("Sub-agent spawn cancelled.", "warning");
228
+ return;
229
+ }
230
+ lastFeedbackLines = [`Spawned ${phase.id} phase in a sub-agent`];
231
+ setUiState(ctx, state, lastFeedbackLines);
232
+ ctx.ui.notify(`CodeCartographer phase: ${phase.id} (sub-agent)`, "info");
233
+ return;
234
+ }
235
+ // Phase-child path: we're inside a phase sub-agent. Switch the TUI back
236
+ // to the orchestrator and chain the next phase atomically. The fresh
237
+ // state read inside withSession reflects any closeouts the child wrote.
238
+ const switchResult = await ctx.switchSession(orchestrator.sessionFile, {
239
+ withSession: async (orchestratorCtx) => {
240
+ const freshState = await getWorkspaceState(orchestratorCtx.cwd);
241
+ if (!freshState) {
242
+ orchestratorCtx.ui.notify("Workspace state not found after switch.", "error");
243
+ return;
244
+ }
245
+ const nextPhase = getNextEligiblePhase(freshState);
246
+ if (!nextPhase) {
247
+ orchestratorCtx.ui.notify("All CodeCartographer phases are complete.", "info");
248
+ return;
249
+ }
250
+ const nextPrompt = await buildPhasePrompt(freshState, nextPhase, false);
251
+ await orchestratorCtx.newSession({
252
+ parentSession: orchestrator.sessionFile,
253
+ withSession: async (nextChildCtx) => {
254
+ nextChildCtx.sendUserMessage(nextPrompt);
255
+ },
256
+ });
257
+ },
258
+ });
259
+ if (switchResult.cancelled) {
260
+ ctx.ui.notify("Switch back to orchestrator cancelled.", "warning");
261
+ return;
192
262
  }
193
- lastFeedbackLines = [`Queued phase prompt for ${phase.id}`];
263
+ lastFeedbackLines = ["Returned to orchestrator and queued next phase as a sub-agent"];
194
264
  setUiState(ctx, state, lastFeedbackLines);
195
- ctx.ui.notify(`Queued CodeCartographer phase: ${phase.id}`, "info");
196
265
  },
197
266
  });
198
267
  pi.registerCommand("codecarto-phase", {
@@ -367,7 +367,7 @@ const HANDLERS = {
367
367
  };
368
368
  // ---------- server bootstrap ----------
369
369
  export function buildServer() {
370
- const server = new Server({ name: "codecartographer", version: "0.1.1" }, { capabilities: { tools: {} } });
370
+ const server = new Server({ name: "codecartographer", version: "0.1.3" }, { capabilities: { tools: {} } });
371
371
  server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
372
372
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
373
373
  const handler = HANDLERS[request.params.name];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codecartographer-pi",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "CodeCartographer packaged for Pi as an extension-driven workflow wrapper.",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -39,10 +39,10 @@
39
39
  "smoke": "node scripts/smoke-mcp.mjs"
40
40
  },
41
41
  "dependencies": {
42
- "@modelcontextprotocol/sdk": "*"
42
+ "@modelcontextprotocol/sdk": "^1.29.0"
43
43
  },
44
44
  "peerDependencies": {
45
- "@mariozechner/pi-coding-agent": "*",
45
+ "@earendil-works/pi-coding-agent": "^0.74.0",
46
46
  "@sinclair/typebox": "*"
47
47
  },
48
48
  "pi": {