@orion-agents/orion-code 0.3.28 → 0.3.29
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 +54 -3
- package/README.md +9 -11
- package/README.zh-CN.md +7 -8
- package/dist/commands/workflow-command-handlers.d.ts +1 -1
- package/dist/services/code-review/runner.d.ts +6 -1
- package/dist/services/code-review/runner.js +19 -5
- package/dist/services/code-review/selector.js +4 -1
- package/dist/web/code-review-run-service.d.ts +68 -0
- package/dist/web/code-review-run-service.js +228 -0
- package/dist/web/server.js +37 -0
- package/dist/web/workbench-controller.d.ts +16 -0
- package/dist/web/workbench-controller.js +61 -0
- package/dist/web-client/assets/{FilesPanel-CdoCa8u0.js → FilesPanel-iBYj9ZRf.js} +1 -1
- package/dist/web-client/assets/{GitPanel-CRr_iIDz.js → GitPanel-D33R_bZr.js} +1 -1
- package/dist/web-client/assets/ReviewPanel-C_0upsDk.js +6 -0
- package/dist/web-client/assets/{TerminalPanel-BniCjatZ.js → TerminalPanel-C7A8dMNf.js} +1 -1
- package/dist/web-client/assets/{index-DCYKIeOj.css → index-BaOxrwGB.css} +1 -1
- package/dist/web-client/assets/{index-kQqA3li6.js → index-CqCgJ2ov.js} +13 -13
- package/dist/web-client/assets/{useAutoNotice-DPXFWIwK.js → useAutoNotice-C1qM-_Sg.js} +1 -1
- package/dist/web-client/index.html +2 -2
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/dist/web-client/assets/ReviewPanel-C1Cu2IRV.js +0 -5
package/CHANGELOG.md
CHANGED
|
@@ -22,9 +22,59 @@ which is **not** a pass.
|
|
|
22
22
|
|
|
23
23
|
## [Unreleased]
|
|
24
24
|
|
|
25
|
-
## [0.3.
|
|
25
|
+
## [0.3.29] — UNRELEASED
|
|
26
|
+
|
|
27
|
+
> **Status: candidate.** Open. The Web shared review loop
|
|
28
|
+
> (plan: `docs/plan/v0.3.29-plan.md`, PR3 core of the v0.3.27 review loop):
|
|
29
|
+
> the Host now starts the same bounded single-pass review runner the CLI uses
|
|
30
|
+
> (`runCodeReview`, v0.3.28) against the active Workspace. `POST /review/run`
|
|
31
|
+
> performs fast admission (one in-flight run per Host, typed
|
|
32
|
+
> `review_run_in_progress` 409 and `review_no_model` 422) and the model
|
|
33
|
+
> completion continues in the background; `GET /review/runs` and
|
|
34
|
+
> `GET /review/runs/:runId` read a bounded in-memory history where every
|
|
35
|
+
> receipt passes digest re-verification and is flagged `stale` when the
|
|
36
|
+
> repository revision changed after admission. The Review panel lists the runs
|
|
37
|
+
> with honest state banners (running / cancelled / failed / partial / stale
|
|
38
|
+
> never render as a green pass), groups findings P0→P3 with file:line anchors
|
|
39
|
+
> into the existing diff view, counts contract-invalid model output instead of
|
|
40
|
+
> hiding it, and can send each finding back to the Composer as a typed draft
|
|
41
|
+
> the user still confirms. The fileKey→path mapping is captured through an
|
|
42
|
+
> additive `onSnapshotCaptured` runner hook; the frozen receipt contract is
|
|
43
|
+
> unchanged. Not merged, tagged or published.
|
|
26
44
|
|
|
27
|
-
|
|
45
|
+
### Added
|
|
46
|
+
|
|
47
|
+
- `src/web/code-review-run-service.ts` — the Host-side review run service:
|
|
48
|
+
single-flight admission, background execution of the shared runner, bounded
|
|
49
|
+
receipt history (20 runs, oldest finished evicted first), digest
|
|
50
|
+
re-verification on read, repository-revision staleness flags, cancellation,
|
|
51
|
+
and the `parseWebReviewSelectorV1` request validation.
|
|
52
|
+
- `src/web/workbench-controller.ts` — `reviewRunStart` / `reviewRunList` /
|
|
53
|
+
`reviewRunGet` / `reviewRunCancel`, with the review model adapter derived
|
|
54
|
+
from the control-plane Runtime's `llm` (typed 422 when the Host has no
|
|
55
|
+
configured model).
|
|
56
|
+
- `src/web/server.ts` — `POST /review/run` and
|
|
57
|
+
`POST /review/runs/:runId/cancel` behind `assertMutation` +
|
|
58
|
+
`executeMutation`, plus read-only `GET /review/runs[/:runId]` with context
|
|
59
|
+
guards.
|
|
60
|
+
- `web/src/components/review/ReviewRunsSection.tsx` and
|
|
61
|
+
`review-runs-view.ts` — the Review panel results section: run button,
|
|
62
|
+
polling while a run is in flight, priority-grouped findings, honest banners,
|
|
63
|
+
diff anchoring and typed Composer drafts.
|
|
64
|
+
- Tests: `tests/web-code-review-run-service.test.ts` (10 unit tests),
|
|
65
|
+
`tests/web-review-run-endpoint.test.ts` (4 real-host tests on a temp Git
|
|
66
|
+
repository, including the 403 nonce guard and the 422 no-model path),
|
|
67
|
+
`tests/web-review-runs-view.test.tsx` (13 pure + SSR view tests).
|
|
68
|
+
|
|
69
|
+
### Not run
|
|
70
|
+
|
|
71
|
+
- Real Host + Chrome full-chain E2E and the human-labeled quality benchmark
|
|
72
|
+
(PR4) — the local automation sandbox has no model credentials; recorded
|
|
73
|
+
honestly as `not_run`, not silently skipped.
|
|
74
|
+
|
|
75
|
+
## [0.3.28] — 2026-10-22
|
|
76
|
+
|
|
77
|
+
> **Status: published.** The typed `/review` runtime & CLI slice
|
|
28
78
|
> (plan: `docs/plan/v0.3.28-plan.md`, PR2 core of the v0.3.27 review loop):
|
|
29
79
|
> `/review` arguments now parse into the frozen `ReviewSelectorV1` (worktree /
|
|
30
80
|
> `--base` / repeatable `--path`, back-compat single-path filter) and unknown
|
|
@@ -38,7 +88,8 @@ which is **not** a pass.
|
|
|
38
88
|
> reports "no changes under review" without calling the model, and partial,
|
|
39
89
|
> failed, cancelled or inconclusive runs never render as a green pass.
|
|
40
90
|
> Web wiring (PR3) and the qualification benchmark (PR4) remain subsequent
|
|
41
|
-
> slices.
|
|
91
|
+
> slices. Merged as #284, tagged `v0.3.28` on main (`8a4722b`), published to
|
|
92
|
+
> npm (registry `latest` = 0.3.28).
|
|
42
93
|
|
|
43
94
|
### Added
|
|
44
95
|
|
package/README.md
CHANGED
|
@@ -2,15 +2,13 @@
|
|
|
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
|
-
> previous release 0.3.27 is on npm (registry `latest`), tagged `v0.3.27`
|
|
13
|
-
> (`0be5b18`, #283) and released on GitHub.
|
|
5
|
+
> v0.3.29 candidate — open. The Web shared review loop: the Host starts the same
|
|
6
|
+
> bounded `/review` runner the CLI uses, the Review panel lists digest-verifiable
|
|
7
|
+
> receipts with honest state banners (running / partial / failed / cancelled /
|
|
8
|
+
> stale never green), findings are grouped P0→P3 and anchor to the existing diff
|
|
9
|
+
> view, and each finding can be sent back to the Composer as a typed draft. The
|
|
10
|
+
> previous release 0.3.28 is on npm (registry `latest`), tagged `v0.3.28`
|
|
11
|
+
> (`8a4722b`, #284) and released on GitHub.
|
|
14
12
|
|
|
15
13
|
[中文说明](README.zh-CN.md) ·
|
|
16
14
|
[v0.3.24 plan](docs/plan/v0.3.24-plan.md) ·
|
|
@@ -173,10 +171,10 @@ user extension boundaries.
|
|
|
173
171
|
Node.js 22.12+, 24, and 26 are supported. Use Node 24 LTS for production or Node 26 Current for
|
|
174
172
|
current development environments. Node 20 is upstream EOL and is no longer a v0.3 runtime.
|
|
175
173
|
|
|
176
|
-
To install the pinned v0.3.
|
|
174
|
+
To install the pinned v0.3.29 package when it is available on npm:
|
|
177
175
|
|
|
178
176
|
```bash
|
|
179
|
-
npm install -g @orion-agents/orion-code@0.3.
|
|
177
|
+
npm install -g @orion-agents/orion-code@0.3.29
|
|
180
178
|
orion --version
|
|
181
179
|
orion doctor
|
|
182
180
|
```
|
package/README.zh-CN.md
CHANGED
|
@@ -2,12 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
面向终端与浏览器、本地优先的目标驱动 Coding Agent。
|
|
4
4
|
|
|
5
|
-
> v0.3.
|
|
6
|
-
>
|
|
7
|
-
>
|
|
8
|
-
>
|
|
9
|
-
>
|
|
10
|
-
> (registry `latest`)、打 tag `v0.3.27`(`0be5b18`,#283)并发布 GitHub Release。
|
|
5
|
+
> v0.3.29 候选版本:进行中。Web 共享审查闭环:Host 复用 CLI 同一有界审查运行器发起
|
|
6
|
+
> 审查;审阅面板列出 digest 可验证的 receipt 与如实状态横幅(running / partial /
|
|
7
|
+
> failed / cancelled / 过期永远不会显示为绿色);finding 按 P0→P3 分组并可定位到既有
|
|
8
|
+
> diff 视图;每个 finding 可作为类型化草稿送回对话。上一版 0.3.28 已发布 npm
|
|
9
|
+
> (registry `latest`)、打 tag `v0.3.28`(`8a4722b`,#284)并发布 GitHub Release。
|
|
11
10
|
|
|
12
11
|
[English](README.md) ·
|
|
13
12
|
[v0.3.24 方案](docs/plan/v0.3.24-plan.md) ·
|
|
@@ -102,10 +101,10 @@ MCP 仍是用户可见的扩展边界。
|
|
|
102
101
|
支持 Node.js 22.12+、24 和 26。生产环境建议使用 Node 24 LTS,当前开发环境也支持 Node 26
|
|
103
102
|
Current。Node 20 已结束上游维护,不再属于 v0.3 Runtime 合同。
|
|
104
103
|
|
|
105
|
-
v0.3.
|
|
104
|
+
v0.3.29 在 npm 可用后,可安装固定版本:
|
|
106
105
|
|
|
107
106
|
```bash
|
|
108
|
-
npm install -g @orion-agents/orion-code@0.3.
|
|
107
|
+
npm install -g @orion-agents/orion-code@0.3.29
|
|
109
108
|
orion --version
|
|
110
109
|
orion doctor
|
|
111
110
|
```
|
|
@@ -12,5 +12,5 @@ declare function continueAsSlashChat(name: string, args: string): CommandResult;
|
|
|
12
12
|
* Git snapshot, and the receipt is rendered by the shared formatter.
|
|
13
13
|
*/
|
|
14
14
|
declare function handleReview(ctx: CommandContext, args: string): Promise<CommandResult>;
|
|
15
|
-
export { handlePlan, handleDiff, handleCommitPlan, continueAsSlashChat, handleReview
|
|
15
|
+
export { handlePlan, handleDiff, handleCommitPlan, continueAsSlashChat, handleReview };
|
|
16
16
|
//# sourceMappingURL=workflow-command-handlers.d.ts.map
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { DEFAULT_REVIEW_BUDGETS, type CodeReviewResultV1, type ReviewBudgetsV1, type ReviewSelectorV1 } from './contract';
|
|
2
|
-
import { type ReviewGitRunner } from './snapshot';
|
|
2
|
+
import { type CodeReviewSnapshotV1, type ReviewGitRunner } from './snapshot';
|
|
3
3
|
/** One bounded model completion. CLI wiring adapts `LLMService.chat`. */
|
|
4
4
|
export interface ReviewModelAdapterV1 {
|
|
5
5
|
complete(messages: readonly {
|
|
@@ -29,6 +29,11 @@ export interface ReviewRunOptionsV1 {
|
|
|
29
29
|
readonly abortSignal?: AbortSignal;
|
|
30
30
|
readonly runGit?: ReviewGitRunner;
|
|
31
31
|
readonly now?: () => number;
|
|
32
|
+
/** v0.3.29 — additive observation hook fired inside the admission window
|
|
33
|
+
* right after the frozen snapshot is captured. Lets a host keep the
|
|
34
|
+
* fileKey -> path mapping for diff anchoring without changing the frozen
|
|
35
|
+
* receipt contract. */
|
|
36
|
+
readonly onSnapshotCaptured?: (snapshot: CodeReviewSnapshotV1) => void;
|
|
32
37
|
}
|
|
33
38
|
export type ReviewRunFailureV1 = {
|
|
34
39
|
readonly kind: 'budget';
|
|
@@ -94,7 +94,9 @@ function buildReviewSystemPrompt(rulesDigest) {
|
|
|
94
94
|
function buildSnapshotMaterial(snapshot, diffText, budgets) {
|
|
95
95
|
const sections = [
|
|
96
96
|
`Snapshot: ${snapshot.snapshotId} (kind=${snapshot.kind}`,
|
|
97
|
-
snapshot.kind === 'base'
|
|
97
|
+
snapshot.kind === 'base'
|
|
98
|
+
? `base=${snapshot.baseOid} head=${snapshot.headOid})`
|
|
99
|
+
: 'worktree scope)',
|
|
98
100
|
`Input snapshot digest: ${snapshot.inputSnapshotDigest}`,
|
|
99
101
|
'Changed files (fileKey -> path):',
|
|
100
102
|
...snapshot.files.map(file => {
|
|
@@ -145,8 +147,12 @@ function snapshotAuthorizesFile(snapshot, finding) {
|
|
|
145
147
|
return false;
|
|
146
148
|
}
|
|
147
149
|
const relevant = file.hunks.filter(hunk => finding.side === 'old'
|
|
148
|
-
? hunk.oldLines > 0 &&
|
|
149
|
-
|
|
150
|
+
? hunk.oldLines > 0 &&
|
|
151
|
+
finding.startLine <= hunk.oldStart + hunk.oldLines - 1 &&
|
|
152
|
+
finding.endLine >= hunk.oldStart
|
|
153
|
+
: hunk.newLines > 0 &&
|
|
154
|
+
finding.startLine <= hunk.newStart + hunk.newLines - 1 &&
|
|
155
|
+
finding.endLine >= hunk.newStart);
|
|
150
156
|
// A finding whose lines touch no diff hunk is not anchored in the frozen
|
|
151
157
|
// material; the model must cite the changes under review.
|
|
152
158
|
return relevant.length > 0;
|
|
@@ -175,6 +181,7 @@ async function runCodeReview(options) {
|
|
|
175
181
|
runGit,
|
|
176
182
|
readWorktreeFile: (0, snapshot_1.createContainedWorktreeReader)(options.cwd),
|
|
177
183
|
});
|
|
184
|
+
options.onSnapshotCaptured?.(snapshot);
|
|
178
185
|
if (aborted())
|
|
179
186
|
return { ok: false, failure: { kind: 'cancelled' } };
|
|
180
187
|
// The exact same frozen diff arguments the snapshot used, against the
|
|
@@ -189,7 +196,11 @@ async function runCodeReview(options) {
|
|
|
189
196
|
const code = error instanceof snapshot_1.ReviewSnapshotError ? error.code : 'not_a_repository';
|
|
190
197
|
return {
|
|
191
198
|
ok: false,
|
|
192
|
-
failure: {
|
|
199
|
+
failure: {
|
|
200
|
+
kind: 'snapshot',
|
|
201
|
+
code,
|
|
202
|
+
message: error instanceof Error ? error.message : String(error),
|
|
203
|
+
},
|
|
193
204
|
};
|
|
194
205
|
}
|
|
195
206
|
const rulesDigest = await computeReviewRulesDigest(options.cwd);
|
|
@@ -233,7 +244,10 @@ async function runCodeReview(options) {
|
|
|
233
244
|
startedAt,
|
|
234
245
|
endedAt,
|
|
235
246
|
};
|
|
236
|
-
return {
|
|
247
|
+
return {
|
|
248
|
+
ok: true,
|
|
249
|
+
result: Object.freeze({ ...base, receiptDigest: (0, contract_1.computeReceiptDigest)(base) }),
|
|
250
|
+
};
|
|
237
251
|
}
|
|
238
252
|
const material = buildSnapshotMaterial(snapshot, diffText, budgets.budgets);
|
|
239
253
|
if (material.truncated) {
|
|
@@ -28,7 +28,10 @@ function looksLikeRelativePath(value) {
|
|
|
28
28
|
* path (back-compat path filter). Anything else is a usage error.
|
|
29
29
|
*/
|
|
30
30
|
function parseReviewSelector(rawArgs) {
|
|
31
|
-
const tokens = rawArgs
|
|
31
|
+
const tokens = rawArgs
|
|
32
|
+
.trim()
|
|
33
|
+
.split(/\s+/)
|
|
34
|
+
.filter(token => token.length > 0);
|
|
32
35
|
let base = null;
|
|
33
36
|
const paths = [];
|
|
34
37
|
for (let index = 0; index < tokens.length; index += 1) {
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { type CodeReviewResultV1, type ReviewRunStateV1, type ReviewSelectorV1 } from '../services/code-review/contract';
|
|
2
|
+
import { runCodeReview, type ReviewModelAdapterV1 } from '../services/code-review/runner';
|
|
3
|
+
export interface WebCodeReviewRunRecordV1 {
|
|
4
|
+
readonly runId: string;
|
|
5
|
+
readonly selector: ReviewSelectorV1;
|
|
6
|
+
readonly status: ReviewRunStateV1 | 'running';
|
|
7
|
+
readonly startedAt: number;
|
|
8
|
+
readonly endedAt: number | null;
|
|
9
|
+
/** Repository revision captured at admission; compared against the current
|
|
10
|
+
* value on read to mark a stored receipt stale. */
|
|
11
|
+
readonly repositoryRevisionAtAdmission: string;
|
|
12
|
+
/** fileKey -> path mapping captured with the frozen snapshot, so findings
|
|
13
|
+
* can be anchored to a diff without changing the receipt contract. */
|
|
14
|
+
readonly files: ReadonlyArray<{
|
|
15
|
+
readonly fileKey: string;
|
|
16
|
+
readonly path: string;
|
|
17
|
+
}>;
|
|
18
|
+
/** Present only for finished runs whose receipt passed digest re-verification. */
|
|
19
|
+
readonly result: CodeReviewResultV1 | null;
|
|
20
|
+
readonly failure: {
|
|
21
|
+
readonly kind: string;
|
|
22
|
+
readonly message: string;
|
|
23
|
+
} | null;
|
|
24
|
+
/** Recomputed on read: true when the repository changed since admission. */
|
|
25
|
+
readonly stale: boolean;
|
|
26
|
+
}
|
|
27
|
+
export interface WebCodeReviewRunServiceOptionsV1 {
|
|
28
|
+
/** Returns the review model adapter, or null when the Host has no model. */
|
|
29
|
+
readonly adapterProvider: () => ReviewModelAdapterV1 | null;
|
|
30
|
+
/** Workspace root used for the review snapshot; read at start time. */
|
|
31
|
+
readonly cwdProvider: () => string;
|
|
32
|
+
/** Current repository revision, sampled on every read for staleness. */
|
|
33
|
+
readonly repositoryRevisionProvider: () => Promise<string>;
|
|
34
|
+
/** Injectable runner (tests use fakes); defaults to the shared runner. */
|
|
35
|
+
readonly runReview?: typeof runCodeReview;
|
|
36
|
+
readonly now?: () => number;
|
|
37
|
+
/** Bounded receipt history; oldest finished records are evicted first. */
|
|
38
|
+
readonly maxRuns?: number;
|
|
39
|
+
}
|
|
40
|
+
export declare class WebCodeReviewRunServiceV1 {
|
|
41
|
+
private readonly records;
|
|
42
|
+
private readonly inflight;
|
|
43
|
+
private readonly adapterProvider;
|
|
44
|
+
private readonly cwdProvider;
|
|
45
|
+
private readonly repositoryRevisionProvider;
|
|
46
|
+
private readonly runReview;
|
|
47
|
+
private readonly now;
|
|
48
|
+
private readonly maxRuns;
|
|
49
|
+
constructor(options: WebCodeReviewRunServiceOptionsV1);
|
|
50
|
+
/** Admission is synchronous and fast: validate, record, return. The model
|
|
51
|
+
* completion continues in the background. */
|
|
52
|
+
start(selector: ReviewSelectorV1): Promise<{
|
|
53
|
+
readonly runId: string;
|
|
54
|
+
readonly status: 'running';
|
|
55
|
+
}>;
|
|
56
|
+
/** Cancels a running review; finished or unknown runs are a no-op. */
|
|
57
|
+
cancel(runId: string): void;
|
|
58
|
+
list(): Promise<readonly WebCodeReviewRunRecordV1[]>;
|
|
59
|
+
get(runId: string): Promise<WebCodeReviewRunRecordV1>;
|
|
60
|
+
private execute;
|
|
61
|
+
private evictFinished;
|
|
62
|
+
private present;
|
|
63
|
+
}
|
|
64
|
+
/** v0.3.29 — validates the JSON selector accepted by `POST /review/run`.
|
|
65
|
+
* Mirrors the CLI selector contract: worktree scope, base ref scope, optional
|
|
66
|
+
* path filters. Unknown shapes are typed 400s, never guesses. */
|
|
67
|
+
export declare function parseWebReviewSelectorV1(value: unknown): ReviewSelectorV1;
|
|
68
|
+
//# sourceMappingURL=code-review-run-service.d.ts.map
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.WebCodeReviewRunServiceV1 = void 0;
|
|
4
|
+
exports.parseWebReviewSelectorV1 = parseWebReviewSelectorV1;
|
|
5
|
+
/**
|
|
6
|
+
* v0.3.29 — Host-side shared code-review run service
|
|
7
|
+
* (docs/plan/v0.3.29-plan.md S1). The Web Host starts the same bounded
|
|
8
|
+
* single-pass review runner the CLI uses (v0.3.28 `runCodeReview`) against the
|
|
9
|
+
* active Workspace, keeps a bounded in-memory receipt history, and re-verifies
|
|
10
|
+
* every receipt digest plus repository revision freshness when results are
|
|
11
|
+
* read. No second review engine: receipts, budgets, validation and honest
|
|
12
|
+
* failure states come from the frozen `src/services/code-review` contract.
|
|
13
|
+
*
|
|
14
|
+
* Delivery model: `start()` validates admission and returns immediately; the
|
|
15
|
+
* model completion runs in the background and clients poll `list()`/`get()`.
|
|
16
|
+
* At most one run is in flight per Host (a second start is a typed 409), and
|
|
17
|
+
* a run may be cancelled to release the slot.
|
|
18
|
+
*/
|
|
19
|
+
const crypto_1 = require("crypto");
|
|
20
|
+
const contract_1 = require("../services/code-review/contract");
|
|
21
|
+
const runner_1 = require("../services/code-review/runner");
|
|
22
|
+
const errors_1 = require("./errors");
|
|
23
|
+
class WebCodeReviewRunServiceV1 {
|
|
24
|
+
constructor(options) {
|
|
25
|
+
this.records = new Map();
|
|
26
|
+
this.inflight = new Map();
|
|
27
|
+
this.adapterProvider = options.adapterProvider;
|
|
28
|
+
this.cwdProvider = options.cwdProvider;
|
|
29
|
+
this.repositoryRevisionProvider = options.repositoryRevisionProvider;
|
|
30
|
+
this.runReview = options.runReview ?? runner_1.runCodeReview;
|
|
31
|
+
this.now = options.now ?? Date.now;
|
|
32
|
+
this.maxRuns = Math.max(1, options.maxRuns ?? 20);
|
|
33
|
+
}
|
|
34
|
+
/** Admission is synchronous and fast: validate, record, return. The model
|
|
35
|
+
* completion continues in the background. */
|
|
36
|
+
async start(selector) {
|
|
37
|
+
if (this.inflight.size > 0) {
|
|
38
|
+
throw new errors_1.WebWorkbenchError(409, 'A code review is already running in this Host. Wait for it to finish or cancel it.', 'review_run_in_progress');
|
|
39
|
+
}
|
|
40
|
+
const adapter = this.adapterProvider();
|
|
41
|
+
if (!adapter) {
|
|
42
|
+
throw new errors_1.WebWorkbenchError(422, 'The Host has no configured model. Configure a model first, then retry the review.', 'review_no_model');
|
|
43
|
+
}
|
|
44
|
+
const runId = (0, crypto_1.randomUUID)();
|
|
45
|
+
const repositoryRevisionAtAdmission = await this.repositoryRevisionProvider();
|
|
46
|
+
const startedAt = this.now();
|
|
47
|
+
let capturedFiles = [];
|
|
48
|
+
this.records.set(runId, {
|
|
49
|
+
runId,
|
|
50
|
+
selector,
|
|
51
|
+
status: 'running',
|
|
52
|
+
startedAt,
|
|
53
|
+
endedAt: null,
|
|
54
|
+
repositoryRevisionAtAdmission,
|
|
55
|
+
files: capturedFiles,
|
|
56
|
+
result: null,
|
|
57
|
+
failure: null,
|
|
58
|
+
stale: false,
|
|
59
|
+
});
|
|
60
|
+
const abort = new AbortController();
|
|
61
|
+
this.inflight.set(runId, abort);
|
|
62
|
+
const cwd = this.cwdProvider();
|
|
63
|
+
void this.execute({
|
|
64
|
+
runId,
|
|
65
|
+
selector,
|
|
66
|
+
cwd,
|
|
67
|
+
adapter,
|
|
68
|
+
abort,
|
|
69
|
+
onSnapshot: snapshot => {
|
|
70
|
+
capturedFiles = Object.freeze(snapshot.files.map(file => ({ fileKey: file.fileKey, path: file.path })));
|
|
71
|
+
const record = this.records.get(runId);
|
|
72
|
+
if (record)
|
|
73
|
+
this.records.set(runId, { ...record, files: capturedFiles });
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
return Object.freeze({ runId, status: 'running' });
|
|
77
|
+
}
|
|
78
|
+
/** Cancels a running review; finished or unknown runs are a no-op. */
|
|
79
|
+
cancel(runId) {
|
|
80
|
+
this.inflight.get(runId)?.abort();
|
|
81
|
+
}
|
|
82
|
+
async list() {
|
|
83
|
+
const currentRevision = await this.repositoryRevisionProvider();
|
|
84
|
+
return [...this.records.values()]
|
|
85
|
+
.sort((left, right) => right.startedAt - left.startedAt)
|
|
86
|
+
.map(record => this.present(record, currentRevision));
|
|
87
|
+
}
|
|
88
|
+
async get(runId) {
|
|
89
|
+
const record = this.records.get(runId);
|
|
90
|
+
if (!record) {
|
|
91
|
+
throw new errors_1.WebWorkbenchError(404, 'Unknown review run.', 'review_run_not_found');
|
|
92
|
+
}
|
|
93
|
+
const currentRevision = await this.repositoryRevisionProvider();
|
|
94
|
+
return this.present(record, currentRevision);
|
|
95
|
+
}
|
|
96
|
+
async execute(input) {
|
|
97
|
+
const { runId, selector, cwd, adapter, abort, onSnapshot } = input;
|
|
98
|
+
const options = {
|
|
99
|
+
cwd,
|
|
100
|
+
selector,
|
|
101
|
+
adapter,
|
|
102
|
+
identity: {
|
|
103
|
+
workspaceIdentity: cwd,
|
|
104
|
+
// Web control-plane runs are their own session identity; they are not
|
|
105
|
+
// attributed to any chat Session actor.
|
|
106
|
+
sessionId: `web-host:${runId}`,
|
|
107
|
+
threadId: `web-host:${runId}`,
|
|
108
|
+
turnId: runId,
|
|
109
|
+
},
|
|
110
|
+
abortSignal: abort.signal,
|
|
111
|
+
...(onSnapshot ? { onSnapshotCaptured: onSnapshot } : {}),
|
|
112
|
+
};
|
|
113
|
+
let failure = null;
|
|
114
|
+
let result = null;
|
|
115
|
+
try {
|
|
116
|
+
const outcome = await this.runReview(options);
|
|
117
|
+
if (outcome.ok) {
|
|
118
|
+
result = outcome.result;
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
failure = {
|
|
122
|
+
kind: outcome.failure.kind,
|
|
123
|
+
message: failureMessage(outcome.failure),
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
failure = {
|
|
129
|
+
kind: 'model_error',
|
|
130
|
+
message: error instanceof Error ? error.message : String(error),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
this.inflight.delete(runId);
|
|
134
|
+
const existing = this.records.get(runId);
|
|
135
|
+
if (!existing)
|
|
136
|
+
return;
|
|
137
|
+
const finished = {
|
|
138
|
+
...existing,
|
|
139
|
+
status: result ? result.state : failure?.kind === 'cancelled' ? 'cancelled' : 'failed',
|
|
140
|
+
endedAt: this.now(),
|
|
141
|
+
result,
|
|
142
|
+
failure,
|
|
143
|
+
};
|
|
144
|
+
this.records.set(runId, finished);
|
|
145
|
+
this.evictFinished();
|
|
146
|
+
}
|
|
147
|
+
evictFinished() {
|
|
148
|
+
const finished = [...this.records.values()]
|
|
149
|
+
.filter(record => record.status !== 'running')
|
|
150
|
+
.sort((left, right) => (left.endedAt ?? left.startedAt) - (right.endedAt ?? right.startedAt));
|
|
151
|
+
let excess = this.records.size - this.maxRuns;
|
|
152
|
+
for (const record of finished) {
|
|
153
|
+
if (excess <= 0)
|
|
154
|
+
break;
|
|
155
|
+
this.records.delete(record.runId);
|
|
156
|
+
excess -= 1;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
present(record, currentRevision) {
|
|
160
|
+
const stale = record.result !== null && record.repositoryRevisionAtAdmission !== currentRevision;
|
|
161
|
+
if (record.result && !(0, contract_1.verifyReceiptDigest)(record.result)) {
|
|
162
|
+
// A receipt that no longer verifies must never be presented as findings;
|
|
163
|
+
// surface it as a failed run with an honest message instead.
|
|
164
|
+
return {
|
|
165
|
+
...record,
|
|
166
|
+
status: 'failed',
|
|
167
|
+
stale,
|
|
168
|
+
result: null,
|
|
169
|
+
failure: {
|
|
170
|
+
kind: 'receipt_digest_mismatch',
|
|
171
|
+
message: 'The stored review receipt failed digest re-verification.',
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
return { ...record, stale };
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
exports.WebCodeReviewRunServiceV1 = WebCodeReviewRunServiceV1;
|
|
179
|
+
function failureMessage(failure) {
|
|
180
|
+
switch (failure.kind) {
|
|
181
|
+
case 'cancelled':
|
|
182
|
+
return 'Review cancelled.';
|
|
183
|
+
case 'budget':
|
|
184
|
+
return `Review budget rejected: ${failure.field ?? 'unknown'}`;
|
|
185
|
+
case 'snapshot':
|
|
186
|
+
return `Review scope error (${failure.code ?? 'unknown'}): ${failure.message ?? ''}`;
|
|
187
|
+
case 'model_error':
|
|
188
|
+
return `Review model call failed: ${failure.message ?? ''}`;
|
|
189
|
+
case 'unparsable_output':
|
|
190
|
+
return `Review output could not be parsed: ${failure.message ?? ''}`;
|
|
191
|
+
default:
|
|
192
|
+
return failure.message ?? 'Review failed.';
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
/** v0.3.29 — validates the JSON selector accepted by `POST /review/run`.
|
|
196
|
+
* Mirrors the CLI selector contract: worktree scope, base ref scope, optional
|
|
197
|
+
* path filters. Unknown shapes are typed 400s, never guesses. */
|
|
198
|
+
function parseWebReviewSelectorV1(value) {
|
|
199
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
200
|
+
throw new errors_1.WebWorkbenchError(400, 'Review selector must be an object.', 'review_selector_invalid');
|
|
201
|
+
}
|
|
202
|
+
const record = value;
|
|
203
|
+
const keys = Object.keys(record).sort();
|
|
204
|
+
const allowed = record.kind === 'base' ? ['kind', 'paths', 'ref'] : ['kind', 'paths'];
|
|
205
|
+
if (keys.some(key => !allowed.includes(key))) {
|
|
206
|
+
throw new errors_1.WebWorkbenchError(400, `Review selector accepts only ${allowed.join(', ')}.`, 'review_selector_invalid');
|
|
207
|
+
}
|
|
208
|
+
const paths = parseSelectorPaths(record.paths);
|
|
209
|
+
if (record.kind === 'base') {
|
|
210
|
+
if (typeof record.ref !== 'string' || record.ref.trim() === '') {
|
|
211
|
+
throw new errors_1.WebWorkbenchError(400, 'Review selector base ref must be a non-empty string.', 'review_selector_invalid');
|
|
212
|
+
}
|
|
213
|
+
return Object.freeze({ kind: 'base', ref: record.ref, ...(paths ? { paths } : {}) });
|
|
214
|
+
}
|
|
215
|
+
if (record.kind === 'worktree') {
|
|
216
|
+
return Object.freeze({ kind: 'worktree', ...(paths ? { paths } : {}) });
|
|
217
|
+
}
|
|
218
|
+
throw new errors_1.WebWorkbenchError(400, 'Review selector kind must be "worktree" or "base".', 'review_selector_invalid');
|
|
219
|
+
}
|
|
220
|
+
function parseSelectorPaths(value) {
|
|
221
|
+
if (value === undefined)
|
|
222
|
+
return undefined;
|
|
223
|
+
if (!Array.isArray(value) || value.some(path => typeof path !== 'string' || path.trim() === '')) {
|
|
224
|
+
throw new errors_1.WebWorkbenchError(400, 'Review selector paths must be an array of non-empty strings.', 'review_selector_invalid');
|
|
225
|
+
}
|
|
226
|
+
return Object.freeze([...value]);
|
|
227
|
+
}
|
|
228
|
+
//# sourceMappingURL=code-review-run-service.js.map
|
package/dist/web/server.js
CHANGED
|
@@ -9,6 +9,7 @@ const version_1 = require("../product/version");
|
|
|
9
9
|
const redaction_1 = require("../services/redaction");
|
|
10
10
|
const protocol_1 = require("./protocol");
|
|
11
11
|
const errors_1 = require("./errors");
|
|
12
|
+
const code_review_run_service_1 = require("./code-review-run-service");
|
|
12
13
|
const cli_options_1 = require("./cli-options");
|
|
13
14
|
const terminal_server_1 = require("./terminal-server");
|
|
14
15
|
const workbench_controller_1 = require("./workbench-controller");
|
|
@@ -885,6 +886,42 @@ async function handleRequest(context) {
|
|
|
885
886
|
sendJson(response, 200, await context.workbench.review(requireContextGuardQuery(url)));
|
|
886
887
|
return;
|
|
887
888
|
}
|
|
889
|
+
// v0.3.29 — shared code-review runs. Start is a fast mutation (admission
|
|
890
|
+
// only; the model completion runs in the background and clients poll the
|
|
891
|
+
// read-only list/get endpoints below).
|
|
892
|
+
if (method === 'POST' && path === '/review/run') {
|
|
893
|
+
assertMutation(request, context.nonce, context.origin);
|
|
894
|
+
const body = requireRecord(await readJson(request), 'Review run request');
|
|
895
|
+
assertOnlyKeys(body, ['requestId', 'selector', 'expectedContextRevision', 'workspaceId']);
|
|
896
|
+
const requestId = requireUuid(body.requestId, 'requestId');
|
|
897
|
+
const contextGuard = requireContextGuardRecord(body);
|
|
898
|
+
const selector = (0, code_review_run_service_1.parseWebReviewSelectorV1)(body.selector);
|
|
899
|
+
const result = await context.workbench.executeMutation(requestId, 'review.run', { selector, ...contextGuard }, () => context.workbench.reviewRunStart(selector, contextGuard));
|
|
900
|
+
sendJson(response, 200, result);
|
|
901
|
+
return;
|
|
902
|
+
}
|
|
903
|
+
if (method === 'GET' && path === '/review/runs') {
|
|
904
|
+
sendJson(response, 200, await context.workbench.reviewRunList(requireContextGuardQuery(url)));
|
|
905
|
+
return;
|
|
906
|
+
}
|
|
907
|
+
const reviewRunMatch = path.match(/^\/review\/runs\/([^/]+)$/);
|
|
908
|
+
if (method === 'GET' && reviewRunMatch) {
|
|
909
|
+
const runId = requireUuid(safeDecodePathSegment(reviewRunMatch[1]), 'runId');
|
|
910
|
+
sendJson(response, 200, await context.workbench.reviewRunGet(runId, requireContextGuardQuery(url)));
|
|
911
|
+
return;
|
|
912
|
+
}
|
|
913
|
+
const reviewRunCancelMatch = path.match(/^\/review\/runs\/([^/]+)\/cancel$/);
|
|
914
|
+
if (method === 'POST' && reviewRunCancelMatch) {
|
|
915
|
+
assertMutation(request, context.nonce, context.origin);
|
|
916
|
+
const body = requireRecord(await readJson(request), 'Review cancel request');
|
|
917
|
+
assertOnlyKeys(body, ['requestId', 'expectedContextRevision', 'workspaceId']);
|
|
918
|
+
const requestId = requireUuid(body.requestId, 'requestId');
|
|
919
|
+
const contextGuard = requireContextGuardRecord(body);
|
|
920
|
+
const runId = requireUuid(safeDecodePathSegment(reviewRunCancelMatch[1]), 'runId');
|
|
921
|
+
const result = await context.workbench.executeMutation(requestId, 'review.run.cancel', { runId, ...contextGuard }, () => context.workbench.reviewRunCancel(runId, contextGuard));
|
|
922
|
+
sendJson(response, 200, result);
|
|
923
|
+
return;
|
|
924
|
+
}
|
|
888
925
|
if (method === 'GET' && path === '/terminals') {
|
|
889
926
|
const contextGuard = requireContextGuardQuery(url);
|
|
890
927
|
sendJson(response, 200, collectionPage(url, 'terminals', context.workbench.listTerminals(contextGuard), terminal => terminal.id));
|
|
@@ -10,6 +10,7 @@ import { type WebComposerActionResultV1, type WebComposerActionV1, type WebCompo
|
|
|
10
10
|
import { type NativeDirectoryPickResult, type NativeDirectoryPicker } from './native-directory-picker';
|
|
11
11
|
import { WorkspaceDiscoveryTelemetryV1 } from './workspace-discovery';
|
|
12
12
|
import { WorkspaceRootsRegistryV1 } from '../services/workspace-roots-registry';
|
|
13
|
+
import type { ReviewSelectorV1 } from '../services/code-review/contract';
|
|
13
14
|
import { type WebSessionRuntimeSummaryV1 } from './session-runtime-registry';
|
|
14
15
|
import { TerminalManagerV1 } from './terminal-manager';
|
|
15
16
|
export { WebWorkbenchError } from './errors';
|
|
@@ -70,6 +71,8 @@ export declare class WebWorkbenchController {
|
|
|
70
71
|
private fileWriteService;
|
|
71
72
|
private gitService;
|
|
72
73
|
private reviewService;
|
|
74
|
+
/** v0.3.29 — shared code-review run history for the Review panel. */
|
|
75
|
+
private readonly codeReviewRuns;
|
|
73
76
|
private contextRevisionValue;
|
|
74
77
|
private composerRevisionValue;
|
|
75
78
|
private composerAuthorityDigest;
|
|
@@ -281,6 +284,19 @@ export declare class WebWorkbenchController {
|
|
|
281
284
|
gitLog(context: WebContextGuardV1, input: Parameters<GitReadModelServiceV1['log']>[0]): Promise<import("./git-read-model-service").WebGitLogPageV1>;
|
|
282
285
|
gitDiff(context: WebContextGuardV1, input: Parameters<GitReadModelServiceV1['diff']>[0]): Promise<import("./git-read-model-service").WebGitDiffPageV1>;
|
|
283
286
|
review(context: WebContextGuardV1): Promise<import("./review-service").WebReviewSnapshotV1>;
|
|
287
|
+
/** v0.3.29 — starts a shared code review run (fast admission, background run). */
|
|
288
|
+
reviewRunStart(selector: ReviewSelectorV1, context: WebContextGuardV1): Promise<{
|
|
289
|
+
readonly runId: string;
|
|
290
|
+
readonly status: 'running';
|
|
291
|
+
}>;
|
|
292
|
+
/** v0.3.29 — bounded list of review runs with freshness flags. */
|
|
293
|
+
reviewRunList(context: WebContextGuardV1): Promise<readonly import("./code-review-run-service").WebCodeReviewRunRecordV1[]>;
|
|
294
|
+
/** v0.3.29 — one review run record with digest re-verification. */
|
|
295
|
+
reviewRunGet(runId: string, context: WebContextGuardV1): Promise<import("./code-review-run-service").WebCodeReviewRunRecordV1>;
|
|
296
|
+
/** v0.3.29 — cancels a running review to release the single in-flight slot. */
|
|
297
|
+
reviewRunCancel(runId: string, context: WebContextGuardV1): {
|
|
298
|
+
readonly cancelled: boolean;
|
|
299
|
+
};
|
|
284
300
|
listTerminals(context: WebContextGuardV1): readonly import("./terminal-manager").WebTerminalMetadataV1[];
|
|
285
301
|
createSession(name?: string, context?: WebContextGuardV1): Promise<WebSessionSummaryV1>;
|
|
286
302
|
activateSession(sessionId: string, context?: WebContextGuardV1): Promise<WebCommandResultV1>;
|