@open-agent-toolkit/cli 0.1.13 → 0.1.14

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 (38) hide show
  1. package/assets/agents/oat-reviewer.md +54 -10
  2. package/assets/docs/workflows/projects/reviews.md +21 -2
  3. package/assets/docs/workflows/skills/index.md +2 -0
  4. package/assets/public-package-versions.json +4 -4
  5. package/assets/skills/oat-project-review-provide-remote/SKILL.md +354 -0
  6. package/assets/skills/oat-project-review-receive/SKILL.md +7 -10
  7. package/assets/skills/oat-project-review-receive-remote/SKILL.md +4 -4
  8. package/assets/skills/oat-review-provide-remote/SKILL.md +273 -0
  9. package/assets/skills/oat-review-receive/SKILL.md +6 -6
  10. package/assets/skills/oat-review-receive-remote/SKILL.md +5 -5
  11. package/dist/commands/init/tools/shared/skill-manifest.d.ts +2 -2
  12. package/dist/commands/init/tools/shared/skill-manifest.d.ts.map +1 -1
  13. package/dist/commands/init/tools/shared/skill-manifest.js +2 -0
  14. package/dist/review-remote/body-builder.d.ts +79 -0
  15. package/dist/review-remote/body-builder.d.ts.map +1 -0
  16. package/dist/review-remote/body-builder.js +103 -0
  17. package/dist/review-remote/capability-probe.d.ts +61 -0
  18. package/dist/review-remote/capability-probe.d.ts.map +1 -0
  19. package/dist/review-remote/capability-probe.js +87 -0
  20. package/dist/review-remote/line-mapper.d.ts +81 -0
  21. package/dist/review-remote/line-mapper.d.ts.map +1 -0
  22. package/dist/review-remote/line-mapper.js +165 -0
  23. package/dist/review-remote/marker-parser.d.ts +44 -0
  24. package/dist/review-remote/marker-parser.d.ts.map +1 -0
  25. package/dist/review-remote/marker-parser.js +97 -0
  26. package/dist/review-remote/narrowing.d.ts +81 -0
  27. package/dist/review-remote/narrowing.d.ts.map +1 -0
  28. package/dist/review-remote/narrowing.js +90 -0
  29. package/dist/review-remote/project-resolver.d.ts +46 -0
  30. package/dist/review-remote/project-resolver.d.ts.map +1 -0
  31. package/dist/review-remote/project-resolver.js +60 -0
  32. package/dist/review-remote/reviewer-dispatch.d.ts +108 -0
  33. package/dist/review-remote/reviewer-dispatch.d.ts.map +1 -0
  34. package/dist/review-remote/reviewer-dispatch.js +153 -0
  35. package/dist/review-remote/worktree.d.ts +62 -0
  36. package/dist/review-remote/worktree.d.ts.map +1 -0
  37. package/dist/review-remote/worktree.js +117 -0
  38. package/package.json +2 -2
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Project resolution helper for the project-rail provide-remote skill
3
+ * (see design.md → Component Design → `oat-project-review-provide-remote`).
4
+ *
5
+ * Locates the OAT project on machine B by scanning the PR diff for
6
+ * `state.md` files two levels deep under `.oat/projects/` (scope/project). An
7
+ * explicit `--project <path>` override takes precedence over the scan and is
8
+ * validated to resolve to a directory containing `state.md`.
9
+ *
10
+ * The result is a discriminated union mirroring {@link NarrowingResult}'s
11
+ * pattern so callers branch on `kind`.
12
+ */
13
+ export interface ResolvedProject {
14
+ kind: 'resolved';
15
+ /** Project directory path (no trailing slash, no `state.md` suffix). */
16
+ projectPath: string;
17
+ }
18
+ export interface AmbiguousProject {
19
+ kind: 'ambiguous';
20
+ /** Sorted, de-duplicated candidate project directory paths. */
21
+ candidates: string[];
22
+ }
23
+ export interface ProjectNotFound {
24
+ kind: 'not-found';
25
+ }
26
+ export interface InvalidOverride {
27
+ kind: 'invalid-override';
28
+ overridePath: string;
29
+ message: string;
30
+ }
31
+ export type ResolveResult = ResolvedProject | AmbiguousProject | ProjectNotFound | InvalidOverride;
32
+ export interface ResolveOptions {
33
+ /** Explicit `--project <path>` override; takes precedence over diff scan. */
34
+ overridePath?: string;
35
+ /**
36
+ * Existence probe for `state.md`. Injected so tests avoid the real
37
+ * filesystem. Receives a candidate `state.md` path.
38
+ */
39
+ pathExists?: (path: string) => boolean;
40
+ }
41
+ /**
42
+ * Resolve the target OAT project from a PR's changed-file list, honoring an
43
+ * explicit override.
44
+ */
45
+ export declare function resolveProject(diffFiles: string[], options?: ResolveOptions): ResolveResult;
46
+ //# sourceMappingURL=project-resolver.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"project-resolver.d.ts","sourceRoot":"","sources":["../../src/review-remote/project-resolver.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAKH,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,UAAU,CAAC;IACjB,wEAAwE;IACxE,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,WAAW,CAAC;IAClB,+DAA+D;IAC/D,UAAU,EAAE,MAAM,EAAE,CAAC;CACtB;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,WAAW,CAAC;CACnB;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,kBAAkB,CAAC;IACzB,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,MAAM,aAAa,GACrB,eAAe,GACf,gBAAgB,GAChB,eAAe,GACf,eAAe,CAAC;AAEpB,MAAM,WAAW,cAAc;IAC7B,6EAA6E;IAC7E,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;OAGG;IACH,UAAU,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC;CACxC;AAaD;;;GAGG;AACH,wBAAgB,cAAc,CAC5B,SAAS,EAAE,MAAM,EAAE,EACnB,OAAO,GAAE,cAAmB,GAC3B,aAAa,CAgCf"}
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Project resolution helper for the project-rail provide-remote skill
3
+ * (see design.md → Component Design → `oat-project-review-provide-remote`).
4
+ *
5
+ * Locates the OAT project on machine B by scanning the PR diff for
6
+ * `state.md` files two levels deep under `.oat/projects/` (scope/project). An
7
+ * explicit `--project <path>` override takes precedence over the scan and is
8
+ * validated to resolve to a directory containing `state.md`.
9
+ *
10
+ * The result is a discriminated union mirroring {@link NarrowingResult}'s
11
+ * pattern so callers branch on `kind`.
12
+ */
13
+ /** Matches a two-level `.oat/projects/<scope>/<project>/state.md` path. */
14
+ const STATE_MD_PATTERN = /^\.oat\/projects\/([^/]+)\/([^/]+)\/state\.md$/;
15
+ /** Strip a trailing slash and a trailing `state.md` segment from a path. */
16
+ function normalizeProjectDir(path) {
17
+ let dir = path.replace(/\/+$/, '');
18
+ if (dir.endsWith('/state.md')) {
19
+ dir = dir.slice(0, -'/state.md'.length);
20
+ }
21
+ else if (dir === 'state.md') {
22
+ dir = '';
23
+ }
24
+ return dir;
25
+ }
26
+ /**
27
+ * Resolve the target OAT project from a PR's changed-file list, honoring an
28
+ * explicit override.
29
+ */
30
+ export function resolveProject(diffFiles, options = {}) {
31
+ // 1. Explicit override wins, but must point at a real project (has state.md).
32
+ if (options.overridePath !== undefined && options.overridePath !== '') {
33
+ const projectDir = normalizeProjectDir(options.overridePath);
34
+ const stateMdPath = `${projectDir}/state.md`;
35
+ const exists = options.pathExists?.(stateMdPath) ?? false;
36
+ if (!exists) {
37
+ return {
38
+ kind: 'invalid-override',
39
+ overridePath: options.overridePath,
40
+ message: `--project path "${options.overridePath}" does not resolve to a directory containing state.md.`,
41
+ };
42
+ }
43
+ return { kind: 'resolved', projectPath: projectDir };
44
+ }
45
+ // 2. Diff scan for `.oat/projects/<scope>/<project>/state.md`.
46
+ const candidates = new Set();
47
+ for (const file of diffFiles) {
48
+ const match = file.match(STATE_MD_PATTERN);
49
+ if (match) {
50
+ candidates.add(`.oat/projects/${match[1]}/${match[2]}`);
51
+ }
52
+ }
53
+ if (candidates.size === 0) {
54
+ return { kind: 'not-found' };
55
+ }
56
+ if (candidates.size > 1) {
57
+ return { kind: 'ambiguous', candidates: [...candidates].sort() };
58
+ }
59
+ return { kind: 'resolved', projectPath: [...candidates][0] };
60
+ }
@@ -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.13",
3
+ "version": "0.1.14",
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.13"
37
+ "@open-agent-toolkit/control-plane": "0.1.14"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@types/node": "^22.10.0",