codecartographer-pi 0.8.0 → 0.9.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.
@@ -6,15 +6,53 @@ export interface OrchestratorConfig {
6
6
  * tokens, opt-in. */
7
7
  llm_steer_next_phase: boolean;
8
8
  }
9
+ export interface LibraryConfig {
10
+ /** Absolute, tilde-expanded path to the CodeCartographer library
11
+ * this workspace publishes to and reads from. Null if unconfigured —
12
+ * callers should prompt the user on first use. */
13
+ path: string | null;
14
+ /** Default namespace for entries published by this workspace.
15
+ * Null if unconfigured (single-tenant libraries should use null). */
16
+ namespace: string | null;
17
+ /** Whether `codecarto publish` should display a confirmation prompt
18
+ * with slug + source + library path before writing. Default true. */
19
+ publish_confirm: boolean;
20
+ }
9
21
  export interface CodecartoConfig {
10
22
  orchestrator: OrchestratorConfig;
23
+ library: LibraryConfig;
11
24
  }
12
25
  export declare const CONFIG_RELATIVE_PATH = "workflow/config.yaml";
26
+ export declare const USER_CONFIG_DIR: string;
27
+ export declare const USER_CONFIG_PATH: string;
28
+ /**
29
+ * Tests and tooling can override the user-global config path by setting
30
+ * `CODECARTO_USER_CONFIG_PATH`. The exported constant above is the default
31
+ * for documentation and onboarding flows. Internal load functions go
32
+ * through `resolveUserConfigPath()` so the override takes effect.
33
+ */
34
+ export declare function resolveUserConfigPath(): string;
13
35
  type RawConfig = {
14
36
  orchestrator?: Partial<{
15
37
  llm_steer_next_phase: unknown;
16
38
  }>;
39
+ library?: Partial<{
40
+ path: unknown;
41
+ namespace: unknown;
42
+ publish_confirm: unknown;
43
+ }>;
17
44
  };
18
45
  export declare function loadCodecartoConfig(workspaceDir: PathLike): Promise<CodecartoConfig>;
46
+ /**
47
+ * Read the user-global config directly. Exposed so wrappers can show
48
+ * "your library is at <path>" in onboarding flows without having to
49
+ * load a workspace first.
50
+ */
51
+ export declare function loadUserConfig(): Promise<CodecartoConfig>;
52
+ /**
53
+ * Apply one raw config layer over an existing config. Public so tests can
54
+ * exercise layering without filesystem fixtures, and so wrappers can mock
55
+ * a layer in memory (e.g. "what if library_path were X").
56
+ */
19
57
  export declare function mergeConfig(raw: RawConfig | null | undefined): CodecartoConfig;
20
58
  export {};
@@ -1,45 +1,112 @@
1
- // Workspace-level orchestrator configuration. Lives at
2
- // `.codecarto/workflow/config.yaml`. Missing file or missing keys fall back
3
- // to defaults, so existing workspaces created before this file existed
4
- // keep working unchanged. Schema is intentionally narrow — one surface
5
- // per feature, easy to grow.
6
- import { join } from "node:path";
7
- import { pathExists } from "./utils.js";
1
+ // Workspace-level orchestrator configuration. Two layers:
2
+ //
3
+ // 1. User-global — `~/.codecarto/config.yaml`. Default location for
4
+ // `library.path`, `library.namespace`, `library.publish_confirm`, and
5
+ // the orchestrator toggles. Shared across all workspaces on this
6
+ // machine.
7
+ // 2. Per-workspace `.codecarto/workflow/config.yaml` inside the
8
+ // workspace. Overrides individual keys from the user-global layer.
9
+ //
10
+ // Resolution order (top wins): per-workspace > user-global > defaults.
11
+ // Missing files at either layer fall back to defaults; malformed YAML
12
+ // at either layer is non-fatal (drops the layer, logs nothing).
13
+ //
14
+ // `library.path` is returned tilde-expanded and absolute so consumers
15
+ // don't have to expand themselves.
16
+ import { homedir } from "node:os";
17
+ import { join, resolve } from "node:path";
18
+ import { expandTilde, pathExists } from "./utils.js";
8
19
  import { loadYamlFile } from "./yaml.js";
