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
|
@@ -10,6 +10,8 @@ export interface RewritePhasePromptResult {
|
|
|
10
10
|
prompt: string;
|
|
11
11
|
used: boolean;
|
|
12
12
|
skipReason?: string;
|
|
13
|
+
/** ID of the previous phase whose closeout the rewriter read, when used. */
|
|
14
|
+
prevPhaseId?: string;
|
|
13
15
|
}
|
|
14
16
|
/**
|
|
15
17
|
* Run the rewriter and return the customized seed prompt. On any failure
|
|
@@ -18,3 +20,14 @@ export interface RewritePhasePromptResult {
|
|
|
18
20
|
* throws. The caller decides what to surface to the user.
|
|
19
21
|
*/
|
|
20
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;
|
|
@@ -48,7 +48,27 @@ export async function rewritePhasePrompt(input) {
|
|
|
48
48
|
if (!trimmed) {
|
|
49
49
|
return { prompt: originalPrompt, used: false, skipReason: "rewriter returned empty output" };
|
|
50
50
|
}
|
|
51
|
-
return { prompt: trimmed, used: true };
|
|
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");
|
|
52
72
|
}
|
|
53
73
|
function findPreviousPhaseId(state, nextPhaseId) {
|
|
54
74
|
const order = state.pipeline.phase_order;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { type PhaseActivity } from "./agent-state.ts";
|
|
3
|
+
import { type PipelinePhase, type ValidationOverall, type ValidationResult, type WorkspaceState } from "../../core/index.ts";
|
|
4
|
+
export interface RunSinglePhaseOptions {
|
|
5
|
+
llmSteerEnabled: boolean;
|
|
6
|
+
signal?: AbortSignal;
|
|
7
|
+
}
|
|
8
|
+
export interface SinglePhaseResult {
|
|
9
|
+
status: "completed" | "aborted" | "error";
|
|
10
|
+
activity: PhaseActivity;
|
|
11
|
+
error?: string;
|
|
12
|
+
responseText?: string;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Run one phase end to end: optional LLM-steered rewrite, spawn the sub-agent,
|
|
16
|
+
* wait for it, then emit the side effects the historical /codecarto-next chain
|
|
17
|
+
* fired (notify, phase-summary sendMessage, recordUsage, writeDashboard). The
|
|
18
|
+
* .finally linger-timeout for clearPhase fires here so both callers (one-shot
|
|
19
|
+
* + auto loop) get the same lifecycle.
|
|
20
|
+
*
|
|
21
|
+
* Pre-conditions: caller verified the phase isn't already running (via
|
|
22
|
+
* getPhaseActivity) and attached the agents widget if it wanted live progress.
|
|
23
|
+
*/
|
|
24
|
+
export declare function runSinglePhase(ctx: ExtensionContext, pi: ExtensionAPI, state: WorkspaceState, phase: PipelinePhase, options: RunSinglePhaseOptions): Promise<SinglePhaseResult>;
|
|
25
|
+
/**
|
|
26
|
+
* Re-entry guard: returns true if the phase is already running from a prior
|
|
27
|
+
* spawn (manual or auto). Callers (both /codecarto-next paths) should reject
|
|
28
|
+
* before invoking runSinglePhase.
|
|
29
|
+
*/
|
|
30
|
+
export declare function isPhaseRunning(phaseId: string): boolean;
|
|
31
|
+
export interface AutoCompleteResult {
|
|
32
|
+
updatedState: WorkspaceState;
|
|
33
|
+
closeoutNotice?: string;
|
|
34
|
+
}
|
|
35
|
+
export declare function autoCompletePhase(ctx: ExtensionContext, validation: ValidationResult): Promise<AutoCompleteResult>;
|
|
36
|
+
export type AutoOutcome = "complete" | "stopped" | "aborted";
|
|
37
|
+
export interface AutoRunOptions {
|
|
38
|
+
strict: boolean;
|
|
39
|
+
llmSteerOverride?: boolean;
|
|
40
|
+
signal?: AbortSignal;
|
|
41
|
+
}
|
|
42
|
+
export interface AutoRunResult {
|
|
43
|
+
outcome: AutoOutcome;
|
|
44
|
+
reason: string;
|
|
45
|
+
phasesRun: string[];
|
|
46
|
+
totalPhases: number;
|
|
47
|
+
startedAt: number;
|
|
48
|
+
endedAt: number;
|
|
49
|
+
totalTokens: {
|
|
50
|
+
input: number;
|
|
51
|
+
output: number;
|
|
52
|
+
cacheWrite: number;
|
|
53
|
+
};
|
|
54
|
+
stoppedAt?: {
|
|
55
|
+
phaseId: string;
|
|
56
|
+
validation?: ValidationOverall;
|
|
57
|
+
error?: string;
|
|
58
|
+
};
|
|
59
|
+
validationSummary?: string[];
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Per-iteration decision: given the outcome of a sub-agent run and (if it
|
|
63
|
+
* completed) its validation result, what should the auto loop do next?
|
|
64
|
+
* Pure function — no I/O, no module state — so the decision matrix is
|
|
65
|
+
* unit-testable without mocking the SDK.
|
|
66
|
+
*/
|
|
67
|
+
export type AutoDecision = {
|
|
68
|
+
action: "continue";
|
|
69
|
+
} | {
|
|
70
|
+
action: "stop";
|
|
71
|
+
reason: string;
|
|
72
|
+
validation?: ValidationOverall;
|
|
73
|
+
error?: string;
|
|
74
|
+
validationSummary?: string[];
|
|
75
|
+
} | {
|
|
76
|
+
action: "aborted";
|
|
77
|
+
};
|
|
78
|
+
export declare function decideAfterPhase(phaseStatus: SinglePhaseResult["status"], phaseError: string | undefined, validation: ValidationResult | null, strict: boolean): AutoDecision;
|
|
79
|
+
export declare function runAuto(ctx: ExtensionContext, pi: ExtensionAPI, initialState: WorkspaceState, options: AutoRunOptions): Promise<AutoRunResult>;
|
|
80
|
+
export declare function buildAutoSummary(result: AutoRunResult, availableSkills?: string[]): string;
|
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
// End-to-end auto runner for /codecarto-next --auto plus the two helpers
|
|
2
|
+
// shared between the one-shot path and the auto loop.
|
|
3
|
+
//
|
|
4
|
+
// runSinglePhase encapsulates everything the historical inline /codecarto-next
|
|
5
|
+
// did between getNextEligiblePhase and the .finally setTimeout. The one-shot
|
|
6
|
+
// handler still consumes it as a fire-and-forget Promise (void runSinglePhase
|
|
7
|
+
// keeps the TUI responsive while the phase runs). The auto loop awaits the
|
|
8
|
+
// same Promise, validates the output, and decides whether to advance.
|
|
9
|
+
//
|
|
10
|
+
// autoCompletePhase mirrors /codecarto-complete's updateStatusAtomically
|
|
11
|
+
// block — the gap → open_questions extraction, owner-notes append, THREAD_LOG
|
|
12
|
+
// write, closeout-stub creation, dashboard regen. UI notifications stay in
|
|
13
|
+
// the calling handler.
|
|
14
|
+
import { runPhase } from "./agent-runner.js";
|
|
15
|
+
import { clearPhase, finishPhase, getPhaseActivity, startPhase } from "./agent-state.js";
|
|
16
|
+
import { buildSteeringMessage, rewritePhasePrompt } from "./agent-rewriter.js";
|
|
17
|
+
import { buildPhaseSummary } from "./agent-summary.js";
|
|
18
|
+
import { getAgentsWidget } from "./agent-widget.js";
|
|
19
|
+
import { writeDashboard } from "./dashboard-writer.js";
|
|
20
|
+
import { appendUsageRun, buildPhasePrompt, buildThreadLogEntry, buildValidationSummary, closeoutFileName, dateOnly, ensureCloseoutStub, formatMillis, formatTokenCount, getNextEligiblePhase, getWorkspaceState, loadCodecartoConfig, normalizeStatus, PACKAGE_VERSION, resolvePhase, uniqueStrings, updateStatusAtomically, validatePhaseOutput, } from "../../core/index.js";
|
|
21
|
+
/**
|
|
22
|
+
* Run one phase end to end: optional LLM-steered rewrite, spawn the sub-agent,
|
|
23
|
+
* wait for it, then emit the side effects the historical /codecarto-next chain
|
|
24
|
+
* fired (notify, phase-summary sendMessage, recordUsage, writeDashboard). The
|
|
25
|
+
* .finally linger-timeout for clearPhase fires here so both callers (one-shot
|
|
26
|
+
* + auto loop) get the same lifecycle.
|
|
27
|
+
*
|
|
28
|
+
* Pre-conditions: caller verified the phase isn't already running (via
|
|
29
|
+
* getPhaseActivity) and attached the agents widget if it wanted live progress.
|
|
30
|
+
*/
|
|
31
|
+
export async function runSinglePhase(ctx, pi, state, phase, options) {
|
|
32
|
+
let prompt = await buildPhasePrompt(state, phase, false);
|
|
33
|
+
if (options.llmSteerEnabled) {
|
|
34
|
+
if (ctx.hasUI)
|
|
35
|
+
ctx.ui.notify(`Customizing ${phase.id} prompt via LLM rewriter…`, "info");
|
|
36
|
+
const rewrite = await rewritePhasePrompt({ ctx, state, originalPrompt: prompt, nextPhaseId: phase.id });
|
|
37
|
+
if (rewrite.used) {
|
|
38
|
+
prompt = rewrite.prompt;
|
|
39
|
+
if (ctx.hasUI)
|
|
40
|
+
ctx.ui.notify(`LLM rewriter customized ${phase.id} seed prompt.`, "info");
|
|
41
|
+
pi.sendMessage({
|
|
42
|
+
customType: "codecarto-steering",
|
|
43
|
+
content: buildSteeringMessage({
|
|
44
|
+
nextPhaseId: phase.id,
|
|
45
|
+
prevPhaseId: rewrite.prevPhaseId,
|
|
46
|
+
rewrittenPrompt: rewrite.prompt,
|
|
47
|
+
}),
|
|
48
|
+
display: true,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
else if (ctx.hasUI) {
|
|
52
|
+
ctx.ui.notify(`LLM rewriter skipped (${rewrite.skipReason}); using stock prompt.`, "warning");
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const activity = startPhase(phase.id);
|
|
56
|
+
if (ctx.hasUI) {
|
|
57
|
+
ctx.ui.notify(`CodeCartographer phase: ${phase.id} (sub-agent running)`, "info");
|
|
58
|
+
getAgentsWidget().attach(ctx.ui);
|
|
59
|
+
}
|
|
60
|
+
try {
|
|
61
|
+
const result = await runPhase(ctx, prompt, {
|
|
62
|
+
onSessionCreated: (session) => { activity.session = session; },
|
|
63
|
+
onToolStart: (id, name) => { activity.activeTools.set(id, name); activity.toolUses++; },
|
|
64
|
+
onToolEnd: (id) => { activity.activeTools.delete(id); },
|
|
65
|
+
onTextDelta: (_delta, fullText) => { activity.responseText = fullText; },
|
|
66
|
+
onTurnEnd: (turnCount) => { activity.turnCount = turnCount; },
|
|
67
|
+
onMessageEnd: (usage) => {
|
|
68
|
+
activity.lifetimeUsage.input += usage.input;
|
|
69
|
+
activity.lifetimeUsage.output += usage.output;
|
|
70
|
+
activity.lifetimeUsage.cacheWrite += usage.cacheWrite;
|
|
71
|
+
},
|
|
72
|
+
}, { sessionName: `CodeCartographer phase: ${phase.id}` }, options.signal);
|
|
73
|
+
const status = result.aborted ? "aborted" : "completed";
|
|
74
|
+
finishPhase(phase.id, { status });
|
|
75
|
+
if (ctx.hasUI) {
|
|
76
|
+
ctx.ui.notify(result.aborted
|
|
77
|
+
? `Phase ${phase.id} aborted.`
|
|
78
|
+
: `Phase ${phase.id} sub-agent finished (${result.toolUses} tool uses, ${result.turnCount} turns).`, result.aborted ? "warning" : "info");
|
|
79
|
+
}
|
|
80
|
+
pi.sendMessage({
|
|
81
|
+
customType: "codecarto-phase-summary",
|
|
82
|
+
content: buildPhaseSummary({
|
|
83
|
+
phaseId: phase.id,
|
|
84
|
+
status: result.aborted ? "aborted" : "completed",
|
|
85
|
+
turnCount: activity.turnCount,
|
|
86
|
+
toolUses: activity.toolUses,
|
|
87
|
+
tokens: activity.lifetimeUsage,
|
|
88
|
+
durationMs: (activity.completedAt ?? Date.now()) - activity.startedAt,
|
|
89
|
+
responseText: result.responseText,
|
|
90
|
+
}),
|
|
91
|
+
display: true,
|
|
92
|
+
});
|
|
93
|
+
void recordUsage(state.workspaceDir, phase.id, status, activity);
|
|
94
|
+
void writeDashboard(ctx.cwd, PACKAGE_VERSION);
|
|
95
|
+
return {
|
|
96
|
+
status: result.aborted ? "aborted" : "completed",
|
|
97
|
+
activity,
|
|
98
|
+
responseText: result.responseText,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
catch (err) {
|
|
102
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
103
|
+
finishPhase(phase.id, { status: "error", error: message });
|
|
104
|
+
if (ctx.hasUI) {
|
|
105
|
+
ctx.ui.notify(`Phase ${phase.id} sub-agent failed: ${message}`, "error");
|
|
106
|
+
}
|
|
107
|
+
pi.sendMessage({
|
|
108
|
+
customType: "codecarto-phase-summary",
|
|
109
|
+
content: buildPhaseSummary({
|
|
110
|
+
phaseId: phase.id,
|
|
111
|
+
status: "error",
|
|
112
|
+
turnCount: activity.turnCount,
|
|
113
|
+
toolUses: activity.toolUses,
|
|
114
|
+
tokens: activity.lifetimeUsage,
|
|
115
|
+
durationMs: (activity.completedAt ?? Date.now()) - activity.startedAt,
|
|
116
|
+
responseText: "",
|
|
117
|
+
error: message,
|
|
118
|
+
}),
|
|
119
|
+
display: true,
|
|
120
|
+
});
|
|
121
|
+
void recordUsage(state.workspaceDir, phase.id, "error", activity);
|
|
122
|
+
void writeDashboard(ctx.cwd, PACKAGE_VERSION);
|
|
123
|
+
return { status: "error", activity, error: message };
|
|
124
|
+
}
|
|
125
|
+
finally {
|
|
126
|
+
// Linger 30s so /codecarto-status can show that the phase ran.
|
|
127
|
+
setTimeout(() => clearPhase(phase.id), 30_000);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Re-entry guard: returns true if the phase is already running from a prior
|
|
132
|
+
* spawn (manual or auto). Callers (both /codecarto-next paths) should reject
|
|
133
|
+
* before invoking runSinglePhase.
|
|
134
|
+
*/
|
|
135
|
+
export function isPhaseRunning(phaseId) {
|
|
136
|
+
const existing = getPhaseActivity(phaseId);
|
|
137
|
+
return existing?.status === "running";
|
|
138
|
+
}
|
|
139
|
+
export async function autoCompletePhase(ctx, validation) {
|
|
140
|
+
const completionTimestamp = new Date().toISOString();
|
|
141
|
+
const updatedState = await updateStatusAtomically(ctx.cwd, (lockedState) => {
|
|
142
|
+
const phase = resolvePhase(lockedState, validation.phaseId);
|
|
143
|
+
if (!phase?.primary_output) {
|
|
144
|
+
throw new Error(`Phase ${validation.phaseId} is missing primary_output.`);
|
|
145
|
+
}
|
|
146
|
+
const nextStatus = normalizeStatus(lockedState.status, lockedState.pipeline, lockedState.status.pipeline, lockedState.cwd);
|
|
147
|
+
const existingPhase = nextStatus.phases[validation.phaseId] ?? {
|
|
148
|
+
status: "pending",
|
|
149
|
+
owner_notes: [],
|
|
150
|
+
outputs_present: [],
|
|
151
|
+
open_questions: [],
|
|
152
|
+
carry_forward: [],
|
|
153
|
+
};
|
|
154
|
+
const gapEntries = validation.rows
|
|
155
|
+
.filter((row) => row.result.toUpperCase().includes("PARTIAL"))
|
|
156
|
+
.map((row) => ({
|
|
157
|
+
kind: "needs-maintainer-decision",
|
|
158
|
+
description: row.criterion || "Partial validation gap",
|
|
159
|
+
deferred_reason: row.evidence || "Marked PARTIAL by validation",
|
|
160
|
+
}));
|
|
161
|
+
const mergedOpenQuestions = [...existingPhase.open_questions];
|
|
162
|
+
for (const candidate of gapEntries) {
|
|
163
|
+
const dupe = mergedOpenQuestions.some((entry) => entry.description === candidate.description && entry.deferred_reason === candidate.deferred_reason);
|
|
164
|
+
if (!dupe)
|
|
165
|
+
mergedOpenQuestions.push(candidate);
|
|
166
|
+
}
|
|
167
|
+
nextStatus.phases[validation.phaseId] = {
|
|
168
|
+
status: "complete",
|
|
169
|
+
owner_notes: uniqueStrings([
|
|
170
|
+
...existingPhase.owner_notes,
|
|
171
|
+
`Completed via /codecarto-complete on ${completionTimestamp}.`,
|
|
172
|
+
`Primary output: .codecarto/${validation.primaryOutput}`,
|
|
173
|
+
`Validation: ${validation.overall}`,
|
|
174
|
+
]).slice(-3),
|
|
175
|
+
outputs_present: uniqueStrings([...existingPhase.outputs_present, validation.primaryOutput]),
|
|
176
|
+
open_questions: mergedOpenQuestions,
|
|
177
|
+
carry_forward: existingPhase.carry_forward ?? [],
|
|
178
|
+
};
|
|
179
|
+
nextStatus.last_updated = completionTimestamp;
|
|
180
|
+
const updatedWorkspaceState = {
|
|
181
|
+
...lockedState,
|
|
182
|
+
status: nextStatus,
|
|
183
|
+
};
|
|
184
|
+
const nextEligible = getNextEligiblePhase(updatedWorkspaceState);
|
|
185
|
+
nextStatus.current_phase = nextEligible?.id ?? "complete";
|
|
186
|
+
nextStatus.next_actions = nextEligible
|
|
187
|
+
? [`Begin ${nextEligible.id} phase by producing ${nextEligible.primary_output ?? `findings/${nextEligible.id}/`}`]
|
|
188
|
+
: ["All phases complete. Review findings, open questions, and downstream implementation notes."];
|
|
189
|
+
return {
|
|
190
|
+
state: { ...updatedWorkspaceState, status: nextStatus },
|
|
191
|
+
threadLogEntry: buildThreadLogEntry(validation.phaseId, validation, completionTimestamp),
|
|
192
|
+
};
|
|
193
|
+
});
|
|
194
|
+
let closeoutNotice;
|
|
195
|
+
try {
|
|
196
|
+
const created = await ensureCloseoutStub(updatedState.workspaceDir, validation.phaseId, completionTimestamp);
|
|
197
|
+
if (created) {
|
|
198
|
+
closeoutNotice = `Closeout stub: .codecarto/closeouts/${closeoutFileName(dateOnly(completionTimestamp), validation.phaseId)} (fill it in)`;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
catch (error) {
|
|
202
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
203
|
+
closeoutNotice = `Closeout stub not created: ${message}`;
|
|
204
|
+
}
|
|
205
|
+
void writeDashboard(ctx.cwd, PACKAGE_VERSION);
|
|
206
|
+
return { updatedState, closeoutNotice };
|
|
207
|
+
}
|
|
208
|
+
export function decideAfterPhase(phaseStatus, phaseError, validation, strict) {
|
|
209
|
+
if (phaseStatus === "aborted")
|
|
210
|
+
return { action: "aborted" };
|
|
211
|
+
if (phaseStatus === "error") {
|
|
212
|
+
return { action: "stop", reason: phaseError ?? "Sub-agent errored.", error: phaseError };
|
|
213
|
+
}
|
|
214
|
+
if (!validation) {
|
|
215
|
+
return { action: "stop", reason: "Validation skipped (no result)." };
|
|
216
|
+
}
|
|
217
|
+
if (validation.overall === "FAIL" || validation.overall === "MISSING") {
|
|
218
|
+
return {
|
|
219
|
+
action: "stop",
|
|
220
|
+
reason: `Validation ${validation.overall} on ${validation.phaseId}.`,
|
|
221
|
+
validation: validation.overall,
|
|
222
|
+
validationSummary: buildValidationSummary(validation),
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
if (validation.overall === "PASS WITH GAPS" && strict) {
|
|
226
|
+
return {
|
|
227
|
+
action: "stop",
|
|
228
|
+
reason: `PASS WITH GAPS on ${validation.phaseId} (strict mode).`,
|
|
229
|
+
validation: validation.overall,
|
|
230
|
+
validationSummary: buildValidationSummary(validation),
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
return { action: "continue" };
|
|
234
|
+
}
|
|
235
|
+
export async function runAuto(ctx, pi, initialState, options) {
|
|
236
|
+
const startedAt = Date.now();
|
|
237
|
+
const phasesRun = [];
|
|
238
|
+
const totalTokens = { input: 0, output: 0, cacheWrite: 0 };
|
|
239
|
+
const totalPhases = initialState.pipeline.phase_order.length;
|
|
240
|
+
const config = await loadCodecartoConfig(initialState.workspaceDir);
|
|
241
|
+
const llmSteerEnabled = options.llmSteerOverride ?? config.orchestrator.llm_steer_next_phase;
|
|
242
|
+
let state = initialState;
|
|
243
|
+
while (true) {
|
|
244
|
+
if (options.signal?.aborted) {
|
|
245
|
+
return finish({
|
|
246
|
+
outcome: "aborted",
|
|
247
|
+
reason: "User aborted the auto run.",
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
const phase = getNextEligiblePhase(state);
|
|
251
|
+
if (!phase) {
|
|
252
|
+
return finish({
|
|
253
|
+
outcome: "complete",
|
|
254
|
+
reason: "Pipeline complete.",
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
if (isPhaseRunning(phase.id)) {
|
|
258
|
+
return finish({
|
|
259
|
+
outcome: "stopped",
|
|
260
|
+
reason: `Phase ${phase.id} is already running from a prior invocation.`,
|
|
261
|
+
stoppedAt: { phaseId: phase.id },
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
const phaseResult = await runSinglePhase(ctx, pi, state, phase, {
|
|
265
|
+
llmSteerEnabled,
|
|
266
|
+
signal: options.signal,
|
|
267
|
+
});
|
|
268
|
+
// Accumulate tokens whether the phase succeeded, was aborted, or errored.
|
|
269
|
+
totalTokens.input += phaseResult.activity.lifetimeUsage.input;
|
|
270
|
+
totalTokens.output += phaseResult.activity.lifetimeUsage.output;
|
|
271
|
+
totalTokens.cacheWrite += phaseResult.activity.lifetimeUsage.cacheWrite;
|
|
272
|
+
// Validate only when the phase actually completed. Aborts and errors
|
|
273
|
+
// short-circuit; decideAfterPhase handles all three outcomes.
|
|
274
|
+
let validation = null;
|
|
275
|
+
if (phaseResult.status === "completed") {
|
|
276
|
+
// State must be refreshed because the sub-agent may have written
|
|
277
|
+
// findings to disk that the validator reads.
|
|
278
|
+
const stateForValidation = (await getWorkspaceState(ctx.cwd)) ?? state;
|
|
279
|
+
validation = await validatePhaseOutput(stateForValidation, phase.id);
|
|
280
|
+
}
|
|
281
|
+
const decision = decideAfterPhase(phaseResult.status, phaseResult.error, validation, options.strict);
|
|
282
|
+
if (decision.action === "aborted") {
|
|
283
|
+
return finish({
|
|
284
|
+
outcome: "aborted",
|
|
285
|
+
reason: `Aborted during ${phase.id}.`,
|
|
286
|
+
stoppedAt: { phaseId: phase.id },
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
if (decision.action === "stop") {
|
|
290
|
+
return finish({
|
|
291
|
+
outcome: "stopped",
|
|
292
|
+
reason: decision.reason,
|
|
293
|
+
stoppedAt: { phaseId: phase.id, validation: decision.validation, error: decision.error },
|
|
294
|
+
validationSummary: decision.validationSummary,
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
// decision.action === "continue" → auto-complete and loop.
|
|
298
|
+
// validation is guaranteed non-null on the continue branch.
|
|
299
|
+
try {
|
|
300
|
+
const { updatedState } = await autoCompletePhase(ctx, validation);
|
|
301
|
+
state = updatedState;
|
|
302
|
+
phasesRun.push(phase.id);
|
|
303
|
+
}
|
|
304
|
+
catch (err) {
|
|
305
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
306
|
+
return finish({
|
|
307
|
+
outcome: "stopped",
|
|
308
|
+
reason: `Auto-complete failed on ${phase.id}: ${message}`,
|
|
309
|
+
stoppedAt: { phaseId: phase.id, error: message },
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
function finish(partial) {
|
|
314
|
+
return {
|
|
315
|
+
...partial,
|
|
316
|
+
phasesRun,
|
|
317
|
+
totalPhases,
|
|
318
|
+
startedAt,
|
|
319
|
+
endedAt: Date.now(),
|
|
320
|
+
totalTokens,
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
// ----------------------------------------------------------------------------
|
|
325
|
+
// buildAutoSummary — the codecarto-auto-summary message body
|
|
326
|
+
// ----------------------------------------------------------------------------
|
|
327
|
+
export function buildAutoSummary(result, availableSkills = []) {
|
|
328
|
+
const totalTokens = result.totalTokens.input + result.totalTokens.output;
|
|
329
|
+
const wallTime = formatMillis(result.endedAt - result.startedAt);
|
|
330
|
+
const tokensStr = formatTokenCount(totalTokens);
|
|
331
|
+
const ranOf = `${result.phasesRun.length}/${result.totalPhases} phase${result.totalPhases === 1 ? "" : "s"}`;
|
|
332
|
+
const header = (() => {
|
|
333
|
+
switch (result.outcome) {
|
|
334
|
+
case "complete":
|
|
335
|
+
return `**Auto pipeline complete.**`;
|
|
336
|
+
case "stopped":
|
|
337
|
+
return `**Auto pipeline stopped at \`${result.stoppedAt?.phaseId ?? "?"}\`.**`;
|
|
338
|
+
case "aborted":
|
|
339
|
+
return `**Auto pipeline aborted${result.stoppedAt?.phaseId ? ` during \`${result.stoppedAt.phaseId}\`` : ""}.**`;
|
|
340
|
+
}
|
|
341
|
+
})();
|
|
342
|
+
const statsLine = `_⟳ ${ranOf} · ${tokensStr} tokens · ${wallTime}_`;
|
|
343
|
+
const lines = [header, "", statsLine];
|
|
344
|
+
if (result.outcome === "stopped" && result.validationSummary && result.validationSummary.length > 0) {
|
|
345
|
+
lines.push("", "```");
|
|
346
|
+
lines.push(...result.validationSummary);
|
|
347
|
+
lines.push("```");
|
|
348
|
+
}
|
|
349
|
+
if (result.outcome === "stopped" || result.outcome === "aborted") {
|
|
350
|
+
lines.push("", result.reason);
|
|
351
|
+
lines.push("", recoveryHint(result));
|
|
352
|
+
}
|
|
353
|
+
if (result.outcome === "complete") {
|
|
354
|
+
lines.push("", "Dashboard: `.codecarto/dashboard.html`");
|
|
355
|
+
if (availableSkills.length > 0) {
|
|
356
|
+
lines.push(`Next: try \`/codecarto-skill ${availableSkills[0]}\` (also available: ${availableSkills.slice(1).join(", ") || "none"}).`);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
return lines.join("\n");
|
|
360
|
+
}
|
|
361
|
+
function recoveryHint(result) {
|
|
362
|
+
if (result.outcome === "aborted") {
|
|
363
|
+
return "Run `/codecarto-next --auto` to resume.";
|
|
364
|
+
}
|
|
365
|
+
const v = result.stoppedAt?.validation;
|
|
366
|
+
if (v === "FAIL" || v === "MISSING") {
|
|
367
|
+
return `Fix the phase output, then \`/codecarto-next --auto\` to resume.`;
|
|
368
|
+
}
|
|
369
|
+
if (v === "PASS WITH GAPS") {
|
|
370
|
+
return `Review gaps via \`/codecarto-validate\`, then \`/codecarto-complete ${result.stoppedAt?.phaseId ?? "<phase>"}\`, then \`/codecarto-next --auto\`.`;
|
|
371
|
+
}
|
|
372
|
+
if (result.stoppedAt?.error) {
|
|
373
|
+
return `Re-run \`/codecarto-next\` (single-step) on \`${result.stoppedAt.phaseId}\` to retry once the issue is resolved.`;
|
|
374
|
+
}
|
|
375
|
+
return "Run `/codecarto-next --auto` to resume.";
|
|
376
|
+
}
|
|
377
|
+
// ----------------------------------------------------------------------------
|
|
378
|
+
// Helpers
|
|
379
|
+
// ----------------------------------------------------------------------------
|
|
380
|
+
async function recordUsage(workspaceDir, phaseId, status, activity) {
|
|
381
|
+
try {
|
|
382
|
+
await appendUsageRun(workspaceDir, {
|
|
383
|
+
timestamp: new Date().toISOString(),
|
|
384
|
+
phase: phaseId,
|
|
385
|
+
status,
|
|
386
|
+
turn_count: activity.turnCount,
|
|
387
|
+
tool_uses: activity.toolUses,
|
|
388
|
+
duration_ms: (activity.completedAt ?? Date.now()) - activity.startedAt,
|
|
389
|
+
tokens: {
|
|
390
|
+
input: activity.lifetimeUsage.input,
|
|
391
|
+
output: activity.lifetimeUsage.output,
|
|
392
|
+
cache_write: activity.lifetimeUsage.cacheWrite,
|
|
393
|
+
},
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
catch {
|
|
397
|
+
// Best-effort, matches the original recordUsage discipline.
|
|
398
|
+
}
|
|
399
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// Flag parser for /codecarto-dashboard. The user invokes the slash command
|
|
2
|
+
// with an optional "--narrate" flag to trigger the opt-in LLM narrator.
|
|
3
|
+
// Same shape and discipline as next-flags.ts so future flags slot in
|
|
4
|
+
// without refactoring.
|
|
5
|
+
const KNOWN = new Set(["--narrate"]);
|
|
6
|
+
export function parseDashboardFlags(args) {
|
|
7
|
+
const tokens = args.trim().split(/\s+/).filter((t) => t.length > 0);
|
|
8
|
+
const result = { narrate: false, unknown: [] };
|
|
9
|
+
for (const t of tokens) {
|
|
10
|
+
if (t === "--narrate")
|
|
11
|
+
result.narrate = true;
|
|
12
|
+
else
|
|
13
|
+
result.unknown.push(t);
|
|
14
|
+
}
|
|
15
|
+
return result;
|
|
16
|
+
}
|
|
17
|
+
export const KNOWN_DASHBOARD_FLAGS = [...KNOWN];
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { type WorkspaceState } from "../../core/index.ts";
|
|
3
|
+
export interface NarrateDashboardResult {
|
|
4
|
+
narration?: string;
|
|
5
|
+
used: boolean;
|
|
6
|
+
skipReason?: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function narrateDashboard(ctx: ExtensionContext, state: WorkspaceState): Promise<NarrateDashboardResult>;
|