@orion-agents/orion-code 0.3.27 → 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.
package/CHANGELOG.md CHANGED
@@ -22,9 +22,70 @@ which is **not** a pass.
22
22
 
23
23
  ## [Unreleased]
24
24
 
25
- ## [0.3.27] — UNRELEASED
25
+ ## [0.3.28] — UNRELEASED
26
+
27
+ > **Status: candidate.** Open. The typed `/review` runtime & CLI slice
28
+ > (plan: `docs/plan/v0.3.28-plan.md`, PR2 core of the v0.3.27 review loop):
29
+ > `/review` arguments now parse into the frozen `ReviewSelectorV1` (worktree /
30
+ > `--base` / repeatable `--path`, back-compat single-path filter) and unknown
31
+ > input returns the usage text instead of silently continuing as an ordinary
32
+ > chat turn. The review runs against the admission-time frozen Git snapshot
33
+ > through a bounded single-pass model completion; the model's findings are
34
+ > validated against the contract and anchored to the snapshot manifest, with
35
+ > invalid or unanchored findings counted and surfaced rather than silently
36
+ > dropped. Every run produces a digest-verifiable `CodeReviewResultV1` receipt
37
+ > rendered by the shared formatter with honest state banners: an empty scope
38
+ > reports "no changes under review" without calling the model, and partial,
39
+ > failed, cancelled or inconclusive runs never render as a green pass.
40
+ > Web wiring (PR3) and the qualification benchmark (PR4) remain subsequent
41
+ > slices. Not merged, tagged or published.
26
42
 
27
- > **Status: candidate.** Open. First slice of the professional code review loop
43
+ ### Added
44
+
45
+ - `src/services/code-review/selector.ts` — the typed `/review` argument
46
+ parser: empty / `all` → worktree scope, `--base <ref>` → base scope,
47
+ repeatable `--path <path>` filters, single bare relative path kept as a
48
+ back-compat worktree filter; unknown options, missing values, repeated
49
+ `--base`, absolute paths and traversal are typed usage errors. Fourteen
50
+ tests pin every accepted shape and rejection path.
51
+ - `src/services/code-review/runner.ts` — the bounded single-pass review
52
+ runner: `resolveReviewBudgets` (tighten-only) → `captureReviewSnapshot`
53
+ (worktree contained reader) → frozen unified diff captured with the same
54
+ admission-time arguments → rules digest binding the read-only constraints
55
+ digest plus ancestor `AGENTS.md` contents → one model completion through an
56
+ injected adapter → contract validation + snapshot-membership anchoring +
57
+ `maxFindings` budget → `resolveReviewConclusion` → receipt digest
58
+ recomputation. Empty scope completes without a model call and is labeled
59
+ `empty_scope_no_changes`; cancellation, model errors and unparsable output
60
+ are typed failures, never green passes.
61
+ - `src/services/code-review/format.ts` — the shared TUI formatter: scope and
62
+ resolved target header, findings grouped P0→P3 with side:line locations,
63
+ invalid-finding counts, coverage / skips / budget exhaustion footer and a
64
+ receipt digest line; partial / failed / cancelled banners never render as a
65
+ clean pass.
66
+ - `/review` command wiring (`src/commands/workflow-commands.ts`,
67
+ `workflow-command-handlers.ts`): the command is now a read-only builtin
68
+ that runs the typed workflow; unrecognized arguments return usage, a
69
+ missing model returns an explicit error, and no path falls back to ordinary
70
+ chat.
71
+
72
+ ### Changed
73
+
74
+ - `src/services/code-review/snapshot.ts` exports `reviewUnifiedDiffArgs` so
75
+ the runner reuses the exact frozen diff arguments instead of re-deriving
76
+ OIDs after admission; no frozen semantics changed.
77
+
78
+ ### Not run
79
+
80
+ - Real-PTY CLI verification, Web E2E (no Web changes in this slice), and the
81
+ paid human-annotated review-quality benchmark remain `not_run`, matching
82
+ the v0.3.27 plan's PR3/PR4 sequencing.
83
+
84
+ ## [0.3.27] — 2026-10-22
85
+
86
+ > **Status: published, 2026-10-22.** Tagged `v0.3.27` (`0be5b18`), merged via
87
+ > #283, and visible on npm as registry `latest`. First slice of the
88
+ > professional code review loop
28
89
  > (plan: `docs/plan/v0.3.27-plan.md`): the shared review contract (selectors,
29
90
  > states, conclusions, findings, tighten-only budgets, receipt digests,
30
91
  > stale-applicability) is frozen in `src/services/code-review` and bound by
@@ -34,7 +95,7 @@ which is **not** a pass.
34
95
  > workflow constraints deny every non-read tool by default with a dispatch-level
35
96
  > guard whose digest binds receipts. Runtime / CLI wiring (PR2), the Web loop
36
97
  > (PR3) and the qualification benchmark (PR4) are subsequent slices of the same
37
- > plan and are not part of this release. Not merged, tagged or published.
98
+ > plan and are not part of this release; PR2's core CLI slice ships in 0.3.28.
38
99
 
39
100
  ### Added
40
101
 
package/README.md CHANGED
@@ -2,14 +2,15 @@
2
2
 
3
3
  Local-first, goal-driven coding agent for the terminal and browser.
4
4
 
