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
|
@@ -1,157 +1,5 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
|
|
6
|
-
import { join } from "node:path";
|
|
7
|
-
import { DASHBOARD_RELATIVE_PATH, getWorkspaceState, loadUsage, NARRATION_CACHE_RELATIVE_PATH, parseSimpleYaml, pathExists, renderDashboard, } from "../../core/index.js";
|
|
8
|
-
const CLOSEOUT_FILENAME_RE = /^(\d{4}-\d{2}-\d{2})-(.+)\.md$/;
|
|
9
|
-
/**
|
|
10
|
-
* Render and atomically replace `.codecarto/dashboard.html`.
|
|
11
|
-
* @returns true when a fresh dashboard landed on disk; false when the
|
|
12
|
-
* workspace is missing or any gather/render/write step failed (swallowed —
|
|
13
|
-
* lifecycle callers must never fail on a dashboard problem, but they may
|
|
14
|
-
* report truthfully whether a refresh happened).
|
|
15
|
-
*/
|
|
16
|
-
export async function writeDashboard(cwd, packageVersion) {
|
|
17
|
-
try {
|
|
18
|
-
const state = await getWorkspaceState(cwd);
|
|
19
|
-
if (!state)
|
|
20
|
-
return false;
|
|
21
|
-
const workspaceDir = state.workspaceDir;
|
|
22
|
-
const [usage, closeouts, outputsPresent, narration] = await Promise.all([
|
|
23
|
-
loadUsage(workspaceDir),
|
|
24
|
-
listCloseouts(workspaceDir),
|
|
25
|
-
buildOutputsPresent(state.workspaceDir, state.pipeline),
|
|
26
|
-
loadNarration(workspaceDir),
|
|
27
|
-
]);
|
|
28
|
-
const inputs = {
|
|
29
|
-
status: state.status,
|
|
30
|
-
pipeline: state.pipeline,
|
|
31
|
-
usage,
|
|
32
|
-
closeouts,
|
|
33
|
-
outputsPresent,
|
|
34
|
-
packageVersion,
|
|
35
|
-
generatedAt: new Date().toISOString(),
|
|
36
|
-
narration,
|
|
37
|
-
};
|
|
38
|
-
const html = renderDashboard(inputs);
|
|
39
|
-
const path = join(workspaceDir, DASHBOARD_RELATIVE_PATH);
|
|
40
|
-
const tempPath = `${path}.${process.pid}.${Date.now()}.tmp`;
|
|
41
|
-
await writeFile(tempPath, html, "utf8");
|
|
42
|
-
await rename(tempPath, path);
|
|
43
|
-
return true;
|
|
44
|
-
}
|
|
45
|
-
catch {
|
|
46
|
-
// Best-effort: a failed dashboard write must not surface as a phase
|
|
47
|
-
// error. The user's pipeline state is unaffected; the next state
|
|
48
|
-
// change will trigger another render attempt.
|
|
49
|
-
return false;
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
async function listCloseouts(workspaceDir) {
|
|
53
|
-
const dir = join(workspaceDir, "closeouts");
|
|
54
|
-
if (!(await pathExists(dir)))
|
|
55
|
-
return [];
|
|
56
|
-
let entries;
|
|
57
|
-
try {
|
|
58
|
-
entries = await readdir(dir);
|
|
59
|
-
}
|
|
60
|
-
catch {
|
|
61
|
-
return [];
|
|
62
|
-
}
|
|
63
|
-
const out = [];
|
|
64
|
-
for (const name of entries) {
|
|
65
|
-
const m = CLOSEOUT_FILENAME_RE.exec(name);
|
|
66
|
-
if (!m)
|
|
67
|
-
continue;
|
|
68
|
-
out.push({ date: m[1], phaseOrModule: m[2], fileName: name, summary: await readCloseoutSummary(join(dir, name)) });
|
|
69
|
-
}
|
|
70
|
-
return out;
|
|
71
|
-
}
|
|
72
|
-
async function readCloseoutSummary(path) {
|
|
73
|
-
try {
|
|
74
|
-
const raw = await readFile(path, "utf8");
|
|
75
|
-
const lines = raw.split(/\r?\n/);
|
|
76
|
-
const summaryStart = lines.findIndex((line) => /^##\s+Summary\s*$/i.test(line.trim()));
|
|
77
|
-
if (summaryStart === -1)
|
|
78
|
-
return undefined;
|
|
79
|
-
const body = [];
|
|
80
|
-
for (const line of lines.slice(summaryStart + 1)) {
|
|
81
|
-
if (/^##\s+/.test(line.trim()))
|
|
82
|
-
break;
|
|
83
|
-
const trimmed = line.trim();
|
|
84
|
-
if (!trimmed || trimmed === "-")
|
|
85
|
-
continue;
|
|
86
|
-
body.push(trimmed.replace(/^[-*]\s+/, ""));
|
|
87
|
-
if (body.join(" ").length > 280)
|
|
88
|
-
break;
|
|
89
|
-
}
|
|
90
|
-
const summary = body.join(" ").trim();
|
|
91
|
-
return summary ? `${summary.slice(0, 280)}${summary.length > 280 ? "…" : ""}` : undefined;
|
|
92
|
-
}
|
|
93
|
-
catch {
|
|
94
|
-
return undefined;
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
async function buildOutputsPresent(workspaceDir, pipeline) {
|
|
98
|
-
const out = new Map();
|
|
99
|
-
for (const phaseId of pipeline.phase_order) {
|
|
100
|
-
const phaseDef = pipeline.phases.find((p) => p.id === phaseId);
|
|
101
|
-
if (!phaseDef)
|
|
102
|
-
continue;
|
|
103
|
-
const entry = { secondary: [] };
|
|
104
|
-
if (phaseDef.primary_output) {
|
|
105
|
-
entry.primary = {
|
|
106
|
-
path: phaseDef.primary_output,
|
|
107
|
-
exists: await pathExists(join(workspaceDir, phaseDef.primary_output)),
|
|
108
|
-
};
|
|
109
|
-
}
|
|
110
|
-
for (const sec of phaseDef.secondary_outputs ?? []) {
|
|
111
|
-
entry.secondary.push({
|
|
112
|
-
path: sec.path,
|
|
113
|
-
exists: await pathExists(join(workspaceDir, sec.path)),
|
|
114
|
-
});
|
|
115
|
-
}
|
|
116
|
-
out.set(phaseId, entry);
|
|
117
|
-
}
|
|
118
|
-
return out;
|
|
119
|
-
}
|
|
120
|
-
async function loadNarration(workspaceDir) {
|
|
121
|
-
const path = join(workspaceDir, NARRATION_CACHE_RELATIVE_PATH);
|
|
122
|
-
if (!(await pathExists(path)))
|
|
123
|
-
return undefined;
|
|
124
|
-
try {
|
|
125
|
-
const raw = await readFile(path, "utf8");
|
|
126
|
-
const { frontmatter, body } = splitFrontmatter(raw);
|
|
127
|
-
if (!frontmatter)
|
|
128
|
-
return undefined;
|
|
129
|
-
const generatedAt = typeof frontmatter.generatedAt === "string" ? frontmatter.generatedAt : "";
|
|
130
|
-
const phaseCountAtGeneration = typeof frontmatter.phaseCountAtGeneration === "number" ? frontmatter.phaseCountAtGeneration : 0;
|
|
131
|
-
if (!generatedAt)
|
|
132
|
-
return undefined;
|
|
133
|
-
return { content: body.trim(), generatedAt, phaseCountAtGeneration };
|
|
134
|
-
}
|
|
135
|
-
catch {
|
|
136
|
-
return undefined;
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
function splitFrontmatter(raw) {
|
|
140
|
-
if (!raw.startsWith("---\n"))
|
|
141
|
-
return { frontmatter: null, body: raw };
|
|
142
|
-
const end = raw.indexOf("\n---\n", 4);
|
|
143
|
-
if (end === -1)
|
|
144
|
-
return { frontmatter: null, body: raw };
|
|
145
|
-
const yamlText = raw.slice(4, end);
|
|
146
|
-
const body = raw.slice(end + 5);
|
|
147
|
-
try {
|
|
148
|
-
const parsed = parseSimpleYaml(yamlText);
|
|
149
|
-
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
150
|
-
return { frontmatter: parsed, body };
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
catch {
|
|
154
|
-
// fall through
|
|
155
|
-
}
|
|
156
|
-
return { frontmatter: null, body };
|
|
157
|
-
}
|
|
1
|
+
// Moved to core/dashboard-writer.ts (#254): both surfaces render the dashboard
|
|
2
|
+
// at the same lifecycle points, so it is a shared primitive. This re-export
|
|
3
|
+
// keeps the old import path — scripts/build-demo-dashboard.mjs among them —
|
|
4
|
+
// working.
|
|
5
|
+
export { writeDashboard } from "../../core/dashboard-writer.js";
|
|
@@ -5,13 +5,12 @@ import { autoCompletePhase, buildAutoSummary, isPhaseRunning, runAuto, runSingle
|
|
|
5
5
|
import { disposeAgentsWidget } from "./agent-widget.js";
|
|
6
6
|
import { parseDashboardFlags } from "./dashboard-flags.js";
|
|
7
7
|
import { narrateDashboard } from "./dashboard-narrator.js";
|
|
8
|
-
import { writeDashboard } from "./dashboard-writer.js";
|
|
9
8
|
import { parseBroadsideFlags, KNOWN_BROADSIDE_TOKENS } from "./broadside-flags.js";
|
|
10
9
|
import { parseNextFlags } from "./next-flags.js";
|
|
11
10
|
import { buildPiGuideMessage } from "./guide-framing.js";
|
|
12
11
|
import { isCtxLive, notifyCtx } from "./notify.js";
|
|
13
12
|
import { phaseCompactionExtension } from "./phase-compaction.js";
|
|
14
|
-
import { applyAmendment, buildPhasePrompt, buildSkillPrompt, buildValidationSummary, canonicalPath, copyPackagedWorkspace, computePerPhaseTotals, computeTotals, ConfidentialityMismatchError, createEmptyStatus, DEFAULT_PIPELINE_PATH, describeScaffoldStaleness, deriveSlug, discoverLibrary, getNextEligiblePhase, getPipelineLabel, getWorkspaceState,
|
|
13
|
+
import { applyAmendment, buildPhasePrompt, buildSkillPrompt, buildValidationSummary, backupWorkspaceState, canonicalPath, copyPackagedWorkspace, computePerPhaseTotals, computeTotals, ConfidentialityMismatchError, createEmptyStatus, DEFAULT_PIPELINE_PATH, describeScaffoldStaleness, deriveSlug, discoverLibrary, describeDanglingCarryForward, describeMissingCompletedOutputs, getNextEligiblePhase, getPipelineLabel, getWorkspaceState, isWithinPath, resolveExistingPrefix, BROADSIDE_LENS_IDS, BROADSIDE_SKILL_NAME, BroadsideCancelledError, broadsideDirFor, collectResultText, estimateSubmitText, getLens, listAmendmentNames, listBatchModels, listGuideTopics, listScaffoldRefreshFiles, listMissingCompletedOutputs, listSkillNames, resolveSkillName, loadAmendmentFile, loadBroadsideConfig, modelsText, runBroadsideCollect, runBroadsideStatus, runBroadsideSubmit, statusText, describeConfigProblems, loadCodecartoConfig, loadUsage, loadYamlFile, normalizeForComparison, packagedWorkspaceDir, pathExists, PACKAGE_VERSION, readBroadsideSkill, readGuide, refreshScaffold, PhasePreflightError, PIPELINE_ALIASES, publishEntry, resolvePhase, resolvePipelineChoice, resolvePublishSourceRepo, SourceRepoMismatchError, runPhasePreflight, SCAFFOLD_REFRESH_PROTECTED, seedOrchestratorFiles, stringifySimpleYaml, switchPipeline, validatePhaseOutput, writeLibraryConfig, writeDashboard, } from "../../core/index.js";
|
|
15
14
|
import { initLibrary } from "../../core/library.js";
|
|
16
15
|
import { resolveUserConfigPath } from "../../core/orchestrator-config.js";
|
|
17
16
|
const STATUS_WIDGET_ID = "codecarto-widget";
|
|
@@ -343,13 +342,17 @@ export default function codeCartographerExtension(pi) {
|
|
|
343
342
|
if (event.toolName === "edit" || event.toolName === "write") {
|
|
344
343
|
const inputPath = typeof event.input.path === "string" ? event.input.path : "";
|
|
345
344
|
const strippedPath = inputPath.startsWith("@") ? inputPath.slice(1) : inputPath;
|
|
346
|
-
|
|
345
|
+
// Resolve through whatever already exists on disk — symlinks included —
|
|
346
|
+
// before appending the unborn tail. `resolve()` would collapse
|
|
347
|
+
// `link/..` lexically and `realpath()` throws on a file that is not
|
|
348
|
+
// there yet, and either gap let a write escape the workspace (#223).
|
|
349
|
+
const targetPath = await resolveExistingPrefix(strippedPath, ctx.cwd);
|
|
347
350
|
const allowedRoots = [await canonicalPath(workspaceDir)];
|
|
348
351
|
const config = await loadCodecartoConfig(workspaceDir);
|
|
349
352
|
if (config.library.path && await discoverLibrary(config.library.path)) {
|
|
350
353
|
allowedRoots.push(await canonicalPath(config.library.path));
|
|
351
354
|
}
|
|
352
|
-
const withinAllowed =
|
|
355
|
+
const withinAllowed = allowedRoots.map((allowedRoot) => isWithinPath(targetPath, allowedRoot));
|
|
353
356
|
if (!withinAllowed.some((result) => result)) {
|
|
354
357
|
notifyCtx(ctx, `Blocked ${event.toolName} outside .codecarto/ or configured library: ${inputPath}`, "warning");
|
|
355
358
|
return { block: true, reason: `CodeCartographer mode only allows ${event.toolName} within .codecarto/ or the configured CodeCartographer library.` };
|
|
@@ -433,11 +436,21 @@ export default function codeCartographerExtension(pi) {
|
|
|
433
436
|
const targetExists = await pathExists(targetWorkspaceDir);
|
|
434
437
|
if (targetExists) {
|
|
435
438
|
const sameWorkspace = normalizeForComparison(await canonicalPath(targetWorkspaceDir)) === normalizeForComparison(await canonicalPath(sourceWorkspaceDir));
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
439
|
+
// The packaged template itself (a checkout install) is an existing
|
|
440
|
+
// workspace like any other: ask, and back up. It cannot be renamed
|
|
441
|
+
// away — it is what init copies from — so its session state is
|
|
442
|
+
// moved out file by file instead (#245).
|
|
443
|
+
const overwrite = await ctx.ui.confirm("CodeCartographer already exists — data will be lost", sameWorkspace
|
|
444
|
+
? "This .codecarto/ is CodeCartographer's own packaged template (a checkout install), and it holds workspace state. Re-initializing will move that state — status, findings, handoffs, usage data, closeouts, dashboard — to .codecarto-backup-TIMESTAMP/ and reset; the framework files stay in place. Consider /codecarto-open to reattach without resetting. Continue?"
|
|
445
|
+
: "A .codecarto/ directory already exists in this repository. Re-initializing will back up the existing workspace to .codecarto-backup-TIMESTAMP/ and create a fresh one. All phase findings, handoffs, usage data, closeouts, and progress will be moved to the backup. Consider /codecarto-open to reattach without resetting. Continue?");
|
|
446
|
+
if (!overwrite)
|
|
447
|
+
return;
|
|
448
|
+
const backupDir = join(ctx.cwd, `.codecarto-backup-${new Date().toISOString().replace(/[:.]/g, "-")}`);
|
|
449
|
+
if (sameWorkspace) {
|
|
450
|
+
const moved = await backupWorkspaceState(targetWorkspaceDir, backupDir);
|
|
451
|
+
notifyCtx(ctx, `Moved ${moved.length} workspace state file${moved.length === 1 ? "" : "s"} to ${basename(backupDir)}/`, "info");
|
|
452
|
+
}
|
|
453
|
+
else {
|
|
441
454
|
await rename(targetWorkspaceDir, backupDir);
|
|
442
455
|
notifyCtx(ctx, `Backed up existing workspace to ${basename(backupDir)}/`, "info");
|
|
443
456
|
}
|
|
@@ -447,7 +460,8 @@ export default function codeCartographerExtension(pi) {
|
|
|
447
460
|
await copyPackagedWorkspace(targetWorkspaceDir);
|
|
448
461
|
}
|
|
449
462
|
const rawStatusPath = join(targetWorkspaceDir, "workflow", "status.yaml");
|
|
450
|
-
|
|
463
|
+
// Absent on a fresh copy: the template ships no status.yaml.
|
|
464
|
+
const rawStatus = (await pathExists(rawStatusPath)) ? ((await loadYamlFile(rawStatusPath)) ?? {}) : {};
|
|
451
465
|
const selectedPipelinePath = pipelineChoice ?? rawStatus.pipeline?.trim() ?? DEFAULT_PIPELINE_PATH;
|
|
452
466
|
const resolvedPipelinePath = join(targetWorkspaceDir, selectedPipelinePath);
|
|
453
467
|
if (!(await pathExists(resolvedPipelinePath))) {
|
|
@@ -485,9 +499,17 @@ export default function codeCartographerExtension(pi) {
|
|
|
485
499
|
if (!state)
|
|
486
500
|
return;
|
|
487
501
|
const nextPhase = getNextEligiblePhase(state)?.id ?? "complete";
|
|
488
|
-
|
|
502
|
+
// Same check codecarto_status makes: a phase complete in status.yaml
|
|
503
|
+
// whose report is not on disk (#259).
|
|
504
|
+
const missingOutputs = describeMissingCompletedOutputs(await listMissingCompletedOutputs(state));
|
|
505
|
+
lastFeedbackLines = [`Current phase: ${nextPhase}`, `Pipeline: ${getPipelineLabel(state.status.pipeline)}`, ...missingOutputs];
|
|
489
506
|
setUiState(ctx, state, lastFeedbackLines);
|
|
490
|
-
|
|
507
|
+
if (missingOutputs.length > 0) {
|
|
508
|
+
notifyCtx(ctx, `CodeCartographer phase: ${nextPhase}. ${missingOutputs[0]}`, "warning");
|
|
509
|
+
}
|
|
510
|
+
else {
|
|
511
|
+
notifyCtx(ctx, `CodeCartographer phase: ${nextPhase}`, "info");
|
|
512
|
+
}
|
|
491
513
|
},
|
|
492
514
|
});
|
|
493
515
|
pi.registerCommand("codecarto-switch-pipeline", {
|
|
@@ -528,6 +550,7 @@ export default function codeCartographerExtension(pi) {
|
|
|
528
550
|
lines.push(`New phases: ${result.newPhases.join(", ")}`);
|
|
529
551
|
if (result.dropped.length > 0)
|
|
530
552
|
lines.push(`Phases not in new pipeline: ${result.dropped.join(", ")} (findings remain on disk)`);
|
|
553
|
+
lines.push(...describeDanglingCarryForward(result.dangling));
|
|
531
554
|
lastFeedbackLines = lines;
|
|
532
555
|
await refreshWorkspaceUi(ctx, lastFeedbackLines);
|
|
533
556
|
notifyCtx(ctx, `Switched to pipeline: ${getPipelineLabel(pipelineChoice)}`, "info");
|
|
@@ -626,6 +649,10 @@ export default function codeCartographerExtension(pi) {
|
|
|
626
649
|
return;
|
|
627
650
|
}
|
|
628
651
|
const config = await loadCodecartoConfig(state.workspaceDir);
|
|
652
|
+
// A bad config file must not block a phase run, but it must not be
|
|
653
|
+
// silent either: the toggle read here may be the one it dropped.
|
|
654
|
+
if (config.problems.length > 0)
|
|
655
|
+
notifyCtx(ctx, describeConfigProblems(config).join("\n"), "warning");
|
|
629
656
|
const llmSteerEnabled = flags.llmSteerOverride ?? config.orchestrator.llm_steer_next_phase;
|
|
630
657
|
lastFeedbackLines = [`Running ${phase.id} phase as sub-agent`];
|
|
631
658
|
setUiState(ctx, state, lastFeedbackLines);
|
|
@@ -846,23 +873,27 @@ export default function codeCartographerExtension(pi) {
|
|
|
846
873
|
notifyCtx(ctx, `Cannot run skill: pipeline is not complete (next phase: ${nextPhase.id}). Finish the pipeline before running post-pipeline skills.`, "error");
|
|
847
874
|
return;
|
|
848
875
|
}
|
|
849
|
-
|
|
850
|
-
|
|
876
|
+
// Resolve against the installed list only: the name is never joined
|
|
877
|
+
// onto a path, so a traversal like `../findings/architecture` cannot
|
|
878
|
+
// splice a phase SKILL.md into the post-pipeline prompt. Same rule as
|
|
879
|
+
// the MCP surface's handleSkill.
|
|
880
|
+
const resolved = await resolveSkillName(state.workspaceDir, skillName);
|
|
881
|
+
if (!resolved) {
|
|
851
882
|
const available = await listSkillNames(state.workspaceDir);
|
|
852
883
|
const hint = available.length > 0 ? ` (available: ${available.join(", ")})` : " (no skills installed)";
|
|
853
884
|
notifyCtx(ctx, `Unknown skill: ${skillName}${hint}. The Broad-Side reading guide is served as \`${BROADSIDE_SKILL_NAME}\` and is not pipeline-gated.`, "error");
|
|
854
885
|
return;
|
|
855
886
|
}
|
|
856
|
-
const prompt = await buildSkillPrompt(state,
|
|
887
|
+
const prompt = await buildSkillPrompt(state, resolved);
|
|
857
888
|
if (ctx.isIdle()) {
|
|
858
889
|
pi.sendUserMessage(prompt);
|
|
859
890
|
}
|
|
860
891
|
else {
|
|
861
892
|
pi.sendUserMessage(prompt, { deliverAs: "followUp" });
|
|
862
893
|
}
|
|
863
|
-
lastFeedbackLines = [`Queued post-pipeline skill: ${
|
|
894
|
+
lastFeedbackLines = [`Queued post-pipeline skill: ${resolved}`];
|
|
864
895
|
setUiState(ctx, state, lastFeedbackLines);
|
|
865
|
-
notifyCtx(ctx, `Queued CodeCartographer skill: ${
|
|
896
|
+
notifyCtx(ctx, `Queued CodeCartographer skill: ${resolved}`, "info");
|
|
866
897
|
},
|
|
867
898
|
});
|
|
868
899
|
pi.registerCommand("codecarto-list-skills", {
|
|
@@ -1103,6 +1134,13 @@ export default function codeCartographerExtension(pi) {
|
|
|
1103
1134
|
if (!state)
|
|
1104
1135
|
return;
|
|
1105
1136
|
const config = await loadCodecartoConfig(state.workspaceDir);
|
|
1137
|
+
// The library path, namespace, and confirm gate all come from the
|
|
1138
|
+
// config, so a file that could not be used is a refusal, not a silent
|
|
1139
|
+
// fallback to the other layer (#242). Same rule as codecarto_publish.
|
|
1140
|
+
if (config.problems.length > 0) {
|
|
1141
|
+
notifyCtx(ctx, ["Publish refused: the configuration has problems. Fix or remove the offending file, then retry.", ...describeConfigProblems(config)].join("\n"), "error");
|
|
1142
|
+
return;
|
|
1143
|
+
}
|
|
1106
1144
|
if (!config.library.path) {
|
|
1107
1145
|
notifyCtx(ctx, "No library.path is configured. Create a library directory with a .codecarto-library marker, then set library.path in ~/.codecarto/config.yaml or .codecarto/workflow/config.yaml.", "error");
|
|
1108
1146
|
return;
|
|
@@ -1223,15 +1261,21 @@ export default function codeCartographerExtension(pi) {
|
|
|
1223
1261
|
namespaced: !!namespace,
|
|
1224
1262
|
...(namespace ? {} : {}),
|
|
1225
1263
|
});
|
|
1226
|
-
// Write
|
|
1264
|
+
// Write library.path (and library.namespace when given) to the
|
|
1265
|
+
// user-global config and nothing else — not publish_confirm, so
|
|
1266
|
+
// initializing a library does not change how publish behaves on
|
|
1267
|
+
// MCP (#244). A config file that cannot be parsed is left alone;
|
|
1268
|
+
// the error lands in the catch below.
|
|
1227
1269
|
const configPath = resolveUserConfigPath();
|
|
1228
1270
|
await writeLibraryConfig(configPath, libraryPath, namespace);
|
|
1271
|
+
const written = namespace ? "library.path and library.namespace" : "library.path";
|
|
1229
1272
|
const msg = result.alreadyExisted
|
|
1230
1273
|
? `Library already exists at ${libraryPath} (marker preserved). Config updated.`
|
|
1231
1274
|
: `Created library at ${libraryPath} with marker "${result.marker.name}".`;
|
|
1232
|
-
|
|
1275
|
+
const configLine = `Wrote ${written} to ${configPath}; other keys untouched`;
|
|
1276
|
+
lastFeedbackLines = [msg, configLine];
|
|
1233
1277
|
notifyCtx(ctx, msg, "info");
|
|
1234
|
-
notifyCtx(ctx,
|
|
1278
|
+
notifyCtx(ctx, configLine, "info");
|
|
1235
1279
|
}
|
|
1236
1280
|
catch (error) {
|
|
1237
1281
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -1258,10 +1302,18 @@ export default function codeCartographerExtension(pi) {
|
|
|
1258
1302
|
const marker = await discoverLibrary(config.library.path);
|
|
1259
1303
|
lines.push(` Library marker: ${marker ? `found ("${marker.name}", namespaced: ${marker.namespaced})` : "MISSING — run /codecarto-library-init"}`);
|
|
1260
1304
|
}
|
|
1305
|
+
// A file that could not be used is the one thing this command exists
|
|
1306
|
+
// to surface; /codecarto-publish refuses while any is listed (#242).
|
|
1307
|
+
lines.push(...describeConfigProblems(config));
|
|
1261
1308
|
lastFeedbackLines = lines;
|
|
1262
1309
|
if (state)
|
|
1263
1310
|
setUiState(ctx, state, lastFeedbackLines);
|
|
1264
|
-
|
|
1311
|
+
if (config.problems.length > 0) {
|
|
1312
|
+
notifyCtx(ctx, describeConfigProblems(config).join("\n"), "warning");
|
|
1313
|
+
}
|
|
1314
|
+
else {
|
|
1315
|
+
notifyCtx(ctx, "Configuration shown in status widget.", "info");
|
|
1316
|
+
}
|
|
1265
1317
|
},
|
|
1266
1318
|
});
|
|
1267
1319
|
pi.registerCommand("codecarto-usage", {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { mkdir
|
|
2
|
-
import { join
|
|
1
|
+
import { mkdir } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
3
|
import { compact } from "@earendil-works/pi-coding-agent";
|
|
4
|
-
import { canonicalPath, getWorkspaceState, isWithinPath } from "../../core/index.js";
|
|
4
|
+
import { atomicWriteFile, canonicalPath, getWorkspaceState, isWithinPath, resolveExistingPrefix } from "../../core/index.js";
|
|
5
5
|
const PHASE_SESSION_PREFIX = "CodeCartographer phase: ";
|
|
6
6
|
export function phaseIdFromSessionName(sessionName) {
|
|
7
7
|
if (!sessionName?.startsWith(PHASE_SESSION_PREFIX))
|
|
@@ -29,7 +29,6 @@ export function buildPhaseCompactionInstructions(phaseId, primaryOutput) {
|
|
|
29
29
|
export async function writePhaseCheckpoint(cwd, phaseId, summary, tokensBefore) {
|
|
30
30
|
const dir = join(cwd, ".codecarto", "scratch", "checkpoints");
|
|
31
31
|
const target = join(dir, `${phaseId}.md`);
|
|
32
|
-
const temp = `${target}.${process.pid}.${Date.now()}.tmp`;
|
|
33
32
|
await mkdir(dir, { recursive: true });
|
|
34
33
|
const content = [
|
|
35
34
|
"---",
|
|
@@ -44,8 +43,7 @@ export async function writePhaseCheckpoint(cwd, phaseId, summary, tokensBefore)
|
|
|
44
43
|
summary.trim(),
|
|
45
44
|
"",
|
|
46
45
|
].join("\n");
|
|
47
|
-
await
|
|
48
|
-
await rename(temp, target);
|
|
46
|
+
await atomicWriteFile(target, content);
|
|
49
47
|
return target;
|
|
50
48
|
}
|
|
51
49
|
/**
|
|
@@ -64,7 +62,9 @@ export function phaseCompactionExtension(pi) {
|
|
|
64
62
|
if (event.toolName === "edit" || event.toolName === "write") {
|
|
65
63
|
const inputPath = typeof event.input.path === "string" ? event.input.path : "";
|
|
66
64
|
const strippedPath = inputPath.startsWith("@") ? inputPath.slice(1) : inputPath;
|
|
67
|
-
|
|
65
|
+
// Same containment as the parent extension's hook (#223): follow the
|
|
66
|
+
// existing prefix through symlinks, then append the unborn tail.
|
|
67
|
+
const targetPath = await resolveExistingPrefix(strippedPath, ctx.cwd);
|
|
68
68
|
const allowedRoot = await canonicalPath(join(ctx.cwd, ".codecarto"));
|
|
69
69
|
if (!isWithinPath(targetPath, allowedRoot)) {
|
|
70
70
|
return {
|