codecartographer-pi 0.1.2 → 0.1.4

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
@@ -77,13 +77,24 @@ If you install the whole repository as a Pi package, Pi may still run package in
77
77
  What the Pi extension adds:
78
78
 
79
79
  - `/codecarto-init` to copy `.codecarto/` into the current repository
80
- - `/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)
81
81
  - `/codecarto-status` to show current phase progress
82
82
  - `/codecarto-validate` and `/codecarto-complete` for validation-gated status updates
83
83
  - a footer/widget showing the active CodeCartographer phase
84
84
  - tool interception that blocks `edit` and `write` outside `.codecarto/`
85
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
86
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
+
87
98
  ## MCP Server
88
99
 
89
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.
@@ -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
+ }
@@ -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,71 @@ 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
+ //
220
+ // SDK contract: after `ctx.newSession()` resolves, `ctx` is invalidated
221
+ // — touching `ctx.ui` or `ctx.sessionManager` from the outer scope
222
+ // raises "extension ctx is stale after session replacement". So all
223
+ // outer-session work (status line, widget) has to happen BEFORE the
224
+ // call, and any work that needs a fresh ctx happens INSIDE the
225
+ // `withSession` callback against the new session's ctx.
226
+ if (currentSessionFile === orchestrator.sessionFile) {
227
+ lastFeedbackLines = [`Spawned ${phase.id} phase in a sub-agent`];
228
+ setUiState(ctx, state, lastFeedbackLines);
229
+ ctx.ui.notify(`CodeCartographer phase: ${phase.id} (sub-agent)`, "info");
230
+ await ctx.newSession({
231
+ parentSession: orchestrator.sessionFile,
232
+ withSession: async (childCtx) => {
233
+ childCtx.sendUserMessage(prompt);
234
+ },
235
+ });
236
+ return;
192
237
  }
193
- lastFeedbackLines = [`Queued phase prompt for ${phase.id}`];
238
+ // Phase-child path: switch the TUI back to the orchestrator and chain
239
+ // the next phase atomically. Same staleness rule: pre-switch work uses
240
+ // `ctx`; post-switch work uses the fresh `orchestratorCtx` passed to
241
+ // `withSession`. Inside that callback the inner `newSession()` again
242
+ // invalidates `orchestratorCtx`, so the inner spawn must be the last
243
+ // thing the callback does.
244
+ lastFeedbackLines = ["Returning to orchestrator and queuing next phase as a sub-agent"];
194
245
  setUiState(ctx, state, lastFeedbackLines);
195
- ctx.ui.notify(`Queued CodeCartographer phase: ${phase.id}`, "info");
246
+ await ctx.switchSession(orchestrator.sessionFile, {
247
+ withSession: async (orchestratorCtx) => {
248
+ const freshState = await getWorkspaceState(orchestratorCtx.cwd);
249
+ if (!freshState) {
250
+ orchestratorCtx.ui.notify("Workspace state not found after switch.", "error");
251
+ return;
252
+ }
253
+ const nextPhase = getNextEligiblePhase(freshState);
254
+ if (!nextPhase) {
255
+ orchestratorCtx.ui.notify("All CodeCartographer phases are complete.", "info");
256
+ return;
257
+ }
258
+ const nextPrompt = await buildPhasePrompt(freshState, nextPhase, false);
259
+ await orchestratorCtx.newSession({
260
+ parentSession: orchestrator.sessionFile,
261
+ withSession: async (nextChildCtx) => {
262
+ nextChildCtx.sendUserMessage(nextPrompt);
263
+ },
264
+ });
265
+ },
266
+ });
196
267
  },
197
268
  });
198
269
  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.2" }, { capabilities: { tools: {} } });
370
+ const server = new Server({ name: "codecartographer", version: "0.1.4" }, { 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.2",
3
+ "version": "0.1.4",
4
4
  "description": "CodeCartographer packaged for Pi as an extension-driven workflow wrapper.",
5
5
  "type": "module",
6
6
  "keywords": [