@orion-agents/orion-code 0.3.26 → 0.3.28

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.
@@ -0,0 +1,330 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ReviewSnapshotError = void 0;
4
+ exports.reviewUnifiedDiffArgs = reviewUnifiedDiffArgs;
5
+ exports.captureReviewSnapshot = captureReviewSnapshot;
6
+ exports.createContainedWorktreeReader = createContainedWorktreeReader;
7
+ /**
8
+ * v0.3.27 PR1 — the minimal shared Git snapshot reader for code review
9
+ * (plan §4.2). One reader, two scopes:
10
+ *
11
+ * - `worktree`: staged, unstaged and untracked changes are recorded as
12
+ * separate sources for the same path and are never merged into one
13
+ * composite diff.
14
+ * - `base`: the ref, HEAD and merge-base are resolved to immutable OIDs at
15
+ * admission; later ref movement cannot change the frozen input.
16
+ *
17
+ * Security posture (§4.2.7): git runs with argument arrays through the read
18
+ * profile, never a shell; external diff drivers and textconv are disabled;
19
+ * blobs are read by OID; untracked working files are read directly under the
20
+ * same sensitive/binary/boundary policies as everything else. No optional
21
+ * locks, no arbitrary subcommands exposed to the model.
22
+ */
23
+ const crypto_1 = require("crypto");
24
+ const fs_1 = require("fs");
25
+ const promises_1 = require("fs/promises");
26
+ const path_1 = require("path");
27
+ const redaction_1 = require("../redaction");
28
+ const contract_1 = require("./contract");
29
+ class ReviewSnapshotError extends Error {
30
+ constructor(message, code) {
31
+ super(message);
32
+ this.code = code;
33
+ this.name = 'ReviewSnapshotError';
34
+ }
35
+ }
36
+ exports.ReviewSnapshotError = ReviewSnapshotError;
37
+ function sha256(value) {
38
+ return (0, crypto_1.createHash)('sha256').update(value, 'utf8').digest('hex');
39
+ }
40
+ function assertSafePath(value) {
41
+ if (!value ||
42
+ value.length > 1024 ||
43
+ value.startsWith('/') ||
44
+ value.includes('\0') ||
45
+ value.split('/').includes('..')) {
46
+ throw new ReviewSnapshotError(`Unsafe review path: ${value}`, 'path_invalid');
47
+ }
48
+ return value;
49
+ }
50
+ /** Parses `git diff --name-status -z` records into status/path groups. */
51
+ function parseNameStatusZ(records) {
52
+ const parsed = [];
53
+ let index = 0;
54
+ while (index < records.length) {
55
+ const record = records[index];
56
+ if (!record) {
57
+ index += 1;
58
+ continue;
59
+ }
60
+ const status = record.slice(0, 1);
61
+ const paths = [];
62
+ if (status === 'R' || status === 'C') {
63
+ paths.push(records[index + 1] ?? '', records[index + 2] ?? '');
64
+ index += 3;
65
+ }
66
+ else {
67
+ paths.push(records[index + 1] ?? '');
68
+ index += 2;
69
+ }
70
+ parsed.push({ status, paths });
71
+ }
72
+ return parsed;
73
+ }
74
+ function parseDiffHunks(diffText) {
75
+ const files = [];
76
+ let current;
77
+ const HUNK = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)$/;
78
+ for (const line of diffText.split('\n')) {
79
+ if (line.startsWith('diff --git ')) {
80
+ const rest = line.slice('diff --git '.length);
81
+ const split = rest.indexOf(' b/');
82
+ current = {
83
+ path: split >= 0 ? rest.slice(split + 3) : rest,
84
+ hunks: [],
85
+ diffBytes: 0,
86
+ };
87
+ files.push(current);
88
+ continue;
89
+ }
90
+ if (!current)
91
+ continue;
92
+ current.diffBytes += line.length + 1;
93
+ const header = HUNK.exec(line);
94
+ if (header) {
95
+ current.hunks.push({
96
+ oldStart: Number(header[1]),
97
+ oldLines: header[2] === undefined ? 1 : Number(header[2]),
98
+ newStart: Number(header[3]),
99
+ newLines: header[4] === undefined ? 1 : Number(header[4]),
100
+ header: line,
101
+ });
102
+ }
103
+ }
104
+ return files;
105
+ }
106
+ function pathMatches(path, requestedPaths) {
107
+ if (requestedPaths.length === 0)
108
+ return true;
109
+ return requestedPaths.some(filter => path === filter || path.startsWith(`${filter}/`));
110
+ }
111
+ /** The read-only git arguments for the scope's unified diff. Exported so the
112
+ * v0.3.28 runner captures the exact same diff the snapshot froze instead of
113
+ * re-deriving OIDs after admission. */
114
+ function reviewUnifiedDiffArgs(selector, oids) {
115
+ return selector.kind === 'base'
116
+ ? ['diff', '--no-ext-diff', '--no-textconv', oids.baseOid, oids.headOid]
117
+ : ['diff', '--no-ext-diff', '--no-textconv', 'HEAD'];
118
+ }
119
+ /** Resolves the selector to a frozen snapshot. Every OID and digest in the
120
+ * returned object was taken at admission time; callers persist the returned
121
+ * digest as the review's input identity. */
122
+ async function captureReviewSnapshot(options) {
123
+ const budgets = { ...contract_1.DEFAULT_REVIEW_BUDGETS, ...options.budgets };
124
+ const { runGit } = options;
125
+ const requestedPaths = (options.selector.paths ?? []).map(assertSafePath);
126
+ let baseOid = null;
127
+ let headOid = null;
128
+ let nameStatusArgs;
129
+ let unifiedArgs;
130
+ if (options.selector.kind === 'base') {
131
+ headOid = (await runGit(['rev-parse', '--verify', 'HEAD'])).trim();
132
+ const refOid = (await runGit(['rev-parse', '--verify', `${options.selector.ref}^{commit}`]).catch(() => '')).trim();
133
+ if (!refOid) {
134
+ throw new ReviewSnapshotError(`Review base ref could not be resolved: ${options.selector.ref}`, 'ref_unresolved');
135
+ }
136
+ baseOid = (await runGit(['merge-base', refOid, headOid]).catch(() => '')).trim();
137
+ if (!baseOid) {
138
+ throw new ReviewSnapshotError(`No merge base between ${options.selector.ref} and HEAD`, 'no_merge_base');
139
+ }
140
+ nameStatusArgs = [
141
+ 'diff',
142
+ '--no-ext-diff',
143
+ '--no-textconv',
144
+ '--find-renames',
145
+ '--name-status',
146
+ '-z',
147
+ baseOid,
148
+ headOid,
149
+ ];
150
+ unifiedArgs = reviewUnifiedDiffArgs(options.selector, { baseOid, headOid });
151
+ }
152
+ else {
153
+ nameStatusArgs = [
154
+ 'diff',
155
+ '--no-ext-diff',
156
+ '--no-textconv',
157
+ '--find-renames',
158
+ '--name-status',
159
+ '-z',
160
+ 'HEAD',
161
+ ];
162
+ unifiedArgs = reviewUnifiedDiffArgs(options.selector, { baseOid, headOid });
163
+ }
164
+ const nameStatusRaw = await runGit(nameStatusArgs);
165
+ const changed = parseNameStatusZ(nameStatusRaw.split('\0')).filter(entry => {
166
+ const path = entry.paths.at(-1);
167
+ return Boolean(path) && pathMatches(path, requestedPaths);
168
+ });
169
+ const files = [];
170
+ const skipped = [];
171
+ const seenPaths = new Set();
172
+ for (const entry of changed) {
173
+ const path = assertSafePath(entry.paths.at(-1));
174
+ if (seenPaths.has(path))
175
+ continue;
176
+ if (files.length >= budgets.maxFiles) {
177
+ skipped.push({ path, reason: 'over_budget_files' });
178
+ continue;
179
+ }
180
+ seenPaths.add(path);
181
+ const oldPath = entry.status === 'R' || entry.status === 'C' ? (entry.paths[0] ?? null) : null;
182
+ const sensitive = (0, redaction_1.isSensitiveFilePath)(path) || (oldPath ? (0, redaction_1.isSensitiveFilePath)(oldPath) : false);
183
+ if (sensitive) {
184
+ skipped.push({ path, reason: 'sensitive' });
185
+ continue;
186
+ }
187
+ if (entry.status === 'B') {
188
+ skipped.push({ path, reason: 'binary' });
189
+ continue;
190
+ }
191
+ files.push({
192
+ fileKey: sha256(`${entry.status}:${oldPath ?? ''}:${path}`).slice(0, 16),
193
+ path,
194
+ oldPath,
195
+ sensitive: false,
196
+ sides: [],
197
+ hunks: [],
198
+ });
199
+ }
200
+ const parsedFiles = parseDiffHunks(await runGit(unifiedArgs));
201
+ for (const file of files) {
202
+ const match = parsedFiles.find(candidate => candidate.path === file.path);
203
+ if (match)
204
+ file.hunks.push(...match.hunks);
205
+ }
206
+ for (const file of files) {
207
+ const totalDiff = file.hunks.reduce((sum, hunk) => sum + hunk.oldLines + hunk.newLines, 0);
208
+ if (totalDiff * 128 > budgets.maxDiffBytes) {
209
+ skipped.push({ path: file.path, reason: 'over_budget_bytes' });
210
+ continue;
211
+ }
212
+ if (options.selector.kind === 'base') {
213
+ const headOidForPath = (await runGit(['rev-parse', '--verify', `${headOid}:${file.path}`]).catch(() => '')).trim();
214
+ const baseOidForPath = (await runGit([
215
+ 'rev-parse',
216
+ '--verify',
217
+ `${baseOid}:${file.oldPath ?? file.path}`,
218
+ ]).catch(() => '')).trim();
219
+ if (headOidForPath) {
220
+ file.sides.push({
221
+ source: 'head',
222
+ oid: headOidForPath,
223
+ contentDigest: headOidForPath,
224
+ byteSize: Number((await runGit(['cat-file', '-s', headOidForPath])).trim()) || 0,
225
+ truncated: false,
226
+ });
227
+ }
228
+ if (baseOidForPath) {
229
+ file.sides.push({
230
+ source: 'base',
231
+ oid: baseOidForPath,
232
+ contentDigest: baseOidForPath,
233
+ byteSize: Number((await runGit(['cat-file', '-s', baseOidForPath])).trim()) || 0,
234
+ truncated: false,
235
+ });
236
+ }
237
+ }
238
+ else {
239
+ const indexOid = (await runGit(['ls-files', '-s', '-z', '--', file.path]).then(raw => {
240
+ const record = raw.split('\0').find(item => item.length > 0);
241
+ return record ? (record.split(' ')[1] ?? '') : '';
242
+ })).trim();
243
+ if (indexOid) {
244
+ file.sides.push({
245
+ source: 'staged',
246
+ oid: indexOid,
247
+ contentDigest: indexOid,
248
+ byteSize: Number((await runGit(['cat-file', '-s', indexOid])).trim()) || 0,
249
+ truncated: false,
250
+ });
251
+ }
252
+ if (options.readWorktreeFile) {
253
+ const before = await options.readWorktreeFile(file.path);
254
+ if (before === null) {
255
+ file.sides.push({
256
+ source: 'unstaged',
257
+ oid: null,
258
+ contentDigest: sha256(''),
259
+ byteSize: 0,
260
+ truncated: false,
261
+ });
262
+ }
263
+ else {
264
+ const beforeDigest = sha256(before);
265
+ const after = await options.readWorktreeFile(file.path);
266
+ if (after === null || sha256(after) !== beforeDigest) {
267
+ throw new ReviewSnapshotError(`Working file changed while snapshotting: ${file.path}`, 'worktree_changed');
268
+ }
269
+ file.sides.push({
270
+ source: 'unstaged',
271
+ oid: null,
272
+ contentDigest: beforeDigest,
273
+ byteSize: Buffer.byteLength(before, 'utf8'),
274
+ truncated: before.length > budgets.maxBodyBytes,
275
+ });
276
+ }
277
+ }
278
+ }
279
+ }
280
+ const manifest = {
281
+ kind: options.selector.kind,
282
+ baseOid,
283
+ headOid,
284
+ requestedPaths,
285
+ files: files.map(file => ({
286
+ fileKey: file.fileKey,
287
+ path: file.path,
288
+ oldPath: file.oldPath,
289
+ sides: file.sides.map(side => ({ source: side.source, contentDigest: side.contentDigest })),
290
+ })),
291
+ };
292
+ const inputSnapshotDigest = sha256(JSON.stringify(manifest));
293
+ const snapshotId = `${options.snapshotIdPrefix ?? 'rev'}-${inputSnapshotDigest.slice(0, 16)}`;
294
+ return Object.freeze({
295
+ schemaVersion: 1,
296
+ snapshotId,
297
+ kind: options.selector.kind,
298
+ baseOid,
299
+ headOid,
300
+ requestedPaths,
301
+ files: Object.freeze(files.map(file => Object.freeze({
302
+ ...file,
303
+ sides: Object.freeze(file.sides),
304
+ hunks: Object.freeze(file.hunks),
305
+ }))),
306
+ inputSnapshotDigest,
307
+ skipped: Object.freeze(skipped),
308
+ });
309
+ }
310
+ /** Reads one working-tree file for review purposes, refusing to leave the
311
+ * repository root (same containment discipline as the v0.3.25 conflict
312
+ * reader). Returns null when the file is missing or unreadable. */
313
+ function createContainedWorktreeReader(rootInput) {
314
+ const root = (0, fs_1.realpathSync)(rootInput);
315
+ return async (relativePath) => {
316
+ try {
317
+ const absolute = (0, path_1.resolve)(root, relativePath);
318
+ const fileReal = (0, fs_1.realpathSync)(absolute);
319
+ if (fileReal !== root && !fileReal.startsWith(root + path_1.sep))
320
+ return null;
321
+ if (!(0, fs_1.statSync)(fileReal).isFile())
322
+ return null;
323
+ return await (0, promises_1.readFile)(fileReal, 'utf8');
324
+ }
325
+ catch {
326
+ return null;
327
+ }
328
+ };
329
+ }
330
+ //# sourceMappingURL=snapshot.js.map
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@orion-agents/orion-code",
3
- "version": "0.3.26",
3
+ "version": "0.3.28",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@orion-agents/orion-code",
9
- "version": "0.3.26",
9
+ "version": "0.3.28",
10
10
  "license": "MIT",
