@aiwg/cli 2026.10.6 → 2026.10.8

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.
Files changed (100) hide show
  1. package/bin/aiwg.mjs +4 -2
  2. package/dist/src/a2a/agent-card.js +11 -0
  3. package/dist/src/api/index.d.ts +1 -0
  4. package/dist/src/api/index.js +1 -0
  5. package/dist/src/artifacts/fortemi-shard-contract/2.2.0/contract.json +103 -0
  6. package/dist/src/artifacts/fortemi-shard-contract/2.2.0/full-v1-embedding-config.schema.json +212 -0
  7. package/dist/src/artifacts/fortemi-shard-contract/2.2.0/full-v1-embedding-set.schema.json +240 -0
  8. package/dist/src/artifacts/fortemi-shard-contract/2.2.0/full-v1-manifest.schema.json +451 -0
  9. package/dist/src/artifacts/fortemi-shard-export.js +284 -0
  10. package/dist/src/artifacts/index-preparation.js +2 -0
  11. package/dist/src/auth/client.js +5 -1
  12. package/dist/src/auth/credential-store.js +5 -1
  13. package/dist/src/auth/sensitive-input.js +151 -0
  14. package/dist/src/channel/manager.mjs +3 -0
  15. package/dist/src/cli/handlers/agentcard.js +47 -25
  16. package/dist/src/cli/handlers/auth.js +22 -2
  17. package/dist/src/cli/handlers/cost-report.js +7 -1
  18. package/dist/src/cli/handlers/decision.js +36 -10
  19. package/dist/src/cli/handlers/effect.js +4 -84
  20. package/dist/src/cli/handlers/guided-setup.js +1 -1
  21. package/dist/src/cli/handlers/init.js +1 -1
  22. package/dist/src/cli/handlers/install.js +6 -0
  23. package/dist/src/cli/handlers/local-executor.js +33 -6
  24. package/dist/src/cli/handlers/marketplace.js +15 -4
  25. package/dist/src/cli/handlers/setup.js +24 -20
  26. package/dist/src/cli/handlers/use.js +57 -15
  27. package/dist/src/cli/prompt-utils.js +73 -0
  28. package/dist/src/config/gitignore.js +4 -0
  29. package/dist/src/config/workspace.js +2 -1
  30. package/dist/src/cost/fleet-report.js +20 -22
  31. package/dist/src/decision/adapters/jev.js +20 -31
  32. package/dist/src/decision/ask.js +6 -1
  33. package/dist/src/decision/benchmark.js +19 -6
  34. package/dist/src/decision/driver.js +2 -2
  35. package/dist/src/decision/jev-credentials.js +18 -5
  36. package/dist/src/decision/ledger.js +230 -34
  37. package/dist/src/decision/qualification/gate-evidence.js +6 -4
  38. package/dist/src/decision/qualification/load-staged.js +47 -10
  39. package/dist/src/decision/qualification/runner.js +7 -3
  40. package/dist/src/discovery/interview.js +10 -5
  41. package/dist/src/discovery/plan-apply.js +26 -30
  42. package/dist/src/discovery/plan-validation.js +2 -2
  43. package/dist/src/effects/host.js +91 -0
  44. package/dist/src/extensions/commands/definitions.js +2 -2
  45. package/dist/src/extensions/project-local-gitignore.js +2 -0
  46. package/dist/src/issues/cli.js +16 -5
  47. package/dist/src/issues/live.js +44 -12
  48. package/dist/src/marketplace/exchange.js +18 -6
  49. package/dist/src/marketplace/provenance.js +3 -2
  50. package/dist/src/mcp/adapters/codex-runtime.js +46 -76
  51. package/dist/src/mcp/cli.mjs +122 -109
  52. package/dist/src/mcp/credentials.d.mts +4 -0
  53. package/dist/src/mcp/credentials.mjs +44 -3
  54. package/dist/src/mcp/grok-build-config.mjs +4 -1
  55. package/dist/src/mcp/muse-native-config.mjs +2 -1
  56. package/dist/src/mcp/omp-config.mjs +2 -0
  57. package/dist/src/mcp/registry.js +19 -15
  58. package/dist/src/mcp/registry.mjs +2 -18
  59. package/dist/src/mcp/tools/agentic-sandbox.mjs +46 -28
  60. package/dist/src/mcp/tools/worker-invoke.mjs +13 -2
  61. package/dist/src/memory/project-registry.js +2 -1
  62. package/dist/src/mission-protocol/worker-status.js +91 -1
  63. package/dist/src/models/grok-build-models.js +20 -0
  64. package/dist/src/models/worker-installation.d.mts +1 -1
  65. package/dist/src/models/worker-installation.mjs +65 -4
  66. package/dist/src/packages/adapters/git.js +99 -23
  67. package/dist/src/packages/adapters/gitea.js +8 -27
  68. package/dist/src/packages/adapters/github.js +7 -25
  69. package/dist/src/packages/registry.js +4 -4
  70. package/dist/src/process/cli-command.d.mts +5 -0
  71. package/dist/src/process/cli-command.mjs +42 -0
  72. package/dist/src/providers/provider-definitions.js +2 -2
  73. package/dist/src/serve/shared-host-scheduler.js +4 -1
  74. package/dist/src/serve/worker-invocation.js +10 -0
  75. package/dist/src/smiths/context-pipeline/claude-hook.js +9 -9
  76. package/dist/src/smiths/context-pipeline/context-file-safety.js +330 -9
  77. package/dist/src/smiths/context-pipeline/finalization.js +3 -2
  78. package/dist/src/smiths/context-pipeline/generator.js +7 -19
  79. package/dist/src/smiths/context-pipeline/legacy-inject.js +3 -10
  80. package/dist/src/smiths/context-pipeline/managed-hook.js +5 -5
  81. package/dist/src/smiths/context-pipeline/workspace-context.js +4 -12
  82. package/dist/src/storage/backends/fortemi.js +107 -49
  83. package/dist/src/tracker/project-inputs.js +2 -0
  84. package/dist/src/update/notifier.mjs +2 -4
  85. package/package.json +1 -1
  86. package/tools/agents/providers/base.mjs +5 -1
  87. package/tools/agents/providers/claude.mjs +5 -2
  88. package/tools/agents/providers/codex.mjs +4 -1
  89. package/tools/agents/providers/context-write.mjs +28 -0
  90. package/tools/agents/providers/copilot.mjs +5 -2
  91. package/tools/agents/providers/hermes.mjs +5 -2
  92. package/tools/agents/providers/omp.mjs +3 -1
  93. package/tools/agents/providers/rule-delivery.mjs +29 -12
  94. package/tools/agents/providers/warp.mjs +4 -1
  95. package/tools/agents/providers/windsurf.mjs +6 -3
  96. package/tools/ralph-external/lib/consecutive-failure-circuit-breaker.mjs +26 -0
  97. package/tools/ralph-external/lib/launcher-recovery-evidence.mjs +7 -1
  98. package/tools/ralph-external/lib/recovery-probe.mjs +14 -4
  99. package/tools/ralph-external/process-monitor.mjs +42 -4
  100. package/tools/ralph-external/session-launcher.mjs +125 -3
