codecartographer-pi 0.18.0 → 0.19.1
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 +7 -0
- package/.codecarto/broadside/SKILL.md +7 -0
- package/.codecarto/broadside/config.yaml +2 -2
- package/.codecarto/templates/phase-handoff.yaml +20 -1
- package/.codecarto/workflow/scaffold-version.yaml +1 -1
- package/agent-skill/codecartographer/references/handoff-contract.md +30 -2
- package/dist/core/broadside.d.ts +99 -4
- package/dist/core/broadside.js +284 -30
- package/dist/core/completion.d.ts +15 -0
- package/dist/core/completion.js +80 -3
- package/dist/core/coverage.d.ts +45 -0
- package/dist/core/coverage.js +131 -0
- package/dist/core/dashboard.d.ts +13 -0
- package/dist/core/dashboard.js +13 -1
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.js +1 -0
- package/dist/core/prompts.js +19 -1
- package/dist/core/status.d.ts +8 -1
- package/dist/core/status.js +37 -2
- package/dist/core/types.d.ts +24 -1
- package/dist/core/yaml.js +27 -7
- package/dist/extensions/codecarto/auto-runner.d.ts +1 -1
- package/dist/extensions/codecarto/auto-runner.js +11 -4
- package/dist/extensions/codecarto/index.js +70 -23
- package/dist/mcp-server/server.js +42 -8
- package/package.json +1 -1
|
@@ -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,7 @@ export default function codeCartographerExtension(pi) {
|
|
|
738
785
|
ctx.ui.notify(`Cannot complete ${validation.phaseId}: ${validation.overall}`, "error");
|
|
739
786
|
return;
|
|
740
787
|
}
|
|
741
|
-
const { updatedState, closeoutNotice, warnings } = await autoCompletePhase(ctx, validation);
|
|
788
|
+
const { updatedState, closeoutNotice, warnings } = await autoCompletePhase(ctx.cwd, validation);
|
|
742
789
|
lastFeedbackLines = [
|
|
743
790
|
`Completed phase: ${validation.phaseId}`,
|
|
744
791
|
`Validation: ${validation.overall}`,
|
|
@@ -884,7 +931,7 @@ export default function codeCartographerExtension(pi) {
|
|
|
884
931
|
}
|
|
885
932
|
lastFeedbackLines = [`Queued the CodeCartographer guide: ${document.topic}`];
|
|
886
933
|
if (codecartoModeActive)
|
|
887
|
-
void refreshWorkspaceUi(ctx, lastFeedbackLines);
|
|
934
|
+
void refreshWorkspaceUi(ctx, lastFeedbackLines).catch(() => undefined);
|
|
888
935
|
ctx.ui.notify(`Queued the CodeCartographer guide (${document.topic})`, "info");
|
|
889
936
|
},
|
|
890
937
|
});
|
|
@@ -917,7 +964,7 @@ export default function codeCartographerExtension(pi) {
|
|
|
917
964
|
// A workspace session already has a widget; fold the result into it.
|
|
918
965
|
if (ctx.hasUI)
|
|
919
966
|
ctx.ui.setWidget(BROADSIDE_WIDGET_ID, undefined);
|
|
920
|
-
void refreshWorkspaceUi(ctx, lines);
|
|
967
|
+
void refreshWorkspaceUi(ctx, lines).catch(() => undefined);
|
|
921
968
|
}
|
|
922
969
|
else if (ctx.hasUI) {
|
|
923
970
|
// 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.
|
|
3
|
+
"version": "0.19.1",
|
|
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",
|