codecartographer-pi 0.1.4 → 0.6.1

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.
@@ -0,0 +1,13 @@
1
+ # CodeCartographer workspace-level orchestrator configuration. Optional.
2
+ # Missing keys fall back to defaults defined in core/orchestrator-config.ts.
3
+
4
+ orchestrator:
5
+ # When true, /codecarto-next runs the orchestrator's LLM as a one-shot
6
+ # rewriter: it reads the previous phase's closeout + the next phase's
7
+ # template and produces a customized seed prompt for the sub-agent.
8
+ # Adds orchestrator-side tokens; off by default.
9
+ #
10
+ # Per-invocation overrides:
11
+ # /codecarto-next --llm-steer -> force on for this run
12
+ # /codecarto-next --no-llm-steer -> force off for this run
13
+ llm_steer_next_phase: false
package/README.md CHANGED
@@ -77,23 +77,36 @@ 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 (or, in sub-agent mode, spawn the phase as a child session)
80
+ - `/codecarto-next [--llm-steer | --no-llm-steer]` to spawn the next eligible phase as a sub-agent (the optional flag opts into the LLM-rewriter for the seed prompt; see *Phase orchestration* below)
81
81
  - `/codecarto-status` to show current phase progress
82
82
  - `/codecarto-validate` and `/codecarto-complete` for validation-gated status updates
83
- - a footer/widget showing the active CodeCartographer phase
83
+ - `/codecarto-phase <id>` to force a specific phase even out of pipeline order
84
+ - `/codecarto-skill <name>` to run a post-pipeline skill once all phases are complete
85
+ - `/codecarto-usage` to show cumulative + per-phase token usage from local phase runs (0.6.0+)
86
+ - a footer/widget showing the active CodeCartographer phase, plus a live **Agents** widget above the editor while a phase sub-agent is running
84
87
  - tool interception that blocks `edit` and `write` outside `.codecarto/`
85
88
  - 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
89
 
