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.
- package/.codecarto/workflow/config.yaml +13 -0
- package/README.md +21 -8
- package/dist/core/index.d.ts +2 -1
- package/dist/core/index.js +2 -1
- package/dist/core/orchestrator-config.d.ts +20 -0
- package/dist/core/orchestrator-config.js +45 -0
- package/dist/core/usage.d.ts +31 -0
- package/dist/core/usage.js +94 -0
- package/dist/extensions/codecarto/agent-rewriter.d.ts +33 -0
- package/dist/extensions/codecarto/agent-rewriter.js +169 -0
- package/dist/extensions/codecarto/agent-runner.d.ts +44 -0
- package/dist/extensions/codecarto/agent-runner.js +169 -0
- package/dist/extensions/codecarto/agent-state.d.ts +35 -0
- package/dist/extensions/codecarto/agent-state.js +46 -0
- package/dist/extensions/codecarto/agent-summary.d.ts +17 -0
- package/dist/extensions/codecarto/agent-summary.js +79 -0
- package/dist/extensions/codecarto/agent-widget.d.ts +26 -0
- package/dist/extensions/codecarto/agent-widget.js +260 -0
- package/dist/extensions/codecarto/index.js +205 -78
- package/dist/extensions/codecarto/next-flags.d.ts +6 -0
- package/dist/extensions/codecarto/next-flags.js +19 -0
- package/dist/mcp-server/server.js +1 -1
- package/package.json +2 -2
- package/dist/core/orchestrator.d.ts +0 -6
- package/dist/core/orchestrator.js +0 -36
|
@@ -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
|
|
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
|
|
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
|
-
###
|
|
90
|
+
### Phase orchestration (0.2.0 – 0.6.0)
|
|
88
91
|
|
|
89
|
-
|
|
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
|
-
|
|
92
|
-
|
|
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
|
-
|
|
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
|
|
package/dist/core/index.d.ts
CHANGED
package/dist/core/index.js
CHANGED
|
@@ -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>;
|