11
11
  "dependencies": {
12
12
  "@clack/prompts": "^0.7.0",
@@ -4293,9 +4293,9 @@
4293
4293
  "license": "ISC"
4294
4294
  },
4295
4295
  "node_modules/follow-redirects": {
4296
- "version": "1.16.0",
4297
- "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz",
4298
- "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==",
4296
+ "version": "1.16.1",
4297
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.1.tgz",
4298
+ "integrity": "sha512-FNvFGzoMLWmE6Yj9spb/zjd7yiNCHiAW9/Tg9CXrQ8wuu32HtlJOwWO11OJafl5FfY3DxTdQ0vj42zU1kvv5jg==",
4299
4299
  "funding": [
4300
4300
  {
4301
4301
  "type": "individual",
@@ -4563,13 +4563,13 @@
4563
4563
  "license": "MIT"
4564
4564
  },
4565
4565
  "node_modules/handlebars": {
4566
- "version": "4.7.9",
4567
- "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.9.tgz",
4568
- "integrity": "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==",
4566
+ "version": "4.7.10",
4567
+ "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.10.tgz",
4568
+ "integrity": "sha512-P5VJMVM7qgBn6vjXMw8WG9uVI+ncf2pi72j4de4yz5ZULLj2RGqLYaKOYGsgyrViQ0tePOVlN1tDCCXXtFqXKg==",
4569
4569
  "dev": true,
4570
4570
  "license": "MIT",
4571
4571
  "dependencies": {
4572
- "minimist": "^1.2.5",
4572
+ "minimist": "^1.2.8",
4573
4573
  "neo-async": "^2.6.2",
4574
4574
  "source-map": "^0.6.1",
4575
4575
  "wordwrap": "^1.0.0"
@@ -7147,9 +7147,9 @@
7147
7147
  }
7148
7148
  },
7149
7149
  "node_modules/source-map-js": {
7150
- "version": "1.2.1",
7151
- "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
7152
- "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
7150
+ "version": "1.2.2",
7151
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz",
7152
+ "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==",
7153
7153
  "dev": true,
7154
7154
  "license": "BSD-3-Clause",
7155
7155
  "engines": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orion-agents/orion-code",
3
- "version": "0.3.26",
3
+ "version": "0.3.28",
4
4
  "description": "Orion Code - local-first goal-driven coding agent for terminal and Web.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",