9
20
  export const CONFIG_RELATIVE_PATH = "workflow/config.yaml";
21
+ export const USER_CONFIG_DIR = join(homedir(), ".codecarto");
22
+ export const USER_CONFIG_PATH = join(USER_CONFIG_DIR, "config.yaml");
23
+ /**
24
+ * Tests and tooling can override the user-global config path by setting
25
+ * `CODECARTO_USER_CONFIG_PATH`. The exported constant above is the default
26
+ * for documentation and onboarding flows. Internal load functions go
27
+ * through `resolveUserConfigPath()` so the override takes effect.
28
+ */
29
+ export function resolveUserConfigPath() {
30
+ return process.env.CODECARTO_USER_CONFIG_PATH ?? USER_CONFIG_PATH;
31
+ }
10
32
  const DEFAULT_CONFIG = {
11
33
  orchestrator: {
12
34
  llm_steer_next_phase: false,
13
35
  },
36
+ library: {
37
+ path: null,
38
+ namespace: null,
39
+ publish_confirm: true,
40
+ },
14
41
  };
15
42
  export async function loadCodecartoConfig(workspaceDir) {
16
- const configPath = join(workspaceDir, CONFIG_RELATIVE_PATH);
17
- if (!(await pathExists(configPath)))
18
- return cloneDefault();
43
+ const userRaw = await loadRawIfExists(resolveUserConfigPath());
44
+ const workspaceRaw = await loadRawIfExists(join(workspaceDir, CONFIG_RELATIVE_PATH));
45
+ return mergeLayered([userRaw, workspaceRaw]);
46
+ }
47
+ /**
48
+ * Read the user-global config directly. Exposed so wrappers can show
49
+ * "your library is at <path>" in onboarding flows without having to
50
+ * load a workspace first.
51
+ */
52
+ export async function loadUserConfig() {
53
+ const userRaw = await loadRawIfExists(resolveUserConfigPath());
54
+ return mergeLayered([userRaw]);
55
+ }
56
+ async function loadRawIfExists(path) {
57
+ if (!(await pathExists(path)))
58
+ return null;
19
59
  try {
20
- const raw = await loadYamlFile(configPath);
21
- return mergeConfig(raw);
60
+ return await loadYamlFile(path);
22
61
  }
23
62
  catch {
24
- // Malformed YAML: fall back to defaults rather than failing the
25
- // command. The user can fix it; a broken config shouldn't block work.
26
- return cloneDefault();
63
+ return null;
27
64
  }
28
65
  }
66
+ function mergeLayered(layers) {
67
+ let merged = cloneDefault();
68
+ for (const layer of layers)
69
+ merged = applyRaw(merged, layer);
70
+ return merged;
71
+ }
72
+ /**
73
+ * Apply one raw config layer over an existing config. Public so tests can
74
+ * exercise layering without filesystem fixtures, and so wrappers can mock
75
+ * a layer in memory (e.g. "what if library_path were X").
76
+ */
29
77
  export function mergeConfig(raw) {
30
- const merged = cloneDefault();
78
+ return applyRaw(cloneDefault(), raw);
79
+ }
80
+ function applyRaw(base, raw) {
31
81
  if (!raw || typeof raw !== "object")
32
- return merged;
82
+ return base;
83
+ const out = {
84
+ orchestrator: { ...base.orchestrator },
85
+ library: { ...base.library },
86
+ };
33
87
  const o = raw.orchestrator;
34
88
  if (o && typeof o === "object") {
35
89
  if (typeof o.llm_steer_next_phase === "boolean") {
36
- merged.orchestrator.llm_steer_next_phase = o.llm_steer_next_phase;
90
+ out.orchestrator.llm_steer_next_phase = o.llm_steer_next_phase;
37
91
  }
38
92
  }
39
- return merged;
93
+ const l = raw.library;
94
+ if (l && typeof l === "object") {
95
+ if (typeof l.path === "string" && l.path.trim() !== "") {
96
+ out.library.path = resolve(expandTilde(l.path.trim()));
97
+ }
98
+ if (typeof l.namespace === "string" && l.namespace.trim() !== "") {
99
+ out.library.namespace = l.namespace.trim();
100
+ }
101
+ if (typeof l.publish_confirm === "boolean") {
102
+ out.library.publish_confirm = l.publish_confirm;
103
+ }
104
+ }
105
+ return out;
40
106
  }
