@anureo/code-review 0.0.0-stage → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/LICENSE +201 -0
  2. package/bin/anureo-review.d.ts +2 -0
  3. package/bin/anureo-review.js +3 -0
  4. package/package.json +43 -4
  5. package/src/angles.d.ts +19 -0
  6. package/src/angles.js +217 -0
  7. package/src/cli.d.ts +27 -0
  8. package/src/cli.js +157 -0
  9. package/src/client.d.ts +18 -0
  10. package/src/client.js +54 -0
  11. package/src/errors.d.ts +27 -0
  12. package/src/errors.js +44 -0
  13. package/src/file-state.d.ts +13 -0
  14. package/src/file-state.js +24 -0
  15. package/src/fix-patch.d.ts +13 -0
  16. package/src/fix-patch.js +86 -0
  17. package/src/fix-tree.d.ts +25 -0
  18. package/src/fix-tree.js +43 -0
  19. package/src/fix.d.ts +37 -0
  20. package/src/fix.js +110 -0
  21. package/src/git-state.d.ts +9 -0
  22. package/src/git-state.js +23 -0
  23. package/src/git.d.ts +15 -0
  24. package/src/git.js +36 -0
  25. package/src/index.d.ts +36 -0
  26. package/src/index.js +30 -0
  27. package/src/options.d.ts +61 -0
  28. package/src/options.js +56 -0
  29. package/src/paths.d.ts +22 -0
  30. package/src/paths.js +46 -0
  31. package/src/progress.d.ts +30 -0
  32. package/src/progress.js +0 -0
  33. package/src/prompts.d.ts +42 -0
  34. package/src/prompts.js +104 -0
  35. package/src/report.d.ts +5 -0
  36. package/src/report.js +80 -0
  37. package/src/results.d.ts +69 -0
  38. package/src/results.js +0 -0
  39. package/src/review-execution.d.ts +52 -0
  40. package/src/review-execution.js +81 -0
  41. package/src/review-report.d.ts +21 -0
  42. package/src/review-report.js +30 -0
  43. package/src/review.d.ts +16 -0
  44. package/src/review.js +184 -0
  45. package/src/schema.d.ts +77 -0
  46. package/src/schema.js +47 -0
  47. package/src/validation.d.ts +21 -0
  48. package/src/validation.js +105 -0
  49. package/src/worktree.d.ts +23 -0
  50. package/src/worktree.js +117 -0
  51. package/README.md +0 -3
