omni-pi 0.10.1 → 0.12.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,50 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.12.0 - 2026-04-27
4
+
5
+ ### Security and robustness
6
+
7
+ - Sanitized untrusted text before it reaches brain prompts and `DECISIONS.md`
8
+ - Sanitized generated `SKILL.md` files and fixed `Set` mutation in loop
9
+ - Added atomic writes for `.omni/` and `.pi/` state files
10
+ - Hardened version parsing in the self-updater
11
+
12
+ ### Bug fixes
13
+
14
+ - Fixed `isRequestRelated` to actually use the overlap ratio for planning continuity
15
+ - Fixed repo-map indexing, fingerprints, and dirty-path tracking
16
+ - Fixed backtick code span tracking in the task table parser
17
+ - Fixed prerelease ordering so pre-releases sort below their matching release in the updater
18
+ - Used `os.homedir()` for reliable home directory resolution in the updater
19
+ - Cached package version at module load to avoid repeated filesystem reads in the header
20
+
21
+ ### Dependencies
22
+
23
+ - Upgraded `@mariozechner/pi-coding-agent` to `0.70.2`
24
+ - Upgraded `@anthropic-ai/claude-agent-sdk` to `0.2.119`
25
+ - Upgraded `@juanibiapina/pi-powerbar` to `0.9.1`
26
+ - Upgraded `pi-interview` to `0.8.6`
27
+ - Upgraded `pi-prompt-template-model` to `0.9.1`
28
+ - Upgraded `glimpseui` to `0.8.0`
29
+ - Upgraded `@biomejs/biome` to `2.4.13`
30
+ - Upgraded `typescript` to `6.0.3`
31
+ - Upgraded `vitest` to `4.1.5`
32
+ - Upgraded `@types/node` to `25.6.0`
33
+
34
+ ### Housekeeping
35
+
36
+ - Hoisted control-char regexes and applied Biome formatting
37
+ - Included tests in `tsc` check and added launcher type declarations
38
+ - Added `tsbuildinfo` and coverage artifacts to `.gitignore`
39
+ - Enforced commit-after-every-task rule in `AGENTS.md`
40
+
41
+ ## 0.11.0 - 2026-04-24
42
+
43
+ ### Removed
44
+
45
+ - Removed `/model-setup`, `/manage-providers`, the `omni-providers` extension, and the bundled provider catalog. Pi now handles provider and model management natively.
46
+ - Removed `PROVIDERS.md` and the provider docs spec.
47
+
3
48
  ## 0.10.1 - 2026-04-24
4
49
 
5
50
  ### Release follow-up
@@ -9,6 +54,10 @@
9
54
 
10
55
  ## 0.10.0 - 2026-04-24
11
56
 
57
+ ### Removed
58
+
59
+ - Removed `/model-setup`, `/manage-providers`, and the `omni-providers` extension. Pi now handles provider and model management natively.
60
+
12
61
  ### Runtime and integrations
13
62
 
14
63
  - upgraded `@mariozechner/pi-coding-agent` to `0.70.0`
package/README.md CHANGED
@@ -15,7 +15,7 @@ Requires Node.js 22 or newer.
15
15
  - Keeps durable standards and project context in `.omni/`, even when Omni mode is off.
16
16
  - Writes specs, tasks, and progress into `.omni/` once Omni mode is enabled.
17
17
  - Adds a repo map that indexes supported source files, ranks them by structure plus recent activity, and injects a compact codebase-awareness block into Omni prompts.
18
- - Bundles web search, guided interviews, themed UI, native micro-UI via Glimpse, native git diff review, prompt-template-powered workflow commands, a task viewer, a powerbar, custom provider/model management, and automatic updates out of the box.
18
+ - Bundles web search, guided interviews, themed UI, native micro-UI via Glimpse, native git diff review, prompt-template-powered workflow commands, a task viewer, a powerbar, and automatic updates out of the box.
19
19
 
20
20
  ## Install
21
21
 
@@ -30,8 +30,6 @@ cd your-project
30
30
  omni
