@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
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tier-1 dispatch wrapper for the `oat-reviewer` subagent's structured-output
|
|
3
|
+
* mode (see design.md → Component Design → `oat-project-review-provide-remote`
|
|
4
|
+
* and `.agents/agents/oat-reviewer.md` → Structured-Output Mode).
|
|
5
|
+
*
|
|
6
|
+
* The project-rail provide-remote skill's Tier 1 dispatch hands the reviewer a
|
|
7
|
+
* payload carrying the project context, the posted-review-body schema
|
|
8
|
+
* reference, the resolved re-review narrowing range, and the
|
|
9
|
+
* `oat_output_mode: structured` flag. In that mode the reviewer returns a
|
|
10
|
+
* `StructuredFindings` object in-memory rather than writing a review artifact;
|
|
11
|
+
* this wrapper validates that object and hands it back to the skill, which is
|
|
12
|
+
* then responsible for building the posted body and posting to GitHub.
|
|
13
|
+
*
|
|
14
|
+
* Responsibilities are deliberately narrow:
|
|
15
|
+
*
|
|
16
|
+
* - Build the dispatch payload with the structured-output flag p03 wired.
|
|
17
|
+
* - Forward it to an injected {@link Dispatcher} (real provider dispatch in the
|
|
18
|
+
* skill; stubs in tests).
|
|
19
|
+
* - Surface dispatcher errors to the caller WITHOUT retry — the Tier 2/3
|
|
20
|
+
* fallback decision belongs to the skill, not this wrapper.
|
|
21
|
+
* - Validate the returned `StructuredFindings` shape with a hand-rolled
|
|
22
|
+
* validator (matching the zero-dependency style of the other review-remote
|
|
23
|
+
* helpers) and raise a typed {@link StructuredFindingsError} on malformed
|
|
24
|
+
* output.
|
|
25
|
+
*/
|
|
26
|
+
import type { NarrowingResult } from './narrowing.js';
|
|
27
|
+
/**
|
|
28
|
+
* The dispatch-payload key that selects structured-output mode on the
|
|
29
|
+
* `oat-reviewer` agent. Mirrors the flag p03 added (verified against
|
|
30
|
+
* `.agents/agents/oat-reviewer.md`); parallels the existing
|
|
31
|
+
* `oat_review_invocation` dispatch-payload naming.
|
|
32
|
+
*/
|
|
33
|
+
export declare const STRUCTURED_OUTPUT_MODE_FLAG: "oat_output_mode";
|
|
34
|
+
/** The single accepted value of {@link STRUCTURED_OUTPUT_MODE_FLAG}. */
|
|
35
|
+
export declare const STRUCTURED_OUTPUT_MODE_VALUE: "structured";
|
|
36
|
+
export type FindingSeverity = 'critical' | 'important' | 'medium' | 'minor';
|
|
37
|
+
/** A single structured finding (see design.md → Data Models). */
|
|
38
|
+
export interface StructuredFinding {
|
|
39
|
+
/** Stable per-dispatch ID with a C/I/M/m prefix. */
|
|
40
|
+
id: string;
|
|
41
|
+
severity: FindingSeverity;
|
|
42
|
+
title: string;
|
|
43
|
+
/** Repo-relative path; both `file` and `line` are set or both `null`. */
|
|
44
|
+
file: string | null;
|
|
45
|
+
/** 1-based line in the post-image; paired with `file`. */
|
|
46
|
+
line: number | null;
|
|
47
|
+
body: string;
|
|
48
|
+
fix_guidance: string | null;
|
|
49
|
+
}
|
|
50
|
+
/** Typed return shape from `oat-reviewer` in structured-output mode. */
|
|
51
|
+
export interface StructuredFindings {
|
|
52
|
+
summary: string;
|
|
53
|
+
findings: StructuredFinding[];
|
|
54
|
+
verification_commands: string[];
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Raw response envelope from a provider dispatch. The reviewer's structured
|
|
58
|
+
* return lands on `findings`; the wrapper validates it before returning.
|
|
59
|
+
*/
|
|
60
|
+
export interface RawAgentResponse {
|
|
61
|
+
findings: unknown;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Narrow dispatcher interface. The skill provides the real provider dispatch
|
|
65
|
+
* (Claude Code Task / Cursor invocation / Codex spawn); tests pass stubs.
|
|
66
|
+
*/
|
|
67
|
+
export interface Dispatcher {
|
|
68
|
+
spawn(payload: Record<string, unknown>): Promise<RawAgentResponse>;
|
|
69
|
+
}
|
|
70
|
+
/** Context the wrapper needs to build the structured-output dispatch payload. */
|
|
71
|
+
export interface ReviewDispatchContext {
|
|
72
|
+
/** Resolved OAT project directory path. */
|
|
73
|
+
projectPath: string;
|
|
74
|
+
/** Current scope token (`pNN`, `final`, …). */
|
|
75
|
+
scope: string;
|
|
76
|
+
/** Full 40-char PR HEAD SHA being reviewed. */
|
|
77
|
+
headSha: string;
|
|
78
|
+
/** The Review Scope metadata block the reviewer consumes as its prompt. */
|
|
79
|
+
reviewScopeMetadata: string;
|
|
80
|
+
/** Pointer to the posted-review-body schema the skill will build against. */
|
|
81
|
+
postedBodySchemaRef: string;
|
|
82
|
+
/** Resolved re-review narrowing decision from {@link pickNarrowingTarget}. */
|
|
83
|
+
narrowing: NarrowingResult;
|
|
84
|
+
}
|
|
85
|
+
/** Typed error raised when the reviewer returns a malformed structured shape. */
|
|
86
|
+
export declare class StructuredFindingsError extends Error {
|
|
87
|
+
constructor(message: string);
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Build the dispatch payload for a structured-output reviewer run. The
|
|
91
|
+
* structured-mode flag is always set; the narrowing range is included only when
|
|
92
|
+
* the guard actually produced one (`narrow-range`), otherwise it is `null`.
|
|
93
|
+
*/
|
|
94
|
+
export declare function buildDispatchPayload(context: ReviewDispatchContext): Record<string, unknown>;
|
|
95
|
+
/**
|
|
96
|
+
* Validate an unknown value against the `StructuredFindings` contract. Throws a
|
|
97
|
+
* {@link StructuredFindingsError} on the first violation; returns the typed
|
|
98
|
+
* object otherwise.
|
|
99
|
+
*/
|
|
100
|
+
export declare function validateStructuredFindings(value: unknown): StructuredFindings;
|
|
101
|
+
/**
|
|
102
|
+
* Run a Tier-1 structured-output review: build the payload, dispatch once, and
|
|
103
|
+
* validate the returned findings. Dispatcher errors propagate unchanged (no
|
|
104
|
+
* retry) so the skill can decide on a Tier 2/3 fallback. Malformed reviewer
|
|
105
|
+
* output raises {@link StructuredFindingsError}.
|
|
106
|
+
*/
|
|
107
|
+
export declare function dispatchStructuredReview(context: ReviewDispatchContext, dispatcher: Dispatcher): Promise<StructuredFindings>;
|
|
108
|
+
//# sourceMappingURL=reviewer-dispatch.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reviewer-dispatch.d.ts","sourceRoot":"","sources":["../../src/review-remote/reviewer-dispatch.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAEnD;;;;;GAKG;AACH,eAAO,MAAM,2BAA2B,EAAG,iBAA0B,CAAC;AAEtE,wEAAwE;AACxE,eAAO,MAAM,4BAA4B,EAAG,YAAqB,CAAC;AAElE,MAAM,MAAM,eAAe,GAAG,UAAU,GAAG,WAAW,GAAG,QAAQ,GAAG,OAAO,CAAC;AAS5E,iEAAiE;AACjE,MAAM,WAAW,iBAAiB;IAChC,oDAAoD;IACpD,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,eAAe,CAAC;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,yEAAyE;IACzE,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,0DAA0D;IAC1D,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B;AAED,wEAAwE;AACxE,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,iBAAiB,EAAE,CAAC;IAC9B,qBAAqB,EAAE,MAAM,EAAE,CAAC;CACjC;AAED;;;GAGG;AACH,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED;;;GAGG;AACH,MAAM,WAAW,UAAU;IACzB,KAAK,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAAC;CACpE;AAED,iFAAiF;AACjF,MAAM,WAAW,qBAAqB;IACpC,2CAA2C;IAC3C,WAAW,EAAE,MAAM,CAAC;IACpB,+CAA+C;IAC/C,KAAK,EAAE,MAAM,CAAC;IACd,+CAA+C;IAC/C,OAAO,EAAE,MAAM,CAAC;IAChB,2EAA2E;IAC3E,mBAAmB,EAAE,MAAM,CAAC;IAC5B,6EAA6E;IAC7E,mBAAmB,EAAE,MAAM,CAAC;IAC5B,8EAA8E;IAC9E,SAAS,EAAE,eAAe,CAAC;CAC5B;AAED,iFAAiF;AACjF,qBAAa,uBAAwB,SAAQ,KAAK;gBACpC,OAAO,EAAE,MAAM;CAI5B;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,qBAAqB,GAC7B,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAezB;AAgED;;;;GAIG;AACH,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,OAAO,GAAG,kBAAkB,CA2B7E;AAED;;;;;GAKG;AACH,wBAAsB,wBAAwB,CAC5C,OAAO,EAAE,qBAAqB,EAC9B,UAAU,EAAE,UAAU,GACrB,OAAO,CAAC,kBAAkB,CAAC,CAK7B"}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tier-1 dispatch wrapper for the `oat-reviewer` subagent's structured-output
|
|
3
|
+
* mode (see design.md → Component Design → `oat-project-review-provide-remote`
|
|
4
|
+
* and `.agents/agents/oat-reviewer.md` → Structured-Output Mode).
|
|
5
|
+
*
|
|
6
|
+
* The project-rail provide-remote skill's Tier 1 dispatch hands the reviewer a
|
|
7
|
+
* payload carrying the project context, the posted-review-body schema
|
|
8
|
+
* reference, the resolved re-review narrowing range, and the
|
|
9
|
+
* `oat_output_mode: structured` flag. In that mode the reviewer returns a
|
|
10
|
+
* `StructuredFindings` object in-memory rather than writing a review artifact;
|
|
11
|
+
* this wrapper validates that object and hands it back to the skill, which is
|
|
12
|
+
* then responsible for building the posted body and posting to GitHub.
|
|
13
|
+
*
|
|
14
|
+
* Responsibilities are deliberately narrow:
|
|
15
|
+
*
|
|
16
|
+
* - Build the dispatch payload with the structured-output flag p03 wired.
|
|
17
|
+
* - Forward it to an injected {@link Dispatcher} (real provider dispatch in the
|
|
18
|
+
* skill; stubs in tests).
|
|
19
|
+
* - Surface dispatcher errors to the caller WITHOUT retry — the Tier 2/3
|
|
20
|
+
* fallback decision belongs to the skill, not this wrapper.
|
|
21
|
+
* - Validate the returned `StructuredFindings` shape with a hand-rolled
|
|
22
|
+
* validator (matching the zero-dependency style of the other review-remote
|
|
23
|
+
* helpers) and raise a typed {@link StructuredFindingsError} on malformed
|
|
24
|
+
* output.
|
|
25
|
+
*/
|
|
26
|
+
/**
|
|
27
|
+
* The dispatch-payload key that selects structured-output mode on the
|
|
28
|
+
* `oat-reviewer` agent. Mirrors the flag p03 added (verified against
|
|
29
|
+
* `.agents/agents/oat-reviewer.md`); parallels the existing
|
|
30
|
+
* `oat_review_invocation` dispatch-payload naming.
|
|
31
|
+
*/
|
|
32
|
+
export const STRUCTURED_OUTPUT_MODE_FLAG = 'oat_output_mode';
|
|
33
|
+
/** The single accepted value of {@link STRUCTURED_OUTPUT_MODE_FLAG}. */
|
|
34
|
+
export const STRUCTURED_OUTPUT_MODE_VALUE = 'structured';
|
|
35
|
+
const SEVERITIES = new Set([
|
|
36
|
+
'critical',
|
|
37
|
+
'important',
|
|
38
|
+
'medium',
|
|
39
|
+
'minor',
|
|
40
|
+
]);
|
|
41
|
+
/** Typed error raised when the reviewer returns a malformed structured shape. */
|
|
42
|
+
export class StructuredFindingsError extends Error {
|
|
43
|
+
constructor(message) {
|
|
44
|
+
super(message);
|
|
45
|
+
this.name = 'StructuredFindingsError';
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Build the dispatch payload for a structured-output reviewer run. The
|
|
50
|
+
* structured-mode flag is always set; the narrowing range is included only when
|
|
51
|
+
* the guard actually produced one (`narrow-range`), otherwise it is `null`.
|
|
52
|
+
*/
|
|
53
|
+
export function buildDispatchPayload(context) {
|
|
54
|
+
const narrowingRange = context.narrowing.kind === 'narrow-range'
|
|
55
|
+
? `${context.narrowing.priorSha}..${context.narrowing.headSha}`
|
|
56
|
+
: null;
|
|
57
|
+
return {
|
|
58
|
+
[STRUCTURED_OUTPUT_MODE_FLAG]: STRUCTURED_OUTPUT_MODE_VALUE,
|
|
59
|
+
oat_project: context.projectPath,
|
|
60
|
+
oat_review_scope: context.scope,
|
|
61
|
+
oat_review_head_sha: context.headSha,
|
|
62
|
+
review_scope_metadata: context.reviewScopeMetadata,
|
|
63
|
+
posted_body_schema_ref: context.postedBodySchemaRef,
|
|
64
|
+
narrowing_range: narrowingRange,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
function isObject(value) {
|
|
68
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
69
|
+
}
|
|
70
|
+
function validateFinding(value, index) {
|
|
71
|
+
const at = `findings[${index}]`;
|
|
72
|
+
if (!isObject(value)) {
|
|
73
|
+
throw new StructuredFindingsError(`${at} must be an object.`);
|
|
74
|
+
}
|
|
75
|
+
if (typeof value['id'] !== 'string' || value['id'] === '') {
|
|
76
|
+
throw new StructuredFindingsError(`${at}.id must be a non-empty string.`);
|
|
77
|
+
}
|
|
78
|
+
if (typeof value['severity'] !== 'string' ||
|
|
79
|
+
!SEVERITIES.has(value['severity'])) {
|
|
80
|
+
throw new StructuredFindingsError(`${at}.severity must be one of critical|important|medium|minor.`);
|
|
81
|
+
}
|
|
82
|
+
if (typeof value['title'] !== 'string') {
|
|
83
|
+
throw new StructuredFindingsError(`${at}.title must be a string.`);
|
|
84
|
+
}
|
|
85
|
+
if (typeof value['body'] !== 'string') {
|
|
86
|
+
throw new StructuredFindingsError(`${at}.body must be a string.`);
|
|
87
|
+
}
|
|
88
|
+
const file = value['file'];
|
|
89
|
+
const line = value['line'];
|
|
90
|
+
const fileSet = file !== null;
|
|
91
|
+
const lineSet = line !== null;
|
|
92
|
+
if (fileSet !== lineSet) {
|
|
93
|
+
throw new StructuredFindingsError(`${at} must set both file and line, or set both to null.`);
|
|
94
|
+
}
|
|
95
|
+
if (fileSet && typeof file !== 'string') {
|
|
96
|
+
throw new StructuredFindingsError(`${at}.file must be a string or null.`);
|
|
97
|
+
}
|
|
98
|
+
if (lineSet && typeof line !== 'number') {
|
|
99
|
+
throw new StructuredFindingsError(`${at}.line must be a number or null.`);
|
|
100
|
+
}
|
|
101
|
+
const fixGuidance = value['fix_guidance'];
|
|
102
|
+
if (fixGuidance !== null && typeof fixGuidance !== 'string') {
|
|
103
|
+
throw new StructuredFindingsError(`${at}.fix_guidance must be a string or null.`);
|
|
104
|
+
}
|
|
105
|
+
return {
|
|
106
|
+
id: value['id'],
|
|
107
|
+
severity: value['severity'],
|
|
108
|
+
title: value['title'],
|
|
109
|
+
file: fileSet ? file : null,
|
|
110
|
+
line: lineSet ? line : null,
|
|
111
|
+
body: value['body'],
|
|
112
|
+
fix_guidance: fixGuidance === null ? null : fixGuidance,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Validate an unknown value against the `StructuredFindings` contract. Throws a
|
|
117
|
+
* {@link StructuredFindingsError} on the first violation; returns the typed
|
|
118
|
+
* object otherwise.
|
|
119
|
+
*/
|
|
120
|
+
export function validateStructuredFindings(value) {
|
|
121
|
+
if (!isObject(value)) {
|
|
122
|
+
throw new StructuredFindingsError('StructuredFindings must be an object.');
|
|
123
|
+
}
|
|
124
|
+
if (typeof value['summary'] !== 'string') {
|
|
125
|
+
throw new StructuredFindingsError('summary must be a string.');
|
|
126
|
+
}
|
|
127
|
+
if (!Array.isArray(value['findings'])) {
|
|
128
|
+
throw new StructuredFindingsError('findings must be an array.');
|
|
129
|
+
}
|
|
130
|
+
const commands = value['verification_commands'];
|
|
131
|
+
if (!Array.isArray(commands) ||
|
|
132
|
+
!commands.every((c) => typeof c === 'string')) {
|
|
133
|
+
throw new StructuredFindingsError('verification_commands must be an array of strings.');
|
|
134
|
+
}
|
|
135
|
+
const findings = value['findings'].map((f, i) => validateFinding(f, i));
|
|
136
|
+
return {
|
|
137
|
+
summary: value['summary'],
|
|
138
|
+
findings,
|
|
139
|
+
verification_commands: commands,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Run a Tier-1 structured-output review: build the payload, dispatch once, and
|
|
144
|
+
* validate the returned findings. Dispatcher errors propagate unchanged (no
|
|
145
|
+
* retry) so the skill can decide on a Tier 2/3 fallback. Malformed reviewer
|
|
146
|
+
* output raises {@link StructuredFindingsError}.
|
|
147
|
+
*/
|
|
148
|
+
export async function dispatchStructuredReview(context, dispatcher) {
|
|
149
|
+
const payload = buildDispatchPayload(context);
|
|
150
|
+
// No try/catch — a dispatcher error surfaces to the skill without retry.
|
|
151
|
+
const response = await dispatcher.spawn(payload);
|
|
152
|
+
return validateStructuredFindings(response.findings);
|
|
153
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ephemeral worktree lifecycle helper (see design.md → Data Flow step 2).
|
|
3
|
+
*
|
|
4
|
+
* The provide-remote skills review a PR without mutating the caller's working
|
|
5
|
+
* tree. They acquire an ephemeral, repo-scoped worktree, run `gh pr checkout`
|
|
6
|
+
* INSIDE it (that step lives in the skill, not here), review, then tear the
|
|
7
|
+
* worktree down. This helper owns only the create/run/release lifecycle:
|
|
8
|
+
*
|
|
9
|
+
* - `acquireWorktree({ repoRoot })` — `mktemp -d` an ephemeral path OUTSIDE
|
|
10
|
+
* `repoRoot`, then `git -C "$repoRoot" worktree add --detach <path> HEAD`.
|
|
11
|
+
* The `-C "$repoRoot"` flag is load-bearing: it lets the command run even
|
|
12
|
+
* when the caller's CWD is not inside the repository (a thin remote-review
|
|
13
|
+
* machine invoking the skill from a home directory). `HEAD` is a
|
|
14
|
+
* placeholder ref the skill overwrites with `gh pr checkout <N>`.
|
|
15
|
+
* - `runInWorktree(handle, cb)` — invoke `cb(worktreePath)`. The helper does
|
|
16
|
+
* NOT chdir the host process; it passes the path so callers run repo-scoped
|
|
17
|
+
* git / `cd "$path" && gh pr checkout` in a subshell. This keeps the
|
|
18
|
+
* caller's CWD unchanged (verified by tests).
|
|
19
|
+
* - `releaseWorktree(handle)` — `git -C "$repoRoot" worktree remove --force`
|
|
20
|
+
* then remove the temp directory. Idempotent and safe even if the worktree
|
|
21
|
+
* never populated, so callers run it in a `finally`.
|
|
22
|
+
*
|
|
23
|
+
* Design Open Question resolution: `oat-worktree-bootstrap-auto` reuse was
|
|
24
|
+
* considered but the helper is hand-rolled per the design fallback. The plan's
|
|
25
|
+
* mechanics (repo-scoped git invocation, ephemeral path outside repo root,
|
|
26
|
+
* force-removal teardown) are implemented directly here; the helper stays
|
|
27
|
+
* agnostic to PR checkout so it has no dependency on the bootstrap contract.
|
|
28
|
+
*/
|
|
29
|
+
export interface AcquireWorktreeOptions {
|
|
30
|
+
/** Absolute path to the repository root (`git rev-parse --show-toplevel`). */
|
|
31
|
+
repoRoot: string;
|
|
32
|
+
}
|
|
33
|
+
/** Opaque handle returned by {@link acquireWorktree}. */
|
|
34
|
+
export interface WorktreeHandle {
|
|
35
|
+
/** The repository root the worktree was created against. */
|
|
36
|
+
repoRoot: string;
|
|
37
|
+
/** The ephemeral worktree path (outside `repoRoot`). */
|
|
38
|
+
worktreePath: string;
|
|
39
|
+
/** Internal: whether the worktree is still live (false after release). */
|
|
40
|
+
released: boolean;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Create an ephemeral, detached worktree outside the repo root and register it
|
|
44
|
+
* with git. The path is created via `mktemp`-style `mkdtempSync` under the
|
|
45
|
+
* system temp dir, so it is guaranteed to be outside `repoRoot`.
|
|
46
|
+
*/
|
|
47
|
+
export declare function acquireWorktree(options: AcquireWorktreeOptions): Promise<WorktreeHandle>;
|
|
48
|
+
/**
|
|
49
|
+
* Run a callback "inside" the worktree. The callback receives the worktree
|
|
50
|
+
* path; the helper does not change the host process's working directory, so
|
|
51
|
+
* callers must scope filesystem / git operations to that path themselves
|
|
52
|
+
* (e.g., `git -C <path> …` or `cd <path> && gh pr checkout` in a subshell).
|
|
53
|
+
*/
|
|
54
|
+
export declare function runInWorktree<T>(handle: WorktreeHandle, callback: (worktreePath: string) => Promise<T>): Promise<T>;
|
|
55
|
+
/**
|
|
56
|
+
* Remove the git worktree (force) and clean up the temp directory. Idempotent:
|
|
57
|
+
* a second call is a no-op, and a failed `git worktree remove` (e.g., the
|
|
58
|
+
* worktree never populated, or was already pruned) does not prevent the temp
|
|
59
|
+
* directory cleanup. Safe to call in a `finally`.
|
|
60
|
+
*/
|
|
61
|
+
export declare function releaseWorktree(handle: WorktreeHandle): Promise<void>;
|
|
62
|
+
//# sourceMappingURL=worktree.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"worktree.d.ts","sourceRoot":"","sources":["../../src/review-remote/worktree.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAUH,MAAM,WAAW,sBAAsB;IACrC,8EAA8E;IAC9E,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,yDAAyD;AACzD,MAAM,WAAW,cAAc;IAC7B,4DAA4D;IAC5D,QAAQ,EAAE,MAAM,CAAC;IACjB,wDAAwD;IACxD,YAAY,EAAE,MAAM,CAAC;IACrB,0EAA0E;IAC1E,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED;;;;GAIG;AACH,wBAAsB,eAAe,CACnC,OAAO,EAAE,sBAAsB,GAC9B,OAAO,CAAC,cAAc,CAAC,CAmCzB;AAED;;;;;GAKG;AACH,wBAAsB,aAAa,CAAC,CAAC,EACnC,MAAM,EAAE,cAAc,EACtB,QAAQ,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,GAC7C,OAAO,CAAC,CAAC,CAAC,CAEZ;AAED;;;;;GAKG;AACH,wBAAsB,eAAe,CAAC,MAAM,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CA2B3E"}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ephemeral worktree lifecycle helper (see design.md → Data Flow step 2).
|
|
3
|
+
*
|
|
4
|
+
* The provide-remote skills review a PR without mutating the caller's working
|
|
5
|
+
* tree. They acquire an ephemeral, repo-scoped worktree, run `gh pr checkout`
|
|
6
|
+
* INSIDE it (that step lives in the skill, not here), review, then tear the
|
|
7
|
+
* worktree down. This helper owns only the create/run/release lifecycle:
|
|
8
|
+
*
|
|
9
|
+
* - `acquireWorktree({ repoRoot })` — `mktemp -d` an ephemeral path OUTSIDE
|
|
10
|
+
* `repoRoot`, then `git -C "$repoRoot" worktree add --detach <path> HEAD`.
|
|
11
|
+
* The `-C "$repoRoot"` flag is load-bearing: it lets the command run even
|
|
12
|
+
* when the caller's CWD is not inside the repository (a thin remote-review
|
|
13
|
+
* machine invoking the skill from a home directory). `HEAD` is a
|
|
14
|
+
* placeholder ref the skill overwrites with `gh pr checkout <N>`.
|
|
15
|
+
* - `runInWorktree(handle, cb)` — invoke `cb(worktreePath)`. The helper does
|
|
16
|
+
* NOT chdir the host process; it passes the path so callers run repo-scoped
|
|
17
|
+
* git / `cd "$path" && gh pr checkout` in a subshell. This keeps the
|
|
18
|
+
* caller's CWD unchanged (verified by tests).
|
|
19
|
+
* - `releaseWorktree(handle)` — `git -C "$repoRoot" worktree remove --force`
|
|
20
|
+
* then remove the temp directory. Idempotent and safe even if the worktree
|
|
21
|
+
* never populated, so callers run it in a `finally`.
|
|
22
|
+
*
|
|
23
|
+
* Design Open Question resolution: `oat-worktree-bootstrap-auto` reuse was
|
|
24
|
+
* considered but the helper is hand-rolled per the design fallback. The plan's
|
|
25
|
+
* mechanics (repo-scoped git invocation, ephemeral path outside repo root,
|
|
26
|
+
* force-removal teardown) are implemented directly here; the helper stays
|
|
27
|
+
* agnostic to PR checkout so it has no dependency on the bootstrap contract.
|
|
28
|
+
*/
|
|
29
|
+
import { execFile as execFileCallback } from 'node:child_process';
|
|
30
|
+
import { mkdtempSync, rmSync } from 'node:fs';
|
|
31
|
+
import { tmpdir } from 'node:os';
|
|
32
|
+
import { join } from 'node:path';
|
|
33
|
+
import { promisify } from 'node:util';
|
|
34
|
+
const execFile = promisify(execFileCallback);
|
|
35
|
+
/**
|
|
36
|
+
* Create an ephemeral, detached worktree outside the repo root and register it
|
|
37
|
+
* with git. The path is created via `mktemp`-style `mkdtempSync` under the
|
|
38
|
+
* system temp dir, so it is guaranteed to be outside `repoRoot`.
|
|
39
|
+
*/
|
|
40
|
+
export async function acquireWorktree(options) {
|
|
41
|
+
const { repoRoot } = options;
|
|
42
|
+
// Ephemeral path under the OS temp dir — outside repoRoot by construction.
|
|
43
|
+
const worktreePath = mkdtempSync(join(tmpdir(), 'oat-review-wt-'));
|
|
44
|
+
// `git worktree add` requires the target path to NOT already exist, so remove
|
|
45
|
+
// the placeholder mkdtemp directory and let git create it fresh.
|
|
46
|
+
rmSync(worktreePath, { recursive: true, force: true });
|
|
47
|
+
try {
|
|
48
|
+
await execFile('git', [
|
|
49
|
+
'-C',
|
|
50
|
+
repoRoot,
|
|
51
|
+
'worktree',
|
|
52
|
+
'add',
|
|
53
|
+
'--detach',
|
|
54
|
+
worktreePath,
|
|
55
|
+
'HEAD',
|
|
56
|
+
]);
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
// `git worktree add` can fail after partially creating the target dir
|
|
60
|
+
// and/or registering `.git/worktrees/<name>` metadata. Because we throw
|
|
61
|
+
// without returning a handle, the caller's `finally { releaseWorktree }`
|
|
62
|
+
// never runs — so clean up the partial worktree here before rethrowing.
|
|
63
|
+
// Best-effort: prune dangling worktree metadata, then remove the temp dir.
|
|
64
|
+
try {
|
|
65
|
+
await execFile('git', ['-C', repoRoot, 'worktree', 'prune']);
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
// Best-effort; ignore (e.g. repoRoot is not a git repo).
|
|
69
|
+
}
|
|
70
|
+
rmSync(worktreePath, { recursive: true, force: true });
|
|
71
|
+
throw error;
|
|
72
|
+
}
|
|
73
|
+
return { repoRoot, worktreePath, released: false };
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Run a callback "inside" the worktree. The callback receives the worktree
|
|
77
|
+
* path; the helper does not change the host process's working directory, so
|
|
78
|
+
* callers must scope filesystem / git operations to that path themselves
|
|
79
|
+
* (e.g., `git -C <path> …` or `cd <path> && gh pr checkout` in a subshell).
|
|
80
|
+
*/
|
|
81
|
+
export async function runInWorktree(handle, callback) {
|
|
82
|
+
return callback(handle.worktreePath);
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Remove the git worktree (force) and clean up the temp directory. Idempotent:
|
|
86
|
+
* a second call is a no-op, and a failed `git worktree remove` (e.g., the
|
|
87
|
+
* worktree never populated, or was already pruned) does not prevent the temp
|
|
88
|
+
* directory cleanup. Safe to call in a `finally`.
|
|
89
|
+
*/
|
|
90
|
+
export async function releaseWorktree(handle) {
|
|
91
|
+
if (handle.released) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
handle.released = true;
|
|
95
|
+
try {
|
|
96
|
+
await execFile('git', [
|
|
97
|
+
'-C',
|
|
98
|
+
handle.repoRoot,
|
|
99
|
+
'worktree',
|
|
100
|
+
'remove',
|
|
101
|
+
'--force',
|
|
102
|
+
handle.worktreePath,
|
|
103
|
+
]);
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
// `worktree remove` can fail if the worktree was never populated or was
|
|
107
|
+
// already removed. That is non-fatal — proceed to temp cleanup so we never
|
|
108
|
+
// leak the directory, then prune stale worktree metadata best-effort.
|
|
109
|
+
try {
|
|
110
|
+
await execFile('git', ['-C', handle.repoRoot, 'worktree', 'prune']);
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
// Best-effort; ignore.
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
rmSync(handle.worktreePath, { recursive: true, force: true });
|
|
117
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-agent-toolkit/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.15",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Open Agent Toolkit CLI",
|
|
6
6
|
"homepage": "https://github.com/voxmedia/open-agent-toolkit/tree/main/packages/cli",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"ora": "^9.0.0",
|
|
35
35
|
"yaml": "2.8.2",
|
|
36
36
|
"zod": "^3.25.76",
|
|
37
|
-
"@open-agent-toolkit/control-plane": "0.1.
|
|
37
|
+
"@open-agent-toolkit/control-plane": "0.1.15"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
40
|
"@types/node": "^22.10.0",
|