@orion-agents/orion-code 0.3.11 → 0.3.12

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 (35) hide show
  1. package/CHANGELOG.md +47 -2
  2. package/README.md +8 -5
  3. package/README.zh-CN.md +5 -4
  4. package/dist/cli.js +14 -1
  5. package/dist/web/context-text.d.ts +17 -0
  6. package/dist/web/context-text.js +40 -0
  7. package/dist/web/file-read-service.d.ts +23 -0
  8. package/dist/web/file-read-service.js +134 -0
  9. package/dist/web/git-read-model-service.d.ts +14 -0
  10. package/dist/web/git-read-model-service.js +64 -0
  11. package/dist/web/host-daemon.d.ts +9 -0
  12. package/dist/web/host-daemon.js +45 -1
  13. package/dist/web/protocol.d.ts +7 -0
  14. package/dist/web/protocol.js +26 -0
  15. package/dist/web/review-service.d.ts +14 -0
  16. package/dist/web/review-service.js +34 -14
  17. package/dist/web/server.js +82 -0
  18. package/dist/web/workbench-controller.d.ts +45 -0
  19. package/dist/web/workbench-controller.js +84 -0
  20. package/dist/web-client/assets/{DiffViewer-BrE5tfZc.js → DiffViewer-Bs1C5uRM.js} +1 -1
  21. package/dist/web-client/assets/FilesPanel-dW61EHHn.js +2 -0
  22. package/dist/web-client/assets/GitPanel-ykbaVNlo.js +1 -0
  23. package/dist/web-client/assets/ResourceSplitLayout-B0Zs9aMC.js +1 -0
  24. package/dist/web-client/assets/ReviewPanel-1oqfaqoh.js +1 -0
  25. package/dist/web-client/assets/{TerminalPanel-CMLU7j08.js → TerminalPanel-Cz0ohyPm.js} +1 -1
  26. package/dist/web-client/assets/index-B11PjVU2.css +1 -0
  27. package/dist/web-client/assets/index-DiNz6Qhw.js +17 -0
  28. package/dist/web-client/index.html +2 -2
  29. package/npm-shrinkwrap.json +2 -2
  30. package/package.json +1 -1
  31. package/dist/web-client/assets/FilesPanel-ZwlVgxJi.js +0 -2
  32. package/dist/web-client/assets/GitPanel-Cnz52VRO.js +0 -1
  33. package/dist/web-client/assets/ReviewPanel-D_xxkVG_.js +0 -1
  34. package/dist/web-client/assets/index-BlFUf8dG.js +0 -17
  35. package/dist/web-client/assets/index-CUG6w669.css +0 -1
package/CHANGELOG.md CHANGED
@@ -22,9 +22,54 @@ which is **not** a pass.
22
22
 
23
23
  ## [Unreleased]
24
24
 
25
- ## [0.3.11] — CANDIDATE
25
+ ## [0.3.12] — CANDIDATE
26
26
 
