codecartographer-pi 0.17.0 → 0.18.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.
Files changed (66) hide show
  1. package/.codecarto/GUIDE.md +3 -3
  2. package/.codecarto/findings/architecture/SKILL.md +1 -0
  3. package/.codecarto/findings/contracts/SKILL.md +1 -0
  4. package/.codecarto/findings/defect-scan/SKILL.md +15 -1
  5. package/.codecarto/findings/defect-scan/passes/01-logic-and-correctness.md +1 -1
  6. package/.codecarto/findings/defect-scan/passes/02-error-handling.md +1 -1
  7. package/.codecarto/findings/defect-scan/passes/03-concurrency-and-resources.md +1 -1
  8. package/.codecarto/findings/defect-scan/passes/04-security-and-trust.md +1 -1
  9. package/.codecarto/findings/defect-scan/passes/05-api-contract-violations.md +2 -1
  10. package/.codecarto/findings/defect-scan/passes/06-config-and-environment.md +1 -1
  11. package/.codecarto/findings/defect-scan-mechanical/SKILL.md +3 -2
  12. package/.codecarto/findings/defect-scan-semantic/SKILL.md +2 -0
  13. package/.codecarto/findings/porting/SKILL.md +2 -1
  14. package/.codecarto/findings/protocols/SKILL.md +1 -0
  15. package/.codecarto/findings/reimplementation-spec/SKILL.md +1 -1
  16. package/.codecarto/skills/spec-delta-application/SKILL.md +1 -1
  17. package/.codecarto/templates/amendment.yaml +3 -3
  18. package/.codecarto/templates/architecture-map.md +1 -1
  19. package/.codecarto/templates/defect-report.md +23 -0
  20. package/.codecarto/templates/mechanical-defects.md +22 -0
  21. package/.codecarto/templates/reverse-engineering-bundle.md +2 -2
  22. package/.codecarto/templates/semantic-defects.md +26 -0
  23. package/.codecarto/templates/spike-report.md +2 -2
  24. package/.codecarto/workflow/VALIDATE.md +1 -1
  25. package/.codecarto/workflow/pipeline-defect-scan.yaml +2 -0
  26. package/.codecarto/workflow/pipeline-full-with-audit.yaml +3 -1
  27. package/.codecarto/workflow/pipeline-full-with-deep-audit.yaml +5 -1
  28. package/.codecarto/workflow/pipeline-scout-first.yaml +5 -1
  29. package/.codecarto/workflow/scaffold-version.yaml +1 -1
  30. package/README.md +14 -10
  31. package/agent-skill/codecartographer/SKILL.md +1 -1
  32. package/agent-skill/codecartographer/references/deep-audit-synthesis.md +4 -1
  33. package/agent-skill/codecartographer/references/library.md +3 -3
  34. package/agent-skill/codecartographer/references/orchestration.md +1 -1
  35. package/agent-skill/codecartographer/references/phase-recovery.md +1 -1
  36. package/dist/core/amendment.d.ts +5 -0
  37. package/dist/core/amendment.js +23 -4
  38. package/dist/core/completion.d.ts +5 -0
  39. package/dist/core/completion.js +18 -2
  40. package/dist/core/dashboard.js +5 -3
  41. package/dist/core/findings.d.ts +59 -0
  42. package/dist/core/findings.js +145 -0
  43. package/dist/core/index.d.ts +1 -0
  44. package/dist/core/index.js +1 -0
  45. package/dist/core/library.d.ts +139 -1
  46. package/dist/core/library.js +291 -40
  47. package/dist/core/orchestrator-config.d.ts +6 -0
  48. package/dist/core/orchestrator-config.js +2 -0
  49. package/dist/core/pipeline.js +15 -0
  50. package/dist/core/prompts.js +1 -1
  51. package/dist/core/status.d.ts +2 -1
  52. package/dist/core/status.js +33 -11
  53. package/dist/core/types.d.ts +6 -0
  54. package/dist/core/utils.d.ts +14 -0
  55. package/dist/core/utils.js +30 -0
  56. package/dist/core/workspace.d.ts +16 -0
  57. package/dist/core/workspace.js +42 -22
  58. package/dist/core/yaml.js +19 -4
  59. package/dist/extensions/codecarto/auto-runner.d.ts +2 -0
  60. package/dist/extensions/codecarto/broadside-flags.d.ts +7 -2
  61. package/dist/extensions/codecarto/broadside-flags.js +22 -9
  62. package/dist/extensions/codecarto/dashboard-narrator.js +1 -1
  63. package/dist/extensions/codecarto/index.js +379 -20
  64. package/dist/extensions/codecarto/phase-compaction.js +4 -0
  65. package/dist/mcp-server/server.js +127 -10
  66. package/package.json +2 -2
