@signalridge/pi-goal 1.4.0 → 1.4.1

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,39 @@
1
1
  # @signalridge/pi-goal
2
2
 
3
+ ## 1.4.1
4
+ ### Patch Changes
5
+
6
+ - c1b1741: Durability and containment fixes found in a repository-wide scan.
7
+
8
+ `pi-gpt-fast` rewrote pi's own global `settings.json` with a plain
9
+ `writeFileSync`. That file is shared with pi and every other extension, and this
10
+ one is the only writer of it; a torn write would have left the user with no pi
11
+ configuration at all rather than just no gpt-fast setting. It now writes a temp
12
+ file and renames, matching every other settings writer in the repository.
13
+ `pi-goal` gets the same treatment for its cross-project goal state file, which
14
+ its own settings module already did.
15
+
16
+ `pi-ralph-wiggum` resolved `/ralph start <path>` straight against the session
17
+ cwd, so `../../notes.md` would create directories and a file outside the project
18
+ the user opened — silently, before the loop started. This extension drives long
19
+ unattended loops, so that command is as likely to come from a model as from a
20
+ person. Task-file paths are now required to stay inside the workspace, on the
21
+ command, on the `ralph_start` tool, and on the paths read back out of persisted
22
+ state, so a state file written earlier or edited by hand cannot pull a file in
23
+ from outside either.
24
+
25
+ The containment test canonicalizes both sides before comparing, because a purely
26
+ lexical one is defeated by a symlink: every segment of `linked/plan.md` reads as
27
+ inside the workspace when `linked` is a door out of it. `realpathSync` throws on
28
+ a path that does not exist yet — the ordinary case, since `start` is usually
29
+ creating the file — so it resolves the deepest existing ancestor and re-attaches
30
+ the not-yet-created suffix, which cannot itself be a link. The traversal test
31
+ matches a `..` segment rather than a `..` prefix; the prefix form also rejected
32
+ `..notes.md`, an ordinary filename sitting in the workspace.
33
+
34
+ Every remaining bare `catch {}` now says why the error is safe to drop, matching
35
+ the convention the rest of the repository already follows.
36
+
3
37
  ## 1.4.0
4
38
  ### Minor Changes
5
39
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@signalridge/pi-goal",
3
- "version": "1.4.0",
3
+ "version": "1.4.1",
4
4
  "description": "Pi extension for autonomous /goal completion with opt-in experimental ordered queues.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1,5 +1,6 @@
1
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
- import { dirname, join } from "node:path";
1
+ import { randomUUID } from "node:crypto";
2
+ import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
3
+ import { basename, dirname, join } from "node:path";
3
4
  import { getAgentDir } from "@earendil-works/pi-coding-agent";
4
5
  import { isNonNegativeFiniteNumber, nonNegativeFiniteNumber, normalizeTokenBudget } from "./accounting.js";
5
6
  import type { GoalStatus } from "./prompts.js";
@@ -238,12 +239,28 @@ function normalizeSafetyPauseCause(value: unknown): SafetyPauseCause | undefined
238
239
  return value === "continuation_limit" || value === "no_progress" ? value : undefined;
239
240
  }
240
241
 
242
+ /**
243
+ * Atomic for the same reason `saveGoalSettings` is: this file holds every
244
+ * project's goal state, so a torn write while clearing one project's entry
245
+ * would take the rest with it.
246
+ */
241
247
  export function clearLegacyPersistedGoal(cwd: string) {
242
248
  if (!existsSync(STATE_FILE)) return;
243
249
  const goals = readState();
244
250
  delete goals[cwd];
245
- mkdirSync(dirname(STATE_FILE), { recursive: true });
246
- writeFileSync(STATE_FILE, `${JSON.stringify(goals, null, 2)}\n`);
251
+ const document = `${JSON.stringify(goals, null, 2)}\n`;
252
+ const temporaryPath = join(dirname(STATE_FILE), `.${basename(STATE_FILE)}.${randomUUID()}.tmp`);
253
+ try {
254
+ mkdirSync(dirname(STATE_FILE), { recursive: true });
255
+ writeFileSync(temporaryPath, document, { encoding: "utf8", flag: "wx" });
256
+ renameSync(temporaryPath, STATE_FILE);
257
+ } finally {
258
+ try {
259
+ rmSync(temporaryPath, { force: true });
260
+ } catch {
261
+ // Best-effort cleanup must not replace the clear result.
262
+ }
263
+ }
247
264
  }
248
265
 
249
266
  function readState(): Record<string, unknown> {