@urateam/core 0.1.31 → 0.1.32
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/__tests__/agent-stream.test.js +35 -1
- package/dist/__tests__/agent-stream.test.js.map +1 -1
- package/dist/__tests__/control-signals.test.d.ts +2 -0
- package/dist/__tests__/control-signals.test.d.ts.map +1 -0
- package/dist/__tests__/control-signals.test.js +77 -0
- package/dist/__tests__/control-signals.test.js.map +1 -0
- package/dist/__tests__/pm-slack-interface.test.js +45 -0
- package/dist/__tests__/pm-slack-interface.test.js.map +1 -1
- package/dist/__tests__/pm-triage.test.js +101 -0
- package/dist/__tests__/pm-triage.test.js.map +1 -1
- package/dist/__tests__/util-linear.test.d.ts +10 -0
- package/dist/__tests__/util-linear.test.d.ts.map +1 -0
- package/dist/__tests__/util-linear.test.js +244 -0
- package/dist/__tests__/util-linear.test.js.map +1 -0
- package/dist/audit/events.d.ts +24 -0
- package/dist/audit/events.d.ts.map +1 -1
- package/dist/audit/events.js +32 -0
- package/dist/audit/events.js.map +1 -1
- package/dist/executor/agent-stream.d.ts +16 -0
- package/dist/executor/agent-stream.d.ts.map +1 -1
- package/dist/executor/agent-stream.js +43 -1
- package/dist/executor/agent-stream.js.map +1 -1
- package/dist/executor/executor.d.ts.map +1 -1
- package/dist/executor/executor.js +24 -5
- package/dist/executor/executor.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/notifier/linear.d.ts +0 -1
- package/dist/notifier/linear.d.ts.map +1 -1
- package/dist/notifier/linear.js +2 -5
- package/dist/notifier/linear.js.map +1 -1
- package/dist/pipeline/control-signals.d.ts +49 -0
- package/dist/pipeline/control-signals.d.ts.map +1 -0
- package/dist/pipeline/control-signals.js +93 -0
- package/dist/pipeline/control-signals.js.map +1 -0
- package/dist/pipeline/feedback-pipeline.d.ts +140 -0
- package/dist/pipeline/feedback-pipeline.d.ts.map +1 -0
- package/dist/pipeline/feedback-pipeline.js +427 -0
- package/dist/pipeline/feedback-pipeline.js.map +1 -0
- package/dist/pipeline/index.d.ts +1 -0
- package/dist/pipeline/index.d.ts.map +1 -1
- package/dist/pipeline/index.js +1 -0
- package/dist/pipeline/index.js.map +1 -1
- package/dist/pipeline/runner.d.ts +49 -33
- package/dist/pipeline/runner.d.ts.map +1 -1
- package/dist/pipeline/runner.js +136 -349
- package/dist/pipeline/runner.js.map +1 -1
- package/dist/pm/actions/promote.d.ts.map +1 -1
- package/dist/pm/actions/promote.js +15 -11
- package/dist/pm/actions/promote.js.map +1 -1
- package/dist/pm/actions/recover-stuck.d.ts.map +1 -1
- package/dist/pm/actions/recover-stuck.js +9 -5
- package/dist/pm/actions/recover-stuck.js.map +1 -1
- package/dist/pm/actions/triage.d.ts.map +1 -1
- package/dist/pm/actions/triage.js +67 -1
- package/dist/pm/actions/triage.js.map +1 -1
- package/dist/pm/linear-helpers.d.ts +10 -0
- package/dist/pm/linear-helpers.d.ts.map +1 -1
- package/dist/pm/linear-helpers.js +13 -21
- package/dist/pm/linear-helpers.js.map +1 -1
- package/dist/pm/pause-state.d.ts +29 -0
- package/dist/pm/pause-state.d.ts.map +1 -0
- package/dist/pm/pause-state.js +34 -0
- package/dist/pm/pause-state.js.map +1 -0
- package/dist/pm/slack-bulk.d.ts +34 -0
- package/dist/pm/slack-bulk.d.ts.map +1 -0
- package/dist/pm/slack-bulk.js +110 -0
- package/dist/pm/slack-bulk.js.map +1 -0
- package/dist/pm/slack-commands.d.ts +101 -0
- package/dist/pm/slack-commands.d.ts.map +1 -0
- package/dist/pm/slack-commands.js +309 -0
- package/dist/pm/slack-commands.js.map +1 -0
- package/dist/pm/slack-helpers.d.ts +10 -0
- package/dist/pm/slack-helpers.d.ts.map +1 -1
- package/dist/pm/slack-helpers.js +32 -0
- package/dist/pm/slack-helpers.js.map +1 -1
- package/dist/pm/slack-interface.d.ts +32 -58
- package/dist/pm/slack-interface.d.ts.map +1 -1
- package/dist/pm/slack-interface.js +150 -320
- package/dist/pm/slack-interface.js.map +1 -1
- package/dist/rbac/matrix.d.ts +2 -0
- package/dist/rbac/matrix.d.ts.map +1 -1
- package/dist/rbac/matrix.js +2 -0
- package/dist/rbac/matrix.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +7 -0
- package/dist/server.js.map +1 -1
- package/dist/sync/gh-linear-sync.d.ts.map +1 -1
- package/dist/sync/gh-linear-sync.js +2 -2
- package/dist/sync/gh-linear-sync.js.map +1 -1
- package/dist/types.d.ts +13 -4
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +6 -0
- package/dist/types.js.map +1 -1
- package/dist/util/linear.d.ts +70 -0
- package/dist/util/linear.d.ts.map +1 -0
- package/dist/util/linear.js +108 -0
- package/dist/util/linear.js.map +1 -0
- package/package.json +1 -1
package/dist/pipeline/runner.js
CHANGED
|
@@ -7,6 +7,8 @@ import { checkRequirements, buildRalphContext } from "../executor/ralph.js";
|
|
|
7
7
|
import { computeEffectiveRalphIterations } from "./runner-ralph-helpers.js";
|
|
8
8
|
import { checkTestQuality } from "../executor/test-quality.js";
|
|
9
9
|
import { buildDeepReviewContext } from "../executor/deep-review.js";
|
|
10
|
+
import { getStopSignal, requestStop, clearStopSignal } from "./control-signals.js";
|
|
11
|
+
import { setPmPaused } from "../pm/pause-state.js";
|
|
10
12
|
import { runReviewProviders } from "./review-providers-runner.js";
|
|
11
13
|
import { postFanoutCommentsToPR } from "../executor/review/post-fanout-comments.js";
|
|
12
14
|
import { extractHandoff } from "../executor/extract-handoff.js";
|
|
@@ -19,8 +21,8 @@ import { homedir } from "node:os";
|
|
|
19
21
|
import { execFile as execFileCb } from "node:child_process";
|
|
20
22
|
import { promisify } from "node:util";
|
|
21
23
|
const execFileAsync = promisify(execFileCb);
|
|
22
|
-
import { cloneRepo, createWorktree, deleteWorktree, pushBranch, pushBranchForce, choosePushStrategy, rebaseBranch, abortRebase, autoCommitChanges, getAgentCommits, createPRViaCli, mergePRViaCli, getDiffLineCount, getChangedFiles, checkDuplicateBranch, branchName,
|
|
23
|
-
import { addPRComment, createGitHubClient, createPR, prHasCommentStartingWith,
|
|
24
|
+
import { cloneRepo, createWorktree, deleteWorktree, pushBranch, pushBranchForce, choosePushStrategy, rebaseBranch, abortRebase, autoCommitChanges, getAgentCommits, createPRViaCli, mergePRViaCli, getDiffLineCount, getChangedFiles, checkDuplicateBranch, branchName, pruneWorktreesInRepoDirs, } from "../repo/git.js";
|
|
25
|
+
import { addPRComment, createGitHubClient, createPR, prHasCommentStartingWith, } from "../repo/github.js";
|
|
24
26
|
import { createMR, buildAuthenticatedUrl, } from "../repo/gitlab.js";
|
|
25
27
|
import { parseRepoUrl, parseGitLabUrl } from "../repo/config.js";
|
|
26
28
|
import { detectTechStack } from "../repo/tech-stack.js";
|
|
@@ -36,35 +38,13 @@ import { evaluatePolicyGates } from "../policy/evaluate.js";
|
|
|
36
38
|
import { buildReviewerRequest, verifyApprovalsReceived } from "../policy/index.js";
|
|
37
39
|
import { logAuditEvent, policyReviewersRequestedEvent, reviewFanoutFallbackUsedEvent } from "../audit/index.js";
|
|
38
40
|
import { matchesAnyPattern } from "../util/glob.js";
|
|
41
|
+
import { startFeedbackPipeline, } from "./feedback-pipeline.js";
|
|
42
|
+
// Re-export from extracted module so existing callers (including tests) still
|
|
43
|
+
// find buildReviewFeedbackContext at pipeline/runner.js without changing their
|
|
44
|
+
// import paths.
|
|
45
|
+
export { buildReviewFeedbackContext } from "./feedback-pipeline.js";
|
|
39
46
|
// Module-level logger (no runId yet — used for pre-run messages)
|
|
40
47
|
const log = createLogger({ component: "PipelineRunner" });
|
|
41
|
-
/**
|
|
42
|
-
* Map webhook-shaped `ReviewFeedbackComment[]` (the wire format we receive
|
|
43
|
-
* from GitHub) into the `ReviewFeedbackContext` that the implement template
|
|
44
|
-
* expects when handling PR review feedback.
|
|
45
|
-
*
|
|
46
|
-
* Routes the implement stage into the dedicated review-feedback prompt path
|
|
47
|
-
* (templates.ts:233-253) — "address review comments on existing branch, push
|
|
48
|
-
* to same branch, do NOT create a new PR" — instead of falling through to
|
|
49
|
-
* the standard "create branch and implement issue from scratch" prompt.
|
|
50
|
-
*
|
|
51
|
-
* `createdAt` is not captured by the GitHub webhook handler today, so the
|
|
52
|
-
* mapped comments use an empty string. The template only renders this for
|
|
53
|
-
* display; an empty value is harmless.
|
|
54
|
-
*/
|
|
55
|
-
export function buildReviewFeedbackContext(prUrl, prBranch, comments) {
|
|
56
|
-
return {
|
|
57
|
-
prUrl,
|
|
58
|
-
prBranch,
|
|
59
|
-
comments: comments.map((c) => ({
|
|
60
|
-
author: c.author,
|
|
61
|
-
body: c.body,
|
|
62
|
-
file: c.filePath,
|
|
63
|
-
line: c.lineNumber,
|
|
64
|
-
createdAt: "",
|
|
65
|
-
})),
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
48
|
export class PipelineRunner {
|
|
69
49
|
queue;
|
|
70
50
|
/** Push queue: concurrency=1 serialises push+PR creation within this process.
|
|
@@ -330,6 +310,83 @@ export class PipelineRunner {
|
|
|
330
310
|
.where(eq(pipelineRuns.id, runId));
|
|
331
311
|
this.activeRuns.delete(issueId);
|
|
332
312
|
}
|
|
313
|
+
/**
|
|
314
|
+
* Operator-initiated stop for a single run, addressed by runId.
|
|
315
|
+
*
|
|
316
|
+
* - `"cancel"` aborts the active Agent SDK stream immediately. The current
|
|
317
|
+
* stage exits with `status: "cancelled"`; the pipeline marks the run
|
|
318
|
+
* `cancelled` and returns without creating a PR.
|
|
319
|
+
* - `"graceful"` lets the current stage complete, then skips remaining
|
|
320
|
+
* stages. Slower than cancel but leaves the worktree consistent.
|
|
321
|
+
*
|
|
322
|
+
* Idempotent. Returns the issueId resolved from the active map (or null if
|
|
323
|
+
* the runId isn't currently active). The caller is responsible for emitting
|
|
324
|
+
* the audit event since it knows the actor.
|
|
325
|
+
*/
|
|
326
|
+
requestStop(runId, mode) {
|
|
327
|
+
let issueId = null;
|
|
328
|
+
for (const [iss, rid] of this.activeRuns) {
|
|
329
|
+
if (rid === runId) {
|
|
330
|
+
issueId = iss;
|
|
331
|
+
break;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
const effective = requestStop(runId, mode);
|
|
335
|
+
return { issueId, mode: effective };
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Halt the whole container's autonomous work:
|
|
339
|
+
* 1. Pauses the PM Agent (BEC-170 mechanism) so no new runs get promoted.
|
|
340
|
+
* 2. Sends a `"cancel"` signal to every active pipeline + feedback run.
|
|
341
|
+
*
|
|
342
|
+
* Returns the set of run ids that were cancelled — the caller emits the
|
|
343
|
+
* audit event. Reversible: PM Agent can be unpaused via `/pm resume` and
|
|
344
|
+
* individual runs can be re-triggered via the retry button. Cancelled runs
|
|
345
|
+
* stay cancelled.
|
|
346
|
+
*/
|
|
347
|
+
haltAll() {
|
|
348
|
+
setPmPaused(true);
|
|
349
|
+
const cancelled = new Set();
|
|
350
|
+
for (const runId of this.activeRuns.values()) {
|
|
351
|
+
requestStop(runId, "cancel");
|
|
352
|
+
cancelled.add(runId);
|
|
353
|
+
}
|
|
354
|
+
for (const runId of this.activeFeedbackRuns.values()) {
|
|
355
|
+
requestStop(runId, "cancel");
|
|
356
|
+
cancelled.add(runId);
|
|
357
|
+
}
|
|
358
|
+
log.info({ count: cancelled.size, runIds: [...cancelled] }, "haltAll: PM paused and cancel signals sent to active runs");
|
|
359
|
+
return { cancelledRunIds: [...cancelled] };
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* Mark a run as cancelled in the DB and clean up bookkeeping. Shared by the
|
|
363
|
+
* pre-stage graceful path and the mid-stream cancel path.
|
|
364
|
+
*
|
|
365
|
+
* For feedback-pipeline runs, pass `feedbackPrUrl` so the per-PR rate-limit
|
|
366
|
+
* slot is freed immediately rather than waiting on the queue's `finally`
|
|
367
|
+
* block. The queue's `finally` still fires (idempotent — Map.delete on a
|
|
368
|
+
* missing key is a no-op), this just shortens the window during which a new
|
|
369
|
+
* feedback comment on the same PR is rejected as "already active".
|
|
370
|
+
*/
|
|
371
|
+
markRunCancelled(db, runId, run, mode, feedbackPrUrl) {
|
|
372
|
+
return this.markRunCancelledImpl(db, runId, run, mode, feedbackPrUrl);
|
|
373
|
+
}
|
|
374
|
+
async markRunCancelledImpl(db, runId, run, mode, feedbackPrUrl) {
|
|
375
|
+
await removeActiveWork(db, runId);
|
|
376
|
+
await db
|
|
377
|
+
.update(pipelineRuns)
|
|
378
|
+
.set({
|
|
379
|
+
status: "cancelled",
|
|
380
|
+
errorMessage: `cancelled by operator (${mode})`,
|
|
381
|
+
completedAt: new Date(),
|
|
382
|
+
})
|
|
383
|
+
.where(eq(pipelineRuns.id, runId));
|
|
384
|
+
run.status = "cancelled";
|
|
385
|
+
this.activeRuns.delete(run.issueId);
|
|
386
|
+
if (feedbackPrUrl)
|
|
387
|
+
this.activeFeedbackRuns.delete(feedbackPrUrl);
|
|
388
|
+
clearStopSignal(runId);
|
|
389
|
+
}
|
|
333
390
|
isActive(issueId) {
|
|
334
391
|
return this.activeRuns.has(issueId);
|
|
335
392
|
}
|
|
@@ -340,77 +397,31 @@ export class PipelineRunner {
|
|
|
340
397
|
/**
|
|
341
398
|
* Start a review-feedback pipeline run triggered by a PR review comment.
|
|
342
399
|
*
|
|
343
|
-
*
|
|
344
|
-
*
|
|
345
|
-
*
|
|
346
|
-
* - Does NOT create a new PR — it pushes to the same branch.
|
|
347
|
-
* - Optionally re-requests review after pushing.
|
|
400
|
+
* Thin wrapper — all orchestration logic (rate-limiting, DB insert, queue
|
|
401
|
+
* management) and execution are delegated to feedback-pipeline.ts.
|
|
402
|
+
* See startFeedbackPipeline() for full documentation.
|
|
348
403
|
*/
|
|
349
404
|
async startFeedback(params) {
|
|
350
|
-
const
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
.where(eq(pipelineRuns.id, parentRunId))
|
|
371
|
-
.limit(1);
|
|
372
|
-
linearTeamId = parentRows[0]?.linearTeamId ?? null;
|
|
373
|
-
}
|
|
374
|
-
runLog.info({ branch, prUrl }, "inserting feedback run into DB");
|
|
375
|
-
await db.insert(pipelineRuns).values({
|
|
376
|
-
id: runId,
|
|
377
|
-
issueId: issue.identifier,
|
|
378
|
-
issueTitle: issue.title,
|
|
379
|
-
pipelineKey,
|
|
380
|
-
repoUrl: repoConfig.url,
|
|
381
|
-
branch,
|
|
382
|
-
status: "queued",
|
|
383
|
-
prUrl,
|
|
384
|
-
runType: "review-feedback",
|
|
385
|
-
parentRunId: parentRunId ?? null,
|
|
386
|
-
feedbackContext: JSON.stringify(feedbackComments),
|
|
387
|
-
linearTeamId,
|
|
388
|
-
});
|
|
389
|
-
const run = this.buildPipelineRun(runId, issue, pipelineKey, repoConfig, branch);
|
|
390
|
-
run.prUrl = prUrl;
|
|
391
|
-
run.runType = "review-feedback";
|
|
392
|
-
run.feedbackContext = JSON.stringify(feedbackComments);
|
|
393
|
-
// Register in activeFeedbackRuns BEFORE enqueue so rate-limit check works
|
|
394
|
-
this.activeFeedbackRuns.set(prUrl, runId);
|
|
395
|
-
this.queue
|
|
396
|
-
.enqueue(async () => {
|
|
397
|
-
if (!this.activeFeedbackRuns.has(prUrl))
|
|
398
|
-
return; // was cancelled
|
|
399
|
-
runLog.info("executing feedback pipeline");
|
|
400
|
-
try {
|
|
401
|
-
await runWithLogContext({ runId, issueId: issue.identifier }, () => this.executeFeedbackPipeline(runId, run, pipelineConfig, repoConfig, sanitizedIssue, branch, prUrl, prNumber, feedbackComments, rerequestReview ?? false));
|
|
402
|
-
}
|
|
403
|
-
catch (err) {
|
|
404
|
-
runLog.error({ err }, "feedback pipeline execution failed");
|
|
405
|
-
}
|
|
406
|
-
finally {
|
|
407
|
-
this.activeFeedbackRuns.delete(prUrl);
|
|
408
|
-
}
|
|
409
|
-
})
|
|
410
|
-
.catch((err) => {
|
|
411
|
-
runLog.error({ err }, "feedback queue execution failed");
|
|
412
|
-
this.activeFeedbackRuns.delete(prUrl);
|
|
413
|
-
});
|
|
405
|
+
const ctx = {
|
|
406
|
+
db: this.db,
|
|
407
|
+
notifier: this.notifier,
|
|
408
|
+
repoCloneDir: this.repoCloneDir,
|
|
409
|
+
agentRunDir: this.agentRunDir,
|
|
410
|
+
githubConfig: this.githubConfig,
|
|
411
|
+
gitlabConfig: this.gitlabConfig,
|
|
412
|
+
pushQueue: this.pushQueue,
|
|
413
|
+
lockAdapter: this.lockAdapter,
|
|
414
|
+
prLockTimeoutMs: this.prLockTimeoutMs,
|
|
415
|
+
budgetAlertedRuns: this.budgetAlertedRuns,
|
|
416
|
+
checkTokenBudget: this.checkTokenBudget.bind(this),
|
|
417
|
+
failPipeline: this.failPipeline.bind(this),
|
|
418
|
+
injectAgentConfig: this.injectAgentConfig.bind(this),
|
|
419
|
+
markRunCancelled: this.markRunCancelled.bind(this),
|
|
420
|
+
activeFeedbackRuns: this.activeFeedbackRuns,
|
|
421
|
+
queue: this.queue,
|
|
422
|
+
buildPipelineRun: this.buildPipelineRun.bind(this),
|
|
423
|
+
};
|
|
424
|
+
return startFeedbackPipeline(ctx, params);
|
|
414
425
|
}
|
|
415
426
|
/**
|
|
416
427
|
* Inject the framework's CLAUDE.md into the worktree if the repo doesn't
|
|
@@ -564,6 +575,16 @@ export class PipelineRunner {
|
|
|
564
575
|
const stageType = stage;
|
|
565
576
|
lastStageIndex = config.stages.indexOf(stage);
|
|
566
577
|
runLog.info({ stage: stageType }, "executing stage");
|
|
578
|
+
// Operator stop check (graceful path) — fires between stages so the
|
|
579
|
+
// previous stage's work is preserved. The "cancel" path is interrupted
|
|
580
|
+
// mid-stream by the executor's AbortController and surfaces below as
|
|
581
|
+
// result.status === "cancelled".
|
|
582
|
+
const preStageStopSignal = getStopSignal(runId);
|
|
583
|
+
if (preStageStopSignal) {
|
|
584
|
+
runLog.info({ stage: stageType, mode: preStageStopSignal }, "pipeline stop requested — aborting remaining stages");
|
|
585
|
+
await this.markRunCancelled(db, runId, run, preStageStopSignal);
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
567
588
|
if (stageType === "await-approval") {
|
|
568
589
|
// Save the full resume context so resume() can re-attach the worktree
|
|
569
590
|
// and continue from the next stage with the correct handoff artifact.
|
|
@@ -619,6 +640,16 @@ export class PipelineRunner {
|
|
|
619
640
|
devcontainerSession,
|
|
620
641
|
stageModels: config.stageModels,
|
|
621
642
|
});
|
|
643
|
+
// Operator stop check (cancel path) — the AbortController inside the
|
|
644
|
+
// executor surfaces as result.status === "cancelled". Don't try to use
|
|
645
|
+
// the (possibly partial) handoff; exit immediately like the pre-stage
|
|
646
|
+
// graceful check above.
|
|
647
|
+
if (result.status === "cancelled") {
|
|
648
|
+
const mode = getStopSignal(runId) ?? "cancel";
|
|
649
|
+
runLog.info({ stage: stageType, mode }, "stage cancelled by operator — aborting pipeline");
|
|
650
|
+
await this.markRunCancelled(db, runId, run, mode);
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
622
653
|
// BEC-134: track the most recent review stage_run id so fanout
|
|
623
654
|
// persistence can reuse it.
|
|
624
655
|
if (stageType === "review") {
|
|
@@ -884,18 +915,15 @@ export class PipelineRunner {
|
|
|
884
915
|
}
|
|
885
916
|
}
|
|
886
917
|
}
|
|
887
|
-
// After stage completes: update
|
|
888
|
-
//
|
|
918
|
+
// After stage completes: update the in-memory file list so the next
|
|
919
|
+
// stage's pre-loop upsertActiveWork persists the accumulated set.
|
|
920
|
+
// We intentionally skip a second DB write here — the row was already
|
|
921
|
+
// written at the start of this stage and will be refreshed again at
|
|
922
|
+
// the start of the next one, avoiding a redundant intermediate write.
|
|
889
923
|
if (worktreePath) {
|
|
890
924
|
const freshFiles = await getModifiedFiles(worktreePath);
|
|
891
925
|
if (freshFiles.length > 0) {
|
|
892
926
|
allModifiedFiles = freshFiles;
|
|
893
|
-
await upsertActiveWork(db, {
|
|
894
|
-
runId,
|
|
895
|
-
issueId: sanitizedIssue.id,
|
|
896
|
-
stage: stageType,
|
|
897
|
-
filesModified: allModifiedFiles,
|
|
898
|
-
});
|
|
899
927
|
}
|
|
900
928
|
}
|
|
901
929
|
}
|
|
@@ -1684,6 +1712,8 @@ export class PipelineRunner {
|
|
|
1684
1712
|
? parseInt(summaryPrMatch[1], 10)
|
|
1685
1713
|
: null;
|
|
1686
1714
|
if (summaryPrNumber !== null) {
|
|
1715
|
+
// Fetch stage runs first; reviewModelRuns query depends on the
|
|
1716
|
+
// resulting stageIds so the two queries are necessarily sequential.
|
|
1687
1717
|
const stages = await this.db
|
|
1688
1718
|
.select()
|
|
1689
1719
|
.from(stageRuns)
|
|
@@ -2004,249 +2034,6 @@ export class PipelineRunner {
|
|
|
2004
2034
|
};
|
|
2005
2035
|
await this.notifier.onDailyTokenSummary?.(summary);
|
|
2006
2036
|
}
|
|
2007
|
-
/**
|
|
2008
|
-
* Execute a review-feedback pipeline run.
|
|
2009
|
-
*
|
|
2010
|
-
* Key differences from executePipeline():
|
|
2011
|
-
* - Checks out the EXISTING PR branch (not a new one).
|
|
2012
|
-
* - Skips triage, reproduce, await-approval stages.
|
|
2013
|
-
* - Does NOT create a new PR — pushes to the same branch.
|
|
2014
|
-
* - Optionally re-requests review via GitHub App after push.
|
|
2015
|
-
* - Feedback comment context is injected into the implement stage prompt.
|
|
2016
|
-
*/
|
|
2017
|
-
async executeFeedbackPipeline(runId, run, config, repoConfig, sanitizedIssue, branch, prUrl, prNumber, feedbackComments, rerequestReview) {
|
|
2018
|
-
const db = this.db;
|
|
2019
|
-
const runLog = createLogger({
|
|
2020
|
-
component: "PipelineRunner",
|
|
2021
|
-
runId,
|
|
2022
|
-
issueId: run.issueId,
|
|
2023
|
-
});
|
|
2024
|
-
let worktreePath;
|
|
2025
|
-
let devcontainerSession;
|
|
2026
|
-
await db
|
|
2027
|
-
.update(pipelineRuns)
|
|
2028
|
-
.set({ status: "running" })
|
|
2029
|
-
.where(eq(pipelineRuns.id, runId));
|
|
2030
|
-
run.status = "running";
|
|
2031
|
-
await upsertActiveWork(db, {
|
|
2032
|
-
runId,
|
|
2033
|
-
issueId: sanitizedIssue.id,
|
|
2034
|
-
stage: "implement",
|
|
2035
|
-
});
|
|
2036
|
-
await this.notifier.onPipelineStart(run);
|
|
2037
|
-
try {
|
|
2038
|
-
// -----------------------------------------------------------------------
|
|
2039
|
-
// Set up worktree from existing remote branch
|
|
2040
|
-
// -----------------------------------------------------------------------
|
|
2041
|
-
const repoDir = `${this.repoCloneDir}/${sanitizedIssue.slug}`;
|
|
2042
|
-
const cloneUrl = repoConfig.provider === "gitlab" && this.gitlabConfig
|
|
2043
|
-
? buildAuthenticatedUrl(repoConfig.url, this.gitlabConfig)
|
|
2044
|
-
: repoConfig.url;
|
|
2045
|
-
const logUrl = cloneUrl.replace(/:\/\/[^@]+@/, "://[redacted]@");
|
|
2046
|
-
runLog.info({ repoUrl: logUrl, repoDir }, "feedback: cloning/fetching repository");
|
|
2047
|
-
await cloneRepo(cloneUrl, repoDir);
|
|
2048
|
-
runLog.info({ branch }, "feedback: creating worktree from existing remote branch");
|
|
2049
|
-
worktreePath = await createWorktreeFromRemote(repoDir, runId, branch, this.agentRunDir);
|
|
2050
|
-
runLog.info({ worktreePath }, "feedback: worktree created");
|
|
2051
|
-
// Devcontainer (if configured)
|
|
2052
|
-
const useDevcontainer = await shouldUseDevcontainer(worktreePath, repoConfig.devcontainer);
|
|
2053
|
-
if (useDevcontainer) {
|
|
2054
|
-
runLog.info("feedback: starting devcontainer");
|
|
2055
|
-
devcontainerSession = await devcontainerUp(worktreePath, repoConfig.devcontainer);
|
|
2056
|
-
}
|
|
2057
|
-
await this.injectAgentConfig(worktreePath);
|
|
2058
|
-
if (repoConfig.setupCommands) {
|
|
2059
|
-
for (const cmdArgs of repoConfig.setupCommands) {
|
|
2060
|
-
const [command, ...args] = cmdArgs;
|
|
2061
|
-
runLog.info({ command, args }, "feedback: running setup command");
|
|
2062
|
-
try {
|
|
2063
|
-
await execFileAsync(command, args, { cwd: worktreePath });
|
|
2064
|
-
}
|
|
2065
|
-
catch (err) {
|
|
2066
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
2067
|
-
runLog.error({ command, args, err }, "feedback: setup command failed");
|
|
2068
|
-
throw new Error(`Setup command failed: ${command} ${args.join(" ")} — ${msg}`);
|
|
2069
|
-
}
|
|
2070
|
-
}
|
|
2071
|
-
}
|
|
2072
|
-
const techStack = await detectTechStack(worktreePath);
|
|
2073
|
-
runLog.info({
|
|
2074
|
-
languages: techStack.languages,
|
|
2075
|
-
frameworks: techStack.frameworks,
|
|
2076
|
-
buildSystems: techStack.buildSystems,
|
|
2077
|
-
}, "feedback: tech stack detected");
|
|
2078
|
-
// -----------------------------------------------------------------------
|
|
2079
|
-
// Build review-feedback context for the implement stage. This routes the
|
|
2080
|
-
// implement template into its dedicated review-feedback branch
|
|
2081
|
-
// ("address comments on existing branch, push to same branch") instead
|
|
2082
|
-
// of the standard "create new branch + implement from scratch" path,
|
|
2083
|
-
// which is wrong for PR-comment triggered runs.
|
|
2084
|
-
// -----------------------------------------------------------------------
|
|
2085
|
-
const reviewFeedback = buildReviewFeedbackContext(prUrl, branch, feedbackComments);
|
|
2086
|
-
// -----------------------------------------------------------------------
|
|
2087
|
-
// Execute pipeline stages — skip triage, reproduce, await-approval
|
|
2088
|
-
// -----------------------------------------------------------------------
|
|
2089
|
-
const skipStages = new Set(["triage", "reproduce", "await-approval"]);
|
|
2090
|
-
const stagesToRun = config.stages.filter((s) => !skipStages.has(s));
|
|
2091
|
-
runLog.info({ stages: stagesToRun }, "feedback: starting pipeline stages");
|
|
2092
|
-
let handoff;
|
|
2093
|
-
let allModifiedFiles = [];
|
|
2094
|
-
for (const stage of stagesToRun) {
|
|
2095
|
-
const stageType = stage;
|
|
2096
|
-
runLog.info({ stage: stageType }, "feedback: executing stage");
|
|
2097
|
-
await upsertActiveWork(db, {
|
|
2098
|
-
runId,
|
|
2099
|
-
issueId: sanitizedIssue.id,
|
|
2100
|
-
stage: stageType,
|
|
2101
|
-
filesModified: allModifiedFiles.length > 0 ? allModifiedFiles : undefined,
|
|
2102
|
-
});
|
|
2103
|
-
// Only the implement stage uses reviewFeedback; the test/review stages
|
|
2104
|
-
// get their context from the implement stage's handoff.
|
|
2105
|
-
const stageReviewFeedback = stageType === "implement" ? reviewFeedback : undefined;
|
|
2106
|
-
let result = await executeStage({
|
|
2107
|
-
runId,
|
|
2108
|
-
issueId: sanitizedIssue.id,
|
|
2109
|
-
stage: stageType,
|
|
2110
|
-
sanitizedIssue,
|
|
2111
|
-
repoConfig,
|
|
2112
|
-
handoff,
|
|
2113
|
-
workdir: worktreePath,
|
|
2114
|
-
db: this.db,
|
|
2115
|
-
techStack,
|
|
2116
|
-
devcontainerSession,
|
|
2117
|
-
reviewFeedback: stageReviewFeedback,
|
|
2118
|
-
stageModels: config.stageModels,
|
|
2119
|
-
});
|
|
2120
|
-
run.totalInputTokens += result.inputTokens;
|
|
2121
|
-
run.totalOutputTokens += result.outputTokens;
|
|
2122
|
-
if (await this.checkTokenBudget(db, runId, run, config, stage))
|
|
2123
|
-
return;
|
|
2124
|
-
await this.notifier.onStageComplete(run, stage, result);
|
|
2125
|
-
if (result.status === "failed") {
|
|
2126
|
-
const errorMsg = result.errorMessage ?? "Stage failed";
|
|
2127
|
-
await this.failPipeline(db, runId, run, stage, errorMsg, false);
|
|
2128
|
-
return;
|
|
2129
|
-
}
|
|
2130
|
-
handoff = result.handoffArtifact;
|
|
2131
|
-
if (await autoCommitChanges(worktreePath, sanitizedIssue.id, branch)) {
|
|
2132
|
-
run.autoCommitted = true;
|
|
2133
|
-
}
|
|
2134
|
-
if (worktreePath) {
|
|
2135
|
-
const freshFiles = await getModifiedFiles(worktreePath);
|
|
2136
|
-
if (freshFiles.length > 0) {
|
|
2137
|
-
allModifiedFiles = freshFiles;
|
|
2138
|
-
await upsertActiveWork(db, {
|
|
2139
|
-
runId,
|
|
2140
|
-
issueId: sanitizedIssue.id,
|
|
2141
|
-
stage: stageType,
|
|
2142
|
-
filesModified: allModifiedFiles,
|
|
2143
|
-
});
|
|
2144
|
-
}
|
|
2145
|
-
}
|
|
2146
|
-
}
|
|
2147
|
-
// -----------------------------------------------------------------------
|
|
2148
|
-
// Push to existing branch — no new PR
|
|
2149
|
-
// -----------------------------------------------------------------------
|
|
2150
|
-
await this.pushQueue.enqueue(async () => {
|
|
2151
|
-
await withBranchLock(this.lockAdapter, branch, this.prLockTimeoutMs, async () => {
|
|
2152
|
-
const wtPath = worktreePath;
|
|
2153
|
-
if (await autoCommitChanges(wtPath, sanitizedIssue.id, branch)) {
|
|
2154
|
-
run.autoCommitted = true;
|
|
2155
|
-
}
|
|
2156
|
-
runLog.info({ defaultBranch: repoConfig.defaultBranch }, "feedback push: rebasing before push");
|
|
2157
|
-
const rebaseResult = await rebaseBranch(wtPath, repoConfig.defaultBranch);
|
|
2158
|
-
const feedbackHasConflicts = !rebaseResult.success && rebaseResult.hasConflicts;
|
|
2159
|
-
if (feedbackHasConflicts) {
|
|
2160
|
-
runLog.warn("feedback push: rebase conflicts — force-pushing for human review");
|
|
2161
|
-
await abortRebase(wtPath);
|
|
2162
|
-
await pushBranchForce(wtPath, branch);
|
|
2163
|
-
await this.notifier.onHumanReviewNeeded?.(run, prUrl, "Merge conflicts in feedback run — please resolve manually");
|
|
2164
|
-
}
|
|
2165
|
-
else {
|
|
2166
|
-
const feedbackPushStrategy = choosePushStrategy(branch, false);
|
|
2167
|
-
if (feedbackPushStrategy === "force-with-lease") {
|
|
2168
|
-
runLog.info({ branch }, "feedback push: force-with-lease push for agent branch");
|
|
2169
|
-
await pushBranchForce(wtPath, branch);
|
|
2170
|
-
}
|
|
2171
|
-
else {
|
|
2172
|
-
await pushBranch(wtPath, branch);
|
|
2173
|
-
}
|
|
2174
|
-
}
|
|
2175
|
-
runLog.info({ prUrl }, "feedback: pushed to existing PR branch");
|
|
2176
|
-
// Re-request review via GitHub App if configured
|
|
2177
|
-
if (rerequestReview && this.githubConfig && prNumber) {
|
|
2178
|
-
try {
|
|
2179
|
-
const { owner, repo } = parseRepoUrl(repoConfig.url);
|
|
2180
|
-
const octokit = await createGitHubClient(this.githubConfig);
|
|
2181
|
-
const reRequested = await rerequestPRReview(octokit, owner, repo, prNumber);
|
|
2182
|
-
if (reRequested) {
|
|
2183
|
-
runLog.info({ prUrl, prNumber }, "feedback: re-requested review");
|
|
2184
|
-
}
|
|
2185
|
-
else {
|
|
2186
|
-
runLog.info({ prUrl, prNumber }, "feedback: no existing reviewers to re-request");
|
|
2187
|
-
}
|
|
2188
|
-
}
|
|
2189
|
-
catch (reviewErr) {
|
|
2190
|
-
runLog.error({ err: reviewErr }, "feedback: failed to re-request review");
|
|
2191
|
-
}
|
|
2192
|
-
}
|
|
2193
|
-
});
|
|
2194
|
-
});
|
|
2195
|
-
await db
|
|
2196
|
-
.update(pipelineRuns)
|
|
2197
|
-
.set({
|
|
2198
|
-
status: "completed",
|
|
2199
|
-
completedAt: new Date(),
|
|
2200
|
-
totalInputTokens: run.totalInputTokens,
|
|
2201
|
-
totalOutputTokens: run.totalOutputTokens,
|
|
2202
|
-
prUrl,
|
|
2203
|
-
autoCommitted: run.autoCommitted ?? null,
|
|
2204
|
-
})
|
|
2205
|
-
.where(eq(pipelineRuns.id, runId));
|
|
2206
|
-
run.status = "completed";
|
|
2207
|
-
runLog.info({
|
|
2208
|
-
prUrl,
|
|
2209
|
-
totalInputTokens: run.totalInputTokens,
|
|
2210
|
-
totalOutputTokens: run.totalOutputTokens,
|
|
2211
|
-
autoCommitted: run.autoCommitted ?? false,
|
|
2212
|
-
}, "feedback pipeline completed");
|
|
2213
|
-
await this.notifier.onPipelineComplete(run, {
|
|
2214
|
-
prUrl,
|
|
2215
|
-
totalInputTokens: run.totalInputTokens,
|
|
2216
|
-
totalOutputTokens: run.totalOutputTokens,
|
|
2217
|
-
stagesCompleted: stagesToRun.length,
|
|
2218
|
-
autoMerged: false,
|
|
2219
|
-
});
|
|
2220
|
-
}
|
|
2221
|
-
catch (error) {
|
|
2222
|
-
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
2223
|
-
runLog.error({ err: error }, "feedback pipeline failed with unexpected error");
|
|
2224
|
-
await this.failPipeline(db, runId, run, "unknown", errorMsg, true);
|
|
2225
|
-
}
|
|
2226
|
-
finally {
|
|
2227
|
-
this.budgetAlertedRuns.delete(runId);
|
|
2228
|
-
await removeActiveWork(db, runId);
|
|
2229
|
-
if (devcontainerSession) {
|
|
2230
|
-
try {
|
|
2231
|
-
await devcontainerDown(devcontainerSession);
|
|
2232
|
-
}
|
|
2233
|
-
catch {
|
|
2234
|
-
// Ignore cleanup errors
|
|
2235
|
-
}
|
|
2236
|
-
}
|
|
2237
|
-
// Feedback runs don't pause — always clean up worktree on completion or failure.
|
|
2238
|
-
// Cast to string because failPipeline mutates run.status at runtime beyond the initial type.
|
|
2239
|
-
const feedbackStatus = run.status;
|
|
2240
|
-
if (worktreePath && (feedbackStatus === "completed" || feedbackStatus === "failed")) {
|
|
2241
|
-
try {
|
|
2242
|
-
await deleteWorktree(worktreePath);
|
|
2243
|
-
}
|
|
2244
|
-
catch {
|
|
2245
|
-
// Ignore cleanup errors
|
|
2246
|
-
}
|
|
2247
|
-
}
|
|
2248
|
-
}
|
|
2249
|
-
}
|
|
2250
2037
|
buildPipelineRun(runId, issue, pipelineKey, repoConfig, branch) {
|
|
2251
2038
|
return {
|
|
2252
2039
|
id: runId,
|