@open-agent-toolkit/cli 0.1.13 → 0.1.15
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/assets/agents/oat-reviewer.md +54 -10
- package/assets/docs/workflows/projects/reviews.md +21 -2
- package/assets/docs/workflows/skills/index.md +2 -0
- package/assets/public-package-versions.json +4 -4
- package/assets/skills/oat-project-review-provide-remote/SKILL.md +354 -0
- package/assets/skills/oat-project-review-receive/SKILL.md +7 -10
- package/assets/skills/oat-project-review-receive-remote/SKILL.md +4 -4
- package/assets/skills/oat-review-provide/SKILL.md +2 -2
- package/assets/skills/oat-review-provide/scripts/resolve-review-output.sh +16 -6
- package/assets/skills/oat-review-provide-remote/SKILL.md +273 -0
- package/assets/skills/oat-review-receive/SKILL.md +6 -6
- package/assets/skills/oat-review-receive-remote/SKILL.md +5 -5
- package/dist/commands/init/tools/shared/skill-manifest.d.ts +2 -2
- package/dist/commands/init/tools/shared/skill-manifest.d.ts.map +1 -1
- package/dist/commands/init/tools/shared/skill-manifest.js +2 -0
- package/dist/commands/project/new/index.d.ts +1 -0
- package/dist/commands/project/new/index.d.ts.map +1 -1
- package/dist/commands/project/new/index.js +23 -0
- package/dist/commands/project/new/scaffold.d.ts +17 -0
- package/dist/commands/project/new/scaffold.d.ts.map +1 -1
- package/dist/commands/project/new/scaffold.js +74 -0
- package/dist/review-remote/body-builder.d.ts +79 -0
- package/dist/review-remote/body-builder.d.ts.map +1 -0
- package/dist/review-remote/body-builder.js +103 -0
- package/dist/review-remote/capability-probe.d.ts +61 -0
- package/dist/review-remote/capability-probe.d.ts.map +1 -0
- package/dist/review-remote/capability-probe.js +87 -0
- package/dist/review-remote/line-mapper.d.ts +81 -0
- package/dist/review-remote/line-mapper.d.ts.map +1 -0
- package/dist/review-remote/line-mapper.js +165 -0
- package/dist/review-remote/marker-parser.d.ts +44 -0
- package/dist/review-remote/marker-parser.d.ts.map +1 -0
- package/dist/review-remote/marker-parser.js +97 -0
- package/dist/review-remote/narrowing.d.ts +81 -0
- package/dist/review-remote/narrowing.d.ts.map +1 -0
- package/dist/review-remote/narrowing.js +90 -0
- package/dist/review-remote/project-resolver.d.ts +46 -0
- package/dist/review-remote/project-resolver.d.ts.map +1 -0
- package/dist/review-remote/project-resolver.js +60 -0
- package/dist/review-remote/reviewer-dispatch.d.ts +108 -0
- package/dist/review-remote/reviewer-dispatch.d.ts.map +1 -0
- package/dist/review-remote/reviewer-dispatch.js +153 -0
- package/dist/review-remote/worktree.d.ts +62 -0
- package/dist/review-remote/worktree.d.ts.map +1 -0
- package/dist/review-remote/worktree.js +117 -0
- package/package.json +2 -2
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
1
2
|
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
3
|
import { join } from 'node:path';
|
|
3
4
|
import { resolveProjectsRoot } from '../../shared/oat-paths.js';
|
|
@@ -105,6 +106,62 @@ function applyTemplateReplacements(template, projectName, today, nowUtc, mode) {
|
|
|
105
106
|
async function defaultRefreshDashboard(repoRoot) {
|
|
106
107
|
await generateStateDashboard({ repoRoot });
|
|
107
108
|
}
|
|
109
|
+
/**
|
|
110
|
+
* Scoped, fail-safe commit of just the files this run created.
|
|
111
|
+
*
|
|
112
|
+
* Stages and commits only the pathspecs derived from `createdFiles` (under
|
|
113
|
+
* `projectPath`) so unrelated working-tree changes — including pre-existing
|
|
114
|
+
* dirty edits inside the same project directory on a re-run, and the
|
|
115
|
+
* `.oat/state.md` dashboard outside it — are never swept in. The returned
|
|
116
|
+
* status distinguishes a clean commit from each skip reason and from a genuine
|
|
117
|
+
* git failure; on failure the captured git stderr is surfaced via `error`. This
|
|
118
|
+
* never throws: any git error is classified as `failed`, not propagated.
|
|
119
|
+
*/
|
|
120
|
+
function commitScaffold(cwd, projectPath, projectName, createdFiles) {
|
|
121
|
+
const run = (args) => execFileSync('git', args, {
|
|
122
|
+
cwd,
|
|
123
|
+
encoding: 'utf8',
|
|
124
|
+
// Capture stderr instead of inheriting it so deliberate skip/failure
|
|
125
|
+
// probes (e.g. tests) do not leak raw `git fatal:` lines to the terminal.
|
|
126
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
127
|
+
}).trim();
|
|
128
|
+
try {
|
|
129
|
+
run(['rev-parse', '--is-inside-work-tree']);
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
return { status: 'skipped_no_worktree', committed: false };
|
|
133
|
+
}
|
|
134
|
+
// Only commit files this run created. Nothing created => nothing to commit,
|
|
135
|
+
// which guarantees a re-run never touches unrelated working-tree edits.
|
|
136
|
+
if (createdFiles.length === 0) {
|
|
137
|
+
return { status: 'skipped_nothing', committed: false };
|
|
138
|
+
}
|
|
139
|
+
const pathspecs = createdFiles.map((file) => join(projectPath, file));
|
|
140
|
+
try {
|
|
141
|
+
run(['add', '--', ...pathspecs]);
|
|
142
|
+
const staged = run(['diff', '--cached', '--name-only', '--', ...pathspecs]);
|
|
143
|
+
if (staged.length === 0) {
|
|
144
|
+
return { status: 'skipped_nothing', committed: false };
|
|
145
|
+
}
|
|
146
|
+
run([
|
|
147
|
+
'commit',
|
|
148
|
+
'-m',
|
|
149
|
+
`chore(oat): scaffold ${projectName}`,
|
|
150
|
+
'--',
|
|
151
|
+
...pathspecs,
|
|
152
|
+
]);
|
|
153
|
+
const commitSha = run(['rev-parse', 'HEAD']);
|
|
154
|
+
return { status: 'committed', committed: true, commitSha };
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
const stderr = error && typeof error === 'object' && 'stderr' in error
|
|
158
|
+
? error.stderr
|
|
159
|
+
: undefined;
|
|
160
|
+
const message = (stderr != null ? stderr.toString().trim() : '') ||
|
|
161
|
+
(error instanceof Error ? error.message : String(error));
|
|
162
|
+
return { status: 'failed', committed: false, error: message };
|
|
163
|
+
}
|
|
164
|
+
}
|
|
108
165
|
async function scaffoldModeTemplates(repoRoot, projectPath, projectName, mode, today, nowUtc) {
|
|
109
166
|
const templatesDir = join(repoRoot, '.oat', 'templates');
|
|
110
167
|
const createdFiles = [];
|
|
@@ -169,6 +226,19 @@ export async function scaffoldProject(options) {
|
|
|
169
226
|
console.error(`Warning: dashboard refresh failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
170
227
|
}
|
|
171
228
|
}
|
|
229
|
+
let committed = false;
|
|
230
|
+
let commitSha;
|
|
231
|
+
let commitStatus = 'skipped_disabled';
|
|
232
|
+
let commitError;
|
|
233
|
+
if (options.commit) {
|
|
234
|
+
// `projectPath` is relative to `repoRoot`, so git must run there for the
|
|
235
|
+
// pathspecs to resolve to the scaffolded files.
|
|
236
|
+
const commitResult = commitScaffold(options.repoRoot, projectPath, options.projectName, createdFiles);
|
|
237
|
+
committed = commitResult.committed;
|
|
238
|
+
commitSha = commitResult.commitSha;
|
|
239
|
+
commitStatus = commitResult.status;
|
|
240
|
+
commitError = commitResult.error;
|
|
241
|
+
}
|
|
172
242
|
return {
|
|
173
243
|
mode,
|
|
174
244
|
projectsRoot,
|
|
@@ -177,5 +247,9 @@ export async function scaffoldProject(options) {
|
|
|
177
247
|
skippedFiles,
|
|
178
248
|
activePointerUpdated: setActive,
|
|
179
249
|
dashboardRefreshed,
|
|
250
|
+
committed,
|
|
251
|
+
commitSha,
|
|
252
|
+
commitStatus,
|
|
253
|
+
commitError,
|
|
180
254
|
};
|
|
181
255
|
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builder for the posted-review-body (see design.md → Data Models →
|
|
3
|
+
* Posted-review-body) and the verdict mapper that decides the GitHub review
|
|
4
|
+
* `event`.
|
|
5
|
+
*
|
|
6
|
+
* The body is the durable handoff to `*-receive-remote`: a leading
|
|
7
|
+
* HTML-comment marker block (parsed back by {@link parseMarkerBlock}) followed
|
|
8
|
+
* by human-readable prose (summary, severity counts, optional minor-fix nudge,
|
|
9
|
+
* optional verification commands).
|
|
10
|
+
*/
|
|
11
|
+
import { type ReviewInvocation } from './marker-parser.js';
|
|
12
|
+
export type ReviewVerdict = 'REQUEST_CHANGES' | 'COMMENT';
|
|
13
|
+
export type FindingSeverity = 'critical' | 'important' | 'medium' | 'minor';
|
|
14
|
+
/** Minimal finding shape the builder needs — only severity is required. */
|
|
15
|
+
export interface BuilderFinding {
|
|
16
|
+
severity: FindingSeverity;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* A finding whose `file:line` is NOT present in the PR diff and therefore
|
|
20
|
+
* cannot be posted as a GitHub inline comment (see design.md → Error Handling →
|
|
21
|
+
* Inline-comment line mapping). Such findings must NOT be dropped — they are
|
|
22
|
+
* downgraded into the top-level body via a "Findings outside the PR diff"
|
|
23
|
+
* subsection carrying the original `file:line` reference and finding body.
|
|
24
|
+
*
|
|
25
|
+
* Field names mirror the `StructuredFindings` finding shape (design.md → Data
|
|
26
|
+
* Models → StructuredFindings) so callers can pass entries through unchanged;
|
|
27
|
+
* `line` is `number | null` because a reviewer-level finding may be file-scoped
|
|
28
|
+
* with no specific line.
|
|
29
|
+
*/
|
|
30
|
+
export interface OutOfDiffFinding {
|
|
31
|
+
/** Repo-relative path the original finding referenced. */
|
|
32
|
+
file: string;
|
|
33
|
+
/** 1-based line the original finding referenced, or `null` if file-scoped. */
|
|
34
|
+
line: number | null;
|
|
35
|
+
severity: FindingSeverity;
|
|
36
|
+
/** Optional short title carried from the structured finding. */
|
|
37
|
+
title?: string;
|
|
38
|
+
/** Finding description / rationale — preserved verbatim, never dropped. */
|
|
39
|
+
body: string;
|
|
40
|
+
}
|
|
41
|
+
export interface BuildInput {
|
|
42
|
+
/** Full 40-char hex SHA of the reviewed PR HEAD. */
|
|
43
|
+
headSha: string;
|
|
44
|
+
/** Scope token (`pNN`, `final`, …) or the `ad-hoc` sentinel. */
|
|
45
|
+
scope: string;
|
|
46
|
+
/** Project path — set only on the project rail; omitted on ad-hoc. */
|
|
47
|
+
project?: string;
|
|
48
|
+
/** How the review was invoked. */
|
|
49
|
+
invocation: ReviewInvocation;
|
|
50
|
+
/** 2-3 sentence human-readable summary. */
|
|
51
|
+
summary: string;
|
|
52
|
+
findings: BuilderFinding[];
|
|
53
|
+
/**
|
|
54
|
+
* Findings whose `file:line` is not in the PR diff, downgraded into the body
|
|
55
|
+
* instead of posted inline. Omitted/empty renders no subsection (the body is
|
|
56
|
+
* byte-identical to a build without the field).
|
|
57
|
+
*
|
|
58
|
+
* Count contract: these findings MUST also appear in {@link BuildInput.findings}
|
|
59
|
+
* so the severity counts stay complete. This field only drives body rendering
|
|
60
|
+
* — the builder never re-derives severity counts from it.
|
|
61
|
+
*/
|
|
62
|
+
outOfDiffFindings?: OutOfDiffFinding[];
|
|
63
|
+
/** Commands the user can run to verify fixes; omitted when absent/empty. */
|
|
64
|
+
verificationCommands?: string[];
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Map a finding set to the GitHub review verdict: `REQUEST_CHANGES` when any
|
|
68
|
+
* critical or important finding is present, otherwise `COMMENT` (including the
|
|
69
|
+
* zero-findings clean-review case). Never auto-`APPROVE`.
|
|
70
|
+
*/
|
|
71
|
+
export declare function mapVerdict(findings: BuilderFinding[]): ReviewVerdict;
|
|
72
|
+
/**
|
|
73
|
+
* Build the posted-review body and compute its verdict.
|
|
74
|
+
*/
|
|
75
|
+
export declare function buildReviewBody(input: BuildInput): {
|
|
76
|
+
body: string;
|
|
77
|
+
verdict: ReviewVerdict;
|
|
78
|
+
};
|
|
79
|
+
//# sourceMappingURL=body-builder.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"body-builder.d.ts","sourceRoot":"","sources":["../../src/review-remote/body-builder.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAqB,KAAK,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAE3E,MAAM,MAAM,aAAa,GAAG,iBAAiB,GAAG,SAAS,CAAC;AAE1D,MAAM,MAAM,eAAe,GAAG,UAAU,GAAG,WAAW,GAAG,QAAQ,GAAG,OAAO,CAAC;AAE5E,2EAA2E;AAC3E,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,eAAe,CAAC;CAC3B;AAED;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,gBAAgB;IAC/B,0DAA0D;IAC1D,IAAI,EAAE,MAAM,CAAC;IACb,8EAA8E;IAC9E,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,QAAQ,EAAE,eAAe,CAAC;IAC1B,gEAAgE;IAChE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,2EAA2E;IAC3E,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,UAAU;IACzB,oDAAoD;IACpD,OAAO,EAAE,MAAM,CAAC;IAChB,gEAAgE;IAChE,KAAK,EAAE,MAAM,CAAC;IACd,sEAAsE;IACtE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,kCAAkC;IAClC,UAAU,EAAE,gBAAgB,CAAC;IAC7B,2CAA2C;IAC3C,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,cAAc,EAAE,CAAC;IAC3B;;;;;;;;OAQG;IACH,iBAAiB,CAAC,EAAE,gBAAgB,EAAE,CAAC;IACvC,4EAA4E;IAC5E,oBAAoB,CAAC,EAAE,MAAM,EAAE,CAAC;CACjC;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,QAAQ,EAAE,cAAc,EAAE,GAAG,aAAa,CAKpE;AAkED;;GAEG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,UAAU,GAAG;IAClD,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,aAAa,CAAC;CACxB,CAkCA"}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builder for the posted-review-body (see design.md → Data Models →
|
|
3
|
+
* Posted-review-body) and the verdict mapper that decides the GitHub review
|
|
4
|
+
* `event`.
|
|
5
|
+
*
|
|
6
|
+
* The body is the durable handoff to `*-receive-remote`: a leading
|
|
7
|
+
* HTML-comment marker block (parsed back by {@link parseMarkerBlock}) followed
|
|
8
|
+
* by human-readable prose (summary, severity counts, optional minor-fix nudge,
|
|
9
|
+
* optional verification commands).
|
|
10
|
+
*/
|
|
11
|
+
import { MARKER_BLOCK_OPEN } from './marker-parser.js';
|
|
12
|
+
/**
|
|
13
|
+
* Map a finding set to the GitHub review verdict: `REQUEST_CHANGES` when any
|
|
14
|
+
* critical or important finding is present, otherwise `COMMENT` (including the
|
|
15
|
+
* zero-findings clean-review case). Never auto-`APPROVE`.
|
|
16
|
+
*/
|
|
17
|
+
export function mapVerdict(findings) {
|
|
18
|
+
const hasBlocking = findings.some((f) => f.severity === 'critical' || f.severity === 'important');
|
|
19
|
+
return hasBlocking ? 'REQUEST_CHANGES' : 'COMMENT';
|
|
20
|
+
}
|
|
21
|
+
function countSeverities(findings) {
|
|
22
|
+
const counts = {
|
|
23
|
+
critical: 0,
|
|
24
|
+
important: 0,
|
|
25
|
+
medium: 0,
|
|
26
|
+
minor: 0,
|
|
27
|
+
};
|
|
28
|
+
for (const f of findings) {
|
|
29
|
+
counts[f.severity] += 1;
|
|
30
|
+
}
|
|
31
|
+
return counts;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Emit the leading marker block. Mirrors the parser's expected single-line
|
|
35
|
+
* scalar shape so {@link parseMarkerBlock} round-trips a built body cleanly.
|
|
36
|
+
* `oat_project` is emitted only when present (key-omitted on the ad-hoc rail).
|
|
37
|
+
*/
|
|
38
|
+
function buildMarkerBlock(input) {
|
|
39
|
+
const lines = [
|
|
40
|
+
'oat_provide_remote: true',
|
|
41
|
+
`oat_review_head_sha: ${input.headSha}`,
|
|
42
|
+
`oat_review_scope: ${input.scope}`,
|
|
43
|
+
];
|
|
44
|
+
if (input.project !== undefined && input.project !== '') {
|
|
45
|
+
lines.push(`oat_project: ${input.project}`);
|
|
46
|
+
}
|
|
47
|
+
lines.push(`oat_review_invocation: ${input.invocation}`);
|
|
48
|
+
return `<!-- ${MARKER_BLOCK_OPEN}\n${lines.join('\n')}\n-->`;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Render the "Findings outside the PR diff" subsection (design.md → Error
|
|
52
|
+
* Handling → Inline-comment line mapping). Each entry shows its original
|
|
53
|
+
* `file:line` reference followed by the preserved finding body. A `null` line
|
|
54
|
+
* renders the bare file path (file-scoped finding). Returns `null` when there
|
|
55
|
+
* are no out-of-diff findings so no empty heading is emitted.
|
|
56
|
+
*/
|
|
57
|
+
function buildOutOfDiffSection(findings) {
|
|
58
|
+
if (!findings || findings.length === 0) {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
const entries = findings.map((f) => {
|
|
62
|
+
const reference = f.line === null ? f.file : `${f.file}:${f.line}`;
|
|
63
|
+
const heading = f.title ? `${reference} — ${f.title}` : reference;
|
|
64
|
+
return `- **${heading}**\n\n ${f.body}`;
|
|
65
|
+
});
|
|
66
|
+
return ['## Findings outside the PR diff', '', ...entries].join('\n');
|
|
67
|
+
}
|
|
68
|
+
const MINOR_FIX_NUDGE = 'Minor findings are included inline. We recommend fixing minors during ' +
|
|
69
|
+
'this cycle rather than tracking them as backlog items — they are usually ' +
|
|
70
|
+
'faster to just resolve than to manage.';
|
|
71
|
+
/**
|
|
72
|
+
* Build the posted-review body and compute its verdict.
|
|
73
|
+
*/
|
|
74
|
+
export function buildReviewBody(input) {
|
|
75
|
+
const counts = countSeverities(input.findings);
|
|
76
|
+
const verdict = mapVerdict(input.findings);
|
|
77
|
+
const sections = [
|
|
78
|
+
buildMarkerBlock(input),
|
|
79
|
+
`## Summary\n\n${input.summary}`,
|
|
80
|
+
[
|
|
81
|
+
'## Severity Counts',
|
|
82
|
+
'',
|
|
83
|
+
`- Critical: ${counts.critical}`,
|
|
84
|
+
`- Important: ${counts.important}`,
|
|
85
|
+
`- Medium: ${counts.medium}`,
|
|
86
|
+
`- Minor: ${counts.minor}`,
|
|
87
|
+
].join('\n'),
|
|
88
|
+
];
|
|
89
|
+
const outOfDiffSection = buildOutOfDiffSection(input.outOfDiffFindings);
|
|
90
|
+
if (outOfDiffSection !== null) {
|
|
91
|
+
sections.push(outOfDiffSection);
|
|
92
|
+
}
|
|
93
|
+
if (counts.minor > 0) {
|
|
94
|
+
sections.push(`## Notes\n\n${MINOR_FIX_NUDGE}`);
|
|
95
|
+
}
|
|
96
|
+
if (input.verificationCommands && input.verificationCommands.length > 0) {
|
|
97
|
+
const commands = input.verificationCommands
|
|
98
|
+
.map((cmd) => `- \`${cmd}\``)
|
|
99
|
+
.join('\n');
|
|
100
|
+
sections.push(`## Verification\n\n${commands}`);
|
|
101
|
+
}
|
|
102
|
+
return { body: sections.join('\n\n'), verdict };
|
|
103
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Capability probe for the optional `agent-reviews` posting flow (see design.md
|
|
3
|
+
* → Error Handling → Capability probe).
|
|
4
|
+
*
|
|
5
|
+
* The provide-remote skills prefer `agent-reviews` for tooling symmetry IF it
|
|
6
|
+
* exposes a "post / submit a full PR review" flow. The probe runs a
|
|
7
|
+
* non-mutating `agent-reviews --help` once, parses the help text for such a
|
|
8
|
+
* flag, and caches the result for the run. `gh api` is always the safe
|
|
9
|
+
* fallback — the probe never fails the skill.
|
|
10
|
+
*
|
|
11
|
+
* Empirical current-state finding (probed 2026-05-29, `agent-reviews@1.0.2`):
|
|
12
|
+
* the CLI exposes list/detail/watch commands plus `--reply <id>` for replying
|
|
13
|
+
* to existing comments, but NO command or flag that posts a full PR review. So
|
|
14
|
+
* the probe returns `not-supported` today and the skill posts via `gh api`.
|
|
15
|
+
* The probe is forward-compatible: when `agent-reviews` gains a posting flow,
|
|
16
|
+
* this parser recognizes it without a code change to the skill.
|
|
17
|
+
*/
|
|
18
|
+
/** Outcome of running the `agent-reviews --help` probe command. */
|
|
19
|
+
export interface HelpProbeResult {
|
|
20
|
+
/** Whether the probe command ran to completion (exit 0). */
|
|
21
|
+
ok: boolean;
|
|
22
|
+
/** Captured stdout (the help text), empty when the command errored. */
|
|
23
|
+
stdout: string;
|
|
24
|
+
/** Captured stderr, when the command errored. */
|
|
25
|
+
stderr?: string;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Injectable invoker that runs `agent-reviews --help` (or equivalent). The
|
|
29
|
+
* skill provides a real `npx agent-reviews --help` runner; tests pass a stub.
|
|
30
|
+
* It MUST resolve (never reject) — a failed invocation is reported via
|
|
31
|
+
* `ok: false` so the probe can map it to `unknown`.
|
|
32
|
+
*/
|
|
33
|
+
export type HelpProbe = () => Promise<HelpProbeResult>;
|
|
34
|
+
export type PostingSupport = 'supported' | 'not-supported' | 'unknown';
|
|
35
|
+
export interface CapabilityResult {
|
|
36
|
+
/**
|
|
37
|
+
* Whether `agent-reviews` exposes a review-posting flow:
|
|
38
|
+
* - `supported`: a posting flag was found (`flag` is set).
|
|
39
|
+
* - `not-supported`: the probe ran but no posting flag exists.
|
|
40
|
+
* - `unknown`: the probe could not run; caller falls back to `gh api`.
|
|
41
|
+
*/
|
|
42
|
+
posting: PostingSupport;
|
|
43
|
+
/** The discovered posting flag (e.g., `--post-review`), when supported. */
|
|
44
|
+
flag?: string;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Per-run cache. Pass the same object across calls within a single skill run so
|
|
48
|
+
* the help command is invoked at most once. Omit it to force a fresh probe.
|
|
49
|
+
*/
|
|
50
|
+
export interface ProbeCache {
|
|
51
|
+
result?: CapabilityResult;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Probe whether `agent-reviews` supports posting a full PR review.
|
|
55
|
+
*
|
|
56
|
+
* @param probe injectable `agent-reviews --help` runner.
|
|
57
|
+
* @param cache optional per-run cache; when provided, the probe runs at most
|
|
58
|
+
* once and subsequent calls return the cached result.
|
|
59
|
+
*/
|
|
60
|
+
export declare function probeAgentReviewsPosting(probe: HelpProbe, cache?: ProbeCache): Promise<CapabilityResult>;
|
|
61
|
+
//# sourceMappingURL=capability-probe.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"capability-probe.d.ts","sourceRoot":"","sources":["../../src/review-remote/capability-probe.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,mEAAmE;AACnE,MAAM,WAAW,eAAe;IAC9B,4DAA4D;IAC5D,EAAE,EAAE,OAAO,CAAC;IACZ,uEAAuE;IACvE,MAAM,EAAE,MAAM,CAAC;IACf,iDAAiD;IACjD,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;;;GAKG;AACH,MAAM,MAAM,SAAS,GAAG,MAAM,OAAO,CAAC,eAAe,CAAC,CAAC;AAEvD,MAAM,MAAM,cAAc,GAAG,WAAW,GAAG,eAAe,GAAG,SAAS,CAAC;AAEvE,MAAM,WAAW,gBAAgB;IAC/B;;;;;OAKG;IACH,OAAO,EAAE,cAAc,CAAC;IACxB,2EAA2E;IAC3E,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;;GAGG;AACH,MAAM,WAAW,UAAU;IACzB,MAAM,CAAC,EAAE,gBAAgB,CAAC;CAC3B;AA4CD;;;;;;GAMG;AACH,wBAAsB,wBAAwB,CAC5C,KAAK,EAAE,SAAS,EAChB,KAAK,CAAC,EAAE,UAAU,GACjB,OAAO,CAAC,gBAAgB,CAAC,CA2B3B"}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Capability probe for the optional `agent-reviews` posting flow (see design.md
|
|
3
|
+
* → Error Handling → Capability probe).
|
|
4
|
+
*
|
|
5
|
+
* The provide-remote skills prefer `agent-reviews` for tooling symmetry IF it
|
|
6
|
+
* exposes a "post / submit a full PR review" flow. The probe runs a
|
|
7
|
+
* non-mutating `agent-reviews --help` once, parses the help text for such a
|
|
8
|
+
* flag, and caches the result for the run. `gh api` is always the safe
|
|
9
|
+
* fallback — the probe never fails the skill.
|
|
10
|
+
*
|
|
11
|
+
* Empirical current-state finding (probed 2026-05-29, `agent-reviews@1.0.2`):
|
|
12
|
+
* the CLI exposes list/detail/watch commands plus `--reply <id>` for replying
|
|
13
|
+
* to existing comments, but NO command or flag that posts a full PR review. So
|
|
14
|
+
* the probe returns `not-supported` today and the skill posts via `gh api`.
|
|
15
|
+
* The probe is forward-compatible: when `agent-reviews` gains a posting flow,
|
|
16
|
+
* this parser recognizes it without a code change to the skill.
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* Candidate flag tokens that, if present in the help text, indicate a
|
|
20
|
+
* full-review posting flow. Reply-to-comment flags (`--reply`) are deliberately
|
|
21
|
+
* excluded — replying to an existing comment is not posting a review.
|
|
22
|
+
*
|
|
23
|
+
* The intent-specific tokens (`--post-review`, `--submit-review`) come first;
|
|
24
|
+
* the generic `--review`/`--post` are kept as broad fallbacks. The generic
|
|
25
|
+
* tokens carry a small forward-compat false-positive risk: a future
|
|
26
|
+
* `agent-reviews` could add `--review` with an unrelated meaning (e.g.
|
|
27
|
+
* "show a review"), which the probe would report as `supported`. That risk is
|
|
28
|
+
* accepted here because `gh api` remains the safe posting path even when the
|
|
29
|
+
* probe mis-detects an `agent-reviews` posting flow — a false positive degrades
|
|
30
|
+
* to the same fallback as `unknown`, it never drops or corrupts a review.
|
|
31
|
+
*/
|
|
32
|
+
const POSTING_FLAG_CANDIDATES = [
|
|
33
|
+
'--post-review',
|
|
34
|
+
'--submit-review',
|
|
35
|
+
'--review',
|
|
36
|
+
'--post',
|
|
37
|
+
];
|
|
38
|
+
/** Extract the first posting flag present in the help text, or `undefined`. */
|
|
39
|
+
function findPostingFlag(helpText) {
|
|
40
|
+
for (const flag of POSTING_FLAG_CANDIDATES) {
|
|
41
|
+
// Word-boundary-ish match: the flag followed by whitespace, `=`, or EOL, so
|
|
42
|
+
// `--post` does not spuriously match `--post-review` (and vice versa) and a
|
|
43
|
+
// substring of an unrelated token never matches.
|
|
44
|
+
const pattern = new RegExp(`(?:^|\\s)${escapeRegExp(flag)}(?=$|[\\s=])`, 'm');
|
|
45
|
+
if (pattern.test(helpText)) {
|
|
46
|
+
return flag;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
function escapeRegExp(value) {
|
|
52
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Probe whether `agent-reviews` supports posting a full PR review.
|
|
56
|
+
*
|
|
57
|
+
* @param probe injectable `agent-reviews --help` runner.
|
|
58
|
+
* @param cache optional per-run cache; when provided, the probe runs at most
|
|
59
|
+
* once and subsequent calls return the cached result.
|
|
60
|
+
*/
|
|
61
|
+
export async function probeAgentReviewsPosting(probe, cache) {
|
|
62
|
+
if (cache?.result !== undefined) {
|
|
63
|
+
return cache.result;
|
|
64
|
+
}
|
|
65
|
+
let outcome;
|
|
66
|
+
try {
|
|
67
|
+
outcome = await probe();
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
// A rejecting probe is treated the same as an errored one: unknown.
|
|
71
|
+
outcome = { ok: false, stdout: '' };
|
|
72
|
+
}
|
|
73
|
+
let result;
|
|
74
|
+
if (!outcome.ok) {
|
|
75
|
+
result = { posting: 'unknown' };
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
const flag = findPostingFlag(outcome.stdout);
|
|
79
|
+
result = flag
|
|
80
|
+
? { posting: 'supported', flag }
|
|
81
|
+
: { posting: 'not-supported' };
|
|
82
|
+
}
|
|
83
|
+
if (cache) {
|
|
84
|
+
cache.result = result;
|
|
85
|
+
}
|
|
86
|
+
return result;
|
|
87
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inline-comment line-mapping validator (see design.md → Error Handling →
|
|
3
|
+
* Inline-comment line mapping).
|
|
4
|
+
*
|
|
5
|
+
* GitHub's `POST /repos/:owner/:repo/pulls/:N/reviews` rejects inline comments
|
|
6
|
+
* at file:line positions not present in the PR diff. Before adding a finding to
|
|
7
|
+
* the `comments[]` payload, the caller classifies it against the parsed hunk
|
|
8
|
+
* ranges. Out-of-diff findings are NOT silently dropped or shifted — the caller
|
|
9
|
+
* downgrades them to a top-level "Findings outside the PR diff" subsection.
|
|
10
|
+
*
|
|
11
|
+
* Two parsers feed one classifier with a single shared {@link HunkRange} shape:
|
|
12
|
+
* - {@link parsePullFilesPatch} for the per-file `patch` field of
|
|
13
|
+
* `gh api /repos/.../pulls/<N>/files` (rich-context mode).
|
|
14
|
+
* - {@link parseUnifiedDiff} for `gh pr diff <N>` (diff-only fallback mode).
|
|
15
|
+
*/
|
|
16
|
+
/** A single diff hunk's old- and new-side ranges. Shared by both parsers. */
|
|
17
|
+
export interface HunkRange {
|
|
18
|
+
/** 1-based start line on the old (LEFT) side. */
|
|
19
|
+
oldStart: number;
|
|
20
|
+
/** Line count on the old side. */
|
|
21
|
+
oldCount: number;
|
|
22
|
+
/** 1-based start line on the new (RIGHT) side. */
|
|
23
|
+
newStart: number;
|
|
24
|
+
/** Line count on the new side. */
|
|
25
|
+
newCount: number;
|
|
26
|
+
/** Pre-rename path, when the owning file was renamed. */
|
|
27
|
+
previousFilename?: string;
|
|
28
|
+
}
|
|
29
|
+
/** A finding location to classify against a file's hunk ranges. */
|
|
30
|
+
export interface FindingLocation {
|
|
31
|
+
file: string;
|
|
32
|
+
line: number;
|
|
33
|
+
/** Set when the finding is explicitly about removed code (LEFT side). */
|
|
34
|
+
removed?: boolean;
|
|
35
|
+
}
|
|
36
|
+
export interface InDiffClassification {
|
|
37
|
+
status: 'in-diff';
|
|
38
|
+
side: 'RIGHT' | 'LEFT';
|
|
39
|
+
line: number;
|
|
40
|
+
}
|
|
41
|
+
export interface OutOfDiffClassification {
|
|
42
|
+
status: 'out-of-diff';
|
|
43
|
+
/** Original file reference, carried through for the downgrade subsection. */
|
|
44
|
+
file: string;
|
|
45
|
+
/** Original line reference, carried through for the downgrade subsection. */
|
|
46
|
+
line: number;
|
|
47
|
+
}
|
|
48
|
+
export type FindingClassification = InDiffClassification | OutOfDiffClassification;
|
|
49
|
+
/**
|
|
50
|
+
* Parse the `patch` field of a single `gh api .../pulls/<N>/files` entry into
|
|
51
|
+
* its hunk ranges. Returns `[]` for an empty patch (e.g., a binary file, whose
|
|
52
|
+
* entry has no `patch`).
|
|
53
|
+
*
|
|
54
|
+
* Caller contract for renamed files: a `gh api .../files` entry carries the
|
|
55
|
+
* pre-rename path in a sibling `previous_filename` field, NOT in `patch`. This
|
|
56
|
+
* parser only sees `patch`, so it cannot populate `HunkRange.previousFilename`
|
|
57
|
+
* in rich-context (gh api) mode. When a finding is reported against the
|
|
58
|
+
* pre-rename path, the caller must remap it to the entry's post-rename
|
|
59
|
+
* `filename` (using the JSON `previous_filename`) before calling
|
|
60
|
+
* `classifyFinding` — otherwise the finding is treated as out-of-diff. (The
|
|
61
|
+
* `gh pr diff` path handles this internally: `parseUnifiedDiff` records
|
|
62
|
+
* `previousFilename` from the `rename from` header.)
|
|
63
|
+
*/
|
|
64
|
+
export declare function parsePullFilesPatch(patch: string): HunkRange[];
|
|
65
|
+
/**
|
|
66
|
+
* Parse a full `gh pr diff <N>` unified diff into a map of post-image file path
|
|
67
|
+
* → hunk ranges. Renamed files are keyed under their post-rename path with
|
|
68
|
+
* `previousFilename` recorded; binary files map to an empty array.
|
|
69
|
+
*/
|
|
70
|
+
export declare function parseUnifiedDiff(diff: string): Record<string, HunkRange[]>;
|
|
71
|
+
/**
|
|
72
|
+
* Classify a finding against a file's hunk ranges.
|
|
73
|
+
*
|
|
74
|
+
* A line inside any hunk's new-side range is `in-diff` on the `RIGHT` side
|
|
75
|
+
* (additions/context); when the finding is explicitly about removed code it is
|
|
76
|
+
* mapped to the `LEFT` side instead. A line outside every hunk — or a file with
|
|
77
|
+
* no ranges (binary) — is `out-of-diff`, carrying the original `file:line` so
|
|
78
|
+
* the caller can downgrade it without mutating the source finding.
|
|
79
|
+
*/
|
|
80
|
+
export declare function classifyFinding(finding: FindingLocation, ranges: HunkRange[]): FindingClassification;
|
|
81
|
+
//# sourceMappingURL=line-mapper.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"line-mapper.d.ts","sourceRoot":"","sources":["../../src/review-remote/line-mapper.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,6EAA6E;AAC7E,MAAM,WAAW,SAAS;IACxB,iDAAiD;IACjD,QAAQ,EAAE,MAAM,CAAC;IACjB,kCAAkC;IAClC,QAAQ,EAAE,MAAM,CAAC;IACjB,kDAAkD;IAClD,QAAQ,EAAE,MAAM,CAAC;IACjB,kCAAkC;IAClC,QAAQ,EAAE,MAAM,CAAC;IACjB,yDAAyD;IACzD,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,mEAAmE;AACnE,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,yEAAyE;IACzE,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,SAAS,CAAC;IAClB,IAAI,EAAE,OAAO,GAAG,MAAM,CAAC;IACvB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,uBAAuB;IACtC,MAAM,EAAE,aAAa,CAAC;IACtB,6EAA6E;IAC7E,IAAI,EAAE,MAAM,CAAC;IACb,6EAA6E;IAC7E,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,MAAM,qBAAqB,GAC7B,oBAAoB,GACpB,uBAAuB,CAAC;AAkB5B;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,CAY9D;AAUD;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,SAAS,EAAE,CAAC,CA+E1E;AAED;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAC7B,OAAO,EAAE,eAAe,EACxB,MAAM,EAAE,SAAS,EAAE,GAClB,qBAAqB,CAevB"}
|