codecartographer-pi 0.6.0 → 0.8.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/README.md +254 -208
- package/dist/core/dashboard.d.ts +36 -0
- package/dist/core/dashboard.js +526 -0
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.js +1 -0
- package/dist/core/utils.d.ts +13 -0
- package/dist/core/utils.js +27 -0
- package/dist/core/workspace.d.ts +1 -0
- package/dist/core/workspace.js +13 -1
- package/dist/extensions/codecarto/agent-rewriter.d.ts +13 -0
- package/dist/extensions/codecarto/agent-rewriter.js +21 -1
- package/dist/extensions/codecarto/auto-runner.d.ts +80 -0
- package/dist/extensions/codecarto/auto-runner.js +399 -0
- package/dist/extensions/codecarto/dashboard-flags.d.ts +6 -0
- package/dist/extensions/codecarto/dashboard-flags.js +17 -0
- package/dist/extensions/codecarto/dashboard-narrator.d.ts +8 -0
- package/dist/extensions/codecarto/dashboard-narrator.js +182 -0
- package/dist/extensions/codecarto/dashboard-writer.d.ts +1 -0
- package/dist/extensions/codecarto/dashboard-writer.js +123 -0
- package/dist/extensions/codecarto/index.js +75 -192
- package/dist/extensions/codecarto/next-flags.d.ts +4 -0
- package/dist/extensions/codecarto/next-flags.js +19 -6
- package/package.json +1 -1
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
// Optional LLM-narrated executive summary for the dashboard. Opt-in via
|
|
2
|
+
// /codecarto-dashboard --narrate. Runs the orchestrator's model as a
|
|
3
|
+
// one-shot in-memory AgentSession with no tools, reads recent closeouts +
|
|
4
|
+
// status + usage totals, and produces a 200-400 word Markdown summary.
|
|
5
|
+
//
|
|
6
|
+
// The summary is cached to .codecarto/.dashboard-narration.local.md with a
|
|
7
|
+
// YAML frontmatter recording when it was generated and the completed-phase
|
|
8
|
+
// count at that time. Subsequent deterministic re-renders consult this
|
|
9
|
+
// cache and surface a "<N> runs since" staleness note.
|
|
10
|
+
//
|
|
11
|
+
// Same one-shot pattern as agent-rewriter.ts:runRewriterOnce — different
|
|
12
|
+
// system prompt, different output target. Never throws.
|
|
13
|
+
import { readFile, readdir, rename, writeFile } from "node:fs/promises";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
import { createAgentSession, DefaultResourceLoader, getAgentDir, SessionManager, SettingsManager, } from "@earendil-works/pi-coding-agent";
|
|
16
|
+
import { computeTotals, NARRATION_CACHE_RELATIVE_PATH, loadUsage, pathExists, stringifySimpleYaml, } from "../../core/index.js";
|
|
17
|
+
// Per-closeout byte budget when stuffing the narrator's input. Three
|
|
18
|
+
// closeouts × 4 KB each ≈ 12 KB of prompt context, which is well under any
|
|
19
|
+
// reasonable model's input window.
|
|
20
|
+
const CLOSEOUT_BYTE_BUDGET = 4000;
|
|
21
|
+
const MAX_CLOSEOUTS = 3;
|
|
22
|
+
export async function narrateDashboard(ctx, state) {
|
|
23
|
+
const closeouts = await readRecentCloseouts(state.workspaceDir);
|
|
24
|
+
if (closeouts.length === 0) {
|
|
25
|
+
return { used: false, skipReason: "no closeouts to narrate from" };
|
|
26
|
+
}
|
|
27
|
+
const usage = await loadUsage(state.workspaceDir);
|
|
28
|
+
const prompt = buildNarratorPrompt({
|
|
29
|
+
projectName: state.status.project_name || "(unnamed project)",
|
|
30
|
+
pipeline: state.status.pipeline,
|
|
31
|
+
currentPhase: state.status.current_phase || "—",
|
|
32
|
+
completedCount: completedPhaseCount(state),
|
|
33
|
+
totalPhases: state.pipeline.phase_order.length,
|
|
34
|
+
usageSummary: summarizeUsage(usage),
|
|
35
|
+
closeouts,
|
|
36
|
+
});
|
|
37
|
+
let narration;
|
|
38
|
+
try {
|
|
39
|
+
narration = await runNarratorOnce(ctx, prompt);
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
43
|
+
return { used: false, skipReason: `narrator session failed: ${message}` };
|
|
44
|
+
}
|
|
45
|
+
const trimmed = narration.trim();
|
|
46
|
+
if (!trimmed) {
|
|
47
|
+
return { used: false, skipReason: "narrator returned empty output" };
|
|
48
|
+
}
|
|
49
|
+
await writeNarrationCache(state.workspaceDir, trimmed, completedPhaseCount(state));
|
|
50
|
+
return { narration: trimmed, used: true };
|
|
51
|
+
}
|
|
52
|
+
async function readRecentCloseouts(workspaceDir) {
|
|
53
|
+
const dir = join(workspaceDir, "closeouts");
|
|
54
|
+
if (!(await pathExists(dir)))
|
|
55
|
+
return [];
|
|
56
|
+
let entries;
|
|
57
|
+
try {
|
|
58
|
+
entries = await readdir(dir);
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return [];
|
|
62
|
+
}
|
|
63
|
+
const matched = entries
|
|
64
|
+
.map((name) => {
|
|
65
|
+
const m = /^(\d{4}-\d{2}-\d{2})-(.+)\.md$/.exec(name);
|
|
66
|
+
return m ? { date: m[1], phaseOrModule: m[2], fileName: name } : null;
|
|
67
|
+
})
|
|
68
|
+
.filter((x) => x !== null)
|
|
69
|
+
.sort((a, b) => (a.date < b.date ? 1 : -1))
|
|
70
|
+
.slice(0, MAX_CLOSEOUTS);
|
|
71
|
+
const out = [];
|
|
72
|
+
for (const entry of matched) {
|
|
73
|
+
try {
|
|
74
|
+
const raw = await readFile(join(dir, entry.fileName), "utf8");
|
|
75
|
+
const truncated = raw.length > CLOSEOUT_BYTE_BUDGET
|
|
76
|
+
? `${raw.slice(0, CLOSEOUT_BYTE_BUDGET)}\n\n[…truncated for narration budget…]`
|
|
77
|
+
: raw;
|
|
78
|
+
out.push({ ...entry, content: truncated });
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
// skip unreadable closeouts
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return out;
|
|
85
|
+
}
|
|
86
|
+
function buildNarratorPrompt(input) {
|
|
87
|
+
const closeoutBlocks = input.closeouts.map((c) => [
|
|
88
|
+
`=== CLOSEOUT ${c.date} ${c.phaseOrModule} ===`,
|
|
89
|
+
c.content,
|
|
90
|
+
`=== END CLOSEOUT ${c.date} ${c.phaseOrModule} ===`,
|
|
91
|
+
].join("\n")).join("\n\n");
|
|
92
|
+
return [
|
|
93
|
+
`You are writing a 200-400 word executive summary of a CodeCartographer run for a human reader (project owner, reviewer, or new team member).`,
|
|
94
|
+
"",
|
|
95
|
+
"Constraints:",
|
|
96
|
+
"- Cite specific findings from the closeouts below; quote phase IDs and artifact names verbatim.",
|
|
97
|
+
"- Do not invent findings the closeouts do not state.",
|
|
98
|
+
"- Lead with what the run discovered, not what it did mechanically. Findings > activity.",
|
|
99
|
+
"- Surface any cross-cutting risks or open questions worth pulling forward.",
|
|
100
|
+
"- Output Markdown only, no commentary, no preface, no fenced code blocks.",
|
|
101
|
+
"- 200-400 words; tight prose, no bullet-list-soup.",
|
|
102
|
+
"",
|
|
103
|
+
"=== RUN METADATA ===",
|
|
104
|
+
`Project: ${input.projectName}`,
|
|
105
|
+
`Pipeline: ${input.pipeline}`,
|
|
106
|
+
`Progress: ${input.completedCount}/${input.totalPhases} phases complete`,
|
|
107
|
+
`Current phase: ${input.currentPhase}`,
|
|
108
|
+
`Usage: ${input.usageSummary}`,
|
|
109
|
+
"=== END RUN METADATA ===",
|
|
110
|
+
"",
|
|
111
|
+
closeoutBlocks,
|
|
112
|
+
].join("\n");
|
|
113
|
+
}
|
|
114
|
+
function summarizeUsage(usage) {
|
|
115
|
+
if (usage.runs.length === 0)
|
|
116
|
+
return "no runs recorded";
|
|
117
|
+
const totals = computeTotals(usage);
|
|
118
|
+
const tokensTotal = totals.tokens.input + totals.tokens.output;
|
|
119
|
+
return `${totals.runs} runs · ${formatK(tokensTotal)} tokens · ${totals.tool_uses} tool uses`;
|
|
120
|
+
}
|
|
121
|
+
function formatK(n) {
|
|
122
|
+
if (n >= 1_000_000)
|
|
123
|
+
return `${(n / 1_000_000).toFixed(2)}M`;
|
|
124
|
+
if (n >= 1_000)
|
|
125
|
+
return `${(n / 1_000).toFixed(1)}k`;
|
|
126
|
+
return `${n}`;
|
|
127
|
+
}
|
|
128
|
+
function completedPhaseCount(state) {
|
|
129
|
+
return Object.values(state.status.phases).filter((p) => p.status === "complete").length;
|
|
130
|
+
}
|
|
131
|
+
async function writeNarrationCache(workspaceDir, content, phaseCount) {
|
|
132
|
+
const generatedAt = new Date().toISOString();
|
|
133
|
+
const frontmatter = stringifySimpleYaml({ generatedAt, phaseCountAtGeneration: phaseCount });
|
|
134
|
+
const body = `---\n${frontmatter}\n---\n${content}\n`;
|
|
135
|
+
const path = join(workspaceDir, NARRATION_CACHE_RELATIVE_PATH);
|
|
136
|
+
const tempPath = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
137
|
+
await writeFile(tempPath, body, "utf8");
|
|
138
|
+
await rename(tempPath, path);
|
|
139
|
+
}
|
|
140
|
+
async function runNarratorOnce(ctx, prompt) {
|
|
141
|
+
const cwd = ctx.cwd;
|
|
142
|
+
const agentDir = getAgentDir();
|
|
143
|
+
const loader = new DefaultResourceLoader({
|
|
144
|
+
cwd,
|
|
145
|
+
agentDir,
|
|
146
|
+
noExtensions: true,
|
|
147
|
+
noSkills: true,
|
|
148
|
+
noPromptTemplates: true,
|
|
149
|
+
noThemes: true,
|
|
150
|
+
noContextFiles: true,
|
|
151
|
+
});
|
|
152
|
+
await loader.reload();
|
|
153
|
+
const { session } = await createAgentSession({
|
|
154
|
+
cwd,
|
|
155
|
+
agentDir,
|
|
156
|
+
sessionManager: SessionManager.inMemory(cwd),
|
|
157
|
+
settingsManager: SettingsManager.create(cwd, agentDir),
|
|
158
|
+
modelRegistry: ctx.modelRegistry,
|
|
159
|
+
model: ctx.model,
|
|
160
|
+
tools: [],
|
|
161
|
+
resourceLoader: loader,
|
|
162
|
+
});
|
|
163
|
+
await session.prompt(prompt);
|
|
164
|
+
return getLastAssistantText(session);
|
|
165
|
+
}
|
|
166
|
+
function getLastAssistantText(session) {
|
|
167
|
+
for (let i = session.messages.length - 1; i >= 0; i--) {
|
|
168
|
+
const msg = session.messages[i];
|
|
169
|
+
if (msg.role !== "assistant")
|
|
170
|
+
continue;
|
|
171
|
+
const blocks = msg.content;
|
|
172
|
+
const parts = [];
|
|
173
|
+
for (const c of blocks) {
|
|
174
|
+
if (c.type === "text" && c.text)
|
|
175
|
+
parts.push(c.text);
|
|
176
|
+
}
|
|
177
|
+
const joined = parts.join("\n").trim();
|
|
178
|
+
if (joined)
|
|
179
|
+
return joined;
|
|
180
|
+
}
|
|
181
|
+
return "";
|
|
182
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function writeDashboard(cwd: string, packageVersion: string): Promise<void>;
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// I/O wrapper that gathers all dashboard inputs and writes the rendered
|
|
2
|
+
// HTML to `.codecarto/dashboard.html`. Best-effort — failures are swallowed
|
|
3
|
+
// and never escalate to a phase error the user sees, mirroring the
|
|
4
|
+
// recordUsage discipline at extensions/codecarto/index.ts.
|
|
5
|
+
import { readdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { DASHBOARD_RELATIVE_PATH, getWorkspaceState, loadUsage, NARRATION_CACHE_RELATIVE_PATH, parseSimpleYaml, pathExists, renderDashboard, } from "../../core/index.js";
|
|
8
|
+
const CLOSEOUT_FILENAME_RE = /^(\d{4}-\d{2}-\d{2})-(.+)\.md$/;
|
|
9
|
+
export async function writeDashboard(cwd, packageVersion) {
|
|
10
|
+
try {
|
|
11
|
+
const state = await getWorkspaceState(cwd);
|
|
12
|
+
if (!state)
|
|
13
|
+
return;
|
|
14
|
+
const workspaceDir = state.workspaceDir;
|
|
15
|
+
const [usage, closeouts, outputsPresent, narration] = await Promise.all([
|
|
16
|
+
loadUsage(workspaceDir),
|
|
17
|
+
listCloseouts(workspaceDir),
|
|
18
|
+
buildOutputsPresent(state.workspaceDir, state.pipeline),
|
|
19
|
+
loadNarration(workspaceDir),
|
|
20
|
+
]);
|
|
21
|
+
const inputs = {
|
|
22
|
+
status: state.status,
|
|
23
|
+
pipeline: state.pipeline,
|
|
24
|
+
usage,
|
|
25
|
+
closeouts,
|
|
26
|
+
outputsPresent,
|
|
27
|
+
packageVersion,
|
|
28
|
+
generatedAt: new Date().toISOString(),
|
|
29
|
+
narration,
|
|
30
|
+
};
|
|
31
|
+
const html = renderDashboard(inputs);
|
|
32
|
+
const path = join(workspaceDir, DASHBOARD_RELATIVE_PATH);
|
|
33
|
+
const tempPath = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
34
|
+
await writeFile(tempPath, html, "utf8");
|
|
35
|
+
await rename(tempPath, path);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
// Best-effort: a failed dashboard write must not surface as a phase
|
|
39
|
+
// error. The user's pipeline state is unaffected; the next state
|
|
40
|
+
// change will trigger another render attempt.
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
async function listCloseouts(workspaceDir) {
|
|
44
|
+
const dir = join(workspaceDir, "closeouts");
|
|
45
|
+
if (!(await pathExists(dir)))
|
|
46
|
+
return [];
|
|
47
|
+
let entries;
|
|
48
|
+
try {
|
|
49
|
+
entries = await readdir(dir);
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return [];
|
|
53
|
+
}
|
|
54
|
+
const out = [];
|
|
55
|
+
for (const name of entries) {
|
|
56
|
+
const m = CLOSEOUT_FILENAME_RE.exec(name);
|
|
57
|
+
if (!m)
|
|
58
|
+
continue;
|
|
59
|
+
out.push({ date: m[1], phaseOrModule: m[2], fileName: name });
|
|
60
|
+
}
|
|
61
|
+
return out;
|
|
62
|
+
}
|
|
63
|
+
async function buildOutputsPresent(workspaceDir, pipeline) {
|
|
64
|
+
const out = new Map();
|
|
65
|
+
for (const phaseId of pipeline.phase_order) {
|
|
66
|
+
const phaseDef = pipeline.phases.find((p) => p.id === phaseId);
|
|
67
|
+
if (!phaseDef)
|
|
68
|
+
continue;
|
|
69
|
+
const entry = { secondary: [] };
|
|
70
|
+
if (phaseDef.primary_output) {
|
|
71
|
+
entry.primary = {
|
|
72
|
+
path: phaseDef.primary_output,
|
|
73
|
+
exists: await pathExists(join(workspaceDir, phaseDef.primary_output)),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
for (const sec of phaseDef.secondary_outputs ?? []) {
|
|
77
|
+
entry.secondary.push({
|
|
78
|
+
path: sec.path,
|
|
79
|
+
exists: await pathExists(join(workspaceDir, sec.path)),
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
out.set(phaseId, entry);
|
|
83
|
+
}
|
|
84
|
+
return out;
|
|
85
|
+
}
|
|
86
|
+
async function loadNarration(workspaceDir) {
|
|
87
|
+
const path = join(workspaceDir, NARRATION_CACHE_RELATIVE_PATH);
|
|
88
|
+
if (!(await pathExists(path)))
|
|
89
|
+
return undefined;
|
|
90
|
+
try {
|
|
91
|
+
const raw = await readFile(path, "utf8");
|
|
92
|
+
const { frontmatter, body } = splitFrontmatter(raw);
|
|
93
|
+
if (!frontmatter)
|
|
94
|
+
return undefined;
|
|
95
|
+
const generatedAt = typeof frontmatter.generatedAt === "string" ? frontmatter.generatedAt : "";
|
|
96
|
+
const phaseCountAtGeneration = typeof frontmatter.phaseCountAtGeneration === "number" ? frontmatter.phaseCountAtGeneration : 0;
|
|
97
|
+
if (!generatedAt)
|
|
98
|
+
return undefined;
|
|
99
|
+
return { content: body.trim(), generatedAt, phaseCountAtGeneration };
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
function splitFrontmatter(raw) {
|
|
106
|
+
if (!raw.startsWith("---\n"))
|
|
107
|
+
return { frontmatter: null, body: raw };
|
|
108
|
+
const end = raw.indexOf("\n---\n", 4);
|
|
109
|
+
if (end === -1)
|
|
110
|
+
return { frontmatter: null, body: raw };
|
|
111
|
+
const yamlText = raw.slice(4, end);
|
|
112
|
+
const body = raw.slice(end + 5);
|
|
113
|
+
try {
|
|
114
|
+
const parsed = parseSimpleYaml(yamlText);
|
|
115
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
116
|
+
return { frontmatter: parsed, body };
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
// fall through
|
|
121
|
+
}
|
|
122
|
+
return { frontmatter: null, body };
|
|
123
|
+
}
|
|
@@ -1,36 +1,15 @@
|
|
|
1
1
|
import { cp, mkdir, rm, writeFile } from "node:fs/promises";
|
|
2
2
|
import { basename, join, resolve } from "node:path";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
3
|
+
import { autoCompletePhase, buildAutoSummary, isPhaseRunning, runAuto, runSinglePhase } from "./auto-runner.js";
|
|
4
|
+
import { disposeAgentsWidget } from "./agent-widget.js";
|
|
5
|
+
import { parseDashboardFlags } from "./dashboard-flags.js";
|
|
6
|
+
import { narrateDashboard } from "./dashboard-narrator.js";
|
|
7
|
+
import { writeDashboard } from "./dashboard-writer.js";
|
|
8
8
|
import { parseNextFlags } from "./next-flags.js";
|
|
9
|
-
import {
|
|
9
|
+
import { buildPhasePrompt, buildSkillPrompt, buildValidationSummary, canonicalPath, computePerPhaseTotals, computeTotals, createEmptyStatus, DEFAULT_PIPELINE_PATH, getNextEligiblePhase, getPipelineLabel, getWorkspaceState, isWithinPath, listSkillNames, loadCodecartoConfig, loadUsage, loadYamlFile, normalizeForComparison, packagedWorkspaceDir, pathExists, PACKAGE_VERSION, PIPELINE_ALIASES, resolvePhase, resolvePipelineChoice, stringifySimpleYaml, validatePhaseOutput, } from "../../core/index.js";
|
|
10
10
|
const STATUS_WIDGET_ID = "codecarto-widget";
|
|
11
11
|
const STATUS_LINE_ID = "codecarto-status";
|
|
12
12
|
const SAFE_TOOL_NAMES = ["read", "grep", "find", "ls", "edit", "write"];
|
|
13
|
-
async function recordUsage(workspaceDir, phaseId, status, activity) {
|
|
14
|
-
try {
|
|
15
|
-
await appendUsageRun(workspaceDir, {
|
|
16
|
-
timestamp: new Date().toISOString(),
|
|
17
|
-
phase: phaseId,
|
|
18
|
-
status,
|
|
19
|
-
turn_count: activity.turnCount,
|
|
20
|
-
tool_uses: activity.toolUses,
|
|
21
|
-
duration_ms: (activity.completedAt ?? Date.now()) - activity.startedAt,
|
|
22
|
-
tokens: {
|
|
23
|
-
input: activity.lifetimeUsage.input,
|
|
24
|
-
output: activity.lifetimeUsage.output,
|
|
25
|
-
cache_write: activity.lifetimeUsage.cacheWrite,
|
|
26
|
-
},
|
|
27
|
-
});
|
|
28
|
-
}
|
|
29
|
-
catch {
|
|
30
|
-
// Local usage logging is best-effort. A full disk or permission
|
|
31
|
-
// failure shouldn't surface as a phase error to the user.
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
13
|
function formatUsageTokens(count) {
|
|
35
14
|
if (count >= 1_000_000)
|
|
36
15
|
return `${(count / 1_000_000).toFixed(2)}M`;
|
|
@@ -203,6 +182,9 @@ export default function codeCartographerExtension(pi) {
|
|
|
203
182
|
lastFeedbackLines = [`Initialized workspace with pipeline: ${getPipelineLabel(selectedPipelinePath)}`];
|
|
204
183
|
ctx.ui.notify(`Initialized CodeCartographer (${getPipelineLabel(selectedPipelinePath)})`, "info");
|
|
205
184
|
await ctx.reload();
|
|
185
|
+
// Render the initial dashboard (empty usage, all phases pending) so
|
|
186
|
+
// the user sees the file exist immediately after /codecarto-init.
|
|
187
|
+
void writeDashboard(ctx.cwd, PACKAGE_VERSION);
|
|
206
188
|
return;
|
|
207
189
|
},
|
|
208
190
|
});
|
|
@@ -219,15 +201,19 @@ export default function codeCartographerExtension(pi) {
|
|
|
219
201
|
},
|
|
220
202
|
});
|
|
221
203
|
pi.registerCommand("codecarto-next", {
|
|
222
|
-
description: "Run the next eligible CodeCartographer phase as a sub-agent. Flags: --llm-steer / --no-llm-steer",
|
|
204
|
+
description: "Run the next eligible CodeCartographer phase as a sub-agent. Flags: --llm-steer / --no-llm-steer / --auto [--strict]",
|
|
223
205
|
getArgumentCompletions: (prefix) => {
|
|
224
|
-
const items = ["--llm-steer", "--no-llm-steer"]
|
|
206
|
+
const items = ["--llm-steer", "--no-llm-steer", "--auto", "--strict"]
|
|
225
207
|
.filter((value) => value.startsWith(prefix))
|
|
226
208
|
.map((value) => ({ value, label: value }));
|
|
227
209
|
return items.length > 0 ? items : null;
|
|
228
210
|
},
|
|
229
211
|
handler: async (args, ctx) => {
|
|
230
212
|
const flags = parseNextFlags(args);
|
|
213
|
+
if (flags.error) {
|
|
214
|
+
ctx.ui.notify(flags.error, "error");
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
231
217
|
if (flags.unknown.length > 0) {
|
|
232
218
|
ctx.ui.notify(`Unknown /codecarto-next flag: ${flags.unknown.join(" ")}`, "error");
|
|
233
219
|
return;
|
|
@@ -235,6 +221,24 @@ export default function codeCartographerExtension(pi) {
|
|
|
235
221
|
const state = await ensureWorkspaceState(ctx);
|
|
236
222
|
if (!state)
|
|
237
223
|
return;
|
|
224
|
+
if (flags.auto) {
|
|
225
|
+
ctx.ui.notify(`Auto pipeline${flags.strict ? " (strict)" : ""} running…`, "info");
|
|
226
|
+
const result = await runAuto(ctx, pi, state, {
|
|
227
|
+
strict: flags.strict,
|
|
228
|
+
llmSteerOverride: flags.llmSteerOverride,
|
|
229
|
+
signal: ctx.signal,
|
|
230
|
+
});
|
|
231
|
+
const availableSkills = await listSkillNames(state.workspaceDir).catch(() => []);
|
|
232
|
+
pi.sendMessage({
|
|
233
|
+
customType: "codecarto-auto-summary",
|
|
234
|
+
content: buildAutoSummary(result, availableSkills),
|
|
235
|
+
display: true,
|
|
236
|
+
});
|
|
237
|
+
lastFeedbackLines = [`Auto pipeline ${result.outcome}: ${result.reason}`];
|
|
238
|
+
await refreshWorkspaceUi(ctx, lastFeedbackLines);
|
|
239
|
+
ctx.ui.notify(`Auto pipeline ${result.outcome}: ${result.phasesRun.length}/${result.totalPhases} phases.`, result.outcome === "complete" ? "info" : "warning");
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
238
242
|
const phase = getNextEligiblePhase(state);
|
|
239
243
|
if (!phase) {
|
|
240
244
|
lastFeedbackLines = ["All phases complete."];
|
|
@@ -244,108 +248,23 @@ export default function codeCartographerExtension(pi) {
|
|
|
244
248
|
}
|
|
245
249
|
// Reject re-entry: don't spawn a duplicate runner for a phase that's
|
|
246
250
|
// already in flight from a previous /codecarto-next invocation.
|
|
247
|
-
|
|
248
|
-
if (existing && existing.status === "running") {
|
|
251
|
+
if (isPhaseRunning(phase.id)) {
|
|
249
252
|
ctx.ui.notify(`Phase ${phase.id} is already running.`, "warning");
|
|
250
253
|
return;
|
|
251
254
|
}
|
|
252
255
|
const config = await loadCodecartoConfig(state.workspaceDir);
|
|
253
256
|
const llmSteerEnabled = flags.llmSteerOverride ?? config.orchestrator.llm_steer_next_phase;
|
|
254
|
-
let prompt = await buildPhasePrompt(state, phase, false);
|
|
255
|
-
if (llmSteerEnabled) {
|
|
256
|
-
ctx.ui.notify(`Customizing ${phase.id} prompt via LLM rewriter…`, "info");
|
|
257
|
-
const rewrite = await rewritePhasePrompt({ ctx, state, originalPrompt: prompt, nextPhaseId: phase.id });
|
|
258
|
-
if (rewrite.used) {
|
|
259
|
-
prompt = rewrite.prompt;
|
|
260
|
-
ctx.ui.notify(`LLM rewriter customized ${phase.id} seed prompt.`, "info");
|
|
261
|
-
}
|
|
262
|
-
else {
|
|
263
|
-
ctx.ui.notify(`LLM rewriter skipped (${rewrite.skipReason}); using stock prompt.`, "warning");
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
const activity = startPhase(phase.id);
|
|
267
257
|
lastFeedbackLines = [`Running ${phase.id} phase as sub-agent`];
|
|
268
258
|
setUiState(ctx, state, lastFeedbackLines);
|
|
269
|
-
|
|
270
|
-
//
|
|
271
|
-
// phase
|
|
272
|
-
|
|
273
|
-
getAgentsWidget().attach(ctx.ui);
|
|
274
|
-
// Fire-and-forget: spawn the phase runner asynchronously so the
|
|
275
|
-
// orchestrator's TUI stays responsive. The runner mutates the shared
|
|
276
|
-
// agent-state map from event callbacks; M2 will read that map from a
|
|
277
|
-
// persistent widget. For M1 we just notify on completion.
|
|
278
|
-
void runPhase(ctx, prompt, {
|
|
279
|
-
onSessionCreated: (session) => { activity.session = session; },
|
|
280
|
-
onToolStart: (id, name) => { activity.activeTools.set(id, name); activity.toolUses++; },
|
|
281
|
-
onToolEnd: (id) => { activity.activeTools.delete(id); },
|
|
282
|
-
onTextDelta: (_delta, fullText) => { activity.responseText = fullText; },
|
|
283
|
-
onTurnEnd: (turnCount) => { activity.turnCount = turnCount; },
|
|
284
|
-
onMessageEnd: (usage) => {
|
|
285
|
-
activity.lifetimeUsage.input += usage.input;
|
|
286
|
-
activity.lifetimeUsage.output += usage.output;
|
|
287
|
-
activity.lifetimeUsage.cacheWrite += usage.cacheWrite;
|
|
288
|
-
},
|
|
289
|
-
}, { sessionName: `CodeCartographer phase: ${phase.id}` })
|
|
290
|
-
.then((result) => {
|
|
291
|
-
const status = result.aborted ? "aborted" : "completed";
|
|
292
|
-
finishPhase(phase.id, { status });
|
|
293
|
-
if (ctx.hasUI) {
|
|
294
|
-
ctx.ui.notify(result.aborted
|
|
295
|
-
? `Phase ${phase.id} aborted.`
|
|
296
|
-
: `Phase ${phase.id} sub-agent finished (${result.toolUses} tool uses, ${result.turnCount} turns).`, result.aborted ? "warning" : "info");
|
|
297
|
-
}
|
|
298
|
-
// Inject a CustomMessageEntry into the orchestrator's session so
|
|
299
|
-
// the user sees a closeout summary in the TUI scrollback and the
|
|
300
|
-
// orchestrator's LLM picks up the phase result as context on the
|
|
301
|
-
// next turn. display:true renders in the TUI; no triggerTurn so
|
|
302
|
-
// the LLM doesn't auto-respond — the user remains in control.
|
|
303
|
-
pi.sendMessage({
|
|
304
|
-
customType: "codecarto-phase-summary",
|
|
305
|
-
content: buildPhaseSummary({
|
|
306
|
-
phaseId: phase.id,
|
|
307
|
-
status: result.aborted ? "aborted" : "completed",
|
|
308
|
-
turnCount: activity.turnCount,
|
|
309
|
-
toolUses: activity.toolUses,
|
|
310
|
-
tokens: activity.lifetimeUsage,
|
|
311
|
-
durationMs: (activity.completedAt ?? Date.now()) - activity.startedAt,
|
|
312
|
-
responseText: result.responseText,
|
|
313
|
-
}),
|
|
314
|
-
display: true,
|
|
315
|
-
});
|
|
316
|
-
void recordUsage(state.workspaceDir, phase.id, status, activity);
|
|
317
|
-
})
|
|
318
|
-
.catch((err) => {
|
|
319
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
320
|
-
finishPhase(phase.id, { status: "error", error: message });
|
|
321
|
-
if (ctx.hasUI) {
|
|
322
|
-
ctx.ui.notify(`Phase ${phase.id} sub-agent failed: ${message}`, "error");
|
|
323
|
-
}
|
|
324
|
-
pi.sendMessage({
|
|
325
|
-
customType: "codecarto-phase-summary",
|
|
326
|
-
content: buildPhaseSummary({
|
|
327
|
-
phaseId: phase.id,
|
|
328
|
-
status: "error",
|
|
329
|
-
turnCount: activity.turnCount,
|
|
330
|
-
toolUses: activity.toolUses,
|
|
331
|
-
tokens: activity.lifetimeUsage,
|
|
332
|
-
durationMs: (activity.completedAt ?? Date.now()) - activity.startedAt,
|
|
333
|
-
responseText: "",
|
|
334
|
-
error: message,
|
|
335
|
-
}),
|
|
336
|
-
display: true,
|
|
337
|
-
});
|
|
338
|
-
void recordUsage(state.workspaceDir, phase.id, "error", activity);
|
|
339
|
-
})
|
|
259
|
+
// Fire-and-forget: keep the TUI responsive while the sub-agent works.
|
|
260
|
+
// runSinglePhase handles all side effects (steering message, notify,
|
|
261
|
+
// phase summary, recordUsage, dashboard regen, clearPhase linger).
|
|
262
|
+
void runSinglePhase(ctx, pi, state, phase, { llmSteerEnabled, signal: ctx.signal })
|
|
340
263
|
.finally(() => {
|
|
341
|
-
//
|
|
342
|
-
//
|
|
343
|
-
//
|
|
344
|
-
// state without waiting for the user to run /codecarto-status.
|
|
264
|
+
// Refresh the status widget after the phase resolves so the
|
|
265
|
+
// "Open questions / Carry-forward / Next" lines reflect any
|
|
266
|
+
// owner_notes the sub-agent wrote to status.yaml.
|
|
345
267
|
void refreshWorkspaceUi(ctx);
|
|
346
|
-
// Linger 30s in M1 so /codecarto-status can show that the phase ran;
|
|
347
|
-
// M2's widget owns the proper "linger N turns" lifecycle.
|
|
348
|
-
setTimeout(() => clearPhase(phase.id), 30_000);
|
|
349
268
|
});
|
|
350
269
|
},
|
|
351
270
|
});
|
|
@@ -403,76 +322,7 @@ export default function codeCartographerExtension(pi) {
|
|
|
403
322
|
ctx.ui.notify(`Cannot complete ${validation.phaseId}: ${validation.overall}`, "error");
|
|
404
323
|
return;
|
|
405
324
|
}
|
|
406
|
-
const
|
|
407
|
-
const updatedState = await updateStatusAtomically(ctx.cwd, (lockedState) => {
|
|
408
|
-
const phase = resolvePhase(lockedState, validation.phaseId);
|
|
409
|
-
if (!phase?.primary_output) {
|
|
410
|
-
throw new Error(`Phase ${validation.phaseId} is missing primary_output.`);
|
|
411
|
-
}
|
|
412
|
-
const nextStatus = normalizeStatus(lockedState.status, lockedState.pipeline, lockedState.status.pipeline, lockedState.cwd);
|
|
413
|
-
const existingPhase = nextStatus.phases[validation.phaseId] ?? {
|
|
414
|
-
status: "pending",
|
|
415
|
-
owner_notes: [],
|
|
416
|
-
outputs_present: [],
|
|
417
|
-
open_questions: [],
|
|
418
|
-
carry_forward: [],
|
|
419
|
-
};
|
|
420
|
-
const gapEntries = validation.rows
|
|
421
|
-
.filter((row) => row.result.toUpperCase().includes("PARTIAL"))
|
|
422
|
-
.map((row) => ({
|
|
423
|
-
kind: "needs-maintainer-decision",
|
|
424
|
-
description: row.criterion || "Partial validation gap",
|
|
425
|
-
deferred_reason: row.evidence || "Marked PARTIAL by validation",
|
|
426
|
-
}));
|
|
427
|
-
const mergedOpenQuestions = [...existingPhase.open_questions];
|
|
428
|
-
for (const candidate of gapEntries) {
|
|
429
|
-
const dupe = mergedOpenQuestions.some((entry) => entry.description === candidate.description && entry.deferred_reason === candidate.deferred_reason);
|
|
430
|
-
if (!dupe)
|
|
431
|
-
mergedOpenQuestions.push(candidate);
|
|
432
|
-
}
|
|
433
|
-
nextStatus.phases[validation.phaseId] = {
|
|
434
|
-
status: "complete",
|
|
435
|
-
owner_notes: uniqueStrings([
|
|
436
|
-
...existingPhase.owner_notes,
|
|
437
|
-
`Completed via /codecarto-complete on ${completionTimestamp}.`,
|
|
438
|
-
`Primary output: .codecarto/${validation.primaryOutput}`,
|
|
439
|
-
`Validation: ${validation.overall}`,
|
|
440
|
-
]).slice(-3),
|
|
441
|
-
outputs_present: uniqueStrings([...existingPhase.outputs_present, validation.primaryOutput]),
|
|
442
|
-
open_questions: mergedOpenQuestions,
|
|
443
|
-
carry_forward: existingPhase.carry_forward ?? [],
|
|
444
|
-
};
|
|
445
|
-
nextStatus.last_updated = completionTimestamp;
|
|
446
|
-
const updatedWorkspaceState = {
|
|
447
|
-
...lockedState,
|
|
448
|
-
status: nextStatus,
|
|
449
|
-
};
|
|
450
|
-
const nextEligible = getNextEligiblePhase(updatedWorkspaceState);
|
|
451
|
-
nextStatus.current_phase = nextEligible?.id ?? "complete";
|
|
452
|
-
nextStatus.next_actions = nextEligible
|
|
453
|
-
? [
|
|
454
|
-
`Begin ${nextEligible.id} phase by producing ${nextEligible.primary_output ?? `findings/${nextEligible.id}/`}`,
|
|
455
|
-
]
|
|
456
|
-
: ["All phases complete. Review findings, open questions, and downstream implementation notes."];
|
|
457
|
-
return {
|
|
458
|
-
state: {
|
|
459
|
-
...updatedWorkspaceState,
|
|
460
|
-
status: nextStatus,
|
|
461
|
-
},
|
|
462
|
-
threadLogEntry: buildThreadLogEntry(validation.phaseId, validation, completionTimestamp),
|
|
463
|
-
};
|
|
464
|
-
});
|
|
465
|
-
let closeoutNotice;
|
|
466
|
-
try {
|
|
467
|
-
const created = await ensureCloseoutStub(updatedState.workspaceDir, validation.phaseId, completionTimestamp);
|
|
468
|
-
if (created) {
|
|
469
|
-
closeoutNotice = `Closeout stub: .codecarto/closeouts/${closeoutFileName(dateOnly(completionTimestamp), validation.phaseId)} (fill it in)`;
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
catch (error) {
|
|
473
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
474
|
-
closeoutNotice = `Closeout stub not created: ${message}`;
|
|
475
|
-
}
|
|
325
|
+
const { updatedState, closeoutNotice } = await autoCompletePhase(ctx, validation);
|
|
476
326
|
lastFeedbackLines = [
|
|
477
327
|
`Completed phase: ${validation.phaseId}`,
|
|
478
328
|
`Validation: ${validation.overall}`,
|
|
@@ -553,4 +403,37 @@ export default function codeCartographerExtension(pi) {
|
|
|
553
403
|
ctx.ui.notify(`CodeCartographer usage: ${totals.runs} run${totals.runs === 1 ? "" : "s"}, ${formatUsageTokens(totals.tokens.input + totals.tokens.output)} tokens total`, "info");
|
|
554
404
|
},
|
|
555
405
|
});
|
|
406
|
+
pi.registerCommand("codecarto-dashboard", {
|
|
407
|
+
description: "Regenerate .codecarto/dashboard.html (use --narrate for an LLM executive summary)",
|
|
408
|
+
getArgumentCompletions: (prefix) => {
|
|
409
|
+
const items = ["--narrate"]
|
|
410
|
+
.filter((value) => value.startsWith(prefix))
|
|
411
|
+
.map((value) => ({ value, label: value }));
|
|
412
|
+
return items.length > 0 ? items : null;
|
|
413
|
+
},
|
|
414
|
+
handler: async (args, ctx) => {
|
|
415
|
+
const flags = parseDashboardFlags(args);
|
|
416
|
+
if (flags.unknown.length > 0) {
|
|
417
|
+
ctx.ui.notify(`Unknown /codecarto-dashboard flag: ${flags.unknown.join(" ")}`, "error");
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
const state = await ensureWorkspaceState(ctx);
|
|
421
|
+
if (!state)
|
|
422
|
+
return;
|
|
423
|
+
if (flags.narrate) {
|
|
424
|
+
ctx.ui.notify(`Narrating dashboard via LLM…`, "info");
|
|
425
|
+
const result = await narrateDashboard(ctx, state);
|
|
426
|
+
if (result.used) {
|
|
427
|
+
ctx.ui.notify("Narration written to .codecarto/.dashboard-narration.local.md", "info");
|
|
428
|
+
}
|
|
429
|
+
else {
|
|
430
|
+
ctx.ui.notify(`LLM narration skipped (${result.skipReason}); rendering deterministic dashboard.`, "warning");
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
await writeDashboard(ctx.cwd, PACKAGE_VERSION);
|
|
434
|
+
lastFeedbackLines = ["Dashboard regenerated: .codecarto/dashboard.html"];
|
|
435
|
+
setUiState(ctx, state, lastFeedbackLines);
|
|
436
|
+
ctx.ui.notify("Dashboard regenerated: .codecarto/dashboard.html", "info");
|
|
437
|
+
},
|
|
438
|
+
});
|
|
556
439
|
}
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
export interface NextFlags {
|
|
2
2
|
llmSteerOverride?: boolean;
|
|
3
|
+
auto: boolean;
|
|
4
|
+
strict: boolean;
|
|
3
5
|
unknown: string[];
|
|
6
|
+
/** Set when --strict is passed without --auto. Caller surfaces as an error. */
|
|
7
|
+
error?: string;
|
|
4
8
|
}
|
|
5
9
|
export declare function parseNextFlags(args: string): NextFlags;
|
|
6
10
|
export declare const KNOWN_NEXT_FLAGS: readonly string[];
|