@revisium/orchestrator 0.1.0-alpha.9 → 0.1.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/README.md +143 -157
- package/control-plane/default-playbook/catalog/pipelines.json +217 -8
- package/control-plane/default-playbook/package.json +1 -1
- package/control-plane/default-playbook/prompts/developer.md +1 -0
- package/control-plane/default-playbook/prompts/triager.md +3 -0
- package/dist/api/graphql-api/graphql-ws/subscription-mappers.js +3 -0
- package/dist/api/graphql-api/graphql-ws/subscription-mappers.js.map +1 -1
- package/dist/api/graphql-api/pr/inputs/pr-readiness.input.js +6 -0
- package/dist/api/graphql-api/pr/inputs/pr-readiness.input.js.map +1 -1
- package/dist/api/graphql-api/pr/model/pr-readiness.model.js +30 -0
- package/dist/api/graphql-api/pr/model/pr-readiness.model.js.map +1 -1
- package/dist/api/graphql-api/runs/inputs/create-run.input.js +6 -0
- package/dist/api/graphql-api/runs/inputs/create-run.input.js.map +1 -1
- package/dist/api/graphql-api/runs/model/run.model.js +6 -0
- package/dist/api/graphql-api/runs/model/run.model.js.map +1 -1
- package/dist/api/graphql-api/share/model/issue-ref.model.js +53 -0
- package/dist/api/graphql-api/share/model/issue-ref.model.js.map +1 -0
- package/dist/control-plane/bootstrap.js +14 -1
- package/dist/control-plane/bootstrap.js.map +1 -1
- package/dist/control-plane/client-transport.js +1 -1
- package/dist/control-plane/client-transport.js.map +1 -1
- package/dist/control-plane/default-playbook-policy.js +593 -0
- package/dist/control-plane/default-playbook-policy.js.map +1 -0
- package/dist/control-plane/seed-default-playbook.js +77 -8
- package/dist/control-plane/seed-default-playbook.js.map +1 -1
- package/dist/e2e/kit/agents.js +15 -21
- package/dist/e2e/kit/agents.js.map +1 -1
- package/dist/e2e/kit/assertions.js +12 -8
- package/dist/e2e/kit/assertions.js.map +1 -1
- package/dist/e2e/kit/fake-integrator.js +11 -2
- package/dist/e2e/kit/fake-integrator.js.map +1 -1
- package/dist/e2e/kit/git-target-repo.js +1 -1
- package/dist/engine/dbos.service.js +3 -0
- package/dist/engine/dbos.service.js.map +1 -1
- package/dist/features/pr/queries/impl/get-pr-readiness.query.js.map +1 -1
- package/dist/features/runs/commands/impl/create-run.command.js.map +1 -1
- package/dist/features/runs/queries/handlers/runs-query.handlers.js +1 -0
- package/dist/features/runs/queries/handlers/runs-query.handlers.js.map +1 -1
- package/dist/mcp/mcp-capabilities.js +14 -1
- package/dist/mcp/mcp-capabilities.js.map +1 -1
- package/dist/mcp/mcp-facade.service.js +306 -11
- package/dist/mcp/mcp-facade.service.js.map +1 -1
- package/dist/mcp/mcp-tools.js +41 -14
- package/dist/mcp/mcp-tools.js.map +1 -1
- package/dist/observability/activity-signal.js +107 -0
- package/dist/observability/activity-signal.js.map +1 -0
- package/dist/observability/index.js +1 -0
- package/dist/observability/index.js.map +1 -1
- package/dist/pipeline/data-driven-task.workflow.js +482 -40
- package/dist/pipeline/data-driven-task.workflow.js.map +1 -1
- package/dist/pipeline/pipeline.service.js +195 -101
- package/dist/pipeline/pipeline.service.js.map +1 -1
- package/dist/pipeline/route-contract.js +4 -3
- package/dist/pipeline/route-contract.js.map +1 -1
- package/dist/pipeline-core/kit/fixtures.js +72 -21
- package/dist/pipeline-core/kit/fixtures.js.map +1 -1
- package/dist/playbook/import-mapper.js +8 -1
- package/dist/playbook/import-mapper.js.map +1 -1
- package/dist/poller/pr-readiness-core.js +299 -28
- package/dist/poller/pr-readiness-core.js.map +1 -1
- package/dist/revisium/playbooks.service.js +17 -2
- package/dist/revisium/playbooks.service.js.map +1 -1
- package/dist/revisium/run.service.js +1 -0
- package/dist/revisium/run.service.js.map +1 -1
- package/dist/run/append-event.js +8 -5
- package/dist/run/append-event.js.map +1 -1
- package/dist/run/create-run.js +5 -1
- package/dist/run/create-run.js.map +1 -1
- package/dist/run/inspect-run.js +10 -3
- package/dist/run/inspect-run.js.map +1 -1
- package/dist/run/issue-ref.js +88 -0
- package/dist/run/issue-ref.js.map +1 -0
- package/dist/runners/claude-code.service.js +2 -2
- package/dist/runners/claude-code.service.js.map +1 -1
- package/dist/runners/codex.service.js +2 -2
- package/dist/runners/codex.service.js.map +1 -1
- package/dist/runners/integrator-branch-naming.js +13 -8
- package/dist/runners/integrator-branch-naming.js.map +1 -1
- package/dist/runners/integrator.js +254 -89
- package/dist/runners/integrator.js.map +1 -1
- package/dist/runners/worktree.service.js +2 -2
- package/dist/runners/worktree.service.js.map +1 -1
- package/dist/task-control-plane/pr-readiness.service.js +15 -13
- package/dist/task-control-plane/pr-readiness.service.js.map +1 -1
- package/dist/task-control-plane/run-watch.service.js +234 -3
- package/dist/task-control-plane/run-watch.service.js.map +1 -1
- package/dist/task-control-plane/task-control-plane-api.service.js +272 -25
- package/dist/task-control-plane/task-control-plane-api.service.js.map +1 -1
- package/dist/worker/artifact-store.js +21 -3
- package/dist/worker/artifact-store.js.map +1 -1
- package/dist/worker/build-context.js +19 -3
- package/dist/worker/build-context.js.map +1 -1
- package/dist/worker/claude-code-runner.js +76 -15
- package/dist/worker/claude-code-runner.js.map +1 -1
- package/dist/worker/codex-runner.js +40 -13
- package/dist/worker/codex-runner.js.map +1 -1
- package/dist/worker/process-executor.js +149 -20
- package/dist/worker/process-executor.js.map +1 -1
- package/dist/worker/runner-common.js +29 -0
- package/dist/worker/runner-common.js.map +1 -1
- package/dist/worker/runner.js +16 -1
- package/dist/worker/runner.js.map +1 -1
- package/package.json +1 -1
|
@@ -115,7 +115,7 @@ function resolvePrByBranch(repo, headBranch, baseBranch, execGh) {
|
|
|
115
115
|
function fetchPrView(prNumber, repo, execGh) {
|
|
116
116
|
const raw = execGh([
|
|
117
117
|
'pr', 'view', String(prNumber), '--repo', repo,
|
|
118
|
-
'--json', 'number,url,state,isDraft,baseRefName,headRefName,headRefOid,statusCheckRollup,mergeStateStatus,reviewDecision,mergeable',
|
|
118
|
+
'--json', 'number,url,state,isDraft,baseRefName,headRefName,headRefOid,title,statusCheckRollup,mergeStateStatus,reviewDecision,mergeable',
|
|
119
119
|
]);
|
|
120
120
|
return parseGhJson(raw, `pr view #${prNumber}`);
|
|
121
121
|
}
|
|
@@ -253,6 +253,48 @@ function fetchReviewThreads(repo, prNumber, execGh) {
|
|
|
253
253
|
]);
|
|
254
254
|
return mapReviewThreads(parseGhJson(raw, `review-threads #${prNumber}`));
|
|
255
255
|
}
|
|
256
|
+
/** Map the statusCheckRollup contexts GraphQL payload to the SET of check names branch protection
|
|
257
|
+
* marks REQUIRED. The check name matches {@link checkName}'s convention so it joins cleanly against
|
|
258
|
+
* CI failure names: CheckRun → `name`, StatusContext → `context`. */
|
|
259
|
+
function mapRequiredCheckNames(raw) {
|
|
260
|
+
const root = asRecord(raw);
|
|
261
|
+
const repository = asRecord(root?.['repository']);
|
|
262
|
+
const pullRequest = asRecord(repository?.['pullRequest']);
|
|
263
|
+
const rollup = asRecord(pullRequest?.['statusCheckRollup']);
|
|
264
|
+
const contexts = asRecord(rollup?.['contexts']);
|
|
265
|
+
const nodes = Array.isArray(contexts?.['nodes']) ? contexts.nodes : [];
|
|
266
|
+
const required = new Set();
|
|
267
|
+
for (const node of nodes) {
|
|
268
|
+
const ctx = asRecord(node);
|
|
269
|
+
if (!ctx || ctx['isRequired'] !== true)
|
|
270
|
+
continue;
|
|
271
|
+
// CheckRun carries `name`; StatusContext carries `context` — mirror checkName().
|
|
272
|
+
const checkName = asStr(ctx['name']) || asStr(ctx['context']);
|
|
273
|
+
if (checkName)
|
|
274
|
+
required.add(checkName);
|
|
275
|
+
}
|
|
276
|
+
return required;
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* fetchRequiredCheckNames — the SET of check names branch protection marks REQUIRED for this PR.
|
|
280
|
+
*
|
|
281
|
+
* `gh pr view --json statusCheckRollup` (fetchPrView) does NOT carry isRequired, so this issues a
|
|
282
|
+
* dedicated GraphQL read mirroring {@link fetchReviewThreads}. `isRequired` lives on
|
|
283
|
+
* StatusCheckRollupContext and needs the `pullRequestNumber:` arg. Used by pollPr to limit the
|
|
284
|
+
* `ci_changes` verdict to required-check failures (advisory checks — Sonar/CodeRabbit — must not
|
|
285
|
+
* trigger a rework loop). Throws on a gh error so callers can apply the fail-safe fallback.
|
|
286
|
+
*/
|
|
287
|
+
export function fetchRequiredCheckNames(repo, prNumber, execGh) {
|
|
288
|
+
const { owner, name } = splitRepo(repo);
|
|
289
|
+
const raw = execGh([
|
|
290
|
+
'api', 'graphql',
|
|
291
|
+
'-f', 'query=query($owner:String!,$name:String!,$number:Int!){repository(owner:$owner,name:$name){pullRequest(number:$number){statusCheckRollup{contexts(first:100){nodes{__typename ... on CheckRun{name isRequired(pullRequestNumber:$number)} ... on StatusContext{context isRequired(pullRequestNumber:$number)}}}}}}}',
|
|
292
|
+
'-f', `owner=${owner}`,
|
|
293
|
+
'-f', `name=${name}`,
|
|
294
|
+
'-F', `number=${prNumber}`,
|
|
295
|
+
]);
|
|
296
|
+
return mapRequiredCheckNames(parseGhJson(raw, `required-checks #${prNumber}`));
|
|
297
|
+
}
|
|
256
298
|
function collectReviewThreads(input, prNumber, execGh) {
|
|
257
299
|
const threads = input.includeReviewThreads === false ? [] : fetchReviewThreads(input.repo, prNumber, execGh);
|
|
258
300
|
const unresolved = threads.filter((thread) => !thread.isResolved && !thread.isOutdated);
|
|
@@ -264,7 +306,7 @@ function collectReviewThreads(input, prNumber, execGh) {
|
|
|
264
306
|
}
|
|
265
307
|
function fetchComments(input, prNumber, execGh) {
|
|
266
308
|
if (input.includeComments === false) {
|
|
267
|
-
return { human_reviews: [], human_comments: [], bot_comments: [] };
|
|
309
|
+
return { human_reviews: [], human_comments: [], bot_comments: [], coderabbit_reviews: [] };
|
|
268
310
|
}
|
|
269
311
|
const reviewsRaw = execGh(['api', `repos/${input.repo}/pulls/${prNumber}/reviews`]);
|
|
270
312
|
const reviews = parseGhJson(reviewsRaw, `reviews #${prNumber}`);
|
|
@@ -273,36 +315,149 @@ function fetchComments(input, prNumber, execGh) {
|
|
|
273
315
|
const issueCommentsRaw = execGh(['api', `repos/${input.repo}/issues/${prNumber}/comments`]);
|
|
274
316
|
const issueComments = parseGhJson(issueCommentsRaw, `issue-comments #${prNumber}`);
|
|
275
317
|
const allComments = [...reviewComments, ...issueComments];
|
|
318
|
+
// reviews come back chronological; keep only the latest per author so a stale review never
|
|
319
|
+
// overrides a newer one. Mirror this for CodeRabbit (a Bot author, hence excluded from the human
|
|
320
|
+
// map) — otherwise a stale actionable CodeRabbit body keeps a PR stuck even after a newer clean
|
|
321
|
+
// review (#142 Finding #2).
|
|
276
322
|
const latestHumanReviewByAuthor = new Map();
|
|
323
|
+
const latestCodeRabbitReviewByAuthor = new Map();
|
|
277
324
|
for (const review of reviews) {
|
|
278
|
-
if (!review.user
|
|
325
|
+
if (!review.user)
|
|
326
|
+
continue;
|
|
327
|
+
if (isBot(review.user)) {
|
|
328
|
+
if (isCodeRabbit(review.user))
|
|
329
|
+
latestCodeRabbitReviewByAuthor.set(review.user.login, review);
|
|
279
330
|
continue;
|
|
331
|
+
}
|
|
280
332
|
latestHumanReviewByAuthor.set(review.user.login, review);
|
|
281
333
|
}
|
|
282
334
|
return {
|
|
283
335
|
human_reviews: [...latestHumanReviewByAuthor.values()],
|
|
284
336
|
human_comments: allComments.filter((comment) => !isBot(comment.user)),
|
|
285
337
|
bot_comments: allComments.filter((comment) => isBot(comment.user)),
|
|
338
|
+
// CodeRabbit review bodies are excluded from human_reviews above (they are Bot-authored). They
|
|
339
|
+
// carry actionable findings as a top-level review body, so keep them for buildFeedback (#142).
|
|
340
|
+
coderabbit_reviews: [...latestCodeRabbitReviewByAuthor.values()],
|
|
286
341
|
};
|
|
287
342
|
}
|
|
343
|
+
function isCodeRabbit(user) {
|
|
344
|
+
return (user?.login.toLowerCase() ?? '').includes('coderabbit');
|
|
345
|
+
}
|
|
346
|
+
/** Classify a single CodeRabbit body into a non-actionable provider-WAIT reason (or '' for none).
|
|
347
|
+
* Single source of truth shared by codeRabbitCommentReason (drives providerWait) and
|
|
348
|
+
* isCodeRabbitActionable (so a provider-wait body is never ALSO emitted as a developerFix — #144). */
|
|
349
|
+
function codeRabbitProviderWaitReason(rawBody) {
|
|
350
|
+
const body = rawBody.toLowerCase();
|
|
351
|
+
if (/rate.limit|provider.limit|quota|capacity|temporar/.test(body) && /did not start|could not start|not start|unable to start|paused/.test(body)) {
|
|
352
|
+
return 'provider_limit';
|
|
353
|
+
}
|
|
354
|
+
if (/review in progress|reviewing|processing/.test(body))
|
|
355
|
+
return 'review_in_progress';
|
|
356
|
+
if (/skipped|did not review|no files to review/.test(body))
|
|
357
|
+
return 'skipped';
|
|
358
|
+
if (/no actionable comments|no issues found|looks good/.test(body))
|
|
359
|
+
return 'no_actionable_comments';
|
|
360
|
+
return '';
|
|
361
|
+
}
|
|
288
362
|
function codeRabbitCommentReason(comments) {
|
|
289
363
|
for (const comment of comments) {
|
|
290
|
-
|
|
291
|
-
if (!login.includes('coderabbit'))
|
|
364
|
+
if (!isCodeRabbit(comment.user))
|
|
292
365
|
continue;
|
|
293
|
-
const
|
|
294
|
-
if (
|
|
295
|
-
return
|
|
296
|
-
}
|
|
297
|
-
if (/review in progress|reviewing|processing/.test(body))
|
|
298
|
-
return 'review_in_progress';
|
|
299
|
-
if (/skipped|did not review|no files to review/.test(body))
|
|
300
|
-
return 'skipped';
|
|
301
|
-
if (/no actionable comments|no issues found|looks good/.test(body))
|
|
302
|
-
return 'no_actionable_comments';
|
|
366
|
+
const reason = codeRabbitProviderWaitReason(comment.body);
|
|
367
|
+
if (reason)
|
|
368
|
+
return reason;
|
|
303
369
|
}
|
|
304
370
|
return '';
|
|
305
371
|
}
|
|
372
|
+
/** Best-effort extraction of a "retry in N <unit>" hint from a bot comment body, e.g.
|
|
373
|
+
* "retry in 10 min". Returns null when no hint is present — the caller must keep the
|
|
374
|
+
* raw evidence either way. Case-insensitive and intentionally lenient. */
|
|
375
|
+
function extractResumeHint(comments) {
|
|
376
|
+
for (const comment of comments) {
|
|
377
|
+
if (!isCodeRabbit(comment.user))
|
|
378
|
+
continue;
|
|
379
|
+
const match = comment.body.match(/\/retry[^.]*?(\d+)\s*(min|minute|hour|sec)/i);
|
|
380
|
+
if (match) {
|
|
381
|
+
const unit = match[2].toLowerCase().startsWith('min') ? 'min' : match[2].toLowerCase().startsWith('hour') ? 'hour' : 'sec';
|
|
382
|
+
return `retry in ${match[1]} ${unit}`;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
return null;
|
|
386
|
+
}
|
|
387
|
+
// CodeRabbit's own structured markers for a finding that still needs developer action. A review
|
|
388
|
+
// body or comment carrying any of these is an actionable blocker — UNLESS it is one of the
|
|
389
|
+
// non-actionable states classified by codeRabbitCommentReason (provider_limit / review_in_progress
|
|
390
|
+
// / skipped / no_actionable_comments), or it has been addressed/resolved/outdated.
|
|
391
|
+
const CODERABBIT_FINDING_MARKERS = [
|
|
392
|
+
/⚠️\s*potential issue/i,
|
|
393
|
+
/🛠️\s*refactor suggestion/i,
|
|
394
|
+
/⚠️\s*outside diff range/i,
|
|
395
|
+
/🧹\s*nitpick/i,
|
|
396
|
+
];
|
|
397
|
+
// "Actionable comments posted: N" — the count CodeRabbit prints atop a review summary. N>0 means
|
|
398
|
+
// the review found work; N=0 (handled as no_actionable_comments) is explicitly NOT a blocker.
|
|
399
|
+
const CODERABBIT_ACTIONABLE_COUNT_RE = /actionable comments posted:\s*(\d+)/i;
|
|
400
|
+
// A finding CodeRabbit reports as already handled. Anchored to CodeRabbit's own addressed-markers
|
|
401
|
+
// rather than bare words so a live finding whose prose merely mentions e.g. "remove these outdated
|
|
402
|
+
// comments" or "unresolved promise" survives. Such evidence is stale and must NOT remain a blocker.
|
|
403
|
+
const CODERABBIT_ADDRESSED_RE = /✅\s*addressed|\baddressed in\b|marked as resolved|now outdated|marked (?:as )?outdated/i;
|
|
404
|
+
/** True when a CodeRabbit review-body / comment text reports a finding that still needs developer
|
|
405
|
+
* action. Conservative — returns false (item dropped, not kept as a blocker) when the body is:
|
|
406
|
+
* an explicit `Actionable comments posted: 0`; a non-actionable provider-WAIT state (so a wait is
|
|
407
|
+
* never ALSO a developerFix — #144); or carries an addressed/resolved/outdated marker. */
|
|
408
|
+
function isCodeRabbitActionable(body) {
|
|
409
|
+
const countMatch = body.match(CODERABBIT_ACTIONABLE_COUNT_RE);
|
|
410
|
+
if (countMatch && Number(countMatch[1]) === 0)
|
|
411
|
+
return false;
|
|
412
|
+
if (codeRabbitProviderWaitReason(body) !== '')
|
|
413
|
+
return false;
|
|
414
|
+
if (CODERABBIT_ADDRESSED_RE.test(body))
|
|
415
|
+
return false;
|
|
416
|
+
if (countMatch && Number(countMatch[1]) > 0)
|
|
417
|
+
return true;
|
|
418
|
+
return CODERABBIT_FINDING_MARKERS.some((marker) => marker.test(body));
|
|
419
|
+
}
|
|
420
|
+
/** Actionable CodeRabbit feedback that arrives as a top-level review body or bot comment rather
|
|
421
|
+
* than a resolvable GraphQL review thread. `fetchComments` already retrieves this data (bot reviews
|
|
422
|
+
* and bot comments) but buildFeedback never routed it to developerFixes; an actual finding therefore
|
|
423
|
+
* vanished (#142). Review bodies come from the `reviews` REST call independent of includeReviewThreads,
|
|
424
|
+
* so this also covers the poll path. The non-actionable provider states (provider_limit /
|
|
425
|
+
* review_in_progress / skipped / no_actionable_comments) stay with providerWait — they are excluded
|
|
426
|
+
* here so the WAIT and FIXES buckets never collapse (#144).
|
|
427
|
+
*
|
|
428
|
+
* `threadLocations` carries the path:line of unresolved review threads already emitted as the
|
|
429
|
+
* richer `review_thread` developerFix; a bot comment at the same location is the SAME inline
|
|
430
|
+
* finding (the thread's first comment), so it is skipped to avoid emitting it twice (#142
|
|
431
|
+
* Finding #1). Only non-empty locations dedup — a comment with no path:line is never matched. */
|
|
432
|
+
function codeRabbitActionableFeedback(reviews, botComments, threadLocations = new Set()) {
|
|
433
|
+
const items = [];
|
|
434
|
+
for (const review of reviews) {
|
|
435
|
+
if (!isCodeRabbit(review.user) || !isCodeRabbitActionable(review.body))
|
|
436
|
+
continue;
|
|
437
|
+
items.push({
|
|
438
|
+
source: 'coderabbit_review_body',
|
|
439
|
+
summary: compactBody(review.body),
|
|
440
|
+
author: review.user?.login ?? '',
|
|
441
|
+
location: '',
|
|
442
|
+
evidence: compactBody(review.body),
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
for (const comment of botComments) {
|
|
446
|
+
if (!isCodeRabbit(comment.user) || !isCodeRabbitActionable(comment.body))
|
|
447
|
+
continue;
|
|
448
|
+
const location = locationOf(comment);
|
|
449
|
+
if (location && threadLocations.has(location))
|
|
450
|
+
continue;
|
|
451
|
+
items.push({
|
|
452
|
+
source: 'coderabbit_comment',
|
|
453
|
+
summary: compactBody(comment.body),
|
|
454
|
+
author: comment.user?.login ?? '',
|
|
455
|
+
location,
|
|
456
|
+
evidence: compactBody(comment.body),
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
return items;
|
|
460
|
+
}
|
|
306
461
|
function compactCheckLists(checks) {
|
|
307
462
|
const terminal = [];
|
|
308
463
|
const pending = [];
|
|
@@ -359,11 +514,63 @@ function locationOf(item) {
|
|
|
359
514
|
const path = item.path ?? item.component ?? '';
|
|
360
515
|
return item.line ? `${path}:${item.line}` : path;
|
|
361
516
|
}
|
|
362
|
-
function providerWaitFeedback(state) {
|
|
517
|
+
function providerWaitFeedback(state, botComments) {
|
|
363
518
|
const codeRabbit = state.codeRabbit;
|
|
364
519
|
if (codeRabbit?.state !== 'waiting')
|
|
365
520
|
return [];
|
|
366
|
-
|
|
521
|
+
// This function is reached on the TERMINAL path (collectPrReadiness only calls buildFeedback
|
|
522
|
+
// after the ci.pending gate). By then any CodeRabbit check is terminal, so a rate-limit /
|
|
523
|
+
// review-in-progress BOT COMMENT is necessarily stale relative to the current green head —
|
|
524
|
+
// it is informational, NOT a readiness blocker. A genuinely-pending CodeRabbit check is
|
|
525
|
+
// caught earlier by the pending gate and surfaced as a blocking providerWait (see
|
|
526
|
+
// buildWaitingReadiness), so the two cases are distinguishable via `blocking`.
|
|
527
|
+
return [{
|
|
528
|
+
provider: 'CodeRabbit',
|
|
529
|
+
reason: codeRabbit.reason,
|
|
530
|
+
evidence: codeRabbit.evidence ?? codeRabbit.statusContext,
|
|
531
|
+
blocking: false,
|
|
532
|
+
nature: 'informational',
|
|
533
|
+
resumeAfter: extractResumeHint(botComments),
|
|
534
|
+
}];
|
|
535
|
+
}
|
|
536
|
+
function escapeRegExp(value) {
|
|
537
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
538
|
+
}
|
|
539
|
+
function issueRefTitleTag(issueRef, repo) {
|
|
540
|
+
return issueRef.repo.toLowerCase() === repo.toLowerCase()
|
|
541
|
+
? `#${issueRef.number}`
|
|
542
|
+
: `${issueRef.repo}#${issueRef.number}`;
|
|
543
|
+
}
|
|
544
|
+
function hasIssueBranchToken(head, issueRef) {
|
|
545
|
+
return new RegExp(`(?:^|[/-])issue-${issueRef.number}(?:-|$)`).test(head ?? '');
|
|
546
|
+
}
|
|
547
|
+
function hasIssueTitleToken(title, issueRef, repo) {
|
|
548
|
+
const boundaryBefore = String.raw `(?:^|[\s([{"'])`;
|
|
549
|
+
const boundaryAfter = String.raw `(?=$|[^A-Za-z0-9_-])`;
|
|
550
|
+
const qualified = new RegExp(`${boundaryBefore}${escapeRegExp(`${issueRef.repo}#${issueRef.number}`)}${boundaryAfter}`, 'i');
|
|
551
|
+
if (qualified.test(title ?? ''))
|
|
552
|
+
return true;
|
|
553
|
+
if (issueRef.repo.toLowerCase() !== repo.toLowerCase())
|
|
554
|
+
return false;
|
|
555
|
+
return new RegExp(`${boundaryBefore}#${issueRef.number}${boundaryAfter}`).test(title ?? '');
|
|
556
|
+
}
|
|
557
|
+
function issueLinkageFeedback(issueRef, repo, pr) {
|
|
558
|
+
if (!issueRef)
|
|
559
|
+
return [];
|
|
560
|
+
const issueBranch = `issue-${issueRef.number}`;
|
|
561
|
+
const issueTag = issueRefTitleTag(issueRef, repo);
|
|
562
|
+
const missing = [];
|
|
563
|
+
if (!hasIssueBranchToken(pr.head, issueRef))
|
|
564
|
+
missing.push(`branch missing ${issueBranch}`);
|
|
565
|
+
if (!hasIssueTitleToken(pr.title, issueRef, repo))
|
|
566
|
+
missing.push(`title missing ${issueTag}`);
|
|
567
|
+
if (missing.length === 0)
|
|
568
|
+
return [];
|
|
569
|
+
return [{
|
|
570
|
+
source: 'issue_ref_linkage',
|
|
571
|
+
summary: `PR is missing issueRef linkage for ${issueTag}`,
|
|
572
|
+
evidence: `${missing.join('; ')} (head=${pr.head ?? ''}, title=${pr.title ?? ''})`,
|
|
573
|
+
}];
|
|
367
574
|
}
|
|
368
575
|
function buildFeedback(input) {
|
|
369
576
|
const developerFixes = [
|
|
@@ -397,6 +604,12 @@ function buildFeedback(input) {
|
|
|
397
604
|
location: locationOf(thread),
|
|
398
605
|
evidence: thread.url ?? thread.id,
|
|
399
606
|
})),
|
|
607
|
+
// Actionable CodeRabbit feedback delivered as a review body / bot comment rather than a
|
|
608
|
+
// resolvable review thread (#142). Appended after review_thread items so the thread-aware
|
|
609
|
+
// classification keeps producing the leading developerFix where threads are present. Pass the
|
|
610
|
+
// thread path:line locations so a bot comment for a thread already emitted above is not
|
|
611
|
+
// emitted a second time (#142 Finding #1).
|
|
612
|
+
...codeRabbitActionableFeedback(input.coderabbitReviews ?? [], input.botComments, new Set(input.reviewThreads.items.map((thread) => locationOf(thread)).filter((loc) => loc !== ''))),
|
|
400
613
|
];
|
|
401
614
|
const reviewerQuestions = input.humanComments
|
|
402
615
|
.filter((comment) => comment.body.includes('?'))
|
|
@@ -406,12 +619,13 @@ function buildFeedback(input) {
|
|
|
406
619
|
author: comment.user?.login ?? '',
|
|
407
620
|
location: locationOf(comment),
|
|
408
621
|
}));
|
|
409
|
-
const providerWait = providerWaitFeedback(input.providerState);
|
|
622
|
+
const providerWait = providerWaitFeedback(input.providerState, input.botComments);
|
|
410
623
|
const humanDecisions = [
|
|
411
624
|
...(input.reviewDecision && input.reviewDecision !== 'APPROVED'
|
|
412
625
|
? [{ source: 'github_review_decision', summary: `Review decision is ${input.reviewDecision}` }]
|
|
413
626
|
: []),
|
|
414
627
|
...(input.sonar.unavailable ? [{ source: 'sonar', summary: 'Sonar was configured but unavailable.' }] : []),
|
|
628
|
+
...issueLinkageFeedback(input.issueRef, input.repo, input.pr ?? {}),
|
|
415
629
|
];
|
|
416
630
|
const ignoredNoise = input.botComments
|
|
417
631
|
.filter((comment) => !comment.user?.login.toLowerCase().includes('coderabbit'))
|
|
@@ -431,6 +645,7 @@ function emptyPr(prNumber, state = '') {
|
|
|
431
645
|
base: '',
|
|
432
646
|
head: '',
|
|
433
647
|
headSha: '',
|
|
648
|
+
title: '',
|
|
434
649
|
mergeState: '',
|
|
435
650
|
};
|
|
436
651
|
}
|
|
@@ -443,6 +658,7 @@ function prFromView(prNumber, view) {
|
|
|
443
658
|
base: view?.baseRefName ?? '',
|
|
444
659
|
head: view?.headRefName ?? '',
|
|
445
660
|
headSha: view?.headRefOid ?? '',
|
|
661
|
+
title: view?.title ?? '',
|
|
446
662
|
mergeState: view?.mergeStateStatus ?? '',
|
|
447
663
|
};
|
|
448
664
|
}
|
|
@@ -465,6 +681,8 @@ function buildEmptyFeedback(input) {
|
|
|
465
681
|
humanReviews: [],
|
|
466
682
|
humanComments: [],
|
|
467
683
|
botComments: [],
|
|
684
|
+
issueRef: input.issueRef,
|
|
685
|
+
repo: '',
|
|
468
686
|
});
|
|
469
687
|
}
|
|
470
688
|
function buildMergedReadiness(input, resolved) {
|
|
@@ -481,7 +699,8 @@ function buildMergedReadiness(input, resolved) {
|
|
|
481
699
|
nextAction: 'none',
|
|
482
700
|
evidence: [`PR #${resolved.prNumber} is merged.`],
|
|
483
701
|
feedback: buildEmptyFeedback({ sonarConfigured, includeReviewThreads }),
|
|
484
|
-
ciSummary: emptyCiSummary(true),
|
|
702
|
+
ciSummary: { ...emptyCiSummary(true), ...(input.issueRef ? { issueRef: input.issueRef } : {}) },
|
|
703
|
+
...(input.issueRef ? { issueRef: input.issueRef } : {}),
|
|
485
704
|
};
|
|
486
705
|
}
|
|
487
706
|
function buildNeedsHumanReadiness(input, resolved) {
|
|
@@ -499,47 +718,83 @@ function buildNeedsHumanReadiness(input, resolved) {
|
|
|
499
718
|
nextAction: 'human_decision',
|
|
500
719
|
evidence: [resolved.lesson],
|
|
501
720
|
feedback: { ...feedback, humanDecisions: [{ source: 'pr_resolution', summary: resolved.lesson }] },
|
|
502
|
-
ciSummary: emptyCiSummary(false),
|
|
721
|
+
ciSummary: { ...emptyCiSummary(false), ...(input.issueRef ? { issueRef: input.issueRef } : {}) },
|
|
722
|
+
...(input.issueRef ? { issueRef: input.issueRef } : {}),
|
|
503
723
|
};
|
|
504
724
|
}
|
|
505
725
|
function buildWaitingReadiness(input) {
|
|
726
|
+
// The pending path previously left providerState empty and built providerWait from it (also
|
|
727
|
+
// empty), so a genuinely-pending CodeRabbit check was never explained as a provider wait.
|
|
728
|
+
// Synthesize a blocking providerState/providerWait from the pending check names so the output
|
|
729
|
+
// surfaces WHY the wait is blocking — mirroring the informational entry emitted on the
|
|
730
|
+
// terminal path. This does not change the verdict (still waiting/watcher_wait).
|
|
731
|
+
const pendingCodeRabbit = input.ci.pendingNames.find((name) => name.toLowerCase().includes('coderabbit'));
|
|
732
|
+
const pendingProviderState = pendingCodeRabbit
|
|
733
|
+
? {
|
|
734
|
+
codeRabbit: {
|
|
735
|
+
state: 'waiting',
|
|
736
|
+
reason: 'check_pending',
|
|
737
|
+
statusContext: 'IN_PROGRESS',
|
|
738
|
+
evidence: 'CodeRabbit check is still pending for the current head — waiting is blocking.',
|
|
739
|
+
},
|
|
740
|
+
}
|
|
741
|
+
: {};
|
|
506
742
|
const feedback = buildFeedback({
|
|
507
743
|
checks: input.checkLists,
|
|
508
|
-
providerState:
|
|
744
|
+
providerState: pendingProviderState,
|
|
509
745
|
sonar: emptySonar(input.sonarConfigured),
|
|
510
746
|
reviewDecision: input.prView.reviewDecision ?? '',
|
|
511
747
|
reviewThreads: input.reviewThreads,
|
|
512
748
|
humanReviews: [],
|
|
513
749
|
humanComments: [],
|
|
514
750
|
botComments: [],
|
|
751
|
+
issueRef: input.issueRef,
|
|
752
|
+
repo: input.repo,
|
|
753
|
+
pr: { head: input.prView.headRefName, title: input.prView.title },
|
|
515
754
|
});
|
|
755
|
+
// buildFeedback -> providerWaitFeedback classifies from providerState, but on the pending path
|
|
756
|
+
// the wait IS blocking (a live check), so emit a blocking item in place of the synthesized one.
|
|
757
|
+
const providerWait = pendingCodeRabbit
|
|
758
|
+
? [{
|
|
759
|
+
provider: 'CodeRabbit',
|
|
760
|
+
reason: 'check_pending',
|
|
761
|
+
evidence: 'CodeRabbit check is still pending for the current head — waiting is blocking.',
|
|
762
|
+
blocking: true,
|
|
763
|
+
nature: 'blocking',
|
|
764
|
+
resumeAfter: null,
|
|
765
|
+
}]
|
|
766
|
+
: feedback.providerWait;
|
|
516
767
|
return {
|
|
517
768
|
verdict: 'waiting',
|
|
518
769
|
pr: prFromView(input.prNumber, input.prView),
|
|
519
770
|
checks: input.checkLists,
|
|
520
771
|
reviewDecision: input.prView.reviewDecision ?? '',
|
|
521
772
|
reviewThreads: input.reviewThreads,
|
|
522
|
-
providerState:
|
|
773
|
+
providerState: pendingProviderState,
|
|
523
774
|
sonar: emptySonar(input.sonarConfigured),
|
|
524
775
|
nextAction: 'watcher_wait',
|
|
525
776
|
evidence: input.evidence,
|
|
526
|
-
feedback,
|
|
777
|
+
feedback: { ...feedback, providerWait },
|
|
527
778
|
ciSummary: {
|
|
528
779
|
ci_passed: false,
|
|
529
780
|
checks: input.ci.checks,
|
|
530
781
|
...(input.isDraft ? { isDraft: true } : {}),
|
|
782
|
+
...(input.issueRef ? { issueRef: input.issueRef } : {}),
|
|
531
783
|
sonar_issues: [],
|
|
532
784
|
sonar_hotspots_to_review: [],
|
|
533
785
|
human_reviews: [],
|
|
534
786
|
human_comments: [],
|
|
535
787
|
bot_comments: [],
|
|
536
788
|
},
|
|
789
|
+
...(input.issueRef ? { issueRef: input.issueRef } : {}),
|
|
537
790
|
};
|
|
538
791
|
}
|
|
539
792
|
function readinessAction(input) {
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
793
|
+
// NOTE: a CodeRabbit provider_limit / review_in_progress bot comment is intentionally NOT a
|
|
794
|
+
// `waiting` verdict here. This function is only reached after the ci.pending gate (see
|
|
795
|
+
// collectPrReadiness), so every check is already terminal — such a comment is stale relative
|
|
796
|
+
// to the current green head and is surfaced as an informational providerWait instead. A
|
|
797
|
+
// genuinely-pending CodeRabbit check is handled by the pending gate before this point.
|
|
543
798
|
if (!input.ci.ci_passed || input.feedback.developerFixes.length > 0) {
|
|
544
799
|
return { verdict: 'needs_work', nextAction: 'developer_fix' };
|
|
545
800
|
}
|
|
@@ -568,24 +823,28 @@ export async function collectPrReadiness(input, execGh = defaultExecGh, fetchSon
|
|
|
568
823
|
const sonarConfigured = Boolean(input.sonarProject);
|
|
569
824
|
if (prView.isDraft === true) {
|
|
570
825
|
return buildWaitingReadiness({
|
|
826
|
+
repo: input.repo,
|
|
571
827
|
prNumber,
|
|
572
828
|
prView,
|
|
573
829
|
checkLists,
|
|
574
830
|
ci,
|
|
575
831
|
reviewThreads,
|
|
576
832
|
sonarConfigured,
|
|
833
|
+
issueRef: input.issueRef,
|
|
577
834
|
evidence: [`PR #${prNumber} is still draft.`],
|
|
578
835
|
isDraft: true,
|
|
579
836
|
});
|
|
580
837
|
}
|
|
581
838
|
if (ci.pending) {
|
|
582
839
|
return buildWaitingReadiness({
|
|
840
|
+
repo: input.repo,
|
|
583
841
|
prNumber,
|
|
584
842
|
prView,
|
|
585
843
|
checkLists,
|
|
586
844
|
ci,
|
|
587
845
|
reviewThreads,
|
|
588
846
|
sonarConfigured,
|
|
847
|
+
issueRef: input.issueRef,
|
|
589
848
|
evidence: ci.pendingNames.length > 0 ? [`Pending checks: ${ci.pendingNames.join(', ')}`] : ['No check rollup entries are registered yet.'],
|
|
590
849
|
});
|
|
591
850
|
}
|
|
@@ -609,6 +868,10 @@ export async function collectPrReadiness(input, execGh = defaultExecGh, fetchSon
|
|
|
609
868
|
humanReviews: comments.human_reviews,
|
|
610
869
|
humanComments: comments.human_comments,
|
|
611
870
|
botComments: comments.bot_comments,
|
|
871
|
+
coderabbitReviews: comments.coderabbit_reviews,
|
|
872
|
+
issueRef: input.issueRef,
|
|
873
|
+
repo: input.repo,
|
|
874
|
+
pr: { head: prView.headRefName, title: prView.title },
|
|
612
875
|
});
|
|
613
876
|
const ciSummary = {
|
|
614
877
|
ci_passed: ci.ci_passed,
|
|
@@ -616,6 +879,7 @@ export async function collectPrReadiness(input, execGh = defaultExecGh, fetchSon
|
|
|
616
879
|
mergeStateStatus: prView.mergeStateStatus,
|
|
617
880
|
reviewDecision: prView.reviewDecision,
|
|
618
881
|
mergeable: prView.mergeable,
|
|
882
|
+
...(input.issueRef ? { issueRef: input.issueRef } : {}),
|
|
619
883
|
sonar_issues: sonar.issues,
|
|
620
884
|
sonar_hotspots_to_review: sonar.hotspots,
|
|
621
885
|
human_reviews: comments.human_reviews,
|
|
@@ -623,7 +887,8 @@ export async function collectPrReadiness(input, execGh = defaultExecGh, fetchSon
|
|
|
623
887
|
bot_comments: comments.bot_comments,
|
|
624
888
|
...(sonar.unavailable ? { sonar_unavailable: true } : {}),
|
|
625
889
|
};
|
|
626
|
-
const { verdict, nextAction } = readinessAction({ ci, feedback
|
|
890
|
+
const { verdict, nextAction } = readinessAction({ ci, feedback });
|
|
891
|
+
const hasInformationalProviderWait = feedback.providerWait.some((item) => item.blocking === false);
|
|
627
892
|
return {
|
|
628
893
|
verdict,
|
|
629
894
|
pr: prFromView(prNumber, prView),
|
|
@@ -636,10 +901,15 @@ export async function collectPrReadiness(input, execGh = defaultExecGh, fetchSon
|
|
|
636
901
|
evidence: [
|
|
637
902
|
`PR #${prNumber} state=${prView.state ?? 'unknown'} draft=${Boolean(prView.isDraft)}`,
|
|
638
903
|
`checks pass=${checkLists.pass.length} fail=${checkLists.fail.length} pending=${checkLists.pending.length}`,
|
|
639
|
-
...(
|
|
904
|
+
...(input.issueRef ? [`issueRef ${issueRefTitleTag(input.issueRef, input.repo)} expected in branch and title`] : []),
|
|
905
|
+
...(providers.codeRabbit?.reason === 'provider_limit' ? ['CodeRabbit provider/rate limit comment detected.'] : []),
|
|
906
|
+
...(verdict === 'ready' && hasInformationalProviderWait
|
|
907
|
+
? ['Provider wait is informational (stale CodeRabbit rate-limit comment); not blocking readiness.']
|
|
908
|
+
: []),
|
|
640
909
|
],
|
|
641
910
|
feedback,
|
|
642
911
|
ciSummary,
|
|
912
|
+
...(input.issueRef ? { issueRef: input.issueRef } : {}),
|
|
643
913
|
};
|
|
644
914
|
}
|
|
645
915
|
export function toReadinessInput(input) {
|
|
@@ -649,6 +919,7 @@ export function toReadinessInput(input) {
|
|
|
649
919
|
headBranch: input.head_branch,
|
|
650
920
|
baseBranch: input.base_branch,
|
|
651
921
|
sonarProject: input.sonar_project,
|
|
922
|
+
issueRef: input.issue_ref,
|
|
652
923
|
includeComments: true,
|
|
653
924
|
includeReviewThreads: false,
|
|
654
925
|
};
|