@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.
- package/bin/aiwg.mjs +4 -2
- package/dist/src/a2a/agent-card.js +11 -0
- package/dist/src/api/index.d.ts +1 -0
- package/dist/src/api/index.js +1 -0
- package/dist/src/artifacts/fortemi-shard-contract/2.2.0/contract.json +103 -0
- package/dist/src/artifacts/fortemi-shard-contract/2.2.0/full-v1-embedding-config.schema.json +212 -0
- package/dist/src/artifacts/fortemi-shard-contract/2.2.0/full-v1-embedding-set.schema.json +240 -0
- package/dist/src/artifacts/fortemi-shard-contract/2.2.0/full-v1-manifest.schema.json +451 -0
- package/dist/src/artifacts/fortemi-shard-export.js +284 -0
- package/dist/src/artifacts/index-preparation.js +2 -0
- package/dist/src/auth/client.js +5 -1
- package/dist/src/auth/credential-store.js +5 -1
- package/dist/src/auth/sensitive-input.js +151 -0
- package/dist/src/channel/manager.mjs +3 -0
- package/dist/src/cli/handlers/agentcard.js +47 -25
- package/dist/src/cli/handlers/auth.js +22 -2
- package/dist/src/cli/handlers/cost-report.js +7 -1
- package/dist/src/cli/handlers/decision.js +36 -10
- package/dist/src/cli/handlers/effect.js +4 -84
- package/dist/src/cli/handlers/guided-setup.js +1 -1
- package/dist/src/cli/handlers/init.js +1 -1
- package/dist/src/cli/handlers/install.js +6 -0
- package/dist/src/cli/handlers/local-executor.js +33 -6
- package/dist/src/cli/handlers/marketplace.js +15 -4
- package/dist/src/cli/handlers/setup.js +24 -20
- package/dist/src/cli/handlers/use.js +57 -15
- package/dist/src/cli/prompt-utils.js +73 -0
- package/dist/src/config/gitignore.js +4 -0
- package/dist/src/config/workspace.js +2 -1
- package/dist/src/cost/fleet-report.js +20 -22
- package/dist/src/decision/adapters/jev.js +20 -31
- package/dist/src/decision/ask.js +6 -1
- package/dist/src/decision/benchmark.js +19 -6
- package/dist/src/decision/driver.js +2 -2
- package/dist/src/decision/jev-credentials.js +18 -5
- package/dist/src/decision/ledger.js +230 -34
- package/dist/src/decision/qualification/gate-evidence.js +6 -4
- package/dist/src/decision/qualification/load-staged.js +47 -10
- package/dist/src/decision/qualification/runner.js +7 -3
- package/dist/src/discovery/interview.js +10 -5
- package/dist/src/discovery/plan-apply.js +26 -30
- package/dist/src/discovery/plan-validation.js +2 -2
- package/dist/src/effects/host.js +91 -0
- package/dist/src/extensions/commands/definitions.js +2 -2
- package/dist/src/extensions/project-local-gitignore.js +2 -0
- package/dist/src/issues/cli.js +16 -5
- package/dist/src/issues/live.js +44 -12
- package/dist/src/marketplace/exchange.js +18 -6
- package/dist/src/marketplace/provenance.js +3 -2
- package/dist/src/mcp/adapters/codex-runtime.js +46 -76
- package/dist/src/mcp/cli.mjs +122 -109
- package/dist/src/mcp/credentials.d.mts +4 -0
- package/dist/src/mcp/credentials.mjs +44 -3
- package/dist/src/mcp/grok-build-config.mjs +4 -1
- package/dist/src/mcp/muse-native-config.mjs +2 -1
- package/dist/src/mcp/omp-config.mjs +2 -0
- package/dist/src/mcp/registry.js +19 -15
- package/dist/src/mcp/registry.mjs +2 -18
- package/dist/src/mcp/tools/agentic-sandbox.mjs +46 -28
- package/dist/src/mcp/tools/worker-invoke.mjs +13 -2
- package/dist/src/memory/project-registry.js +2 -1
- package/dist/src/mission-protocol/worker-status.js +91 -1
- package/dist/src/models/grok-build-models.js +20 -0
- package/dist/src/models/worker-installation.d.mts +1 -1
- package/dist/src/models/worker-installation.mjs +65 -4
- package/dist/src/packages/adapters/git.js +99 -23
- package/dist/src/packages/adapters/gitea.js +8 -27
- package/dist/src/packages/adapters/github.js +7 -25
- package/dist/src/packages/registry.js +4 -4
- package/dist/src/process/cli-command.d.mts +5 -0
- package/dist/src/process/cli-command.mjs +42 -0
- package/dist/src/providers/provider-definitions.js +2 -2
- package/dist/src/serve/shared-host-scheduler.js +4 -1
- package/dist/src/serve/worker-invocation.js +10 -0
- package/dist/src/smiths/context-pipeline/claude-hook.js +9 -9
- package/dist/src/smiths/context-pipeline/context-file-safety.js +330 -9
- package/dist/src/smiths/context-pipeline/finalization.js +3 -2
- package/dist/src/smiths/context-pipeline/generator.js +7 -19
- package/dist/src/smiths/context-pipeline/legacy-inject.js +3 -10
- package/dist/src/smiths/context-pipeline/managed-hook.js +5 -5
- package/dist/src/smiths/context-pipeline/workspace-context.js +4 -12
- package/dist/src/storage/backends/fortemi.js +107 -49
- package/dist/src/tracker/project-inputs.js +2 -0
- package/dist/src/update/notifier.mjs +2 -4
- package/package.json +1 -1
- package/tools/agents/providers/base.mjs +5 -1
- package/tools/agents/providers/claude.mjs +5 -2
- package/tools/agents/providers/codex.mjs +4 -1
- package/tools/agents/providers/context-write.mjs +28 -0
- package/tools/agents/providers/copilot.mjs +5 -2
- package/tools/agents/providers/hermes.mjs +5 -2
- package/tools/agents/providers/omp.mjs +3 -1
- package/tools/agents/providers/rule-delivery.mjs +29 -12
- package/tools/agents/providers/warp.mjs +4 -1
- package/tools/agents/providers/windsurf.mjs +6 -3
- package/tools/ralph-external/lib/consecutive-failure-circuit-breaker.mjs +26 -0
- package/tools/ralph-external/lib/launcher-recovery-evidence.mjs +7 -1
- package/tools/ralph-external/lib/recovery-probe.mjs +14 -4
- package/tools/ralph-external/process-monitor.mjs +42 -4
- package/tools/ralph-external/session-launcher.mjs +125 -3
|
@@ -1,18 +1,339 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
|
|
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
|
-
|
|
15
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) });
|