@@ -0,0 +1,5 @@
1
+ import type { ReviewReport } from "./results.js";
2
+ /** Render a ReviewReport as text with a trailing newline. Sections follow the report's module and
3
+ * angle order and retain failed coverage, fix outcomes and observer errors. The caller owns output.
4
+ * The CLI's reviewAndPrintReport writes this text to stdout when JSON output is disabled. */
5
+ export declare function formatReport(report: ReviewReport): string;
package/src/report.js ADDED
@@ -0,0 +1,80 @@
1
+ /** Render a ReviewReport as text with a trailing newline. Sections follow the report's module and
2
+ * angle order and retain failed coverage, fix outcomes and observer errors. The caller owns output.
3
+ * The CLI's reviewAndPrintReport writes this text to stdout when JSON output is disabled. */
4
+ export function formatReport(report) {
5
+ const lines = [
6
+ `Code review: ${report.status}`,
7
+ `Run: ${report.runId}`,
8
+ `Workflow storage: ${report.workflowDirectory}`,
9
+ `Directory: ${report.cwd}`,
10
+ `Scope: ${report.scope.moduleIds.length} of ${report.plan.modules.length} planned modules; angles: ${report.scope.angleIds.join(', ')}`,
11
+ '', report.plan.summary,
12
+ ];
13
+ for (const outcome of report.modules) {
14
+ appendModuleOutcome(lines, outcome);
15
+ }
16
+ if (report.progressErrors.length > 0) {
17
+ lines.push('', '## Progress reporting errors');
18
+ for (const error of report.progressErrors) {
19
+ appendFailure(lines, { error, prefix: '- ' });
20
+ }
21
+ }
22
+ return `${lines.join('\n')}\n`;
23
+ }
24
+ /** Keep the original review, fix and final verification together under the module heading. */
25
+ function appendModuleOutcome(lines, outcome) {
26
+ const { review } = outcome;
27
+ lines.push('', `## ${review.module.name} (${review.module.id})`);
28
+ appendAngleReviews(lines, review.angles, '###');
29
+ if (outcome.fix)
30
+ appendFix(lines, outcome.fix);
31
+ if (outcome.verification) {
32
+ lines.push('', '### Review after fixes');
33
+ appendAngleReviews(lines, outcome.verification.angles, '####');
34
+ }
35
+ }
36
+ /** Render angle outcomes in report order with caller-selected heading depth, retaining failures. */
37
+ function appendAngleReviews(lines, angles, headingPrefix) {
38
+ for (const result of angles) {
39
+ lines.push('', `${headingPrefix} ${result.angle.title}`);
40
+ appendAngleResult(lines, result);
41
+ }
42
+ }
43
+ /** Distinguish failed coverage from a completed angle with no findings. Indent continuation
44
+ * lines so multiline finding bodies and suggestions stay under their finding. */
45
+ function appendAngleResult(lines, result) {
46
+ if (result.status === 'failed') {
47
+ appendFailure(lines, { error: result.error, prefix: 'Review failed: ',
48
+ unknownOutcomeMessage: 'Agent execution outcome is unknown.' });
49
+ return;
50
+ }
51
+ if (result.findings.length === 0) {
52
+ lines.push('No findings.');
53
+ return;
54
+ }
55
+ const indent = (text) => text.replace(/\n/g, '\n ');
56
+ for (const finding of result.findings) {
57
+ lines.push(`- [${finding.severity}] ${finding.title} (${finding.file}:${finding.lineStart}-${finding.lineEnd})`, ` ${indent(finding.body)}`, ` Suggestion: ${indent(finding.suggestion)}`);
58
+ }
59
+ }
60
+ /** Show recorded edits even if a later failure made the fix incomplete. */
61
+ function appendFix(lines, fix) {
62
+ lines.push('', `### Fix: ${fix.status}`, fix.summary);
63
+ if (fix.changedFiles.length > 0)
64
+ lines.push(`Changed files: ${fix.changedFiles.join(', ')}`);
65
+ if (fix.error) {
66
+ appendFailure(lines, { error: fix.error, prefix: 'Fix failed: ',
67
+ unknownOutcomeMessage: 'The backend could not confirm the fix agent outcome.' });
68
+ }
69
+ }
70
+ /** Append a failure's message, optional code and validation issues in that order. Review and fix
71
+ * sections supply their own uncertainty notices. The progress section leaves the notice unset. */
72
+ function appendFailure(lines, input) {
73
+ const { error, prefix, unknownOutcomeMessage } = input;
74
+ lines.push(`${prefix}${error.message}${error.code ? ` [${error.code}]` : ''}`);
75
+ for (const issue of error.issues ?? []) {
76
+ lines.push(`Validation issue: ${JSON.stringify(issue.path)} [${issue.code}]`);
77
+ }
78
+ if (error.outcomeUnknown && unknownOutcomeMessage)
79
+ lines.push(unknownOutcomeMessage);
80
+ }
@@ -0,0 +1,69 @@
1
+ import type { ReviewAngle } from "./angles.js";
2
+ import type { ReviewFailure } from "./errors.js";
3
+ import type { Finding, ModulePlan, ReviewModule } from "./schema.js";
4
+ /** Check whether a fix wrote validated changes to the original work tree and inspect any failure. */
5
+ export interface FixReport {
6
+ /** 'not-needed' means no findings in a complete review. 'skipped' covers incomplete reviews,
7
+ * a prior fix failure or no observed edits. 'applied' means validated edits reached the original
8
+ * work tree and cleanup finished. Check verification for remaining findings. */
9
+ status: 'not-needed' | 'skipped' | 'applied' | 'failed';
10
+ /** Agent summary when reconciliation produced an outcome, or a host explanation. */
11
+ summary: string;
12
+ /** Sorted paths relative to the review directory, applied to the original work tree.
13
+ * These can be present if failure occurred after integration, such as during cleanup. */
14
+ changedFiles: string[];
15
+ /** Normalized failure for 'failed'; null for other statuses. */
16
+ error: ReviewFailure | null;
17
+ }
18
+ /** Inspect an angle's validated findings or the failure that left its coverage incomplete. */
19
+ export type AngleReview = {
20
+ angle: ReviewAngle;
21
+ } & ({
22
+ status: 'completed';
23
+ findings: Finding[];
24
+ } | {
25
+ status: 'failed';
26
+ error: ReviewFailure;
27
+ });
28
+ /** Read one module's assignment and outcomes in selected angle order, including failed slots. */
29
+ export interface ModuleReview {
30
+ module: ReviewModule;
31
+ angles: AngleReview[];
32
+ }
33
+ /** Compare a module's original review, queued fix, and verification of the final work tree. */
34
+ export interface ModuleOutcome {
35
+ review: ModuleReview;
36
+ /** Null when no fix was requested or the lifecycle task returned no outcome. */
37
+ fix: FixReport | null;
38
+ /** Review after all queued fixes settle, including the effects of edits from other modules.
39
+ * Null if no fix outcome recorded changed files. A fix that fails after integration can
40
+ * still trigger verification. */
41
+ verification: ModuleReview | null;
42
+ }
43
+ /** Returned after review, requested fixes and any final verification settle. Module order follows
44
+ * the selected plan; callers inspect status and failures before treating findingCount as complete. */
45
+ export interface ReviewReport {
46
+ /** Durable workflow identity; records are retained beneath workflowDirectory/runId. */
47
+ runId: string;
48
+ /** Absolute workflow storage root, which can be outside the source directory. */
49
+ workflowDirectory: string;
50
+ /** Canonical absolute directory reviewed by the agents. */
51
+ cwd: string;
52
+ /** 'incomplete' when any recorded angle or fix failed, or the workflow reports incomplete
53
+ * execution. A 'completed' report can still contain findings or skipped fixes. */
54
+ status: 'completed' | 'incomplete';
55
+ /** Full validated analysis plan, including modules omitted by selection or limits. */
56
+ plan: ModulePlan;
57
+ /** Reviewed module IDs in plan order and selected angle IDs in caller/default order. */
58
+ scope: {
59
+ moduleIds: string[];
60
+ angleIds: string[];
61
+ };
62
+ modules: ModuleOutcome[];
63
+ /** Synchronous exceptions from onProgress, in delivery order. These diagnostics do not affect status. */
64
+ progressErrors: ReviewFailure[];
65
+ /** Sum of findings from completed angles, using each module's verification when present and its
66
+ * original review otherwise. Failed angles contribute zero. The CLI uses this count to choose
67
+ * exit code 0 or 1 for a completed report. */
68
+ findingCount: number;
69
+ }
package/src/results.js ADDED
File without changes
@@ -0,0 +1,52 @@
1
+ import { type Client } from '@anureo/sdk';
2
+ import type { ReviewAngle } from "./angles.js";
3
+ import type { ReviewProgress } from "./progress.js";
4
+ import type { ModuleReview } from "./results.js";
5
+ import { type ReviewModule } from "./schema.js";
6
+ /** Prepared run settings shared by analysis, angle execution and fix scheduling.
7
+ * The client remains borrowed. reviewCode supplies an observer that collects synchronous exceptions. */
8
+ export interface ReviewExecutionOptions {
9
+ client: Client;
10
+ /** Canonical absolute source directory established by prepareReview. */
11
+ cwd: string;
12
+ /** Git root used to isolate agents in fix mode. Omission runs agents in cwd. */
13
+ repositoryRoot?: string;
14
+ /** Absolute workflow storage root excluded from source validation. */
15
+ directory: string;
16
+ model?: string;
17
+ focus?: string;
18
+ /** Git metadata fingerprint embedded in prompts. Null omits provenance. Sources can change after sampling. */
19
+ sourceVersion: string | null;
20
+ angles: readonly ReviewAngle[];
21
+ concurrency: number;
22
+ onProgress?: (event: ReviewProgress) => void;
23
+ }
24
+ /** Phase used by angle prompts and progress events. Verification starts after the writer queue drains. */
25
+ export type ReviewPhase = 'review' | 'verification';
26
+ /** Await work under the run permit pool, propagating its value or rejection. The callback
27
+ * owns its resources and should finish cleanup before settling so the permit covers that work. */
28
+ export type Schedule = <T>(execute: () => Promise<T>) => Promise<T>;
29
+ /** Dispatch a module's angles with the run's shared capacity. Initial review takes a planned
30
+ * module. Verification takes its original review, whose completed findings are matched by angle ID. */
31
+ export type ModuleReviewInput = {
32
+ options: ReviewExecutionOptions;
33
+ schedule: Schedule;
34
+ } & ({
35
+ phase: 'review';
36
+ module: ReviewModule;
37
+ } | {
38
+ phase: 'verification';
39
+ previous: ModuleReview;
40
+ });
41
+ /** Create one permit pool for reviewCode's prevalidated positive concurrency limit.
42
+ * reviewModule uses it for individual angles and runModuleFix uses it for the queue head.
43
+ * Each settled operation hands its permit to the oldest waiter or returns it to the pool. */
44
+ export declare function createReviewSchedule(limit: number): Schedule;
45
+ /** Return a module's coverage in angle order after every scheduled angle settles.
46
+ * reviewModuleLifecycle passes this coverage to the optional fix queue. verifyFinalTree stores
47
+ * it for the final report. Rejected tasks become failed slots. Fatal agent errors are also
48
+ * recorded by workflow and can cause reviewCode to reject. */
49
+ export declare function reviewModule(input: ModuleReviewInput): Promise<ModuleReview>;
50
+ /** Supply failed angle slots in configured order when reviewModules or verifyFinalTree has no
51
+ * result for a module. Each caller stores the fallback in its corresponding report slot. */
52
+ export declare function failedModuleReview(angles: readonly ReviewAngle[], module: ReviewModule, message: string): ModuleReview;
@@ -0,0 +1,81 @@
1
+ import { HarnessSdkError } from '@anureo/sdk';
2
+ import { agent, parallel } from '@anureo/workflow';
3
+ import { reviewFailure } from "./errors.js";
4
+ import { buildReviewPrompt } from "./prompts.js";
5
+ import { reviewOutputSchema } from "./schema.js";
6
+ import { validateFindings, validateOutput } from "./validation.js";
7
+ import { withAgentWorktree } from "./worktree.js";
8
+ /** Create one permit pool for reviewCode's prevalidated positive concurrency limit.
9
+ * reviewModule uses it for individual angles and runModuleFix uses it for the queue head.
10
+ * Each settled operation hands its permit to the oldest waiter or returns it to the pool. */
11
+ export function createReviewSchedule(limit) {
12
+ let available = limit;
13
+ const waiting = [];
14
+ return async (execute) => {
15
+ if (available === 0)
16
+ await new Promise(resume => waiting.push(resume));
17
+ else
18
+ available--;
19
+ try {
20
+ return await execute();
21
+ }
22
+ finally {
23
+ const resume = waiting.shift();
24
+ if (resume)
25
+ resume();
26
+ else
27
+ available++;
28
+ }
29
+ };
30
+ }
31
+ /** Return a module's coverage in angle order after every scheduled angle settles.
32
+ * reviewModuleLifecycle passes this coverage to the optional fix queue. verifyFinalTree stores
33
+ * it for the final report. Rejected tasks become failed slots. Fatal agent errors are also
34
+ * recorded by workflow and can cause reviewCode to reject. */
35
+ export async function reviewModule(input) {
36
+ const { options, phase, schedule } = input;
37
+ const module = phase === 'review' ? input.module : input.previous.module;
38
+ const previousAngles = phase === 'verification' ? input.previous.angles : undefined;
39
+ const results = await parallel(options.angles.map(angle => () => schedule(() => reviewAngle({
40
+ options, module, angle, phase,
41
+ original: previousAngles?.find(value => value.angle.id === angle.id),
42
+ }))));
43
+ // parallel preserves positions; retain coverage when a task fails outside its agent call.
44
+ return { module, angles: options.angles.map((angle, index) => results[index] ??
45
+ failedAngleReview(angle, `Angle task for ${angle.id} failed before producing a result`)) };
46
+ }
47
+ /** Supply failed angle slots in configured order when reviewModules or verifyFinalTree has no
48
+ * result for a module. Each caller stores the fallback in its corresponding report slot. */
49
+ export function failedModuleReview(angles, module, message) {
50
+ return { module, angles: angles.map(angle => failedAngleReview(angle, message)) };
51
+ }
52
+ /** Run one angle for reviewModule while it holds a permit. Validate findings in the same source
53
+ * view the agent used and wait for any temporary checkout cleanup. Agent, validation
54
+ * and worktree failures become failed coverage. A failure preparing the prompt or emitting an
55
+ * event rejects the task for reviewModule's positional fallback. */
56
+ async function reviewAngle({ options, module, angle, phase, original }) {
57
+ const progress = { stage: phase, moduleId: module.id, angleId: angle.id };
58
+ options.onProgress?.({ ...progress, status: 'started' });
59
+ const prompt = buildReviewPrompt({ ...options, phase, module, angle, original });
60
+ let result;
61
+ try {
62
+ const findings = await withAgentWorktree({ root: options.repositoryRoot, cwd: options.cwd, directory: options.directory }, async (cwd, directory) => {
63
+ const output = await agent(prompt, {
64
+ client: options.client, cwd, model: options.model, schema: reviewOutputSchema,
65
+ });
66
+ if (output === null)
67
+ throw new HarnessSdkError('REVIEW_AGENT_INCOMPLETE', 'Review agent returned no result');
68
+ return validateOutput(() => validateFindings(cwd, module, output.findings, directory));
69
+ });
70
+ result = { angle, status: 'completed', findings };
71
+ }
72
+ catch (cause) {
73
+ result = { angle, status: 'failed', error: reviewFailure(cause) };
74
+ }
75
+ options.onProgress?.({ ...progress, status: result.status === 'completed' ? 'completed' : 'failed' });
76
+ return result;
77
+ }
78
+ /** Preserve an angle slot when parallel work returned no result, using a normalized local failure. */
79
+ function failedAngleReview(angle, message) {
80
+ return { angle, status: 'failed', error: reviewFailure(new Error(message)) };
81
+ }
@@ -0,0 +1,21 @@
1
+ import type { ReviewFailure } from "./errors.js";
2
+ import type { ModuleOutcome, ReviewReport } from "./results.js";
3
+ import type { ModulePlan, ReviewModule } from "./schema.js";
4
+ /** Build the report from settled outcomes supplied in selected plan order. Count each module's
5
+ * verification when present, otherwise its original review. reviewCode returns this object from
6
+ * the workflow body for persistence, then applies any workflow status downgrade to its library
7
+ * result. This builder retains the supplied outcome and progress arrays by reference. */
8
+ export declare function buildReviewReport(input: {
9
+ runId: string;
10
+ execution: {
11
+ directory: string;
12
+ cwd: string;
13
+ angles: readonly {
14
+ id: string;
15
+ }[];
16
+ };
17
+ plan: ModulePlan;
18
+ selectedModules: readonly ReviewModule[];
19
+ outcomes: ModuleOutcome[];
20
+ progressErrors: ReviewFailure[];
21
+ }): ReviewReport;
@@ -0,0 +1,30 @@
1
+ /** Build the report from settled outcomes supplied in selected plan order. Count each module's
2
+ * verification when present, otherwise its original review. reviewCode returns this object from
3
+ * the workflow body for persistence, then applies any workflow status downgrade to its library
4
+ * result. This builder retains the supplied outcome and progress arrays by reference. */
5
+ export function buildReviewReport(input) {
6
+ const { runId, execution, plan, selectedModules, outcomes, progressErrors } = input;
7
+ const findingCount = outcomes.reduce((count, outcome) => count + countFindings(outcome.verification ?? outcome.review), 0);
8
+ return {
9
+ runId,
10
+ workflowDirectory: execution.directory,
11
+ cwd: execution.cwd,
12
+ plan,
13
+ modules: outcomes,
14
+ progressErrors,
15
+ scope: { moduleIds: selectedModules.map(module => module.id), angleIds: execution.angles.map(angle => angle.id) },
16
+ status: outcomes.every(moduleOutcomeComplete) ? 'completed' : 'incomplete',
17
+ findingCount,
18
+ };
19
+ }
20
+ /** Require completed angle coverage and no failed fix; a skipped fix alone does not fail the outcome. */
21
+ function moduleOutcomeComplete(outcome) {
22
+ const anglesComplete = (review) => review.angles.every(angle => angle.status === 'completed');
23
+ return anglesComplete(outcome.review)
24
+ && (outcome.verification === null || anglesComplete(outcome.verification))
25
+ && outcome.fix?.status !== 'failed';
26
+ }
27
+ /** Count only observed findings; failed angle slots contribute zero without implying complete coverage. */
28
+ function countFindings(review) {
29
+ return review.angles.reduce((count, angle) => count + (angle.status === 'completed' ? angle.findings.length : 0), 0);
30
+ }
@@ -0,0 +1,16 @@
1
+ import { type ReviewOptions } from "./options.js";
2
+ import type { ReviewReport } from "./results.js";
3
+ export type { ReviewFailure } from "./errors.js";
4
+ export { DEFAULT_REVIEW_CONCURRENCY } from "./options.js";
5
+ export type { ReviewOptions } from "./options.js";
6
+ export type { ReviewProgress } from "./progress.js";
7
+ export type { AngleReview, ModuleReview, ModuleOutcome, ReviewReport } from "./results.js";
8
+ /** Analyze source modules, review selected angles and optionally apply fixes to the original
9
+ * work tree. If a fix reports changed files, recheck every selected module after the writers settle.
10
+ * Resolves with a report after workflow publication and lock release. Ordinary angle and fix
11
+ * failures are recorded in an incomplete report. Finding counts come from verification when
12
+ * it runs. Invalid options or module IDs, analysis failures and fatal execution or workflow
13
+ * storage failures reject.
14
+ * Synchronous observer exceptions are collected in the report's progressErrors. Keep the
15
+ * initialized client usable until the promise settles, then dispose it separately. */
16
+ export declare function reviewCode(options: ReviewOptions): Promise<ReviewReport>;
package/src/review.js ADDED
@@ -0,0 +1,184 @@
1
+ // Coordinates analysis, module review, serialized fixes and final verification.
2
+ import { resolve } from 'node:path';
3
+ import { HarnessSdkError } from '@anureo/sdk';
4
+ import { agent, parallel, workflow } from '@anureo/workflow';
5
+ import { reviewFailure } from "./errors.js";
6
+ import { fixFindings } from "./fix.js";
7
+ import { reviewRepositoryRoot } from "./git.js";
8
+ import { readGitStateFingerprint } from "./git-state.js";
9
+ import { validateReviewOptions } from "./options.js";
10
+ import { DEFAULT_WORKFLOW_DIRECTORY, resolveReviewDirectory } from "./paths.js";
11
+ import { buildAnalysisPrompt, readOnlyInstructions } from "./prompts.js";
12
+ import { createReviewSchedule, failedModuleReview, reviewModule } from "./review-execution.js";
13
+ import { buildReviewReport } from "./review-report.js";
14
+ import { modulePlanSchema } from "./schema.js";
15
+ import { validateOutput, validatePlan } from "./validation.js";
16
+ import { withAgentWorktree } from "./worktree.js";
17
+ export { DEFAULT_REVIEW_CONCURRENCY } from "./options.js";
18
+ /** Analyze source modules, review selected angles and optionally apply fixes to the original
19
+ * work tree. If a fix reports changed files, recheck every selected module after the writers settle.
20
+ * Resolves with a report after workflow publication and lock release. Ordinary angle and fix
21
+ * failures are recorded in an incomplete report. Finding counts come from verification when
22
+ * it runs. Invalid options or module IDs, analysis failures and fatal execution or workflow
23
+ * storage failures reject.
24
+ * Synchronous observer exceptions are collected in the report's progressErrors. Keep the
25
+ * initialized client usable until the promise settles, then dispose it separately. */
26
+ export async function reviewCode(options) {
27
+ const progressErrors = [];
28
+ const prepared = await prepareReview({ ...options, onProgress: event => {
29
+ try {
30
+ options.onProgress?.(event);
31
+ }
32
+ catch (cause) {
33
+ progressErrors.push(reviewFailure(cause));
34
+ }
35
+ } });
36
+ const { execution, fixRequested, moduleIds, maxModules } = prepared;
37
+ // Bump revision when the persisted input/checkpoint contract changes.
38
+ // sourceVersion carries the Git state fingerprint; backendConfiguration remains reserved.
39
+ const completed = await workflow({
40
+ name: 'code-review', revision: '8', directory: execution.directory, input: prepared.workflowInput,
41
+ }, async (run) => {
42
+ execution.onProgress?.({ stage: 'workflow', status: 'started', runId: run.runId, directory: execution.directory });
43
+ const plan = await analyzeModules(execution, prepared.analysisPrompt);
44
+ const selectedModules = selectModules({ plan, moduleIds, maxModules });
45
+ const schedule = createReviewSchedule(execution.concurrency);
46
+ const runFix = fixRequested
47
+ ? createFixRunner({ execution, schedule, permissionMode: options.fixPermissionMode })
48
+ : null;
49
+ const outcomes = await reviewModules({ execution, modules: selectedModules, schedule, runFix });
50
+ await verifyFinalTree({ execution, outcomes, schedule });
51
+ return buildReviewReport({ runId: run.runId, execution, plan, selectedModules, outcomes, progressErrors });
52
+ });
53
+ return { ...completed.result, status: completed.status === 'incomplete' ? 'incomplete' : completed.result.status };
54
+ }
55
+ /** Return plan-ordered outcomes to reviewCode after every module lifecycle and queued fix settles.
56
+ * Replace rejected lifecycles with failed coverage so final verification and reporting retain
57
+ * the selected scope. The enclosing workflow still rejects if it recorded a fatal error. */
58
+ async function reviewModules({ execution, modules, schedule, runFix }) {
59
+ const results = await parallel(modules.map(module => () => reviewModuleLifecycle({ execution, module, schedule, runFix })));
60
+ return modules.map((module, index) => results[index] ?? {
61
+ review: failedModuleReview(execution.angles, module, `Module task for ${module.id} failed before producing a result`),
62
+ fix: null, verification: null,
63
+ });
64
+ }
65
+ /** Fill verification slots in place for reviewCode's report once reviewModules has drained the
66
+ * writers. A fix that reports changed files triggers a recheck of all selected modules because
67
+ * edits can affect callers elsewhere. Share one provenance value across the recheck and turn
68
+ * rejected module tasks into failed coverage. */
69
+ async function verifyFinalTree({ execution, outcomes, schedule }) {
70
+ if (!outcomes.some(outcome => outcome.fix && outcome.fix.changedFiles.length > 0))
71
+ return;
72
+ const finalExecution = { ...execution, sourceVersion: await readGitStateFingerprint(execution.cwd) };
73
+ const verifications = await parallel(outcomes.map(outcome => () => reviewModule({
74
+ options: finalExecution, phase: 'verification', schedule, previous: outcome.review,
75
+ })));
76
+ for (const [index, outcome] of outcomes.entries()) {
77
+ outcome.verification = verifications[index] ?? failedModuleReview(execution.angles, outcome.review.module, `Verification task for ${outcome.review.module.id} failed before producing a result`);
78
+ }
79
+ }
80
+ /** Give reviewCode the requested modules in plan order, capped only after ID filtering.
81
+ * Throws for unknown requested IDs before any module review or fix starts. */
82
+ function selectModules({ plan, moduleIds, maxModules }) {
83
+ const available = new Set(plan.modules.map(module => module.id));
84
+ const unknown = moduleIds?.filter(id => !available.has(id)) ?? [];
85
+ if (unknown.length > 0) {
86
+ throw new HarnessSdkError('REVIEW_MODULE_NOT_FOUND', `Unknown module ids: ${unknown.join(', ')}. Available: ${[...available].join(', ')}`);
87
+ }
88
+ const selected = moduleIds ? plan.modules.filter(module => moduleIds.includes(module.id)) : plan.modules;
89
+ return maxModules === undefined ? selected : selected.slice(0, maxModules);
90
+ }
91
+ /** Prepare execution settings and persisted identity inputs for reviewCode. Validate selections,
92
+ * resolve the source directory and require a Git root for fix mode before opening workflow
93
+ * storage or agent sessions. A failed preflight rejects the call. */
94
+ async function prepareReview(options) {
95
+ const selection = validateReviewOptions(options);
96
+ const { fixRequested, concurrency, maxModules, moduleIds, angles } = selection;
97
+ const cwd = await resolveReviewDirectory(options.cwd);
98
+ const repositoryRoot = fixRequested ? await reviewRepositoryRoot(cwd) : undefined;
99
+ const version = await readGitStateFingerprint(cwd);
100
+ const analysisPrompt = buildAnalysisPrompt({ focus: options.focus, sourceVersion: version });
101
+ const directory = resolve(options.workflowDirectory ?? resolve(cwd, DEFAULT_WORKFLOW_DIRECTORY));
102
+ const execution = {
103
+ client: options.client, cwd, directory, model: options.model, focus: options.focus,
104
+ sourceVersion: version, repositoryRoot, angles, concurrency, onProgress: options.onProgress,
105
+ };
106
+ return { ...selection, execution, analysisPrompt, workflowInput: {
107
+ cwd, model: options.model ?? null, focus: options.focus ?? null, concurrency,
108
+ moduleIds: moduleIds ?? null, maxModules: maxModules ?? null, fix: fixRequested,
109
+ angles, instructions: readOnlyInstructions, analysisPrompt,
110
+ sourceVersion: version, backendConfiguration: null,
111
+ } };
112
+ }
113
+ /** Supply reviewCode with the normalized plan for module selection. In fix mode, validate paths
114
+ * first in the agent's checkout, then in the original tree where fixes will be applied.
115
+ * A missing result, failed validation or failed agent/worktree operation rejects before dispatch. */
116
+ async function analyzeModules(execution, prompt) {
117
+ execution.onProgress?.({ stage: 'analysis', status: 'started' });
118
+ const plan = await withAgentWorktree({ root: execution.repositoryRoot, cwd: execution.cwd, directory: execution.directory }, async (cwd, directory) => {
119
+ const analysis = await agent(prompt, {
120
+ client: execution.client, cwd, model: execution.model, schema: modulePlanSchema,
121
+ });
122
+ if (analysis === null)
123
+ throw new HarnessSdkError('REVIEW_AGENT_INCOMPLETE', 'Module analysis failed: agent returned no result');
124
+ const localPlan = await validateOutput(() => validatePlan(cwd, analysis, directory));
125
+ return execution.repositoryRoot
126
+ ? validateOutput(() => validatePlan(execution.cwd, localPlan, execution.directory))
127
+ : localPlan;
128
+ });
129
+ execution.onProgress?.({ stage: 'analysis', status: 'completed' });
130
+ return plan;
131
+ }
132
+ /** Supply reviewModuleLifecycle with one fix queue. Lifecycles enqueue after their reviews
133
+ * complete, so fixes run in enqueue order and only the head competes for an agent permit.
134
+ * A failed fix report or rejection closes the queue to further writes. The current lifecycle
135
+ * receives the original report or rejection, and later lifecycles receive skipped reports. */
136
+ function createFixRunner({ execution, schedule, permissionMode }) {
137
+ let tail = Promise.resolve();
138
+ let stoppedBy;
139
+ return (review) => {
140
+ const result = tail.then(async () => {
141
+ if (stoppedBy !== undefined) {
142
+ execution.onProgress?.({ stage: 'fix', status: 'skipped', moduleId: review.module.id });
143
+ return { status: 'skipped', summary: `Fix skipped because module ${stoppedBy}'s fix failed. Previously applied fixes remain in the original work tree.`,
144
+ changedFiles: [], error: null };
145
+ }
146
+ const fix = await runModuleFix({ execution, schedule, permissionMode, review });
147
+ if (fix.status === 'failed')
148
+ stoppedBy = review.module.id;
149
+ return fix;
150
+ });
151
+ // Unexpected local failures must also stop queued writers. Workflow-level
152
+ // fatal errors retain their original cause in the engine's run state.
153
+ tail = result.then(() => undefined, () => { stoppedBy = review.module.id; });
154
+ return result;
155
+ };
156
+ }
157
+ /** Run the queue head under the shared permit. Capture provenance after acquiring capacity
158
+ * and forward fixFindings' synchronous progress to the run observer. The queue consumes the
159
+ * fix report or rejection after the permit is released. */
160
+ function runModuleFix({ execution, schedule, permissionMode, review }) {
161
+ return schedule(async () => fixFindings({
162
+ sourceVersion: await readGitStateFingerprint(execution.cwd),
163
+ client: execution.client,
164
+ cwd: execution.cwd,
165
+ directory: execution.directory,
166
+ repositoryRoot: execution.repositoryRoot,
167
+ modules: [review],
168
+ model: execution.model,
169
+ ...(permissionMode !== undefined ? { permissionMode } : {}),
170
+ onProgress: status => execution.onProgress?.({ stage: 'fix', status, moduleId: review.module.id }),
171
+ }));
172
+ }
173
+ /** Return one initial review and its optional queued fix to reviewModules. The review's
174
+ * provenance is sampled at module dispatch, which may overlap fixes for other modules.
175
+ * Await the queued fix so reviewModules can drain writers and replace any rejected lifecycle
176
+ * with failed coverage before final verification. */
177
+ async function reviewModuleLifecycle(options) {
178
+ const { execution, module, schedule, runFix } = options;
179
+ const review = await reviewModule({
180
+ options: { ...execution, sourceVersion: await readGitStateFingerprint(execution.cwd) }, module, phase: 'review', schedule,
181
+ });
182
+ const fix = runFix ? await runFix(review) : null;
183
+ return { review, fix, verification: null };
184
+ }
@@ -0,0 +1,77 @@
1
+ import { z } from 'zod';
2
+ /** Read a source assignment from ReviewReport.plan or a module outcome. In returned reports,
3
+ * files are canonical, slash-separated paths relative to the review directory, with one owner
4
+ * per plan. The exact id identifies the module for selection, progress events and reports. */
5
+ export type ReviewModule = z.infer<typeof moduleSchema>;
6
+ /** Read the full validated analysis plan, including assignments omitted by review selection.
7
+ * Every listed file has one owner. The plan may omit reviewable source files. */
8
+ export type ModulePlan = z.infer<typeof modulePlanSchema>;
9
+ /** Read an issue from a completed angle. The canonical file path is relative to the review
10
+ * directory, and the 1-based inclusive range was checked against source at validation time.
11
+ * The body and suggested remedy are agent assessments. */
12
+ export type Finding = z.infer<typeof findingSchema>;
13
+ /** Internal structured response from the fix agent. applyFix takes the summary from the
14
+ * response and determines the report's changed files through Git reconciliation. */
15
+ export type FixResult = z.infer<typeof fixResultSchema>;
16
+ /** Internal structural schema for one analysis assignment; validatePlan checks source scope,
17
+ * canonical ownership and ID limits after structured output parsing. Unknown fields are rejected. */
18
+ export declare const moduleSchema: z.ZodObject<{
19
+ id: z.ZodString;
20
+ name: z.ZodString;
21
+ description: z.ZodString;
22
+ files: z.ZodArray<z.ZodString>;
23
+ }, z.core.$strict>;
24
+ /** Internal structured output contract submitted by analyzeModules; requires a nonempty plan
25
+ * and rejects unknown fields. Filesystem validity and ownership are checked by validatePlan. */
26
+ export declare const modulePlanSchema: z.ZodObject<{
27
+ summary: z.ZodString;
28
+ modules: z.ZodArray<z.ZodObject<{
29
+ id: z.ZodString;
30
+ name: z.ZodString;
31
+ description: z.ZodString;
32
+ files: z.ZodArray<z.ZodString>;
33
+ }, z.core.$strict>>;
34
+ }, z.core.$strict>;
35
+ /** Severity values accepted for Finding.severity.
36
+ * The findings schema and review prompt both derive their vocabulary from this list. */
37
+ export declare const findingSeverities: readonly ["P0", "P1", "P2", "P3"];
38
+ /** Internal finding shape used by reviewOutputSchema. Rejects unknown fields; validateFindings
39
+ * separately checks canonical module ownership and inclusive ranges against current source text. */
40
+ export declare const findingSchema: z.ZodObject<{
41
+ severity: z.ZodEnum<{
42
+ P0: "P0";
43
+ P1: "P1";
44
+ P2: "P2";
45
+ P3: "P3";
46
+ }>;
47
+ title: z.ZodString;
48
+ body: z.ZodString;
49
+ suggestion: z.ZodString;
50
+ file: z.ZodString;
51
+ lineStart: z.ZodNumber;
52
+ lineEnd: z.ZodNumber;
53
+ }, z.core.$strict>;
54
+ /** Internal output schema supplied by reviewAngle for review and verification agents.
55
+ * An empty findings array is valid. validateFindings checks source locations after parsing. */
56
+ export declare const reviewOutputSchema: z.ZodObject<{
57
+ findings: z.ZodArray<z.ZodObject<{
58
+ severity: z.ZodEnum<{
59
+ P0: "P0";
60
+ P1: "P1";
61
+ P2: "P2";
62
+ P3: "P3";
63
+ }>;
64
+ title: z.ZodString;
65
+ body: z.ZodString;
66
+ suggestion: z.ZodString;
67
+ file: z.ZodString;
68
+ lineStart: z.ZodNumber;
69
+ lineEnd: z.ZodNumber;
70
+ }, z.core.$strict>>;
71
+ }, z.core.$strict>;
72
+ /** Internal output schema supplied by applyFix for the agent's result after editing sources.
73
+ * applyFix checks Git state and observed edits before applying a patch to the original tree. */
74
+ export declare const fixResultSchema: z.ZodObject<{
75
+ summary: z.ZodString;
76
+ changedFiles: z.ZodArray<z.ZodString>;
77
+ }, z.core.$strict>;