@@ -1,18 +1,339 @@
1
- import { lstat } from 'node:fs/promises';
2
- import { basename } from 'node:path';
3
- /** Never read or update a provider instruction file through a filesystem link. */
4
- export async function assertSafeContextFile(filePath) {
5
- let stat;
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ import { tmpdir } from 'node:os';
4
+ import { createHash } from 'node:crypto';
5
+ import { trackTemporaryDirectory } from '../../utils/temporary-directory.js';
6
+ const JOURNAL_ENV = 'AIWG_CONTEXT_WRITE_JOURNAL';
7
+ export const CONTEXT_STAGE_ENV = 'AIWG_CONTEXT_STAGE_LOG';
8
+ const digest = (bytes) => createHash('sha256').update(bytes).digest('hex');
9
+ const identity = (stat) => ({ dev: String(stat.dev), ino: String(stat.ino) });
10
+ const sameIdentity = (a, b) => a.dev === b.dev && a.ino === b.ino;
11
+ const sameAncestors = (a, b) => a.length === b.length && a.every((identity, index) => sameIdentity(identity, b[index]));
12
+ // Optional flags are absent on some supported hosts (notably Windows).
13
+ function flags(...names) {
14
+ return names.reduce((value, name) => {
15
+ const flag = fs.constants[name];
16
+ return typeof flag === 'number' ? value | flag : value;
17
+ }, 0);
18
+ }
19
+ // Identity is dev/ino/size/mtime. A replacement that recycles the freed inode
20
+ // number with the same size inside one coarse timestamp tick is
21
+ // indistinguishable by metadata; the content hash still has to match deploy
22
+ // output, so restoring over such a file loses nothing the deploy wrote.
23
+ const version = (stat) => ({ ...identity(stat), size: String(stat.size), mtime: String(stat.mtimeNs) });
24
+ const sameVersion = (a, b) => sameIdentity(a, b) && a.size === b.size && a.mtime === b.mtime;
25
+ /** Probe child traversal, not just the descriptor link itself. /dev/fd on macOS
26
+ * can expose a descriptor without supporting directory-relative child opens. */
27
+ function descriptorRoot(root) {
28
+ if (process.platform !== 'linux')
29
+ return null;
30
+ let fd;
31
+ try {
32
+ fd = fs.openSync(root, flags('O_RDONLY', 'O_DIRECTORY', 'O_NOFOLLOW'));
33
+ const child = `/proc/self/fd/${fd}/.`;
34
+ return sameIdentity(identity(fs.statSync(child, { bigint: true })), identity(fs.fstatSync(fd, { bigint: true }))) ? '/proc/self/fd' : null;
35
+ }
36
+ catch {
37
+ return null;
38
+ }
39
+ finally {
40
+ if (fd !== undefined)
41
+ fs.closeSync(fd);
42
+ }
43
+ }
44
+ /** Pin directories on Linux when descriptor-relative child traversal works.
45
+ * Elsewhere lstat every ancestor and recheck paths around each mutation. The
46
+ * portable fallback has a residual check-then-write race: only descriptor
47
+ * anchoring closes the interval between the last path check and the write. */
48
+ function withParent(file, create, action) {
49
+ const absolute = path.resolve(file);
50
+ const root = path.parse(absolute).root;
51
+ const parts = path.relative(root, absolute).split(path.sep);
52
+ const descriptors = [];
53
+ const identities = [];
54
+ const fdRoot = descriptorRoot(root);
55
+ try {
56
+ let directory = root;
57
+ for (const part of ['', ...parts.slice(0, -1)]) {
58
+ const next = part ? path.join(directory, part) : directory;
59
+ // Drive roots already exist; mkdir on a Windows drive root can be EPERM.
60
+ if (create && part) {
61
+ try {
62
+ fs.mkdirSync(next);
63
+ }
64
+ catch (error) {
65
+ if (error.code !== 'EEXIST')
66
+ throw error;
67
+ }
68
+ }
69
+ const checked = fs.lstatSync(next, { bigint: true });
70
+ if (checked.isSymbolicLink() || !checked.isDirectory())
71
+ throw new Error('Context ancestor changed');
72
+ identities.push(identity(checked));
73
+ directory = next;
74
+ if (fdRoot) {
75
+ const fd = fs.openSync(next, flags('O_RDONLY', 'O_DIRECTORY', 'O_NOFOLLOW'));
76
+ descriptors.push(fd);
77
+ if (!sameIdentity(identity(fs.fstatSync(fd, { bigint: true })), identity(checked)))
78
+ throw new Error('Context ancestor changed');
79
+ directory = `${fdRoot}/${fd}`;
80
+ }
81
+ }
82
+ const verify = () => {
83
+ let current = root;
84
+ for (let i = 0; i < identities.length; i++) {
85
+ if (i)
86
+ current = path.join(current, parts[i - 1]);
87
+ const stat = fs.lstatSync(current, { bigint: true });
88
+ if (!stat.isDirectory() || stat.isSymbolicLink() || !sameIdentity(identity(stat), identities[i]))
89
+ throw new Error('Context ancestor changed');
90
+ }
91
+ };
92
+ verify();
93
+ return action(path.join(directory, parts[parts.length - 1]), identities, verify);
94
+ }
95
+ finally {
96
+ for (const fd of descriptors.reverse())
97
+ fs.closeSync(fd);
98
+ }
99
+ }
100
+ function checkedVersion(stat) {
101
+ if (stat.isSymbolicLink() || !stat.isFile() || !(stat.mode & 292n) || !(stat.mode & 146n) || stat.nlink !== 1n)
102
+ throw new Error('Refusing unreadable, unwritable, linked or non-file context target');
103
+ return version(stat);
104
+ }
105
+ function verifyTarget(file, expected) {
106
+ if (!sameVersion(checkedVersion(fs.lstatSync(file, { bigint: true })), expected))
107
+ throw new Error('Context identity changed');
108
+ }
109
+ function state(fd) {
110
+ const stat = fs.fstatSync(fd, { bigint: true });
111
+ const before = checkedVersion(stat);
112
+ const bytes = Buffer.alloc(Number(stat.size));
113
+ let offset = 0;
114
+ while (offset < bytes.length) {
115
+ const count = fs.readSync(fd, bytes, offset, bytes.length - offset, offset);
116
+ if (!count)
117
+ throw new Error('Context changed while reading');
118
+ offset += count;
119
+ }
120
+ if (!sameVersion(checkedVersion(fs.fstatSync(fd, { bigint: true })), before))
121
+ throw new Error('Context changed while reading');
122
+ return { ...before, mode: Number(stat.mode & 4095n), bytes: bytes.toString('base64'), hash: digest(bytes) };
123
+ }
124
+ function readState(file) {
125
+ let checked;
6
126
  try {
7
- stat = await lstat(filePath);
127
+ checked = checkedVersion(fs.lstatSync(file, { bigint: true }));
8
128
  }
9
129
  catch (error) {
10
130
  if (error.code === 'ENOENT')
11
- return;
131
+ return null;
12
132
  throw error;
13
133
  }
14
- if (stat.isSymbolicLink() || !stat.isFile()) {
15
- throw new Error(`Refusing unsafe ${basename(filePath)} target: ${filePath}`);
134
+ const fd = fs.openSync(file, flags('O_RDONLY', 'O_NOFOLLOW', 'O_NONBLOCK'));
135
+ try {
136
+ const result = state(fd);
137
+ if (!sameVersion(result, checked))
138
+ throw new Error('Context identity changed');
139
+ verifyTarget(file, result);
140
+ return result;
141
+ }
142
+ finally {
143
+ fs.closeSync(fd);
144
+ }
145
+ }
146
+ function matches(a, b) {
147
+ return sameVersion(a, b) && a.hash === b.hash && a.mode === b.mode;
148
+ }
149
+ function entries() {
150
+ const file = process.env[JOURNAL_ENV];
151
+ return file ? fs.readFileSync(file, 'utf8').split('\n').filter(Boolean).map(line => JSON.parse(line)) : [];
152
+ }
153
+ /** Every context writer uses this boundary, including staged provider writers.
154
+ * Record immediately after the mutation, before returning to a fallible step. */
155
+ export function writeContextFileSync(file, content, _encoding) {
156
+ const absolute = path.resolve(file);
157
+ const stageLog = process.env[CONTEXT_STAGE_ENV];
158
+ const priorEntries = stageLog ? [] : entries();
159
+ withParent(absolute, true, (anchored, ancestors, verify) => {
160
+ const prior = readState(anchored);
161
+ const previous = priorEntries.reverse().find(entry => entry.path === absolute);
162
+ if (previous && (!prior || !matches(prior, previous.result) || !sameAncestors(ancestors, previous.ancestors)))
163
+ throw new Error(`Context changed during deployment: ${absolute}`);
164
+ verify();
165
+ const fd = fs.openSync(anchored, flags('O_RDWR', 'O_NOFOLLOW', 'O_NONBLOCK', ...(prior ? [] : ['O_CREAT', 'O_EXCL'])), prior?.mode ?? 0o666);
166
+ try {
167
+ verify();
168
+ if (prior && !matches(state(fd), prior))
169
+ throw new Error(`Context identity changed: ${absolute}`);
170
+ verify();
171
+ verifyTarget(anchored, prior ?? checkedVersion(fs.fstatSync(fd, { bigint: true })));
172
+ const buffer = Buffer.isBuffer(content) ? content : Buffer.from(content);
173
+ const opened = fs.fstatSync(fd, { bigint: true });
174
+ const priorBytes = prior ? Buffer.from(prior.bytes, 'base64') : Buffer.alloc(0);
175
+ const expected = Buffer.alloc(Math.max(buffer.length, priorBytes.length));
176
+ priorBytes.copy(expected);
177
+ let length = priorBytes.length;
178
+ try {
179
+ let offset = 0;
180
+ while (offset < buffer.length) {
181
+ const count = fs.writeSync(fd, buffer, offset, buffer.length - offset, offset);
182
+ if (!count)
183
+ throw new Error('Context write made no progress');
184
+ buffer.copy(expected, offset, offset, offset + count);
185
+ offset += count;
186
+ length = Math.max(length, offset);
187
+ }
188
+ fs.ftruncateSync(fd, buffer.length);
189
+ length = buffer.length;
190
+ }
191
+ finally {
192
+ // Hash the bytes this writer produced, never a later read that could
193
+ // accidentally claim an operator's concurrent edit as deploy-owned.
194
+ const completed = fs.fstatSync(fd, { bigint: true });
195
+ const result = { ...version(completed), ...identity(opened), size: String(length), mode: Number(opened.mode & 4095n), hash: digest(expected.subarray(0, length)) };
196
+ if (stageLog)
197
+ fs.appendFileSync(stageLog, `${JSON.stringify(absolute)}\n`);
198
+ else if (process.env[JOURNAL_ENV])
199
+ fs.appendFileSync(process.env[JOURNAL_ENV], `${JSON.stringify({ path: absolute, prior, result, ancestors })}\n`);
200
+ verify();
201
+ verifyTarget(anchored, result);
202
+ if (!matches(state(fd), result))
203
+ throw new Error(`Context changed during write: ${absolute}`);
204
+ }
205
+ }
206
+ finally {
207
+ fs.closeSync(fd);
208
+ }
209
+ });
210
+ }
211
+ /** Verify before reading provider startup files as well as at the write boundary. */
212
+ export async function assertSafeContextFile(file) {
213
+ try {
214
+ withParent(file, false, anchored => { readState(anchored); });
215
+ }
216
+ catch (error) {
217
+ if (error.code === 'ENOENT')
218
+ return;
219
+ throw new Error(`Refusing unsafe ${path.basename(file)} target: ${file}`, { cause: error });
220
+ }
221
+ }
222
+ /** Owned by the outer use invocation; the private log also crosses subprocesses. */
223
+ export class ContextWriteJournal {
224
+ directory = fs.mkdtempSync(path.join(tmpdir(), 'aiwg-context-writes-'));
225
+ removeDirectory = trackTemporaryDirectory(this.directory);
226
+ previous = process.env[JOURNAL_ENV];
227
+ constructor() {
228
+ fs.chmodSync(this.directory, 0o700);
229
+ const file = path.join(this.directory, 'writes.jsonl');
230
+ fs.writeFileSync(file, '', { mode: 0o600, flag: 'wx' });
231
+ process.env[JOURNAL_ENV] = file;
232
+ }
233
+ rollback() {
234
+ const result = { restored: [], conflicts: [] };
235
+ const blocked = new Set();
236
+ // Restoring a later pass changes mtime. Carry that verified version back
237
+ // through earlier entries rather than treating our own restore as an edit.
238
+ const restoredVersions = new Map();
239
+ for (const entry of entries().reverse()) {
240
+ if (blocked.has(entry.path))
241
+ continue;
242
+ try {
243
+ withParent(entry.path, false, (anchored, ancestors, verify) => {
244
+ if (!sameAncestors(ancestors, entry.ancestors))
245
+ throw new Error('Context ancestor identity changed');
246
+ const checked = checkedVersion(fs.lstatSync(anchored, { bigint: true }));
247
+ const fd = fs.openSync(anchored, flags('O_RDWR', 'O_NOFOLLOW', 'O_NONBLOCK'));
248
+ try {
249
+ verify();
250
+ const current = state(fd);
251
+ if (!sameVersion(current, checked))
252
+ throw new Error('Context identity changed');
253
+ if (!matches(current, restoredVersions.get(entry.path) ?? entry.result))
254
+ throw new Error('Context changed since deployment wrote it');
255
+ verify();
256
+ verifyTarget(anchored, current);
257
+ if (!matches(state(fd), current))
258
+ throw new Error('Context changed before restore');
259
+ if (entry.prior) {
260
+ const bytes = Buffer.from(entry.prior.bytes, 'base64');
261
+ let offset = 0;
262
+ while (offset < bytes.length) {
263
+ const count = fs.writeSync(fd, bytes, offset, bytes.length - offset, offset);
264
+ if (!count)
265
+ throw new Error('Context restore made no progress');
266
+ offset += count;
267
+ }
268
+ fs.ftruncateSync(fd, bytes.length);
269
+ fs.fchmodSync(fd, entry.prior.mode);
270
+ verify();
271
+ const restored = state(fd);
272
+ if (!sameIdentity(restored, current) || restored.hash !== entry.prior.hash || restored.mode !== entry.prior.mode)
273
+ throw new Error('Context changed during restore');
274
+ verifyTarget(anchored, restored);
275
+ restoredVersions.set(entry.path, restored);
276
+ }
277
+ else {
278
+ // Quarantine before deleting: a final-component swap between the
279
+ // identity check and rename must be kept, never silently unlinked.
280
+ const quarantine = fs.mkdtempSync(path.join(path.dirname(anchored), '.aiwg-context-rollback-'));
281
+ const retained = path.join(quarantine, 'context');
282
+ try {
283
+ fs.renameSync(anchored, retained);
284
+ try {
285
+ const moved = readState(retained);
286
+ if (!moved || !matches(moved, current))
287
+ throw new Error('Context changed during removal');
288
+ verify();
289
+ fs.unlinkSync(retained);
290
+ }
291
+ catch (error) {
292
+ try {
293
+ fs.linkSync(retained, anchored);
294
+ fs.unlinkSync(retained);
295
+ }
296
+ catch {
297
+ const saved = path.join(path.dirname(entry.path), path.basename(quarantine), 'context');
298
+ throw new Error(`Context changed during removal; preserved at ${saved}`, { cause: error });
299
+ }
300
+ throw error;
301
+ }
302
+ verify();
303
+ if (fs.existsSync(anchored))
304
+ throw new Error('Context path replaced during rollback');
305
+ }
306
+ finally {
307
+ try {
308
+ fs.rmdirSync(quarantine);
309
+ }
310
+ catch (error) {
311
+ if (error.code !== 'ENOTEMPTY')
312
+ throw error;
313
+ }
314
+ }
315
+ }
316
+ }
317
+ finally {
318
+ fs.closeSync(fd);
319
+ }
320
+ });
321
+ if (!result.restored.includes(entry.path))
322
+ result.restored.push(entry.path);
323
+ }
324
+ catch (error) {
325
+ blocked.add(entry.path);
326
+ result.conflicts.push({ path: entry.path, reason: error instanceof Error ? error.message : String(error) });
327
+ }
328
+ }
329
+ return result;
330
+ }
331
+ dispose() {
332
+ if (this.previous === undefined)
333
+ delete process.env[JOURNAL_ENV];
334
+ else
335
+ process.env[JOURNAL_ENV] = this.previous;
336
+ this.removeDirectory();
16
337
  }
17
338
  }
18
339
  //# sourceMappingURL=context-file-safety.js.map
@@ -1,3 +1,4 @@
1
+ import { writeContextFileSync } from './context-file-safety.js';
1
2
  /**
2
3
  * Context-finalization pass for generated provider context files.
3
4
  *
@@ -132,13 +133,13 @@ export async function writeNormalizedAiwgMd(projectPath) {
132
133
  const content = await buildNormalizedAiwgMd(projectPath, existing);
133
134
  await fs.mkdir(path.dirname(targetPath), { recursive: true });
134
135
  const normalizedContent = content.endsWith('\n') ? content : `${content}\n`;
135
- await fs.writeFile(targetPath, normalizedContent, 'utf8');
136
+ writeContextFileSync(targetPath, normalizedContent, 'utf8');
136
137
  const artifactRoot = resolveProjectAiwgDir(projectPath);
137
138
  const artifactPath = projectAiwgPath(projectPath, 'AIWG.md');
138
139
  if (artifactPath !== targetPath) {
139
140
  try {
140
141
  await fs.access(artifactRoot);
141
- await fs.writeFile(artifactPath, normalizedContent, 'utf8');
142
+ writeContextFileSync(artifactPath, normalizedContent, 'utf8');
142
143
  }
143
144
  catch (error) {
144
145
  if (error.code !== 'ENOENT')
@@ -20,7 +20,7 @@ import { writeNormalizedAiwgMd } from './finalization.js';
20
20
  import { shouldEmitAgentsMd, shouldEmitAiwgMd, shouldEmitClaudeMdHook } from './provider-policy.js';
21
21
  import { ensureClaudeMdHook } from './claude-hook.js';
22
22
  import { ensureManagedHook } from './managed-hook.js';
23
- import { assertSafeContextFile } from './context-file-safety.js';
23
+ import { assertSafeContextFile, writeContextFileSync } from './context-file-safety.js';
24
24
  import { buildProviderBootstrapBlock, ensureWorkspaceContext, registerProviderContext, } from './workspace-context.js';
25
25
  import { projectControlPath } from '../../config/project-artifacts.js';
26
26
  const SECTION_TITLES = {
@@ -195,9 +195,8 @@ export async function isOverwriteSafe(filePath) {
195
195
  }
196
196
  }
197
197
  /**
198
- * Atomically write a file via tmpfile + rename.
199
- *
200
- * Per ADR-4 §4 atomic write requirement; on failure no partial state is persisted.
198
+ * Preserve rule-owned blocks, then write through the safe context boundary.
199
+ * The deploy journal retains the preimage even if a later step fails.
201
200
  */
202
201
  async function atomicWrite(filePath, content, provider) {
203
202
  // Rule delivery owns this block. Context regeneration runs after deployment
@@ -217,18 +216,7 @@ async function atomicWrite(filePath, content, provider) {
217
216
  if (provider === 'hermes' && ['AGENTS.md', '.hermes.md'].includes(path.basename(filePath)) && content.length > 19000) {
218
217
  throw new Error(`Hermes ${path.basename(filePath)} exceeds hard cap of 19000 characters`);
219
218
  }
220
- const dir = path.dirname(filePath);
221
- const tmp = path.join(dir, `.${path.basename(filePath)}.tmp.${process.pid}`);
222
- await fs.mkdir(dir, { recursive: true });
223
- await fs.writeFile(tmp, content, 'utf8');
224
- try {
225
- await fs.rename(tmp, filePath);
226
- }
227
- catch (err) {
228
- // Best-effort cleanup of tmpfile on rename failure.
229
- await fs.unlink(tmp).catch(() => undefined);
230
- throw err;
231
- }
219
+ writeContextFileSync(filePath, content);
232
220
  return bytes;
233
221
  }
234
222
  /**
@@ -312,7 +300,7 @@ async function writeTwinFiles(provider, projectPath, agentsMdContent, opts, resu
312
300
  const original = await fs.readFile(twinPath, 'utf8');
313
301
  if (!original.includes(AIWG_SIGNATURE_COMMENT) && !/Generated by\s+(AIWG|aiwg)/i.test(original.split('\n').slice(0, 4).join('\n'))) {
314
302
  const backupPath = `${twinPath}.bak.${new Date().toISOString().replace(/[:.]/g, '-')}`;
315
- await fs.writeFile(backupPath, original, 'utf8');
303
+ writeContextFileSync(backupPath, original, 'utf8');
316
304
  result.backupPaths.push(backupPath);
317
305
  }
318
306
  }
@@ -412,7 +400,7 @@ export async function generate(opts) {
412
400
  const original = await fs.readFile(agentsMdPath, 'utf8');
413
401
  if (!original.includes(AIWG_SIGNATURE_COMMENT)) {
414
402
  const backupPath = `${agentsMdPath}.bak.${new Date().toISOString().replace(/[:.]/g, '-')}`;
415
- await fs.writeFile(backupPath, original, 'utf8');
403
+ writeContextFileSync(backupPath, original, 'utf8');
416
404
  result.backupPaths.push(backupPath);
417
405
  }
418
406
  }
@@ -463,7 +451,7 @@ export async function generate(opts) {
463
451
  const original = await fs.readFile(aiwgMdPath, 'utf8');
464
452
  if (!original.includes(AIWG_SIGNATURE_COMMENT)) {
465
453
  const backupPath = `${aiwgMdPath}.bak.${new Date().toISOString().replace(/[:.]/g, '-')}`;
466
- await fs.writeFile(backupPath, original, 'utf8');
454
+ writeContextFileSync(backupPath, original, 'utf8');
467
455
  result.backupPaths.push(backupPath);
468
456
  }
469
457
  }
@@ -1,3 +1,4 @@
1
+ import { writeContextFileSync } from './context-file-safety.js';
1
2
  import { promises as fs } from 'node:fs';
2
3
  import path from 'node:path';
3
4
  import { getProviderDefinition } from '../../providers/provider-definitions.js';
@@ -60,15 +61,7 @@ async function readOptional(file) {
60
61
  }
61
62
  async function atomicWrite(file, content) {
62
63
  await fs.mkdir(path.dirname(file), { recursive: true });
63
- const temporary = path.join(path.dirname(file), `.${path.basename(file)}.tmp.${process.pid}`);
64
- await fs.writeFile(temporary, content, 'utf8');
65
- try {
66
- await fs.rename(temporary, file);
67
- }
68
- catch (error) {
69
- await fs.rm(temporary, { force: true });
70
- throw error;
71
- }
64
+ writeContextFileSync(file, content);
72
65
  }
73
66
  export async function injectLegacyContext(provider, projectPath, frameworkContent, options = {}) {
74
67
  const definition = getProviderDefinition(provider);
@@ -93,7 +86,7 @@ export async function injectLegacyContext(provider, projectPath, frameworkConten
93
86
  if (existing !== null) {
94
87
  const backup = `${target}.bak.${new Date().toISOString().replace(/[:.]/g, '-')}`;
95
88
  await fs.mkdir(path.dirname(backup), { recursive: true });
96
- await fs.writeFile(backup, existing, 'utf8');
89
+ writeContextFileSync(backup, existing, 'utf8');
97
90
  backups.push(backup);
98
91
  }
99
92
  await atomicWrite(target, output);
@@ -15,7 +15,7 @@ import * as fs from 'fs/promises';
15
15
  import * as path from 'path';
16
16
  import { buildProviderBootstrapBlock } from './workspace-context.js';
17
17
  import { dominantLineEnding, withLineEnding } from './line-endings.js';
18
- import { assertSafeContextFile } from './context-file-safety.js';
18
+ import { assertSafeContextFile, writeContextFileSync } from './context-file-safety.js';
19
19
  export const CONTEXT_HOOK_START = '<!-- AIWG:context-hook:start -->';
20
20
  export const CONTEXT_HOOK_END = '<!-- AIWG:context-hook:end -->';
21
21
  /** The managed block — loads canonical workspace context before framework context. */
@@ -53,7 +53,7 @@ export async function ensureManagedHook(filePath, opts = {}) {
53
53
  }
54
54
  catch (err) {
55
55
  if (err.code === 'ENOENT') {
56
- await fs.writeFile(filePath, `${block}\n`, 'utf8');
56
+ writeContextFileSync(filePath, `${block}\n`, 'utf8');
57
57
  result.action = 'created';
58
58
  return result;
59
59
  }
@@ -71,7 +71,7 @@ export async function ensureManagedHook(filePath, opts = {}) {
71
71
  // No managed block — append it to the end, preserving everything above.
72
72
  if (s === -1 && e === -1) {
73
73
  const trimmed = existing.replace(/(?:\r?\n)+$/, lineEnding);
74
- await fs.writeFile(filePath, `${trimmed}${lineEnding}${block}${lineEnding}`, 'utf8');
74
+ writeContextFileSync(filePath, `${trimmed}${lineEnding}${block}${lineEnding}`, 'utf8');
75
75
  result.action = 'inserted';
76
76
  return result;
77
77
  }
@@ -79,7 +79,7 @@ export async function ensureManagedHook(filePath, opts = {}) {
79
79
  if (s === -1 || e === -1) {
80
80
  if (opts.force) {
81
81
  const trimmed = existing.replace(/(?:\r?\n)+$/, lineEnding);
82
- await fs.writeFile(filePath, `${trimmed}${lineEnding}${block}${lineEnding}`, 'utf8');
82
+ writeContextFileSync(filePath, `${trimmed}${lineEnding}${block}${lineEnding}`, 'utf8');
83
83
  result.action = 'inserted';
84
84
  return result;
85
85
  }
@@ -93,7 +93,7 @@ export async function ensureManagedHook(filePath, opts = {}) {
93
93
  result.action = 'unchanged';
94
94
  return result;
95
95
  }
96
- await fs.writeFile(filePath, existing.substring(0, s) + block + existing.substring(endOff), 'utf8');
96
+ writeContextFileSync(filePath, existing.substring(0, s) + block + existing.substring(endOff), 'utf8');
97
97
  result.action = 'updated';
98
98
  return result;
99
99
  }
@@ -1,3 +1,4 @@
1
+ import { writeContextFileSync } from './context-file-safety.js';
1
2
  /**
2
3
  * Canonical cross-provider workspace context graph (#1811).
3
4
  *
@@ -71,16 +72,7 @@ async function readOptional(filePath) {
71
72
  }
72
73
  }
73
74
  async function atomicWrite(filePath, content) {
74
- await fs.mkdir(path.dirname(filePath), { recursive: true });
75
- const temporary = path.join(path.dirname(filePath), `.${path.basename(filePath)}.tmp.${process.pid}`);
76
- await fs.writeFile(temporary, content, 'utf8');
77
- try {
78
- await fs.rename(temporary, filePath);
79
- }
80
- catch (error) {
81
- await fs.rm(temporary, { force: true }).catch(() => undefined);
82
- throw error;
83
- }
75
+ writeContextFileSync(filePath, content);
84
76
  }
85
77
  function resolveProjectRelative(projectPath, relativePath) {
86
78
  if (path.isAbsolute(relativePath))
@@ -513,7 +505,7 @@ export async function ensureWorkspaceContext(projectPath, options = {}) {
513
505
  return { path: workspacePath, action: 'preserved', warnings };
514
506
  }
515
507
  const backupPath = `${workspacePath}.bak.${new Date().toISOString().replace(/[:.]/g, '-')}`;
516
- await fs.writeFile(backupPath, existing, 'utf8');
508
+ writeContextFileSync(backupPath, existing, 'utf8');
517
509
  await atomicWrite(workspacePath, buildWorkspaceDocument(projectPath, existing, providerFiles, residentMemory));
518
510
  return { path: workspacePath, action: 'updated', backupPath, warnings };
519
511
  }
@@ -1122,7 +1114,7 @@ export async function migrateWorkspaceContext(projectPath, options = {}) {
1122
1114
  let preimage = null;
1123
1115
  if (existing !== null) {
1124
1116
  preimage = path.relative(projectPath, path.join(preimageDir, safePreimageName(relativePath))).replace(/\\/g, '/');
1125
- await fs.writeFile(path.join(projectPath, preimage), existing, 'utf8');
1117
+ writeContextFileSync(path.join(projectPath, preimage), existing, 'utf8');
1126
1118
  backups.push(preimage);
1127
1119
  }
1128
1120
  manifest.files.push({ path: relativePath, existed: existing !== null, preimage, outputChecksum: sha256(content) });