codecartographer-pi 0.19.4 → 0.19.6

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.
@@ -9,8 +9,9 @@ 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 { buildPiGuideMessage } from "./guide-framing.js";
12
+ import { isCtxLive, notifyCtx } from "./notify.js";
12
13
  import { phaseCompactionExtension } from "./phase-compaction.js";
13
- 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";
14
+ import { applyAmendment, buildPhasePrompt, buildSkillPrompt, buildValidationSummary, canonicalPath, copyPackagedWorkspace, computePerPhaseTotals, computeTotals, ConfidentialityMismatchError, createEmptyStatus, DEFAULT_PIPELINE_PATH, describeScaffoldStaleness, deriveSlug, discoverLibrary, getNextEligiblePhase, getPipelineLabel, getWorkspaceState, isWithinPath, resolveExistingPrefix, 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";
14
15
  import { initLibrary } from "../../core/library.js";
15
16
  import { resolveUserConfigPath } from "../../core/orchestrator-config.js";
16
17
  const STATUS_WIDGET_ID = "codecarto-widget";
@@ -84,40 +85,6 @@ function buildStatusLines(state, extraLines = []) {
84
85
  }
85
86
  return lines;
86
87
  }
87
- /**
88
- * Whether `ctx` still belongs to the live session.
89
- *
90
- * Pi invalidates an extension ctx when the session is replaced, and from then
91
- * on *every* property access on it throws — `ctx.cwd` and `ctx.hasUI` included.
92
- * A phase runs as a sub-agent, so by the time post-phase work fires, the ctx
93
- * captured when the command started may already be dead. That is an ordinary
94
- * outcome rather than an error: the UI it would have refreshed is gone with the
95
- * session. Callers skip their UI work instead of throwing into a `void` call
96
- * that nothing is waiting on.
97
- */
98
- function isCtxLive(ctx) {
99
- try {
100
- return typeof ctx.cwd === "string";
101
- }
102
- catch {
103
- return false;
104
- }
105
- }
106
- /**
107
- * Notify through `ctx`, dropping the message if the session it belonged to is
108
- * gone.
109
- *
110
- * `ctx.hasUI` throws on a stale ctx rather than returning false, so the usual
111
- * `if (ctx.hasUI) ctx.ui.notify(...)` guard was itself a throw site. Inside a
112
- * promise chain that was worse than a lost message: the `.catch` handler threw
113
- * while reporting the original failure, and that second rejection had nothing
114
- * left to catch it.
115
- */
116
- function notifyCtx(ctx, message, level) {
117
- if (!isCtxLive(ctx) || !ctx.hasUI)
118
- return;
119
- ctx.ui.notify(message, level);
120
- }
121
88
  function setUiState(ctx, state, extraLines = []) {
122
89
  if (!isCtxLive(ctx) || !ctx.hasUI)
123
90
  return;
@@ -310,8 +277,8 @@ export default function codeCartographerExtension(pi) {
310
277
  setUiState(ctx, null);
311
278
  // The ctx can die between the read above and here, so the error
312
279
  // path must not assume it is still usable either.
313
- if (notifyOnError && isCtxLive(ctx) && ctx.hasUI)
314
- ctx.ui.notify(message, "error");
280
+ if (notifyOnError)
281
+ notifyCtx(ctx, message, "error");
315
282
  return null;
316
283
  }
317
284
  };
@@ -370,24 +337,25 @@ export default function codeCartographerExtension(pi) {
370
337
  if (!(await pathExists(workspaceDir)))
371
338
  return undefined;
372
339
  if (event.toolName === "bash") {
373
- if (ctx.hasUI)
374
- ctx.ui.notify("Blocked bash in CodeCartographer mode", "warning");
340
+ notifyCtx(ctx, "Blocked bash in CodeCartographer mode", "warning");
375
341
  return { block: true, reason: "CodeCartographer mode disables bash to keep source analysis read-only." };
376
342
  }
377
343
  if (event.toolName === "edit" || event.toolName === "write") {
378
344
  const inputPath = typeof event.input.path === "string" ? event.input.path : "";
379
345
  const strippedPath = inputPath.startsWith("@") ? inputPath.slice(1) : inputPath;
380
- const targetPath = await canonicalPath(resolve(ctx.cwd, strippedPath));
346
+ // Resolve through whatever already exists on disk — symlinks included —
347
+ // before appending the unborn tail. `resolve()` would collapse
348
+ // `link/..` lexically and `realpath()` throws on a file that is not
349
+ // there yet, and either gap let a write escape the workspace (#223).
350
+ const targetPath = await resolveExistingPrefix(strippedPath, ctx.cwd);
381
351
  const allowedRoots = [await canonicalPath(workspaceDir)];
382
352
  const config = await loadCodecartoConfig(workspaceDir);
383
353
  if (config.library.path && await discoverLibrary(config.library.path)) {
384
354
  allowedRoots.push(await canonicalPath(config.library.path));
385
355
  }
386
- const withinAllowed = await Promise.all(allowedRoots.map((allowedRoot) => isWithinPathResolved(targetPath, allowedRoot)));
356
+ const withinAllowed = allowedRoots.map((allowedRoot) => isWithinPath(targetPath, allowedRoot));
387
357
  if (!withinAllowed.some((result) => result)) {
388
- if (ctx.hasUI) {
389
- ctx.ui.notify(`Blocked ${event.toolName} outside .codecarto/ or configured library: ${inputPath}`, "warning");
390
- }
358
+ notifyCtx(ctx, `Blocked ${event.toolName} outside .codecarto/ or configured library: ${inputPath}`, "warning");
391
359
  return { block: true, reason: `CodeCartographer mode only allows ${event.toolName} within .codecarto/ or the configured CodeCartographer library.` };
392
360
  }
393
361
  }
@@ -398,7 +366,7 @@ export default function codeCartographerExtension(pi) {
398
366
  handler: async (_args, ctx) => {
399
367
  const workspaceDir = join(ctx.cwd, ".codecarto");
400
368
  if (!(await pathExists(join(workspaceDir, "workflow", "status.yaml")))) {
401
- ctx.ui.notify("No existing CodeCartographer workspace found. Run /codecarto-init first.", "warning");
369
+ notifyCtx(ctx, "No existing CodeCartographer workspace found. Run /codecarto-init first.", "warning");
402
370
  return;
403
371
  }
404
372
  try {
@@ -407,11 +375,11 @@ export default function codeCartographerExtension(pi) {
407
375
  lastFeedbackLines = [`Opened existing workspace: ${getPipelineLabel(state.status.pipeline)}`];
408
376
  pi.setActiveTools(SAFE_TOOL_NAMES);
409
377
  await refreshWorkspaceUi(ctx, lastFeedbackLines);
410
- ctx.ui.notify("Opened existing CodeCartographer workspace without resetting state.", "info");
378
+ notifyCtx(ctx, "Opened existing CodeCartographer workspace without resetting state.", "info");
411
379
  }
412
380
  catch (error) {
413
381
  const message = error instanceof Error ? error.message : String(error);
414
- ctx.ui.notify(`Unable to open CodeCartographer workspace: ${message}`, "error");
382
+ notifyCtx(ctx, `Unable to open CodeCartographer workspace: ${message}`, "error");
415
383
  }
416
384
  },
417
385
  });
@@ -420,7 +388,7 @@ export default function codeCartographerExtension(pi) {
420
388
  handler: async (_args, ctx) => {
421
389
  const interviewPath = join(ctx.cwd, ".codecarto", "findings", "vision-capture", "INTERVIEW.md");
422
390
  if (!(await pathExists(interviewPath))) {
423
- ctx.ui.notify("Vision interview skill not found. Run /codecarto-init synthesis first.", "warning");
391
+ notifyCtx(ctx, "Vision interview skill not found. Run /codecarto-init synthesis first.", "warning");
424
392
  return;
425
393
  }
426
394
  const interviewSkill = await readFile(interviewPath, "utf8");
@@ -442,7 +410,7 @@ export default function codeCartographerExtension(pi) {
442
410
  pi.sendUserMessage(prompt, { deliverAs: "followUp" });
443
411
  }
444
412
  lastFeedbackLines = ["Vision interview started — answer the questions in chat."];
445
- ctx.ui.notify("Vision interview queued — answer the questions in the chat.", "info");
413
+ notifyCtx(ctx, "Vision interview queued — answer the questions in the chat.", "info");
446
414
  },
447
415
  });
