codecartographer-pi 0.19.6 → 0.21.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 (57) hide show
  1. package/.codecarto/GUIDE.md +2 -2
  2. package/.codecarto/broadside/SKILL.md +21 -3
  3. package/.codecarto/broadside/config.yaml +35 -9
  4. package/.codecarto/findings/contracts/SKILL.md +4 -1
  5. package/.codecarto/findings/defect-scan/SKILL.md +10 -0
  6. package/.codecarto/findings/defect-scan-mechanical/SKILL.md +6 -0
  7. package/.codecarto/findings/defect-scan-semantic/SKILL.md +8 -1
  8. package/.codecarto/findings/porting/SKILL.md +4 -0
  9. package/.codecarto/findings/protocols/SKILL.md +4 -0
  10. package/.codecarto/templates/mechanical-defects.md +15 -0
  11. package/.codecarto/templates/reimplementation-spec.md +5 -3
  12. package/.codecarto/templates/reverse-engineering-bundle.md +10 -1
  13. package/.codecarto/templates/semantic-defects.md +15 -0
  14. package/.codecarto/workflow/VALIDATE.md +3 -2
  15. package/.codecarto/workflow/scaffold-version.yaml +1 -1
  16. package/README.md +13 -9
  17. package/dist/core/amendment.js +9 -4
  18. package/dist/core/broadside.d.ts +121 -2
  19. package/dist/core/broadside.js +478 -92
  20. package/dist/core/completion.js +88 -23
  21. package/dist/core/dashboard-writer.d.ts +8 -0
  22. package/dist/core/dashboard-writer.js +159 -0
  23. package/dist/core/index.d.ts +2 -0
  24. package/dist/core/index.js +2 -0
  25. package/dist/core/library.js +9 -4
  26. package/dist/core/orchestrator-config.d.ts +32 -7
  27. package/dist/core/orchestrator-config.js +124 -44
  28. package/dist/core/pipeline.d.ts +73 -0
  29. package/dist/core/pipeline.js +134 -10
  30. package/dist/core/prompts.d.ts +20 -0
  31. package/dist/core/prompts.js +53 -13
  32. package/dist/core/secrets.d.ts +16 -0
  33. package/dist/core/secrets.js +98 -0
  34. package/dist/core/status.d.ts +16 -0
  35. package/dist/core/status.js +47 -20
  36. package/dist/core/synthesis.js +5 -2
  37. package/dist/core/utils.d.ts +7 -0
  38. package/dist/core/utils.js +7 -0
  39. package/dist/core/workspace.d.ts +55 -8
  40. package/dist/core/workspace.js +116 -8
  41. package/dist/core/yaml.js +181 -15
  42. package/dist/extensions/codecarto/agent-rewriter.js +21 -14
  43. package/dist/extensions/codecarto/agent-runner.d.ts +6 -2
  44. package/dist/extensions/codecarto/agent-runner.js +27 -9
  45. package/dist/extensions/codecarto/agent-state.d.ts +0 -2
  46. package/dist/extensions/codecarto/auto-runner.d.ts +1 -1
  47. package/dist/extensions/codecarto/auto-runner.js +27 -8
  48. package/dist/extensions/codecarto/broadside-flags.d.ts +3 -1
  49. package/dist/extensions/codecarto/broadside-flags.js +12 -0
  50. package/dist/extensions/codecarto/dashboard-narrator.js +9 -2
  51. package/dist/extensions/codecarto/dashboard-writer.d.ts +1 -8
  52. package/dist/extensions/codecarto/dashboard-writer.js +5 -154
  53. package/dist/extensions/codecarto/index.js +158 -47
  54. package/dist/extensions/codecarto/phase-compaction.js +5 -1
  55. package/dist/mcp-server/server.d.ts +3 -1
  56. package/dist/mcp-server/server.js +205 -77
  57. package/package.json +3 -2
