codecartographer-pi 0.10.0 → 0.12.0
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/GUIDE.md +40 -18
- package/.codecarto/README.md +3 -0
- package/.codecarto/findings/goal-synthesis/README.md +3 -0
- package/.codecarto/findings/goal-synthesis-finalize/SKILL.md +30 -0
- package/.codecarto/findings/goal-synthesis-propose/SKILL.md +24 -0
- package/.codecarto/findings/porting/SKILL.md +7 -0
- package/.codecarto/findings/reimplementation-spec/SKILL.md +10 -0
- package/.codecarto/findings/spec-merge/README.md +3 -0
- package/.codecarto/findings/spec-merge/SKILL.md +23 -0
- package/.codecarto/findings/vision-capture/README.md +3 -0
- package/.codecarto/findings/vision-capture/SKILL.md +26 -0
- package/.codecarto/inputs/vision.md +11 -0
- package/.codecarto/templates/architecture-map.md +9 -0
- package/.codecarto/templates/behavioral-contracts.md +9 -0
- package/.codecarto/templates/defect-report.md +9 -0
- package/.codecarto/templates/mechanical-defects.md +9 -0
- package/.codecarto/templates/merged-spec.md +58 -0
- package/.codecarto/templates/phase-checkpoint.md +41 -0
- package/.codecarto/templates/phase-handoff.yaml +22 -0
- package/.codecarto/templates/project-plan.md +70 -0
- package/.codecarto/templates/proposal.md +43 -0
- package/.codecarto/templates/protocols-and-state.md +9 -0
- package/.codecarto/templates/reimplementation-spec-opinionated.md +10 -0
- package/.codecarto/templates/reimplementation-spec.md +10 -0
- package/.codecarto/templates/reverse-engineering-bundle.md +29 -3
- package/.codecarto/templates/semantic-defects.md +9 -0
- package/.codecarto/templates/vision.md +63 -0
- package/.codecarto/workflow/pipeline-architecture-only.yaml +1 -0
- package/.codecarto/workflow/pipeline-defect-scan.yaml +2 -0
- package/.codecarto/workflow/pipeline-full-with-audit.yaml +8 -3
- package/.codecarto/workflow/pipeline-full-with-deep-audit.yaml +9 -5
- package/.codecarto/workflow/pipeline-lite.yaml +3 -0
- package/.codecarto/workflow/pipeline-synthesis.yaml +105 -0
- package/.codecarto/workflow/pipeline.yaml +7 -3
- package/.codecarto/workflow/status.yaml +2 -0
- package/README.md +89 -7
- package/dist/core/completion.d.ts +6 -0
- package/dist/core/completion.js +127 -0
- package/dist/core/dashboard.js +37 -7
- package/dist/core/index.d.ts +2 -0
- package/dist/core/index.js +2 -0
- package/dist/core/pipeline.js +1 -0
- package/dist/core/prompts.d.ts +9 -3
- package/dist/core/prompts.js +43 -26
- package/dist/core/status.d.ts +7 -1
- package/dist/core/status.js +187 -2
- package/dist/core/synthesis.d.ts +31 -0
- package/dist/core/synthesis.js +140 -0
- package/dist/core/types.d.ts +29 -1
- package/dist/core/usage.d.ts +11 -0
- package/dist/core/usage.js +64 -46
- package/dist/core/workspace.d.ts +3 -1
- package/dist/core/workspace.js +39 -3
- package/dist/core/yaml.js +24 -0
- package/dist/extensions/codecarto/agent-rewriter.js +0 -1
- package/dist/extensions/codecarto/agent-runner.d.ts +19 -0
- package/dist/extensions/codecarto/agent-runner.js +68 -6
- package/dist/extensions/codecarto/agent-state.d.ts +3 -0
- package/dist/extensions/codecarto/agent-state.js +2 -0
- package/dist/extensions/codecarto/agent-summary.d.ts +5 -0
- package/dist/extensions/codecarto/agent-summary.js +9 -0
- package/dist/extensions/codecarto/agent-widget.js +6 -0
- package/dist/extensions/codecarto/auto-runner.d.ts +3 -1
- package/dist/extensions/codecarto/auto-runner.js +33 -69
- package/dist/extensions/codecarto/dashboard-narrator.js +0 -1
- package/dist/extensions/codecarto/index.d.ts +1 -1
- package/dist/extensions/codecarto/index.js +153 -12
- package/dist/extensions/codecarto/phase-compaction.d.ts +11 -0
- package/dist/extensions/codecarto/phase-compaction.js +115 -0
- package/dist/mcp-server/server.js +24 -68
- package/package.json +4 -3
package/dist/core/usage.js
CHANGED
|
@@ -13,6 +13,9 @@ import { pathExists } from "./utils.js";
|
|
|
13
13
|
import { parseSimpleYaml, stringifySimpleYaml } from "./yaml.js";
|
|
14
14
|
export const USAGE_RELATIVE_PATH = "workflow/.usage.local.yaml";
|
|
15
15
|
const SCHEMA_VERSION = 1;
|
|
16
|
+
export function emptyCompactionTelemetry() {
|
|
17
|
+
return { successful: 0, failed: 0, aborted: 0, reasons: { threshold: 0, overflow: 0, manual: 0 } };
|
|
18
|
+
}
|
|
16
19
|
export async function loadUsage(workspaceDir) {
|
|
17
20
|
const path = join(workspaceDir, USAGE_RELATIVE_PATH);
|
|
18
21
|
if (!(await pathExists(path)))
|
|
@@ -23,8 +26,6 @@ export async function loadUsage(workspaceDir) {
|
|
|
23
26
|
return normalize(parsed);
|
|
24
27
|
}
|
|
25
28
|
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
29
|
return emptyUsage();
|
|
29
30
|
}
|
|
30
31
|
}
|
|
@@ -33,45 +34,56 @@ export async function appendUsageRun(workspaceDir, run) {
|
|
|
33
34
|
current.runs.push(run);
|
|
34
35
|
const path = join(workspaceDir, USAGE_RELATIVE_PATH);
|
|
35
36
|
const tempPath = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
36
|
-
|
|
37
|
-
await writeFile(tempPath, serialized, "utf8");
|
|
37
|
+
await writeFile(tempPath, `${stringifySimpleYaml(current)}\n`, "utf8");
|
|
38
38
|
await rename(tempPath, path);
|
|
39
39
|
}
|
|
40
40
|
export function computeTotals(file) {
|
|
41
|
-
const totals =
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
}
|
|
41
|
+
const totals = emptyTotals();
|
|
42
|
+
totals.runs = file.runs.length;
|
|
43
|
+
for (const run of file.runs)
|
|
44
|
+
addRun(totals, run);
|
|
54
45
|
return totals;
|
|
55
46
|
}
|
|
56
47
|
export function computePerPhaseTotals(file) {
|
|
57
48
|
const byPhase = new Map();
|
|
58
|
-
for (const
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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);
|
|
49
|
+
for (const run of file.runs) {
|
|
50
|
+
const totals = byPhase.get(run.phase) ?? emptyTotals();
|
|
51
|
+
totals.runs += 1;
|
|
52
|
+
addRun(totals, run);
|
|
53
|
+
byPhase.set(run.phase, totals);
|
|
72
54
|
}
|
|
73
55
|
return byPhase;
|
|
74
56
|
}
|
|
57
|
+
function emptyTotals() {
|
|
58
|
+
return {
|
|
59
|
+
runs: 0,
|
|
60
|
+
compaction_runs: 0,
|
|
61
|
+
tokens: { input: 0, output: 0, cache_write: 0 },
|
|
62
|
+
tool_uses: 0,
|
|
63
|
+
duration_ms: 0,
|
|
64
|
+
compactions: emptyCompactionTelemetry(),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
function addRun(totals, run) {
|
|
68
|
+
totals.tokens.input += run.tokens?.input ?? 0;
|
|
69
|
+
totals.tokens.output += run.tokens?.output ?? 0;
|
|
70
|
+
totals.tokens.cache_write += run.tokens?.cache_write ?? 0;
|
|
71
|
+
totals.tool_uses += run.tool_uses ?? 0;
|
|
72
|
+
totals.duration_ms += run.duration_ms ?? 0;
|
|
73
|
+
if (run.compactions)
|
|
74
|
+
totals.compaction_runs += 1;
|
|
75
|
+
addCompactions(totals.compactions, run.compactions);
|
|
76
|
+
}
|
|
77
|
+
function addCompactions(target, source) {
|
|
78
|
+
if (!source)
|
|
79
|
+
return;
|
|
80
|
+
target.successful += source.successful;
|
|
81
|
+
target.failed += source.failed;
|
|
82
|
+
target.aborted += source.aborted;
|
|
83
|
+
target.reasons.threshold += source.reasons.threshold;
|
|
84
|
+
target.reasons.overflow += source.reasons.overflow;
|
|
85
|
+
target.reasons.manual += source.reasons.manual;
|
|
86
|
+
}
|
|
75
87
|
function emptyUsage() {
|
|
76
88
|
return { version: SCHEMA_VERSION, runs: [] };
|
|
77
89
|
}
|
|
@@ -79,29 +91,35 @@ function normalize(raw) {
|
|
|
79
91
|
if (!raw || typeof raw !== "object")
|
|
80
92
|
return emptyUsage();
|
|
81
93
|
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
|
-
};
|
|
94
|
+
return { version: typeof raw.version === "number" ? raw.version : SCHEMA_VERSION, runs };
|
|
86
95
|
}
|
|
87
96
|
function isUsageRun(x) {
|
|
88
97
|
if (!x || typeof x !== "object")
|
|
89
98
|
return false;
|
|
90
|
-
const
|
|
91
|
-
return (typeof
|
|
92
|
-
typeof
|
|
93
|
-
(
|
|
94
|
-
isFiniteNumber(
|
|
95
|
-
isFiniteNumber(
|
|
96
|
-
isFiniteNumber(
|
|
97
|
-
isUsageTokens(
|
|
98
|
-
(
|
|
99
|
+
const run = x;
|
|
100
|
+
return (typeof run.timestamp === "string" &&
|
|
101
|
+
typeof run.phase === "string" &&
|
|
102
|
+
(run.status === "completed" || run.status === "aborted" || run.status === "error") &&
|
|
103
|
+
isFiniteNumber(run.turn_count) &&
|
|
104
|
+
isFiniteNumber(run.tool_uses) &&
|
|
105
|
+
isFiniteNumber(run.duration_ms) &&
|
|
106
|
+
isUsageTokens(run.tokens) &&
|
|
107
|
+
(run.session_file === undefined || typeof run.session_file === "string") &&
|
|
108
|
+
(run.compactions === undefined || isCompactionTelemetry(run.compactions)));
|
|
99
109
|
}
|
|
100
110
|
function isUsageTokens(x) {
|
|
101
111
|
if (!x || typeof x !== "object")
|
|
102
112
|
return false;
|
|
103
|
-
const
|
|
104
|
-
return isFiniteNumber(
|
|
113
|
+
const tokens = x;
|
|
114
|
+
return isFiniteNumber(tokens.input) && isFiniteNumber(tokens.output) && isFiniteNumber(tokens.cache_write);
|
|
115
|
+
}
|
|
116
|
+
function isCompactionTelemetry(x) {
|
|
117
|
+
if (!x || typeof x !== "object")
|
|
118
|
+
return false;
|
|
119
|
+
const telemetry = x;
|
|
120
|
+
const reasons = telemetry.reasons;
|
|
121
|
+
return isFiniteNumber(telemetry.successful) && isFiniteNumber(telemetry.failed) && isFiniteNumber(telemetry.aborted) &&
|
|
122
|
+
Boolean(reasons) && isFiniteNumber(reasons?.threshold) && isFiniteNumber(reasons?.overflow) && isFiniteNumber(reasons?.manual);
|
|
105
123
|
}
|
|
106
124
|
function isFiniteNumber(x) {
|
|
107
125
|
return typeof x === "number" && Number.isFinite(x);
|
package/dist/core/workspace.d.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
import type { WorkspaceState } from "./types.ts";
|
|
1
|
+
import type { PhaseHandoff, WorkspaceState } from "./types.ts";
|
|
2
2
|
export declare const packagedWorkspaceDir: string;
|
|
3
3
|
export declare const PACKAGE_VERSION: string;
|
|
4
4
|
export declare function getWorkspaceState(cwd: string): Promise<WorkspaceState | null>;
|
|
5
5
|
export declare function updateStatusAtomically(cwd: string, updater: (state: WorkspaceState) => Promise<{
|
|
6
6
|
state: WorkspaceState;
|
|
7
|
+
handoff?: PhaseHandoff;
|
|
7
8
|
threadLogEntry?: string;
|
|
8
9
|
}> | {
|
|
9
10
|
state: WorkspaceState;
|
|
11
|
+
handoff?: PhaseHandoff;
|
|
10
12
|
threadLogEntry?: string;
|
|
11
13
|
}): Promise<WorkspaceState>;
|
package/dist/core/workspace.js
CHANGED
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
// + normalizes the per-project workspace state from disk, and provides the
|
|
4
4
|
// atomic status-update primitive used by /codecarto-complete.
|
|
5
5
|
import { existsSync, readFileSync } from "node:fs";
|
|
6
|
-
import { appendFile, rename, writeFile } from "node:fs/promises";
|
|
6
|
+
import { appendFile, readFile, rename, writeFile } from "node:fs/promises";
|
|
7
7
|
import { dirname, join, relative } from "node:path";
|
|
8
8
|
import { fileURLToPath } from "node:url";
|
|
9
|
-
import { acquireLock, normalizeStatus } from "./status.js";
|
|
9
|
+
import { acquireLock, applyHandoff, normalizeStatus, parseHandoff } from "./status.js";
|
|
10
10
|
import { pathExists } from "./utils.js";
|
|
11
11
|
import { loadYamlFile, stringifySimpleYaml } from "./yaml.js";
|
|
12
12
|
// Walk up from the current file to find the package root. Needed because the
|
|
@@ -42,6 +42,24 @@ export const PACKAGE_VERSION = (() => {
|
|
|
42
42
|
return "0.0.0";
|
|
43
43
|
}
|
|
44
44
|
})();
|
|
45
|
+
function assertCanonicalStatus(status) {
|
|
46
|
+
if (status.schema_version !== 1) {
|
|
47
|
+
throw new Error(`Cannot write unsupported status schema_version ${String(status.schema_version)}.`);
|
|
48
|
+
}
|
|
49
|
+
if (!Array.isArray(status.post_pipeline)) {
|
|
50
|
+
throw new Error("Cannot write status: post_pipeline must be an array.");
|
|
51
|
+
}
|
|
52
|
+
if (!status.phases || typeof status.phases !== "object" || Array.isArray(status.phases)) {
|
|
53
|
+
throw new Error("Cannot write status: phases must be a mapping.");
|
|
54
|
+
}
|
|
55
|
+
for (const [phaseId, phase] of Object.entries(status.phases)) {
|
|
56
|
+
for (const field of ["owner_notes", "outputs_present", "open_questions", "carry_forward"]) {
|
|
57
|
+
if (!Array.isArray(phase?.[field])) {
|
|
58
|
+
throw new Error(`Cannot write status: phases.${phaseId}.${field} must be an array.`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
45
63
|
export async function getWorkspaceState(cwd) {
|
|
46
64
|
const workspaceDir = join(cwd, ".codecarto");
|
|
47
65
|
const statusPath = join(workspaceDir, "workflow", "status.yaml");
|
|
@@ -79,13 +97,31 @@ export async function updateStatusAtomically(cwd, updater) {
|
|
|
79
97
|
}
|
|
80
98
|
const result = await updater(currentState);
|
|
81
99
|
const nextState = result.state;
|
|
100
|
+
// Apply handoff if provided
|
|
101
|
+
if (result.handoff) {
|
|
102
|
+
const handoff = parseHandoff(result.handoff);
|
|
103
|
+
applyHandoff(nextState.status, handoff);
|
|
104
|
+
}
|
|
105
|
+
assertCanonicalStatus(nextState.status);
|
|
82
106
|
const serialized = `${stringifySimpleYaml(nextState.status)}\n`;
|
|
83
107
|
const tempPath = `${statusPath}.${process.pid}.${Date.now()}.tmp`;
|
|
84
108
|
await writeFile(tempPath, serialized, "utf8");
|
|
85
109
|
await rename(tempPath, statusPath);
|
|
86
110
|
if (result.threadLogEntry) {
|
|
87
111
|
const threadLogPath = join(workspaceDir, "THREAD_LOG.md");
|
|
88
|
-
|
|
112
|
+
let currentLog = "";
|
|
113
|
+
try {
|
|
114
|
+
currentLog = await readFile(threadLogPath, "utf8");
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
// File may not exist yet
|
|
118
|
+
}
|
|
119
|
+
const logEntries = currentLog.split(/\r?\n/).filter((line) => line.trim().startsWith("- "));
|
|
120
|
+
const normalizedEntry = result.threadLogEntry.trim();
|
|
121
|
+
const isDuplicate = logEntries.some((line) => line.trim() === normalizedEntry);
|
|
122
|
+
if (!isDuplicate) {
|
|
123
|
+
await appendFile(threadLogPath, result.threadLogEntry, "utf8");
|
|
124
|
+
}
|
|
89
125
|
}
|
|
90
126
|
return nextState;
|
|
91
127
|
}
|
package/dist/core/yaml.js
CHANGED
|
@@ -132,6 +132,30 @@ export function parseSimpleYaml(raw) {
|
|
|
132
132
|
const key = trimmed.slice(0, separator).trim();
|
|
133
133
|
const rawValue = trimmed.slice(separator + 1).trim();
|
|
134
134
|
index++;
|
|
135
|
+
if (key in result) {
|
|
136
|
+
throw new Error(`Duplicate YAML key: ${key} near line: ${line.trim()}`);
|
|
137
|
+
}
|
|
138
|
+
if (rawValue === "|" || rawValue === "|-") {
|
|
139
|
+
const blockLines = [];
|
|
140
|
+
let contentIndent = null;
|
|
141
|
+
while (index < lines.length) {
|
|
142
|
+
const blockLine = lines[index] ?? "";
|
|
143
|
+
if (blockLine.trim() === "") {
|
|
144
|
+
blockLines.push("");
|
|
145
|
+
index++;
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
const blockIndent = countIndent(blockLine);
|
|
149
|
+
if (blockIndent <= indent)
|
|
150
|
+
break;
|
|
151
|
+
contentIndent ??= blockIndent;
|
|
152
|
+
blockLines.push(blockLine.slice(Math.min(contentIndent, blockIndent)));
|
|
153
|
+
index++;
|
|
154
|
+
}
|
|
155
|
+
const content = blockLines.join("\n").replace(/\n+$/, "");
|
|
156
|
+
result[key] = rawValue === "|" ? `${content}\n` : content;
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
135
159
|
if (rawValue !== "") {
|
|
136
160
|
result[key] = parseYamlScalar(rawValue);
|
|
137
161
|
continue;
|
|
@@ -145,7 +145,6 @@ async function runRewriterOnce(ctx, prompt) {
|
|
|
145
145
|
agentDir,
|
|
146
146
|
sessionManager: SessionManager.inMemory(cwd),
|
|
147
147
|
settingsManager: SettingsManager.create(cwd, agentDir),
|
|
148
|
-
modelRegistry: ctx.modelRegistry,
|
|
149
148
|
model: ctx.model,
|
|
150
149
|
tools: [],
|
|
151
150
|
resourceLoader: loader,
|
|
@@ -1,4 +1,15 @@
|
|
|
1
1
|
import { type AgentSession, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
export declare function needsPhaseContinuation(messages: ReadonlyArray<{
|
|
3
|
+
role: string;
|
|
4
|
+
stopReason?: string;
|
|
5
|
+
}>): boolean;
|
|
6
|
+
export declare function shouldContinuePhase(messages: ReadonlyArray<{
|
|
7
|
+
role: string;
|
|
8
|
+
stopReason?: string;
|
|
9
|
+
}>, primaryOutputPresent: boolean): boolean;
|
|
10
|
+
export declare function primaryOutputExists(cwd: string, primaryOutput: string): Promise<boolean>;
|
|
11
|
+
export declare function buildPhaseContinuationPrompt(compacted: boolean): string;
|
|
12
|
+
export declare function waitForCompaction(compactionCompleted: Promise<boolean>, timeoutMs?: number): Promise<boolean>;
|
|
2
13
|
export interface PhaseRunCallbacks {
|
|
3
14
|
onSessionCreated?: (session: AgentSession) => void;
|
|
4
15
|
onToolStart?: (toolCallId: string, toolName: string) => void;
|
|
@@ -10,12 +21,20 @@ export interface PhaseRunCallbacks {
|
|
|
10
21
|
output: number;
|
|
11
22
|
cacheWrite: number;
|
|
12
23
|
}) => void;
|
|
24
|
+
onCompactionEnd?: (event: {
|
|
25
|
+
reason: "manual" | "threshold" | "overflow";
|
|
26
|
+
successful: boolean;
|
|
27
|
+
aborted: boolean;
|
|
28
|
+
}) => void;
|
|
13
29
|
}
|
|
14
30
|
export interface PhaseRunOptions {
|
|
15
31
|
/** Display name written via appendSessionInfo so the session shows up in
|
|
16
32
|
* /resume's picker as e.g. "CodeCartographer phase: blueprint". Pi reads
|
|
17
33
|
* it via SessionManager.getSessionName(). */
|
|
18
34
|
sessionName?: string;
|
|
35
|
+
/** Primary output relative to `.codecarto/`; used to detect provider runs
|
|
36
|
+
* that stop normally before writing their required artifact. */
|
|
37
|
+
primaryOutput?: string;
|
|
19
38
|
}
|
|
20
39
|
export interface PhaseRunResult {
|
|
21
40
|
session: AgentSession;
|
|
@@ -9,11 +9,51 @@
|
|
|
9
9
|
// system prompt, no parent-context inheritance, no turn-limit grace logic.
|
|
10
10
|
// Codecarto phases are bounded by their phase prompt and validation gate;
|
|
11
11
|
// they don't need the full subagent-framework machinery.
|
|
12
|
+
import { access } from "node:fs/promises";
|
|
13
|
+
import { join, resolve } from "node:path";
|
|
12
14
|
import { createAgentSession, DefaultResourceLoader, getAgentDir, SessionManager, SettingsManager, } from "@earendil-works/pi-coding-agent";
|
|
15
|
+
import { canonicalPath, isWithinPath } from "../../core/index.js";
|
|
16
|
+
import { phaseCompactionExtension } from "./phase-compaction.js";
|
|
13
17
|
// Tools available to the phase sub-agent. Matches the codecarto interception
|
|
14
18
|
// allowlist (SAFE_TOOL_NAMES in extensions/codecarto/index.ts), minus bash.
|
|
15
19
|
// Phases analyze source code and write findings; they don't need a shell.
|
|
16
20
|
const PHASE_TOOL_NAMES = ["read", "edit", "write", "grep", "find", "ls"];
|
|
21
|
+
const COMPACTION_SETTLE_TIMEOUT_MS = 30_000;
|
|
22
|
+
export function needsPhaseContinuation(messages) {
|
|
23
|
+
const last = messages.at(-1);
|
|
24
|
+
if (!last)
|
|
25
|
+
return false;
|
|
26
|
+
return last.role === "toolResult" || (last.role === "assistant" && last.stopReason === "toolUse");
|
|
27
|
+
}
|
|
28
|
+
export function shouldContinuePhase(messages, primaryOutputPresent) {
|
|
29
|
+
return !primaryOutputPresent || needsPhaseContinuation(messages);
|
|
30
|
+
}
|
|
31
|
+
export async function primaryOutputExists(cwd, primaryOutput) {
|
|
32
|
+
const workspaceRoot = await canonicalPath(join(cwd, ".codecarto"));
|
|
33
|
+
const candidate = await canonicalPath(resolve(workspaceRoot, primaryOutput));
|
|
34
|
+
if (!isWithinPath(candidate, workspaceRoot))
|
|
35
|
+
return false;
|
|
36
|
+
return access(candidate).then(() => true, () => false);
|
|
37
|
+
}
|
|
38
|
+
export function buildPhaseContinuationPrompt(compacted) {
|
|
39
|
+
const recovery = compacted
|
|
40
|
+
? "Continue the current CodeCartographer phase from the compacted context and durable checkpoint."
|
|
41
|
+
: "The previous phase run stopped before finalizing its required output. Continue from the current session context.";
|
|
42
|
+
return `${recovery} Finish the declared primary output, validation block, status updates, and closeout before ending.`;
|
|
43
|
+
}
|
|
44
|
+
export async function waitForCompaction(compactionCompleted, timeoutMs = COMPACTION_SETTLE_TIMEOUT_MS) {
|
|
45
|
+
let timer;
|
|
46
|
+
try {
|
|
47
|
+
return await Promise.race([
|
|
48
|
+
compactionCompleted,
|
|
49
|
+
new Promise((resolve) => { timer = setTimeout(() => resolve(false), timeoutMs); }),
|
|
50
|
+
]);
|
|
51
|
+
}
|
|
52
|
+
finally {
|
|
53
|
+
if (timer)
|
|
54
|
+
clearTimeout(timer);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
17
57
|
/**
|
|
18
58
|
* Run one CodeCartographer phase as an isolated AgentSession. Awaiting this
|
|
19
59
|
* function blocks until the phase completes (or aborts via signal). The
|
|
@@ -30,17 +70,19 @@ const PHASE_TOOL_NAMES = ["read", "edit", "write", "grep", "find", "ls"];
|
|
|
30
70
|
export async function runPhase(ctx, prompt, callbacks = {}, options = {}, signal) {
|
|
31
71
|
const cwd = ctx.cwd;
|
|
32
72
|
const agentDir = getAgentDir();
|
|
33
|
-
// Resource loader:
|
|
34
|
-
//
|
|
35
|
-
//
|
|
73
|
+
// Resource loader: isolate the child from global extensions/skills and load
|
|
74
|
+
// only CodeCartographer's inline phase guards and compaction hooks. This
|
|
75
|
+
// avoids duplicate registration when CodeCartographer is globally installed
|
|
76
|
+
// while preserving the same safety when it was loaded explicitly with -e.
|
|
36
77
|
const loader = new DefaultResourceLoader({
|
|
37
78
|
cwd,
|
|
38
79
|
agentDir,
|
|
39
|
-
noExtensions:
|
|
40
|
-
noSkills:
|
|
80
|
+
noExtensions: true,
|
|
81
|
+
noSkills: true,
|
|
41
82
|
noPromptTemplates: true,
|
|
42
83
|
noThemes: true,
|
|
43
84
|
noContextFiles: true,
|
|
85
|
+
extensionFactories: [phaseCompactionExtension],
|
|
44
86
|
});
|
|
45
87
|
await loader.reload();
|
|
46
88
|
// File-backed session in the same directory the orchestrator's TUI uses.
|
|
@@ -64,7 +106,6 @@ export async function runPhase(ctx, prompt, callbacks = {}, options = {}, signal
|
|
|
64
106
|
agentDir,
|
|
65
107
|
sessionManager,
|
|
66
108
|
settingsManager: SettingsManager.create(cwd, agentDir),
|
|
67
|
-
modelRegistry: ctx.modelRegistry,
|
|
68
109
|
model: ctx.model,
|
|
69
110
|
tools: PHASE_TOOL_NAMES,
|
|
70
111
|
resourceLoader: loader,
|
|
@@ -75,6 +116,8 @@ export async function runPhase(ctx, prompt, callbacks = {}, options = {}, signal
|
|
|
75
116
|
let turnCount = 0;
|
|
76
117
|
let currentMessageText = "";
|
|
77
118
|
let aborted = false;
|
|
119
|
+
let resolveCompaction;
|
|
120
|
+
const compactionCompleted = new Promise((resolve) => { resolveCompaction = resolve; });
|
|
78
121
|
const unsubscribe = session.subscribe((event) => {
|
|
79
122
|
switch (event.type) {
|
|
80
123
|
case "tool_execution_start": {
|
|
@@ -117,6 +160,16 @@ export async function runPhase(ctx, prompt, callbacks = {}, options = {}, signal
|
|
|
117
160
|
}
|
|
118
161
|
break;
|
|
119
162
|
}
|
|
163
|
+
case "compaction_end": {
|
|
164
|
+
const compactEvent = event;
|
|
165
|
+
callbacks.onCompactionEnd?.({
|
|
166
|
+
reason: compactEvent.reason,
|
|
167
|
+
successful: compactEvent.result !== undefined && compactEvent.result !== null && !compactEvent.aborted && !compactEvent.errorMessage,
|
|
168
|
+
aborted: compactEvent.aborted,
|
|
169
|
+
});
|
|
170
|
+
resolveCompaction?.(true);
|
|
171
|
+
break;
|
|
172
|
+
}
|
|
120
173
|
}
|
|
121
174
|
});
|
|
122
175
|
let abortCleanup = () => { };
|
|
@@ -130,6 +183,15 @@ export async function runPhase(ctx, prompt, callbacks = {}, options = {}, signal
|
|
|
130
183
|
}
|
|
131
184
|
try {
|
|
132
185
|
await session.prompt(prompt);
|
|
186
|
+
let primaryOutputPresent = true;
|
|
187
|
+
if (options.primaryOutput) {
|
|
188
|
+
primaryOutputPresent = await primaryOutputExists(cwd, options.primaryOutput);
|
|
189
|
+
}
|
|
190
|
+
if (!aborted && shouldContinuePhase(session.messages, primaryOutputPresent)) {
|
|
191
|
+
const compacted = await waitForCompaction(compactionCompleted);
|
|
192
|
+
if (!aborted)
|
|
193
|
+
await session.prompt(buildPhaseContinuationPrompt(compacted));
|
|
194
|
+
}
|
|
133
195
|
}
|
|
134
196
|
finally {
|
|
135
197
|
unsubscribe();
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AgentSession } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { type CompactionTelemetry } from "../../core/usage.ts";
|
|
2
3
|
export type PhaseStatus = "running" | "completed" | "error" | "aborted";
|
|
3
4
|
export interface PhaseActivity {
|
|
4
5
|
phaseId: string;
|
|
@@ -17,6 +18,8 @@ export interface PhaseActivity {
|
|
|
17
18
|
output: number;
|
|
18
19
|
cacheWrite: number;
|
|
19
20
|
};
|
|
21
|
+
/** Compaction outcomes observed during this phase session. */
|
|
22
|
+
compactions: CompactionTelemetry;
|
|
20
23
|
session?: AgentSession;
|
|
21
24
|
error?: string;
|
|
22
25
|
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// it from session-event callbacks; the agents widget (M2) reads it on each
|
|
3
3
|
// render. Module-scoped Map so different command handlers can hand work to
|
|
4
4
|
// the runner and the widget sees the same state without explicit plumbing.
|
|
5
|
+
import { emptyCompactionTelemetry } from "../../core/usage.js";
|
|
5
6
|
const phaseActivity = new Map();
|
|
6
7
|
export function getPhaseActivity(phaseId) {
|
|
7
8
|
return phaseActivity.get(phaseId);
|
|
@@ -22,6 +23,7 @@ export function startPhase(phaseId) {
|
|
|
22
23
|
activeTools: new Map(),
|
|
23
24
|
responseText: "",
|
|
24
25
|
lifetimeUsage: { input: 0, output: 0, cacheWrite: 0 },
|
|
26
|
+
compactions: emptyCompactionTelemetry(),
|
|
25
27
|
};
|
|
26
28
|
phaseActivity.set(phaseId, activity);
|
|
27
29
|
return activity;
|
|
@@ -39,6 +39,15 @@ function formatStats(input) {
|
|
|
39
39
|
const totalTokens = input.tokens.input + input.tokens.output;
|
|
40
40
|
if (totalTokens > 0)
|
|
41
41
|
parts.push(formatTokens(totalTokens));
|
|
42
|
+
const compact = input.compactions;
|
|
43
|
+
if (compact && compact.successful + compact.failed + compact.aborted > 0) {
|
|
44
|
+
const details = [`${compact.successful} compaction${compact.successful === 1 ? "" : "s"}`];
|
|
45
|
+
if (compact.failed > 0)
|
|
46
|
+
details.push(`${compact.failed} failed`);
|
|
47
|
+
if (compact.aborted > 0)
|
|
48
|
+
details.push(`${compact.aborted} aborted`);
|
|
49
|
+
parts.push(details.join(", "));
|
|
50
|
+
}
|
|
42
51
|
if (input.durationMs > 0)
|
|
43
52
|
parts.push(formatDuration(input.durationMs));
|
|
44
53
|
return parts.length > 0 ? `_${parts.join(" · ")}_` : "_(no activity recorded)_";
|
|
@@ -186,6 +186,9 @@ function formatRunningStats(a) {
|
|
|
186
186
|
const tokens = a.lifetimeUsage.input + a.lifetimeUsage.output;
|
|
187
187
|
if (tokens > 0)
|
|
188
188
|
parts.push(formatTokens(tokens));
|
|
189
|
+
const compactions = a.compactions.successful + a.compactions.failed + a.compactions.aborted;
|
|
190
|
+
if (compactions > 0)
|
|
191
|
+
parts.push(`${compactions} compact`);
|
|
189
192
|
parts.push(formatDuration(Date.now() - a.startedAt));
|
|
190
193
|
return parts.join(" · ");
|
|
191
194
|
}
|
|
@@ -198,6 +201,9 @@ function formatFinishedStats(a) {
|
|
|
198
201
|
const tokens = a.lifetimeUsage.input + a.lifetimeUsage.output;
|
|
199
202
|
if (tokens > 0)
|
|
200
203
|
parts.push(formatTokens(tokens));
|
|
204
|
+
const compactions = a.compactions.successful + a.compactions.failed + a.compactions.aborted;
|
|
205
|
+
if (compactions > 0)
|
|
206
|
+
parts.push(`${compactions} compact`);
|
|
201
207
|
const dur = a.completedAt ? a.completedAt - a.startedAt : 0;
|
|
202
208
|
if (dur > 0)
|
|
203
209
|
parts.push(formatDuration(dur));
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { type PhaseActivity } from "./agent-state.ts";
|
|
3
|
-
import { type PipelinePhase, type ValidationOverall, type ValidationResult, type WorkspaceState } from "../../core/index.ts";
|
|
3
|
+
import { type PhasePreflightResult, type PipelinePhase, type ValidationOverall, type ValidationResult, type WorkspaceState } from "../../core/index.ts";
|
|
4
4
|
export interface RunSinglePhaseOptions {
|
|
5
5
|
llmSteerEnabled: boolean;
|
|
6
6
|
signal?: AbortSignal;
|
|
7
|
+
/** Preflight validated by the caller so prompt construction does not repeat it. */
|
|
8
|
+
preflight?: PhasePreflightResult;
|
|
7
9
|
/**
|
|
8
10
|
* True when this phase is being driven by `/codecarto-next --auto`. The flag
|
|
9
11
|
* propagates into `buildPhasePrompt` so interactive hooks (notably the
|