448
416
  pi.registerCommand("codecarto-init", {
@@ -457,13 +425,13 @@ export default function codeCartographerExtension(pi) {
457
425
  const trimmedArgs = args.trim();
458
426
  const pipelineChoice = resolvePipelineChoice(trimmedArgs);
459
427
  if (trimmedArgs && !pipelineChoice) {
460
- ctx.ui.notify(`Unknown pipeline: ${trimmedArgs}`, "error");
428
+ notifyCtx(ctx, `Unknown pipeline: ${trimmedArgs}`, "error");
461
429
  return;
462
430
  }
463
431
  const targetWorkspaceDir = join(ctx.cwd, ".codecarto");
464
432
  const sourceWorkspaceDir = packagedWorkspaceDir;
465
433
  if (!(await pathExists(sourceWorkspaceDir))) {
466
- ctx.ui.notify("Packaged .codecarto assets are missing.", "error");
434
+ notifyCtx(ctx, "Packaged .codecarto assets are missing.", "error");
467
435
  return;
468
436
  }
469
437
  const targetExists = await pathExists(targetWorkspaceDir);
@@ -475,8 +443,7 @@ export default function codeCartographerExtension(pi) {
475
443
  return;
476
444
  const backupDir = join(ctx.cwd, `.codecarto-backup-${new Date().toISOString().replace(/[:.]/g, "-")}`);
477
445
  await rename(targetWorkspaceDir, backupDir);
478
- if (ctx.hasUI)
479
- ctx.ui.notify(`Backed up existing workspace to ${basename(backupDir)}/`, "info");
446
+ notifyCtx(ctx, `Backed up existing workspace to ${basename(backupDir)}/`, "info");
480
447
  }
481
448
  }
482
449
  if (!(await pathExists(targetWorkspaceDir))) {
@@ -484,11 +451,12 @@ export default function codeCartographerExtension(pi) {
484
451
  await copyPackagedWorkspace(targetWorkspaceDir);
485
452
  }
486
453
  const rawStatusPath = join(targetWorkspaceDir, "workflow", "status.yaml");
487
- const rawStatus = (await loadYamlFile(rawStatusPath)) ?? {};
454
+ // Absent on a fresh copy: the template ships no status.yaml.
455
+ const rawStatus = (await pathExists(rawStatusPath)) ? ((await loadYamlFile(rawStatusPath)) ?? {}) : {};
488
456
  const selectedPipelinePath = pipelineChoice ?? rawStatus.pipeline?.trim() ?? DEFAULT_PIPELINE_PATH;
489
457
  const resolvedPipelinePath = join(targetWorkspaceDir, selectedPipelinePath);
490
458
  if (!(await pathExists(resolvedPipelinePath))) {
491
- ctx.ui.notify(`Pipeline not found: ${selectedPipelinePath}`, "error");
459
+ notifyCtx(ctx, `Pipeline not found: ${selectedPipelinePath}`, "error");
492
460
  return;
493
461
  }
494
462
  const pipeline = await loadYamlFile(resolvedPipelinePath);
@@ -506,7 +474,7 @@ export default function codeCartographerExtension(pi) {
506
474
  `Initialized workspace with pipeline: ${getPipelineLabel(selectedPipelinePath)}`,
507
475
  "Full run: `/codecarto-next --auto --llm-steer` — or `/codecarto-next` to watch one phase first.",
508
476
  ];
509
- ctx.ui.notify(`Initialized CodeCartographer (${getPipelineLabel(selectedPipelinePath)}). Full run: /codecarto-next --auto --llm-steer`, "info");
477
+ notifyCtx(ctx, `Initialized CodeCartographer (${getPipelineLabel(selectedPipelinePath)}). Full run: /codecarto-next --auto --llm-steer`, "info");
510
478
  // Render the initial dashboard (empty usage, all phases pending) so
511
479
  // the user sees the file exist immediately after /codecarto-init.
512
480
  void writeDashboard(ctx.cwd, PACKAGE_VERSION);
@@ -524,7 +492,7 @@ export default function codeCartographerExtension(pi) {
524
492
  const nextPhase = getNextEligiblePhase(state)?.id ?? "complete";
525
493
  lastFeedbackLines = [`Current phase: ${nextPhase}`, `Pipeline: ${getPipelineLabel(state.status.pipeline)}`];
526
494
  setUiState(ctx, state, lastFeedbackLines);
527
- ctx.ui.notify(`CodeCartographer phase: ${nextPhase}`, "info");
495
+ notifyCtx(ctx, `CodeCartographer phase: ${nextPhase}`, "info");
528
496
  },
529
497
  });
530
498
  pi.registerCommand("codecarto-switch-pipeline", {
@@ -538,12 +506,12 @@ export default function codeCartographerExtension(pi) {
538
506
  handler: async (args, ctx) => {
539
507
  const trimmedArgs = args.trim();
540
508
  if (!trimmedArgs) {
541
- ctx.ui.notify("Usage: /codecarto-switch-pipeline <variant> (e.g. lite, full, synthesis)", "warning");
509
+ notifyCtx(ctx, "Usage: /codecarto-switch-pipeline <variant> (e.g. lite, full, synthesis)", "warning");
542
510
  return;
543
511
  }
544
512
  const pipelineChoice = resolvePipelineChoice(trimmedArgs);
545
513
  if (!pipelineChoice) {
546
- ctx.ui.notify(`Unknown pipeline: ${trimmedArgs}`, "error");
514
+ notifyCtx(ctx, `Unknown pipeline: ${trimmedArgs}`, "error");
547
515
  return;
548
516
  }
549
517
  const state = await ensureWorkspaceState(ctx);
@@ -551,7 +519,7 @@ export default function codeCartographerExtension(pi) {
551
519
  return;
552
520
  const currentPipeline = state.status.pipeline;
553
521
  if (currentPipeline === pipelineChoice) {
554
- ctx.ui.notify(`Already on pipeline: ${getPipelineLabel(pipelineChoice)}`, "info");
522
+ notifyCtx(ctx, `Already on pipeline: ${getPipelineLabel(pipelineChoice)}`, "info");
555
523
  return;
556
524
  }
557
525
  try {
@@ -567,14 +535,14 @@ export default function codeCartographerExtension(pi) {
567
535
  lines.push(`Phases not in new pipeline: ${result.dropped.join(", ")} (findings remain on disk)`);
568
536
  lastFeedbackLines = lines;
569
537
  await refreshWorkspaceUi(ctx, lastFeedbackLines);
570
- ctx.ui.notify(`Switched to pipeline: ${getPipelineLabel(pipelineChoice)}`, "info");
538
+ notifyCtx(ctx, `Switched to pipeline: ${getPipelineLabel(pipelineChoice)}`, "info");
571
539
  void writeDashboard(ctx.cwd, PACKAGE_VERSION);
572
540
  }
573
541
  catch (error) {
574
542
  const message = error instanceof Error ? error.message : String(error);
575
543
  lastFeedbackLines = [message];
576
544
  setUiState(ctx, state, lastFeedbackLines);
577
- ctx.ui.notify(message, "error");
545
+ notifyCtx(ctx, message, "error");
578
546
  }
579
547
  },
580
548
  });
@@ -601,18 +569,18 @@ export default function codeCartographerExtension(pi) {
601
569
  handler: async (args, ctx) => {
602
570
  const flags = parseNextFlags(args);
603
571
  if (flags.error) {
604
- ctx.ui.notify(flags.error, "error");
572
+ notifyCtx(ctx, flags.error, "error");
605
573
  return;
606
574
  }
607
575
  if (flags.unknown.length > 0) {
608
- ctx.ui.notify(`Unknown /codecarto-next flag: ${flags.unknown.join(" ")}`, "error");
576
+ notifyCtx(ctx, `Unknown /codecarto-next flag: ${flags.unknown.join(" ")}`, "error");
609
577
  return;
610
578
  }
611
579
  const state = await ensureWorkspaceState(ctx);
612
580
  if (!state)
613
581
  return;
614
582
  if (flags.auto) {
615
- ctx.ui.notify(`Auto pipeline${flags.strict ? " (strict)" : ""} running…`, "info");
583
+ notifyCtx(ctx, `Auto pipeline${flags.strict ? " (strict)" : ""} running…`, "info");
616
584
  const result = await runAuto(ctx, pi, state, {
617
585
  strict: flags.strict,
618
586
  llmSteerOverride: flags.llmSteerOverride,
@@ -635,14 +603,14 @@ export default function codeCartographerExtension(pi) {
635
603
  });
636
604
  lastFeedbackLines = [`Auto pipeline ${result.outcome}: ${result.reason}`];
637
605
  await refreshWorkspaceUi(ctx, lastFeedbackLines);
638
- ctx.ui.notify(`Auto pipeline ${result.outcome}: ${result.phasesRun.length}/${result.totalPhases} phases.`, result.outcome === "complete" ? "info" : "warning");
606
+ notifyCtx(ctx, `Auto pipeline ${result.outcome}: ${result.phasesRun.length}/${result.totalPhases} phases.`, result.outcome === "complete" ? "info" : "warning");
639
607
  return;
640
608
  }
641
609
  const phase = getNextEligiblePhase(state);
642
610
  if (!phase) {
643
611
  lastFeedbackLines = ["All phases complete."];
644
612
  setUiState(ctx, state, lastFeedbackLines);
645
- ctx.ui.notify("All CodeCartographer phases are complete.", "info");
613
+ notifyCtx(ctx, "All CodeCartographer phases are complete.", "info");
646
614
  return;
647
615
  }
648
616
  let preflight;
@@ -653,13 +621,13 @@ export default function codeCartographerExtension(pi) {
653
621
  const message = error instanceof Error ? error.message : String(error);
654
622
  lastFeedbackLines = [message];
655
623
  setUiState(ctx, state, lastFeedbackLines);
656
- ctx.ui.notify(message, error instanceof PhasePreflightError ? "warning" : "error");
624
+ notifyCtx(ctx, message, error instanceof PhasePreflightError ? "warning" : "error");
657
625
  return;
658
626
  }
659
627
  // Reject re-entry: don't spawn a duplicate runner for a phase that's
660
628
  // already in flight from a previous /codecarto-next invocation.
661
629
  if (isPhaseRunning(phase.id)) {
662
- ctx.ui.notify(`Phase ${phase.id} is already running.`, "warning");
630
+ notifyCtx(ctx, `Phase ${phase.id} is already running.`, "warning");
663
631
  return;
664
632
  }
665
633
  const config = await loadCodecartoConfig(state.workspaceDir);
@@ -735,7 +703,7 @@ export default function codeCartographerExtension(pi) {
735
703
  handler: async (args, ctx) => {
736
704
  const phaseId = args.trim();
737
705
  if (!phaseId) {
738
- ctx.ui.notify("Usage: /codecarto-phase <phase>", "warning");
706
+ notifyCtx(ctx, "Usage: /codecarto-phase <phase>", "warning");
739
707
  return;
740
708
  }
741
709
  const state = await ensureWorkspaceState(ctx);
@@ -743,7 +711,7 @@ export default function codeCartographerExtension(pi) {
743
711
  return;
744
712
  const phase = resolvePhase(state, phaseId);
745
713
  if (!phase) {
746
- ctx.ui.notify(`Unknown phase: ${phaseId}`, "error");
714
+ notifyCtx(ctx, `Unknown phase: ${phaseId}`, "error");
747
715
  return;
748
716
  }
749
717
  let prompt;
@@ -752,7 +720,7 @@ export default function codeCartographerExtension(pi) {
752
720
  }
753
721
  catch (error) {
754
722
  const message = error instanceof Error ? error.message : String(error);
755
- ctx.ui.notify(message, error instanceof PhasePreflightError ? "warning" : "error");
723
+ notifyCtx(ctx, message, error instanceof PhasePreflightError ? "warning" : "error");
756
724
  return;
757
725
  }
758
726
  if (ctx.isIdle()) {
@@ -763,7 +731,7 @@ export default function codeCartographerExtension(pi) {
763
731
  }
764
732
  lastFeedbackLines = [`Queued explicit phase prompt for ${phase.id}`];
765
733
  setUiState(ctx, state, lastFeedbackLines);
766
- ctx.ui.notify(`Queued CodeCartographer phase: ${phase.id}`, "info");
734
+ notifyCtx(ctx, `Queued CodeCartographer phase: ${phase.id}`, "info");
767
735
  },
768
736
  });
769
737
  pi.registerCommand("codecarto-validate", {
@@ -776,13 +744,13 @@ export default function codeCartographerExtension(pi) {
776
744
  if (validation instanceof Error) {
777
745
  lastFeedbackLines = [validation.message];
778
746
  setUiState(ctx, state, lastFeedbackLines);
779
- ctx.ui.notify(validation.message, "error");
747
+ notifyCtx(ctx, validation.message, "error");
780
748
  return;
781
749
  }
782
750
  lastFeedbackLines = buildValidationSummary(validation);
783
751
  setUiState(ctx, state, lastFeedbackLines);
784
752
  const level = validation.overall === "FAIL" || validation.overall === "MISSING" ? "error" : validation.overall === "PASS WITH GAPS" ? "warning" : "info";
785
- ctx.ui.notify(`Validation ${validation.phaseId}: ${validation.overall}`, level);
753
+ notifyCtx(ctx, `Validation ${validation.phaseId}: ${validation.overall}`, level);
786
754
  },
787
755
  });
788
756
  pi.registerCommand("codecarto-complete", {
@@ -795,13 +763,13 @@ export default function codeCartographerExtension(pi) {
795
763
  if (validation instanceof Error) {
796
764
  lastFeedbackLines = [validation.message];
797
765
  setUiState(ctx, currentState, lastFeedbackLines);
798
- ctx.ui.notify(validation.message, "error");
766
+ notifyCtx(ctx, validation.message, "error");
799
767
  return;
800
768
  }
801
769
  if (validation.overall === "FAIL" || validation.overall === "MISSING") {
802
770
  lastFeedbackLines = buildValidationSummary(validation);
803
771
  setUiState(ctx, currentState, lastFeedbackLines);
804
- ctx.ui.notify(`Cannot complete ${validation.phaseId}: ${validation.overall}`, "error");
772
+ notifyCtx(ctx, `Cannot complete ${validation.phaseId}: ${validation.overall}`, "error");
805
773
  return;
806
774
  }
807
775
  // Completion refuses for reasons the framework words carefully — a
@@ -819,7 +787,7 @@ export default function codeCartographerExtension(pi) {
819
787
  const message = error instanceof Error ? error.message : String(error);
820
788
  lastFeedbackLines = [`Completion refused: ${message}`];
821
789
  setUiState(ctx, currentState, lastFeedbackLines);
822
- ctx.ui.notify(message, "error");
790
+ notifyCtx(ctx, message, "error");
823
791
  return;
824
792
  }
825
793
  const { updatedState, closeoutNotice, warnings } = completion;
@@ -834,11 +802,11 @@ export default function codeCartographerExtension(pi) {
834
802
  for (const note of notes)
835
803
  lastFeedbackLines.push(`NOTE: ${note} Non-gating.`);
836
804
  setUiState(ctx, updatedState, lastFeedbackLines);
837
- ctx.ui.notify(`Marked ${validation.phaseId} complete`, validation.overall === "PASS WITH GAPS" || notes.length > 0 ? "warning" : "info");
805
+ notifyCtx(ctx, `Marked ${validation.phaseId} complete`, validation.overall === "PASS WITH GAPS" || notes.length > 0 ? "warning" : "info");
838
806
  if (closeoutNotice)
839
- ctx.ui.notify(closeoutNotice, "info");
807
+ notifyCtx(ctx, closeoutNotice, "info");
840
808
  for (const note of notes)
841
- ctx.ui.notify(note, "warning");
809
+ notifyCtx(ctx, note, "warning");
842
810
  },
843
811
  });
844
812
  pi.registerCommand("codecarto-skill", {
@@ -848,7 +816,7 @@ export default function codeCartographerExtension(pi) {
848
816
  if (!skillName) {
849
817
  const available = await listSkillNames(join(ctx.cwd, ".codecarto"));
850
818
  const hint = available.length > 0 ? ` (available: ${available.join(", ")})` : "";
851
- ctx.ui.notify(`Usage: /codecarto-skill <name>${hint}`, "warning");
819
+ notifyCtx(ctx, `Usage: /codecarto-skill <name>${hint}`, "warning");
852
820
  return;
853
821
  }
854
822
  // Broad-Side is a reading guide for batch reconnaissance output, not a
@@ -858,7 +826,7 @@ export default function codeCartographerExtension(pi) {
858
826
  if (skillName === BROADSIDE_SKILL_NAME) {
859
827
  const skill = await readBroadsideSkill(ctx.cwd).catch(() => null);
860
828
  if (!skill) {
861
- ctx.ui.notify("Broad-Side reading guide not found. Reinstall codecartographer-pi.", "error");
829
+ notifyCtx(ctx, "Broad-Side reading guide not found. Reinstall codecartographer-pi.", "error");
862
830
  return;
863
831
  }
864
832
  const message = [
@@ -872,7 +840,7 @@ export default function codeCartographerExtension(pi) {
872
840
  else {
873
841
  pi.sendUserMessage(message, { deliverAs: "followUp" });
874
842
  }
875
- ctx.ui.notify("Queued the Broad-Side reading guide", "info");
843
+ notifyCtx(ctx, "Queued the Broad-Side reading guide", "info");
876
844
  return;
877
845
  }
878
846
  const state = await ensureWorkspaceState(ctx);
@@ -880,14 +848,14 @@ export default function codeCartographerExtension(pi) {
880
848
  return;
881
849
  const nextPhase = getNextEligiblePhase(state);
882
850
  if (nextPhase) {
883
- ctx.ui.notify(`Cannot run skill: pipeline is not complete (next phase: ${nextPhase.id}). Finish the pipeline before running post-pipeline skills.`, "error");
851
+ notifyCtx(ctx, `Cannot run skill: pipeline is not complete (next phase: ${nextPhase.id}). Finish the pipeline before running post-pipeline skills.`, "error");
884
852
  return;
885
853
  }
886
854
  const skillFile = join(state.workspaceDir, "skills", skillName, "SKILL.md");
887
855
  if (!(await pathExists(skillFile))) {
888
856
  const available = await listSkillNames(state.workspaceDir);
889
857
  const hint = available.length > 0 ? ` (available: ${available.join(", ")})` : " (no skills installed)";
890
- ctx.ui.notify(`Unknown skill: ${skillName}${hint}. The Broad-Side reading guide is served as \`${BROADSIDE_SKILL_NAME}\` and is not pipeline-gated.`, "error");
858
+ notifyCtx(ctx, `Unknown skill: ${skillName}${hint}. The Broad-Side reading guide is served as \`${BROADSIDE_SKILL_NAME}\` and is not pipeline-gated.`, "error");
891
859
  return;
892
860
  }
893
861
  const prompt = await buildSkillPrompt(state, skillName);
@@ -899,7 +867,7 @@ export default function codeCartographerExtension(pi) {
899
867
  }
900
868
  lastFeedbackLines = [`Queued post-pipeline skill: ${skillName}`];
901
869
  setUiState(ctx, state, lastFeedbackLines);
902
- ctx.ui.notify(`Queued CodeCartographer skill: ${skillName}`, "info");
870
+ notifyCtx(ctx, `Queued CodeCartographer skill: ${skillName}`, "info");
903
871
  },
904
872
  });
905
873
  pi.registerCommand("codecarto-list-skills", {
@@ -928,7 +896,7 @@ export default function codeCartographerExtension(pi) {
928
896
  }
929
897
  lastFeedbackLines = lines;
930
898
  setUiState(ctx, state, lastFeedbackLines);
931
- ctx.ui.notify(skills.length > 0
899
+ notifyCtx(ctx, skills.length > 0
932
900
  ? `${skills.length} post-pipeline skill${skills.length === 1 ? "" : "s"}: ${skills.join(", ")}`
933
901
  : "No post-pipeline skills installed.", "info");
934
902
  },
@@ -952,7 +920,7 @@ export default function codeCartographerExtension(pi) {
952
920
  document = await readGuide(args.trim() || undefined);
953
921
  }
954
922
  catch (error) {
955
- ctx.ui.notify(error instanceof Error ? error.message : String(error), "error");
923
+ notifyCtx(ctx, error instanceof Error ? error.message : String(error), "error");
956
924
  return;
957
925
  }
958
926
  const other = topics.filter((name) => name !== document.topic);
@@ -969,7 +937,7 @@ export default function codeCartographerExtension(pi) {
969
937
  lastFeedbackLines = [`Queued the CodeCartographer guide: ${document.topic}`];
970
938
  if (codecartoModeActive)
971
939
  void refreshWorkspaceUi(ctx, lastFeedbackLines).catch(() => undefined);
972
- ctx.ui.notify(`Queued the CodeCartographer guide (${document.topic})`, "info");
940
+ notifyCtx(ctx, `Queued the CodeCartographer guide (${document.topic})`, "info");
973
941
  },
974
942
  });
975
943
  pi.registerCommand("codecarto-broadside", {
@@ -983,12 +951,12 @@ export default function codeCartographerExtension(pi) {
983
951
  handler: async (args, ctx) => {
984
952
  const flags = parseBroadsideFlags(args);
985
953
  if (flags.unknown.length > 0) {
986
- ctx.ui.notify(`Unknown /codecarto-broadside argument: ${flags.unknown.join(" ")}. ` +
954
+ notifyCtx(ctx, `Unknown /codecarto-broadside argument: ${flags.unknown.join(" ")}. ` +
987
955
  `Actions: submit, collect, status, models. Lenses: ${BROADSIDE_LENS_IDS.join(", ")}.`, "error");
988
956
  return;
989
957
  }
990
958
  if (flags.error) {
991
- ctx.ui.notify(flags.error, "error");
959
+ notifyCtx(ctx, flags.error, "error");
992
960
  return;
993
961
  }
994
962
  // Broad-Side runs on any git repository, with or without a workspace —
@@ -1008,7 +976,7 @@ export default function codeCartographerExtension(pi) {
1008
976
  // the result can live, so it holds it instead of being cleared.
1009
977
  ctx.ui.setWidget(BROADSIDE_WIDGET_ID, ["Broad-Side", ...lines]);
1010
978
  }
1011
- ctx.ui.notify(notice, level);
979
+ notifyCtx(ctx, notice, level);
1012
980
  };
1013
981
  if (flags.action === "status") {
1014
982
  const { state } = await runBroadsideStatus(ctx.cwd);
@@ -1018,12 +986,12 @@ export default function codeCartographerExtension(pi) {
1018
986
  }
1019
987
  const apiKey = resolveBroadsideKey(config.apiKey);
1020
988
  if (!apiKey) {
1021
- ctx.ui.notify("No OpenRouter API key. Set OPENROUTER_API_KEY in the environment, or add api_key to " +
989
+ notifyCtx(ctx, "No OpenRouter API key. Set OPENROUTER_API_KEY in the environment, or add api_key to " +
1022
990
  ".codecarto/broadside/config.yaml. (A slash command takes no key: it would land in the transcript.)", "error");
1023
991
  return;
1024
992
  }
1025
993
  if (flags.action === "models") {
1026
- ctx.ui.notify("Fetching the OpenRouter batch-model catalog…", "info");
994
+ notifyCtx(ctx, "Fetching the OpenRouter batch-model catalog…", "info");
1027
995
  try {
1028
996
  const { entries, benchmarks } = await listBatchModels(broadsideDir, config, apiKey, {
1029
997
  includeBenchmarks: flags.benchmarks,
@@ -1031,7 +999,7 @@ export default function codeCartographerExtension(pi) {
1031
999
  finish(modelsText(entries, { benchmarks, defaultModel: config.model }).split("\n"), `Broad-Side: ${entries.length} batch model${entries.length === 1 ? "" : "s"} listed`);
1032
1000
  }
1033
1001
  catch (error) {
1034
- ctx.ui.notify(`Model catalog lookup failed: ${error instanceof Error ? error.message : String(error)}`, "error");
1002
+ notifyCtx(ctx, `Model catalog lookup failed: ${error instanceof Error ? error.message : String(error)}`, "error");
1035
1003
  }
1036
1004
  return;
1037
1005
  }
@@ -1077,10 +1045,10 @@ export default function codeCartographerExtension(pi) {
1077
1045
  if (ctx.hasUI)
1078
1046
  ctx.ui.setWidget(BROADSIDE_WIDGET_ID, undefined);
1079
1047
  if (error instanceof BroadsideCancelledError) {
1080
- ctx.ui.notify("Broad-Side cancelled. Nothing was submitted.", "info");
1048
+ notifyCtx(ctx, "Broad-Side cancelled. Nothing was submitted.", "info");
1081
1049
  return;
1082
1050
  }
1083
- ctx.ui.notify(`Broad-Side submit failed: ${error instanceof Error ? error.message : String(error)}`, "error");
1051
+ notifyCtx(ctx, `Broad-Side submit failed: ${error instanceof Error ? error.message : String(error)}`, "error");
1084
1052
  return;
1085
1053
  }
1086
1054
  const lines = estimateSubmitText(submit, lenses.map(getLens)).split("\n");
@@ -1089,7 +1057,7 @@ export default function codeCartographerExtension(pi) {
1089
1057
  finish(lines, `Broad-Side submitted: run ${submit.runId} (~$${submit.estimatedTotalCost.toFixed(4)})`);
1090
1058
  return;
1091
1059
  }
1092
- ctx.ui.notify(`Broad-Side submitted run ${submit.runId}; polling for up to ${waitSeconds}s…`, "info");
1060
+ notifyCtx(ctx, `Broad-Side submitted run ${submit.runId}; polling for up to ${waitSeconds}s…`, "info");
1093
1061
  try {
1094
1062
  const collect = await runBroadsideCollect(ctx.cwd, apiKey, {
1095
1063
  waitMs,
@@ -1105,7 +1073,7 @@ export default function codeCartographerExtension(pi) {
1105
1073
  ctx.ui.setWidget(BROADSIDE_WIDGET_ID, undefined);
1106
1074
  // The batches are submitted and paid for either way — say so, so
1107
1075
  // nobody re-submits a run that is already in flight.
1108
- ctx.ui.notify(`Broad-Side submitted run ${submit.runId}, but collect failed: ` +
1076
+ notifyCtx(ctx, `Broad-Side submitted run ${submit.runId}, but collect failed: ` +
1109
1077
  `${error instanceof Error ? error.message : String(error)}. Retry with /codecarto-broadside collect.`, "error");
1110
1078
  }
1111
1079
  return;
@@ -1129,7 +1097,7 @@ export default function codeCartographerExtension(pi) {
1129
1097
  catch (error) {
1130
1098
  if (ctx.hasUI)
1131
1099
  ctx.ui.setWidget(BROADSIDE_WIDGET_ID, undefined);
1132
- ctx.ui.notify(`Broad-Side collect failed: ${error instanceof Error ? error.message : String(error)}`, "error");
1100
+ notifyCtx(ctx, `Broad-Side collect failed: ${error instanceof Error ? error.message : String(error)}`, "error");
1133
1101
  }
1134
1102
  },
1135
1103
  });
@@ -1141,22 +1109,22 @@ export default function codeCartographerExtension(pi) {
1141
1109
  return;
1142
1110
  const config = await loadCodecartoConfig(state.workspaceDir);
1143
1111
  if (!config.library.path) {
1144
- ctx.ui.notify("No library.path is configured. Create a library directory with a .codecarto-library marker, then set library.path in ~/.codecarto/config.yaml or .codecarto/workflow/config.yaml.", "error");
1112
+ notifyCtx(ctx, "No library.path is configured. Create a library directory with a .codecarto-library marker, then set library.path in ~/.codecarto/config.yaml or .codecarto/workflow/config.yaml.", "error");
1145
1113
  return;
1146
1114
  }
1147
1115
  const marker = await discoverLibrary(config.library.path);
1148
1116
  if (!marker) {
1149
- ctx.ui.notify(`No CodeCartographer library at ${config.library.path} (missing .codecarto-library). Create a .codecarto-library marker file in that directory.`, "error");
1117
+ notifyCtx(ctx, `No CodeCartographer library at ${config.library.path} (missing .codecarto-library). Create a .codecarto-library marker file in that directory.`, "error");
1150
1118
  return;
1151
1119
  }
1152
1120
  const phase = resolvePhase(state, "reimplementation-spec");
1153
1121
  if (!phase?.primary_output) {
1154
- ctx.ui.notify("The active pipeline does not produce a reimplementation spec to publish.", "error");
1122
+ notifyCtx(ctx, "The active pipeline does not produce a reimplementation spec to publish.", "error");
1155
1123
  return;
1156
1124
  }
1157
1125
  const specPath = join(state.workspaceDir, phase.primary_output);
1158
1126
  if (!(await pathExists(specPath))) {
1159
- ctx.ui.notify(`Reimplementation spec is missing: .codecarto/${phase.primary_output}`, "error");
1127
+ notifyCtx(ctx, `Reimplementation spec is missing: .codecarto/${phase.primary_output}`, "error");
1160
1128
  return;
1161
1129
  }
1162
1130
  const spec = await readFile(specPath, "utf8");
@@ -1167,7 +1135,7 @@ export default function codeCartographerExtension(pi) {
1167
1135
  const headline = derivePublishHeadline(spec, ctx.cwd);
1168
1136
  const namespace = marker.namespaced ? config.library.namespace ?? undefined : undefined;
1169
1137
  if (marker.namespaced && !namespace) {
1170
- ctx.ui.notify("The configured library is namespaced; set library.namespace before publishing.", "error");
1138
+ notifyCtx(ctx, "The configured library is namespaced; set library.namespace before publishing.", "error");
1171
1139
  return;
1172
1140
  }
1173
1141
  const label = `${namespace ? `${namespace}/` : ""}${slug}`;
@@ -1212,7 +1180,7 @@ export default function codeCartographerExtension(pi) {
1212
1180
  // recorded the directory to one that records the git remote.
1213
1181
  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?`);
1214
1182
  if (!moved) {
1215
- ctx.ui.notify("Publish cancelled. Nothing was written.", "info");
1183
+ notifyCtx(ctx, "Publish cancelled. Nothing was written.", "info");
1216
1184
  return;
1217
1185
  }
1218
1186
  options.allowSourceRepoChange = true;
@@ -1222,7 +1190,7 @@ export default function codeCartographerExtension(pi) {
1222
1190
  // default; whether it may go into a wider library is the user's call.
1223
1191
  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?`);
1224
1192
  if (!publishAnyway) {
1225
- ctx.ui.notify("Publish cancelled. Nothing was written.", "info");
1193
+ notifyCtx(ctx, "Publish cancelled. Nothing was written.", "info");
1226
1194
  return;
1227
1195
  }
1228
1196
  options.allowConfidentialityMismatch = true;
@@ -1235,11 +1203,11 @@ export default function codeCartographerExtension(pi) {
1235
1203
  lastFeedbackLines = [`Published ${result.namespace ? `${result.namespace}/` : ""}${result.slug} v${result.version}`, result.isNewVersion ? "New content version." : "Metadata-only update (content unchanged)."];
1236
1204
  await writeDashboard(ctx.cwd, PACKAGE_VERSION);
1237
1205
  await refreshWorkspaceUi(ctx, lastFeedbackLines);
1238
- ctx.ui.notify(`Published ${result.namespace ? `${result.namespace}/` : ""}${result.slug} v${result.version}.`, "info");
1206
+ notifyCtx(ctx, `Published ${result.namespace ? `${result.namespace}/` : ""}${result.slug} v${result.version}.`, "info");
1239
1207
  }
1240
1208
  catch (error) {
1241
1209
  const message = error instanceof Error ? error.message : String(error);
1242
- ctx.ui.notify(`Unable to publish: ${message}`, "error");
1210
+ notifyCtx(ctx, `Unable to publish: ${message}`, "error");
1243
1211
  }
1244
1212
  },
1245
1213
  });
@@ -1251,7 +1219,7 @@ export default function codeCartographerExtension(pi) {
1251
1219
  const namespaceIdx = parts.indexOf("--namespace");
1252
1220
  const namespace = namespaceIdx >= 0 ? parts[namespaceIdx + 1] : null;
1253
1221
  if (!pathArg) {
1254
- ctx.ui.notify("Usage: /codecarto-library-init <path> [--namespace <name>]", "warning");
1222
+ notifyCtx(ctx, "Usage: /codecarto-library-init <path> [--namespace <name>]", "warning");
1255
1223
  return;
1256
1224
  }
1257
1225
  const libraryPath = pathArg.startsWith("~") ? join(homedir(), pathArg.slice(1)) : resolve(pathArg);
@@ -1267,14 +1235,12 @@ export default function codeCartographerExtension(pi) {
1267
1235
  ? `Library already exists at ${libraryPath} (marker preserved). Config updated.`
1268
1236
  : `Created library at ${libraryPath} with marker "${result.marker.name}".`;
1269
1237
  lastFeedbackLines = [msg, `Config written to ${configPath}`];
1270
- if (ctx.hasUI) {
1271
- ctx.ui.notify(msg, "info");
1272
- ctx.ui.notify(`Config written to ${configPath}`, "info");
1273
- }
1238
+ notifyCtx(ctx, msg, "info");
1239
+ notifyCtx(ctx, `Config written to ${configPath}`, "info");
1274
1240
  }
1275
1241
  catch (error) {
1276
1242
  const message = error instanceof Error ? error.message : String(error);
1277
- ctx.ui.notify(`Library init failed: ${message}`, "error");
1243
+ notifyCtx(ctx, `Library init failed: ${message}`, "error");
1278
1244
  }
1279
1245
  },
1280
1246
  });
@@ -1300,7 +1266,7 @@ export default function codeCartographerExtension(pi) {
1300
1266
  lastFeedbackLines = lines;
1301
1267
  if (state)
1302
1268
  setUiState(ctx, state, lastFeedbackLines);
1303
- ctx.ui.notify("Configuration shown in status widget.", "info");
1269
+ notifyCtx(ctx, "Configuration shown in status widget.", "info");
1304
1270
  },
1305
1271
  });
1306
1272
  pi.registerCommand("codecarto-usage", {
@@ -1313,7 +1279,7 @@ export default function codeCartographerExtension(pi) {
1313
1279
  if (usage.runs.length === 0) {
1314
1280
  lastFeedbackLines = ["No phase runs recorded yet."];
1315
1281
  setUiState(ctx, state, lastFeedbackLines);
1316
- ctx.ui.notify("No phase runs recorded yet.", "info");
1282
+ notifyCtx(ctx, "No phase runs recorded yet.", "info");
1317
1283
  return;
1318
1284
  }
1319
1285
  const totals = computeTotals(usage);
@@ -1333,7 +1299,7 @@ export default function codeCartographerExtension(pi) {
1333
1299
  }
1334
1300
  lastFeedbackLines = lines;
1335
1301
  setUiState(ctx, state, lastFeedbackLines);
1336
- ctx.ui.notify(`CodeCartographer usage: ${totals.runs} run${totals.runs === 1 ? "" : "s"}, ${formatUsageTokens(totals.tokens.input + totals.tokens.output)} tokens total`, "info");
1302
+ notifyCtx(ctx, `CodeCartographer usage: ${totals.runs} run${totals.runs === 1 ? "" : "s"}, ${formatUsageTokens(totals.tokens.input + totals.tokens.output)} tokens total`, "info");
1337
1303
  },
1338
1304
  });
1339
1305
  pi.registerCommand("codecarto-dashboard", {
@@ -1347,26 +1313,26 @@ export default function codeCartographerExtension(pi) {
1347
1313
  handler: async (args, ctx) => {
1348
1314
  const flags = parseDashboardFlags(args);
1349
1315
  if (flags.unknown.length > 0) {
1350
- ctx.ui.notify(`Unknown /codecarto-dashboard flag: ${flags.unknown.join(" ")}`, "error");
1316
+ notifyCtx(ctx, `Unknown /codecarto-dashboard flag: ${flags.unknown.join(" ")}`, "error");
1351
1317
  return;
1352
1318
  }
1353
1319
  const state = await ensureWorkspaceState(ctx);
1354
1320
  if (!state)
1355
1321
  return;
1356
1322
  if (flags.narrate) {
1357
- ctx.ui.notify(`Narrating dashboard via LLM…`, "info");
1323
+ notifyCtx(ctx, `Narrating dashboard via LLM…`, "info");
1358
1324
  const result = await narrateDashboard(ctx, state);
1359
1325
  if (result.used) {
1360
- ctx.ui.notify("Narration written to .codecarto/.dashboard-narration.local.md", "info");
1326
+ notifyCtx(ctx, "Narration written to .codecarto/.dashboard-narration.local.md", "info");
1361
1327
  }
1362
1328
  else {
1363
- ctx.ui.notify(`LLM narration skipped (${result.skipReason}); rendering deterministic dashboard.`, "warning");
1329
+ notifyCtx(ctx, `LLM narration skipped (${result.skipReason}); rendering deterministic dashboard.`, "warning");
1364
1330
  }
1365
1331
  }
1366
1332
  await writeDashboard(ctx.cwd, PACKAGE_VERSION);
1367
1333
  lastFeedbackLines = ["Dashboard regenerated: .codecarto/dashboard.html"];
1368
1334
  setUiState(ctx, state, lastFeedbackLines);
1369
- ctx.ui.notify("Dashboard regenerated: .codecarto/dashboard.html", "info");
1335
+ notifyCtx(ctx, "Dashboard regenerated: .codecarto/dashboard.html", "info");
1370
1336
  },
1371
1337
  });
1372
1338
  pi.registerCommand("codecarto-refresh-scaffold", {
@@ -1382,12 +1348,12 @@ export default function codeCartographerExtension(pi) {
1382
1348
  files = await listScaffoldRefreshFiles();
1383
1349
  }
1384
1350
  catch (error) {
1385
- ctx.ui.notify(error instanceof Error ? error.message : String(error), "error");
1351
+ notifyCtx(ctx, error instanceof Error ? error.message : String(error), "error");
1386
1352
  return;
1387
1353
  }
1388
1354
  const approved = await ctx.ui.confirm("Refresh the .codecarto/ scaffold from the packaged template?", describeScaffoldRefreshPreview(files, state.scaffoldVersion));
1389
1355
  if (!approved) {
1390
- ctx.ui.notify("Scaffold refresh cancelled. Nothing was written.", "info");
1356
+ notifyCtx(ctx, "Scaffold refresh cancelled. Nothing was written.", "info");
1391
1357
  return;
1392
1358
  }
1393
1359
  try {
@@ -1400,13 +1366,13 @@ export default function codeCartographerExtension(pi) {
1400
1366
  ];
1401
1367
  // Re-read state so the widget's staleness line clears with the marker.
1402
1368
  await refreshWorkspaceUi(ctx, lastFeedbackLines);
1403
- ctx.ui.notify(`Refreshed ${result.written.length} framework-owned file(s) (${transition}).`, "info");
1369
+ notifyCtx(ctx, `Refreshed ${result.written.length} framework-owned file(s) (${transition}).`, "info");
1404
1370
  }
1405
1371
  catch (error) {
1406
1372
  const message = error instanceof Error ? error.message : String(error);
1407
1373
  lastFeedbackLines = [message];
1408
1374
  setUiState(ctx, state, lastFeedbackLines);
1409
- ctx.ui.notify(`Scaffold refresh failed: ${message}`, "error");
1375
+ notifyCtx(ctx, `Scaffold refresh failed: ${message}`, "error");
1410
1376
  }
1411
1377
  },
1412
1378
  });
@@ -1428,12 +1394,12 @@ export default function codeCartographerExtension(pi) {
1428
1394
  const hint = staged.length > 0
1429
1395
  ? ` (staged: ${staged.join(", ")})`
1430
1396
  : " — write .codecarto/scratch/amendments/<name>.yaml first (see templates/amendment.yaml)";
1431
- ctx.ui.notify(`Usage: /codecarto-amend <name>${hint}`, "warning");
1397
+ notifyCtx(ctx, `Usage: /codecarto-amend <name>${hint}`, "warning");
1432
1398
  return;
1433
1399
  }
1434
1400
  const name = resolveAmendmentName(args, ctx.cwd);
1435
1401
  if (!name) {
1436
- 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");
1402
+ notifyCtx(ctx, `Amendments are read from .codecarto/scratch/amendments/ only; pass the amendment name or a path inside that directory, not ${args.trim()}.`, "error");
1437
1403
  return;
1438
1404
  }
1439
1405
  // The same refusals codecarto_amend surfaces, raised before the
@@ -1443,12 +1409,12 @@ export default function codeCartographerExtension(pi) {
1443
1409
  amendment = await loadAmendmentFile(name, state.workspaceDir);
1444
1410
  }
1445
1411
  catch (error) {
1446
- ctx.ui.notify(error instanceof Error ? error.message : String(error), "error");
1412
+ notifyCtx(ctx, error instanceof Error ? error.message : String(error), "error");
1447
1413
  return;
1448
1414
  }
1449
1415
  const nextPhase = getNextEligiblePhase(state);
1450
1416
  if (nextPhase) {
1451
- ctx.ui.notify(`Cannot amend: the pipeline is not complete (next phase: ${nextPhase.id}). `
1417
+ notifyCtx(ctx, `Cannot amend: the pipeline is not complete (next phase: ${nextPhase.id}). `
1452
1418
  + "Resolve open questions and routed items through that phase's handoff (open_question_closures / carry_forward_closures) instead.", "error");
1453
1419
  return;
1454
1420
  }
@@ -1456,7 +1422,7 @@ export default function codeCartographerExtension(pi) {
1456
1422
  // before anything is written — MCP's codecarto_amend applies on call.
1457
1423
  const approved = await ctx.ui.confirm(`Apply amendment "${amendment.slug}"?`, describeAmendmentPreview(amendment, state));
1458
1424
  if (!approved) {
1459
- ctx.ui.notify(`Amendment ${amendment.slug} cancelled. Nothing was written.`, "info");
1425
+ notifyCtx(ctx, `Amendment ${amendment.slug} cancelled. Nothing was written.`, "info");
1460
1426
  return;
1461
1427
  }
1462
1428
  try {
@@ -1477,14 +1443,14 @@ export default function codeCartographerExtension(pi) {
1477
1443
  lastFeedbackLines = lines;
1478
1444
  await refreshWorkspaceUi(ctx, lastFeedbackLines);
1479
1445
  const closed = applied.openQuestionsClosed.length + applied.postPipelineClosed.length;
1480
- ctx.ui.notify(`Amendment ${amendment.slug} applied: ${applied.openQuestionsClosed.length} open question(s) and ${applied.postPipelineClosed.length} post-pipeline item(s) closed`
1446
+ notifyCtx(ctx, `Amendment ${amendment.slug} applied: ${applied.openQuestionsClosed.length} open question(s) and ${applied.postPipelineClosed.length} post-pipeline item(s) closed`
1481
1447
  + `${applied.unknownIds.length > 0 ? `; ${applied.unknownIds.length} id(s) matched nothing` : ""}.`, closed === 0 || applied.unknownIds.length > 0 ? "warning" : "info");
1482
1448
  }
1483
1449
  catch (error) {
1484
1450
  const message = error instanceof Error ? error.message : String(error);
1485
1451
  lastFeedbackLines = [message];
1486
1452
  setUiState(ctx, state, lastFeedbackLines);
1487
- ctx.ui.notify(`Amendment failed: ${message}`, "error");
1453
+ notifyCtx(ctx, `Amendment failed: ${message}`, "error");
1488
1454
  }
1489
1455
  },
1490
1456
  });