@@ -1,7 +1,7 @@
1
1
  import { appendFile, copyFile, mkdir, readFile, readdir, writeFile } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
- import { getNextEligiblePhase, resolvePhase, validatePhaseOutput } from "./pipeline.js";
4
- import { applyHandoff, autoAssignIds, buildTerminalNextActions, loadHandoffFile, normalizeStatus } from "./status.js";
3
+ import { recomputeCursor, resolvePhase, validatePhaseOutput } from "./pipeline.js";
4
+ import { applyHandoff, autoAssignIds, loadHandoffFile, normalizeStatus } from "./status.js";
5
5
  import { compareDottedVersions, dateOnly, newlineIfUnterminated, pathExists, uniqueStrings } from "./utils.js";
6
6
  import { getWorkspaceState, updateStatusAtomically } from "./workspace.js";
7
7
  /**
@@ -110,7 +110,13 @@ async function appendDecisionLog(workspaceDir, phaseId, closeoutFile, decisions)
110
110
  const number = String(nextNumber + index).padStart(3, "0");
111
111
  return `D${number} | ${decision.trim()} | ${source} | closeouts/${closeoutFile} §Decisions Beyond Prompt (${phaseId})`;
112
112
  });
113
- content += `${content.endsWith("\n") ? "" : "\n"}${rows.join("\n")}\n`;
113
+ // A row appended straight after the section's explanatory paragraph is
114
+ // rendered as part of that paragraph by most Markdown renderers; a blank
115
+ // line makes the rows their own block (self-audit F4). Rows already
116
+ // present stay contiguous with the new ones.
117
+ const trailing = content.replace(/\n+$/, "").split("\n").pop() ?? "";
118
+ const separator = /^D\d+\s*\|/.test(trailing) || trailing.trim() === "" ? "" : "\n";
119
+ content += `${content.endsWith("\n") ? "" : "\n"}${separator}${rows.join("\n")}\n`;
114
120
  await writeFile(join(workspaceDir, "DECISIONS.md"), content, "utf8");
115
121
  return fresh.length;
116
122
  }
@@ -253,6 +259,80 @@ export function closureEvidenceGateActive(scaffoldVersion) {
253
259
  const comparison = compareDottedVersions(scaffoldVersion, CLOSURE_EVIDENCE_GATE_SCAFFOLD_VERSION);
254
260
  return comparison !== null && comparison >= 0;
255
261
  }
262
+ /**
263
+ * Whether `text` names one of `ids` as a whole token: the id must not run
264
+ * straight into another id character on either side, so `arch-CF2` matches
265
+ * "routed as `arch-CF2`" and not "arch-CF20".
266
+ */
267
+ function mentionsAnyId(text, ids) {
268
+ for (const id of ids) {
269
+ let from = 0;
270
+ while (true) {
271
+ const at = text.indexOf(id, from);
272
+ if (at === -1)
273
+ break;
274
+ const before = at === 0 ? "" : text[at - 1];
275
+ const after = text[at + id.length] ?? "";
276
+ if (!/[A-Za-z0-9_-]/.test(before) && !/[A-Za-z0-9_-]/.test(after))
277
+ return true;
278
+ from = at + 1;
279
+ }
280
+ }
281
+ return false;
282
+ }
283
+ /**
284
+ * Turn each PARTIAL validation row into a `needs-maintainer-decision`
285
+ * question on the phase, unless the row's criterion or evidence cell names an
286
+ * entry the workspace already tracks: an open question, carry-forward, or
287
+ * post-pipeline item, from this handoff or an earlier phase. VALIDATE.md asks
288
+ * the phase to name the tracking entry in the evidence cell; honouring it is
289
+ * what stops a routed gap from also becoming a duplicate question that every
290
+ * later phase re-triages and someone must close (#239).
291
+ *
292
+ * Runs after the handoff has been applied. That is also what keeps the auto
293
+ * ids distinct from the handoff's: both `autoAssignIds` from `oq-<phase>-1`,
294
+ * and assigning the gaps first let a later id-less handoff question replace a
295
+ * gap under the same id.
296
+ */
297
+ function addPartialRowQuestions(status, phaseId, rows) {
298
+ const phase = status.phases[phaseId];
299
+ if (!phase)
300
+ return;
301
+ const trackedIds = new Set();
302
+ for (const phaseState of Object.values(status.phases)) {
303
+ for (const entry of [...(phaseState.open_questions ?? []), ...(phaseState.carry_forward ?? [])]) {
304
+ if (entry.id)
305
+ trackedIds.add(entry.id);
306
+ }
307
+ }
308
+ for (const entry of status.post_pipeline) {
309
+ if (entry.id)
310
+ trackedIds.add(entry.id);
311
+ }
312
+ const gapEntries = [];
313
+ for (const row of rows) {
314
+ if (!row.result.toUpperCase().includes("PARTIAL"))
315
+ continue;
316
+ if (mentionsAnyId(`${row.criterion} ${row.evidence}`, trackedIds))
317
+ continue;
318
+ const candidate = {
319
+ kind: "needs-maintainer-decision",
320
+ description: row.criterion || "Partial validation gap",
321
+ deferred_reason: row.evidence || "Marked PARTIAL by validation",
322
+ };
323
+ // Idempotent across re-runs of completion: the same gap is one question.
324
+ if (phase.open_questions.some((entry) => entry.description === candidate.description && entry.deferred_reason === candidate.deferred_reason))
325
+ continue;
326
+ gapEntries.push(candidate);
327
+ }
328
+ if (gapEntries.length === 0)
329
+ return;
330
+ // Reserve every id already on the phase so the new ones skip them; the
331
+ // placeholders keep autoAssignIds from touching the existing entries.
332
+ const reserved = phase.open_questions.filter((entry) => entry.id).map((entry) => ({ id: entry.id }));
333
+ autoAssignIds([...reserved, ...gapEntries], "oq", phaseId);
334
+ phase.open_questions.push(...gapEntries);
335
+ }
256
336
  export async function completeValidatedPhase(cwd, validation, sourceLabel) {
257
337
  const initialState = await getWorkspaceState(cwd);
258
338
  if (!initialState)
@@ -391,20 +471,6 @@ export async function completeValidatedPhase(cwd, validation, sourceLabel) {
391
471
  open_questions: [],
392
472
  carry_forward: [],
393
473
  };
394
- const gapEntries = lockedValidation.rows
395
- .filter((row) => row.result.toUpperCase().includes("PARTIAL"))
396
- .map((row) => ({
397
- kind: "needs-maintainer-decision",
398
- description: row.criterion || "Partial validation gap",
399
- deferred_reason: row.evidence || "Marked PARTIAL by validation",
400
- }));
401
- autoAssignIds(gapEntries, "oq", validation.phaseId);
402
- const mergedOpenQuestions = [...existingPhase.open_questions];
403
- for (const candidate of gapEntries) {
404
- if (!mergedOpenQuestions.some((entry) => entry.description === candidate.description && entry.deferred_reason === candidate.deferred_reason)) {
405
- mergedOpenQuestions.push(candidate);
406
- }
407
- }
408
474
  nextStatus.phases[validation.phaseId] = {
409
475
  status: "complete",
410
476
  owner_notes: uniqueStrings([
@@ -414,18 +480,17 @@ export async function completeValidatedPhase(cwd, validation, sourceLabel) {
414
480
  `Validation: ${lockedValidation.overall}`,
415
481
  ]),
416
482
  outputs_present: uniqueStrings([...existingPhase.outputs_present, validation.primaryOutput]),
417
- open_questions: mergedOpenQuestions,
483
+ open_questions: [...existingPhase.open_questions],
418
484
  carry_forward: existingPhase.carry_forward ?? [],
419
485
  };
420
486
  if (handoff)
421
487
  applyHandoff(nextStatus, handoff);
488
+ // After the handoff, so a gap the handoff routed is recognised as
489
+ // tracked and the ids the handoff introduced are known (#239).
490
+ addPartialRowQuestions(nextStatus, validation.phaseId, lockedValidation.rows);
422
491
  nextStatus.last_updated = completionTimestamp;
423
492
  const nextWorkspace = { ...lockedState, status: nextStatus };
424
- const nextEligible = getNextEligiblePhase(nextWorkspace);
425
- nextStatus.current_phase = nextEligible?.id ?? "complete";
426
- nextStatus.next_actions = nextEligible
427
- ? [`Begin ${nextEligible.id} phase by producing ${nextEligible.primary_output ?? `findings/${nextEligible.id}/`}`]
428
- : buildTerminalNextActions(nextStatus);
493
+ recomputeCursor(nextWorkspace);
429
494
  return {
430
495
  state: { ...nextWorkspace, status: nextStatus },
431
496
  // The closeout, THREAD_LOG line, decision rows, and staged proposals
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Render and atomically replace `.codecarto/dashboard.html`.
3
+ * @returns true when a fresh dashboard landed on disk; false when the
4
+ * workspace is missing or any gather/render/write step failed (swallowed —
5
+ * lifecycle callers must never fail on a dashboard problem, but they may
6
+ * report truthfully whether a refresh happened).
7
+ */
8
+ export declare function writeDashboard(cwd: string, packageVersion: string): Promise<boolean>;
@@ -0,0 +1,159 @@
1
+ // I/O wrapper that gathers all dashboard inputs and writes the rendered
2
+ // HTML to `.codecarto/dashboard.html`. Best-effort — failures are swallowed
3
+ // and never escalate to a phase error the user sees, mirroring the
4
+ // recordUsage discipline at extensions/codecarto/index.ts.
5
+ //
6
+ // Lived in the Pi extension until #254, with the MCP server reaching across
7
+ // to import it; both surfaces refresh the dashboard at the same lifecycle
8
+ // points (init, completion, amendment, pipeline switch, on demand), so it is
9
+ // a core primitive like everything else they share.
10
+ import { readdir, readFile } from "node:fs/promises";
11
+ import { join } from "node:path";
12
+ import { atomicWriteFile, DASHBOARD_RELATIVE_PATH, getWorkspaceState, loadUsage, NARRATION_CACHE_RELATIVE_PATH, parseSimpleYaml, pathExists, renderDashboard, } from "./index.js";
13
+ const CLOSEOUT_FILENAME_RE = /^(\d{4}-\d{2}-\d{2})-(.+)\.md$/;
14
+ /**
15
+ * Render and atomically replace `.codecarto/dashboard.html`.
16
+ * @returns true when a fresh dashboard landed on disk; false when the
17
+ * workspace is missing or any gather/render/write step failed (swallowed —
18
+ * lifecycle callers must never fail on a dashboard problem, but they may
19
+ * report truthfully whether a refresh happened).
20
+ */
21
+ export async function writeDashboard(cwd, packageVersion) {
22
+ try {
23
+ const state = await getWorkspaceState(cwd);
24
+ if (!state)
25
+ return false;
26
+ const workspaceDir = state.workspaceDir;
27
+ const [usage, closeouts, outputsPresent, narration] = await Promise.all([
28
+ loadUsage(workspaceDir),
29
+ listCloseouts(workspaceDir),
30
+ buildOutputsPresent(state.workspaceDir, state.pipeline),
31
+ loadNarration(workspaceDir),
32
+ ]);
33
+ const inputs = {
34
+ status: state.status,
35
+ pipeline: state.pipeline,
36
+ usage,
37
+ closeouts,
38
+ outputsPresent,
39
+ packageVersion,
40
+ generatedAt: new Date().toISOString(),
41
+ narration,
42
+ };
43
+ const html = renderDashboard(inputs);
44
+ await atomicWriteFile(join(workspaceDir, DASHBOARD_RELATIVE_PATH), html);
45
+ return true;
46
+ }
47
+ catch {
48
+ // Best-effort: a failed dashboard write must not surface as a phase
49
+ // error. The user's pipeline state is unaffected; the next state
50
+ // change will trigger another render attempt.
51
+ return false;
52
+ }
53
+ }
54
+ async function listCloseouts(workspaceDir) {
55
+ const dir = join(workspaceDir, "closeouts");
56
+ if (!(await pathExists(dir)))
57
+ return [];
58
+ let entries;
59
+ try {
60
+ entries = await readdir(dir);
61
+ }
62
+ catch {
63
+ return [];
64
+ }
65
+ const out = [];
66
+ for (const name of entries) {
67
+ const m = CLOSEOUT_FILENAME_RE.exec(name);
68
+ if (!m)
69
+ continue;
70
+ out.push({ date: m[1], phaseOrModule: m[2], fileName: name, summary: await readCloseoutSummary(join(dir, name)) });
71
+ }
72
+ return out;
73
+ }
74
+ async function readCloseoutSummary(path) {
75
+ try {
76
+ const raw = await readFile(path, "utf8");
77
+ const lines = raw.split(/\r?\n/);
78
+ const summaryStart = lines.findIndex((line) => /^##\s+Summary\s*$/i.test(line.trim()));
79
+ if (summaryStart === -1)
80
+ return undefined;
81
+ const body = [];
82
+ for (const line of lines.slice(summaryStart + 1)) {
83
+ if (/^##\s+/.test(line.trim()))
84
+ break;
85
+ const trimmed = line.trim();
86
+ if (!trimmed || trimmed === "-")
87
+ continue;
88
+ body.push(trimmed.replace(/^[-*]\s+/, ""));
89
+ if (body.join(" ").length > 280)
90
+ break;
91
+ }
92
+ const summary = body.join(" ").trim();
93
+ return summary ? `${summary.slice(0, 280)}${summary.length > 280 ? "…" : ""}` : undefined;
94
+ }
95
+ catch {
96
+ return undefined;
97
+ }
98
+ }
99
+ async function buildOutputsPresent(workspaceDir, pipeline) {
100
+ const out = new Map();
101
+ for (const phaseId of pipeline.phase_order) {
102
+ const phaseDef = pipeline.phases.find((p) => p.id === phaseId);
103
+ if (!phaseDef)
104
+ continue;
105
+ const entry = { secondary: [] };
106
+ if (phaseDef.primary_output) {
107
+ entry.primary = {
108
+ path: phaseDef.primary_output,
109
+ exists: await pathExists(join(workspaceDir, phaseDef.primary_output)),
110
+ };
111
+ }
112
+ for (const sec of phaseDef.secondary_outputs ?? []) {
113
+ entry.secondary.push({
114
+ path: sec.path,
115
+ exists: await pathExists(join(workspaceDir, sec.path)),
116
+ });
117
+ }
118
+ out.set(phaseId, entry);
119
+ }
120
+ return out;
121
+ }
122
+ async function loadNarration(workspaceDir) {
123
+ const path = join(workspaceDir, NARRATION_CACHE_RELATIVE_PATH);
124
+ if (!(await pathExists(path)))
125
+ return undefined;
126
+ try {
127
+ const raw = await readFile(path, "utf8");
128
+ const { frontmatter, body } = splitFrontmatter(raw);
129
+ if (!frontmatter)
130
+ return undefined;
131
+ const generatedAt = typeof frontmatter.generatedAt === "string" ? frontmatter.generatedAt : "";
132
+ const phaseCountAtGeneration = typeof frontmatter.phaseCountAtGeneration === "number" ? frontmatter.phaseCountAtGeneration : 0;
133
+ if (!generatedAt)
134
+ return undefined;
135
+ return { content: body.trim(), generatedAt, phaseCountAtGeneration };
136
+ }
137
+ catch {
138
+ return undefined;
139
+ }
140
+ }
141
+ function splitFrontmatter(raw) {
142
+ if (!raw.startsWith("---\n"))
143
+ return { frontmatter: null, body: raw };
144
+ const end = raw.indexOf("\n---\n", 4);
145
+ if (end === -1)
146
+ return { frontmatter: null, body: raw };
147
+ const yamlText = raw.slice(4, end);
148
+ const body = raw.slice(end + 5);
149
+ try {
150
+ const parsed = parseSimpleYaml(yamlText);
151
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
152
+ return { frontmatter: parsed, body };
153
+ }
154
+ }
155
+ catch {
156
+ // fall through
157
+ }
158
+ return { frontmatter: null, body };
159
+ }
@@ -16,3 +16,5 @@ export * from "./dashboard.ts";
16
16
  export * from "./library.ts";
17
17
  export * from "./synthesis.ts";
18
18
  export * from "./broadside.ts";
19
+ export * from "./secrets.ts";
20
+ export * from "./dashboard-writer.ts";
@@ -19,3 +19,5 @@ export * from "./dashboard.js";
19
19
  export * from "./library.js";
20
20
  export * from "./synthesis.js";
21
21
  export * from "./broadside.js";
22
+ export * from "./secrets.js";
23
+ export * from "./dashboard-writer.js";
@@ -33,7 +33,7 @@ import { spawn } from "node:child_process";
33
33
  import { mkdir, readFile, readdir, rename, rm, writeFile } from "node:fs/promises";
34
34
  import { basename, join, resolve } from "node:path";
35
35
  import { acquireLock } from "./status.js";
36
- import { atomicWriteFile, canonicalPath, isPlainObject, normalizeForComparison, pathExists, uniqueTempSuffix } from "./utils.js";
36
+ import { atomicWriteFile, canonicalPath, GIT_TIMEOUT_MS, isPlainObject, normalizeForComparison, pathExists, uniqueTempSuffix } from "./utils.js";
37
37
  import { parseSimpleYaml, stringifySimpleYaml } from "./yaml.js";
38
38
  // ─── Constants ──────────────────────────────────────────────────────────────
39
39
  export const LIBRARY_MARKER_FILE = ".codecarto-library";
@@ -676,7 +676,10 @@ export async function listEntries(libraryRoot, filter = {}) {
676
676
  return false;
677
677
  if (filter.slug !== undefined && e.slug !== filter.slug)
678
678
  return false;
679
- if (filter.source_repo !== undefined && e.source_repo !== filter.source_repo)
679
+ // Same equivalence the publish guard applies: `.git`, scheme, userinfo,
680
+ // default port, and forge-host case do not make two references two
681
+ // repositories, so they must not make a filter miss one either (#257).
682
+ if (filter.source_repo !== undefined && !sameSourceRepo(e.source_repo, filter.source_repo))
680
683
  return false;
681
684
  if (filter.tag !== undefined && !e.tags.includes(filter.tag))
682
685
  return false;
@@ -827,7 +830,7 @@ async function writeIndexMarkdown(libraryRoot, index, marker) {
827
830
  const lines = [];
828
831
  lines.push(`# ${escapeMd(marker.name)} — Library Index`);
829
832
  lines.push("");
830
- lines.push(`_Generated ${index.generated_at}. Do not edit by hand — regenerate with \`codecarto library-reindex\`._`);
833
+ lines.push(`_Generated ${index.generated_at}. Do not edit by hand — regenerate with the \`codecarto_library_reindex\` MCP tool._`);
831
834
  lines.push("");
832
835
  // A single-tenant library has no namespaces but is still one namespace's
833
836
  // worth of entries; count once so the noun agrees with the number shown.
@@ -1038,7 +1041,9 @@ export async function commitPublish(libraryRoot, message, opts = {}) {
1038
1041
  }
1039
1042
  function runGit(cwd, args) {
1040
1043
  return new Promise((resolvePromise) => {
1041
- const child = spawn("git", args, { cwd, stdio: ["ignore", "pipe", "pipe"] });
1044
+ // Bounded like every fetch: a credential helper waiting on a prompt
1045
+ // used to hang publish or source-repo resolution for good (sem 3.12).
1046
+ const child = spawn("git", args, { cwd, stdio: ["ignore", "pipe", "pipe"], timeout: GIT_TIMEOUT_MS });
1042
1047
  let stdout = "";
1043
1048
  let stderr = "";
1044
1049
  child.stdout.on("data", (b) => {
@@ -24,9 +24,24 @@ export interface LibraryConfig {
24
24
  * to hosts that actually configured the key. */
25
25
  publish_confirm_configured: boolean;
26
26
  }
27
+ /** One thing a config file said that the loader could not use. */
28
+ export interface ConfigProblem {
29
+ /** The config file the problem was found in. */
30
+ path: string;
31
+ /** What was wrong and what the loader did about it. */
32
+ message: string;
33
+ }
27
34
  export interface CodecartoConfig {
28
35
  orchestrator: OrchestratorConfig;
29
36
  library: LibraryConfig;
37
+ /**
38
+ * Faults in the config files that were read, in layer order (user-global
39
+ * first). Empty when every file that exists was used in full. The loader
40
+ * never throws on a bad file — a phase run should not be blocked by a
41
+ * typo in a toggle — but publish refuses while this is non-empty, since
42
+ * the library path and the confirm gate come from here.
43
+ */
44
+ problems: ConfigProblem[];
30
45
  }
31
46
  export declare const CONFIG_RELATIVE_PATH = "workflow/config.yaml";
32
47
  export declare const USER_CONFIG_DIR: string;
@@ -56,15 +71,25 @@ export declare function loadCodecartoConfig(workspaceDir: PathLike): Promise<Cod
56
71
  */
57
72
  export declare function loadUserConfig(): Promise<CodecartoConfig>;
58
73
  /**
59
- * Apply one raw config layer over an existing config. Public so tests can
74
+ * Apply one raw config layer over the defaults. Public so tests can
60
75
  * exercise layering without filesystem fixtures, and so wrappers can mock
61
- * a layer in memory (e.g. "what if library_path were X").
76
+ * a layer in memory (e.g. "what if library_path were X"). `sourcePath`
77
+ * names the layer in any problem it produces.
78
+ */
79
+ export declare function mergeConfig(raw: RawConfig | null | undefined, sourcePath?: string): CodecartoConfig;
80
+ /**
81
+ * The lines both surfaces print for a config with problems: one header,
82
+ * then one line per fault naming its file. Empty when there are none.
62
83
  */
63
- export declare function mergeConfig(raw: RawConfig | null | undefined): CodecartoConfig;
84
+ export declare function describeConfigProblems(config: CodecartoConfig): string[];
64
85
  /**
65
- * Write a `library:` block into a config file (user-global or workspace).
66
- * Creates the file and parent directories if needed. Preserves any existing
67
- * `orchestrator:` block. Overwrites the `library:` block if present.
86
+ * Record a library in a config file (user-global or workspace): set
87
+ * `library.path`, and `library.namespace` when one is given. Every other key
88
+ * in the file, `library.publish_confirm` included, is left exactly as it was
89
+ * — library-init writes only what it was asked for, so it cannot switch the
90
+ * MCP confirm gate on behind the user's back (#244). Creates the file and
91
+ * parent directories if needed. A file that exists but does not parse is
92
+ * never overwritten: the caller is told to fix it first.
68
93
  */
69
- export declare function writeLibraryConfig(configPath: string, libraryPath: string, namespace?: string | null, publishConfirm?: boolean): Promise<void>;
94
+ export declare function writeLibraryConfig(configPath: string, libraryPath: string, namespace?: string | null): Promise<void>;
70
95
  export {};