@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 +64 -3
- package/README.md +11 -10
- package/README.zh-CN.md +8 -7
- package/dist/commands/workflow-command-handlers.d.ts +9 -1
- package/dist/commands/workflow-command-handlers.js +66 -0
- package/dist/commands/workflow-commands.js +5 -5
- package/dist/services/code-review/format.d.ts +12 -0
- package/dist/services/code-review/format.js +79 -0
- package/dist/services/code-review/runner.d.ts +71 -0
- package/dist/services/code-review/runner.js +354 -0
- package/dist/services/code-review/selector.d.ts +24 -0
- package/dist/services/code-review/selector.js +83 -0
- package/dist/services/code-review/snapshot.d.ts +7 -0
- package/dist/services/code-review/snapshot.js +11 -2
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -22,9 +22,70 @@ which is **not** a pass.
|
|
|
22
22
|
|
|
23
23
|
## [Unreleased]
|
|
24
24
|
|
|
25
|
-
## [0.3.
|
|
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
|
-
|
|
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
|
|
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.
|
|
6
|
-
>
|
|
7
|
-
>
|
|
8
|
-
>
|
|
9
|
-
>
|
|
10
|
-
>
|
|
11
|
-
>
|
|
12
|
-
> (registry `latest`), tagged `v0.3.
|
|
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.
|
|
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.
|
|
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.
|
|
6
|
-
>
|
|
7
|
-
>
|
|
8
|
-
>
|
|
9
|
-
>
|
|
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.
|
|
105
|
+
v0.3.28 在 npm 可用后,可安装固定版本:
|
|
105
106
|
|
|
106
107
|
```bash
|
|
107
|
-
npm install -g @orion-agents/orion-code@0.3.
|
|
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
|
-
|
|
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: '
|
|
58
|
-
argumentHint: '[
|
|
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: '
|
|
62
|
-
execution: '
|
|
61
|
+
type: 'builtin',
|
|
62
|
+
execution: 'builtin',
|
|
63
63
|
risk: 'read-only',
|
|
64
|
-
execute: (
|
|
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 =
|
|
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 =
|
|
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 => {
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@orion-agents/orion-code",
|
|
3
|
-
"version": "0.3.
|
|
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.
|
|
9
|
+
"version": "0.3.28",
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@clack/prompts": "^0.7.0",
|