87
- ### Orchestrator / phase sub-agent mode
90
+ ### Phase orchestration (0.2.0 0.6.0)
88
91
 
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`:
92
+ `/codecarto-next` runs each phase as an isolated `AgentSession` while your TUI stays on the orchestrator session. The phase's tool calls, file reads, and reasoning live in the child's own context window they never accumulate in the orchestrator. A persistent **Agents** widget appears above the editor while a phase is running, showing live tool count, token usage, elapsed time, and the current activity. The widget auto-clears once the phase finishes (and lingers a few seconds after for visibility).
90
93
 
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.
94
+ ```
95
+ CodeCartographer
96
+ └─ ⠹ architecture phase ⟳ 3 · 5 tool uses · 12.3k tokens · 1m32s
97
+ ⎿ reading…
98
+ ```
99
+
100
+ Capabilities layered on top of the parallel-widget runner:
101
+
102
+ - **0.3.0 — file-backed sessions.** Phase sub-agents persist their transcripts to the same `~/.pi/agent/sessions/<encoded-cwd>/` directory the orchestrator uses, so Pi's `/resume`, `/tree`, and `/export` browse them as first-class sessions. The picker shows them with an explicit `CodeCartographer phase: <id>` name and lineage back to the orchestrator's own session.
103
+ - **0.4.0 — phase-completion summary.** When a phase finishes (completed, aborted, or errored), a Markdown closeout block is appended to the orchestrator's transcript via `pi.sendMessage(...)`. You see it in the TUI scrollback; the orchestrator's LLM picks it up as context on your next message. No auto-trigger — control of the next step stays with you.
104
+ - **0.5.0 — opt-in LLM-steered seed prompt.** Set `orchestrator.llm_steer_next_phase: true` in `.codecarto/workflow/config.yaml`, or pass `--llm-steer` per invocation, and the orchestrator's model will run a one-shot rewriter that reads the previous phase's closeout and customizes the next phase's seed prompt to highlight relevant prior findings. Off by default — extra orchestrator-side tokens, opt-in.
105
+ - **0.6.0 — local usage log.** Each phase run is appended to `.codecarto/workflow/.usage.local.yaml` (gitignored). `/codecarto-usage` reports cumulative + per-phase totals. Best-effort logging — write failures don't surface as phase errors.
93
106
 
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.
107
+ Versions 0.1.3 0.1.4 used a different design — a session-switching pattern via `ctx.newSession()` that flipped the TUI to the child. That delivered context isolation but the switch was visually invisible during normal flow, so 0.2.0 replaced it with the parallel-widget approach. 0.1.x workspaces don't need migration; existing `.codecarto/` directories work with 0.6.0 unchanged.
95
108
 
96
- The MCP-server path is unaffected — it has no session concept; the host (Claude Desktop / Claude Code / etc.) is the orchestrator.
109
+ The MCP-server path is unaffected — it has no session concept; the host (Claude Desktop / Claude Code / etc.) is always the orchestrator. `/codecarto-usage` is Pi-only; the MCP path doesn't run sub-agents itself, so there's no per-phase usage to track on that side.
97
110
 
98
111
  ## MCP Server
99
112
 
@@ -5,4 +5,5 @@ 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
+ export * from "./orchestrator-config.ts";
9
+ export * from "./usage.ts";
@@ -8,4 +8,5 @@ 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";
11
+ export * from "./orchestrator-config.js";
12
+ export * from "./usage.js";
@@ -0,0 +1,20 @@
1
+ import type { PathLike } from "node:fs";
2
+ export interface OrchestratorConfig {
3
+ /** When true, /codecarto-next runs an LLM rewriter to produce a seed prompt
4
+ * customized to the previous phase's closeout + the next phase's template
5
+ * before spawning the sub-agent. Off by default — extra orchestrator-side
6
+ * tokens, opt-in. */
7
+ llm_steer_next_phase: boolean;
8
+ }
9
+ export interface CodecartoConfig {
10
+ orchestrator: OrchestratorConfig;
11
+ }
12
+ export declare const CONFIG_RELATIVE_PATH = "workflow/config.yaml";
13
+ type RawConfig = {
14
+ orchestrator?: Partial<{
15
+ llm_steer_next_phase: unknown;
16
+ }>;
17
+ };
18
+ export declare function loadCodecartoConfig(workspaceDir: PathLike): Promise<CodecartoConfig>;
19
+ export declare function mergeConfig(raw: RawConfig | null | undefined): CodecartoConfig;
20
+ export {};
@@ -0,0 +1,45 @@
1
+ // Workspace-level orchestrator configuration. Lives at
2
+ // `.codecarto/workflow/config.yaml`. Missing file or missing keys fall back
3
+ // to defaults, so existing workspaces created before this file existed
4
+ // keep working unchanged. Schema is intentionally narrow — one surface
5
+ // per feature, easy to grow.
6
+ import { join } from "node:path";
7
+ import { pathExists } from "./utils.js";
8
+ import { loadYamlFile } from "./yaml.js";
9
+ export const CONFIG_RELATIVE_PATH = "workflow/config.yaml";
10
+ const DEFAULT_CONFIG = {
11
+ orchestrator: {
12
+ llm_steer_next_phase: false,
13
+ },
14
+ };
15
+ export async function loadCodecartoConfig(workspaceDir) {
16
+ const configPath = join(workspaceDir, CONFIG_RELATIVE_PATH);
17
+ if (!(await pathExists(configPath)))
18
+ return cloneDefault();
19
+ try {
20
+ const raw = await loadYamlFile(configPath);
21
+ return mergeConfig(raw);
22
+ }
23
+ catch {
24
+ // Malformed YAML: fall back to defaults rather than failing the
25
+ // command. The user can fix it; a broken config shouldn't block work.
26
+ return cloneDefault();
27
+ }
28
+ }
29
+ export function mergeConfig(raw) {
30
+ const merged = cloneDefault();
31
+ if (!raw || typeof raw !== "object")
32
+ return merged;
33
+ const o = raw.orchestrator;
34
+ if (o && typeof o === "object") {
35
+ if (typeof o.llm_steer_next_phase === "boolean") {
36
+ merged.orchestrator.llm_steer_next_phase = o.llm_steer_next_phase;
37
+ }
38
+ }
39
+ return merged;
40
+ }
41
+ function cloneDefault() {
42
+ return {
43
+ orchestrator: { ...DEFAULT_CONFIG.orchestrator },
44
+ };
45
+ }
@@ -0,0 +1,31 @@
1
+ export declare const USAGE_RELATIVE_PATH = "workflow/.usage.local.yaml";
2
+ export type UsageRunStatus = "completed" | "aborted" | "error";
3
+ export interface UsageTokens {
4
+ input: number;
5
+ output: number;
6
+ cache_write: number;
7
+ }
8
+ export interface UsageRun {
9
+ timestamp: string;
10
+ phase: string;
11
+ status: UsageRunStatus;
12
+ turn_count: number;
13
+ tool_uses: number;
14
+ duration_ms: number;
15
+ tokens: UsageTokens;
16
+ session_file?: string;
17
+ }
18
+ export interface UsageFile {
19
+ version: number;
20
+ runs: UsageRun[];
21
+ }
22
+ export interface UsageTotals {
23
+ runs: number;
24
+ tokens: UsageTokens;
25
+ tool_uses: number;
26
+ duration_ms: number;
27
+ }
28
+ export declare function loadUsage(workspaceDir: string): Promise<UsageFile>;
29
+ export declare function appendUsageRun(workspaceDir: string, run: UsageRun): Promise<void>;
30
+ export declare function computeTotals(file: UsageFile): UsageTotals;
31
+ export declare function computePerPhaseTotals(file: UsageFile): Map<string, UsageTotals>;
@@ -0,0 +1,94 @@
1
+ // Local-only phase usage log. Lives at
2
+ // `.codecarto/workflow/.usage.local.yaml` (gitignored). Append-only —
3
+ // each finished phase sub-agent contributes one entry. Totals are computed
4
+ // on read so the file never holds a number that contradicts the runs.
5
+ //
6
+ // Concurrency: /codecarto-next rejects re-entry on a phase that's already
7
+ // running, and phases run sequentially against this file, so a plain
8
+ // read-modify-write is safe enough. If parallel-phase dispatch ever ships,
9
+ // switch this to atomic-rename (see core/workspace.ts for the pattern).
10
+ import { readFile, rename, writeFile } from "node:fs/promises";
11
+ import { join } from "node:path";
12
+ import { pathExists } from "./utils.js";
13
+ import { parseSimpleYaml, stringifySimpleYaml } from "./yaml.js";
14
+ export const USAGE_RELATIVE_PATH = "workflow/.usage.local.yaml";
15
+ const SCHEMA_VERSION = 1;
16
+ export async function loadUsage(workspaceDir) {
17
+ const path = join(workspaceDir, USAGE_RELATIVE_PATH);
18
+ if (!(await pathExists(path)))
19
+ return emptyUsage();
20
+ try {
21
+ const raw = await readFile(path, "utf8");
22
+ const parsed = parseSimpleYaml(raw);
23
+ return normalize(parsed);
24
+ }
25
+ catch {
26
+ // Malformed file: treat as empty rather than blocking the user. They
27
+ // can fix or delete the file; corrupt local state shouldn't stop work.
28
+ return emptyUsage();
29
+ }
30
+ }
31
+ export async function appendUsageRun(workspaceDir, run) {
32
+ const current = await loadUsage(workspaceDir);
33
+ current.runs.push(run);
34
+ const path = join(workspaceDir, USAGE_RELATIVE_PATH);
35
+ const tempPath = `${path}.${process.pid}.${Date.now()}.tmp`;
36
+ const serialized = `${stringifySimpleYaml(current)}\n`;
37
+ await writeFile(tempPath, serialized, "utf8");
38
+ await rename(tempPath, path);
39
+ }
40
+ export function computeTotals(file) {
41
+ const totals = {
42
+ runs: file.runs.length,
43
+ tokens: { input: 0, output: 0, cache_write: 0 },
44
+ tool_uses: 0,
45
+ duration_ms: 0,
46
+ };
47
+ for (const r of file.runs) {
48
+ totals.tokens.input += r.tokens?.input ?? 0;
49
+ totals.tokens.output += r.tokens?.output ?? 0;
50
+ totals.tokens.cache_write += r.tokens?.cache_write ?? 0;
51
+ totals.tool_uses += r.tool_uses ?? 0;
52
+ totals.duration_ms += r.duration_ms ?? 0;
53
+ }
54
+ return totals;
55
+ }
56
+ export function computePerPhaseTotals(file) {
57
+ const byPhase = new Map();
58
+ for (const r of file.runs) {
59
+ const t = byPhase.get(r.phase) ?? {
60
+ runs: 0,
61
+ tokens: { input: 0, output: 0, cache_write: 0 },
62
+ tool_uses: 0,
63
+ duration_ms: 0,
64
+ };
65
+ t.runs += 1;
66
+ t.tokens.input += r.tokens?.input ?? 0;
67
+ t.tokens.output += r.tokens?.output ?? 0;
68
+ t.tokens.cache_write += r.tokens?.cache_write ?? 0;
69
+ t.tool_uses += r.tool_uses ?? 0;
70
+ t.duration_ms += r.duration_ms ?? 0;
71
+ byPhase.set(r.phase, t);
72
+ }
73
+ return byPhase;
74
+ }
75
+ function emptyUsage() {
76
+ return { version: SCHEMA_VERSION, runs: [] };
77
+ }
78
+ function normalize(raw) {
79
+ if (!raw || typeof raw !== "object")
80
+ return emptyUsage();
81
+ const runs = Array.isArray(raw.runs) ? raw.runs.filter(isUsageRun) : [];
82
+ return {
83
+ version: typeof raw.version === "number" ? raw.version : SCHEMA_VERSION,
84
+ runs,
85
+ };
86
+ }
87
+ function isUsageRun(x) {
88
+ if (!x || typeof x !== "object")
89
+ return false;
90
+ const r = x;
91
+ return (typeof r.timestamp === "string" &&
92
+ typeof r.phase === "string" &&
93
+ typeof r.status === "string");
94
+ }
@@ -0,0 +1,33 @@
1
+ import { type ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import { type WorkspaceState } from "../../core/index.ts";
3
+ export interface RewritePhasePromptInput {
4
+ ctx: ExtensionContext;
5
+ state: WorkspaceState;
6
+ originalPrompt: string;
7
+ nextPhaseId: string;
8
+ }
9
+ export interface RewritePhasePromptResult {
10
+ prompt: string;
11
+ used: boolean;
12
+ skipReason?: string;
13
+ /** ID of the previous phase whose closeout the rewriter read, when used. */
14
+ prevPhaseId?: string;
15
+ }
16
+ /**
17
+ * Run the rewriter and return the customized seed prompt. On any failure
18
+ * (no prior phase, missing closeout, rewriter session error, empty output)
19
+ * returns the original prompt with `used: false` and a skip reason — never
20
+ * throws. The caller decides what to surface to the user.
21
+ */
22
+ export declare function rewritePhasePrompt(input: RewritePhasePromptInput): Promise<RewritePhasePromptResult>;
23
+ /**
24
+ * Build the markdown block injected into the orchestrator's session
25
+ * (via pi.sendMessage with customType "codecarto-steering") whenever the
26
+ * rewriter produces a customized seed. Lets the user audit what the
27
+ * rewriter chose to emphasize before the phase sub-agent starts.
28
+ */
29
+ export declare function buildSteeringMessage(input: {
30
+ nextPhaseId: string;
31
+ prevPhaseId?: string;
32
+ rewrittenPrompt: string;
33
+ }): string;
@@ -0,0 +1,169 @@
1
+ // Optional LLM-steered prompt rewriter for /codecarto-next.
2
+ //
3
+ // When enabled (workspace config `orchestrator.llm_steer_next_phase: true`,
4
+ // or per-invocation `--llm-steer`), this runs a one-shot in-memory
5
+ // AgentSession on the orchestrator's model with no tools. It receives the
6
+ // stock phase prompt + the previous phase's closeout (if any) and returns
7
+ // a customized seed prompt that acknowledges the prior findings.
8
+ //
9
+ // Off by default. The orchestrator's tokens stay yours unless you opt in.
10
+ import { readFile, readdir } from "node:fs/promises";
11
+ import { join } from "node:path";
12
+ import { createAgentSession, DefaultResourceLoader, getAgentDir, SessionManager, SettingsManager, } from "@earendil-works/pi-coding-agent";
13
+ import { closeoutFileName, pathExists } from "../../core/index.js";
14
+ /** Closeout content over this many bytes is truncated before being passed to
15
+ * the rewriter. Keeps the orchestrator-side cost predictable. */
16
+ const CLOSEOUT_BYTE_BUDGET = 8000;
17
+ /**
18
+ * Run the rewriter and return the customized seed prompt. On any failure
19
+ * (no prior phase, missing closeout, rewriter session error, empty output)
20
+ * returns the original prompt with `used: false` and a skip reason — never
21
+ * throws. The caller decides what to surface to the user.
22
+ */
23
+ export async function rewritePhasePrompt(input) {
24
+ const { ctx, state, originalPrompt, nextPhaseId } = input;
25
+ const prevPhaseId = findPreviousPhaseId(state, nextPhaseId);
26
+ if (!prevPhaseId) {
27
+ return { prompt: originalPrompt, used: false, skipReason: "no previous phase to steer from" };
28
+ }
29
+ const closeout = await readLatestCloseout(state.workspaceDir, prevPhaseId);
30
+ if (!closeout) {
31
+ return { prompt: originalPrompt, used: false, skipReason: `no closeout found for ${prevPhaseId}` };
32
+ }
33
+ const rewriterPrompt = buildRewriterPrompt({
34
+ nextPhaseId,
35
+ prevPhaseId,
36
+ originalPrompt,
37
+ prevCloseout: closeout,
38
+ });
39
+ let customized;
40
+ try {
41
+ customized = await runRewriterOnce(ctx, rewriterPrompt);
42
+ }
43
+ catch (error) {
44
+ const message = error instanceof Error ? error.message : String(error);
45
+ return { prompt: originalPrompt, used: false, skipReason: `rewriter session failed: ${message}` };
46
+ }
47
+ const trimmed = customized.trim();
48
+ if (!trimmed) {
49
+ return { prompt: originalPrompt, used: false, skipReason: "rewriter returned empty output" };
50
+ }
51
+ return { prompt: trimmed, used: true, prevPhaseId };
52
+ }
53
+ /**
54
+ * Build the markdown block injected into the orchestrator's session
55
+ * (via pi.sendMessage with customType "codecarto-steering") whenever the
56
+ * rewriter produces a customized seed. Lets the user audit what the
57
+ * rewriter chose to emphasize before the phase sub-agent starts.
58
+ */
59
+ export function buildSteeringMessage(input) {
60
+ const provenance = input.prevPhaseId
61
+ ? `customized by the orchestrator's LLM from \`${input.prevPhaseId}\`'s closeout`
62
+ : "customized by the orchestrator's LLM";
63
+ return [
64
+ `**Steering: \`${input.nextPhaseId}\` seed prompt**`,
65
+ "",
66
+ `_${provenance}. The phase sub-agent will receive the prompt below as its first user message._`,
67
+ "",
68
+ "---",
69
+ "",
70
+ input.rewrittenPrompt,
71
+ ].join("\n");
72
+ }
73
+ function findPreviousPhaseId(state, nextPhaseId) {
74
+ const order = state.pipeline.phase_order;
75
+ const idx = order.indexOf(nextPhaseId);
76
+ if (idx <= 0)
77
+ return undefined;
78
+ for (let i = idx - 1; i >= 0; i--) {
79
+ if (state.status.phases[order[i]]?.status === "complete")
80
+ return order[i];
81
+ }
82
+ return undefined;
83
+ }
84
+ async function readLatestCloseout(workspaceDir, phaseId) {
85
+ const closeoutsDir = join(workspaceDir, "closeouts");
86
+ if (!(await pathExists(closeoutsDir)))
87
+ return undefined;
88
+ let entries;
89
+ try {
90
+ entries = await readdir(closeoutsDir);
91
+ }
92
+ catch {
93
+ return undefined;
94
+ }
95
+ const suffix = closeoutFileName("", phaseId).replace(/^-/, ""); // "<phaseId>.md"
96
+ const matches = entries.filter((name) => name.endsWith(`-${suffix}`)).sort();
97
+ const latest = matches.at(-1);
98
+ if (!latest)
99
+ return undefined;
100
+ const raw = await readFile(join(closeoutsDir, latest), "utf8");
101
+ if (raw.length <= CLOSEOUT_BYTE_BUDGET)
102
+ return raw;
103
+ return `${raw.slice(0, CLOSEOUT_BYTE_BUDGET)}\n\n[…closeout truncated for rewriter input…]`;
104
+ }
105
+ function buildRewriterPrompt(input) {
106
+ return [
107
+ `You are seeding a sub-agent for the \`${input.nextPhaseId}\` phase of a CodeCartographer pipeline.`,
108
+ `The previous phase \`${input.prevPhaseId}\` just completed. Read its closeout below and produce a CUSTOMIZED seed prompt for the next phase's sub-agent.`,
109
+ "",
110
+ "Constraints for the customized prompt:",
111
+ `- Stay faithful to the original ${input.nextPhaseId} phase template; do not change its structure, required outputs, or completion criteria.`,
112
+ `- Add a short \"context from ${input.prevPhaseId}\" preamble that names the specific findings, open questions, or carry-forward items from the closeout that the ${input.nextPhaseId} phase should pay attention to.`,
113
+ "- Do not invent findings the closeout does not state.",
114
+ "- Do not add commentary directed at the user; the output is the sub-agent's seed prompt.",
115
+ "",
116
+ "Output ONLY the customized seed prompt as plain Markdown. No preface, no fenced code blocks, no commentary.",
117
+ "",
118
+ "=== ORIGINAL NEXT-PHASE PROMPT ===",
119
+ input.originalPrompt,
120
+ "=== END ORIGINAL NEXT-PHASE PROMPT ===",
121
+ "",
122
+ `=== PREVIOUS PHASE (${input.prevPhaseId}) CLOSEOUT ===`,
123
+ input.prevCloseout,
124
+ `=== END PREVIOUS PHASE (${input.prevPhaseId}) CLOSEOUT ===`,
125
+ ].join("\n");
126
+ }
127
+ async function runRewriterOnce(ctx, prompt) {
128
+ const cwd = ctx.cwd;
129
+ const agentDir = getAgentDir();
130
+ const loader = new DefaultResourceLoader({
131
+ cwd,
132
+ agentDir,
133
+ // Even more stripped than the phase runner: no extensions either,
134
+ // since the rewriter has no tools and shouldn't pick up codecarto's
135
+ // tool-interception (it has no tools to intercept).
136
+ noExtensions: true,
137
+ noSkills: true,
138
+ noPromptTemplates: true,
139
+ noThemes: true,
140
+ noContextFiles: true,
141
+ });
142
+ await loader.reload();
143
+ const { session } = await createAgentSession({
144
+ cwd,
145
+ agentDir,
146
+ sessionManager: SessionManager.inMemory(cwd),
147
+ settingsManager: SettingsManager.create(cwd, agentDir),
148
+ modelRegistry: ctx.modelRegistry,
149
+ model: ctx.model,
150
+ tools: [],
151
+ resourceLoader: loader,
152
+ });
153
+ await session.prompt(prompt);
154
+ for (let i = session.messages.length - 1; i >= 0; i--) {
155
+ const msg = session.messages[i];
156
+ if (msg.role !== "assistant")
157
+ continue;
158
+ const blocks = msg.content;
159
+ const parts = [];
160
+ for (const c of blocks) {
161
+ if (c.type === "text" && c.text)
162
+ parts.push(c.text);
163
+ }
164
+ const joined = parts.join("\n").trim();
165
+ if (joined)
166
+ return joined;
167
+ }
168
+ return "";
169
+ }
@@ -0,0 +1,44 @@
1
+ import { type AgentSession, type ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ export interface PhaseRunCallbacks {
3
+ onSessionCreated?: (session: AgentSession) => void;
4
+ onToolStart?: (toolCallId: string, toolName: string) => void;
5
+ onToolEnd?: (toolCallId: string, toolName: string) => void;
6
+ onTextDelta?: (delta: string, fullText: string) => void;
7
+ onTurnEnd?: (turnCount: number) => void;
8
+ onMessageEnd?: (usage: {
9
+ input: number;
10
+ output: number;
11
+ cacheWrite: number;
12
+ }) => void;
13
+ }
14
+ export interface PhaseRunOptions {
15
+ /** Display name written via appendSessionInfo so the session shows up in
16
+ * /resume's picker as e.g. "CodeCartographer phase: blueprint". Pi reads
17
+ * it via SessionManager.getSessionName(). */
18
+ sessionName?: string;
19
+ }
20
+ export interface PhaseRunResult {
21
+ session: AgentSession;
22
+ responseText: string;
23
+ toolUses: number;
24
+ turnCount: number;
25
+ aborted: boolean;
26
+ /** Path to the on-disk session file (under ~/.pi/agent/sessions/<encoded-cwd>/).
27
+ * Stable across the run; useful for /codecarto-usage and any future tooling
28
+ * that wants to point at the phase's transcript. */
29
+ sessionFile: string | undefined;
30
+ }
31
+ /**
32
+ * Run one CodeCartographer phase as an isolated AgentSession. Awaiting this
33
+ * function blocks until the phase completes (or aborts via signal). The
34
+ * orchestrator's TUI stays active throughout — only the phase's own context
35
+ * window holds the tool calls and reasoning.
36
+ *
37
+ * The phase session is **persisted** to the default Pi session directory
38
+ * (`~/.pi/agent/sessions/<encoded-cwd>/`), the same directory the orchestrator
39
+ * uses, so Pi's `/resume`, `/tree`, and `/export` see phase transcripts as
40
+ * first-class sessions. They're tagged via `appendSessionInfo` (display name)
41
+ * and `parentSession` (the orchestrator's session file path) so the picker
42
+ * shows lineage.
43
+ */
44
+ export declare function runPhase(ctx: ExtensionContext, prompt: string, callbacks?: PhaseRunCallbacks, options?: PhaseRunOptions, signal?: AbortSignal): Promise<PhaseRunResult>;