@@ -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, 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 () => {
@@ -618,7 +738,7 @@ export default function codeCartographerExtension(pi) {
618
738
  ctx.ui.notify(`Cannot complete ${validation.phaseId}: ${validation.overall}`, "error");
619
739
  return;
620
740
  }
621
- const { updatedState, closeoutNotice } = await autoCompletePhase(ctx, validation);
741
+ const { updatedState, closeoutNotice, warnings } = await autoCompletePhase(ctx, validation);
622
742
  lastFeedbackLines = [
623
743
  `Completed phase: ${validation.phaseId}`,
624
744
  `Validation: ${validation.overall}`,
@@ -626,10 +746,15 @@ export default function codeCartographerExtension(pi) {
626
746
  ];
627
747
  if (closeoutNotice)
628
748
  lastFeedbackLines.push(closeoutNotice);
749
+ const notes = [...(validation.warnings ?? []), ...warnings];
750
+ for (const note of notes)
751
+ lastFeedbackLines.push(`NOTE: ${note} Non-gating.`);
629
752
  setUiState(ctx, updatedState, lastFeedbackLines);
630
- ctx.ui.notify(`Marked ${validation.phaseId} complete`, validation.overall === "PASS WITH GAPS" ? "warning" : "info");
753
+ ctx.ui.notify(`Marked ${validation.phaseId} complete`, validation.overall === "PASS WITH GAPS" || notes.length > 0 ? "warning" : "info");
631
754
  if (closeoutNotice)
632
755
  ctx.ui.notify(closeoutNotice, "info");
756
+ for (const note of notes)
757
+ ctx.ui.notify(note, "warning");
633
758
  },
634
759
  });