27
- > **Status: candidate.** Not merged, tagged or published to npm.
27
+ > **Status: candidate.** Wide Right Workspace + repository-truth + guarded Git
28
+ > mutations (plan docs/plan/v0.3.12-plan.md) plus the v0.3.13 follow-ups that
29
+ > landed on this branch (plans v3/v4/v5). Not merged, tagged or published.
30
+
31
+ ### Added
32
+
33
+ - Wide Right Workspace logic layer: five-panel registry, per-workspace layout
34
+ preferences (schema v4 with per-panel navigator widths), pure dock/rail/drawer
35
+ geometry with 320px conversation minimum and 360/560/960/max snap points.
36
+ - Repository snapshot store shared by Git/Review/Files decorations
37
+ (coalescing, one-shot invalidation, revision-stale detection).
38
+ - Progressive Review verification paging (GET /review/verification).
39
+ - Guarded Git stage/unstage/commit (POST /git/\*) with host-resolved file ids,
40
+ repository-revision CAS and an explicit user-gesture header.
41
+ - Bounded workspace search (GET /files/search; name/content scopes).
42
+ - Context V2 file_range references (revision-bound, line-extracted, budgeted).
43
+ - **Resource split layout (v0.3.13-plan-v4):** Files/Git/Review share one
44
+ content | splitter | navigator shell (`ResourceSplitLayout`) with explicit
45
+ named grid areas (no more sparse grid auto-placement gaps). Git now shows its
46
+ diff on the left, matching Files/Review. Each panel has an inner draggable
47
+ separator measured against the split root — never the workbench shell — so
48
+ dragging never moves the outer Right Workspace; per-workspace + per-panel
49
+ navigator widths are persisted (300px default, 160–420px drag range,
50
+ double-click/keyboard reset), and <=620px containers stack navigator-over-
51
+ content and hide the handle.
52
+ - **Conversation history navigator (v0.3.13-plan-v5):** a left-inside rail on
53
+ the loaded transcript derived from the unified order timeline. A pure bucket
54
+ model (<=48–64 stable buckets, user/assistant/errors always visible) feeds
55
+ decorative ticks plus one focusable `role="slider"` (Arrow/Page/Home/End,
56
+ reduced-motion aware). An IntersectionObserver + rAF-throttled viewport
57
+ marker tracks the reading position; pointer scrub uses capture and cancels
58
+ pinned auto-follow; the top entry drives the existing paginated
59
+ load-earlier. Container-query degradation: full rail >760px, compact
60
+ 641–760px, hidden <=640px with no horizontal overflow.
61
+ - **Unified Orion brand mark (v0.3.13-plan-v5 §9):** one decorative
62
+ `OrionBrandMark` inline SVG (20×20 artboard, three bars, theme-token
63
+ gradients with forced-colors fallback) replaces the duplicated
64
+ `.brand-mark`/`.boot-mark` span CSS in the project navigator and boot
65
+ screen; the mark stays aria-hidden and non-interactive.
66
+ - E2E: rail-panel scenario ids on the allowlist; real-conversation smoke spec;
67
+ Files/Review/Git split-layout, history-rail and brand visual specs.
68
+
69
+ ## [0.3.11] — NPM-PUBLISHED
70
+
71
+ > **Status: npm-published.** `@orion-agents/orion-code@0.3.11` is available through the npm
72
+ > registry (latest tag). Published 2026-09-05; git tag/release may lag.
28
73
 
29
74
  ### Added
30
75
 
package/README.md CHANGED
@@ -2,10 +2,13 @@
2
2
 
3
3
  Local-first, goal-driven coding agent for the terminal and browser.
4
4
 
5
- > v0.3.11 candidate — the right work panel is a vertical-rail menu: a persistent
6
- > 48px icon rail shows exactly the panel you click (re-click collapses, Esc
7
- > closes), carries terminal status badges, and honors a persisted icon order.
8
- > Candidate source is not an npm publication, Git tag, or merge-ready release.
5
+ > v0.3.12 candidate — a Wide Right Workspace on top of the vertical rail: pure
6
+ > dock/rail/drawer geometry (detail dockable to 80vw while the conversation
7
+ > keeps a 320px minimum), per-workspace layout preferences, a shared repository
8
+ > snapshot store, progressive Review verification, guarded Git stage/unstage/
9
+ > commit, bounded workspace search and revision-bound file_range Context
10
+ > references. Candidate source is not an npm publication, Git tag, or
11
+ > merge-ready release.
9
12
 
10
13
  [中文说明](README.zh-CN.md) ·
11
14
  [v0.3.7 plan](docs/plan/v0.3.7-left-rail-improvement-plan.md) ·
@@ -170,7 +173,7 @@ current development environments. Node 20 is upstream EOL and is no longer a v0.
170
173
  After the immutable `0.3.5` npm receipt exists:
171
174
 
172
175
  ```bash
173
- npm install -g @orion-agents/orion-code@0.3.11
176
+ npm install -g @orion-agents/orion-code@0.3.12
174
177
  orion --version
175
178
  orion doctor
176
179
  ```
package/README.zh-CN.md CHANGED
@@ -2,9 +2,10 @@
2
2
 
3
3
  面向终端与浏览器、本地优先的目标驱动 Coding Agent。
4
4
 
5
- > v0.3.11 候选版本:右侧工作面板改为**竖状 rail 菜单**——常驻 48px 竖栏,
6
- > 点击图标即展示对应面板(再点收起、Esc 关闭),带终端状态徽标并支持持久化图标顺序。
7
- > Candidate 源码不代表已创建 npm 发布、Git tag 或达到可合并状态。
5
+ > v0.3.12 候选版本:在竖状 rail 之上的**大尺寸右侧工作区**——纯 dock/rail/drawer 几何
6
+ > (详情面可拉宽至 80vw,对话保留 320px 最小宽度)、按 workspace 的布局偏好、共享仓库快照
7
+ > store、Review 验证证据渐进分页、受控 Git stage/unstage/commit、受限工作区搜索与带版本
8
+ > 约束的 file_range Context 引用。Candidate 源码不代表已创建 npm 发布、Git tag 或达到可合并状态。
8
9
 