31
31
  ```
32
32
 
33
- Custom provider setup, refresh behavior, and bundled provider behavior are documented in [PROVIDERS.md](PROVIDERS.md).
34
-
35
33
  ## Features
36
34
 
37
35
  ### Bundled Skills
@@ -66,7 +64,6 @@ Current deferred roadmap items remain intentional and visible in docs rather tha
66
64
  | Extension | What it does |
67
65
  |-----------|-------------|
68
66
  | **omni-core** | Brain workflow, themed header, session init, system prompt injection |
69
- | **omni-providers** | Model provider wiring |
70
67
  | **omni-memory** | `.omni/` durable memory bootstrap |
71
68
  | **glimpseui** | Native micro-UI windows and the optional floating companion widget |
72
69
  | **pi-web-access** | Web search and fetch tools for the agent |
@@ -88,8 +85,6 @@ Omni-Pi now bundles [Glimpse](https://github.com/HazAT/glimpse) for native micro
88
85
 
89
86
  | Command | Description |
90
87
  |---------|-------------|
91
- | `/model-setup` | Add, refresh, or remove custom provider/model entries |
92
- | `/manage-providers` | Remove stored auth for bundled providers |
93
88
  | `/omni-mode` | Toggle persistent Omni mode on or off for this project |
94
89
  | `/companion` | Toggle the Glimpse floating companion widget |
95
90
  | `/diff-review` | Open a native git diff review window and insert feedback into the editor |
@@ -108,24 +103,6 @@ Omni-Pi now bundles [Glimpse](https://github.com/HazAT/glimpse) for native micro
108
103
 
109
104
  Omni-Pi checks for new versions on startup (cached, re-checks every 4 hours). When an update is available, it prompts to install and restart. Pi's own update notification is suppressed to avoid duplication.
110
105
 
111
- ## Provider Support
112
-
113
- `/model-setup` is for custom providers and custom model entries only.
114
-
115
- Use `/model-setup` when you want to configure:
116
-
117
- - a custom provider id
118
- - an API type and base URL
119
- - an API key for that custom provider
120
- - discovered models or manual model entries
121
- - a manual refresh of already configured custom providers
122
-
123
- Use `/manage-providers` to remove stored auth for bundled Pi providers.
124
-
125
- Anthropic is intentionally API-key-only in Omni-Pi. Anthropic OAuth login is disabled.
126
-
127
- See [PROVIDERS.md](PROVIDERS.md) for the current supported-provider list and auth-management split.
128
-
129
106
  ## Omni Mode
130
107
 
131
108
  Omni-Pi keeps its current branding and shell at all times, but the specialized workflow is opt-in.
@@ -176,7 +153,7 @@ npm run chat # launch locally in dev mode
176
153
  ## CI/CD
177
154
 
178
155
  - Pull requests and pushes to `main` run `npm run verify`.
179
- - The docs are part of the test contract, including a sync check between `PROVIDERS.md` and the bundled-provider setup list in code.
156
+ - The docs are part of the test contract.
180
157
  - Pushing a `v*` tag runs the release workflow, verifies the repo again, publishes to npm through GitHub Actions trusted publishing with provenance, and then creates the GitHub release.
181
158
  - Trusted publishing still requires npm-side setup for this repository/workflow in the npm package settings.
182
159
 
package/bin/omni.d.ts ADDED
@@ -0,0 +1,22 @@
1
+ export function getOmniPackageDir(): string;
2
+ export function resolvePiCliPath(): string;
3
+ export function buildOmniEnvironment(
4
+ baseEnv?: NodeJS.ProcessEnv,
5
+ ): NodeJS.ProcessEnv;
6
+ export function ensureQuietStartupDefault(baseEnv?: NodeJS.ProcessEnv): void;
7
+ export function buildPiProcessSpec(
8
+ argv?: string[],
9
+ baseEnv?: NodeJS.ProcessEnv,
10
+ ): {
11
+ command: string;
12
+ args: string[];
13
+ env: NodeJS.ProcessEnv;
14
+ };
15
+ export function runOmni(
16
+ argv?: string[],
17
+ options?: { cwd?: string; env?: NodeJS.ProcessEnv },
18
+ ): Promise<number>;
19
+ export function isOmniEntrypointInvocation(
20
+ argvPath?: string,
21
+ moduleUrl?: string,
22
+ ): boolean;
package/bin/omni.js CHANGED
@@ -1,11 +1,45 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { spawn } from "node:child_process";
4
- import { mkdirSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
4
+ import { randomBytes } from "node:crypto";
5
+ import {
6
+ chmodSync,
7
+ mkdirSync,
8
+ readFileSync,
9
+ realpathSync,
10
+ renameSync,
11
+ statSync,
12
+ unlinkSync,
13
+ writeFileSync,
14
+ } from "node:fs";
5
15
  import os from "node:os";
6
16
  import path from "node:path";
7
17
  import { fileURLToPath } from "node:url";
8
18
 
19
+ function writeFileAtomicSync(filePath, content) {
20
+ const tempPath = `${filePath}.${randomBytes(6).toString("hex")}.tmp`;
21
+ let mode;
22
+ try {
23
+ mode = statSync(filePath).mode & 0o777;
24
+ } catch {
25
+ // New file: keep Node's default creation mode.
26
+ }
27
+ try {
28
+ writeFileSync(tempPath, content, "utf8");
29
+ if (mode !== undefined) {
30
+ chmodSync(tempPath, mode);
31
+ }
32
+ renameSync(tempPath, filePath);
33
+ } catch (error) {
34
+ try {
35
+ unlinkSync(tempPath);
36
+ } catch {
37
+ // temp may not exist
38
+ }
39
+ throw error;
40
+ }
41
+ }
42
+
9
43
  export function getOmniPackageDir() {
10
44
  return path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
11
45
  }
@@ -22,8 +56,12 @@ export function resolvePiCliPath() {
22
56
  }
23
57
 
24
58
  export function buildOmniEnvironment(baseEnv = process.env) {
59
+ // Pi has its own update prompt; Omni-Pi runs its own (registerUpdater).
60
+ // Suppress Pi's check at the launcher boundary so the Omni updater is
61
+ // the only one that surfaces upgrade prompts.
25
62
  return {
26
63
  ...baseEnv,
64
+ PI_SKIP_VERSION_CHECK: "1",
27
65
  };
28
66
  }
29
67
 
@@ -56,10 +94,9 @@ export function ensureQuietStartupDefault(baseEnv = process.env) {
56
94
  typeof parsed === "object" &&
57
95
  parsed.quietStartup === undefined
58
96
  ) {
59
- writeFileSync(
97
+ writeFileAtomicSync(
60
98
  settingsFile,
61
99
  `${JSON.stringify({ ...parsed, quietStartup: true }, null, 2)}\n`,
62
- "utf8",
63
100
  );
64
101
  }
65
102
  } catch (error) {
@@ -73,10 +110,9 @@ export function ensureQuietStartupDefault(baseEnv = process.env) {
73
110
  }
74
111
 
75
112
  mkdirSync(agentDir, { recursive: true });
76
- writeFileSync(
113
+ writeFileAtomicSync(
77
114
  settingsFile,
78
115
  `${JSON.stringify({ quietStartup: true }, null, 2)}\n`,
79
- "utf8",
80
116
  );
81
117
  }
82
118
  }
@@ -96,7 +132,7 @@ export async function runOmni(argv = process.argv.slice(2), options = {}) {
96
132
  ensureQuietStartupDefault(options.env);
97
133
  const spec = buildPiProcessSpec(argv, options.env);
98
134
 
99
- await new Promise((resolve, reject) => {
135
+ return await new Promise((resolve, reject) => {
100
136
  const child = spawn(spec.command, spec.args, {
101
137
  cwd: options.cwd ?? process.cwd(),
102
138
  env: spec.env,
@@ -9,13 +9,11 @@ import {
9
9
  } from "../../src/brain.js";
10
10
  import { createOmniCommands } from "../../src/commands.js";
11
11
  import { renderHeader } from "../../src/header.js";
12
- import { registerModelCommand } from "../../src/model-command.js";
13
12
  import {
14
13
  registerOmniMessageRenderer,
15
14
  registerPiCommands,
16
15
  } from "../../src/pi.js";
17
16
  import { ensureBundledPromptTemplates } from "../../src/prompt-template-sync.js";
18
- import { registerProviderAuthCommand } from "../../src/provider-auth-command.js";
19
17
  import {
20
18
  buildRepoMapPromptSuffix,
21
19
  registerRepoMapTracking,
@@ -42,8 +40,6 @@ import { buildOnboardingInterviewKickoff } from "../../src/workflow.js";
42
40
  export default function omniCoreExtension(api: ExtensionAPI): void {
43
41
  registerOmniMessageRenderer(api);
44
42
  registerPiCommands(api, createOmniCommands());
45
- registerModelCommand(api);
46
- registerProviderAuthCommand(api);
47
43
  registerThemeCommand(api);
48
44
  registerTodoShortcut(api);
49
45
  registerUpdater(api);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "omni-pi",
3
- "version": "0.10.1",
3
+ "version": "0.12.0",
4
4
  "description": "Single-agent Pi package that interviews the user, documents the spec, and implements work in bounded slices.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -38,7 +38,6 @@
38
38
  "templates",
39
39
  "vendor/pi-diff-review",
40
40
  "README.md",
41
- "PROVIDERS.md",
42
41
  "CREDITS.md"
43
42
  ],
44
43
  "scripts": {
@@ -52,14 +51,13 @@
52
51
  "prepublishOnly": "npm run verify"
53
52
  },
54
53
  "devDependencies": {
55
- "@biomejs/biome": "2.4.9",
56
- "@types/node": "^24.12.0",
57
- "typescript": "^5.9.3",
58
- "vitest": "^3.2.4"
54
+ "@biomejs/biome": "2.4.13",
55
+ "@types/node": "^25.6.0",
56
+ "typescript": "^6.0.3",
57
+ "vitest": "^4.1.5"
59
58
  },
60
59
  "pi": {
61
60
  "extensions": [
62
- "./extensions/omni-providers/index.ts",
63
61
  "./extensions/omni-core/index.ts",
64
62
  "./extensions/omni-memory/index.ts",
65
63
  "./node_modules/glimpseui/pi-extension/index.ts",
@@ -81,14 +79,14 @@
81
79
  ]
82
80
  },
83
81
  "dependencies": {
84
- "@anthropic-ai/claude-agent-sdk": "0.2.84",
82
+ "@anthropic-ai/claude-agent-sdk": "^0.2.119",
85
83
  "@juanibiapina/pi-extension-settings": "^0.6.1",
86
- "@juanibiapina/pi-powerbar": "^0.8.0",
87
- "@mariozechner/pi-coding-agent": "^0.70.0",
88
- "glimpseui": "^0.7.0",
84
+ "@juanibiapina/pi-powerbar": "^0.9.1",
85
+ "@mariozechner/pi-coding-agent": "^0.70.2",
86
+ "glimpseui": "^0.8.0",
89
87
  "pi-diff-review": "file:./vendor/pi-diff-review",
90
- "pi-interview": "^0.6.2",
91
- "pi-prompt-template-model": "^0.8.2",
88
+ "pi-interview": "^0.8.6",
89
+ "pi-prompt-template-model": "^0.9.1",
92
90
  "pi-web-access": "^0.10.6",
93
91
  "zod": "^4.3.6"
94
92
  },
package/src/atomic.ts ADDED
@@ -0,0 +1,94 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import {
3
+ chmodSync,
4
+ renameSync,
5
+ statSync,
6
+ unlinkSync,
7
+ writeFileSync,
8
+ } from "node:fs";
9
+ import {
10
+ chmod,
11
+ mkdir,
12
+ rename,
13
+ stat,
14
+ unlink,
15
+ writeFile,
16
+ } from "node:fs/promises";
17
+ import path from "node:path";
18
+
19
+ // Atomic write helpers. Writes to a sibling temp file in the same
20
+ // directory, then renames over the destination. POSIX rename(2) is
21
+ // atomic when the source and destination are on the same filesystem,
22
+ // so a reader (or a concurrent writer) never observes a partially
23
+ // written state file. On Windows the rename is also atomic when the
24
+ // destination exists.
25
+ //
26
+ // The temp suffix includes a random nonce so two concurrent writers
27
+ // to the same path don't collide on their temp files.
28
+
29
+ function tempPathFor(filePath: string): string {
30
+ return `${filePath}.${randomBytes(6).toString("hex")}.tmp`;
31
+ }
32
+
33
+ async function existingMode(filePath: string): Promise<number | undefined> {
34
+ try {
35
+ return (await stat(filePath)).mode & 0o777;
36
+ } catch {
37
+ return undefined;
38
+ }
39
+ }
40
+
41
+ function existingModeSync(filePath: string): number | undefined {
42
+ try {
43
+ return statSync(filePath).mode & 0o777;
44
+ } catch {
45
+ return undefined;
46
+ }
47
+ }
48
+
49
+ export async function writeFileAtomic(
50
+ filePath: string,
51
+ content: string | Uint8Array,
52
+ ): Promise<void> {
53
+ await mkdir(path.dirname(filePath), { recursive: true });
54
+ const tempPath = tempPathFor(filePath);
55
+ const mode = await existingMode(filePath);
56
+ try {
57
+ await writeFile(tempPath, content, "utf8");
58
+ if (mode !== undefined) {
59
+ await chmod(tempPath, mode);
60
+ }
61
+ await rename(tempPath, filePath);
62
+ } catch (error) {
63
+ // Best-effort cleanup of the orphaned temp file; ignore failure
64
+ // since the original error is what matters.
65
+ try {
66
+ await unlink(tempPath);
67
+ } catch {
68
+ /* temp may not exist */
69
+ }
70
+ throw error;
71
+ }
72
+ }
73
+
74
+ export function writeFileAtomicSync(
75
+ filePath: string,
76
+ content: string | Uint8Array,
77
+ ): void {
78
+ const tempPath = tempPathFor(filePath);
79
+ const mode = existingModeSync(filePath);
80
+ try {
81
+ writeFileSync(tempPath, content, "utf8");
82
+ if (mode !== undefined) {
83
+ chmodSync(tempPath, mode);
84
+ }
85
+ renameSync(tempPath, filePath);
86
+ } catch (error) {
87
+ try {
88
+ unlinkSync(tempPath);
89
+ } catch {
90
+ /* temp may not exist */
91
+ }
92
+ throw error;
93
+ }
94
+ }
package/src/config.ts CHANGED
@@ -1,6 +1,7 @@
1
- import { mkdir, readFile, writeFile } from "node:fs/promises";
1
+ import { mkdir, readFile } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
 
4
+ import { writeFileAtomic } from "./atomic.js";
4
5
  import type { OmniConfig } from "./contracts.js";
5
6
  import { AVAILABLE_MODELS } from "./providers.js";
6
7
 
@@ -84,7 +85,7 @@ export async function writeConfig(
84
85
  ): Promise<void> {
85
86
  const configPath = path.join(rootDir, CONFIG_PATH);
86
87
  await mkdir(path.dirname(configPath), { recursive: true });
87
- await writeFile(configPath, renderConfigContent(config), "utf8");
88
+ await writeFileAtomic(configPath, renderConfigContent(config));
88
89
  }
89
90
 
90
91
  export async function updateModelConfig(
package/src/header.ts CHANGED
@@ -40,7 +40,7 @@ export function centerIn(text: string, width: number): string {
40
40
  return " ".repeat(pad) + text;
41
41
  }
42
42
 
43
- function readVersion(): string {
43
+ function loadVersion(): string {
44
44
  try {
45
45
  const pkgPath = path.resolve(
46
46
  path.dirname(fileURLToPath(import.meta.url)),
@@ -56,16 +56,19 @@ function readVersion(): string {
56
56
  }
57
57
  }
58
58
 
59
+ // Read once at module load — package.json doesn't change while the
60
+ // process is running, so re-parsing on every render is wasted work.
61
+ const VERSION = loadVersion();
62
+
59
63
  export function pickWelcome(): string {
60
64
  return WELCOME_MESSAGES[Math.floor(Math.random() * WELCOME_MESSAGES.length)];
61
65
  }
62
66
 
63
67
  export function renderHeader(theme: Theme): Text {
64
- const version = readVersion();
65
68
  const welcome = pickWelcome();
66
69
 
67
70
  const logo = ASCII_LOGO.map((line) => brand(line)).join("\n");
68
- const subtitleText = `— P I v${version} —`;
71
+ const subtitleText = `— P I v${VERSION} —`;
69
72
  const subtitle = theme.fg("muted", centerIn(subtitleText, LOGO_WIDTH));
70
73
  const taglineText = "plan · build · verify";
71
74
  const tagline = theme.fg("muted", centerIn(taglineText, LOGO_WIDTH));
package/src/planning.ts CHANGED
@@ -162,6 +162,9 @@ function buildBootstrapTasks(repoSignals: RepoSignals): TaskBrief[] {
162
162
  return tasks;
163
163
  }
164
164
 
165
+ const RELATION_OVERLAP_THRESHOLD = 0.34;
166
+ const RELATION_SMALL_SET_LIMIT = 3;
167
+
165
168
  const RELATION_STOPWORDS = new Set([
166
169
  "a",
167
170
  "an",
@@ -245,13 +248,20 @@ export function isRequestRelated(
245
248
  const overlap = [...currentTokens].filter((token) =>
246
249
  previousTokens.has(token),
247
250
  );
248
- if (overlap.length >= 1) {
249
- return true;
251
+ if (overlap.length === 0) {
252
+ return false;
250
253
  }
251
254
 
252
- return (
253
- overlap.length / Math.min(previousTokens.size, currentTokens.size) >= 0.34
254
- );
255
+ // Compare against the smaller token set: short follow-up summaries
256
+ // ("auth bug fix") have very few tokens, so even one match is meaningful
257
+ // there. With more text on either side, demand a real overlap ratio so a
258
+ // single incidental shared word ("users", "config") doesn't keep an
259
+ // unrelated plan alive.
260
+ const smaller = Math.min(previousTokens.size, currentTokens.size);
261
+ if (smaller <= RELATION_SMALL_SET_LIMIT) {
262
+ return true;
263
+ }
264
+ return overlap.length / smaller >= RELATION_OVERLAP_THRESHOLD;
255
265
  }
256
266
 
257
267
  export function createInitialSpec(
package/src/plans.ts CHANGED
@@ -1,6 +1,7 @@
1
- import { mkdir, readFile, unlink, writeFile } from "node:fs/promises";
1
+ import { mkdir, readFile, unlink } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
 
4
+ import { writeFileAtomic } from "./atomic.js";
4
5
  import type { PlanEntry, PlanStatus } from "./contracts.js";
5
6
  import { OMNI_DIR } from "./contracts.js";
6
7
 
@@ -109,7 +110,7 @@ async function writeIndex(
109
110
  entries: PlanEntry[],
110
111
  ): Promise<void> {
111
112
  await ensurePlansDir(rootDir);
112
- await writeFile(indexPath(rootDir), renderIndex(entries), "utf8");
113
+ await writeFileAtomic(indexPath(rootDir), renderIndex(entries));
113
114
  }
114
115
 
115
116
  export async function createPlan(
@@ -123,10 +124,9 @@ export async function createPlan(
123
124
  const entry: PlanEntry = { id, title, status: "active", createdAt };
124
125
 
125
126
  await ensurePlansDir(rootDir);
126
- await writeFile(
127
+ await writeFileAtomic(
127
128
  planFilePath(rootDir, id),
128
129
  renderPlanFile(entry, description, tasks),
129
- "utf8",
130
130
  );
131
131
 
132
132
  const entries = await readPlanIndex(rootDir);
@@ -157,7 +157,7 @@ export async function updatePlanStatus(
157
157
  const filePath = planFilePath(rootDir, planId);
158
158
  const content = await readFile(filePath, "utf8");
159
159
  const updated = content.replace(/^Status:\s*.+$/mu, `Status: ${status}`);
160
- await writeFile(filePath, updated, "utf8");
160
+ await writeFileAtomic(filePath, updated);
161
161
  } catch {
162
162
  // file may have been cleaned up already
163
163
  }
@@ -198,13 +198,12 @@ export async function appendProgress(
198
198
 
199
199
  try {
200
200
  const content = await readFile(filePath, "utf8");
201
- await writeFile(filePath, `${content.trimEnd()}\n${bullet}\n`, "utf8");
201
+ await writeFileAtomic(filePath, `${content.trimEnd()}\n${bullet}\n`);
202
202
  } catch {
203
203
  await mkdir(path.dirname(filePath), { recursive: true });
204
- await writeFile(
204
+ await writeFileAtomic(
205
205
  filePath,
206
206
  `# Progress\n\nOngoing log of project progress.\n\n${bullet}\n`,
207
- "utf8",
208
207
  );
209
208
  }
210
209
  }
@@ -1,13 +1,9 @@
1
- import {
2
- existsSync,
3
- mkdirSync,
4
- readFileSync,
5
- rmSync,
6
- writeFileSync,
7
- } from "node:fs";
1
+ import { existsSync, mkdirSync, readFileSync, rmSync } from "node:fs";
8
2
  import os from "node:os";
9
3
  import path from "node:path";
10
4
 
5
+ import { writeFileAtomicSync } from "./atomic.js";
6
+
11
7
  const MANAGED_PROMPT_FILES = ["commit.md", "push.md"] as const;
12
8
  const MANAGED_SUBDIR = "omni-pi";
13
9
  const LEGACY_MANAGED_SUBDIRS = ["zz-omni-pi"] as const;
@@ -55,7 +51,7 @@ export function ensureBundledPromptTemplates(
55
51
  continue;
56
52
  }
57
53
 
58
- writeFileSync(targetPath, nextContent, "utf8");
54
+ writeFileAtomicSync(targetPath, nextContent);
59
55
  written.push(targetPath);
60
56
  }
61
57