codecartographer-pi 0.19.5 → 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.
@@ -2,9 +2,9 @@
2
2
  // HTML to `.codecarto/dashboard.html`. Best-effort — failures are swallowed
3
3
  // and never escalate to a phase error the user sees, mirroring the
4
4
  // recordUsage discipline at extensions/codecarto/index.ts.
5
- import { readdir, readFile, rename, writeFile } from "node:fs/promises";
5
+ import { readdir, readFile } from "node:fs/promises";
6
6
  import { join } from "node:path";
7
- import { DASHBOARD_RELATIVE_PATH, getWorkspaceState, loadUsage, NARRATION_CACHE_RELATIVE_PATH, parseSimpleYaml, pathExists, renderDashboard, } from "../../core/index.js";
7
+ import { atomicWriteFile, DASHBOARD_RELATIVE_PATH, getWorkspaceState, loadUsage, NARRATION_CACHE_RELATIVE_PATH, parseSimpleYaml, pathExists, renderDashboard, } from "../../core/index.js";
8
8
  const CLOSEOUT_FILENAME_RE = /^(\d{4}-\d{2}-\d{2})-(.+)\.md$/;
9
9
  /**
10
10
  * Render and atomically replace `.codecarto/dashboard.html`.
@@ -36,10 +36,7 @@ export async function writeDashboard(cwd, packageVersion) {
36
36
  narration,
37
37
  };
38
38
  const html = renderDashboard(inputs);
39
- const path = join(workspaceDir, DASHBOARD_RELATIVE_PATH);
40
- const tempPath = `${path}.${process.pid}.${Date.now()}.tmp`;
41
- await writeFile(tempPath, html, "utf8");
42
- await rename(tempPath, path);
39
+ await atomicWriteFile(join(workspaceDir, DASHBOARD_RELATIVE_PATH), html);
43
40
  return true;
44
41
  }
45
42
  catch {
@@ -11,7 +11,7 @@ import { parseNextFlags } from "./next-flags.js";
11
11
  import { buildPiGuideMessage } from "./guide-framing.js";
12
12
  import { isCtxLive, notifyCtx } from "./notify.js";
13
13
  import { phaseCompactionExtension } from "./phase-compaction.js";
14
- 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";
15
15
  import { initLibrary } from "../../core/library.js";
16
16
  import { resolveUserConfigPath } from "../../core/orchestrator-config.js";
17
17
  const STATUS_WIDGET_ID = "codecarto-widget";
@@ -343,13 +343,17 @@ export default function codeCartographerExtension(pi) {
343
343
  if (event.toolName === "edit" || event.toolName === "write") {
344
344
  const inputPath = typeof event.input.path === "string" ? event.input.path : "";
345
345
  const strippedPath = inputPath.startsWith("@") ? inputPath.slice(1) : inputPath;
346
- 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);
347
351
  const allowedRoots = [await canonicalPath(workspaceDir)];
348
352
  const config = await loadCodecartoConfig(workspaceDir);
349
353
  if (config.library.path && await discoverLibrary(config.library.path)) {
350
354
  allowedRoots.push(await canonicalPath(config.library.path));
351
355
  }
352
- const withinAllowed = await Promise.all(allowedRoots.map((allowedRoot) => isWithinPathResolved(targetPath, allowedRoot)));
356
+ const withinAllowed = allowedRoots.map((allowedRoot) => isWithinPath(targetPath, allowedRoot));
353
357
  if (!withinAllowed.some((result) => result)) {
354
358
  notifyCtx(ctx, `Blocked ${event.toolName} outside .codecarto/ or configured library: ${inputPath}`, "warning");
355
359
  return { block: true, reason: `CodeCartographer mode only allows ${event.toolName} within .codecarto/ or the configured CodeCartographer library.` };
@@ -447,7 +451,8 @@ export default function codeCartographerExtension(pi) {
447
451
  await copyPackagedWorkspace(targetWorkspaceDir);
448
452
  }
449
453
  const rawStatusPath = join(targetWorkspaceDir, "workflow", "status.yaml");
450
- 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)) ?? {}) : {};
451
456
  const selectedPipelinePath = pipelineChoice ?? rawStatus.pipeline?.trim() ?? DEFAULT_PIPELINE_PATH;
452
457
  const resolvedPipelinePath = join(targetWorkspaceDir, selectedPipelinePath);
453
458
  if (!(await pathExists(resolvedPipelinePath))) {
@@ -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.5",
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",