codecartographer-pi 0.19.0 → 0.19.2
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/broadside/SKILL.md +18 -0
- package/.codecarto/broadside/config.yaml +43 -4
- package/.codecarto/workflow/scaffold-version.yaml +1 -1
- package/dist/core/broadside.d.ts +144 -5
- package/dist/core/broadside.js +339 -32
- package/dist/core/dashboard.d.ts +13 -0
- package/dist/core/dashboard.js +13 -1
- package/dist/core/yaml.js +27 -7
- package/dist/extensions/codecarto/agent-rewriter.js +2 -0
- package/dist/extensions/codecarto/agent-runner.js +2 -0
- package/dist/extensions/codecarto/auto-runner.d.ts +1 -1
- package/dist/extensions/codecarto/auto-runner.js +11 -4
- package/dist/extensions/codecarto/child-model-runtime.d.ts +2 -0
- package/dist/extensions/codecarto/child-model-runtime.js +44 -0
- package/dist/extensions/codecarto/dashboard-narrator.js +2 -0
- package/dist/extensions/codecarto/index.js +88 -23
- package/dist/mcp-server/server.js +42 -8
- package/package.json +1 -1
package/dist/core/yaml.js
CHANGED
|
@@ -126,6 +126,19 @@ export function parseSimpleYaml(raw) {
|
|
|
126
126
|
};
|
|
127
127
|
const parseMapping = (indent) => {
|
|
128
128
|
const result = {};
|
|
129
|
+
// Duplicate detection tracks keys explicitly rather than testing
|
|
130
|
+
// `key in result`. A bare object inherits from Object.prototype, so
|
|
131
|
+
// `"constructor" in result` is already true before anything is parsed —
|
|
132
|
+
// a document whose key is `constructor`, `toString`, `valueOf` or any
|
|
133
|
+
// other prototype member was rejected as a duplicate on first sight.
|
|
134
|
+
const seen = new Set();
|
|
135
|
+
// Assignment goes through defineProperty for the same reason: plain
|
|
136
|
+
// `result[key] = value` with the key `__proto__` invokes the prototype
|
|
137
|
+
// setter instead of creating an entry, so a hand-edited YAML file could
|
|
138
|
+
// change the shape of every object in the process rather than parse.
|
|
139
|
+
const assign = (key, value) => {
|
|
140
|
+
Object.defineProperty(result, key, { value, writable: true, enumerable: true, configurable: true });
|
|
141
|
+
};
|
|
129
142
|
while (index < lines.length) {
|
|
130
143
|
skipBlank();
|
|
131
144
|
if (index >= lines.length)
|
|
@@ -147,9 +160,10 @@ export function parseSimpleYaml(raw) {
|
|
|
147
160
|
const key = trimmed.slice(0, separator).trim();
|
|
148
161
|
const rawValue = trimmed.slice(separator + 1).trim();
|
|
149
162
|
index++;
|
|
150
|
-
if (key
|
|
163
|
+
if (seen.has(key)) {
|
|
151
164
|
throw new Error(`Duplicate YAML key: ${key} near line: ${line.trim()}`);
|
|
152
165
|
}
|
|
166
|
+
seen.add(key);
|
|
153
167
|
if (rawValue === "|" || rawValue === "|-") {
|
|
154
168
|
const blockLines = [];
|
|
155
169
|
let contentIndent = null;
|
|
@@ -168,19 +182,19 @@ export function parseSimpleYaml(raw) {
|
|
|
168
182
|
index++;
|
|
169
183
|
}
|
|
170
184
|
const content = blockLines.join("\n").replace(/\n+$/, "");
|
|
171
|
-
|
|
185
|
+
assign(key, rawValue === "|" ? `${content}\n` : content);
|
|
172
186
|
continue;
|
|
173
187
|
}
|
|
174
188
|
if (rawValue !== "") {
|
|
175
|
-
|
|
189
|
+
assign(key, parseYamlScalar(rawValue));
|
|
176
190
|
continue;
|
|
177
191
|
}
|
|
178
192
|
skipBlank();
|
|
179
193
|
if (index < lines.length && countIndent(lines[index] ?? "") > indent) {
|
|
180
|
-
|
|
194
|
+
assign(key, parseBlock(countIndent(lines[index] ?? "")));
|
|
181
195
|
}
|
|
182
196
|
else {
|
|
183
|
-
|
|
197
|
+
assign(key, null);
|
|
184
198
|
}
|
|
185
199
|
}
|
|
186
200
|
return result;
|
|
@@ -198,7 +212,13 @@ export function parseSimpleYaml(raw) {
|
|
|
198
212
|
const trimmed = line.slice(lineIndent);
|
|
199
213
|
if (lineIndent !== indent || (!trimmed.startsWith("- ") && trimmed !== "-"))
|
|
200
214
|
break;
|
|
201
|
-
const
|
|
215
|
+
const afterDash = trimmed === "-" ? "" : trimmed.slice(2);
|
|
216
|
+
const rawItem = afterDash.trim();
|
|
217
|
+
// A sequence item's mapping continues at the column where its own
|
|
218
|
+
// content starts, which is not always two past the dash: `- id: x`
|
|
219
|
+
// aligns its siblings under the `i`, four columns in. Hardcoding two
|
|
220
|
+
// rejected that valid layout as bad indentation.
|
|
221
|
+
const itemIndent = indent + 2 + (afterDash.length - afterDash.trimStart().length);
|
|
202
222
|
index++;
|
|
203
223
|
if (rawItem === "") {
|
|
204
224
|
skipBlank();
|
|
@@ -221,7 +241,7 @@ export function parseSimpleYaml(raw) {
|
|
|
221
241
|
item[key] = parseBlock(countIndent(lines[index] ?? ""));
|
|
222
242
|
}
|
|
223
243
|
if (index < lines.length && countIndent(lines[index] ?? "") > indent) {
|
|
224
|
-
const nested = parseMapping(
|
|
244
|
+
const nested = parseMapping(itemIndent);
|
|
225
245
|
for (const [nestedKey, nestedValue] of Object.entries(nested))
|
|
226
246
|
item[nestedKey] = nestedValue;
|
|
227
247
|
}
|
|
@@ -11,6 +11,7 @@ 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
13
|
import { closeoutFileName, pathExists } from "../../core/index.js";
|
|
14
|
+
import { createChildModelRuntime } from "./child-model-runtime.js";
|
|
14
15
|
/** Closeout content over this many bytes is truncated before being passed to
|
|
15
16
|
* the rewriter. Keeps the orchestrator-side cost predictable. */
|
|
16
17
|
const CLOSEOUT_BYTE_BUDGET = 8000;
|
|
@@ -143,6 +144,7 @@ async function runRewriterOnce(ctx, prompt) {
|
|
|
143
144
|
const { session } = await createAgentSession({
|
|
144
145
|
cwd,
|
|
145
146
|
agentDir,
|
|
147
|
+
modelRuntime: await createChildModelRuntime(ctx, agentDir),
|
|
146
148
|
sessionManager: SessionManager.inMemory(cwd),
|
|
147
149
|
settingsManager: SettingsManager.create(cwd, agentDir),
|
|
148
150
|
model: ctx.model,
|
|
@@ -13,6 +13,7 @@ import { access } from "node:fs/promises";
|
|
|
13
13
|
import { join, resolve } from "node:path";
|
|
14
14
|
import { createAgentSession, DefaultResourceLoader, getAgentDir, SessionManager, SettingsManager, } from "@earendil-works/pi-coding-agent";
|
|
15
15
|
import { canonicalPath, isWithinPath } from "../../core/index.js";
|
|
16
|
+
import { createChildModelRuntime } from "./child-model-runtime.js";
|
|
16
17
|
import { phaseCompactionExtension } from "./phase-compaction.js";
|
|
17
18
|
// Tools available to the phase sub-agent. Matches the codecarto interception
|
|
18
19
|
// allowlist (SAFE_TOOL_NAMES in extensions/codecarto/index.ts), minus bash.
|
|
@@ -104,6 +105,7 @@ export async function runPhase(ctx, prompt, callbacks = {}, options = {}, signal
|
|
|
104
105
|
const { session } = await createAgentSession({
|
|
105
106
|
cwd,
|
|
106
107
|
agentDir,
|
|
108
|
+
modelRuntime: await createChildModelRuntime(ctx, agentDir),
|
|
107
109
|
sessionManager,
|
|
108
110
|
settingsManager: SettingsManager.create(cwd, agentDir),
|
|
109
111
|
model: ctx.model,
|
|
@@ -44,7 +44,7 @@ export interface AutoCompleteResult {
|
|
|
44
44
|
/** Non-gating closure-integrity notes from completion (#122). */
|
|
45
45
|
warnings: string[];
|
|
46
46
|
}
|
|
47
|
-
export declare function autoCompletePhase(
|
|
47
|
+
export declare function autoCompletePhase(cwd: string, validation: ValidationResult): Promise<AutoCompleteResult>;
|
|
48
48
|
export type AutoOutcome = "complete" | "stopped" | "aborted";
|
|
49
49
|
export interface AutoRunOptions {
|
|
50
50
|
strict: boolean;
|
|
@@ -151,9 +151,13 @@ export function isPhaseRunning(phaseId) {
|
|
|
151
151
|
const existing = getPhaseActivity(phaseId);
|
|
152
152
|
return existing?.status === "running";
|
|
153
153
|
}
|
|
154
|
-
export async function autoCompletePhase(
|
|
155
|
-
|
|
156
|
-
|
|
154
|
+
export async function autoCompletePhase(
|
|
155
|
+
// A directory, not a ctx: this only ever needed `ctx.cwd`, and its callers
|
|
156
|
+
// run after a sub-agent has invalidated the ctx they captured, where every
|
|
157
|
+
// property access throws.
|
|
158
|
+
cwd, validation) {
|
|
159
|
+
const result = await completeValidatedPhase(cwd, validation, "/codecarto-complete");
|
|
160
|
+
void writeDashboard(cwd, PACKAGE_VERSION);
|
|
157
161
|
return result;
|
|
158
162
|
}
|
|
159
163
|
export function decideAfterPhase(phaseStatus, phaseError, validation, strict) {
|
|
@@ -185,6 +189,9 @@ export function decideAfterPhase(phaseStatus, phaseError, validation, strict) {
|
|
|
185
189
|
}
|
|
186
190
|
export async function runAuto(ctx, pi, initialState, options) {
|
|
187
191
|
const startedAt = Date.now();
|
|
192
|
+
// Captured once: the loop below spawns a sub-agent per phase, and each one
|
|
193
|
+
// invalidates this ctx, after which reading `ctx.cwd` throws.
|
|
194
|
+
const autoCwd = ctx.cwd;
|
|
188
195
|
const phasesRun = [];
|
|
189
196
|
const totalTokens = { input: 0, output: 0, cacheWrite: 0 };
|
|
190
197
|
const totalPhases = initialState.pipeline.phase_order.length;
|
|
@@ -262,7 +269,7 @@ export async function runAuto(ctx, pi, initialState, options) {
|
|
|
262
269
|
// decision.action === "continue" → auto-complete and loop.
|
|
263
270
|
// validation is guaranteed non-null on the continue branch.
|
|
264
271
|
try {
|
|
265
|
-
const { updatedState } = await autoCompletePhase(
|
|
272
|
+
const { updatedState } = await autoCompletePhase(autoCwd, validation);
|
|
266
273
|
state = updatedState;
|
|
267
274
|
phasesRun.push(phase.id);
|
|
268
275
|
options.onPhaseAdvanced?.(state);
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// Model runtime for codecarto's child sessions (phase sub-agents, the
|
|
2
|
+
// next-phase rewriter, the dashboard narrator).
|
|
3
|
+
//
|
|
4
|
+
// All three child sessions load with `noExtensions: true` so a globally
|
|
5
|
+
// installed codecarto doesn't register its commands and tool guards twice
|
|
6
|
+
// inside its own sub-agent. That isolation has a side effect: providers
|
|
7
|
+
// registered by *other* global extensions via `pi.registerProvider()` — an
|
|
8
|
+
// Ollama Cloud bridge, a company gateway, any custom `streamSimple` provider —
|
|
9
|
+
// are registered onto the parent's ModelRuntime by the resource loader that
|
|
10
|
+
// loaded them. A child that builds a fresh ModelRuntime never sees them, so
|
|
11
|
+
// the parent's selected model resolves to a provider the child does not know,
|
|
12
|
+
// and the session throws `No API key found for <provider>` before its first
|
|
13
|
+
// turn.
|
|
14
|
+
//
|
|
15
|
+
// Carrying the parent's registered provider configs across keeps the child on
|
|
16
|
+
// the same model the user picked without reloading (and re-registering) the
|
|
17
|
+
// extensions themselves.
|
|
18
|
+
import { join } from "node:path";
|
|
19
|
+
import { ModelRuntime } from "@earendil-works/pi-coding-agent";
|
|
20
|
+
export async function createChildModelRuntime(ctx, agentDir) {
|
|
21
|
+
const runtime = await ModelRuntime.create({
|
|
22
|
+
authPath: join(agentDir, "auth.json"),
|
|
23
|
+
modelsPath: join(agentDir, "models.json"),
|
|
24
|
+
});
|
|
25
|
+
for (const providerId of ctx.modelRegistry.getRegisteredProviderIds()) {
|
|
26
|
+
const config = ctx.modelRegistry.getRegisteredProviderConfig(providerId);
|
|
27
|
+
if (!config)
|
|
28
|
+
continue;
|
|
29
|
+
try {
|
|
30
|
+
runtime.registerProvider(providerId, config);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
// A provider the child can't accept is not worth failing the phase
|
|
34
|
+
// over: the child either doesn't need it (the user's model comes
|
|
35
|
+
// from a different provider) or fails later with the provider-
|
|
36
|
+
// specific error, which is more useful than one thrown here.
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
// Recompose the provider table so the newly registered providers land in
|
|
40
|
+
// the availability snapshot that `hasConfiguredAuth` reads. Offline: the
|
|
41
|
+
// parent already paid for any network catalog refresh.
|
|
42
|
+
await runtime.refresh({ allowNetwork: false });
|
|
43
|
+
return runtime;
|
|
44
|
+
}
|
|
@@ -14,6 +14,7 @@ import { readFile, readdir, rename, writeFile } 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
16
|
import { computeTotals, NARRATION_CACHE_RELATIVE_PATH, loadUsage, pathExists, stringifySimpleYaml, } from "../../core/index.js";
|
|
17
|
+
import { createChildModelRuntime } from "./child-model-runtime.js";
|
|
17
18
|
// Per-closeout byte budget when stuffing the narrator's input. Three
|
|
18
19
|
// closeouts × 4 KB each ≈ 12 KB of prompt context, which is well under any
|
|
19
20
|
// reasonable model's input window.
|
|
@@ -153,6 +154,7 @@ async function runNarratorOnce(ctx, prompt) {
|
|
|
153
154
|
const { session } = await createAgentSession({
|
|
154
155
|
cwd,
|
|
155
156
|
agentDir,
|
|
157
|
+
modelRuntime: await createChildModelRuntime(ctx, agentDir),
|
|
156
158
|
sessionManager: SessionManager.inMemory(cwd),
|
|
157
159
|
settingsManager: SettingsManager.create(cwd, agentDir),
|
|
158
160
|
model: ctx.model,
|
|
@@ -83,8 +83,42 @@ function buildStatusLines(state, extraLines = []) {
|
|
|
83
83
|
}
|
|
84
84
|
return lines;
|
|
85
85
|
}
|
|
86
|
+
/**
|
|
87
|
+
* Whether `ctx` still belongs to the live session.
|
|
88
|
+
*
|
|
89
|
+
* Pi invalidates an extension ctx when the session is replaced, and from then
|
|
90
|
+
* on *every* property access on it throws — `ctx.cwd` and `ctx.hasUI` included.
|
|
91
|
+
* A phase runs as a sub-agent, so by the time post-phase work fires, the ctx
|
|
92
|
+
* captured when the command started may already be dead. That is an ordinary
|
|
93
|
+
* outcome rather than an error: the UI it would have refreshed is gone with the
|
|
94
|
+
* session. Callers skip their UI work instead of throwing into a `void` call
|
|
95
|
+
* that nothing is waiting on.
|
|
96
|
+
*/
|
|
97
|
+
function isCtxLive(ctx) {
|
|
98
|
+
try {
|
|
99
|
+
return typeof ctx.cwd === "string";
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Notify through `ctx`, dropping the message if the session it belonged to is
|
|
107
|
+
* gone.
|
|
108
|
+
*
|
|
109
|
+
* `ctx.hasUI` throws on a stale ctx rather than returning false, so the usual
|
|
110
|
+
* `if (ctx.hasUI) ctx.ui.notify(...)` guard was itself a throw site. Inside a
|
|
111
|
+
* promise chain that was worse than a lost message: the `.catch` handler threw
|
|
112
|
+
* while reporting the original failure, and that second rejection had nothing
|
|
113
|
+
* left to catch it.
|
|
114
|
+
*/
|
|
115
|
+
function notifyCtx(ctx, message, level) {
|
|
116
|
+
if (!isCtxLive(ctx) || !ctx.hasUI)
|
|
117
|
+
return;
|
|
118
|
+
ctx.ui.notify(message, level);
|
|
119
|
+
}
|
|
86
120
|
function setUiState(ctx, state, extraLines = []) {
|
|
87
|
-
if (!ctx.hasUI)
|
|
121
|
+
if (!isCtxLive(ctx) || !ctx.hasUI)
|
|
88
122
|
return;
|
|
89
123
|
if (!state) {
|
|
90
124
|
ctx.ui.setStatus(STATUS_LINE_ID, undefined);
|
|
@@ -260,6 +294,11 @@ export default function codeCartographerExtension(pi) {
|
|
|
260
294
|
// remembered here for the completers that list files under .codecarto/.
|
|
261
295
|
let sessionCwd;
|
|
262
296
|
const readWorkspaceState = async (ctx, notifyOnError = true) => {
|
|
297
|
+
// `ctx.cwd` was read before the try, so a stale ctx made this reject
|
|
298
|
+
// rather than return null as its signature promises — and the callers
|
|
299
|
+
// that fire it without awaiting turned that into an unhandled rejection.
|
|
300
|
+
if (!isCtxLive(ctx))
|
|
301
|
+
return null;
|
|
263
302
|
sessionCwd = ctx.cwd;
|
|
264
303
|
try {
|
|
265
304
|
return await getWorkspaceState(ctx.cwd);
|
|
@@ -268,12 +307,16 @@ export default function codeCartographerExtension(pi) {
|
|
|
268
307
|
const message = error instanceof Error ? error.message : String(error);
|
|
269
308
|
lastFeedbackLines = [message];
|
|
270
309
|
setUiState(ctx, null);
|
|
271
|
-
|
|
310
|
+
// The ctx can die between the read above and here, so the error
|
|
311
|
+
// path must not assume it is still usable either.
|
|
312
|
+
if (notifyOnError && isCtxLive(ctx) && ctx.hasUI)
|
|
272
313
|
ctx.ui.notify(message, "error");
|
|
273
314
|
return null;
|
|
274
315
|
}
|
|
275
316
|
};
|
|
276
317
|
const refreshWorkspaceUi = async (ctx, extraLines) => {
|
|
318
|
+
if (!isCtxLive(ctx))
|
|
319
|
+
return null;
|
|
277
320
|
if (!codecartoModeActive) {
|
|
278
321
|
setUiState(ctx, null);
|
|
279
322
|
return null;
|
|
@@ -290,15 +333,19 @@ export default function codeCartographerExtension(pi) {
|
|
|
290
333
|
const ensureWorkspaceState = async (ctx) => {
|
|
291
334
|
if (!codecartoModeActive) {
|
|
292
335
|
setUiState(ctx, null);
|
|
293
|
-
ctx
|
|
336
|
+
notifyCtx(ctx, "CodeCartographer is not active in this session. Run /codecarto-init first.", "warning");
|
|
294
337
|
return null;
|
|
295
338
|
}
|
|
296
339
|
const state = await readWorkspaceState(ctx);
|
|
297
340
|
if (state)
|
|
298
341
|
return state;
|
|
342
|
+
// Reached when the ctx is stale as well as when there is no workspace,
|
|
343
|
+
// so neither `ctx.cwd` nor the notify below may assume a live ctx.
|
|
344
|
+
if (!isCtxLive(ctx))
|
|
345
|
+
return null;
|
|
299
346
|
const hasWorkspace = await pathExists(join(ctx.cwd, ".codecarto", "workflow", "status.yaml"));
|
|
300
347
|
if (!hasWorkspace)
|
|
301
|
-
ctx
|
|
348
|
+
notifyCtx(ctx, "No .codecarto/ workspace found. Run /codecarto-init first.", "warning");
|
|
302
349
|
return null;
|
|
303
350
|
};
|
|
304
351
|
pi.on("session_start", async (_event, ctx) => {
|
|
@@ -607,34 +654,36 @@ export default function codeCartographerExtension(pi) {
|
|
|
607
654
|
// phase so status.yaml advances without requiring the user to manually
|
|
608
655
|
// run /codecarto-validate then /codecarto-complete. This mirrors what
|
|
609
656
|
// the auto loop (runAuto) does after each phase.
|
|
657
|
+
// The sub-agent replaces the session, which invalidates this ctx —
|
|
658
|
+
// every later property access on it throws. Capture the directory
|
|
659
|
+
// now so the post-phase work below does not depend on the ctx
|
|
660
|
+
// surviving, and route UI updates through notifyCtx, which drops
|
|
661
|
+
// them if it has not.
|
|
662
|
+
const phaseCwd = ctx.cwd;
|
|
610
663
|
void runSinglePhase(ctx, pi, state, phase, { llmSteerEnabled, signal: ctx.signal, preflight })
|
|
611
664
|
.then(async (result) => {
|
|
612
665
|
if (result.status !== "completed")
|
|
613
666
|
return;
|
|
614
667
|
// Refresh state from disk — the sub-agent may have written
|
|
615
668
|
// findings that the validator needs to read.
|
|
616
|
-
const stateForValidation = (await getWorkspaceState(
|
|
669
|
+
const stateForValidation = (await getWorkspaceState(phaseCwd)) ?? state;
|
|
617
670
|
const validation = await validatePhaseOutput(stateForValidation, phase.id).catch((error) => (error instanceof Error ? error : new Error(String(error))));
|
|
618
671
|
if (validation instanceof Error) {
|
|
619
|
-
|
|
620
|
-
ctx.ui.notify(`Auto-validation error for ${phase.id}: ${validation.message}`, "warning");
|
|
672
|
+
notifyCtx(ctx, `Auto-validation error for ${phase.id}: ${validation.message}`, "warning");
|
|
621
673
|
lastFeedbackLines = [`Validation error: ${validation.message}`, "Run `/codecarto-validate` then `/codecarto-complete` manually."];
|
|
622
674
|
return;
|
|
623
675
|
}
|
|
624
676
|
if (validation.overall === "FAIL" || validation.overall === "MISSING") {
|
|
625
|
-
|
|
626
|
-
ctx.ui.notify(`Phase ${phase.id} validation: ${validation.overall}. Fix the output, then re-run /codecarto-next.`, "warning");
|
|
677
|
+
notifyCtx(ctx, `Phase ${phase.id} validation: ${validation.overall}. Fix the output, then re-run /codecarto-next.`, "warning");
|
|
627
678
|
lastFeedbackLines = buildValidationSummary(validation);
|
|
628
679
|
return;
|
|
629
680
|
}
|
|
630
681
|
// PASS or PASS WITH GAPS — auto-complete the phase.
|
|
631
682
|
try {
|
|
632
|
-
const { updatedState, closeoutNotice } = await autoCompletePhase(
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
ctx.ui.notify(closeoutNotice, "info");
|
|
637
|
-
}
|
|
683
|
+
const { updatedState, closeoutNotice } = await autoCompletePhase(phaseCwd, validation);
|
|
684
|
+
notifyCtx(ctx, `Phase ${phase.id} auto-completed (validation: ${validation.overall}).`, validation.overall === "PASS WITH GAPS" ? "warning" : "info");
|
|
685
|
+
if (closeoutNotice)
|
|
686
|
+
notifyCtx(ctx, closeoutNotice, "info");
|
|
638
687
|
lastFeedbackLines = [
|
|
639
688
|
`Completed phase: ${validation.phaseId}`,
|
|
640
689
|
`Validation: ${validation.overall}`,
|
|
@@ -645,22 +694,20 @@ export default function codeCartographerExtension(pi) {
|
|
|
645
694
|
}
|
|
646
695
|
catch (error) {
|
|
647
696
|
const message = error instanceof Error ? error.message : String(error);
|
|
648
|
-
|
|
649
|
-
ctx.ui.notify(`Auto-completion failed for ${phase.id}: ${message}. Run /codecarto-complete manually.`, "warning");
|
|
697
|
+
notifyCtx(ctx, `Auto-completion failed for ${phase.id}: ${message}. Run /codecarto-complete manually.`, "warning");
|
|
650
698
|
lastFeedbackLines = [`Auto-completion failed: ${message}`, "Run `/codecarto-complete` manually."];
|
|
651
699
|
}
|
|
652
700
|
})
|
|
653
701
|
.catch((error) => {
|
|
654
702
|
const message = error instanceof Error ? error.message : String(error);
|
|
655
|
-
|
|
656
|
-
ctx.ui.notify(`Post-phase processing error for ${phase.id}: ${message}`, "warning");
|
|
703
|
+
notifyCtx(ctx, `Post-phase processing error for ${phase.id}: ${message}`, "warning");
|
|
657
704
|
lastFeedbackLines = [`Post-phase error: ${message}`];
|
|
658
705
|
})
|
|
659
706
|
.finally(() => {
|
|
660
707
|
// Refresh the status widget after the phase resolves so the
|
|
661
708
|
// "Open questions / Carry-forward / Next" lines reflect any
|
|
662
709
|
// owner_notes the sub-agent wrote to status.yaml.
|
|
663
|
-
void refreshWorkspaceUi(ctx);
|
|
710
|
+
void refreshWorkspaceUi(ctx).catch(() => undefined);
|
|
664
711
|
});
|
|
665
712
|
},
|
|
666
713
|
});
|
|
@@ -738,7 +785,25 @@ export default function codeCartographerExtension(pi) {
|
|
|
738
785
|
ctx.ui.notify(`Cannot complete ${validation.phaseId}: ${validation.overall}`, "error");
|
|
739
786
|
return;
|
|
740
787
|
}
|
|
741
|
-
|
|
788
|
+
// Completion refuses for reasons the framework words carefully — a
|
|
789
|
+
// missing phase handoff, a carry-forward without `derives_from`, a
|
|
790
|
+
// closure lacking runtime evidence. Those messages are the whole
|
|
791
|
+
// point of the refusal, and this was the one call in this file that
|
|
792
|
+
// let them escape as a rejection instead of showing them. The
|
|
793
|
+
// irony was sharp: /codecarto-next catches this same throw and tells
|
|
794
|
+
// the user to run /codecarto-complete manually, which then threw.
|
|
795
|
+
let completion;
|
|
796
|
+
try {
|
|
797
|
+
completion = await autoCompletePhase(ctx.cwd, validation);
|
|
798
|
+
}
|
|
799
|
+
catch (error) {
|
|
800
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
801
|
+
lastFeedbackLines = [`Completion refused: ${message}`];
|
|
802
|
+
setUiState(ctx, currentState, lastFeedbackLines);
|
|
803
|
+
ctx.ui.notify(message, "error");
|
|
804
|
+
return;
|
|
805
|
+
}
|
|
806
|
+
const { updatedState, closeoutNotice, warnings } = completion;
|
|
742
807
|
lastFeedbackLines = [
|
|
743
808
|
`Completed phase: ${validation.phaseId}`,
|
|
744
809
|
`Validation: ${validation.overall}`,
|
|
@@ -884,7 +949,7 @@ export default function codeCartographerExtension(pi) {
|
|
|
884
949
|
}
|
|
885
950
|
lastFeedbackLines = [`Queued the CodeCartographer guide: ${document.topic}`];
|
|
886
951
|
if (codecartoModeActive)
|
|
887
|
-
void refreshWorkspaceUi(ctx, lastFeedbackLines);
|
|
952
|
+
void refreshWorkspaceUi(ctx, lastFeedbackLines).catch(() => undefined);
|
|
888
953
|
ctx.ui.notify(`Queued the CodeCartographer guide (${document.topic})`, "info");
|
|
889
954
|
},
|
|
890
955
|
});
|
|
@@ -917,7 +982,7 @@ export default function codeCartographerExtension(pi) {
|
|
|
917
982
|
// A workspace session already has a widget; fold the result into it.
|
|
918
983
|
if (ctx.hasUI)
|
|
919
984
|
ctx.ui.setWidget(BROADSIDE_WIDGET_ID, undefined);
|
|
920
|
-
void refreshWorkspaceUi(ctx, lines);
|
|
985
|
+
void refreshWorkspaceUi(ctx, lines).catch(() => undefined);
|
|
921
986
|
}
|
|
922
987
|
else if (ctx.hasUI) {
|
|
923
988
|
// Scout-only repository: the Broad-Side widget is the only place
|
|
@@ -24,6 +24,20 @@ import { initLibrary } from "../core/library.js";
|
|
|
24
24
|
import { loadUserConfig, resolveUserConfigPath } from "../core/orchestrator-config.js";
|
|
25
25
|
import { writeDashboard } from "../extensions/codecarto/dashboard-writer.js";
|
|
26
26
|
// ---------- input helpers ----------
|
|
27
|
+
/**
|
|
28
|
+
* Normalize an optional `phase` argument. A client can send any JSON, and
|
|
29
|
+
* `args.phase?.trim()` throws a bare TypeError on a number or an object —
|
|
30
|
+
* surfacing as an opaque InternalError rather than telling the caller which
|
|
31
|
+
* argument was wrong. Sibling handlers already guard the required case.
|
|
32
|
+
*/
|
|
33
|
+
function requireOptionalPhase(phase) {
|
|
34
|
+
if (phase === undefined || phase === null)
|
|
35
|
+
return undefined;
|
|
36
|
+
if (typeof phase !== "string") {
|
|
37
|
+
throw new McpError(ErrorCode.InvalidParams, `phase must be a string when provided, got ${typeof phase}`);
|
|
38
|
+
}
|
|
39
|
+
return phase.trim() || undefined;
|
|
40
|
+
}
|
|
27
41
|
async function validateCwd(cwd) {
|
|
28
42
|
if (typeof cwd !== "string" || !cwd.trim()) {
|
|
29
43
|
throw new McpError(ErrorCode.InvalidParams, "cwd is required");
|
|
@@ -148,7 +162,7 @@ export async function handleStatus(args) {
|
|
|
148
162
|
const currentPhase = nextPhase?.id ?? state.status.current_phase ?? "complete";
|
|
149
163
|
const completed = state.pipeline.phase_order.filter((id) => state.status.phases[id]?.status === "complete").length;
|
|
150
164
|
const totalCarryForward = Object.values(state.status.phases).reduce((sum, phase) => sum + (phase.carry_forward?.length ?? 0), 0);
|
|
151
|
-
const currentOpenQuestions = currentPhase === "complete" ? 0 : state.status.phases[currentPhase]?.open_questions
|
|
165
|
+
const currentOpenQuestions = currentPhase === "complete" ? 0 : state.status.phases[currentPhase]?.open_questions?.length ?? 0;
|
|
152
166
|
const terminalOpenQuestions = Object.values(state.status.phases).reduce((sum, phase) => sum + (phase.open_questions?.length ?? 0), 0);
|
|
153
167
|
const postPipelinePending = state.status.post_pipeline.filter((entry) => entry.status !== "resolved").length;
|
|
154
168
|
const scaffoldNotice = describeScaffoldStaleness(state);
|
|
@@ -237,7 +251,7 @@ export async function handlePhase(args) {
|
|
|
237
251
|
export async function handleValidate(args) {
|
|
238
252
|
const cwd = await validateCwd(args.cwd);
|
|
239
253
|
const state = await requireWorkspace(cwd);
|
|
240
|
-
const validation = await validatePhaseOutput(state, args.phase
|
|
254
|
+
const validation = await validatePhaseOutput(state, requireOptionalPhase(args.phase)).catch((error) => {
|
|
241
255
|
throw new McpError(ErrorCode.InvalidParams, error instanceof Error ? error.message : String(error));
|
|
242
256
|
});
|
|
243
257
|
const summary = buildValidationSummary(validation).join("\n");
|
|
@@ -256,7 +270,7 @@ export async function handleValidate(args) {
|
|
|
256
270
|
export async function handleComplete(args) {
|
|
257
271
|
const cwd = await validateCwd(args.cwd);
|
|
258
272
|
const initialState = await requireWorkspace(cwd);
|
|
259
|
-
const validation = await validatePhaseOutput(initialState, args.phase
|
|
273
|
+
const validation = await validatePhaseOutput(initialState, requireOptionalPhase(args.phase)).catch((error) => {
|
|
260
274
|
throw new McpError(ErrorCode.InvalidParams, error instanceof Error ? error.message : String(error));
|
|
261
275
|
});
|
|
262
276
|
if (validation.overall === "FAIL" || validation.overall === "MISSING") {
|
|
@@ -377,9 +391,17 @@ async function resolveLibraryPath(args) {
|
|
|
377
391
|
* the publish tool enforces.
|
|
378
392
|
*/
|
|
379
393
|
async function loadEffectiveConfig(cwd) {
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
394
|
+
if (typeof cwd !== "string" || cwd.trim() === "")
|
|
395
|
+
return loadUserConfig();
|
|
396
|
+
// A relative path here resolves against the server process's working
|
|
397
|
+
// directory, not the caller's, so it would quietly read some other
|
|
398
|
+
// workspace's config — and this config decides whether publish_confirm
|
|
399
|
+
// gates the write. Refuse rather than answer from the wrong file.
|
|
400
|
+
const trimmed = cwd.trim();
|
|
401
|
+
if (!isAbsolute(trimmed)) {
|
|
402
|
+
throw new McpError(ErrorCode.InvalidParams, `cwd must be an absolute path, got: ${trimmed}`);
|
|
403
|
+
}
|
|
404
|
+
return loadCodecartoConfig(join(trimmed, ".codecarto"));
|
|
383
405
|
}
|
|
384
406
|
function asStringArray(value, fieldName) {
|
|
385
407
|
if (!Array.isArray(value)) {
|
|
@@ -731,6 +753,13 @@ export async function handleLibraryInit(args) {
|
|
|
731
753
|
}
|
|
732
754
|
export async function handleVision(args) {
|
|
733
755
|
const cwd = await validateCwd(args.cwd);
|
|
756
|
+
// raw_text is interpolated straight into the returned prompt, so an absent
|
|
757
|
+
// value silently becomes the literal string "undefined" for the agent to
|
|
758
|
+
// synthesize a vision brief from. Every sibling handler validates its
|
|
759
|
+
// required string argument; this one did not.
|
|
760
|
+
if (typeof args.raw_text !== "string" || !args.raw_text.trim()) {
|
|
761
|
+
throw new McpError(ErrorCode.InvalidParams, "raw_text is required (the user's raw product description)");
|
|
762
|
+
}
|
|
734
763
|
const workspaceDir = join(cwd, ".codecarto");
|
|
735
764
|
const interviewPath = join(workspaceDir, "findings", "vision-capture", "INTERVIEW.md");
|
|
736
765
|
const visionPath = join(workspaceDir, "inputs", "vision.md");
|
|
@@ -757,8 +786,8 @@ export async function handleVision(args) {
|
|
|
757
786
|
});
|
|
758
787
|
}
|
|
759
788
|
export async function handleConfig(args) {
|
|
760
|
-
const config = args.cwd
|
|
761
|
-
? await loadCodecartoConfig(join(args.cwd, ".codecarto"))
|
|
789
|
+
const config = args.cwd !== undefined && args.cwd !== null
|
|
790
|
+
? await loadCodecartoConfig(join(await validateCwd(args.cwd), ".codecarto"))
|
|
762
791
|
: await loadUserConfig();
|
|
763
792
|
const userConfigPath = resolveUserConfigPath();
|
|
764
793
|
const workspaceConfigPath = args.cwd ? join(args.cwd, ".codecarto", "workflow", "config.yaml") : null;
|
|
@@ -976,6 +1005,11 @@ export async function handleBroadside(args) {
|
|
|
976
1005
|
includeTriage,
|
|
977
1006
|
retryTruncated,
|
|
978
1007
|
onStatus: (lensId, status, counts) => lines.push(` ${lensId}: ${status} (${counts.completed ?? 0}/${counts.total ?? "?"})`),
|
|
1008
|
+
}).catch((error) => {
|
|
1009
|
+
// The `collect` action normalizes this same call; without it here,
|
|
1010
|
+
// a failure during submit-with-wait reached the client as an
|
|
1011
|
+
// opaque InternalError instead of naming its cause.
|
|
1012
|
+
throw new McpError(ErrorCode.InvalidRequest, error instanceof Error ? error.message : String(error));
|
|
979
1013
|
});
|
|
980
1014
|
lines.push("", collectResultText(collect));
|
|
981
1015
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codecartographer-pi",
|
|
3
|
-
"version": "0.19.
|
|
3
|
+
"version": "0.19.2",
|
|
4
4
|
"mcpName": "io.github.HuginnIndustries/codecartographer",
|
|
5
5
|
"description": "Turn an unfamiliar codebase into a validated reimplementation spec, then synthesize confirmed specs and a product vision into a traceable plan.",
|
|
6
6
|
"type": "module",
|