635
760
  pi.registerCommand("codecarto-skill", {
@@ -693,6 +818,76 @@ export default function codeCartographerExtension(pi) {
693
818
  ctx.ui.notify(`Queued CodeCartographer skill: ${skillName}`, "info");
694
819
  },
695
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
+ });
696
891
  pi.registerCommand("codecarto-broadside", {
697
892
  description: "Batch reconnaissance (Broad-Side): /codecarto-broadside [submit|collect|status|models] [lenses…] [flags]",
698
893
  getArgumentCompletions: (prefix) => {
@@ -786,7 +981,9 @@ export default function codeCartographerExtension(pi) {
786
981
  lenses,
787
982
  model: config.model,
788
983
  maxCost: flags.maxCost ?? config.maxCost,
789
- incremental: flags.incremental || config.incremental,
984
+ // `??`, not `||`: --no-incremental parses to false and must beat a
985
+ // config-set true, exactly as MCP's `incremental: false` does (#163).
986
+ incremental: flags.incremental ?? config.incremental,
790
987
  // Pi can ask, so it asks instead of refusing over max_cost the
791
988
  // way MCP has to. An approval here IS the force flag.
792
989
  confirm: (estimate) => ctx.ui.confirm(`Broad-Side will spend about $${estimate.totalCost.toFixed(4)}`, describeBroadsideEstimate(estimate)),
@@ -879,35 +1076,78 @@ export default function codeCartographerExtension(pi) {
879
1076
  return;
880
1077
  }
881
1078
  const spec = await readFile(specPath, "utf8");
882
- const slug = deriveSlug(ctx.cwd);
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);
883
1083
  const headline = derivePublishHeadline(spec, ctx.cwd);
884
1084
  const namespace = marker.namespaced ? config.library.namespace ?? undefined : undefined;
885
1085
  if (marker.namespaced && !namespace) {
886
1086
  ctx.ui.notify("The configured library is namespaced; set library.namespace before publishing.", "error");
887
1087
  return;
888
1088
  }
1089
+ const label = `${namespace ? `${namespace}/` : ""}${slug}`;
889
1090
  const preview = [
890
- `Publish ${namespace ? `${namespace}/` : ""}${slug} to ${config.library.path}`,
891
- `Source: ${ctx.cwd}`,
1091
+ `Publish ${label} to ${config.library.path}`,
1092
+ `Source: ${source.source_repo}${source.remote ? ` (git remote ${source.remote})` : ""}`,
892
1093
  `Spec: .codecarto/${phase.primary_output}`,
893
1094
  `Headline: ${headline}`,
894
1095
  `Provenance: Pi / ${ctx.model?.provider ?? "unknown"} / ${ctx.model?.id ?? "unknown"}`,
895
1096
  ].join("\n");
896
1097
  if (config.library.publish_confirm && !(await ctx.ui.confirm("Publish reimplementation spec", preview)))
897
1098
  return;
1099
+ const input = {
1100
+ slug,
1101
+ namespace,
1102
+ source_repo: source.source_repo,
1103
+ analyzed_at: new Date().toISOString(),
1104
+ pipeline: state.status.pipeline,
1105
+ codecarto_version: PACKAGE_VERSION,
1106
+ headline,
1107
+ tags: [],
1108
+ capabilities: [],
1109
+ generation: piGeneration(ctx),
1110
+ };
898
1111
  try {
899
- const result = await publishEntry(config.library.path, spec, {
900
- slug,
901
- namespace,
902
- source_repo: ctx.cwd,
903
- analyzed_at: new Date().toISOString(),
904
- pipeline: state.status.pipeline,
905
- codecarto_version: PACKAGE_VERSION,
906
- headline,
907
- tags: [],
908
- capabilities: [],
909
- generation: piGeneration(ctx),
910
- });
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 = {};
1117
+ let result;
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
+ }
1149
+ }
1150
+ }
911
1151
  lastFeedbackLines = [`Published ${result.namespace ? `${result.namespace}/` : ""}${result.slug} v${result.version}`, result.isNewVersion ? "New content version." : "Metadata-only update (content unchanged)."];
912
1152
  await writeDashboard(ctx.cwd, PACKAGE_VERSION);
913
1153
  await refreshWorkspaceUi(ctx, lastFeedbackLines);
@@ -1045,4 +1285,123 @@ export default function codeCartographerExtension(pi) {
1045
1285
  ctx.ui.notify("Dashboard regenerated: .codecarto/dashboard.html", "info");
1046
1286
  },
1047
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
+ });
1048
1407
  }
@@ -81,6 +81,10 @@ export function phaseCompactionExtension(pi) {
81
81
  return undefined;
82
82
  try {
83
83
  const state = await getWorkspaceState(ctx.cwd);
84
+ // No workspace under this cwd: nothing phase-aware to add, so the host
85
+ // default applies — not a failure worth a warning (#134).
86
+ if (!state)
87
+ return undefined;
84
88
  const phase = state.pipeline.phases.find((candidate) => candidate.id === phaseId);
85
89
  const auth = await ctx.modelRegistry.getApiKeyAndHeaders(ctx.model);
86
90
  if (!auth.ok || !auth.apiKey)