5
- > v0.3.27 candidate — open. First slice of the professional code review loop: the
6
- > shared review contract (selectors, states, conclusions, findings, tighten-only
7
- > budgets, receipt digests, stale-applicability) is frozen and contract-tested,
8
- > the shared Git snapshot reader captures worktree and frozen-base scopes with
9
- > typed errors, and the read-only workflow constraints deny every non-read tool
10
- > by default. Runtime/CLI wiring, the Web loop and the qualification benchmark
11
- > are subsequent slices of the same plan. The previous release 0.3.26 is on npm
12
- > (registry `latest`), tagged `v0.3.26` (`b810996`, #278) and released on GitHub.
5
+ > v0.3.28 candidate — open. The typed `/review` workflow: slash arguments are
6
+ > parsed into the frozen review selector (worktree / `--base` / `--path`, with
7
+ > usage errors instead of silent chat fallback), the review runs against the
8
+ > admission-time frozen Git snapshot through a bounded single-pass model call,
9
+ > model findings are validated against the contract and the snapshot manifest,
10
+ > and the run renders a digest-verifiable receipt with honest state banners
11
+ > (empty scope ≠ review pass; partial / failed / cancelled never green). The
12
+ > previous release 0.3.27 is on npm (registry `latest`), tagged `v0.3.27`
13
+ > (`0be5b18`, #283) and released on GitHub.
13
14
 
14
15
  [中文说明](README.zh-CN.md) ·
15
16
  [v0.3.24 plan](docs/plan/v0.3.24-plan.md) ·
@@ -172,10 +173,10 @@ user extension boundaries.
172
173
  Node.js 22.12+, 24, and 26 are supported. Use Node 24 LTS for production or Node 26 Current for
173
174
  current development environments. Node 20 is upstream EOL and is no longer a v0.3 runtime.
174
175
 
175
- To install the pinned v0.3.27 package when it is available on npm:
176
+ To install the pinned v0.3.28 package when it is available on npm:
176
177
 
177
178
  ```bash
178
- npm install -g @orion-agents/orion-code@0.3.27
179
+ npm install -g @orion-agents/orion-code@0.3.28
179
180
  orion --version
180
181
  orion doctor
181
182
  ```
package/README.zh-CN.md CHANGED
@@ -2,11 +2,12 @@
2
2
 
3
3
  面向终端与浏览器、本地优先的目标驱动 Coding Agent。
4
4
 
5
- > v0.3.27 候选版本:进行中。专业代码审查闭环首切片:共享审查契约(selector、状态、结论、
6
- > finding、只收紧预算、receipt digest、stale 标注)已冻结并通过契约测试;共享 Git snapshot
7
- > reader 捕获 worktree 与冻结 base 两种范围并给出类型化错误;只读 workflow 约束默认拒绝一切
8
- > 非读取工具。Runtime/CLI 接线、Web 闭环与资格验证为同一计划的后续切片。上一版 0.3.26 已发布
9
- > npm(registry `latest`)、打 tag `v0.3.26`(`b810996`,#278)并发布 GitHub Release。
5
+ > v0.3.28 候选版本:进行中。类型化 `/review` 审查工作流:slash 参数解析为冻结的审查
6
+ > selector(worktree / `--base` / `--path`,未知参数返回用法而非静默转入普通聊天);审查
7
+ > 基于 admission 时冻结的 Git snapshot,经有界单轮模型补全执行;模型 finding 按契约与
8
+ > snapshot manifest 校验;输出 digest 可验证的 receipt 与如实状态横幅(空范围 ≠ 审查通过;
9
+ > partial / failed / cancelled 永远不会显示为绿色)。上一版 0.3.27 已发布 npm
10
+ > (registry `latest`)、打 tag `v0.3.27`(`0be5b18`,#283)并发布 GitHub Release。
10
11
 
11
12
  [English](README.md) ·
12
13
  [v0.3.24 方案](docs/plan/v0.3.24-plan.md) ·
@@ -101,10 +102,10 @@ MCP 仍是用户可见的扩展边界。
101
102
  支持 Node.js 22.12+、24 和 26。生产环境建议使用 Node 24 LTS,当前开发环境也支持 Node 26
102
103
  Current。Node 20 已结束上游维护,不再属于 v0.3 Runtime 合同。
103
104
 
104
- v0.3.27 在 npm 可用后,可安装固定版本:
105
+ v0.3.28 在 npm 可用后,可安装固定版本:
105
106
 
106
107
  ```bash
107
- npm install -g @orion-agents/orion-code@0.3.27
108
+ npm install -g @orion-agents/orion-code@0.3.28
108
109
  orion --version
109
110
  orion doctor
110
111
  ```
@@ -4,5 +4,13 @@ declare function handlePlan(ctx: CommandContext, args: string): Promise<CommandR
4
4
  declare function handleDiff(ctx: CommandContext, args: string): CommandResult;
5
5
  declare function handleCommitPlan(ctx: CommandContext, args: string): CommandResult;
6
6
  declare function continueAsSlashChat(name: string, args: string): CommandResult;
7
- export { handlePlan, handleDiff, handleCommitPlan, continueAsSlashChat };
7
+ /**
8
+ * v0.3.28 — the typed `/review` workflow (docs/plan/v0.3.28-plan.md S4).
9
+ * Arguments are parsed into a frozen selector; unrecognized input returns the
10
+ * usage text instead of silently continuing as an ordinary chat turn. The
11
+ * run goes through the bounded single-pass review runner against the frozen
12
+ * Git snapshot, and the receipt is rendered by the shared formatter.
13
+ */
14
+ declare function handleReview(ctx: CommandContext, args: string): Promise<CommandResult>;
15
+ export { handlePlan, handleDiff, handleCommitPlan, continueAsSlashChat, handleReview, };
8
16
  //# sourceMappingURL=workflow-command-handlers.d.ts.map
@@ -5,9 +5,13 @@ exports.handlePlan = handlePlan;
5
5
  exports.handleDiff = handleDiff;
6
6
  exports.handleCommitPlan = handleCommitPlan;
7
7
  exports.continueAsSlashChat = continueAsSlashChat;
8
+ exports.handleReview = handleReview;
8
9
  const workspace_diff_1 = require("../services/workspace-diff");
9
10
  const commit_plan_1 = require("../services/commit-plan");
10
11
  const agent_mode_1 = require("../framework/agent-mode");
12
+ const selector_1 = require("../services/code-review/selector");
13
+ const runner_1 = require("../services/code-review/runner");
14
+ const format_1 = require("../services/code-review/format");
11
15
  async function handlePlan(ctx, args) {
12
16
  const task = args.trim();
13
17
  const [subcommand = '', ...tail] = task.split(/\s+/);
@@ -92,4 +96,66 @@ function continueAsSlashChat(name, args) {
92
96
  chatInput: `/${name}${trimmed ? ` ${trimmed}` : ''}`,
93
97
  };
94
98
  }
99
+ /**
100
+ * v0.3.28 — the typed `/review` workflow (docs/plan/v0.3.28-plan.md S4).
101
+ * Arguments are parsed into a frozen selector; unrecognized input returns the
102
+ * usage text instead of silently continuing as an ordinary chat turn. The
103
+ * run goes through the bounded single-pass review runner against the frozen
104
+ * Git snapshot, and the receipt is rendered by the shared formatter.
105
+ */
106
+ async function handleReview(ctx, args) {
107
+ const parsed = (0, selector_1.parseReviewSelector)(args);
108
+ if (!parsed.ok)
109
+ return { success: false, error: parsed.usage };
110
+ if (!ctx.llm) {
111
+ return {
112
+ success: false,
113
+ error: 'Code review needs a configured model. Run /model first, then retry /review.',
114
+ };
115
+ }
116
+ const llm = ctx.llm;
117
+ const outcome = await (0, runner_1.runCodeReview)({
118
+ cwd: ctx.cwd,
119
+ selector: parsed.selector,
120
+ adapter: {
121
+ async complete(messages) {
122
+ const response = await llm.chat(messages.map(message => ({ role: message.role, content: message.content })));
123
+ return {
124
+ content: response.content,
125
+ model: response.model,
126
+ usage: {
127
+ inputTokens: response.usage?.promptTokens ?? 0,
128
+ outputTokens: response.usage?.completionTokens ?? 0,
129
+ },
130
+ };
131
+ },
132
+ },
133
+ identity: {
134
+ workspaceIdentity: ctx.cwd,
135
+ sessionId: ctx.sessionId ?? 'pending-session',
136
+ threadId: ctx.sessionId ?? 'pending-session',
137
+ turnId: String(ctx.turnId ?? 'ad-hoc'),
138
+ },
139
+ ...(ctx.abortSignal ? { abortSignal: ctx.abortSignal } : {}),
140
+ });
141
+ if (!outcome.ok) {
142
+ const failure = outcome.failure;
143
+ if (failure.kind === 'cancelled')
144
+ return { success: false, error: 'Review cancelled.' };
145
+ if (failure.kind === 'budget') {
146
+ return { success: false, error: `Review budget rejected: ${failure.field}` };
147
+ }
148
+ if (failure.kind === 'snapshot') {
149
+ return {
150
+ success: false,
151
+ error: `Review scope error (${failure.code}): ${failure.message}`,
152
+ };
153
+ }
154
+ if (failure.kind === 'model_error') {
155
+ return { success: false, error: `Review model call failed: ${failure.message}` };
156
+ }
157
+ return { success: false, error: `Review output could not be parsed: ${failure.message}` };
158
+ }
159
+ return { success: true, output: (0, format_1.formatReviewReport)(outcome.result) };
160
+ }
95
161
  //# sourceMappingURL=workflow-command-handlers.js.map
@@ -54,14 +54,14 @@ exports.WORKFLOW_COMMANDS = [
54
54
  },
55
55
  {
56
56
  name: 'review',
57
- description: 'Review the current change or requested files',
58
- argumentHint: '[scope]',
57
+ description: 'Run a bounded read-only code review of the current change or a base comparison',
58
+ argumentHint: '[all | --base <ref>] [--path <path>]',
59
59
  category: 'workflow',
60
60
  priority: 10,
61
- type: 'chat',
62
- execution: 'agent-workflow',
61
+ type: 'builtin',
62
+ execution: 'builtin',
63
63
  risk: 'read-only',
64
- execute: (_ctx, args) => (0, workflow_command_handlers_1.continueAsSlashChat)('review', args),
64
+ execute: (ctx, args) => (0, workflow_command_handlers_1.handleReview)(ctx, args),
65
65
  },
66
66
  {
67
67
  name: 'research',
@@ -0,0 +1,12 @@
1
+ /**
2
+ * v0.3.28 — TUI rendering of a code-review receipt (docs/plan/v0.3.28-plan.md
3
+ * S3). The formatter is deliberately honest about state: a partial, failed,
4
+ * cancelled or inconclusive run never renders as a green "no findings" pass,
5
+ * and an empty scope is reported as "no changes under review", distinct from a
6
+ * completed review that found nothing (v0.3.27 plan §4.1).
7
+ */
8
+ import type { CodeReviewResultV1 } from './contract';
9
+ /** Renders the full report shown after `/review` finishes. `fileLabels` maps
10
+ * snapshot fileKeys to human paths when the caller still holds the manifest. */
11
+ export declare function formatReviewReport(result: CodeReviewResultV1, fileLabels?: ReadonlyMap<string, string>): string;
12
+ //# sourceMappingURL=format.d.ts.map
@@ -0,0 +1,79 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.formatReviewReport = formatReviewReport;
4
+ const PRIORITY_ORDER = ['P0', 'P1', 'P2', 'P3'];
5
+ const STATE_BANNERS = {
6
+ queued: 'Review queued.',
7
+ running: 'Review running.',
8
+ completed: 'Review completed.',
9
+ partial: 'Review PARTIAL — coverage was incomplete; findings below are valid but not exhaustive.',
10
+ failed: 'Review FAILED — no conclusions can be drawn from this run.',
11
+ cancelled: 'Review CANCELLED — no conclusions can be drawn from this run.',
12
+ };
13
+ function formatScope(result) {
14
+ const selector = result.requestedSelector;
15
+ if (selector.kind === 'base') {
16
+ const paths = selector.paths?.length ? ` paths: ${selector.paths.join(', ')}` : '';
17
+ return `base ${selector.ref} (${result.resolvedTarget.baseOid?.slice(0, 12) ?? '?'}..${result.resolvedTarget.headOid?.slice(0, 12) ?? '?'})${paths}`;
18
+ }
19
+ const paths = selector.paths?.length ? ` paths: ${selector.paths.join(', ')}` : '';
20
+ return `worktree (snapshot ${result.resolvedTarget.snapshotId})${paths}`;
21
+ }
22
+ /** Renders the full report shown after `/review` finishes. `fileLabels` maps
23
+ * snapshot fileKeys to human paths when the caller still holds the manifest. */
24
+ function formatReviewReport(result, fileLabels) {
25
+ const lines = [];
26
+ lines.push(`Code review ${result.reviewId} — ${formatScope(result)}`);
27
+ lines.push(`Model: ${result.model.provider}/${result.model.model} · input snapshot ${result.inputSnapshotDigest.slice(0, 12)}`);
28
+ const emptyScope = result.limits.includes('empty_scope_no_changes');
29
+ if (emptyScope) {
30
+ lines.push('');
31
+ lines.push('No changes under the selected scope — nothing to review.');
32
+ return lines.join('\n');
33
+ }
34
+ lines.push('');
35
+ lines.push(STATE_BANNERS[result.state]);
36
+ if (result.findings.length > 0) {
37
+ lines.push('');
38
+ lines.push(`Findings (${result.findings.length}):`);
39
+ for (const priority of PRIORITY_ORDER) {
40
+ const group = result.findings.filter(finding => finding.priority === priority);
41
+ if (group.length === 0)
42
+ continue;
43
+ for (const finding of group) {
44
+ lines.push('');
45
+ lines.push(`[${priority}] ${finding.title}`);
46
+ lines.push(` Location: ${fileLabels?.get(finding.fileKey) ?? finding.fileKey.slice(0, 12)} ${finding.side}:${finding.startLine}-${finding.endLine}`);
47
+ lines.push(` Trigger: ${finding.trigger}`);
48
+ lines.push(` Impact: ${finding.impact}`);
49
+ lines.push(` Evidence: ${finding.evidence}`);
50
+ }
51
+ }
52
+ }
53
+ else if (result.state === 'completed' && result.conclusion === 'no_findings') {
54
+ lines.push('No findings within the reviewed scope.');
55
+ }
56
+ if (result.invalidFindingCount > 0) {
57
+ lines.push('');
58
+ lines.push(`${result.invalidFindingCount} finding(s) from the model were invalid or outside the frozen snapshot and are not shown.`);
59
+ }
60
+ lines.push('');
61
+ const coverage = result.coverage;
62
+ lines.push(`Coverage: ${coverage.inspectedFiles.length} file(s) inspected, ${coverage.skipped.length} skipped${coverage.truncated ? ', output truncated' : ''}`);
63
+ for (const skip of coverage.skipped) {
64
+ lines.push(` skipped: ${skip.fileKey} (${skip.reason})`);
65
+ }
66
+ if (coverage.budgetExhausted.length > 0) {
67
+ lines.push(`Budget exhausted: ${coverage.budgetExhausted.join(', ')}`);
68
+ }
69
+ for (const limit of result.limits) {
70
+ if (limit !== 'empty_scope_no_changes')
71
+ lines.push(`Limit: ${limit}`);
72
+ }
73
+ if (!result.coverageAssertion.complete && result.state === 'completed') {
74
+ lines.push(`Coverage incomplete: ${result.coverageAssertion.reason ?? 'not all changed files were reviewed'}`);
75
+ }
76
+ lines.push(`Receipt digest: ${result.receiptDigest.slice(0, 16)}`);
77
+ return lines.join('\n');
78
+ }
79
+ //# sourceMappingURL=format.js.map
@@ -0,0 +1,71 @@
1
+ import { DEFAULT_REVIEW_BUDGETS, type CodeReviewResultV1, type ReviewBudgetsV1, type ReviewSelectorV1 } from './contract';
2
+ import { type ReviewGitRunner } from './snapshot';
3
+ /** One bounded model completion. CLI wiring adapts `LLMService.chat`. */
4
+ export interface ReviewModelAdapterV1 {
5
+ complete(messages: readonly {
6
+ readonly role: 'system' | 'user';
7
+ readonly content: string;
8
+ }[]): Promise<{
9
+ readonly content: string;
10
+ readonly model: string;
11
+ readonly usage?: {
12
+ readonly inputTokens: number;
13
+ readonly outputTokens: number;
14
+ };
15
+ }>;
16
+ }
17
+ export interface ReviewRunIdentityV1 {
18
+ readonly workspaceIdentity: string;
19
+ readonly sessionId: string;
20
+ readonly threadId: string;
21
+ readonly turnId: string;
22
+ }
23
+ export interface ReviewRunOptionsV1 {
24
+ readonly cwd: string;
25
+ readonly selector: ReviewSelectorV1;
26
+ readonly adapter: ReviewModelAdapterV1;
27
+ readonly identity: ReviewRunIdentityV1;
28
+ readonly budgets?: Partial<ReviewBudgetsV1>;
29
+ readonly abortSignal?: AbortSignal;
30
+ readonly runGit?: ReviewGitRunner;
31
+ readonly now?: () => number;
32
+ }
33
+ export type ReviewRunFailureV1 = {
34
+ readonly kind: 'budget';
35
+ readonly field: string;
36
+ } | {
37
+ readonly kind: 'snapshot';
38
+ readonly code: string;
39
+ readonly message: string;
40
+ } | {
41
+ readonly kind: 'cancelled';
42
+ } | {
43
+ readonly kind: 'model_error';
44
+ readonly message: string;
45
+ } | {
46
+ readonly kind: 'unparsable_output';
47
+ readonly message: string;
48
+ };
49
+ export type ReviewRunOutcomeV1 = {
50
+ readonly ok: true;
51
+ readonly result: CodeReviewResultV1;
52
+ } | {
53
+ readonly ok: false;
54
+ readonly failure: ReviewRunFailureV1;
55
+ };
56
+ /** Production git runner: argument arrays, no shell, read-only profile is the
57
+ * caller's responsibility (the runner only issues read commands). */
58
+ export declare function createProcessGitRunner(cwd: string): ReviewGitRunner;
59
+ /** Rules provenance: AGENTS.md contents from cwd up to the filesystem root
60
+ * (bounded to 16 levels), combined with the frozen read-only constraints
61
+ * digest. Changing the rules or the policy invalidates applicability. */
62
+ export declare function computeReviewRulesDigest(cwd: string): Promise<string>;
63
+ /** The frozen review instruction set assembled for the model. */
64
+ export declare function buildReviewSystemPrompt(rulesDigest: string): string;
65
+ /**
66
+ * Runs one bounded review. Never throws for expected review failures — the
67
+ * outcome is typed; unexpected internal errors surface as `model_error`.
68
+ */
69
+ export declare function runCodeReview(options: ReviewRunOptionsV1): Promise<ReviewRunOutcomeV1>;
70
+ export { DEFAULT_REVIEW_BUDGETS };
71
+ //# sourceMappingURL=runner.d.ts.map
@@ -0,0 +1,354 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DEFAULT_REVIEW_BUDGETS = void 0;
4
+ exports.createProcessGitRunner = createProcessGitRunner;
5
+ exports.computeReviewRulesDigest = computeReviewRulesDigest;
6
+ exports.buildReviewSystemPrompt = buildReviewSystemPrompt;
7
+ exports.runCodeReview = runCodeReview;
8
+ /**
9
+ * v0.3.28 — the bounded single-pass code-review runner (docs/plan/
10
+ * v0.3.28-plan.md S2). Wires the frozen PR0 contract and the PR1 snapshot
11
+ * reader into an executable review: a typed selector is resolved to a frozen
12
+ * Git snapshot, the snapshot material is assembled into a bounded prompt, one
13
+ * model completion is requested, and the output is validated against the
14
+ * contract before a digest-verifiable `CodeReviewResultV1` receipt is built.
15
+ *
16
+ * Read-only posture: the runner performs no writes inside the reviewed
17
+ * repository — git runs read-only with argument arrays, working files are read
18
+ * through the contained reader, and the model receives the frozen material
19
+ * inline instead of an open tool surface. `REVIEW_WORKFLOW_CONSTRAINTS` is
20
+ * bound into the receipt's rules digest so receipts record the policy that
21
+ * governed the run.
22
+ */
23
+ const child_process_1 = require("child_process");
24
+ const crypto_1 = require("crypto");
25
+ const crypto_2 = require("crypto");
26
+ const promises_1 = require("fs/promises");
27
+ const path_1 = require("path");
28
+ const contract_1 = require("./contract");
29
+ Object.defineProperty(exports, "DEFAULT_REVIEW_BUDGETS", { enumerable: true, get: function () { return contract_1.DEFAULT_REVIEW_BUDGETS; } });
30
+ const constraints_1 = require("./constraints");
31
+ const snapshot_1 = require("./snapshot");
32
+ function sha256(value) {
33
+ return (0, crypto_1.createHash)('sha256').update(value, 'utf8').digest('hex');
34
+ }
35
+ /** Production git runner: argument arrays, no shell, read-only profile is the
36
+ * caller's responsibility (the runner only issues read commands). */
37
+ function createProcessGitRunner(cwd) {
38
+ return async (args) => new Promise((resolvePromise, reject) => {
39
+ (0, child_process_1.execFile)('git', args, { cwd, maxBuffer: 64 * 1024 * 1024 }, (error, stdout, stderr) => {
40
+ if (error) {
41
+ reject(new Error(`git ${args.join(' ')} failed: ${stderr || error.message}`));
42
+ return;
43
+ }
44
+ resolvePromise(stdout);
45
+ });
46
+ });
47
+ }
48
+ /** Rules provenance: AGENTS.md contents from cwd up to the filesystem root
49
+ * (bounded to 16 levels), combined with the frozen read-only constraints
50
+ * digest. Changing the rules or the policy invalidates applicability. */
51
+ async function computeReviewRulesDigest(cwd) {
52
+ const chunks = [];
53
+ let current = (0, path_1.resolve)(cwd);
54
+ for (let depth = 0; depth < 16; depth += 1) {
55
+ try {
56
+ const content = await (0, promises_1.readFile)((0, path_1.join)(current, 'AGENTS.md'), 'utf8');
57
+ chunks.push(`${current}::${content}`);
58
+ }
59
+ catch {
60
+ // No AGENTS.md at this level — fine.
61
+ }
62
+ const parent = (0, path_1.dirname)(current);
63
+ if (parent === current)
64
+ break;
65
+ current = parent;
66
+ }
67
+ return sha256(`${constraints_1.REVIEW_WORKFLOW_CONSTRAINTS.constraintsDigest}\n${chunks.join('\n---\n')}`);
68
+ }
69
+ /** The frozen review instruction set assembled for the model. */
70
+ function buildReviewSystemPrompt(rulesDigest) {
71
+ return [
72
+ 'You are a strict code reviewer operating inside Orion Code.',
73
+ 'You receive a frozen snapshot of the changes under review.',
74
+ 'Report only defects introduced or worsened by these changes, with concrete evidence.',
75
+ 'Exclude pure style opinions, speculation without evidence, and duplicates.',
76
+ 'Never suggest running commands, editing files, or any other action: this review is read-only.',
77
+ 'Respond with a single JSON object of the shape {"findings":[...]} and nothing else.',
78
+ 'Each finding object needs exactly these fields:',
79
+ ' findingId: 8-64 hex characters, stable within this response;',
80
+ ' priority: "P0" (blocks usage, proven) | "P1" | "P2" | "P3";',
81
+ ' title: short imperative summary;',
82
+ ' trigger: the concrete condition under which the defect manifests;',
83
+ ' impact: what breaks and for whom;',
84
+ ' fileKey: copy the file key from the snapshot material verbatim;',
85
+ ' side: "old" for removed/base lines, "new" for added/current lines;',
86
+ ' startLine, endLine: 1-based inclusive line numbers in that side;',
87
+ ' evidence: the code excerpt or reasoning that proves the finding.',
88
+ `Active policy digest: ${constraints_1.REVIEW_WORKFLOW_CONSTRAINTS.constraintsDigest}.`,
89
+ `Rules digest: ${rulesDigest}.`,
90
+ ].join('\n');
91
+ }
92
+ /** Assembles the bounded user prompt material from the frozen snapshot plus
93
+ * the diff text captured inside the admission window. */
94
+ function buildSnapshotMaterial(snapshot, diffText, budgets) {
95
+ const sections = [
96
+ `Snapshot: ${snapshot.snapshotId} (kind=${snapshot.kind}`,
97
+ snapshot.kind === 'base' ? `base=${snapshot.baseOid} head=${snapshot.headOid})` : 'worktree scope)',
98
+ `Input snapshot digest: ${snapshot.inputSnapshotDigest}`,
99
+ 'Changed files (fileKey -> path):',
100
+ ...snapshot.files.map(file => {
101
+ const sides = file.sides.map(side => side.source).join('+') || 'no-content';
102
+ return ` ${file.fileKey} ${file.path}${file.oldPath ? ` (from ${file.oldPath})` : ''} [${sides}]`;
103
+ }),
104
+ snapshot.skipped.length > 0
105
+ ? `Skipped by the host: ${snapshot.skipped.map(skip => `${skip.path} (${skip.reason})`).join('; ')}`
106
+ : 'Skipped by the host: none',
107
+ 'Unified diff:',
108
+ diffText,
109
+ ];
110
+ const text = sections.join('\n');
111
+ if (Buffer.byteLength(text, 'utf8') <= budgets.maxBodyBytes) {
112
+ return { text, truncated: false };
113
+ }
114
+ const clipped = Buffer.alloc(budgets.maxBodyBytes);
115
+ const written = Buffer.from(text, 'utf8').subarray(0, budgets.maxBodyBytes);
116
+ clipped.set(written);
117
+ return { text: clipped.toString('utf8'), truncated: true };
118
+ }
119
+ function extractFindingsPayload(content) {
120
+ const trimmed = content.trim();
121
+ const fenced = trimmed.match(/```(?:json)?\s*([\s\S]*?)```/);
122
+ const candidate = fenced ? fenced[1].trim() : trimmed;
123
+ const start = candidate.indexOf('{');
124
+ const end = candidate.lastIndexOf('}');
125
+ if (start < 0 || end <= start)
126
+ return null;
127
+ try {
128
+ const parsed = JSON.parse(candidate.slice(start, end + 1));
129
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
130
+ return null;
131
+ const record = parsed;
132
+ if (!Array.isArray(record.findings))
133
+ return null;
134
+ return { findings: record.findings };
135
+ }
136
+ catch {
137
+ return null;
138
+ }
139
+ }
140
+ function snapshotAuthorizesFile(snapshot, finding) {
141
+ const file = snapshot.files.find(candidate => candidate.fileKey === finding.fileKey);
142
+ if (!file)
143
+ return false;
144
+ if (finding.side === 'old' && !file.oldPath && file.hunks.every(hunk => hunk.oldLines === 0)) {
145
+ return false;
146
+ }
147
+ const relevant = file.hunks.filter(hunk => finding.side === 'old'
148
+ ? hunk.oldLines > 0 && finding.startLine <= hunk.oldStart + hunk.oldLines - 1 && finding.endLine >= hunk.oldStart
149
+ : hunk.newLines > 0 && finding.startLine <= hunk.newStart + hunk.newLines - 1 && finding.endLine >= hunk.newStart);
150
+ // A finding whose lines touch no diff hunk is not anchored in the frozen
151
+ // material; the model must cite the changes under review.
152
+ return relevant.length > 0;
153
+ }
154
+ /**
155
+ * Runs one bounded review. Never throws for expected review failures — the
156
+ * outcome is typed; unexpected internal errors surface as `model_error`.
157
+ */
158
+ async function runCodeReview(options) {
159
+ const now = options.now ?? Date.now;
160
+ const startedAt = now();
161
+ const budgets = (0, contract_1.resolveReviewBudgets)(options.budgets);
162
+ if (!budgets.ok)
163
+ return { ok: false, failure: { kind: 'budget', field: budgets.field } };
164
+ const aborted = () => options.abortSignal?.aborted === true;
165
+ if (aborted())
166
+ return { ok: false, failure: { kind: 'cancelled' } };
167
+ const runGit = options.runGit ?? createProcessGitRunner(options.cwd);
168
+ let snapshot;
169
+ let diffText;
170
+ try {
171
+ snapshot = await (0, snapshot_1.captureReviewSnapshot)({
172
+ root: options.cwd,
173
+ selector: options.selector,
174
+ budgets: budgets.budgets,
175
+ runGit,
176
+ readWorktreeFile: (0, snapshot_1.createContainedWorktreeReader)(options.cwd),
177
+ });
178
+ if (aborted())
179
+ return { ok: false, failure: { kind: 'cancelled' } };
180
+ // The exact same frozen diff arguments the snapshot used, against the
181
+ // OIDs the snapshot resolved; captured once inside the admission window
182
+ // and never re-read afterwards.
183
+ diffText = await runGit((0, snapshot_1.reviewUnifiedDiffArgs)(options.selector, {
184
+ baseOid: snapshot.baseOid,
185
+ headOid: snapshot.headOid,
186
+ }));
187
+ }
188
+ catch (error) {
189
+ const code = error instanceof snapshot_1.ReviewSnapshotError ? error.code : 'not_a_repository';
190
+ return {
191
+ ok: false,
192
+ failure: { kind: 'snapshot', code, message: error instanceof Error ? error.message : String(error) },
193
+ };
194
+ }
195
+ const rulesDigest = await computeReviewRulesDigest(options.cwd);
196
+ const limits = [];
197
+ const budgetExhausted = [];
198
+ if (snapshot.files.length === 0) {
199
+ limits.push('empty_scope_no_changes');
200
+ const endedAt = now();
201
+ const coverage = {
202
+ inspectedFiles: [],
203
+ inspectedRanges: [],
204
+ skipped: [],
205
+ truncated: false,
206
+ budgetExhausted: [],
207
+ };
208
+ const base = {
209
+ schemaVersion: 1,
210
+ reviewId: `cr-${(0, crypto_2.randomBytes)(8).toString('hex')}`,
211
+ workspaceIdentity: options.identity.workspaceIdentity,
212
+ sessionId: options.identity.sessionId,
213
+ threadId: options.identity.threadId,
214
+ turnId: options.identity.turnId,
215
+ requestedSelector: options.selector,
216
+ resolvedTarget: {
217
+ kind: snapshot.kind,
218
+ ...(snapshot.baseOid ? { baseOid: snapshot.baseOid } : {}),
219
+ ...(snapshot.headOid ? { headOid: snapshot.headOid } : {}),
220
+ snapshotId: snapshot.snapshotId,
221
+ },
222
+ inputSnapshotDigest: snapshot.inputSnapshotDigest,
223
+ rulesDigest,
224
+ state: 'completed',
225
+ conclusion: 'no_findings',
226
+ findings: [],
227
+ invalidFindingCount: 0,
228
+ coverage,
229
+ coverageAssertion: { complete: true },
230
+ limits,
231
+ model: { provider: 'none', model: 'none' },
232
+ usage: { inputTokens: 0, outputTokens: 0 },
233
+ startedAt,
234
+ endedAt,
235
+ };
236
+ return { ok: true, result: Object.freeze({ ...base, receiptDigest: (0, contract_1.computeReceiptDigest)(base) }) };
237
+ }
238
+ const material = buildSnapshotMaterial(snapshot, diffText, budgets.budgets);
239
+ if (material.truncated) {
240
+ budgetExhausted.push('maxBodyBytes');
241
+ limits.push('prompt_material_truncated');
242
+ }
243
+ if (aborted())
244
+ return { ok: false, failure: { kind: 'cancelled' } };
245
+ let completion;
246
+ try {
247
+ completion = await options.adapter.complete([
248
+ { role: 'system', content: buildReviewSystemPrompt(rulesDigest) },
249
+ { role: 'user', content: material.text },
250
+ ]);
251
+ }
252
+ catch (error) {
253
+ return {
254
+ ok: false,
255
+ failure: {
256
+ kind: 'model_error',
257
+ message: error instanceof Error ? error.message : String(error),
258
+ },
259
+ };
260
+ }
261
+ const durationExceeded = now() - startedAt > budgets.budgets.maxDurationMs;
262
+ if (durationExceeded)
263
+ budgetExhausted.push('maxDurationMs');
264
+ const payload = extractFindingsPayload(completion.content);
265
+ if (!payload) {
266
+ return {
267
+ ok: false,
268
+ failure: {
269
+ kind: 'unparsable_output',
270
+ message: 'The model response did not contain a {"findings":[...]} JSON object.',
271
+ },
272
+ };
273
+ }
274
+ const validFindings = [];
275
+ let invalidFindingCount = 0;
276
+ for (const raw of payload.findings) {
277
+ const validated = (0, contract_1.validateReviewFinding)(raw);
278
+ if (!validated.valid) {
279
+ invalidFindingCount += 1;
280
+ continue;
281
+ }
282
+ if (validFindings.length >= budgets.budgets.maxFindings) {
283
+ invalidFindingCount += 1;
284
+ budgetExhausted.push('maxFindings');
285
+ continue;
286
+ }
287
+ if (!snapshotAuthorizesFile(snapshot, validated.finding)) {
288
+ invalidFindingCount += 1;
289
+ continue;
290
+ }
291
+ validFindings.push(validated.finding);
292
+ }
293
+ const truncated = material.truncated || budgetExhausted.includes('maxFindings');
294
+ const coverage = {
295
+ inspectedFiles: snapshot.files.map(file => file.path),
296
+ inspectedRanges: [],
297
+ skipped: snapshot.skipped.map(skip => ({
298
+ fileKey: skip.path,
299
+ reason: skip.reason,
300
+ ...(skip.detail ? { detail: skip.detail } : {}),
301
+ })),
302
+ truncated,
303
+ budgetExhausted: [...new Set(budgetExhausted)],
304
+ };
305
+ const coverageComplete = snapshot.skipped.length === 0 && !truncated && !durationExceeded && !material.truncated;
306
+ let state = 'completed';
307
+ if (aborted())
308
+ return { ok: false, failure: { kind: 'cancelled' } };
309
+ if (truncated || durationExceeded || snapshot.skipped.length > 0)
310
+ state = 'partial';
311
+ const conclusion = (0, contract_1.resolveReviewConclusion)({
312
+ state,
313
+ validFindingCount: validFindings.length,
314
+ invalidFindingCount,
315
+ coverageComplete,
316
+ });
317
+ const base = {
318
+ schemaVersion: 1,
319
+ reviewId: `cr-${(0, crypto_2.randomBytes)(8).toString('hex')}`,
320
+ workspaceIdentity: options.identity.workspaceIdentity,
321
+ sessionId: options.identity.sessionId,
322
+ threadId: options.identity.threadId,
323
+ turnId: options.identity.turnId,
324
+ requestedSelector: options.selector,
325
+ resolvedTarget: {
326
+ kind: snapshot.kind,
327
+ ...(snapshot.baseOid ? { baseOid: snapshot.baseOid } : {}),
328
+ ...(snapshot.headOid ? { headOid: snapshot.headOid } : {}),
329
+ snapshotId: snapshot.snapshotId,
330
+ },
331
+ inputSnapshotDigest: snapshot.inputSnapshotDigest,
332
+ rulesDigest,
333
+ state,
334
+ conclusion,
335
+ findings: Object.freeze(validFindings),
336
+ invalidFindingCount,
337
+ coverage,
338
+ coverageAssertion: { complete: coverageComplete },
339
+ limits,
340
+ model: { provider: 'orion-llm', model: completion.model },
341
+ usage: {
342
+ inputTokens: completion.usage?.inputTokens ?? 0,
343
+ outputTokens: completion.usage?.outputTokens ?? 0,
344
+ },
345
+ startedAt,
346
+ endedAt: now(),
347
+ };
348
+ const result = Object.freeze({
349
+ ...base,
350
+ receiptDigest: (0, contract_1.computeReceiptDigest)(base),
351
+ });
352
+ return { ok: true, result };
353
+ }
354
+ //# sourceMappingURL=runner.js.map
@@ -0,0 +1,24 @@
1
+ /**
2
+ * v0.3.28 — typed `/review` selector parsing (docs/plan/v0.3.28-plan.md S1).
3
+ *
4
+ * Slash arguments are parsed once into the frozen `ReviewSelectorV1` contract
5
+ * from PR0. Unknown arguments produce a typed usage error instead of falling
6
+ * back to an ordinary chat turn (v0.3.27 plan §3.1: "不能当成分支、路径或任意
7
+ * 普通聊天悄悄继续").
8
+ */
9
+ import type { ReviewSelectorV1 } from './contract';
10
+ export type ReviewSelectorParseResult = {
11
+ readonly ok: true;
12
+ readonly selector: ReviewSelectorV1;
13
+ } | {
14
+ readonly ok: false;
15
+ readonly usage: string;
16
+ };
17
+ export declare const REVIEW_USAGE_TEXT: string;
18
+ /**
19
+ * Parses `/review` arguments. Tokens: `all` alias, `--base <ref>` (once),
20
+ * `--path <path>` (repeatable, also `--path=<path>`), a single bare relative
21
+ * path (back-compat path filter). Anything else is a usage error.
22
+ */
23
+ export declare function parseReviewSelector(rawArgs: string): ReviewSelectorParseResult;
24
+ //# sourceMappingURL=selector.d.ts.map
@@ -0,0 +1,83 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.REVIEW_USAGE_TEXT = void 0;
4
+ exports.parseReviewSelector = parseReviewSelector;
5
+ exports.REVIEW_USAGE_TEXT = [
6
+ 'Usage: /review [all | --base <ref>] [--path <path>]...',
7
+ ' /review Review the current worktree (staged + unstaged + untracked)',
8
+ ' /review all Alias for the current worktree',
9
+ ' /review --base main Review committed changes merge-base(main, HEAD)..HEAD',
10
+ ' /review --path src/foo.ts Restrict the review to a path (repeatable)',
11
+ ].join('\n');
12
+ function ok(selector) {
13
+ return { ok: true, selector: Object.freeze({ ...selector }) };
14
+ }
15
+ function usageError(reason) {
16
+ return { ok: false, usage: `${reason}\n\n${exports.REVIEW_USAGE_TEXT}` };
17
+ }
18
+ function looksLikeRelativePath(value) {
19
+ return (value.length > 0 &&
20
+ !value.startsWith('/') &&
21
+ !value.startsWith('-') &&
22
+ !value.includes('\\') &&
23
+ value.split('/').every(segment => segment && segment !== '.' && segment !== '..'));
24
+ }
25
+ /**
26
+ * Parses `/review` arguments. Tokens: `all` alias, `--base <ref>` (once),
27
+ * `--path <path>` (repeatable, also `--path=<path>`), a single bare relative
28
+ * path (back-compat path filter). Anything else is a usage error.
29
+ */
30
+ function parseReviewSelector(rawArgs) {
31
+ const tokens = rawArgs.trim().split(/\s+/).filter(token => token.length > 0);
32
+ let base = null;
33
+ const paths = [];
34
+ for (let index = 0; index < tokens.length; index += 1) {
35
+ const token = tokens[index];
36
+ if (token === 'all') {
37
+ if (tokens.length !== 1) {
38
+ return usageError('`all` cannot be combined with other arguments.');
39
+ }
40
+ return ok({ kind: 'worktree' });
41
+ }
42
+ if (token === '--base' || token.startsWith('--base=')) {
43
+ if (base !== null)
44
+ return usageError('--base may only be given once.');
45
+ const value = token.startsWith('--base=') ? token.slice('--base='.length) : tokens[index + 1];
46
+ if (!value)
47
+ return usageError('--base requires a ref argument.');
48
+ if (token === '--base')
49
+ index += 1;
50
+ base = value;
51
+ continue;
52
+ }
53
+ if (token === '--path' || token.startsWith('--path=')) {
54
+ const value = token.startsWith('--path=') ? token.slice('--path='.length) : tokens[index + 1];
55
+ if (!value)
56
+ return usageError('--path requires a path argument.');
57
+ if (token === '--path')
58
+ index += 1;
59
+ if (!looksLikeRelativePath(value)) {
60
+ return usageError(`Invalid --path value: ${value}`);
61
+ }
62
+ paths.push(value);
63
+ continue;
64
+ }
65
+ if (token.startsWith('-')) {
66
+ return usageError(`Unknown option: ${token}`);
67
+ }
68
+ // Bare argument: back-compat single existing-path filter. Multiple bare
69
+ // arguments are ambiguous and rejected rather than guessed.
70
+ if (paths.length > 0 || base !== null) {
71
+ return usageError(`Unexpected argument: ${token}`);
72
+ }
73
+ if (!looksLikeRelativePath(token)) {
74
+ return usageError(`Invalid path argument: ${token}`);
75
+ }
76
+ paths.push(token);
77
+ }
78
+ if (base !== null) {
79
+ return ok(paths.length > 0 ? { kind: 'base', ref: base, paths } : { kind: 'base', ref: base });
80
+ }
81
+ return ok(paths.length > 0 ? { kind: 'worktree', paths } : { kind: 'worktree' });
82
+ }
83
+ //# sourceMappingURL=selector.js.map
@@ -53,6 +53,13 @@ export interface ReviewSnapshotOptionsV1 {
53
53
  readonly readWorktreeFile?: (relativePath: string) => Promise<string | null>;
54
54
  readonly snapshotIdPrefix?: string;
55
55
  }
56
+ /** The read-only git arguments for the scope's unified diff. Exported so the
57
+ * v0.3.28 runner captures the exact same diff the snapshot froze instead of
58
+ * re-deriving OIDs after admission. */
59
+ export declare function reviewUnifiedDiffArgs(selector: ReviewSelectorV1, oids: {
60
+ readonly baseOid: string | null;
61
+ readonly headOid: string | null;
62
+ }): string[];
56
63
  /** Resolves the selector to a frozen snapshot. Every OID and digest in the
57
64
  * returned object was taken at admission time; callers persist the returned
58
65
  * digest as the review's input identity. */
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ReviewSnapshotError = void 0;
4
+ exports.reviewUnifiedDiffArgs = reviewUnifiedDiffArgs;
4
5
  exports.captureReviewSnapshot = captureReviewSnapshot;
5
6
  exports.createContainedWorktreeReader = createContainedWorktreeReader;
6
7
  /**
@@ -107,6 +108,14 @@ function pathMatches(path, requestedPaths) {
107
108
  return true;
108
109
  return requestedPaths.some(filter => path === filter || path.startsWith(`${filter}/`));
109
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
+ }
110
119
  /** Resolves the selector to a frozen snapshot. Every OID and digest in the
111
120
  * returned object was taken at admission time; callers persist the returned
112
121
  * digest as the review's input identity. */
@@ -138,7 +147,7 @@ async function captureReviewSnapshot(options) {
138
147
  baseOid,
139
148
  headOid,
140
149
  ];
141
- unifiedArgs = ['diff', '--no-ext-diff', '--no-textconv', baseOid, headOid];
150
+ unifiedArgs = reviewUnifiedDiffArgs(options.selector, { baseOid, headOid });
142
151
  }
143
152
  else {
144
153
  nameStatusArgs = [
@@ -150,7 +159,7 @@ async function captureReviewSnapshot(options) {
150
159
  '-z',
151
160
  'HEAD',
152
161
  ];
153
- unifiedArgs = ['diff', '--no-ext-diff', '--no-textconv', 'HEAD'];
162
+ unifiedArgs = reviewUnifiedDiffArgs(options.selector, { baseOid, headOid });
154
163
  }
155
164
  const nameStatusRaw = await runGit(nameStatusArgs);
156
165
  const changed = parseNameStatusZ(nameStatusRaw.split('\0')).filter(entry => {
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@orion-agents/orion-code",
3
- "version": "0.3.27",
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.27",
9
+ "version": "0.3.28",
10
10
  "license": "MIT",
11
11
  "dependencies": {
12
12
  "@clack/prompts": "^0.7.0",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orion-agents/orion-code",
3
- "version": "0.3.27",
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",