codecartographer-pi 0.17.1 → 0.19.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 +9 -2
- package/.codecarto/skills/spec-delta-application/SKILL.md +1 -1
- package/.codecarto/templates/amendment.yaml +3 -3
- package/.codecarto/templates/phase-handoff.yaml +20 -1
- package/.codecarto/templates/spike-report.md +2 -2
- package/.codecarto/workflow/scaffold-version.yaml +1 -1
- package/README.md +10 -6
- package/agent-skill/codecartographer/SKILL.md +1 -1
- package/agent-skill/codecartographer/references/handoff-contract.md +30 -2
- package/agent-skill/codecartographer/references/library.md +2 -2
- package/agent-skill/codecartographer/references/phase-recovery.md +1 -1
- package/dist/core/amendment.d.ts +5 -0
- package/dist/core/amendment.js +21 -2
- 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/index.d.ts +1 -0
- package/dist/core/index.js +1 -0
- package/dist/core/library.d.ts +73 -0
- package/dist/core/library.js +135 -37
- package/dist/core/orchestrator-config.d.ts +6 -0
- package/dist/core/orchestrator-config.js +2 -0
- package/dist/core/prompts.js +19 -1
- package/dist/core/status.d.ts +10 -2
- package/dist/core/status.js +56 -7
- package/dist/core/types.d.ts +24 -1
- package/dist/core/workspace.d.ts +16 -0
- package/dist/core/workspace.js +31 -4
- package/dist/extensions/codecarto/index.js +355 -20
- package/dist/mcp-server/server.js +63 -4
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
|
-
import { basename, join, resolve } from "node:path";
|
|
3
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
4
4
|
import { autoCompletePhase, buildAutoSummary, isPhaseRunning, runAuto, runSinglePhase } from "./auto-runner.js";
|
|
5
5
|
import { disposeAgentsWidget } from "./agent-widget.js";
|
|
6
6
|
import { parseDashboardFlags } from "./dashboard-flags.js";
|
|
@@ -9,7 +9,7 @@ import { writeDashboard } from "./dashboard-writer.js";
|
|
|
9
9
|
import { parseBroadsideFlags, KNOWN_BROADSIDE_TOKENS } from "./broadside-flags.js";
|
|
10
10
|
import { parseNextFlags } from "./next-flags.js";
|
|
11
11
|
import { phaseCompactionExtension } from "./phase-compaction.js";
|
|
12
|
-
import { buildPhasePrompt, buildSkillPrompt, buildValidationSummary, canonicalPath, copyPackagedWorkspace, computePerPhaseTotals, computeTotals, ConfidentialityMismatchError, createEmptyStatus, DEFAULT_PIPELINE_PATH, describeScaffoldStaleness, deriveSlug, discoverLibrary, getNextEligiblePhase, getPipelineLabel, getWorkspaceState, isWithinPathResolved, BROADSIDE_LENS_IDS, BROADSIDE_SKILL_NAME, BroadsideCancelledError, broadsideDirFor, collectResultText, estimateSubmitText, getLens, listBatchModels, listSkillNames, loadBroadsideConfig, modelsText, runBroadsideCollect, runBroadsideStatus, runBroadsideSubmit, statusText, loadCodecartoConfig, loadUsage, loadYamlFile, normalizeForComparison, packagedWorkspaceDir, pathExists, PACKAGE_VERSION, readBroadsideSkill, PhasePreflightError, PIPELINE_ALIASES, publishEntry, resolvePhase, resolvePipelineChoice, runPhasePreflight, seedOrchestratorFiles, stringifySimpleYaml, switchPipeline, validatePhaseOutput, writeLibraryConfig, } from "../../core/index.js";
|
|
12
|
+
import { applyAmendment, buildPhasePrompt, buildSkillPrompt, buildValidationSummary, canonicalPath, copyPackagedWorkspace, computePerPhaseTotals, computeTotals, ConfidentialityMismatchError, createEmptyStatus, DEFAULT_PIPELINE_PATH, describeScaffoldStaleness, deriveSlug, discoverLibrary, getNextEligiblePhase, getPipelineLabel, getWorkspaceState, isWithinPathResolved, BROADSIDE_LENS_IDS, BROADSIDE_SKILL_NAME, BroadsideCancelledError, broadsideDirFor, collectResultText, estimateSubmitText, getLens, listAmendmentNames, listBatchModels, listGuideTopics, listScaffoldRefreshFiles, listSkillNames, loadAmendmentFile, loadBroadsideConfig, modelsText, runBroadsideCollect, runBroadsideStatus, runBroadsideSubmit, statusText, 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, } from "../../core/index.js";
|
|
13
13
|
import { initLibrary } from "../../core/library.js";
|
|
14
14
|
import { resolveUserConfigPath } from "../../core/orchestrator-config.js";
|
|
15
15
|
const STATUS_WIDGET_ID = "codecarto-widget";
|
|
@@ -137,11 +137,130 @@ function describeBroadsideEstimate(estimate) {
|
|
|
137
137
|
lines.push("", "The estimate is a pre-flight prediction from file sizes; OpenRouter bills actual usage.");
|
|
138
138
|
return lines.join("\n");
|
|
139
139
|
}
|
|
140
|
+
/**
|
|
141
|
+
* Resolve the argument of /codecarto-amend to the slug applyAmendment takes.
|
|
142
|
+
* Accepts the slug, `slug.yaml`, or a path to the file — but a path only when
|
|
143
|
+
* it lands inside .codecarto/scratch/amendments/, the one place an amendment
|
|
144
|
+
* is read from. The path names the file; the read always goes through the slug.
|
|
145
|
+
*/
|
|
146
|
+
function resolveAmendmentName(rawArg, cwd) {
|
|
147
|
+
const trimmed = rawArg.trim().replace(/^@/, "");
|
|
148
|
+
if (!trimmed)
|
|
149
|
+
return null;
|
|
150
|
+
if (!/[\\/]/.test(trimmed))
|
|
151
|
+
return trimmed;
|
|
152
|
+
const amendmentsDir = resolve(cwd, ".codecarto", "scratch", "amendments");
|
|
153
|
+
for (const candidate of [resolve(cwd, trimmed), resolve(cwd, ".codecarto", trimmed)]) {
|
|
154
|
+
if (dirname(candidate) === amendmentsDir)
|
|
155
|
+
return basename(candidate);
|
|
156
|
+
}
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
function clipDescription(text, max = 140) {
|
|
160
|
+
if (!text)
|
|
161
|
+
return "";
|
|
162
|
+
const oneLine = text.replace(/\s+/g, " ").trim();
|
|
163
|
+
return `: ${oneLine.length > max ? `${oneLine.slice(0, max - 1)}…` : oneLine}`;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* What an amendment will do to canonical state, rendered for a human about to
|
|
167
|
+
* approve it: each closure resolved against status.yaml so an id that matches
|
|
168
|
+
* nothing is visible before the write, not only in the result.
|
|
169
|
+
*/
|
|
170
|
+
function describeAmendmentPreview(amendment, state) {
|
|
171
|
+
const openQuestions = new Map();
|
|
172
|
+
for (const [phaseId, phase] of Object.entries(state.status.phases)) {
|
|
173
|
+
for (const entry of phase.open_questions ?? []) {
|
|
174
|
+
if (!entry.id)
|
|
175
|
+
continue;
|
|
176
|
+
openQuestions.set(entry.id, [...(openQuestions.get(entry.id) ?? []), { phaseId, entry }]);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
const unmatched = " — matches nothing (already closed or unknown; reported, not fatal)";
|
|
180
|
+
const lines = [`Amendment file: .codecarto/scratch/amendments/${amendment.slug}.yaml`];
|
|
181
|
+
if (amendment.open_question_closures.length > 0) {
|
|
182
|
+
lines.push("", `Closes ${amendment.open_question_closures.length} open question(s):`);
|
|
183
|
+
for (const id of amendment.open_question_closures) {
|
|
184
|
+
const matches = openQuestions.get(id);
|
|
185
|
+
if (!matches) {
|
|
186
|
+
lines.push(` - ${id}${unmatched}`);
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
const { entry } = matches[0];
|
|
190
|
+
const where = [matches.map((match) => match.phaseId).join(", "), entry.kind].filter(Boolean).join(", ");
|
|
191
|
+
lines.push(` - ${id} (${where})${clipDescription(entry.description)}`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
if (amendment.post_pipeline_closures.length > 0) {
|
|
195
|
+
lines.push("", `Retires ${amendment.post_pipeline_closures.length} post-pipeline item(s):`);
|
|
196
|
+
for (const id of amendment.post_pipeline_closures) {
|
|
197
|
+
const entry = state.status.post_pipeline.find((item) => item.id === id);
|
|
198
|
+
if (!entry) {
|
|
199
|
+
lines.push(` - ${id}${unmatched}`);
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
const where = [entry.kind, entry.source_phase ? `from ${entry.source_phase}` : ""].filter(Boolean).join(", ");
|
|
203
|
+
lines.push(` - ${id}${where ? ` (${where})` : ""}${clipDescription(entry.description)}`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
if (amendment.notes.length > 0) {
|
|
207
|
+
lines.push("", `Records ${amendment.notes.length} note(s) in the closeout:`, ...amendment.notes.map((note) => ` - ${note}`));
|
|
208
|
+
}
|
|
209
|
+
const summary = amendment.closeout_summary.trim();
|
|
210
|
+
lines.push("", `Writes .codecarto/closeouts/<date>-amendment-${amendment.slug}.md, appends one THREAD_LOG entry${summary ? ` ("${summary}")` : ""}, updates workflow/status.yaml under the completion lock, and refreshes the dashboard.`);
|
|
211
|
+
return lines.join("\n");
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* What a scaffold refresh will overwrite, rendered for a human about to approve
|
|
215
|
+
* it. The file set is the one refreshScaffold writes; the protected set is the
|
|
216
|
+
* one it skips — both come from core, so the preview cannot drift from the write.
|
|
217
|
+
*/
|
|
218
|
+
function describeScaffoldRefreshPreview(files, scaffoldVersionBefore) {
|
|
219
|
+
const topLevel = [];
|
|
220
|
+
const byDir = new Map();
|
|
221
|
+
for (const file of files) {
|
|
222
|
+
const slash = file.indexOf("/");
|
|
223
|
+
if (slash === -1) {
|
|
224
|
+
topLevel.push(file);
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
const dir = file.slice(0, slash);
|
|
228
|
+
byDir.set(dir, [...(byDir.get(dir) ?? []), file.slice(slash + 1)]);
|
|
229
|
+
}
|
|
230
|
+
const from = scaffoldVersionBefore ?? "unversioned";
|
|
231
|
+
const lines = [
|
|
232
|
+
from === PACKAGE_VERSION
|
|
233
|
+
? `Scaffold version: ${from} (already current — the files are re-copied from the packaged template byte-for-byte).`
|
|
234
|
+
: `Scaffold version: ${from} → ${PACKAGE_VERSION}.`,
|
|
235
|
+
"",
|
|
236
|
+
`Overwrites ${files.length} framework-owned file(s) in .codecarto/ with the packaged template:`,
|
|
237
|
+
];
|
|
238
|
+
if (topLevel.length > 0)
|
|
239
|
+
lines.push(` ${topLevel.join(", ")}`);
|
|
240
|
+
for (const [dir, entries] of [...byDir.entries()].sort(([a], [b]) => a.localeCompare(b))) {
|
|
241
|
+
// workflow/ is where the pipelines live, and the version marker: name them.
|
|
242
|
+
lines.push(dir === "workflow" ? ` workflow/: ${entries.join(", ")}` : ` ${dir}/: ${entries.length} file(s)`);
|
|
243
|
+
}
|
|
244
|
+
if (byDir.has("findings")) {
|
|
245
|
+
lines.push(" (findings/ refreshes only the packaged SKILL.md, README.md, and pass files — the findings outputs beside them stay.)");
|
|
246
|
+
}
|
|
247
|
+
const protectedPaths = [
|
|
248
|
+
...SCAFFOLD_REFRESH_PROTECTED.workflowFiles.map((file) => `workflow/${file}`),
|
|
249
|
+
...SCAFFOLD_REFRESH_PROTECTED.topLevel,
|
|
250
|
+
...SCAFFOLD_REFRESH_PROTECTED.dirs.map((dir) => `${dir}/`),
|
|
251
|
+
];
|
|
252
|
+
lines.push("", `Never touched: ${protectedPaths.join(", ")}.`, "One THREAD_LOG entry records the refresh. Continue?");
|
|
253
|
+
return lines.join("\n");
|
|
254
|
+
}
|
|
140
255
|
export default function codeCartographerExtension(pi) {
|
|
141
256
|
phaseCompactionExtension(pi);
|
|
142
257
|
let lastFeedbackLines = [];
|
|
143
258
|
let codecartoModeActive = false;
|
|
259
|
+
// Argument completers receive only the prefix, so the session's cwd is
|
|
260
|
+
// remembered here for the completers that list files under .codecarto/.
|
|
261
|
+
let sessionCwd;
|
|
144
262
|
const readWorkspaceState = async (ctx, notifyOnError = true) => {
|
|
263
|
+
sessionCwd = ctx.cwd;
|
|
145
264
|
try {
|
|
146
265
|
return await getWorkspaceState(ctx.cwd);
|
|
147
266
|
}
|
|
@@ -185,6 +304,7 @@ export default function codeCartographerExtension(pi) {
|
|
|
185
304
|
pi.on("session_start", async (_event, ctx) => {
|
|
186
305
|
codecartoModeActive = false;
|
|
187
306
|
lastFeedbackLines = [];
|
|
307
|
+
sessionCwd = ctx.cwd;
|
|
188
308
|
setUiState(ctx, null);
|
|
189
309
|
});
|
|
190
310
|
pi.on("session_shutdown", async () => {
|
|
@@ -698,6 +818,76 @@ export default function codeCartographerExtension(pi) {
|
|
|
698
818
|
ctx.ui.notify(`Queued CodeCartographer skill: ${skillName}`, "info");
|
|
699
819
|
},
|
|
700
820
|
});
|
|
821
|
+
pi.registerCommand("codecarto-list-skills", {
|
|
822
|
+
description: "List the post-pipeline skills installed in .codecarto/skills/ (and the ungated Broad-Side reading guide)",
|
|
823
|
+
handler: async (_args, ctx) => {
|
|
824
|
+
// Same gate as /codecarto-skill: the listing reads the workspace's
|
|
825
|
+
// skills directory, so it needs a workspace — mirrors handleListSkills.
|
|
826
|
+
const state = await ensureWorkspaceState(ctx);
|
|
827
|
+
if (!state)
|
|
828
|
+
return;
|
|
829
|
+
const skills = await listSkillNames(state.workspaceDir);
|
|
830
|
+
const lines = skills.length > 0
|
|
831
|
+
? [`Available skills (${skills.length}):`, ...skills.map((name) => ` - ${name}`)]
|
|
832
|
+
: ["No skills installed."];
|
|
833
|
+
const nextPhase = getNextEligiblePhase(state);
|
|
834
|
+
if (skills.length > 0) {
|
|
835
|
+
lines.push(nextPhase
|
|
836
|
+
? `Post-pipeline skills unlock when the pipeline completes (next phase: ${nextPhase.id}).`
|
|
837
|
+
: "Run one with /codecarto-skill <name>.");
|
|
838
|
+
}
|
|
839
|
+
// Broad-Side is listed apart from the post-pipeline set because it
|
|
840
|
+
// answers to /codecarto-skill without the completion gate.
|
|
841
|
+
const broadsideAvailable = await readBroadsideSkill(ctx.cwd).then(() => true, () => false);
|
|
842
|
+
if (broadsideAvailable) {
|
|
843
|
+
lines.push("", `Also served by /codecarto-skill (not pipeline-gated): ${BROADSIDE_SKILL_NAME} — how to read a Broad-Side batch reconnaissance run.`);
|
|
844
|
+
}
|
|
845
|
+
lastFeedbackLines = lines;
|
|
846
|
+
setUiState(ctx, state, lastFeedbackLines);
|
|
847
|
+
ctx.ui.notify(skills.length > 0
|
|
848
|
+
? `${skills.length} post-pipeline skill${skills.length === 1 ? "" : "s"}: ${skills.join(", ")}`
|
|
849
|
+
: "No post-pipeline skills installed.", "info");
|
|
850
|
+
},
|
|
851
|
+
});
|
|
852
|
+
pi.registerCommand("codecarto-guide", {
|
|
853
|
+
description: "Read the packaged CodeCartographer agent guide into the session: /codecarto-guide [topic]",
|
|
854
|
+
getArgumentCompletions: async (prefix) => {
|
|
855
|
+
const topics = await listGuideTopics().catch(() => ["overview"]);
|
|
856
|
+
const items = topics
|
|
857
|
+
.filter((value) => value.startsWith(prefix))
|
|
858
|
+
.map((value) => ({ value, label: value }));
|
|
859
|
+
return items.length > 0 ? items : null;
|
|
860
|
+
},
|
|
861
|
+
handler: async (args, ctx) => {
|
|
862
|
+
// The guide is packaged with the extension, not copied into a
|
|
863
|
+
// workspace, so — like codecarto_guide — this needs no workspace.
|
|
864
|
+
let document;
|
|
865
|
+
let topics;
|
|
866
|
+
try {
|
|
867
|
+
topics = await listGuideTopics();
|
|
868
|
+
document = await readGuide(args.trim() || undefined);
|
|
869
|
+
}
|
|
870
|
+
catch (error) {
|
|
871
|
+
ctx.ui.notify(error instanceof Error ? error.message : String(error), "error");
|
|
872
|
+
return;
|
|
873
|
+
}
|
|
874
|
+
const other = topics.filter((name) => name !== document.topic);
|
|
875
|
+
const footer = other.length > 0
|
|
876
|
+
? `\n\n---\nOther guide topics: ${other.join(", ")} (run /codecarto-guide <topic>).`
|
|
877
|
+
: "";
|
|
878
|
+
const message = `${document.content}${footer}`;
|
|
879
|
+
if (ctx.isIdle()) {
|
|
880
|
+
pi.sendUserMessage(message);
|
|
881
|
+
}
|
|
882
|
+
else {
|
|
883
|
+
pi.sendUserMessage(message, { deliverAs: "followUp" });
|
|
884
|
+
}
|
|
885
|
+
lastFeedbackLines = [`Queued the CodeCartographer guide: ${document.topic}`];
|
|
886
|
+
if (codecartoModeActive)
|
|
887
|
+
void refreshWorkspaceUi(ctx, lastFeedbackLines);
|
|
888
|
+
ctx.ui.notify(`Queued the CodeCartographer guide (${document.topic})`, "info");
|
|
889
|
+
},
|
|
890
|
+
});
|
|
701
891
|
pi.registerCommand("codecarto-broadside", {
|
|
702
892
|
description: "Batch reconnaissance (Broad-Side): /codecarto-broadside [submit|collect|status|models] [lenses…] [flags]",
|
|
703
893
|
getArgumentCompletions: (prefix) => {
|
|
@@ -886,16 +1076,20 @@ export default function codeCartographerExtension(pi) {
|
|
|
886
1076
|
return;
|
|
887
1077
|
}
|
|
888
1078
|
const spec = await readFile(specPath, "utf8");
|
|
889
|
-
|
|
1079
|
+
// The git remote when there is one, the directory otherwise (#147).
|
|
1080
|
+
// Slug and source_repo derive from the same value so they agree.
|
|
1081
|
+
const source = await resolvePublishSourceRepo(ctx.cwd);
|
|
1082
|
+
const slug = deriveSlug(source.source_repo);
|
|
890
1083
|
const headline = derivePublishHeadline(spec, ctx.cwd);
|
|
891
1084
|
const namespace = marker.namespaced ? config.library.namespace ?? undefined : undefined;
|
|
892
1085
|
if (marker.namespaced && !namespace) {
|
|
893
1086
|
ctx.ui.notify("The configured library is namespaced; set library.namespace before publishing.", "error");
|
|
894
1087
|
return;
|
|
895
1088
|
}
|
|
1089
|
+
const label = `${namespace ? `${namespace}/` : ""}${slug}`;
|
|
896
1090
|
const preview = [
|
|
897
|
-
`Publish ${
|
|
898
|
-
`Source: ${
|
|
1091
|
+
`Publish ${label} to ${config.library.path}`,
|
|
1092
|
+
`Source: ${source.source_repo}${source.remote ? ` (git remote ${source.remote})` : ""}`,
|
|
899
1093
|
`Spec: .codecarto/${phase.primary_output}`,
|
|
900
1094
|
`Headline: ${headline}`,
|
|
901
1095
|
`Provenance: Pi / ${ctx.model?.provider ?? "unknown"} / ${ctx.model?.id ?? "unknown"}`,
|
|
@@ -905,7 +1099,7 @@ export default function codeCartographerExtension(pi) {
|
|
|
905
1099
|
const input = {
|
|
906
1100
|
slug,
|
|
907
1101
|
namespace,
|
|
908
|
-
source_repo:
|
|
1102
|
+
source_repo: source.source_repo,
|
|
909
1103
|
analyzed_at: new Date().toISOString(),
|
|
910
1104
|
pipeline: state.status.pipeline,
|
|
911
1105
|
codecarto_version: PACKAGE_VERSION,
|
|
@@ -915,22 +1109,44 @@ export default function codeCartographerExtension(pi) {
|
|
|
915
1109
|
generation: piGeneration(ctx),
|
|
916
1110
|
};
|
|
917
1111
|
try {
|
|
1112
|
+
// Both guards in publishEntry raise before anything is written, and
|
|
1113
|
+
// each asks a question only the user can answer, so the command has
|
|
1114
|
+
// no flags for them: a yes is the override. The options accumulate,
|
|
1115
|
+
// so a publish that trips both guards asks both questions in turn.
|
|
1116
|
+
const options = {};
|
|
918
1117
|
let result;
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
1118
|
+
while (!result) {
|
|
1119
|
+
try {
|
|
1120
|
+
result = await publishEntry(config.library.path, spec, input, options);
|
|
1121
|
+
}
|
|
1122
|
+
catch (error) {
|
|
1123
|
+
if (error instanceof SourceRepoMismatchError && !options.allowSourceRepoChange) {
|
|
1124
|
+
// The entry's history belongs to whatever the newest version
|
|
1125
|
+
// records. Appending is right only if that repository and this
|
|
1126
|
+
// one are the same project under a new address (#146) — which
|
|
1127
|
+
// includes the first publish after upgrading from a Pi that
|
|
1128
|
+
// recorded the directory to one that records the git remote.
|
|
1129
|
+
const moved = await ctx.ui.confirm("Source repository changed — did it move?", `Library entry ${label} records source_repo "${error.recorded}", but this publish carries "${error.incoming}". If the repository genuinely moved (rename, org transfer, host change — or this is the first publish since CodeCartographer began recording the git remote instead of the local directory), answer yes and this spec is appended as the entry's next version. If these are two different projects that share a directory name, answer no: nothing is written, and the second project needs a distinct slug (codecarto_publish on MCP accepts one). Did the repository move?`);
|
|
1130
|
+
if (!moved) {
|
|
1131
|
+
ctx.ui.notify("Publish cancelled. Nothing was written.", "info");
|
|
1132
|
+
return;
|
|
1133
|
+
}
|
|
1134
|
+
options.allowSourceRepoChange = true;
|
|
1135
|
+
}
|
|
1136
|
+
else if (error instanceof ConfidentialityMismatchError && !options.allowConfidentialityMismatch) {
|
|
1137
|
+
// Pi declares no confidentiality, so the entry sits at the internal
|
|
1138
|
+
// default; whether it may go into a wider library is the user's call.
|
|
1139
|
+
const publishAnyway = await ctx.ui.confirm("Confidentiality mismatch — publish anyway?", `This spec's confidentiality is "${error.entryConfidentiality}" (CodeCartographer's default; /codecarto-publish declares none), but the library "${marker.name}" has visibility "${error.libraryVisibility}". Publishing would expose it to everyone that library reaches. Publish anyway?`);
|
|
1140
|
+
if (!publishAnyway) {
|
|
1141
|
+
ctx.ui.notify("Publish cancelled. Nothing was written.", "info");
|
|
1142
|
+
return;
|
|
1143
|
+
}
|
|
1144
|
+
options.allowConfidentialityMismatch = true;
|
|
1145
|
+
}
|
|
1146
|
+
else {
|
|
1147
|
+
throw error;
|
|
1148
|
+
}
|
|
932
1149
|
}
|
|
933
|
-
result = await publishEntry(config.library.path, spec, input, { allowConfidentialityMismatch: true });
|
|
934
1150
|
}
|
|
935
1151
|
lastFeedbackLines = [`Published ${result.namespace ? `${result.namespace}/` : ""}${result.slug} v${result.version}`, result.isNewVersion ? "New content version." : "Metadata-only update (content unchanged)."];
|
|
936
1152
|
await writeDashboard(ctx.cwd, PACKAGE_VERSION);
|
|
@@ -1069,4 +1285,123 @@ export default function codeCartographerExtension(pi) {
|
|
|
1069
1285
|
ctx.ui.notify("Dashboard regenerated: .codecarto/dashboard.html", "info");
|
|
1070
1286
|
},
|
|
1071
1287
|
});
|
|
1288
|
+
pi.registerCommand("codecarto-refresh-scaffold", {
|
|
1289
|
+
description: "Refresh the framework-owned .codecarto/ files (GUIDE.md, templates/, workflow/ pipelines and VALIDATE.md) from the packaged template, after confirming; project state is untouched",
|
|
1290
|
+
handler: async (_args, ctx) => {
|
|
1291
|
+
const state = await ensureWorkspaceState(ctx);
|
|
1292
|
+
if (!state)
|
|
1293
|
+
return;
|
|
1294
|
+
// Pi can ask, so it shows the exact file set before overwriting
|
|
1295
|
+
// anything — MCP's codecarto_refresh_scaffold writes on call.
|
|
1296
|
+
let files;
|
|
1297
|
+
try {
|
|
1298
|
+
files = await listScaffoldRefreshFiles();
|
|
1299
|
+
}
|
|
1300
|
+
catch (error) {
|
|
1301
|
+
ctx.ui.notify(error instanceof Error ? error.message : String(error), "error");
|
|
1302
|
+
return;
|
|
1303
|
+
}
|
|
1304
|
+
const approved = await ctx.ui.confirm("Refresh the .codecarto/ scaffold from the packaged template?", describeScaffoldRefreshPreview(files, state.scaffoldVersion));
|
|
1305
|
+
if (!approved) {
|
|
1306
|
+
ctx.ui.notify("Scaffold refresh cancelled. Nothing was written.", "info");
|
|
1307
|
+
return;
|
|
1308
|
+
}
|
|
1309
|
+
try {
|
|
1310
|
+
const result = await refreshScaffold(ctx.cwd);
|
|
1311
|
+
const transition = `${result.scaffoldVersionBefore ?? "unversioned"} → ${result.scaffoldVersionAfter}`;
|
|
1312
|
+
lastFeedbackLines = [
|
|
1313
|
+
`Refreshed ${result.written.length} framework-owned file(s) from the packaged template (${transition}).`,
|
|
1314
|
+
"Project state, user config, findings outputs, scratch, closeouts, and orchestrator files were not touched.",
|
|
1315
|
+
"THREAD_LOG.md: one scaffold-refresh entry appended.",
|
|
1316
|
+
];
|
|
1317
|
+
// Re-read state so the widget's staleness line clears with the marker.
|
|
1318
|
+
await refreshWorkspaceUi(ctx, lastFeedbackLines);
|
|
1319
|
+
ctx.ui.notify(`Refreshed ${result.written.length} framework-owned file(s) (${transition}).`, "info");
|
|
1320
|
+
}
|
|
1321
|
+
catch (error) {
|
|
1322
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1323
|
+
lastFeedbackLines = [message];
|
|
1324
|
+
setUiState(ctx, state, lastFeedbackLines);
|
|
1325
|
+
ctx.ui.notify(`Scaffold refresh failed: ${message}`, "error");
|
|
1326
|
+
}
|
|
1327
|
+
},
|
|
1328
|
+
});
|
|
1329
|
+
pi.registerCommand("codecarto-amend", {
|
|
1330
|
+
description: "Apply a post-pipeline amendment from .codecarto/scratch/amendments/, after a preview: /codecarto-amend <name | scratch/amendments/name.yaml>",
|
|
1331
|
+
getArgumentCompletions: async (prefix) => {
|
|
1332
|
+
const names = await listAmendmentNames(join(sessionCwd ?? process.cwd(), ".codecarto"));
|
|
1333
|
+
const items = names
|
|
1334
|
+
.filter((value) => value.startsWith(prefix))
|
|
1335
|
+
.map((value) => ({ value, label: value }));
|
|
1336
|
+
return items.length > 0 ? items : null;
|
|
1337
|
+
},
|
|
1338
|
+
handler: async (args, ctx) => {
|
|
1339
|
+
const state = await ensureWorkspaceState(ctx);
|
|
1340
|
+
if (!state)
|
|
1341
|
+
return;
|
|
1342
|
+
if (!args.trim()) {
|
|
1343
|
+
const staged = await listAmendmentNames(state.workspaceDir);
|
|
1344
|
+
const hint = staged.length > 0
|
|
1345
|
+
? ` (staged: ${staged.join(", ")})`
|
|
1346
|
+
: " — write .codecarto/scratch/amendments/<name>.yaml first (see templates/amendment.yaml)";
|
|
1347
|
+
ctx.ui.notify(`Usage: /codecarto-amend <name>${hint}`, "warning");
|
|
1348
|
+
return;
|
|
1349
|
+
}
|
|
1350
|
+
const name = resolveAmendmentName(args, ctx.cwd);
|
|
1351
|
+
if (!name) {
|
|
1352
|
+
ctx.ui.notify(`Amendments are read from .codecarto/scratch/amendments/ only; pass the amendment name or a path inside that directory, not ${args.trim()}.`, "error");
|
|
1353
|
+
return;
|
|
1354
|
+
}
|
|
1355
|
+
// The same refusals codecarto_amend surfaces, raised before the
|
|
1356
|
+
// confirmation so nobody approves an amendment that cannot apply.
|
|
1357
|
+
let amendment;
|
|
1358
|
+
try {
|
|
1359
|
+
amendment = await loadAmendmentFile(name, state.workspaceDir);
|
|
1360
|
+
}
|
|
1361
|
+
catch (error) {
|
|
1362
|
+
ctx.ui.notify(error instanceof Error ? error.message : String(error), "error");
|
|
1363
|
+
return;
|
|
1364
|
+
}
|
|
1365
|
+
const nextPhase = getNextEligiblePhase(state);
|
|
1366
|
+
if (nextPhase) {
|
|
1367
|
+
ctx.ui.notify(`Cannot amend: the pipeline is not complete (next phase: ${nextPhase.id}). `
|
|
1368
|
+
+ "Resolve open questions and routed items through that phase's handoff (open_question_closures / carry_forward_closures) instead.", "error");
|
|
1369
|
+
return;
|
|
1370
|
+
}
|
|
1371
|
+
// Pi can ask, so the amendment is previewed against status.yaml
|
|
1372
|
+
// before anything is written — MCP's codecarto_amend applies on call.
|
|
1373
|
+
const approved = await ctx.ui.confirm(`Apply amendment "${amendment.slug}"?`, describeAmendmentPreview(amendment, state));
|
|
1374
|
+
if (!approved) {
|
|
1375
|
+
ctx.ui.notify(`Amendment ${amendment.slug} cancelled. Nothing was written.`, "info");
|
|
1376
|
+
return;
|
|
1377
|
+
}
|
|
1378
|
+
try {
|
|
1379
|
+
const { applied, closeoutNotice } = await applyAmendment(ctx.cwd, name);
|
|
1380
|
+
// An amendment exists precisely to change the numbers the dashboard
|
|
1381
|
+
// shows; refresh it, reporting only a render that actually landed.
|
|
1382
|
+
const dashboardWritten = await writeDashboard(ctx.cwd, PACKAGE_VERSION);
|
|
1383
|
+
const lines = [
|
|
1384
|
+
`Amendment applied: ${amendment.slug}`,
|
|
1385
|
+
`Open questions closed: ${applied.openQuestionsClosed.length > 0 ? applied.openQuestionsClosed.join(", ") : "none"}`,
|
|
1386
|
+
`Post-pipeline items closed: ${applied.postPipelineClosed.length > 0 ? applied.postPipelineClosed.join(", ") : "none"}`,
|
|
1387
|
+
];
|
|
1388
|
+
if (applied.unknownIds.length > 0)
|
|
1389
|
+
lines.push(`Ids that matched nothing (already closed or unknown): ${applied.unknownIds.join(", ")}`);
|
|
1390
|
+
lines.push(closeoutNotice);
|
|
1391
|
+
if (dashboardWritten)
|
|
1392
|
+
lines.push("Dashboard refreshed: .codecarto/dashboard.html");
|
|
1393
|
+
lastFeedbackLines = lines;
|
|
1394
|
+
await refreshWorkspaceUi(ctx, lastFeedbackLines);
|
|
1395
|
+
const closed = applied.openQuestionsClosed.length + applied.postPipelineClosed.length;
|
|
1396
|
+
ctx.ui.notify(`Amendment ${amendment.slug} applied: ${applied.openQuestionsClosed.length} open question(s) and ${applied.postPipelineClosed.length} post-pipeline item(s) closed`
|
|
1397
|
+
+ `${applied.unknownIds.length > 0 ? `; ${applied.unknownIds.length} id(s) matched nothing` : ""}.`, closed === 0 || applied.unknownIds.length > 0 ? "warning" : "info");
|
|
1398
|
+
}
|
|
1399
|
+
catch (error) {
|
|
1400
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1401
|
+
lastFeedbackLines = [message];
|
|
1402
|
+
setUiState(ctx, state, lastFeedbackLines);
|
|
1403
|
+
ctx.ui.notify(`Amendment failed: ${message}`, "error");
|
|
1404
|
+
}
|
|
1405
|
+
},
|
|
1406
|
+
});
|
|
1072
1407
|
}
|
|
@@ -17,7 +17,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
17
17
|
import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } from "@modelcontextprotocol/sdk/types.js";
|
|
18
18
|
import { mkdir, readFile, readdir, rename, writeFile } from "node:fs/promises";
|
|
19
19
|
import { basename, isAbsolute, join } from "node:path";
|
|
20
|
-
import { buildPhasePrompt, buildSkillPrompt, buildValidationSummary, BROADSIDE_DIR, broadsideDirFor, BROADSIDE_LENS_IDS, BROADSIDE_SKILL_NAME, canonicalPath, copyPackagedWorkspace, collectResultText, completeValidatedPhase, computePerPhaseTotals, computeTotals, createEmptyStatus, DEFAULT_PIPELINE_PATH, deriveSlug, discoverLibrary, describeScaffoldStaleness, detectProvenanceConflicts, estimateSubmitText, getLens, getNextEligiblePhase, getPipelineLabel, getWorkspaceState, isValidSlug, isWithinPathResolved, listBatchModels, listEntries, listGuideTopics, loadBroadsideConfig, modelsText, readGuide, listSkillNames, loadCodecartoConfig, loadUsage, loadYamlFile, normalizeForComparison, PACKAGE_VERSION, packagedWorkspaceDir, pathExists, PhasePreflightError, publishEntry, reindex as libraryReindex, refreshScaffold, resolvePhase, resolvePipelineChoice, readBroadsideSkill, runBroadsideCollect, runBroadsideStatus, runBroadsideSubmit, seedOrchestratorFiles, statusText, stringifySimpleYaml, switchPipeline, validatePhaseOutput, writeLibraryConfig, } from "../core/index.js";
|
|
20
|
+
import { buildPhasePrompt, buildSkillPrompt, buildValidationSummary, BROADSIDE_DIR, broadsideDirFor, BROADSIDE_LENS_IDS, BROADSIDE_SKILL_NAME, canonicalPath, copyPackagedWorkspace, collectResultText, completeValidatedPhase, computePerPhaseTotals, computeTotals, createEmptyStatus, DEFAULT_PIPELINE_PATH, deriveSlug, discoverLibrary, describeScaffoldStaleness, detectProvenanceConflicts, estimateSubmitText, getLens, getNextEligiblePhase, getPipelineLabel, getWorkspaceState, isValidSlug, isWithinPathResolved, listBatchModels, listEntries, listGuideTopics, loadBroadsideConfig, modelsText, readGuide, listSkillNames, loadCodecartoConfig, loadUsage, loadYamlFile, normalizeForComparison, PACKAGE_VERSION, packagedWorkspaceDir, pathExists, PhasePreflightError, previewPublishVersion, publishEntry, reindex as libraryReindex, refreshScaffold, resolvePhase, resolvePipelineChoice, readBroadsideSkill, runBroadsideCollect, runBroadsideStatus, runBroadsideSubmit, seedOrchestratorFiles, statusText, stringifySimpleYaml, switchPipeline, validatePhaseOutput, writeLibraryConfig, } from "../core/index.js";
|
|
21
21
|
import { applyAmendment } from "../core/amendment.js";
|
|
22
22
|
import { appendUsageRun } from "../core/usage.js";
|
|
23
23
|
import { initLibrary } from "../core/library.js";
|
|
@@ -370,6 +370,17 @@ async function resolveLibraryPath(args) {
|
|
|
370
370
|
}
|
|
371
371
|
throw new McpError(ErrorCode.InvalidParams, "library_path is required (pass it explicitly, or pass cwd and configure library.path in ~/.codecarto/config.yaml or .codecarto/workflow/config.yaml).");
|
|
372
372
|
}
|
|
373
|
+
/**
|
|
374
|
+
* The configuration `codecarto_config` reports for the same `cwd`: workspace
|
|
375
|
+
* over user-global when a cwd is given, user-global alone otherwise. Publish
|
|
376
|
+
* reads its gate from here so the value the config tool displays is the value
|
|
377
|
+
* the publish tool enforces.
|
|
378
|
+
*/
|
|
379
|
+
async function loadEffectiveConfig(cwd) {
|
|
380
|
+
return typeof cwd === "string" && cwd.trim() !== ""
|
|
381
|
+
? loadCodecartoConfig(join(cwd.trim(), ".codecarto"))
|
|
382
|
+
: loadUserConfig();
|
|
383
|
+
}
|
|
373
384
|
function asStringArray(value, fieldName) {
|
|
374
385
|
if (!Array.isArray(value)) {
|
|
375
386
|
throw new McpError(ErrorCode.InvalidParams, `${fieldName} must be an array of strings`);
|
|
@@ -550,6 +561,50 @@ export async function handlePublish(args) {
|
|
|
550
561
|
const analyzedAt = typeof args.analyzed_at === "string" && args.analyzed_at !== ""
|
|
551
562
|
? args.analyzed_at
|
|
552
563
|
: new Date().toISOString();
|
|
564
|
+
const headline = args.headline.trim();
|
|
565
|
+
const forceNewVersion = args.force_new_version === true;
|
|
566
|
+
// publish_confirm gate (#162). Pi shows a preview and asks before writing.
|
|
567
|
+
// An MCP server has no one to ask, so — as codecarto_broadside does for its
|
|
568
|
+
// spend limit — it refuses with the preview and lets the caller re-invoke
|
|
569
|
+
// with confirm: true. The gate applies only when a config layer actually
|
|
570
|
+
// set the key: the loader's default (true) exists to make Pi's dialog
|
|
571
|
+
// opt-out, and a host that never configured the key keeps the behavior it
|
|
572
|
+
// had. Runs after every argument check so the preview names the resolved
|
|
573
|
+
// slug and namespace, and before publishEntry so nothing is written.
|
|
574
|
+
const config = await loadEffectiveConfig(args.cwd);
|
|
575
|
+
if (config.library.publish_confirm && config.library.publish_confirm_configured && args.confirm !== true) {
|
|
576
|
+
const preview = await previewPublishVersion(libraryPath, spec, { slug, namespace }, { forceNewVersion });
|
|
577
|
+
const label = `${namespace ? `${namespace}/` : ""}${slug}`;
|
|
578
|
+
const versionLine = preview.latestVersion === 0
|
|
579
|
+
? "v1 (first version of a new entry)"
|
|
580
|
+
: preview.isNewVersion
|
|
581
|
+
? `v${preview.version} (new content version; the newest is v${preview.latestVersion})`
|
|
582
|
+
: `v${preview.version} (metadata-only update; the content hash matches the newest version)`;
|
|
583
|
+
const specSource = typeof args.spec_path === "string" && args.spec_path.trim() !== ""
|
|
584
|
+
? args.spec_path.trim()
|
|
585
|
+
: `inline (${spec.length} characters)`;
|
|
586
|
+
throw new McpError(ErrorCode.InvalidRequest, [
|
|
587
|
+
"Publish not performed: library.publish_confirm is set and this call did not carry confirm: true. Nothing was written.",
|
|
588
|
+
`Would publish ${label} to ${libraryPath}`,
|
|
589
|
+
` Version: ${versionLine}`,
|
|
590
|
+
` Source repo: ${sourceRepo}`,
|
|
591
|
+
` Headline: ${headline}`,
|
|
592
|
+
` Confidentiality: ${confidentiality ?? "internal (the default; none declared)"}`,
|
|
593
|
+
` Spec: ${specSource}`,
|
|
594
|
+
"Re-invoke codecarto_publish with the same arguments plus confirm: true to publish, or set library.publish_confirm: false in config to drop this gate.",
|
|
595
|
+
].join("\n"), {
|
|
596
|
+
refused: "publish_confirm",
|
|
597
|
+
libraryPath,
|
|
598
|
+
namespace: namespace ?? null,
|
|
599
|
+
slug,
|
|
600
|
+
version: preview.version,
|
|
601
|
+
isNewVersion: preview.isNewVersion,
|
|
602
|
+
latestVersion: preview.latestVersion,
|
|
603
|
+
source_repo: sourceRepo,
|
|
604
|
+
headline,
|
|
605
|
+
confidentiality: confidentiality ?? null,
|
|
606
|
+
});
|
|
607
|
+
}
|
|
553
608
|
const result = await publishEntry(libraryPath, spec, {
|
|
554
609
|
slug,
|
|
555
610
|
namespace: namespace ?? undefined,
|
|
@@ -560,13 +615,13 @@ export async function handlePublish(args) {
|
|
|
560
615
|
analyzed_at: analyzedAt,
|
|
561
616
|
pipeline: defaults.pipeline,
|
|
562
617
|
codecarto_version: PACKAGE_VERSION,
|
|
563
|
-
headline
|
|
618
|
+
headline,
|
|
564
619
|
tags,
|
|
565
620
|
capabilities,
|
|
566
621
|
confidentiality,
|
|
567
622
|
generation,
|
|
568
623
|
}, {
|
|
569
|
-
forceNewVersion
|
|
624
|
+
forceNewVersion,
|
|
570
625
|
allowSourceRepoChange: args.allow_source_repo_change === true,
|
|
571
626
|
allowConfidentialityMismatch: args.allow_confidentiality_mismatch === true,
|
|
572
627
|
});
|
|
@@ -1060,7 +1115,7 @@ const TOOLS = [
|
|
|
1060
1115
|
},
|
|
1061
1116
|
{
|
|
1062
1117
|
name: "codecarto_publish",
|
|
1063
|
-
description: "Publish a reimplementation-spec to a CodeCartographer library. Identified by library_path (absolute) or cwd's config.yaml. Content-hash idempotent — re-publishing identical spec bytes updates metadata in place rather than bumping the version. Required: source_repo, headline, and either spec (inline) or spec_path (absolute file). Slug derives from source_repo if not provided. If the library is namespaced, namespace is required (or pass cwd to inherit from config). Generation context (agent, model, vendor, reasoning) is passed via model_metadata so the host can record provenance; omitted fields default to 'unknown'.",
|
|
1118
|
+
description: "Publish a reimplementation-spec to a CodeCartographer library. Identified by library_path (absolute) or cwd's config.yaml. Content-hash idempotent — re-publishing identical spec bytes updates metadata in place rather than bumping the version. Required: source_repo, headline, and either spec (inline) or spec_path (absolute file). Slug derives from source_repo if not provided. If the library is namespaced, namespace is required (or pass cwd to inherit from config). Generation context (agent, model, vendor, reasoning) is passed via model_metadata so the host can record provenance; omitted fields default to 'unknown'. When library.publish_confirm is set in config, a call without confirm: true is refused with a preview of what would be published (nothing is written); re-invoke with confirm: true to publish.",
|
|
1064
1119
|
inputSchema: {
|
|
1065
1120
|
type: "object",
|
|
1066
1121
|
properties: {
|
|
@@ -1104,6 +1159,10 @@ const TOOLS = [
|
|
|
1104
1159
|
type: "boolean",
|
|
1105
1160
|
description: "Permit publishing when the entry's confidentiality is more restricted than the library's visibility (internal < shared < public: an internal entry into a shared or public library, a shared entry into a public one). Off by default, because that direction exposes the spec to everyone the library reaches. An omitted confidentiality counts as internal. Set only when the exposure is intended; the recorded confidentiality is not changed.",
|
|
1106
1161
|
},
|
|
1162
|
+
confirm: {
|
|
1163
|
+
type: "boolean",
|
|
1164
|
+
description: "Acknowledge the publish_confirm gate. When library.publish_confirm is set in config (user-global ~/.codecarto/config.yaml, or the workspace's .codecarto/workflow/config.yaml when cwd is passed), a call without confirm: true is refused and the refusal previews what would be published — library, entry, whether a new version or a metadata-only update, source_repo, headline, confidentiality — so the caller can show it before committing to the write. Pass true to publish. No effect when publish_confirm is unset or false.",
|
|
1165
|
+
},
|
|
1107
1166
|
},
|
|
1108
1167
|
required: ["source_repo", "headline"],
|
|
1109
1168
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codecartographer-pi",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.0",
|
|
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",
|