41
107
  function cloneDefault() {
42
108
  return {
43
109
  orchestrator: { ...DEFAULT_CONFIG.orchestrator },
110
+ library: { ...DEFAULT_CONFIG.library },
44
111
  };
45
112
  }
@@ -38,10 +38,22 @@ export function getNextEligiblePhase(state) {
38
38
  }
39
39
  export function resolvePhase(state, phaseId) {
40
40
  const trimmed = phaseId?.trim();
41
- if (trimmed) {
42
- return getPhaseMap(state.pipeline).get(trimmed) ?? null;
41
+ if (!trimmed)
42
+ return getNextEligiblePhase(state);
43
+ const exact = getPhaseMap(state.pipeline).get(trimmed);
44
+ if (exact)
45
+ return exact;
46
+ // Fall back to matching the primary_output filename. Validation errors
47
+ // surface that path (e.g. "Missing primary output: .codecarto/findings/
48
+ // protocols/protocols-and-state.md"), so users naturally paste it back as
49
+ // the phase argument. Accept the basename with or without the .md suffix.
50
+ const wanted = basename(trimmed, ".md");
51
+ for (const phase of state.pipeline.phases) {
52
+ if (phase.primary_output && basename(phase.primary_output, ".md") === wanted) {
53
+ return phase;
54
+ }
43
55
  }
44
- return getNextEligiblePhase(state);
56
+ return null;
45
57
  }
46
58
  export function resolvePipelineChoice(input) {
47
59
  const trimmed = input.trim();
@@ -1,7 +1,18 @@
1
1
  import type { CarryForwardEntry, OpenQuestionEntry, PipelinePhase, ValidationResult, WorkspaceState } from "./types.ts";
2
2
  export declare function describeEntry(entry: OpenQuestionEntry | CarryForwardEntry): string;
3
3
  export declare function collectRoutedCarryForward(state: WorkspaceState, targetPhaseId: string): CarryForwardEntry[];
4
- export declare function buildPhasePrompt(state: WorkspaceState, phase: PipelinePhase, forced: boolean): Promise<string>;
4
+ export interface BuildPhasePromptOptions {
5
+ /**
6
+ * Set when the phase is being run inside `/codecarto-next --auto` (or any
7
+ * other non-interactive driver). Suppresses interactive hooks that would
8
+ * otherwise pause the sub-agent to ask the user a question — those hooks
9
+ * are the dominant cause of the auto loop wedging at `reimplementation-spec`
10
+ * with a `MISSING` primary output. Each suppressed hook documents the
11
+ * default it falls back to.
12
+ */
13
+ auto?: boolean;
14
+ }
15
+ export declare function buildPhasePrompt(state: WorkspaceState, phase: PipelinePhase, forced: boolean, options?: BuildPhasePromptOptions): Promise<string>;
5
16
  export declare function closeoutFileName(date: string, phaseOrModule: string): string;
6
17
  export declare function buildThreadLogEntry(phaseOrModule: string, validation: ValidationResult, timestamp: string): string;
7
18
  export declare function ensureCloseoutStub(workspaceDir: string, phaseOrModule: string, timestamp: string): Promise<string | null>;
@@ -26,7 +26,7 @@ export function collectRoutedCarryForward(state, targetPhaseId) {
26
26
  }
27
27
  return routed;
28
28
  }
29
- export async function buildPhasePrompt(state, phase, forced) {
29
+ export async function buildPhasePrompt(state, phase, forced, options = {}) {
30
30
  const lines = [
31
31
  `Read .codecarto/GUIDE.md and continue the CodeCartographer workflow for the phase \`${phase.id}\`.`,
32
32
  `Work on this phase only. The analyzed source code is the repository outside .codecarto/.`,
@@ -66,11 +66,20 @@ export async function buildPhasePrompt(state, phase, forced) {
66
66
  }
67
67
  if (phase.id === "reimplementation-spec") {
68
68
  lines.push("");
69
- lines.push("Strategic Alignment Hook (run BEFORE producing the spec):");
70
- lines.push("- Confirm with the user whether this spec should be language-agnostic or opinionated:");
71
- lines.push(" - language-agnostic use templates/reimplementation-spec.md (default).");
72
- lines.push(" - opinionated (target stack locked) use templates/reimplementation-spec-opinionated.md.");
73
- lines.push("- Record the chosen variant in the spec front-matter and in your validation block.");
69
+ if (options.auto) {
70
+ lines.push("Strategic Alignment Hook (auto run DO NOT ask the user):");
71
+ lines.push("- This phase is running inside `/codecarto-next --auto`. The user is not in the loop.");
72
+ lines.push("- Default the spec to LANGUAGE-AGNOSTIC: use templates/reimplementation-spec.md.");
73
+ lines.push("- Record `variant: language-agnostic` and `selection: auto-default` in the spec front-matter and in your validation block, so a later opinionated re-run is traceable.");
74
+ lines.push("- Do NOT block on the user. If you would otherwise pause to ask about target stack, project name, or scope cuts, instead produce the language-agnostic spec and capture each unresolved choice as an `open_questions` entry (`kind: needs-maintainer-decision`) for follow-up.");
75
+ }
76
+ else {
77
+ lines.push("Strategic Alignment Hook (run BEFORE producing the spec):");
78
+ lines.push("- Confirm with the user whether this spec should be language-agnostic or opinionated:");
79
+ lines.push(" - language-agnostic → use templates/reimplementation-spec.md (default).");
80
+ lines.push(" - opinionated (target stack locked) → use templates/reimplementation-spec-opinionated.md.");
81
+ lines.push("- Record the chosen variant in the spec front-matter and in your validation block.");
82
+ }
74
83
  }
75
84
  lines.push("", "Rules:");
76
85
  lines.push("- Do not modify source files outside .codecarto/.");
@@ -90,5 +90,19 @@ function isUsageRun(x) {
90
90
  const r = x;
91
91
  return (typeof r.timestamp === "string" &&
92
92
  typeof r.phase === "string" &&
93
- typeof r.status === "string");
93
+ (r.status === "completed" || r.status === "aborted" || r.status === "error") &&
94
+ isFiniteNumber(r.turn_count) &&
95
+ isFiniteNumber(r.tool_uses) &&
96
+ isFiniteNumber(r.duration_ms) &&
97
+ isUsageTokens(r.tokens) &&
98
+ (r.session_file === undefined || typeof r.session_file === "string"));
99
+ }
100
+ function isUsageTokens(x) {
101
+ if (!x || typeof x !== "object")
102
+ return false;
103
+ const t = x;
104
+ return isFiniteNumber(t.input) && isFiniteNumber(t.output) && isFiniteNumber(t.cache_write);
105
+ }
106
+ function isFiniteNumber(x) {
107
+ return typeof x === "number" && Number.isFinite(x);
94
108
  }
@@ -6,6 +6,14 @@ export declare function isWithinPath(path: string, root: string): boolean;
6
6
  export declare function isPlainObject(value: unknown): value is Record<string, unknown>;
7
7
  export declare function uniqueStrings(items: string[]): string[];
8
8
  export declare function dateOnly(timestamp: string): string;
9
+ /**
10
+ * Expand a leading `~` or `~/` to the user's home directory. Node's `path`
11
+ * module deliberately doesn't do this (it's a shell convention, not a path
12
+ * primitive), so callers that accept user-typed paths (config files,
13
+ * `library.path`) need to expand explicitly before passing to `resolve`.
14
+ * Paths without a leading tilde are returned unchanged.
15
+ */
16
+ export declare function expandTilde(path: string): string;
9
17
  /**
10
18
  * Format an integer as `2.50M` / `2.3k` / `500`. Used by the HTML dashboard
11
19
  * for compact numeric cells. The widget and notify paths have their own
@@ -2,7 +2,8 @@
2
2
  // path-boundary enforcement (Pi tool interception, MCP cwd validation).
3
3
  import { access } from "node:fs/promises";
4
4
  import { constants } from "node:fs";
5
- import { normalize, resolve } from "node:path";
5
+ import { homedir } from "node:os";
6
+ import { join, normalize, resolve } from "node:path";
6
7
  import { realpath } from "node:fs/promises";
7
8
  export function sleep(ms) {
8
9
  return new Promise((resolvePromise) => setTimeout(resolvePromise, ms));
@@ -44,6 +45,21 @@ export function uniqueStrings(items) {
44
45
  export function dateOnly(timestamp) {
45
46
  return timestamp.slice(0, 10);
46
47
  }
48
+ /**
49
+ * Expand a leading `~` or `~/` to the user's home directory. Node's `path`
50
+ * module deliberately doesn't do this (it's a shell convention, not a path
51
+ * primitive), so callers that accept user-typed paths (config files,
52
+ * `library.path`) need to expand explicitly before passing to `resolve`.
53
+ * Paths without a leading tilde are returned unchanged.
54
+ */
55
+ export function expandTilde(path) {
56
+ if (path === "~")
57
+ return homedir();
58
+ if (path.startsWith("~/") || path.startsWith("~\\")) {
59
+ return join(homedir(), path.slice(2));
60
+ }
61
+ return path;
62
+ }
47
63
  /**
48
64
  * Format an integer as `2.50M` / `2.3k` / `500`. Used by the HTML dashboard
49
65
  * for compact numeric cells. The widget and notify paths have their own
@@ -4,6 +4,14 @@ import { type PipelinePhase, type ValidationOverall, type ValidationResult, type
4
4
  export interface RunSinglePhaseOptions {
5
5
  llmSteerEnabled: boolean;
6
6
  signal?: AbortSignal;
7
+ /**
8
+ * True when this phase is being driven by `/codecarto-next --auto`. The flag
9
+ * propagates into `buildPhasePrompt` so interactive hooks (notably the
10
+ * `reimplementation-spec` Strategic Alignment Hook) suppress their user-
11
+ * facing question and fall back to a documented default. The one-shot
12
+ * `/codecarto-next` path leaves this unset and the prompt is unchanged.
13
+ */
14
+ auto?: boolean;
7
15
  }
8
16
  export interface SinglePhaseResult {
9
17
  status: "completed" | "aborted" | "error";
@@ -38,6 +46,14 @@ export interface AutoRunOptions {
38
46
  strict: boolean;
39
47
  llmSteerOverride?: boolean;
40
48
  signal?: AbortSignal;
49
+ /**
50
+ * Fired after each phase is auto-completed and `state` has advanced to the
51
+ * next eligible phase. Lets the caller refresh UI that reflects pipeline
52
+ * progress (the status widget, session name) mid-run instead of only after
53
+ * the whole loop returns — otherwise the readout stays frozen at the
54
+ * initial phase/progress until the auto run finishes.
55
+ */
56
+ onPhaseAdvanced?: (state: WorkspaceState) => void;
41
57
  }
42
58
  export interface AutoRunResult {
43
59
  outcome: AutoOutcome;
@@ -29,7 +29,7 @@ import { appendUsageRun, buildPhasePrompt, buildThreadLogEntry, buildValidationS
29
29
  * getPhaseActivity) and attached the agents widget if it wanted live progress.
30
30
  */
31
31
  export async function runSinglePhase(ctx, pi, state, phase, options) {
32
- let prompt = await buildPhasePrompt(state, phase, false);
32
+ let prompt = await buildPhasePrompt(state, phase, false, { auto: options.auto === true });
33
33
  if (options.llmSteerEnabled) {
34
34
  if (ctx.hasUI)
35
35
  ctx.ui.notify(`Customizing ${phase.id} prompt via LLM rewriter…`, "info");
@@ -87,10 +87,11 @@ export async function runSinglePhase(ctx, pi, state, phase, options) {
87
87
  tokens: activity.lifetimeUsage,
88
88
  durationMs: (activity.completedAt ?? Date.now()) - activity.startedAt,
89
89
  responseText: result.responseText,
90
+ sessionFile: result.sessionFile,
90
91
  }),
91
92
  display: true,
92
93
  });
93
- void recordUsage(state.workspaceDir, phase.id, status, activity);
94
+ void recordUsage(state.workspaceDir, phase.id, status, activity, result.sessionFile);
94
95
  void writeDashboard(ctx.cwd, PACKAGE_VERSION);
95
96
  return {
96
97
  status: result.aborted ? "aborted" : "completed",
@@ -264,6 +265,7 @@ export async function runAuto(ctx, pi, initialState, options) {
264
265
  const phaseResult = await runSinglePhase(ctx, pi, state, phase, {
265
266
  llmSteerEnabled,
266
267
  signal: options.signal,
268
+ auto: true,
267
269
  });
268
270
  // Accumulate tokens whether the phase succeeded, was aborted, or errored.
269
271
  totalTokens.input += phaseResult.activity.lifetimeUsage.input;
@@ -300,6 +302,7 @@ export async function runAuto(ctx, pi, initialState, options) {
300
302
  const { updatedState } = await autoCompletePhase(ctx, validation);
301
303
  state = updatedState;
302
304
  phasesRun.push(phase.id);
305
+ options.onPhaseAdvanced?.(state);
303
306
  }
304
307
  catch (err) {
305
308
  const message = err instanceof Error ? err.message : String(err);
@@ -377,7 +380,7 @@ function recoveryHint(result) {
377
380
  // ----------------------------------------------------------------------------
378
381
  // Helpers
379
382
  // ----------------------------------------------------------------------------
380
- async function recordUsage(workspaceDir, phaseId, status, activity) {
383
+ async function recordUsage(workspaceDir, phaseId, status, activity, sessionFile) {
381
384
  try {
382
385
  await appendUsageRun(workspaceDir, {
383
386
  timestamp: new Date().toISOString(),
@@ -391,6 +394,7 @@ async function recordUsage(workspaceDir, phaseId, status, activity) {
391
394
  output: activity.lifetimeUsage.output,
392
395
  cache_write: activity.lifetimeUsage.cacheWrite,
393
396
  },
397
+ ...(sessionFile ? { session_file: sessionFile } : {}),
394
398
  });
395
399
  }
396
400
  catch {
@@ -56,10 +56,35 @@ async function listCloseouts(workspaceDir) {
56
56
  const m = CLOSEOUT_FILENAME_RE.exec(name);
57
57
  if (!m)
58
58
  continue;
59
- out.push({ date: m[1], phaseOrModule: m[2], fileName: name });
59
+ out.push({ date: m[1], phaseOrModule: m[2], fileName: name, summary: await readCloseoutSummary(join(dir, name)) });
60
60
  }
61
61
  return out;
62
62
  }
63
+ async function readCloseoutSummary(path) {
64
+ try {
65
+ const raw = await readFile(path, "utf8");
66
+ const lines = raw.split(/\r?\n/);
67
+ const summaryStart = lines.findIndex((line) => /^##\s+Summary\s*$/i.test(line.trim()));
68
+ if (summaryStart === -1)
69
+ return undefined;
70
+ const body = [];
71
+ for (const line of lines.slice(summaryStart + 1)) {
72
+ if (/^##\s+/.test(line.trim()))
73
+ break;
74
+ const trimmed = line.trim();
75
+ if (!trimmed || trimmed === "-")
76
+ continue;
77
+ body.push(trimmed.replace(/^[-*]\s+/, ""));
78
+ if (body.join(" ").length > 280)
79
+ break;
80
+ }
81
+ const summary = body.join(" ").trim();
82
+ return summary ? `${summary.slice(0, 280)}${summary.length > 280 ? "…" : ""}` : undefined;
83
+ }
84
+ catch {
85
+ return undefined;
86
+ }
87
+ }
63
88
  async function buildOutputsPresent(workspaceDir, pipeline) {
64
89
  const out = new Map();
65
90
  for (const phaseId of pipeline.phase_order) {
@@ -227,6 +227,15 @@ export default function codeCartographerExtension(pi) {
227
227
  strict: flags.strict,
228
228
  llmSteerOverride: flags.llmSteerOverride,
229
229
  signal: ctx.signal,
230
+ onPhaseAdvanced: (advancedState) => {
231
+ // Refresh the status widget + session name between phases so
232
+ // the readout tracks progress live instead of staying frozen
233
+ // at the initial phase until the whole auto run finishes.
234
+ setUiState(ctx, advancedState, [`Auto pipeline${flags.strict ? " (strict)" : ""} running…`]);
235
+ const phaseId = getNextEligiblePhase(advancedState)?.id ?? advancedState.status.current_phase;
236
+ if (phaseId)
237
+ pi.setSessionName(`CodeCartographer: ${phaseId}`);
238
+ },
230
239
  });
231
240
  const availableSkills = await listSkillNames(state.workspaceDir).catch(() => []);
232
241
  pi.sendMessage({
@@ -302,7 +311,13 @@ export default function codeCartographerExtension(pi) {
302
311
  const state = await ensureWorkspaceState(ctx);
303
312
  if (!state)
304
313
  return;
305
- const validation = await validatePhaseOutput(state, args.trim() || undefined);
314
+ const validation = await validatePhaseOutput(state, args.trim() || undefined).catch((error) => error instanceof Error ? error : new Error(String(error)));
315
+ if (validation instanceof Error) {
316
+ lastFeedbackLines = [validation.message];
317
+ setUiState(ctx, state, lastFeedbackLines);
318
+ ctx.ui.notify(validation.message, "error");
319
+ return;
320
+ }
306
321
  lastFeedbackLines = buildValidationSummary(validation);
307
322
  setUiState(ctx, state, lastFeedbackLines);
308
323
  const level = validation.overall === "FAIL" || validation.overall === "MISSING" ? "error" : validation.overall === "PASS WITH GAPS" ? "warning" : "info";
@@ -315,7 +330,13 @@ export default function codeCartographerExtension(pi) {
315
330
  const currentState = await ensureWorkspaceState(ctx);
316
331
  if (!currentState)
317
332
  return;
318
- const validation = await validatePhaseOutput(currentState, args.trim() || undefined);
333
+ const validation = await validatePhaseOutput(currentState, args.trim() || undefined).catch((error) => error instanceof Error ? error : new Error(String(error)));
334
+ if (validation instanceof Error) {
335
+ lastFeedbackLines = [validation.message];
336
+ setUiState(ctx, currentState, lastFeedbackLines);
337
+ ctx.ui.notify(validation.message, "error");
338
+ return;
339
+ }
319
340
  if (validation.overall === "FAIL" || validation.overall === "MISSING") {
320
341
  lastFeedbackLines = buildValidationSummary(validation);
321
342
  setUiState(ctx, currentState, lastFeedbackLines);
@@ -68,6 +68,27 @@ export declare function handleSkill(args: {
68
68
  }>;
69
69
  structuredContent?: Record<string, unknown>;
70
70
  }>;
71
+ export declare function handlePublish(args: Record<string, unknown>): Promise<{
72
+ content: Array<{
73
+ type: "text";
74
+ text: string;
75
+ }>;
76
+ structuredContent?: Record<string, unknown>;
77
+ }>;
78
+ export declare function handleLibraryList(args: Record<string, unknown>): Promise<{
79
+ content: Array<{
80
+ type: "text";
81
+ text: string;
82
+ }>;
83
+ structuredContent?: Record<string, unknown>;
84
+ }>;
85
+ export declare function handleLibraryReindex(args: Record<string, unknown>): Promise<{
86
+ content: Array<{
87
+ type: "text";
88
+ text: string;
89
+ }>;
90
+ structuredContent?: Record<string, unknown>;
91
+ }>;
71
92
  export declare function buildServer(): Server<{
72
93
  method: string;
73
94
  params?: {