9
10
  [English](README.md) ·
10
11
  [v0.3.7 方案](docs/plan/v0.3.7-left-rail-improvement-plan.md) ·
@@ -101,7 +102,7 @@ Current。Node 20 已结束上游维护,不再属于 v0.3 Runtime 合同。
101
102
  当 npm 已存在不可变的 `0.3.4` 发布凭据后:
102
103
 
103
104
  ```bash
104
- npm install -g @orion-agents/orion-code@0.3.11
105
+ npm install -g @orion-agents/orion-code@0.3.12
105
106
  orion --version
106
107
  orion doctor
107
108
  ```
package/dist/cli.js CHANGED
@@ -80,8 +80,21 @@ async function manageBackgroundHost(command, args) {
80
80
  if (status.state === 'running') {
81
81
  console.log(`orion web is running: ${status.pidfile.url} (pid ${status.pidfile.pid})`);
82
82
  console.log(`workspace ${status.pidfile.workspace} · started ${new Date(status.pidfile.startedAt).toISOString()}`);
83
+ return;
84
+ }
85
+ // No pidfile-owned host, or a stale one. A live listener on the port means
86
+ // an externally managed host (e.g. a launchd LaunchAgent) is serving it —
87
+ // report that instead of a confusing 'not running'.
88
+ if (await (0, host_daemon_1.isHostPortTaken)(options.port)) {
89
+ console.log(`Port ${options.port} is being served by an externally managed Orion host ` +
90
+ `(e.g. a launchd LaunchAgent such as ai.orion-code.web). It is not owned ` +
91
+ `by \`orion web status/stop\`; inspect it with \`launchctl list | grep orion\`.`);
92
+ if (status.state === 'stale') {
93
+ console.log(`Note: the pidfile for the former pid ${status.pid} is stale and can be removed safely.`);
94
+ }
95
+ return;
83
96
  }
84
- else if (status.state === 'stale') {
97
+ if (status.state === 'stale') {
85
98
  console.log(`orion web is not running (stale pidfile for pid ${status.pid} on port ${status.port}).`);
86
99
  }
87
100
  else {
@@ -0,0 +1,17 @@
1
+ /**
2
+ * v0.3.12 — Context text helpers shared by the controller when resolving
3
+ * version-bound references (file_range etc.). Pure and unit-tested.
4
+ */
5
+ export interface ExtractedLines {
6
+ readonly text: string;
7
+ /** True when the requested range reached the end of the file before endLine. */
8
+ readonly clampedAtEnd: boolean;
9
+ }
10
+ /** Extract a 1-based inclusive line range. Never throws for out-of-range input. */
11
+ export declare function extractLineRange(content: string, startLine: number, endLine: number): ExtractedLines;
12
+ /** Soft per-reference byte budget for line content sent to the model context. */
13
+ export declare function enforceContextBudget(text: string, maxBytes: number): {
14
+ readonly text: string;
15
+ readonly truncated: boolean;
16
+ };
17
+ //# sourceMappingURL=context-text.d.ts.map
@@ -0,0 +1,40 @@
1
+ "use strict";
2
+ /**
3
+ * v0.3.12 — Context text helpers shared by the controller when resolving
4
+ * version-bound references (file_range etc.). Pure and unit-tested.
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.extractLineRange = extractLineRange;
8
+ exports.enforceContextBudget = enforceContextBudget;
9
+ /** Extract a 1-based inclusive line range. Never throws for out-of-range input. */
10
+ function extractLineRange(content, startLine, endLine) {
11
+ const start = Math.max(1, Math.trunc(startLine));
12
+ const end = Math.max(start, Math.trunc(endLine));
13
+ const lines = content.split('\n');
14
+ if (start > lines.length) {
15
+ return Object.freeze({ text: '', clampedAtEnd: true });
16
+ }
17
+ const last = Math.min(end, lines.length);
18
+ const slice = lines.slice(start - 1, last);
19
+ return Object.freeze({
20
+ text: slice.join('\n'),
21
+ clampedAtEnd: end > lines.length,
22
+ });
23
+ }
24
+ /** Soft per-reference byte budget for line content sent to the model context. */
25
+ function enforceContextBudget(text, maxBytes) {
26
+ if (Buffer.byteLength(text, 'utf8') <= maxBytes) {
27
+ return Object.freeze({ text, truncated: false });
28
+ }
29
+ let low = 0;
30
+ let high = text.length;
31
+ while (low < high) {
32
+ const mid = (low + high + 1) >>> 1;
33
+ if (Buffer.byteLength(text.slice(0, mid), 'utf8') <= maxBytes)
34
+ low = mid;
35
+ else
36
+ high = mid - 1;
37
+ }
38
+ return Object.freeze({ text: text.slice(0, low), truncated: true });
39
+ }
40
+ //# sourceMappingURL=context-text.js.map
@@ -45,12 +45,35 @@ export declare class FileReadServiceV1 {
45
45
  constructor(workspace: string);
46
46
  get rootId(): string;
47
47
  performanceCounters(): WebFileReadPerformanceCountersV1;
48
+ /** v0.3.12 S3 — resolve a remembered file id back to its workspace-relative path. */
49
+ pathForFileId(fileId: string): string;
48
50
  identifyRelativePath(relativePath: string): string;
49
51
  list(input?: {
50
52
  readonly parentId?: string;
51
53
  readonly cursor?: string;
52
54
  readonly pageSize?: number;
53
55
  }): WebFileTreePageV1;
56
+ /**
57
+ * v0.3.12 S4 — bounded workspace search. Name scope walks the tree (skipping
58
+ * VCS/dependency/ignored names, hidden directories, out-of-root symlinks and
59
+ * sensitive paths); content scope additionally matches inside the first
60
+ * 64 KiB of each text file and skips binary payloads. Results cap at
61
+ * `limit`; the scan stops early once the cap is reached or the file budget
62
+ * is exhausted.
63
+ */
64
+ search(input: {
65
+ readonly query: string;
66
+ readonly scope: 'name' | 'content';
67
+ readonly limit?: number;
68
+ }): {
69
+ readonly items: ReadonlyArray<{
70
+ readonly id: string;
71
+ readonly name: string;
72
+ readonly path: string;
73
+ }>;
74
+ readonly truncated: boolean;
75
+ };
76
+ private fileContainsQuery;
54
77
  readContent(input: {
55
78
  readonly fileId: string;
56
79
  readonly cursor?: string;
@@ -41,6 +41,14 @@ class FileReadServiceV1 {
41
41
  performanceCounters() {
42
42
  return Object.freeze({ ...this.performance });
43
43
  }
44
+ /** v0.3.12 S3 — resolve a remembered file id back to its workspace-relative path. */
45
+ pathForFileId(fileId) {
46
+ const relativePath = this.pathById.get(fileId);
47
+ if (relativePath === undefined) {
48
+ throw new errors_1.WebWorkbenchError(404, 'File node was not found.', 'file_not_found');
49
+ }
50
+ return relativePath;
51
+ }
44
52
  identifyRelativePath(relativePath) {
45
53
  const normalized = normalizeRelativePath(relativePath);
46
54
  this.resolveRelative(normalized);
@@ -107,6 +115,132 @@ class FileReadServiceV1 {
107
115
  : null,
108
116
  });
109
117
  }
118
+ /**
119
+ * v0.3.12 S4 — bounded workspace search. Name scope walks the tree (skipping
120
+ * VCS/dependency/ignored names, hidden directories, out-of-root symlinks and
121
+ * sensitive paths); content scope additionally matches inside the first
122
+ * 64 KiB of each text file and skips binary payloads. Results cap at
123
+ * `limit`; the scan stops early once the cap is reached or the file budget
124
+ * is exhausted.
125
+ */
126
+ search(input) {
127
+ const query = input.query.trim().slice(0, 200).toLowerCase();
128
+ if (!query) {
129
+ throw new errors_1.WebWorkbenchError(400, 'A non-empty search query is required.', 'file_search_query_required');
130
+ }
131
+ const limit = boundedPageSize(input.limit ?? 50);
132
+ const items = [];
133
+ const stack = [{ dir: this.root, rel: '' }];
134
+ let visitedDirectories = 0;
135
+ let visitedFiles = 0;
136
+ let truncated = false;
137
+ const fileBudget = 2500;
138
+ while (stack.length > 0 && visitedFiles < fileBudget) {
139
+ const { dir, rel } = stack.pop();
140
+ visitedDirectories += 1;
141
+ if (visitedDirectories > 4000) {
142
+ truncated = true;
143
+ break;
144
+ }
145
+ let directory;
146
+ try {
147
+ directory = (0, fs_1.opendirSync)(dir);
148
+ }
149
+ catch {
150
+ continue;
151
+ }
152
+ const entries = [];
153
+ try {
154
+ for (;;) {
155
+ const entry = directory.readSync();
156
+ if (!entry)
157
+ break;
158
+ if (IGNORED_NAMES.has(entry.name) ||
159
+ (entry.name.startsWith('.') && entry.isDirectory())) {
160
+ continue;
161
+ }
162
+ if (entry.isDirectory())
163
+ entries.push({ name: entry.name, kind: 'directory' });
164
+ else if (entry.isFile())
165
+ entries.push({ name: entry.name, kind: 'file' });
166
+ else if (entry.isSymbolicLink())
167
+ entries.push({ name: entry.name, kind: 'link' });
168
+ }
169
+ }
170
+ finally {
171
+ directory.closeSync();
172
+ }
173
+ entries.sort((left, right) => left.name.localeCompare(right.name));
174
+ for (const entry of entries) {
175
+ if (items.length >= limit) {
176
+ truncated = true;
177
+ break;
178
+ }
179
+ const childRel = rel ? `${rel}/${entry.name}` : entry.name;
180
+ const absolute = (0, path_1.resolve)(dir, entry.name);
181
+ if (entry.kind === 'directory') {
182
+ stack.push({ dir: absolute, rel: childRel });
183
+ continue;
184
+ }
185
+ if (entry.kind === 'link') {
186
+ try {
187
+ const canonical = (0, fs_1.realpathSync)(absolute);
188
+ if (!isWithinRoot(canonical, this.root))
189
+ continue;
190
+ if ((0, fs_1.statSync)(canonical).isDirectory())
191
+ continue;
192
+ }
193
+ catch {
194
+ continue;
195
+ }
196
+ }
197
+ visitedFiles += 1;
198
+ if ((0, redaction_1.isSensitiveFilePath)(childRel))
199
+ continue;
200
+ const nameMatches = entry.name.toLowerCase().includes(query);
201
+ if (input.scope === 'name') {
202
+ if (nameMatches) {
203
+ items.push({ id: this.remember(childRel), name: entry.name, path: childRel });
204
+ }
205
+ continue;
206
+ }
207
+ let contentMatches = false;
208
+ if (!nameMatches) {
209
+ contentMatches = this.fileContainsQuery(absolute, query);
210
+ }
211
+ if (nameMatches || contentMatches) {
212
+ items.push({ id: this.remember(childRel), name: entry.name, path: childRel });
213
+ }
214
+ }
215
+ }
216
+ return Object.freeze({
217
+ items: Object.freeze(items.map(item => Object.freeze(item))),
218
+ truncated: truncated || visitedFiles >= fileBudget,
219
+ });
220
+ }
221
+ fileContainsQuery(absolutePath, query) {
222
+ try {
223
+ const descriptor = (0, fs_1.openSync)(absolutePath, fs_1.constants.O_RDONLY);
224
+ try {
225
+ const buffer = Buffer.alloc(64 * 1024);
226
+ const bytesRead = (0, fs_1.readSync)(descriptor, buffer, 0, buffer.length, 0);
227
+ if (bytesRead === 0)
228
+ return false;
229
+ if (buffer.subarray(0, bytesRead).includes(0))
230
+ return false;
231
+ return new util_1.TextDecoder('utf-8')
232
+ .decode(buffer.subarray(0, bytesRead))
233
+ .toLowerCase()
234
+ .includes(query);
235
+ }
236
+ finally {
237
+ (0, fs_1.closeSync)(descriptor);
238
+ }
239
+ }
240
+ catch {
241
+ return false;
242
+ }
243
+ }
110
244
  readContent(input) {
111
245
  this.performance.readOperations += 1;
112
246
  const limitBytes = boundedContentBytes(input.limitBytes ?? DEFAULT_CONTENT_BYTES);
@@ -77,6 +77,20 @@ export declare class GitReadModelServiceV1 {
77
77
  private capture;
78
78
  private projectFile;
79
79
  private rememberPath;
80
+ /** v0.3.12 S3 — guarded stage: paths are host-resolved file ids only. */
81
+ stagePaths(paths: readonly string[]): Promise<{
82
+ readonly repositoryRevision: string;
83
+ }>;
84
+ unstagePaths(paths: readonly string[]): Promise<{
85
+ readonly repositoryRevision: string;
86
+ }>;
87
+ commit(message: string): Promise<{
88
+ readonly repositoryRevision: string;
89
+ readonly commitSha: string;
90
+ }>;
91
+ private mutate;
92
+ private requireRepositoryRoot;
93
+ private revisionAfterMutation;
80
94
  private runGit;
81
95
  private tryGit;
82
96
  private encodeCursor;
@@ -262,6 +262,56 @@ class GitReadModelServiceV1 {
262
262
  this.pathById.set(id, path);
263
263
  return id;
264
264
  }
265
+ /** v0.3.12 S3 — guarded stage: paths are host-resolved file ids only. */
266
+ async stagePaths(paths) {
267
+ assertSafeGitPaths(paths);
268
+ if (paths.length === 0)
269
+ throw new Error('stage requires at least one path.');
270
+ await this.mutate(['add', '--', ...paths]);
271
+ return { repositoryRevision: await this.revisionAfterMutation() };
272
+ }
273
+ async unstagePaths(paths) {
274
+ assertSafeGitPaths(paths);
275
+ if (paths.length === 0)
276
+ throw new Error('unstage requires at least one path.');
277
+ await this.mutate(['reset', '-q', '--', ...paths]);
278
+ return { repositoryRevision: await this.revisionAfterMutation() };
279
+ }
280
+ async commit(message) {
281
+ const trimmed = message.trim();
282
+ if (!trimmed || trimmed.length > 2000 || /[\x00-\x1f]/.test(trimmed)) {
283
+ throw new Error('A valid commit message is required.');
284
+ }
285
+ const root = await this.requireRepositoryRoot();
286
+ const porcelain = (await this.runGit(['status', '--porcelain=v1'], root)).trim();
287
+ const stagedLines = porcelain
288
+ .split('\n')
289
+ .filter(line => line.length > 1 && line[0] !== ' ' && line[0] !== '?' && line[0] !== '!');
290
+ if (stagedLines.length === 0) {
291
+ throw new Error('Nothing is staged to commit.');
292
+ }
293
+ await this.mutate(['commit', '-m', trimmed]);
294
+ const sha = (await this.runGit(['rev-parse', 'HEAD'], root)).trim();
295
+ return { repositoryRevision: await this.revisionAfterMutation(), commitSha: sha };
296
+ }
297
+ async mutate(args) {
298
+ const root = await this.requireRepositoryRoot();
299
+ const output = await this.runGit(args, root);
300
+ if (output) {
301
+ // Warnings on stderr are not captured by runGit; treat non-empty as unexpected.
302
+ }
303
+ }
304
+ async requireRepositoryRoot() {
305
+ const snapshot = await this.capture();
306
+ if (!snapshot.isRepository || !snapshot.root) {
307
+ throw new Error('Git repository is unavailable.');
308
+ }
309
+ return snapshot.root;
310
+ }
311
+ async revisionAfterMutation() {
312
+ const root = await this.requireRepositoryRoot();
313
+ return this.runGit(['rev-parse', 'HEAD'], root).then(value => value.trim());
314
+ }
265
315
  runGit(args, cwd) {
266
316
  return new Promise((resolvePromise, reject) => {
267
317
  this.performance.processCount += 1;
@@ -590,6 +640,20 @@ function streamGitLines(input) {
590
640
  });
591
641
  });
592
642
  }
643
+ function assertSafeGitPaths(paths) {
644
+ for (const path of paths) {
645
+ if (typeof path !== 'string' ||
646
+ path.length === 0 ||
647
+ path.length > 4096 ||
648
+ path.includes('\0') ||
649
+ path.startsWith('-') ||
650
+ path.includes('/../') ||
651
+ path === '..' ||
652
+ path.startsWith('../')) {
653
+ throw new Error(`Unsafe Git path rejected: ${JSON.stringify(path)}`);
654
+ }
655
+ }
656
+ }
593
657
  function gitEnvironment() {
594
658
  return {
595
659
  PATH: process.env.PATH,
@@ -48,6 +48,15 @@ export declare function hostDaemonStatus(port: number): HostDaemonStatus;
48
48
  * stdio to `~/.orion-code/logs/web-<port>.log`, and wait for the pidfile so the
49
49
  * foreground command can print the URL and exit cleanly.
50
50
  */
51
+ /**
52
+ * True when something already listens on 127.0.0.1:port — typically another
53
+ * Orion host process (e.g. one managed by launchd, which does not write a
54
+ * pidfile). Detecting this up front turns a raw EADDRINUSE crash into a
55
+ * friendly message.
56
+ */
57
+ export declare function isHostPortTaken(port: number): Promise<boolean>;
58
+ /** Best-effort tail of the host log used when a background child exits early. */
59
+ export declare function readHostLogTail(port: number, lines?: number): string;
51
60
  export declare function spawnBackgroundHost(options: {
52
61
  readonly cwd: string;
53
62
  readonly port: number;
@@ -9,6 +9,8 @@ exports.writeHostPidfile = writeHostPidfile;
9
9
  exports.clearHostPidfile = clearHostPidfile;
10
10
  exports.isProcessAlive = isProcessAlive;
11
11
  exports.hostDaemonStatus = hostDaemonStatus;
12
+ exports.isHostPortTaken = isHostPortTaken;
13
+ exports.readHostLogTail = readHostLogTail;
12
14
  exports.spawnBackgroundHost = spawnBackgroundHost;
13
15
  exports.stopBackgroundHost = stopBackgroundHost;
14
16
  /**
@@ -26,6 +28,7 @@ exports.stopBackgroundHost = stopBackgroundHost;
26
28
  * "startedAt": 1725000000000, "workspace": "/abs/path" }
27
29
  */
28
30
  const child_process_1 = require("child_process");
31
+ const net_1 = require("net");
29
32
  const fs_1 = require("fs");
30
33
  const path_1 = require("path");
31
34
  const paths_1 = require("../product/paths");
@@ -119,7 +122,48 @@ function childArgs(webArgs) {
119
122
  * stdio to `~/.orion-code/logs/web-<port>.log`, and wait for the pidfile so the
120
123
  * foreground command can print the URL and exit cleanly.
121
124
  */
122
- function spawnBackgroundHost(options) {
125
+ /**
126
+ * True when something already listens on 127.0.0.1:port — typically another
127
+ * Orion host process (e.g. one managed by launchd, which does not write a
128
+ * pidfile). Detecting this up front turns a raw EADDRINUSE crash into a
129
+ * friendly message.
130
+ */
131
+ function isHostPortTaken(port) {
132
+ return new Promise(resolve => {
133
+ const socket = (0, net_1.connect)({ host: '127.0.0.1', port });
134
+ let settled = false;
135
+ const done = (taken) => {
136
+ if (settled)
137
+ return;
138
+ settled = true;
139
+ socket.destroy();
140
+ resolve(taken);
141
+ };
142
+ socket.once('connect', () => done(true));
143
+ socket.once('error', () => done(false));
144
+ socket.setTimeout(800, () => done(false));
145
+ });
146
+ }
147
+ /** Best-effort tail of the host log used when a background child exits early. */
148
+ function readHostLogTail(port, lines = 6) {
149
+ try {
150
+ const raw = (0, fs_1.readFileSync)((0, path_1.join)(hostLogsDirectory(), `web-${port}.log`), 'utf8');
151
+ return raw.split('\n').filter(Boolean).slice(-lines).join('\n');
152
+ }
153
+ catch {
154
+ return '';
155
+ }
156
+ }
157
+ async function assertHostPortAvailable(port) {
158
+ if (await isHostPortTaken(port)) {
159
+ throw new Error(`Port ${port} is already served by another Orion host process. This is ` +
160
+ `often a launchd LaunchAgent (ai.orion-code.web). Check with ` +
161
+ '`launchctl list | grep orion`; if the running host belongs to this ' +
162
+ `CLI, use \`orion web status --port ${port}\` to confirm before stopping it.`);
163
+ }
164
+ }
165
+ async function spawnBackgroundHost(options) {
166
+ await assertHostPortAvailable(options.port);
123
167
  const logsDirectory = hostLogsDirectory();
124
168
  (0, fs_1.mkdirSync)(logsDirectory, { recursive: true });
125
169
  const logPath = (0, path_1.join)(logsDirectory, `web-${options.port}.log`);
@@ -171,6 +171,13 @@ export type WebContextReferenceV1 = {
171
171
  readonly id: string;
172
172
  readonly label: string;
173
173
  readonly revision: string;
174
+ } | {
175
+ readonly kind: 'file_range';
176
+ readonly id: string;
177
+ readonly label: string;
178
+ readonly revision: string;
179
+ readonly startLine: number;
180
+ readonly endLine: number;
174
181
  } | {
175
182
  readonly kind: 'folder';
176
183
  readonly id: string;
@@ -523,6 +523,7 @@ function parseContextReferences(value) {
523
523
  const row = requireRecord(entry, `contextReferences[${index}]`);
524
524
  const kind = requireEnum(row.kind, `contextReferences[${index}].kind`, [
525
525
  'file',
526
+ 'file_range',
526
527
  'folder',
527
528
  'review',
528
529
  'session',
@@ -534,6 +535,22 @@ function parseContextReferences(value) {
534
535
  }
535
536
  seen.add(`${kind}:${id}`);
536
537
  const label = requireBoundedString(row.label, `contextReferences[${index}].label`, 200);
538
+ if (kind === 'file_range') {
539
+ assertOnlyKeys(row, ['kind', 'id', 'label', 'revision', 'startLine', 'endLine'], 'Context reference');
540
+ const startLine = requireLineNumber(row.startLine, `contextReferences[${index}].startLine`);
541
+ const endLine = requireLineNumber(row.endLine, `contextReferences[${index}].endLine`);
542
+ if (endLine < startLine) {
543
+ throw new WebProtocolError('file_range endLine must not precede startLine');
544
+ }
545
+ return Object.freeze({
546
+ kind,
547
+ id,
548
+ label,
549
+ revision: requireBoundedString(row.revision, `contextReferences[${index}].revision`, 256),
550
+ startLine,
551
+ endLine,
552
+ });
553
+ }
537
554
  if (kind === 'file' || kind === 'folder') {
538
555
  assertOnlyKeys(row, ['kind', 'id', 'label', 'revision'], 'Context reference');
539
556
  return Object.freeze({
@@ -573,6 +590,15 @@ function assertOnlyKeys(row, keys, subject = 'command') {
573
590
  if (unknown)
574
591
  throw new WebProtocolError(`Unknown ${subject} field: ${unknown}`);
575
592
  }
593
+ function requireLineNumber(value, name) {
594
+ if (typeof value !== 'number' ||
595
+ !Number.isSafeInteger(value) ||
596
+ value < 1 ||
597
+ value > 100000000) {
598
+ throw new WebProtocolError(`${name} must be an integer line number from 1 through 100000000`);
599
+ }
600
+ return value;
601
+ }
576
602
  function requireBoundedString(value, name, maxLength) {
577
603
  if (typeof value !== 'string' || !value.trim()) {
578
604
  throw new WebProtocolError(`${name} must be a non-empty string`);
@@ -34,6 +34,20 @@ export declare class ReviewServiceV1 {
34
34
  private readonly listReceiptRefs;
35
35
  constructor(git: GitReadModelServiceV1, listReceiptRefs: () => Promise<readonly VerifiedDurableToolReceiptRefV1[]> | readonly VerifiedDurableToolReceiptRefV1[]);
36
36
  snapshot(): Promise<WebReviewSnapshotV1>;
37
+ /**
38
+ * v0.3.12 S2 — progressive verification evidence. The Review summary loads
39
+ * first; receipts page in behind it, optionally filtered to one session.
40
+ */
41
+ verificationPage(input: {
42
+ readonly sessionId?: string;
43
+ readonly cursor?: number;
44
+ readonly pageSize?: number;
45
+ }): Promise<{
46
+ readonly items: readonly WebReviewVerificationV1[];
47
+ readonly nextCursor: number | null;
48
+ readonly totalForSession: number;
49
+ }>;
50
+ private projectVerification;
37
51
  diff(input: Parameters<GitReadModelServiceV1['diff']>[0]): Promise<WebGitDiffPageV1>;
38
52
  }
39
53
  //# sourceMappingURL=review-service.d.ts.map