codecartographer-pi 0.1.2 → 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
@@ -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,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.2" }, { 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.2",
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": [