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.
|
package/dist/core/index.d.ts
CHANGED
package/dist/core/index.js
CHANGED
|
@@ -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
|
-
|
|
188
|
-
|
|
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
|
-
|
|
191
|
-
|
|
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 = [
|
|
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.
|
|
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];
|