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.
@@ -0,0 +1,16 @@
1
+ import type { ExtensionCommandContext, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ export type NotifyLevel = "info" | "warning" | "error";
3
+ /** Whether `ctx` still belongs to the live session. See the module comment. */
4
+ export declare function isCtxLive(ctx: ExtensionContext | ExtensionCommandContext): boolean;
5
+ /**
6
+ * Format a notification for a stream. Exported so tests pin the shape a script
7
+ * would parse: one line, prefixed, level visible.
8
+ */
9
+ export declare function formatStreamNotification(message: string, level: NotifyLevel): string;
10
+ /**
11
+ * Notify through `ctx`: the TUI when there is one, stderr when there is not,
12
+ * nothing when the ctx is stale.
13
+ */
14
+ export declare function notifyCtx(ctx: ExtensionContext | ExtensionCommandContext, message: string, level: NotifyLevel, stream?: {
15
+ write(chunk: string): unknown;
16
+ }): void;
@@ -0,0 +1,53 @@
1
+ // User-facing notifications from the orchestrator side of the Pi extension.
2
+ //
3
+ // Two things go wrong with a bare `ctx.ui.notify(...)`, and this module is the
4
+ // one place that handles both.
5
+ //
6
+ // A stale ctx throws. Pi invalidates an extension ctx when the session is
7
+ // replaced, and from then on *every* property access on it throws — `ctx.cwd`
8
+ // and `ctx.hasUI` included. A phase runs as a sub-agent, so by the time
9
+ // post-phase work fires, the ctx captured when the command started may already
10
+ // be dead. That is an ordinary outcome, not an error: the UI it would have
11
+ // refreshed is gone with the session, so the message is dropped.
12
+ //
13
+ // A non-interactive session shows nothing. Under `pi -p`, `ctx.hasUI` is false
14
+ // and `ctx.ui.notify` is a silent no-op. Commands whose only output is a notify
15
+ // — /codecarto-status, /codecarto-usage, /codecarto-list-skills — then exit 0
16
+ // having printed nothing, which is byte-for-byte what a silent refusal looks
17
+ // like (#219). So when there is no UI, the message goes to stderr instead:
18
+ // stderr rather than stdout because `--mode json` owns stdout for its event
19
+ // stream and prose there would corrupt it.
20
+ //
21
+ // This is for the orchestrator's ctx only. Code that runs inside the phase
22
+ // sub-agent (phase-compaction.ts) also sees `hasUI === false`, but there the
23
+ // parent TUI may be on screen, and writing to stderr would put text into a live
24
+ // terminal. That code keeps its own `if (ctx.hasUI)` guards.
25
+ /** Whether `ctx` still belongs to the live session. See the module comment. */
26
+ export function isCtxLive(ctx) {
27
+ try {
28
+ return typeof ctx.cwd === "string";
29
+ }
30
+ catch {
31
+ return false;
32
+ }
33
+ }
34
+ /**
35
+ * Format a notification for a stream. Exported so tests pin the shape a script
36
+ * would parse: one line, prefixed, level visible.
37
+ */
38
+ export function formatStreamNotification(message, level) {
39
+ return `[codecarto] ${level}: ${message}\n`;
40
+ }
41
+ /**
42
+ * Notify through `ctx`: the TUI when there is one, stderr when there is not,
43
+ * nothing when the ctx is stale.
44
+ */
45
+ export function notifyCtx(ctx, message, level, stream = process.stderr) {
46
+ if (!isCtxLive(ctx))
47
+ return;
48
+ if (ctx.hasUI) {
49
+ ctx.ui.notify(message, level);
50
+ return;
51
+ }
52
+ stream.write(formatStreamNotification(message, level));
53
+ }
@@ -1,7 +1,7 @@
1
- import { mkdir, rename, writeFile } from "node:fs/promises";
2
- import { join, resolve } from "node:path";
1
+ import { mkdir } from "node:fs/promises";
2
+ import { join } from "node:path";
3
3
  import { compact } from "@earendil-works/pi-coding-agent";
4
- import { canonicalPath, getWorkspaceState, isWithinPath } from "../../core/index.js";
4
+ import { atomicWriteFile, canonicalPath, getWorkspaceState, isWithinPath, resolveExistingPrefix } from "../../core/index.js";
5
5
  const PHASE_SESSION_PREFIX = "CodeCartographer phase: ";
6
6
  export function phaseIdFromSessionName(sessionName) {
7
7
  if (!sessionName?.startsWith(PHASE_SESSION_PREFIX))
@@ -29,7 +29,6 @@ export function buildPhaseCompactionInstructions(phaseId, primaryOutput) {
29
29
  export async function writePhaseCheckpoint(cwd, phaseId, summary, tokensBefore) {
30
30
  const dir = join(cwd, ".codecarto", "scratch", "checkpoints");
31
31
  const target = join(dir, `${phaseId}.md`);
32
- const temp = `${target}.${process.pid}.${Date.now()}.tmp`;
33
32
  await mkdir(dir, { recursive: true });
34
33
  const content = [
35
34
  "---",
@@ -44,8 +43,7 @@ export async function writePhaseCheckpoint(cwd, phaseId, summary, tokensBefore)
44
43
  summary.trim(),
45
44
  "",
46
45
  ].join("\n");
47
- await writeFile(temp, content, "utf8");
48
- await rename(temp, target);
46
+ await atomicWriteFile(target, content);
49
47
  return target;
50
48
  }
51
49
  /**
@@ -64,7 +62,9 @@ export function phaseCompactionExtension(pi) {
64
62
  if (event.toolName === "edit" || event.toolName === "write") {
65
63
  const inputPath = typeof event.input.path === "string" ? event.input.path : "";
66
64
  const strippedPath = inputPath.startsWith("@") ? inputPath.slice(1) : inputPath;
67
- const targetPath = await canonicalPath(resolve(ctx.cwd, strippedPath));
65
+ // Same containment as the parent extension's hook (#223): follow the
66
+ // existing prefix through symlinks, then append the unborn tail.
67
+ const targetPath = await resolveExistingPrefix(strippedPath, ctx.cwd);
68
68
  const allowedRoot = await canonicalPath(join(ctx.cwd, ".codecarto"));
69
69
  if (!isWithinPath(targetPath, allowedRoot)) {
70
70
  return {
@@ -125,8 +125,11 @@ export async function handleInit(args) {
125
125
  // broadside state and results already on disk.
126
126
  await copyPackagedWorkspace(targetWorkspaceDir);
127
127
  }
128
+ // The template no longer carries a status.yaml (it is this repository's
129
+ // own project state); one is only present when init is re-run over an
130
+ // existing workspace, where its pipeline stays the default.
128
131
  const statusPath = join(targetWorkspaceDir, "workflow", "status.yaml");
129
- const rawStatus = (await loadYamlFile(statusPath)) ?? {};
132
+ const rawStatus = (await pathExists(statusPath)) ? ((await loadYamlFile(statusPath)) ?? {}) : {};
130
133
  const selectedPipelinePath = pipelineChoice ?? rawStatus.pipeline?.trim() ?? DEFAULT_PIPELINE_PATH;
131
134
  const resolvedPipelinePath = join(targetWorkspaceDir, selectedPipelinePath);
132
135
  if (!(await pathExists(resolvedPipelinePath))) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codecartographer-pi",
3
- "version": "0.19.4",
3
+ "version": "0.19.6",
4
4
  "mcpName": "io.github.HuginnIndustries/codecartographer",
5
5
  "description": "Turn an unfamiliar codebase into a validated reimplementation spec, then synthesize confirmed specs and a product vision into a traceable plan.",
6
6
  "type": "module",