codecartographer-pi 0.19.5 → 0.20.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 +1 -1
- package/.codecarto/broadside/SKILL.md +14 -0
- package/.codecarto/broadside/config.yaml +18 -0
- package/.codecarto/templates/gitignore +55 -0
- package/.codecarto/workflow/VALIDATE.md +2 -1
- package/.codecarto/workflow/scaffold-version.yaml +1 -1
- package/README.md +10 -6
- package/dist/core/amendment.js +28 -23
- package/dist/core/broadside.d.ts +72 -2
- package/dist/core/broadside.js +351 -68
- package/dist/core/completion.js +95 -26
- package/dist/core/dashboard-writer.d.ts +8 -0
- package/dist/core/dashboard-writer.js +159 -0
- package/dist/core/index.d.ts +2 -0
- package/dist/core/index.js +2 -0
- package/dist/core/library.js +115 -107
- package/dist/core/orchestrator-config.d.ts +32 -7
- package/dist/core/orchestrator-config.js +124 -44
- package/dist/core/pipeline.d.ts +37 -0
- package/dist/core/pipeline.js +80 -10
- package/dist/core/prompts.d.ts +20 -0
- package/dist/core/prompts.js +43 -10
- package/dist/core/secrets.d.ts +16 -0
- package/dist/core/secrets.js +98 -0
- package/dist/core/status.d.ts +30 -2
- package/dist/core/status.js +54 -8
- package/dist/core/synthesis.js +5 -2
- package/dist/core/usage.d.ts +8 -0
- package/dist/core/usage.js +35 -7
- package/dist/core/utils.d.ts +32 -5
- package/dist/core/utils.js +81 -19
- package/dist/core/workspace.d.ts +99 -18
- package/dist/core/workspace.js +275 -36
- package/dist/core/yaml.js +173 -14
- package/dist/extensions/codecarto/agent-rewriter.js +21 -14
- package/dist/extensions/codecarto/agent-runner.d.ts +6 -2
- package/dist/extensions/codecarto/agent-runner.js +27 -9
- package/dist/extensions/codecarto/agent-state.d.ts +0 -2
- package/dist/extensions/codecarto/auto-runner.js +10 -6
- package/dist/extensions/codecarto/dashboard-narrator.js +12 -8
- package/dist/extensions/codecarto/dashboard-writer.d.ts +1 -8
- package/dist/extensions/codecarto/dashboard-writer.js +5 -157
- package/dist/extensions/codecarto/index.js +73 -21
- package/dist/extensions/codecarto/phase-compaction.js +7 -7
- package/dist/mcp-server/server.js +111 -50
- package/package.json +3 -2
package/dist/core/yaml.js
CHANGED
|
@@ -71,6 +71,55 @@ function findKeySeparator(text) {
|
|
|
71
71
|
}
|
|
72
72
|
return -1;
|
|
73
73
|
}
|
|
74
|
+
/**
|
|
75
|
+
* Whether a line's content is a mapping entry in YAML's sense: a key followed
|
|
76
|
+
* by `:` and then a space or the end of the line. `findKeySeparator` alone
|
|
77
|
+
* finds any bare colon, which would read a wrapped value such as
|
|
78
|
+
* `see https://example.com` as a key.
|
|
79
|
+
*/
|
|
80
|
+
function looksLikeMappingEntry(text) {
|
|
81
|
+
const separator = findKeySeparator(text);
|
|
82
|
+
if (separator === -1)
|
|
83
|
+
return false;
|
|
84
|
+
const next = text[separator + 1];
|
|
85
|
+
return next === undefined || next === " " || next === "\t";
|
|
86
|
+
}
|
|
87
|
+
function looksLikeSequenceItem(text) {
|
|
88
|
+
return text.startsWith("- ") || text === "-";
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Whether the scalar text is plain: unquoted, and not a block indicator. Only
|
|
92
|
+
* a plain scalar can continue onto more-indented lines.
|
|
93
|
+
*/
|
|
94
|
+
function isPlainScalarText(text) {
|
|
95
|
+
const trimmed = text.trim();
|
|
96
|
+
if (trimmed === "")
|
|
97
|
+
return false;
|
|
98
|
+
if (trimmed.startsWith('"') || trimmed.startsWith("'"))
|
|
99
|
+
return false;
|
|
100
|
+
if (trimmed.startsWith("[") || trimmed.startsWith("{"))
|
|
101
|
+
return false;
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Fold a plain scalar's first line with its continuation lines: a single line
|
|
106
|
+
* break becomes a space and a run of k blank lines becomes k newlines, as for
|
|
107
|
+
* a folded block scalar.
|
|
108
|
+
*/
|
|
109
|
+
function foldPlainScalar(first, continuation) {
|
|
110
|
+
let result = first;
|
|
111
|
+
let pendingBreaks = 0;
|
|
112
|
+
for (const line of continuation) {
|
|
113
|
+
if (line === "") {
|
|
114
|
+
pendingBreaks++;
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
result += pendingBreaks > 0 ? "\n".repeat(pendingBreaks) : " ";
|
|
118
|
+
pendingBreaks = 0;
|
|
119
|
+
result += line;
|
|
120
|
+
}
|
|
121
|
+
return result;
|
|
122
|
+
}
|
|
74
123
|
export function parseYamlScalar(rawValue) {
|
|
75
124
|
const trimmed = stripYamlComment(rawValue).trim();
|
|
76
125
|
if (trimmed === "")
|
|
@@ -163,10 +212,70 @@ function applyBlockScalar(blockLines, header) {
|
|
|
163
212
|
export function parseSimpleYaml(raw) {
|
|
164
213
|
const lines = raw.split(/\r?\n/);
|
|
165
214
|
let index = 0;
|
|
215
|
+
/**
|
|
216
|
+
* Every parse error names the line it failed on, quotes it, and says which
|
|
217
|
+
* construct was expected: a model writing a handoff has to be able to tell
|
|
218
|
+
* a parser limitation from its own mistake (#246).
|
|
219
|
+
*/
|
|
220
|
+
const fail = (at, message) => {
|
|
221
|
+
throw new Error(`YAML line ${at + 1}: ${message} — "${(lines[at] ?? "").trim()}"`);
|
|
222
|
+
};
|
|
223
|
+
// YAML indents with spaces only. A tab was counted as two columns and then
|
|
224
|
+
// sliced as one character, so `\tkey: v` parsed as the key "" — garbage
|
|
225
|
+
// rather than an error.
|
|
226
|
+
for (let at = 0; at < lines.length; at++) {
|
|
227
|
+
if (/^ *\t/.test(lines[at] ?? "") && !isBlankOrComment(lines[at] ?? "")) {
|
|
228
|
+
fail(at, "tabs are not allowed in YAML indentation; use spaces");
|
|
229
|
+
}
|
|
230
|
+
}
|
|
166
231
|
const skipBlank = () => {
|
|
167
232
|
while (index < lines.length && isBlankOrComment(lines[index] ?? ""))
|
|
168
233
|
index++;
|
|
169
234
|
};
|
|
235
|
+
/**
|
|
236
|
+
* Consume the continuation lines of a plain scalar that began on the line
|
|
237
|
+
* just consumed: lines indented deeper than `baseIndent` that are neither
|
|
238
|
+
* a mapping entry nor a sequence item nor a comment. A wrapped
|
|
239
|
+
* `closeout_summary:` is exactly this shape, and it used to fail the
|
|
240
|
+
* indentation check with a message that blamed whitespace (#246, probe D).
|
|
241
|
+
* Blank lines inside the run are kept (they fold to newlines); trailing
|
|
242
|
+
* blank lines are left for the caller.
|
|
243
|
+
*/
|
|
244
|
+
const collectPlainContinuation = (baseIndent) => {
|
|
245
|
+
const collected = [];
|
|
246
|
+
let pendingBlank = 0;
|
|
247
|
+
let cursor = index;
|
|
248
|
+
while (cursor < lines.length) {
|
|
249
|
+
const candidate = lines[cursor] ?? "";
|
|
250
|
+
if (candidate.trim() === "") {
|
|
251
|
+
pendingBlank++;
|
|
252
|
+
cursor++;
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
const candidateIndent = countIndent(candidate);
|
|
256
|
+
if (candidateIndent <= baseIndent)
|
|
257
|
+
break;
|
|
258
|
+
const text = candidate.slice(candidateIndent);
|
|
259
|
+
if (text.startsWith("#") || looksLikeSequenceItem(text) || looksLikeMappingEntry(text))
|
|
260
|
+
break;
|
|
261
|
+
for (let i = 0; i < pendingBlank; i++)
|
|
262
|
+
collected.push("");
|
|
263
|
+
pendingBlank = 0;
|
|
264
|
+
collected.push(stripYamlComment(text).trim());
|
|
265
|
+
cursor++;
|
|
266
|
+
index = cursor;
|
|
267
|
+
}
|
|
268
|
+
return collected;
|
|
269
|
+
};
|
|
270
|
+
/** A scalar value read from `rawValue`, folded with any continuation lines. */
|
|
271
|
+
const parseScalarWithContinuation = (rawValue, baseIndent) => {
|
|
272
|
+
if (!isPlainScalarText(rawValue))
|
|
273
|
+
return parseYamlScalar(rawValue);
|
|
274
|
+
const continuation = collectPlainContinuation(baseIndent);
|
|
275
|
+
if (continuation.length === 0)
|
|
276
|
+
return parseYamlScalar(rawValue);
|
|
277
|
+
return foldPlainScalar(stripYamlComment(rawValue).trim(), continuation);
|
|
278
|
+
};
|
|
170
279
|
/**
|
|
171
280
|
* Read the body of a block scalar that opened on the line just consumed.
|
|
172
281
|
* Shared by mapping values (`key: >-`) and sequence items (`- >-`): when only
|
|
@@ -228,34 +337,58 @@ export function parseSimpleYaml(raw) {
|
|
|
228
337
|
if (lineIndent < indent)
|
|
229
338
|
break;
|
|
230
339
|
if (lineIndent > indent) {
|
|
231
|
-
|
|
340
|
+
fail(index, `this line is indented ${lineIndent} columns but the mapping it belongs to starts at column ${indent}; a sibling key must align with the first key, and a wrapped value must not contain ": "`);
|
|
232
341
|
}
|
|
233
342
|
const trimmed = line.slice(indent);
|
|
234
|
-
if (trimmed
|
|
235
|
-
|
|
343
|
+
if (looksLikeSequenceItem(trimmed)) {
|
|
344
|
+
// A list that is a key's value is consumed by that key's branch
|
|
345
|
+
// below before the loop ever sees it, so a dash here has no key.
|
|
346
|
+
fail(index, "a sequence item where a mapping entry was expected; a list that belongs to the key above must be indented under it, or sit at that key's own column");
|
|
347
|
+
}
|
|
236
348
|
const separator = findKeySeparator(trimmed);
|
|
237
349
|
if (separator === -1) {
|
|
238
|
-
|
|
350
|
+
fail(index, 'expected a mapping entry ("key: value")');
|
|
239
351
|
}
|
|
240
352
|
const key = trimmed.slice(0, separator).trim();
|
|
241
353
|
const rawValue = trimmed.slice(separator + 1).trim();
|
|
242
|
-
index++;
|
|
243
354
|
if (seen.has(key)) {
|
|
244
|
-
|
|
355
|
+
fail(index, `Duplicate YAML key: ${key}`);
|
|
245
356
|
}
|
|
246
357
|
seen.add(key);
|
|
358
|
+
index++;
|
|
247
359
|
const blockHeader = parseBlockScalarHeader(rawValue);
|
|
248
360
|
if (blockHeader) {
|
|
249
361
|
assign(key, applyBlockScalar(collectBlockScalarLines(indent), blockHeader));
|
|
250
362
|
continue;
|
|
251
363
|
}
|
|
252
364
|
if (rawValue !== "") {
|
|
253
|
-
assign(key,
|
|
365
|
+
assign(key, parseScalarWithContinuation(rawValue, indent));
|
|
254
366
|
continue;
|
|
255
367
|
}
|
|
256
368
|
skipBlank();
|
|
257
|
-
if (index
|
|
258
|
-
assign(key,
|
|
369
|
+
if (index >= lines.length) {
|
|
370
|
+
assign(key, null);
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
373
|
+
const nextLine = lines[index] ?? "";
|
|
374
|
+
const nextIndent = countIndent(nextLine);
|
|
375
|
+
if (nextIndent > indent) {
|
|
376
|
+
const nextText = nextLine.slice(nextIndent);
|
|
377
|
+
if (looksLikeMappingEntry(nextText) || looksLikeSequenceItem(nextText)) {
|
|
378
|
+
assign(key, parseBlock(nextIndent));
|
|
379
|
+
}
|
|
380
|
+
else {
|
|
381
|
+
// A scalar that starts on the line after its key
|
|
382
|
+
// (`summary:` / ` The phase mapped…`), wrapped or not.
|
|
383
|
+
index++;
|
|
384
|
+
assign(key, parseScalarWithContinuation(nextText, indent));
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
else if (nextIndent === indent && looksLikeSequenceItem(nextLine.slice(nextIndent))) {
|
|
388
|
+
// YAML lets a block sequence sit at the same indent as the key it
|
|
389
|
+
// belongs to (`items:` / `- a`). This read as `items: null` and
|
|
390
|
+
// then, at the top level, dropped every line after it (#246).
|
|
391
|
+
assign(key, parseSequence(indent));
|
|
259
392
|
}
|
|
260
393
|
else {
|
|
261
394
|
assign(key, null);
|
|
@@ -286,8 +419,17 @@ export function parseSimpleYaml(raw) {
|
|
|
286
419
|
index++;
|
|
287
420
|
if (rawItem === "") {
|
|
288
421
|
skipBlank();
|
|
289
|
-
|
|
290
|
-
|
|
422
|
+
const nextLine = lines[index] ?? "";
|
|
423
|
+
const nextIndent = countIndent(nextLine);
|
|
424
|
+
if (index < lines.length && nextIndent > indent) {
|
|
425
|
+
const nextText = nextLine.slice(nextIndent);
|
|
426
|
+
if (looksLikeMappingEntry(nextText) || looksLikeSequenceItem(nextText)) {
|
|
427
|
+
result.push(parseBlock(nextIndent));
|
|
428
|
+
}
|
|
429
|
+
else {
|
|
430
|
+
index++;
|
|
431
|
+
result.push(parseScalarWithContinuation(nextText, indent));
|
|
432
|
+
}
|
|
291
433
|
}
|
|
292
434
|
else {
|
|
293
435
|
result.push(null);
|
|
@@ -299,12 +441,21 @@ export function parseSimpleYaml(raw) {
|
|
|
299
441
|
result.push(applyBlockScalar(collectBlockScalarLines(indent), itemBlockHeader));
|
|
300
442
|
continue;
|
|
301
443
|
}
|
|
444
|
+
if (looksLikeSequenceItem(rawItem)) {
|
|
445
|
+
// A sequence item that is itself a sequence (`- - a`). Rewrite the
|
|
446
|
+
// line as the inner item at its own column and parse a block there,
|
|
447
|
+
// so the inner sequence's later items (` - b`) find their first.
|
|
448
|
+
index--;
|
|
449
|
+
lines[index] = `${" ".repeat(itemIndent)}${afterDash.trimStart()}`;
|
|
450
|
+
result.push(parseBlock(itemIndent));
|
|
451
|
+
continue;
|
|
452
|
+
}
|
|
302
453
|
const separator = findKeySeparator(rawItem);
|
|
303
454
|
if (separator !== -1) {
|
|
304
455
|
const key = rawItem.slice(0, separator).trim();
|
|
305
456
|
const rawValue = rawItem.slice(separator + 1).trim();
|
|
306
457
|
const item = {};
|
|
307
|
-
item[key] = rawValue === "" ? null :
|
|
458
|
+
item[key] = rawValue === "" ? null : parseScalarWithContinuation(rawValue, indent);
|
|
308
459
|
skipBlank();
|
|
309
460
|
if (rawValue === "" && index < lines.length && countIndent(lines[index] ?? "") > indent + 1) {
|
|
310
461
|
item[key] = parseBlock(countIndent(lines[index] ?? ""));
|
|
@@ -317,14 +468,22 @@ export function parseSimpleYaml(raw) {
|
|
|
317
468
|
result.push(item);
|
|
318
469
|
continue;
|
|
319
470
|
}
|
|
320
|
-
result.push(
|
|
471
|
+
result.push(parseScalarWithContinuation(rawItem, indent));
|
|
321
472
|
}
|
|
322
473
|
return result;
|
|
323
474
|
};
|
|
324
475
|
skipBlank();
|
|
325
476
|
if (index >= lines.length)
|
|
326
477
|
return {};
|
|
327
|
-
|
|
478
|
+
const document = parseBlock(countIndent(lines[index] ?? ""));
|
|
479
|
+
// A top-level block that ends before the input does used to leave the rest
|
|
480
|
+
// unread and unreported — a mapping followed by a stray `- item` silently
|
|
481
|
+
// lost everything from that line on (#246).
|
|
482
|
+
skipBlank();
|
|
483
|
+
if (index < lines.length) {
|
|
484
|
+
fail(index, "unexpected content after the document's top-level block ended (is this line indented like its neighbours?)");
|
|
485
|
+
}
|
|
486
|
+
return document;
|
|
328
487
|
}
|
|
329
488
|
export function formatYamlScalar(value) {
|
|
330
489
|
if (value === null)
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import { readFile, readdir } from "node:fs/promises";
|
|
11
11
|
import { join } from "node:path";
|
|
12
12
|
import { createAgentSession, DefaultResourceLoader, getAgentDir, SessionManager, SettingsManager, } from "@earendil-works/pi-coding-agent";
|
|
13
|
+
import { disposeChildSession } from "./agent-runner.js";
|
|
13
14
|
import { closeoutFileName, pathExists } from "../../core/index.js";
|
|
14
15
|
import { createChildModelRuntime } from "./child-model-runtime.js";
|
|
15
16
|
/** Closeout content over this many bytes is truncated before being passed to
|
|
@@ -151,20 +152,26 @@ async function runRewriterOnce(ctx, prompt) {
|
|
|
151
152
|
tools: [],
|
|
152
153
|
resourceLoader: loader,
|
|
153
154
|
});
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
155
|
+
try {
|
|
156
|
+
await session.prompt(prompt);
|
|
157
|
+
for (let i = session.messages.length - 1; i >= 0; i--) {
|
|
158
|
+
const msg = session.messages[i];
|
|
159
|
+
if (msg.role !== "assistant")
|
|
160
|
+
continue;
|
|
161
|
+
const blocks = msg.content;
|
|
162
|
+
const parts = [];
|
|
163
|
+
for (const c of blocks) {
|
|
164
|
+
if (c.type === "text" && c.text)
|
|
165
|
+
parts.push(c.text);
|
|
166
|
+
}
|
|
167
|
+
const joined = parts.join("\n").trim();
|
|
168
|
+
if (joined)
|
|
169
|
+
return joined;
|
|
164
170
|
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
171
|
+
return "";
|
|
172
|
+
}
|
|
173
|
+
finally {
|
|
174
|
+
// One prompt, one answer; the child has nothing left to do (#256).
|
|
175
|
+
disposeChildSession(session);
|
|
168
176
|
}
|
|
169
|
-
return "";
|
|
170
177
|
}
|
|
@@ -11,7 +11,6 @@ export declare function primaryOutputExists(cwd: string, primaryOutput: string):
|
|
|
11
11
|
export declare function buildPhaseContinuationPrompt(compacted: boolean): string;
|
|
12
12
|
export declare function waitForCompaction(compactionCompleted: Promise<boolean>, timeoutMs?: number): Promise<boolean>;
|
|
13
13
|
export interface PhaseRunCallbacks {
|
|
14
|
-
onSessionCreated?: (session: AgentSession) => void;
|
|
15
14
|
onToolStart?: (toolCallId: string, toolName: string) => void;
|
|
16
15
|
onToolEnd?: (toolCallId: string, toolName: string) => void;
|
|
17
16
|
onTextDelta?: (delta: string, fullText: string) => void;
|
|
@@ -37,7 +36,6 @@ export interface PhaseRunOptions {
|
|
|
37
36
|
primaryOutput?: string;
|
|
38
37
|
}
|
|
39
38
|
export interface PhaseRunResult {
|
|
40
|
-
session: AgentSession;
|
|
41
39
|
responseText: string;
|
|
42
40
|
toolUses: number;
|
|
43
41
|
turnCount: number;
|
|
@@ -61,3 +59,9 @@ export interface PhaseRunResult {
|
|
|
61
59
|
* shows lineage.
|
|
62
60
|
*/
|
|
63
61
|
export declare function runPhase(ctx: ExtensionContext, prompt: string, callbacks?: PhaseRunCallbacks, options?: PhaseRunOptions, signal?: AbortSignal): Promise<PhaseRunResult>;
|
|
62
|
+
/**
|
|
63
|
+
* Dispose a child session, swallowing whatever dispose throws: the work is
|
|
64
|
+
* done and its result is already in hand, so a failing cleanup hook must not
|
|
65
|
+
* turn a finished phase into an error.
|
|
66
|
+
*/
|
|
67
|
+
export declare function disposeChildSession(session: Pick<AgentSession, "dispose">): void;
|
|
@@ -113,7 +113,6 @@ export async function runPhase(ctx, prompt, callbacks = {}, options = {}, signal
|
|
|
113
113
|
resourceLoader: loader,
|
|
114
114
|
});
|
|
115
115
|
await session.bindExtensions({});
|
|
116
|
-
callbacks.onSessionCreated?.(session);
|
|
117
116
|
let toolUses = 0;
|
|
118
117
|
let turnCount = 0;
|
|
119
118
|
let currentMessageText = "";
|
|
@@ -200,19 +199,38 @@ export async function runPhase(ctx, prompt, callbacks = {}, options = {}, signal
|
|
|
200
199
|
if (!aborted)
|
|
201
200
|
await session.prompt(buildPhaseContinuationPrompt(compacted));
|
|
202
201
|
}
|
|
202
|
+
return {
|
|
203
|
+
responseText: getLastAssistantText(session) || currentMessageText,
|
|
204
|
+
toolUses,
|
|
205
|
+
turnCount,
|
|
206
|
+
aborted,
|
|
207
|
+
sessionFile: sessionManager.getSessionFile(),
|
|
208
|
+
};
|
|
203
209
|
}
|
|
204
210
|
finally {
|
|
205
211
|
unsubscribe();
|
|
206
212
|
abortCleanup();
|
|
213
|
+
// The child is done, on every path. Dispose aborts whatever it still
|
|
214
|
+
// has in flight, drops its agent subscription and listeners, and runs
|
|
215
|
+
// the per-session resource cleanups extensions registered — a seven
|
|
216
|
+
// phase auto run used to keep all of that for every phase, rewrite,
|
|
217
|
+
// and narration until the process exited (#256). Nothing reads the
|
|
218
|
+
// session after this: the result carries the text and the file path.
|
|
219
|
+
disposeChildSession(session);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Dispose a child session, swallowing whatever dispose throws: the work is
|
|
224
|
+
* done and its result is already in hand, so a failing cleanup hook must not
|
|
225
|
+
* turn a finished phase into an error.
|
|
226
|
+
*/
|
|
227
|
+
export function disposeChildSession(session) {
|
|
228
|
+
try {
|
|
229
|
+
session.dispose();
|
|
230
|
+
}
|
|
231
|
+
catch {
|
|
232
|
+
// nothing to do with a cleanup failure but move on
|
|
207
233
|
}
|
|
208
|
-
return {
|
|
209
|
-
session,
|
|
210
|
-
responseText: getLastAssistantText(session) || currentMessageText,
|
|
211
|
-
toolUses,
|
|
212
|
-
turnCount,
|
|
213
|
-
aborted,
|
|
214
|
-
sessionFile: sessionManager.getSessionFile(),
|
|
215
|
-
};
|
|
216
234
|
}
|
|
217
235
|
/**
|
|
218
236
|
* Walk session.messages backward to find the last non-empty assistant text.
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { AgentSession } from "@earendil-works/pi-coding-agent";
|
|
2
1
|
import { type CompactionTelemetry } from "../../core/usage.ts";
|
|
3
2
|
export type PhaseStatus = "running" | "completed" | "error" | "aborted";
|
|
4
3
|
export interface PhaseActivity {
|
|
@@ -20,7 +19,6 @@ export interface PhaseActivity {
|
|
|
20
19
|
};
|
|
21
20
|
/** Compaction outcomes observed during this phase session. */
|
|
22
21
|
compactions: CompactionTelemetry;
|
|
23
|
-
session?: AgentSession;
|
|
24
22
|
error?: string;
|
|
25
23
|
}
|
|
26
24
|
export declare function getPhaseActivity(phaseId: string): PhaseActivity | undefined;
|
|
@@ -17,8 +17,7 @@ import { clearPhase, finishPhase, getPhaseActivity, startPhase } from "./agent-s
|
|
|
17
17
|
import { buildSteeringMessage, rewritePhasePrompt } from "./agent-rewriter.js";
|
|
18
18
|
import { buildPhaseSummary } from "./agent-summary.js";
|
|
19
19
|
import { getAgentsWidget } from "./agent-widget.js";
|
|
20
|
-
import { writeDashboard } from "
|
|
21
|
-
import { appendUsageRun, buildPhasePrompt, buildValidationSummary, completeValidatedPhase, formatMillis, formatTokenCount, getNextEligiblePhase, getWorkspaceState, loadCodecartoConfig, PACKAGE_VERSION, PhasePreflightError, runPhasePreflight, validatePhaseOutput, } from "../../core/index.js";
|
|
20
|
+
import { appendUsageRun, buildPhasePrompt, buildValidationSummary, completeValidatedPhase, formatMillis, formatTokenCount, getNextEligiblePhase, getWorkspaceState, describeConfigProblems, loadCodecartoConfig, PACKAGE_VERSION, PhasePreflightError, runPhasePreflight, validatePhaseOutput, writeDashboard, } from "../../core/index.js";
|
|
22
21
|
/**
|
|
23
22
|
* Run one phase end to end: optional LLM-steered rewrite, spawn the sub-agent,
|
|
24
23
|
* wait for it, then emit the side effects the historical /codecarto-next chain
|
|
@@ -60,7 +59,6 @@ export async function runSinglePhase(ctx, pi, state, phase, options) {
|
|
|
60
59
|
getAgentsWidget().attach(ctx.ui);
|
|
61
60
|
try {
|
|
62
61
|
const result = await runPhase(ctx, prompt, {
|
|
63
|
-
onSessionCreated: (session) => { activity.session = session; },
|
|
64
62
|
onToolStart: (id, name) => { activity.activeTools.set(id, name); activity.toolUses++; },
|
|
65
63
|
onToolEnd: (id) => { activity.activeTools.delete(id); },
|
|
66
64
|
onTextDelta: (_delta, fullText) => { activity.responseText = fullText; },
|
|
@@ -101,7 +99,9 @@ export async function runSinglePhase(ctx, pi, state, phase, options) {
|
|
|
101
99
|
display: true,
|
|
102
100
|
});
|
|
103
101
|
void recordUsage(state.workspaceDir, phase.id, status, activity, result.sessionFile);
|
|
104
|
-
|
|
102
|
+
// The phase's sub-agent replaced the session, so reading ctx.cwd here
|
|
103
|
+
// would throw (#201); the state captured before the run has the root.
|
|
104
|
+
void writeDashboard(state.cwd, PACKAGE_VERSION);
|
|
105
105
|
return {
|
|
106
106
|
status: result.aborted ? "aborted" : "completed",
|
|
107
107
|
activity,
|
|
@@ -128,7 +128,7 @@ export async function runSinglePhase(ctx, pi, state, phase, options) {
|
|
|
128
128
|
display: true,
|
|
129
129
|
});
|
|
130
130
|
void recordUsage(state.workspaceDir, phase.id, "error", activity);
|
|
131
|
-
void writeDashboard(
|
|
131
|
+
void writeDashboard(state.cwd, PACKAGE_VERSION);
|
|
132
132
|
return { status: "error", activity, error: message };
|
|
133
133
|
}
|
|
134
134
|
finally {
|
|
@@ -190,6 +190,10 @@ export async function runAuto(ctx, pi, initialState, options) {
|
|
|
190
190
|
const totalTokens = { input: 0, output: 0, cacheWrite: 0 };
|
|
191
191
|
const totalPhases = initialState.pipeline.phase_order.length;
|
|
192
192
|
const config = await loadCodecartoConfig(initialState.workspaceDir);
|
|
193
|
+
// Once, before the loop: a dropped config file changes what this run does
|
|
194
|
+
// (the steer toggle lives there) and nothing else in the loop reads it.
|
|
195
|
+
if (config.problems.length > 0)
|
|
196
|
+
notifyCtx(ctx, describeConfigProblems(config).join("\n"), "warning");
|
|
193
197
|
const llmSteerEnabled = options.llmSteerOverride ?? config.orchestrator.llm_steer_next_phase;
|
|
194
198
|
let state = initialState;
|
|
195
199
|
while (true) {
|
|
@@ -241,7 +245,7 @@ export async function runAuto(ctx, pi, initialState, options) {
|
|
|
241
245
|
if (phaseResult.status === "completed") {
|
|
242
246
|
// State must be refreshed because the sub-agent may have written
|
|
243
247
|
// findings to disk that the validator reads.
|
|
244
|
-
const stateForValidation = (await getWorkspaceState(
|
|
248
|
+
const stateForValidation = (await getWorkspaceState(autoCwd)) ?? state;
|
|
245
249
|
validation = await validatePhaseOutput(stateForValidation, phase.id);
|
|
246
250
|
}
|
|
247
251
|
const decision = decideAfterPhase(phaseResult.status, phaseResult.error, validation, options.strict);
|
|
@@ -10,10 +10,11 @@
|
|
|
10
10
|
//
|
|
11
11
|
// Same one-shot pattern as agent-rewriter.ts:runRewriterOnce — different
|
|
12
12
|
// system prompt, different output target. Never throws.
|
|
13
|
-
import { readFile, readdir
|
|
13
|
+
import { readFile, readdir } from "node:fs/promises";
|
|
14
14
|
import { join } from "node:path";
|
|
15
15
|
import { createAgentSession, DefaultResourceLoader, getAgentDir, SessionManager, SettingsManager, } from "@earendil-works/pi-coding-agent";
|
|
16
|
-
import {
|
|
16
|
+
import { disposeChildSession } from "./agent-runner.js";
|
|
17
|
+
import { atomicWriteFile, computeTotals, NARRATION_CACHE_RELATIVE_PATH, loadUsage, pathExists, stringifySimpleYaml, } from "../../core/index.js";
|
|
17
18
|
import { createChildModelRuntime } from "./child-model-runtime.js";
|
|
18
19
|
// Per-closeout byte budget when stuffing the narrator's input. Three
|
|
19
20
|
// closeouts × 4 KB each ≈ 12 KB of prompt context, which is well under any
|
|
@@ -133,10 +134,7 @@ async function writeNarrationCache(workspaceDir, content, phaseCount) {
|
|
|
133
134
|
const generatedAt = new Date().toISOString();
|
|
134
135
|
const frontmatter = stringifySimpleYaml({ generatedAt, phaseCountAtGeneration: phaseCount });
|
|
135
136
|
const body = `---\n${frontmatter}\n---\n${content}\n`;
|
|
136
|
-
|
|
137
|
-
const tempPath = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
138
|
-
await writeFile(tempPath, body, "utf8");
|
|
139
|
-
await rename(tempPath, path);
|
|
137
|
+
await atomicWriteFile(join(workspaceDir, NARRATION_CACHE_RELATIVE_PATH), body);
|
|
140
138
|
}
|
|
141
139
|
async function runNarratorOnce(ctx, prompt) {
|
|
142
140
|
const cwd = ctx.cwd;
|
|
@@ -161,8 +159,14 @@ async function runNarratorOnce(ctx, prompt) {
|
|
|
161
159
|
tools: [],
|
|
162
160
|
resourceLoader: loader,
|
|
163
161
|
});
|
|
164
|
-
|
|
165
|
-
|
|
162
|
+
try {
|
|
163
|
+
await session.prompt(prompt);
|
|
164
|
+
return getLastAssistantText(session);
|
|
165
|
+
}
|
|
166
|
+
finally {
|
|
167
|
+
// One prompt, one answer; the child has nothing left to do (#256).
|
|
168
|
+
disposeChildSession(session);
|
|
169
|
+
}
|
|
166
170
|
}
|
|
167
171
|
function getLastAssistantText(session) {
|
|
168
172
|
for (let i = session.messages.length - 1; i >= 0; i--) {
|
|
@@ -1,8 +1 @@
|
|
|
1
|
-
|
|
2
|
-
* Render and atomically replace `.codecarto/dashboard.html`.
|
|
3
|
-
* @returns true when a fresh dashboard landed on disk; false when the
|
|
4
|
-
* workspace is missing or any gather/render/write step failed (swallowed —
|
|
5
|
-
* lifecycle callers must never fail on a dashboard problem, but they may
|
|
6
|
-
* report truthfully whether a refresh happened).
|
|
7
|
-
*/
|
|
8
|
-
export declare function writeDashboard(cwd: string, packageVersion: string): Promise<boolean>;
|
|
1
|
+
export { writeDashboard } from "../../core/dashboard-writer.ts";
|