@cat-factory/orchestration 0.110.0 → 0.112.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/modules/board/BoardService.d.ts +27 -0
- package/dist/modules/board/BoardService.d.ts.map +1 -1
- package/dist/modules/board/BoardService.js +66 -1
- package/dist/modules/board/BoardService.js.map +1 -1
- package/dist/modules/execution/PrReviewController.d.ts +16 -6
- package/dist/modules/execution/PrReviewController.d.ts.map +1 -1
- package/dist/modules/execution/PrReviewController.js +69 -15
- package/dist/modules/execution/PrReviewController.js.map +1 -1
- package/dist/modules/execution/RunDispatcher.d.ts +36 -0
- package/dist/modules/execution/RunDispatcher.d.ts.map +1 -1
- package/dist/modules/execution/RunDispatcher.js +149 -2
- package/dist/modules/execution/RunDispatcher.js.map +1 -1
- package/dist/modules/execution/prReview.logic.d.ts +21 -1
- package/dist/modules/execution/prReview.logic.d.ts.map +1 -1
- package/dist/modules/execution/prReview.logic.js +86 -0
- package/dist/modules/execution/prReview.logic.js.map +1 -1
- package/package.json +11 -11
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ConflictError, DEFAULT_RISK_POLICY, failureKindFromHarnessCause, getErrorMessage, getErrorReason, getProvider, INITIATIVE_ANALYST_AGENT_KIND, INITIATIVE_COMMITTER_AGENT_KIND, INITIATIVE_PLANNER_AGENT_KIND, isAsyncAgentExecutor, NotFoundError, parseLocalModelId, recordGateAttempt, registeredGateFactories, registeredStepResolverFactories, requireProvider, RunContendedError, sameSubtasks, } from '@cat-factory/kernel';
|
|
1
|
+
import { ConflictError, DEFAULT_RISK_POLICY, failureKindFromHarnessCause, FIXER_AGENT_KIND, getErrorMessage, getErrorReason, getProvider, INITIATIVE_ANALYST_AGENT_KIND, INITIATIVE_COMMITTER_AGENT_KIND, INITIATIVE_PLANNER_AGENT_KIND, isAsyncAgentExecutor, NotFoundError, parseLocalModelId, recordGateAttempt, registeredGateFactories, registeredStepResolverFactories, requireProvider, RunContendedError, sameSubtasks, } from '@cat-factory/kernel';
|
|
2
2
|
import { frameProfile, frontendOriginsForService, parseBlueprintService, parseSpecDoc, resolveAprioriWorkingBranch, } from '@cat-factory/contracts';
|
|
3
3
|
import { blueprintPostOp, commitInitiativeTracker, FORK_PROPOSER_KIND, PR_REVIEWER_KIND, hasTrait, isCompanionKind, isContainerBackedCompanion, INTERVIEW_GATE_TRAIT, moduleSlug, runRepoOps, specPostOp, TASK_ESTIMATOR_AGENT_KIND, } from '@cat-factory/agents';
|
|
4
4
|
import { DEPLOYER_AGENT_KIND, isDeployStep } from '@cat-factory/integrations';
|
|
@@ -19,6 +19,7 @@ import { MergeResolver } from './MergeResolver.js';
|
|
|
19
19
|
import { ReviewGateController } from './ReviewGateController.js';
|
|
20
20
|
import { ForkDecisionController } from './ForkDecisionController.js';
|
|
21
21
|
import { PrReviewController, PR_REVIEW_STEP_KIND } from './PrReviewController.js';
|
|
22
|
+
import { buildPrReviewPost, renderPrReviewFixerFeedback } from './prReview.logic.js';
|
|
22
23
|
import { DEFAULT_FORK_MAX_CHAT_TURNS, forkPhasePending, resolveForkTriState, shouldProposeForkAuto, } from './forkDecision.logic.js';
|
|
23
24
|
import { RunStateMachine } from './RunStateMachine.js';
|
|
24
25
|
import { StepGraph } from './StepGraph.js';
|
|
@@ -58,6 +59,19 @@ function parseRepoFromPullUrl(url) {
|
|
|
58
59
|
return undefined;
|
|
59
60
|
return { owner: match[1], repo: match[2] };
|
|
60
61
|
}
|
|
62
|
+
/**
|
|
63
|
+
* The PR number a `review` task targets: the explicit `prNumber` field wins, else parse it from
|
|
64
|
+
* the `prUrl` (`…/pull/42` on GitHub, `…/merge_requests/42` on GitLab). Undefined when neither
|
|
65
|
+
* yields one — the PR-review `fix`/`post` resolutions then report the PR unresolvable.
|
|
66
|
+
*/
|
|
67
|
+
function reviewPrNumber(block) {
|
|
68
|
+
const fields = block?.taskTypeFields;
|
|
69
|
+
if (typeof fields?.prNumber === 'number')
|
|
70
|
+
return fields.prNumber;
|
|
71
|
+
const url = fields?.prUrl?.trim();
|
|
72
|
+
const match = url ? /\/(?:pull|merge_requests)\/(\d+)/.exec(url) : null;
|
|
73
|
+
return match ? Number(match[1]) : undefined;
|
|
74
|
+
}
|
|
61
75
|
/**
|
|
62
76
|
* The `peerEnvUrls` provision input for the frame about to be provisioned: a comma-joined set of
|
|
63
77
|
* `slug=url` pairs for every target frame whose env is ALREADY ready this run — so a later
|
|
@@ -215,7 +229,7 @@ export class RunDispatcher {
|
|
|
215
229
|
* what the dispatch chain falls through to; all the deterministic / gate / inline-review
|
|
216
230
|
* kinds are claimed earlier by their own handlers (see {@link buildStepHandlerRegistry}).
|
|
217
231
|
*/
|
|
218
|
-
async handleAgentStep(ctx, dispatchKind) {
|
|
232
|
+
async handleAgentStep(ctx, dispatchKind, augmentContext) {
|
|
219
233
|
const { workspaceId, instance, step, block, isFinalStep, options } = ctx;
|
|
220
234
|
// Async (container) steps don't block: dispatch the job and park. The durable
|
|
221
235
|
// driver polls `pollAgentJob` between sleeps so the run can span far longer
|
|
@@ -228,6 +242,10 @@ export class RunDispatcher {
|
|
|
228
242
|
// job as a HELPER off the coder step (Phase A). The completion still records against the
|
|
229
243
|
// coder step, and the fork-proposal interceptor keys on `step.agentKind` + the fork state.
|
|
230
244
|
const context = await this.contextBuilder.buildContext(workspaceId, instance, step, isFinalStep, block, dispatchKind ? { agentKind: dispatchKind } : undefined);
|
|
245
|
+
// A caller re-dispatching this step under an overriding kind can fold extra context in
|
|
246
|
+
// (e.g. the PR-review `fix` resolution points the Fixer at the reviewed PR's head branch and
|
|
247
|
+
// hands it the selected findings). Runs before pre-ops / dispatch so the job body sees it.
|
|
248
|
+
augmentContext?.(context);
|
|
231
249
|
// A registered custom kind's PRE-ops run deterministic backend repo work before the
|
|
232
250
|
// agent dispatches (e.g. read a baseline `spec/` shard into the prompt). Gated on the
|
|
233
251
|
// step not having dispatched yet so a Workflows replay (jobId already set) doesn't
|
|
@@ -2280,6 +2298,19 @@ export class RunDispatcher {
|
|
|
2280
2298
|
canHandle: ({ step, block }) => forkPhasePending(step, resolveForkTriState(block.agentConfig)),
|
|
2281
2299
|
handle: (ctx) => this.handleForkDecisionPhase(ctx),
|
|
2282
2300
|
},
|
|
2301
|
+
// The PR deep-review RESOLUTION phase (PR 3): after the human resolved a parked review with
|
|
2302
|
+
// `fix` / `post`, `PrReviewController.resolve` re-armed this `pr-reviewer` step and woke the
|
|
2303
|
+
// driver. Claim it by the re-armed status so it re-dispatches as the Fixer (`fixing`) or
|
|
2304
|
+
// posts the selected findings as inline PR comments (`posting`) — never the generic
|
|
2305
|
+
// pr-reviewer clone the fallthrough would run. A `reviewing`/`awaiting_selection`/resolved
|
|
2306
|
+
// step falls through (this handler doesn't claim it). See {@link handlePrReviewResolution}.
|
|
2307
|
+
{
|
|
2308
|
+
kind: 'pr-review-resolution',
|
|
2309
|
+
order: 175,
|
|
2310
|
+
canHandle: ({ step }) => step.agentKind === PR_REVIEW_STEP_KIND &&
|
|
2311
|
+
(step.prReview?.status === 'fixing' || step.prReview?.status === 'posting'),
|
|
2312
|
+
handle: (ctx) => this.handlePrReviewResolution(ctx),
|
|
2313
|
+
},
|
|
2283
2314
|
// The generic container/inline-agent step — claims every step no more-specific handler
|
|
2284
2315
|
// did. Highest order so it always runs last. See {@link handleAgentStep}.
|
|
2285
2316
|
{
|
|
@@ -2903,6 +2934,122 @@ export class RunDispatcher {
|
|
|
2903
2934
|
// Dispatch (or re-attach to) the proposer as a helper off this coder step.
|
|
2904
2935
|
return this.handleAgentStep(ctx, FORK_PROPOSER_KIND);
|
|
2905
2936
|
}
|
|
2937
|
+
/**
|
|
2938
|
+
* Drive a re-armed PR-review step's RESOLUTION (PR 3). The human resolved a parked review with
|
|
2939
|
+
* `fix` or `post`; {@link PrReviewController.resolve} re-armed this step and woke the driver.
|
|
2940
|
+
* - `fixing`: dispatch the Fixer against the reviewed PR's head branch with the selected
|
|
2941
|
+
* findings folded in (parks on the job; its completion marks the review `done`).
|
|
2942
|
+
* - `posting`: publish the selected findings as inline PR review comments, then finish the step.
|
|
2943
|
+
*/
|
|
2944
|
+
async handlePrReviewResolution(ctx) {
|
|
2945
|
+
const { step } = ctx;
|
|
2946
|
+
if (step.prReview?.status === 'posting')
|
|
2947
|
+
return this.postPrReview(ctx);
|
|
2948
|
+
return this.dispatchPrReviewFixer(ctx);
|
|
2949
|
+
}
|
|
2950
|
+
/**
|
|
2951
|
+
* Dispatch the Fixer for a PR-review `fix` resolution. A `review` task carries no own work
|
|
2952
|
+
* branch — it reviews an EXISTING PR — so resolve the PR's head branch (via the checkout-free
|
|
2953
|
+
* `RepoFiles`) and point the Fixer's clone/push at it: fold a synthetic `pullRequest` + an
|
|
2954
|
+
* apriori WORKING branch into the dispatch context so the shared `container-coding` +
|
|
2955
|
+
* `clone:{branch:'pr'}` fixer body clones + pushes that branch (no new PR), and hand it the
|
|
2956
|
+
* selected findings as a prior output (the same injection point the gate helpers use). Fails
|
|
2957
|
+
* the run loudly when the PR branch can't be resolved (nothing to push to) rather than pushing
|
|
2958
|
+
* blind. On a replay (jobId already set) it re-attaches without re-resolving.
|
|
2959
|
+
*/
|
|
2960
|
+
async dispatchPrReviewFixer(ctx) {
|
|
2961
|
+
const { workspaceId, instance, step, block } = ctx;
|
|
2962
|
+
const review = step.prReview;
|
|
2963
|
+
const selected = (review.findings ?? []).filter((f) => review.selectedFindingIds?.includes(f.id));
|
|
2964
|
+
let headRef = null;
|
|
2965
|
+
let prNumber;
|
|
2966
|
+
if (!step.jobId) {
|
|
2967
|
+
prNumber = reviewPrNumber(block);
|
|
2968
|
+
const runRepo = prNumber != null ? await this.resolveRunRepoContext?.(workspaceId, block.id) : null;
|
|
2969
|
+
const repo = runRepo?.repo;
|
|
2970
|
+
headRef =
|
|
2971
|
+
prNumber != null && repo?.pullRequestHeadRef
|
|
2972
|
+
? await this.runInitiatorScope(instance.initiatedBy, () => repo.pullRequestHeadRef(prNumber))
|
|
2973
|
+
: null;
|
|
2974
|
+
if (prNumber == null || !headRef) {
|
|
2975
|
+
return {
|
|
2976
|
+
kind: 'job_failed',
|
|
2977
|
+
failureKind: 'preflight',
|
|
2978
|
+
error: "Can't resolve the reviewed pull request's head branch to push fixes to. The " +
|
|
2979
|
+
"'fix' resolution needs a same-repo pull request on this service's linked repository " +
|
|
2980
|
+
'(a cross-repo or fork PR is not yet supported — post the findings as comments instead).',
|
|
2981
|
+
};
|
|
2982
|
+
}
|
|
2983
|
+
}
|
|
2984
|
+
const resolvedHeadRef = headRef;
|
|
2985
|
+
const resolvedPrNumber = prNumber;
|
|
2986
|
+
return this.handleAgentStep(ctx, FIXER_AGENT_KIND, (context) => {
|
|
2987
|
+
if (resolvedHeadRef && resolvedPrNumber != null) {
|
|
2988
|
+
context.block.pullRequest = {
|
|
2989
|
+
number: resolvedPrNumber,
|
|
2990
|
+
branch: resolvedHeadRef,
|
|
2991
|
+
url: review.prUrl ?? '',
|
|
2992
|
+
};
|
|
2993
|
+
// Build inside the PR head branch (probed, never created) so the work-branch machinery
|
|
2994
|
+
// targets it rather than minting a stray `cat-factory/<blockId>` off base.
|
|
2995
|
+
context.aprioriBranches = [{ name: resolvedHeadRef, mode: 'working' }];
|
|
2996
|
+
}
|
|
2997
|
+
context.priorOutputs = [
|
|
2998
|
+
...context.priorOutputs,
|
|
2999
|
+
{ agentKind: FIXER_AGENT_KIND, output: renderPrReviewFixerFeedback(selected) },
|
|
3000
|
+
];
|
|
3001
|
+
});
|
|
3002
|
+
}
|
|
3003
|
+
/**
|
|
3004
|
+
* Post a PR-review `post` resolution: publish the human-selected findings as a single advisory
|
|
3005
|
+
* (`COMMENT`) inline review on the reviewed PR via the checkout-free `RepoFiles.createReview`,
|
|
3006
|
+
* then finish the step. At-most-once: the `pendingPrReviewPost` marker is consumed (cleared +
|
|
3007
|
+
* persisted) BEFORE the (side-effecting) post so a Workflows retry can't submit it twice. When
|
|
3008
|
+
* no VCS review write is wired (tests / no GitHub) the findings are still recorded and the step
|
|
3009
|
+
* finishes — the review pipeline never reaches this without GitHub in practice.
|
|
3010
|
+
*
|
|
3011
|
+
* If `createReview` itself throws (GitHub rejects the batched review — e.g. a finding anchored
|
|
3012
|
+
* to a line outside the PR diff 422s the WHOLE review — or a transient network/5xx error), the
|
|
3013
|
+
* marker is already consumed, so a driver retry would NOT re-post. Rather than silently
|
|
3014
|
+
* completing the step as `done` with nothing actually posted (the human would believe the
|
|
3015
|
+
* comments landed), fail the step LOUDLY so the failure surfaces on the board — mirroring the
|
|
3016
|
+
* `fix` resolution's loud preflight failure. `post` is a fallback resolution, so a hard failure
|
|
3017
|
+
* is visible and the human can re-run or choose `fix`/`finish` instead.
|
|
3018
|
+
*/
|
|
3019
|
+
async postPrReview(ctx) {
|
|
3020
|
+
const { workspaceId, instance, step, block, isFinalStep } = ctx;
|
|
3021
|
+
const review = step.prReview;
|
|
3022
|
+
const selected = (review.findings ?? []).filter((f) => review.selectedFindingIds?.includes(f.id));
|
|
3023
|
+
let posted = false;
|
|
3024
|
+
if (step.pendingPrReviewPost) {
|
|
3025
|
+
step.pendingPrReviewPost = null;
|
|
3026
|
+
await this.runStateMachine.casPersist(workspaceId, instance);
|
|
3027
|
+
const prNumber = reviewPrNumber(block);
|
|
3028
|
+
const runRepo = prNumber != null ? await this.resolveRunRepoContext?.(workspaceId, block.id) : null;
|
|
3029
|
+
const repo = runRepo?.repo;
|
|
3030
|
+
if (prNumber != null && repo?.createReview) {
|
|
3031
|
+
try {
|
|
3032
|
+
await this.runInitiatorScope(instance.initiatedBy, () => repo.createReview(prNumber, buildPrReviewPost(selected, review.summary)));
|
|
3033
|
+
}
|
|
3034
|
+
catch (error) {
|
|
3035
|
+
return {
|
|
3036
|
+
kind: 'job_failed',
|
|
3037
|
+
failureKind: 'agent',
|
|
3038
|
+
error: `Failed to post the ${selected.length} selected finding` +
|
|
3039
|
+
`${selected.length === 1 ? '' : 's'} as a pull-request review: ${getErrorMessage(error)}. ` +
|
|
3040
|
+
'GitHub rejects a review whose inline comment anchors a line outside the PR diff — ' +
|
|
3041
|
+
"try the 'fix' resolution, or re-run to post again.",
|
|
3042
|
+
};
|
|
3043
|
+
}
|
|
3044
|
+
posted = true;
|
|
3045
|
+
}
|
|
3046
|
+
}
|
|
3047
|
+
step.prReview = { ...review, status: 'done' };
|
|
3048
|
+
const output = posted
|
|
3049
|
+
? `Posted ${selected.length} review comment${selected.length === 1 ? '' : 's'} to the pull request.`
|
|
3050
|
+
: `Recorded ${selected.length} selected finding${selected.length === 1 ? '' : 's'}.`;
|
|
3051
|
+
return this.recordStepResult(workspaceId, instance, step, isFinalStep, { output });
|
|
3052
|
+
}
|
|
2906
3053
|
/** Read a run's active implementation-fork decision state, or null. */
|
|
2907
3054
|
getForkDecision(workspaceId, executionId) {
|
|
2908
3055
|
return this.forkDecisionController.getActive(workspaceId, executionId);
|