@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
package/src/cli.js ADDED
@@ -0,0 +1,157 @@
1
+ import { parseArgs } from 'node:util';
2
+ import { defaultAngles } from "./angles.js";
3
+ import { createReviewClient, fixPermissionMode, reviewHarnesses } from "./client.js";
4
+ import { formatFailure } from "./errors.js";
5
+ import { formatReport } from "./report.js";
6
+ import { DEFAULT_WORKFLOW_DIRECTORY, resolveReviewDirectory } from "./paths.js";
7
+ import { reviewCode } from "./review.js";
8
+ import { DEFAULT_REVIEW_CONCURRENCY, validatePositiveInteger, validateModuleIds, hasDuplicateIds } from "./options.js";
9
+ /** Run the CLI with arguments following the executable name. The bin entry point awaits the result
10
+ * and assigns it to process.exitCode. Returns 0 for help or a complete review without findings,
11
+ * 1 for a complete review with findings, and 2 for incomplete coverage or reported errors.
12
+ * Help and CLI parsing failures acquire no backend. When a report is available, it is written
13
+ * before client disposal. The command awaits disposal before settling once a client is acquired.
14
+ * A failure to emit the final diagnostic can reject.
15
+ */
16
+ export async function runCli(args, dependencies = {}) {
17
+ const runtime = {
18
+ ...dependencies,
19
+ env: dependencies.env ?? process.env,
20
+ stdout: dependencies.stdout ?? (text => { process.stdout.write(text); }),
21
+ stderr: dependencies.stderr ?? (text => { process.stderr.write(text); }),
22
+ };
23
+ try {
24
+ const command = parseCommand(args);
25
+ if (command.kind === 'help') {
26
+ runtime.stdout(help);
27
+ return 0;
28
+ }
29
+ return await executeReviewCommand(command, runtime);
30
+ }
31
+ catch (cause) {
32
+ runtime.stderr(`anureo-review: ${formatFailure(cause)}\n`);
33
+ return 2;
34
+ }
35
+ }
36
+ const help = `Usage: anureo-review [directory] [options]
37
+
38
+ Analyze modules, then review modules and their angles concurrently.
39
+ Defaults to the current directory and all built-in angles.
40
+ Requires Bun and configured authentication for the selected harness.
41
+ Pi runs in-process; CLI-based harnesses require their corresponding executable.
42
+
43
+ Options:
44
+ --harness <id> Review backend (default: codex): ${reviewHarnesses.join(', ')}
45
+ --model <name> Model override (defaults to CODEX_MODEL or backend default)
46
+ --concurrency <number> Maximum simultaneous review and fix agents (default: ${DEFAULT_REVIEW_CONCURRENCY})
47
+ --max-modules <number> Review at most this many modules, in plan order
48
+ --module <id> Select an analyzed module ID (repeatable)
49
+ --angle <id> Select a built-in angle (repeatable): ${defaultAngles.map(angle => angle.id).join(', ')}
50
+ --focus <text> Additional focus for module analysis and review
51
+ --fix Run each agent in a temporary Git worktree, apply
52
+ validated edits, then re-review the selected scope.
53
+ Existing uncommitted changes are included.
54
+ --workflow-dir <path> Durable run storage (default: <directory>/${DEFAULT_WORKFLOW_DIRECTORY})
55
+ --json Print the structured report as JSON
56
+ --help, -h Show this help
57
+
58
+ Exit codes: 0 complete without findings, 1 complete with findings, 2 failed/incomplete.
59
+ `;
60
+ /** Parse CLI syntax and validate flags that can be checked before runCli acquires a client.
61
+ * Throws flag-specific errors. Help bypasses semantic checks after strict syntax parsing.
62
+ * reviewCode checks module selection against the analyzed plan. */
63
+ function parseCommand(args) {
64
+ const { values, positionals } = parseArgs({ args, allowPositionals: true, strict: true, options: {
65
+ help: { type: 'boolean', short: 'h' }, json: { type: 'boolean' }, fix: { type: 'boolean' },
66
+ model: { type: 'string' }, concurrency: { type: 'string' }, focus: { type: 'string' },
67
+ harness: { type: 'string' },
68
+ 'workflow-dir': { type: 'string' },
69
+ 'max-modules': { type: 'string' },
70
+ module: { type: 'string', multiple: true }, angle: { type: 'string', multiple: true },
71
+ } });
72
+ if (values.help)
73
+ return { kind: 'help' };
74
+ if (positionals.length > 1)
75
+ throw new Error('Expected at most one review directory');
76
+ const harness = values.harness === undefined ? 'codex' : reviewHarnesses.find(id => id === values.harness);
77
+ if (!harness)
78
+ throw new Error(`Unknown harness: ${values.harness}. Available: ${reviewHarnesses.join(', ')}`);
79
+ const concurrency = values.concurrency === undefined ? DEFAULT_REVIEW_CONCURRENCY : Number(values.concurrency);
80
+ validatePositiveInteger(concurrency, '--concurrency');
81
+ const maxModules = values['max-modules'] === undefined ? undefined : Number(values['max-modules']);
82
+ if (maxModules !== undefined)
83
+ validatePositiveInteger(maxModules, '--max-modules');
84
+ validateModuleIds(values.module, '--module requires unique nonempty module ids');
85
+ const angles = resolveSelectedAngles(values.angle);
86
+ return {
87
+ kind: 'review', directory: positionals[0], harness, concurrency, maxModules, angles,
88
+ moduleIds: values.module, focus: values.focus, model: values.model,
89
+ fix: values.fix, json: values.json, workflowDirectory: values['workflow-dir'],
90
+ };
91
+ }
92
+ /** Resolve built-in angle IDs in flag order for parseCommand. Omission lets reviewCode pick its
93
+ * defaults. Duplicate flags take precedence over unknown IDs in the diagnostic. */
94
+ function resolveSelectedAngles(ids) {
95
+ if (ids && hasDuplicateIds(ids))
96
+ throw new Error('--angle requires unique angle ids');
97
+ return ids?.map(id => {
98
+ const angle = defaultAngles.find(angle => angle.id === id);
99
+ if (!angle)
100
+ throw new Error(`Unknown angle: ${id}. Available: ${defaultAngles.map(angle => angle.id).join(', ')}`);
101
+ return angle;
102
+ });
103
+ }
104
+ /** Acquire a client only after directory preflight and retain ownership through report output.
105
+ * runCli receives failures after disposal settles: a cleanup failure rejects an otherwise successful
106
+ * execution, while a review/output failure remains primary if the separate cleanup diagnostic succeeds.
107
+ * A throwing cleanup diagnostic can replace the original failure. */
108
+ async function executeReviewCommand(command, runtime) {
109
+ const cwd = await resolveReviewDirectory(command.directory ?? runtime.cwd ?? process.cwd());
110
+ const client = await (runtime.createClient ?? createReviewClient)(cwd, command.harness);
111
+ let executionFailed = false;
112
+ try {
113
+ return await reviewAndPrintReport({ command, runtime, cwd, client });
114
+ }
115
+ catch (cause) {
116
+ executionFailed = true;
117
+ throw cause;
118
+ }
119
+ finally {
120
+ try {
121
+ await client.dispose();
122
+ }
123
+ catch (cause) {
124
+ if (!executionFailed)
125
+ throw cause;
126
+ runtime.stderr(`anureo-review: client cleanup failed: ${formatFailure(cause)}\n`);
127
+ }
128
+ }
129
+ }
130
+ /** Await reviewCode, synchronously print its report and derive the CLI exit code. Incomplete
131
+ * coverage takes precedence over finding count, which can include post-fix verification.
132
+ * Review rejections and output throws propagate to executeReviewCommand, which owns client cleanup.
133
+ * reviewCode invokes the progress observer synchronously and collects its exceptions in the report. */
134
+ async function reviewAndPrintReport({ command, runtime, cwd, client }) {
135
+ const report = await reviewCode({
136
+ client, cwd, concurrency: command.concurrency, focus: command.focus, model: command.model ?? runtime.env.CODEX_MODEL,
137
+ angles: command.angles, moduleIds: command.moduleIds, maxModules: command.maxModules, fix: command.fix,
138
+ ...(command.fix ? { fixPermissionMode: fixPermissionMode(command.harness) } : {}),
139
+ workflowDirectory: command.workflowDirectory,
140
+ onProgress: event => runtime.stderr(formatProgress(event)),
141
+ });
142
+ runtime.stdout(command.json ? `${JSON.stringify(report, null, 2)}\n` : formatReport(report));
143
+ if (report.status === 'incomplete')
144
+ return 2;
145
+ return report.findingCount > 0 ? 1 : 0;
146
+ }
147
+ /** Render a progress event as one stderr line. reviewAndPrintReport registers the observer with
148
+ * reviewCode, so progress stays out of the report stream even in JSON mode. */
149
+ function formatProgress(event) {
150
+ if (event.stage === 'workflow')
151
+ return `[workflow] ${event.runId} (${event.directory})\n`;
152
+ if (event.stage === 'analysis')
153
+ return `[analysis] ${event.status}\n`;
154
+ if (event.stage === 'fix')
155
+ return `[fix/${event.moduleId}] ${event.status}\n`;
156
+ return `[${event.stage === 'verification' ? 'verification/' : ''}${event.moduleId}/${event.angleId}] ${event.status}\n`;
157
+ }
@@ -0,0 +1,18 @@
1
+ import { type Client, type HarnessAdapter } from '@anureo/sdk';
2
+ /** Internal backend IDs accepted by the CLI, in help and unknown-harness diagnostic order. */
3
+ export declare const reviewHarnesses: readonly ["codex", "claude", "hermes", "pi"];
4
+ /** CLI backend selection shared by client construction and fix-session permissions. */
5
+ export type ReviewHarness = (typeof reviewHarnesses)[number];
6
+ /** Initialize the CLI client. executeReviewCommand awaits it before running the review and owns
7
+ * disposal. Direct source callers must also await disposal. Adapter construction and initialization
8
+ * failures reject.
9
+ *
10
+ * Defaults to Codex, which uses cwd for its initial process launch. An override replaces the
11
+ * built-in adapter. Keep harness aligned with that adapter because the SDK uses it in session bindings.
12
+ * The SDK handles approval requests using the narrowest advertised allow option and attempts
13
+ * cancellation when none is offered. */
14
+ export declare function createReviewClient(cwd: string, harness?: ReviewHarness, override?: HarnessAdapter): Promise<Client>;
15
+ /** Select the CLI fix agent's session permission mode for source edits and project checks.
16
+ * reviewAndPrintReport passes this to reviewCode only for --fix; undefined (Hermes/Pi) leaves
17
+ * fix sessions at backend defaults. Internal CLI policy. */
18
+ export declare function fixPermissionMode(harness: ReviewHarness): string | undefined;
package/src/client.js ADDED
@@ -0,0 +1,54 @@
1
+ // CLI-owned backend construction and fix-session permission policy.
2
+ import { createClaudeAdapter } from '@anureo/adapter-claude';
3
+ import { createCodexAdapter } from '@anureo/adapter-codex';
4
+ import { createHermesAdapter } from '@anureo/adapter-hermes';
5
+ import { createPiAdapter } from '@anureo/adapter-pi';
6
+ import { approveWithNarrowestScope, createClient } from '@anureo/sdk';
7
+ /** Internal backend IDs accepted by the CLI, in help and unknown-harness diagnostic order. */
8
+ export const reviewHarnesses = ['codex', 'claude', 'hermes', 'pi'];
9
+ /** Initialize the CLI client. executeReviewCommand awaits it before running the review and owns
10
+ * disposal. Direct source callers must also await disposal. Adapter construction and initialization
11
+ * failures reject.
12
+ *
13
+ * Defaults to Codex, which uses cwd for its initial process launch. An override replaces the
14
+ * built-in adapter. Keep harness aligned with that adapter because the SDK uses it in session bindings.
15
+ * The SDK handles approval requests using the narrowest advertised allow option and attempts
16
+ * cancellation when none is offered. */
17
+ export async function createReviewClient(cwd, harness = 'codex', override) {
18
+ return createClient({
19
+ adapter: override ?? harnessConfig[harness].adapter(cwd),
20
+ identity: { adapterId: harness, runtimeConfigId: 'code-review' },
21
+ interactions: {
22
+ approval: approveWithNarrowestScope,
23
+ },
24
+ });
25
+ }
26
+ /** Select the CLI fix agent's session permission mode for source edits and project checks.
27
+ * reviewAndPrintReport passes this to reviewCode only for --fix; undefined (Hermes/Pi) leaves
28
+ * fix sessions at backend defaults. Internal CLI policy. */
29
+ export function fixPermissionMode(harness) {
30
+ return harnessConfig[harness].fix;
31
+ }
32
+ /** Include a configured executable for Claude or Hermes. Trim only for the presence check so the
33
+ * adapter receives the exact configured path. An absent or blank override leaves adapter defaults in effect. */
34
+ function optionalCommand(name) {
35
+ return process.env[name]?.trim() ? { command: process.env[name] } : {};
36
+ }
37
+ /** Pair each backend's adapter factory and fix permissions so additions must supply both.
38
+ * Factories defer process.env reads until client creation. Codex trims CODEX_BIN;
39
+ * Claude/Hermes use their raw override only when nonblank, otherwise their adapter defaults. */
40
+ const harnessConfig = {
41
+ codex: {
42
+ adapter: (cwd) => createCodexAdapter({ command: process.env.CODEX_BIN?.trim() || 'codex', cwd }),
43
+ fix: 'agent',
44
+ },
45
+ claude: {
46
+ adapter: () => createClaudeAdapter({ defaultHome: true, env: process.env, ...optionalCommand('CLAUDE_BIN') }),
47
+ fix: 'bypassPermissions',
48
+ },
49
+ hermes: {
50
+ adapter: () => createHermesAdapter(optionalCommand('HERMES_BIN')),
51
+ fix: undefined,
52
+ },
53
+ pi: { adapter: () => createPiAdapter(), fix: undefined },
54
+ };
@@ -0,0 +1,27 @@
1
+ /** Diagnostic data carried by failed angle and fix outcomes and by progress observer errors.
2
+ * Inspect the enclosing outcome for coverage. Fatal workflow errors can reject reviewCode. */
3
+ export interface ReviewFailure {
4
+ /** The cause's Error.message or its string representation. */
5
+ message: string;
6
+ /** String code from the cause, or null when none was supplied. */
7
+ code: string | null;
8
+ /** True when the cause explicitly reports execution uncertainty. Use FixReport.changedFiles to
9
+ * identify recorded edits in the original work tree. */
10
+ outcomeUnknown: boolean;
11
+ /** Present when validation details contain usable path/code pairs. String paths pass through
12
+ * unchanged. Copied array paths contain strings or nonnegative safe integers. */
13
+ issues?: Array<{
14
+ path: string | Array<string | number>;
15
+ code: string;
16
+ }>;
17
+ }
18
+ /** Project a caught or synthesized cause into report data. reviewAngle and failedAngleReview
19
+ * store it as missing coverage, applyFix returns it with the failed fix, and reviewCode collects
20
+ * it when onProgress throws. formatFailure uses the same projection for CLI diagnostics.
21
+ * Reading properties or converting the cause to a string can throw. */
22
+ export declare function reviewFailure(cause: unknown): ReviewFailure;
23
+ /** Return a CLI diagnostic with the message, any nonempty code, and an uncertainty notice when
24
+ * flagged. runCli writes it to stderr for command failures. executeReviewCommand uses it for a
25
+ * secondary cleanup failure. Both callers add the prefix and newline. Errors while projecting
26
+ * the cause propagate to the caller. */
27
+ export declare function formatFailure(cause: unknown): string;
package/src/errors.js ADDED
@@ -0,0 +1,44 @@
1
+ /** Project a caught or synthesized cause into report data. reviewAngle and failedAngleReview
2
+ * store it as missing coverage, applyFix returns it with the failed fix, and reviewCode collects
3
+ * it when onProgress throws. formatFailure uses the same projection for CLI diagnostics.
4
+ * Reading properties or converting the cause to a string can throw. */
5
+ export function reviewFailure(cause) {
6
+ const issues = failureIssues(cause);
7
+ const metadata = cause && typeof cause === 'object' ? cause : undefined;
8
+ return {
9
+ message: cause instanceof Error ? cause.message : String(cause),
10
+ code: metadata && 'code' in metadata && typeof metadata.code === 'string' ? metadata.code : null,
11
+ outcomeUnknown: metadata !== undefined && 'outcomeUnknown' in metadata && metadata.outcomeUnknown === true,
12
+ ...(issues.length > 0 ? { issues } : {}),
13
+ };
14
+ }
15
+ /** Return a CLI diagnostic with the message, any nonempty code, and an uncertainty notice when
16
+ * flagged. runCli writes it to stderr for command failures. executeReviewCommand uses it for a
17
+ * secondary cleanup failure. Both callers add the prefix and newline. Errors while projecting
18
+ * the cause propagate to the caller. */
19
+ export function formatFailure(cause) {
20
+ const error = reviewFailure(cause);
21
+ return `${error.message}${error.code ? ` [${error.code}]` : ''}${error.outcomeUnknown ? ' (execution outcome unknown)' : ''}`;
22
+ }
23
+ /** Keep valid validation paths and codes in source order while omitting issue messages and details.
24
+ * Copy array paths so the report does not retain the cause's path arrays. */
25
+ function failureIssues(cause) {
26
+ const details = cause && typeof cause === 'object' && 'details' in cause ? cause.details : undefined;
27
+ if (!details || typeof details !== 'object' || !('issues' in details) || !Array.isArray(details.issues))
28
+ return [];
29
+ const candidates = details.issues;
30
+ const issues = [];
31
+ for (const issue of candidates) {
32
+ if (!issue || typeof issue !== 'object' || typeof issue.code !== 'string' || !issue.code)
33
+ continue;
34
+ const path = issue.path;
35
+ if (!isValidationIssuePath(path))
36
+ continue;
37
+ issues.push({ path: typeof path === 'string' ? path : [...path], code: issue.code });
38
+ }
39
+ return issues;
40
+ }
41
+ /** Accept SDK string paths and segmented paths with string keys or safe array indices. */
42
+ function isValidationIssuePath(path) {
43
+ return typeof path === 'string' || (Array.isArray(path) && path.every(part => typeof part === 'string' || (typeof part === 'number' && Number.isSafeInteger(part) && part >= 0)));
44
+ }
@@ -0,0 +1,13 @@
1
+ /** Observed state used to detect changes and test regular-file eligibility. */
2
+ export interface TreeState {
3
+ /** Identity of mode and captured content. Null denotes a missing path. */
4
+ fingerprint: string | null;
5
+ regular: boolean;
6
+ }
7
+ /** Fingerprint mode plus regular-file bytes or symlink text. ENOENT returns a null fingerprint.
8
+ * Other filesystem failures reject. captureWorkingTreeBaseline and inspectWorkingTreeChanges
9
+ * use these states to detect edits, and checkOriginalSources compares the target with its patch
10
+ * baseline before applying. Callers supply an absolute checkout or scratch root and a
11
+ * Git-root-relative path. They check parent path resolution where required.
12
+ */
13
+ export declare function fileState(cwd: string, file: string): Promise<TreeState>;
@@ -0,0 +1,24 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { lstat, readFile, readlink } from 'node:fs/promises';
3
+ import { resolve } from 'node:path';
4
+ /** Fingerprint mode plus regular-file bytes or symlink text. ENOENT returns a null fingerprint.
5
+ * Other filesystem failures reject. captureWorkingTreeBaseline and inspectWorkingTreeChanges
6
+ * use these states to detect edits, and checkOriginalSources compares the target with its patch
7
+ * baseline before applying. Callers supply an absolute checkout or scratch root and a
8
+ * Git-root-relative path. They check parent path resolution where required.
9
+ */
10
+ export async function fileState(cwd, file) {
11
+ const path = resolve(cwd, file);
12
+ try {
13
+ const info = await lstat(path);
14
+ const content = info.isFile() ? await readFile(path) : info.isSymbolicLink() ? await readlink(path) : '';
15
+ const fingerprint = createHash('sha256').update(String(info.mode)).update('\0').update(content).digest('hex');
16
+ return { fingerprint, regular: info.isFile() };
17
+ }
18
+ catch (cause) {
19
+ if (cause && typeof cause === 'object' && 'code' in cause && cause.code === 'ENOENT') {
20
+ return { fingerprint: null, regular: false };
21
+ }
22
+ throw cause;
23
+ }
24
+ }
@@ -0,0 +1,13 @@
1
+ import type { AgentWorktree } from "./worktree.js";
2
+ /** Save regular, Git-root-relative source files under the worktree's scratch directory.
3
+ * captureFixBaseline awaits these copies before agent dispatch. applyFixPatch uses them for the
4
+ * diff and original-tree comparison. Copy failures and symlink paths reject.
5
+ */
6
+ export declare function capturePatchBaseline(checkout: AgentWorktree, files: readonly string[]): Promise<void>;
7
+ /** Apply one binary-capable Git patch for the nonempty list of validated, Git-root-relative
8
+ * edits supplied by applyFix. Uses the saved baseline to compare original bytes and mode
9
+ * before applying. Git leaves the original index alone and rejects the patch as a
10
+ * unit on a context conflict. A mismatched source or Git apply failure rejects as
11
+ * REVIEW_FIX_CONFLICT. withAgentWorktree owns scratch cleanup.
12
+ */
13
+ export declare function applyFixPatch(root: string, checkout: AgentWorktree, files: readonly string[]): Promise<void>;
@@ -0,0 +1,86 @@
1
+ // Transfers a validated fix from its checkout to the original work tree.
2
+ import { copyFile, mkdir, realpath, writeFile } from 'node:fs/promises';
3
+ import { dirname, resolve } from 'node:path';
4
+ import { HarnessSdkError } from '@anureo/sdk';
5
+ import { fileState } from "./file-state.js";
6
+ import { execGit, GIT_MAX_BUFFER, reviewGit } from "./git.js";
7
+ /** Save regular, Git-root-relative source files under the worktree's scratch directory.
8
+ * captureFixBaseline awaits these copies before agent dispatch. applyFixPatch uses them for the
9
+ * diff and original-tree comparison. Copy failures and symlink paths reject.
10
+ */
11
+ export function capturePatchBaseline(checkout, files) {
12
+ return copyPatchFiles(checkout, 'before', files);
13
+ }
14
+ /** Apply one binary-capable Git patch for the nonempty list of validated, Git-root-relative
15
+ * edits supplied by applyFix. Uses the saved baseline to compare original bytes and mode
16
+ * before applying. Git leaves the original index alone and rejects the patch as a
17
+ * unit on a context conflict. A mismatched source or Git apply failure rejects as
18
+ * REVIEW_FIX_CONFLICT. withAgentWorktree owns scratch cleanup.
19
+ */
20
+ export async function applyFixPatch(root, checkout, files) {
21
+ await copyPatchFiles(checkout, 'after', files);
22
+ const patchPath = await writeFixPatch(checkout.scratch, files);
23
+ await checkOriginalSources(root, resolve(checkout.scratch, 'before'), files);
24
+ try {
25
+ await reviewGit(root, 'apply', '-p1', '--whitespace=nowarn', patchPath);
26
+ }
27
+ catch (cause) {
28
+ throw new HarnessSdkError('REVIEW_FIX_CONFLICT', cause instanceof Error ? cause.message : String(cause));
29
+ }
30
+ }
31
+ /** Copy Git-relative sources to the requested side of the patch, rejecting symlink paths
32
+ * before copying each file. Partial copies stay in scratch for withAgentWorktree's cleanup.
33
+ */
34
+ async function copyPatchFiles(checkout, phase, files) {
35
+ const destinationRoot = resolve(checkout.scratch, phase);
36
+ for (const file of files) {
37
+ const source = resolve(checkout.root, file);
38
+ if (await realpath(source) !== source) {
39
+ throw new HarnessSdkError('REVIEW_FIX_SCOPE', `Fix source traverses a symlink: ${file}`);
40
+ }
41
+ const destination = resolve(destinationRoot, file);
42
+ await mkdir(dirname(destination), { recursive: true });
43
+ await copyFile(source, destination);
44
+ }
45
+ }
46
+ /** Write the combined patch from staged before and after files. Read, diff and write
47
+ * failures reject before applyFixPatch checks or writes original source files.
48
+ */
49
+ async function writeFixPatch(scratch, files) {
50
+ const patches = [];
51
+ for (const file of files)
52
+ patches.push(await diffFile(scratch, file));
53
+ const patchPath = resolve(scratch, 'fix.patch');
54
+ await writeFile(patchPath, Buffer.concat(patches));
55
+ return patchPath;
56
+ }
57
+ /** Check every original target before applying any part of the patch. Reject missing paths,
58
+ * symlink resolution, nonregular targets and fingerprint mismatches against the scratch baseline.
59
+ */
60
+ async function checkOriginalSources(root, before, files) {
61
+ for (const file of files) {
62
+ const path = resolve(root, file);
63
+ const originalPath = await realpath(path).catch(() => null);
64
+ const current = originalPath === path ? await fileState(root, file) : null;
65
+ const prior = await fileState(before, file);
66
+ if (!current?.regular || current.fingerprint !== prior.fingerprint) {
67
+ throw new HarnessSdkError('REVIEW_FIX_CONFLICT', `Source changed during the fix: ${file}`);
68
+ }
69
+ }
70
+ }
71
+ /** Return the binary-capable patch bytes for one file. Git diff status 1 also carries a
72
+ * successful patch on stdout. Other process failures reject.
73
+ */
74
+ async function diffFile(cwd, file) {
75
+ try {
76
+ const { stdout } = await execGit('git', ['diff', '--no-index', '--no-prefix', '--no-ext-diff', '--no-textconv',
77
+ '--binary', '--', 'before/' + file, 'after/' + file], { cwd, encoding: 'buffer', maxBuffer: GIT_MAX_BUFFER });
78
+ return stdout;
79
+ }
80
+ catch (cause) {
81
+ if (cause && typeof cause === 'object' && 'code' in cause && cause.code === 1
82
+ && 'stdout' in cause && Buffer.isBuffer(cause.stdout))
83
+ return cause.stdout;
84
+ throw cause;
85
+ }
86
+ }
@@ -0,0 +1,25 @@
1
+ import { type TreeState } from "./file-state.js";
2
+ /** Capture selected files and existing Git-visible changes in the isolated checkout before
3
+ * agent dispatch. captureFixBaseline retains this snapshot for the post-agent comparison.
4
+ * root is the Git root and allowed paths are relative to it. Git and file-read failures reject.
5
+ */
6
+ export declare function captureWorkingTreeBaseline(root: string, allowed: readonly string[]): Promise<WorkingTreeBaseline>;
7
+ /** Compare baseline paths and current Git-visible changes after agent settlement. Including
8
+ * baseline paths detects edits that restore a dirty file to its committed contents. Return
9
+ * sorted Git-root-relative paths for selected files that were and remain regular, plus
10
+ * diagnostics for other detected changes. checkFixChanges rejects those diagnostics before
11
+ * integration. Git and file-read failures reject here.
12
+ */
13
+ export declare function inspectWorkingTreeChanges(baseline: WorkingTreeBaseline): Promise<{
14
+ changedFiles: string[];
15
+ violations: string[];
16
+ }>;
17
+ /** Isolated checkout snapshot from before agent execution. All paths except root are
18
+ * Git-root-relative. states includes the allowed paths even when Git status is clean.
19
+ */
20
+ interface WorkingTreeBaseline {
21
+ root: string;
22
+ allowed: readonly string[];
23
+ states: ReadonlyMap<string, TreeState>;
24
+ }
25
+ export {};
@@ -0,0 +1,43 @@
1
+ // Reconciles fix edits against the isolated checkout's pre-agent state.
2
+ import { fileState } from "./file-state.js";
3
+ import { reviewGit } from "./git.js";
4
+ /** Capture selected files and existing Git-visible changes in the isolated checkout before
5
+ * agent dispatch. captureFixBaseline retains this snapshot for the post-agent comparison.
6
+ * root is the Git root and allowed paths are relative to it. Git and file-read failures reject.
7
+ */
8
+ export async function captureWorkingTreeBaseline(root, allowed) {
9
+ const files = new Set([...allowed, ...(await treeEntries(root)).keys()]);
10
+ const states = new Map(await Promise.all([...files].map(async (file) => [file, await fileState(root, file)])));
11
+ return { root, allowed, states };
12
+ }
13
+ /** Compare baseline paths and current Git-visible changes after agent settlement. Including
14
+ * baseline paths detects edits that restore a dirty file to its committed contents. Return
15
+ * sorted Git-root-relative paths for selected files that were and remain regular, plus
16
+ * diagnostics for other detected changes. checkFixChanges rejects those diagnostics before
17
+ * integration. Git and file-read failures reject here.
18
+ */
19
+ export async function inspectWorkingTreeChanges(baseline) {
20
+ const { root, allowed, states: before } = baseline;
21
+ const scope = new Set(allowed);
22
+ const changedFiles = [];
23
+ const violations = [];
24
+ const entries = await treeEntries(root);
25
+ for (const file of new Set([...before.keys(), ...entries.keys()])) {
26
+ const previous = before.get(file);
27
+ const current = await fileState(root, file);
28
+ if (current.fingerprint === (previous?.fingerprint ?? null))
29
+ continue;
30
+ if (scope.has(file) && previous?.regular && current.regular)
31
+ changedFiles.push(file);
32
+ else
33
+ violations.push(`${entries.get(file)?.trim() || 'modified'} ${file}`);
34
+ }
35
+ return { changedFiles: changedFiles.sort(), violations };
36
+ }
37
+ /** Map changed Git paths to status codes for scope discovery and diagnostics. Disable rename
38
+ * detection so additions and removals can be compared independently. Git failures reject.
39
+ */
40
+ async function treeEntries(root) {
41
+ const entries = await reviewGit(root, 'status', '--porcelain=v1', '-z', '--untracked-files=all', '--no-renames');
42
+ return new Map(entries.split('\0').filter(Boolean).map(entry => [entry.slice(3), entry.slice(0, 2)]));
43
+ }
package/src/fix.d.ts ADDED
@@ -0,0 +1,37 @@
1
+ import { type Client } from '@anureo/sdk';
2
+ import type { FixReport, ModuleReview } from "./results.js";
3
+ export type { FixReport } from "./results.js";
4
+ /** Execute a fix for validated module reviews. Incomplete reviews are skipped and an empty
5
+ * findings list needs no fix. Eligible runs await worktree cleanup and report actual edits
6
+ * relative to the review directory. A failed report can include edits if integration finished
7
+ * before cleanup or terminal progress reporting failed.
8
+ *
9
+ * createFixRunner calls this with writer and agent capacity held. It uses failed reports or
10
+ * rejections to stop later writers. The coordinator owns final verification and the client
11
+ * remains caller-owned. Workflow-level fatal agent failures can still reject the enclosing run.
12
+ */
13
+ export declare function fixFindings(options: FixOptions): Promise<FixReport>;
14
+ /** Inputs for a single writer run. */
15
+ interface FixOptions {
16
+ /** Source fingerprint embedded in the fix prompt. Null omits provenance text. */
17
+ sourceVersion: string | null;
18
+ /** Borrowed client, kept usable until the fix settles. */
19
+ client: Client;
20
+ /** Canonical absolute review directory. The module files must already be validated relative to it. */
21
+ cwd: string;
22
+ /** Canonical Git root. Omission resolves it from cwd when a fix is needed. */
23
+ repositoryRoot?: string;
24
+ /** Absolute workflow storage directory, excluded when populating the isolated checkout. */
25
+ directory: string;
26
+ modules: readonly ModuleReview[];
27
+ /** Omission leaves model selection to the session/backend. */
28
+ model?: string;
29
+ /** Forwarded to session creation when supplied. */
30
+ permissionMode?: string;
31
+ /** Synchronous observer forwarded by createFixRunner to reviewCode's protected progress
32
+ * observer. Both early exits emit 'skipped'. For direct callers, exceptions before setup
33
+ * or during 'failed' reject. Exceptions after reconciliation produce a failed report
34
+ * if the subsequent 'failed' notification succeeds.
35
+ */
36
+ onProgress?: (status: 'started' | 'completed' | 'failed' | 'skipped') => void;
37
+ }