@kbediako/codex-orchestrator 0.1.28 → 0.1.30
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
CHANGED
|
@@ -215,6 +215,7 @@ Repo internals, development workflows, and deeper architecture notes (contributo
|
|
|
215
215
|
- `docs/guides/collab-vs-mcp.md` (agent-first decision guide)
|
|
216
216
|
- `docs/guides/rlm-recursion-v2.md` (RLM recursion reference)
|
|
217
217
|
- `docs/guides/cloud-mode-preflight.md` (cloud-mode preflight + fallback guidance)
|
|
218
|
+
- `docs/guides/review-artifacts.md` (where `npm run review` writes prompt/output artifacts)
|
|
218
219
|
|
|
219
220
|
## RLM benchmark graphs
|
|
220
221
|
|
|
@@ -1238,6 +1238,7 @@ Commands:
|
|
|
1238
1238
|
Notes:
|
|
1239
1239
|
RLM recursion guidance: docs/guides/rlm-recursion-v2.md
|
|
1240
1240
|
Cloud-mode preflight/fallback guide: docs/guides/cloud-mode-preflight.md
|
|
1241
|
+
Review artifacts guide: docs/guides/review-artifacts.md
|
|
1241
1242
|
`);
|
|
1242
1243
|
}
|
|
1243
1244
|
void main();
|
|
@@ -1300,6 +1301,9 @@ Subcommands:
|
|
|
1300
1301
|
Examples:
|
|
1301
1302
|
codex-orchestrator pr watch-merge --pr 211 --dry-run --quiet-minutes 10
|
|
1302
1303
|
codex-orchestrator pr watch-merge --pr 211 --auto-merge --merge-method squash
|
|
1304
|
+
|
|
1305
|
+
Guide:
|
|
1306
|
+
Review artifacts (prompt + output log paths): docs/guides/review-artifacts.md
|
|
1303
1307
|
`);
|
|
1304
1308
|
}
|
|
1305
1309
|
function printRlmHelp() {
|
|
@@ -9,6 +9,9 @@ const DEFAULT_MERGE_METHOD = 'squash';
|
|
|
9
9
|
const CHECKRUN_PASS_CONCLUSIONS = new Set(['SUCCESS', 'SKIPPED', 'NEUTRAL']);
|
|
10
10
|
const STATUS_CONTEXT_PASS_STATES = new Set(['SUCCESS']);
|
|
11
11
|
const STATUS_CONTEXT_PENDING_STATES = new Set(['EXPECTED', 'PENDING']);
|
|
12
|
+
const REQUIRED_BUCKET_PASS = new Set(['pass']);
|
|
13
|
+
const REQUIRED_BUCKET_PENDING = new Set(['pending']);
|
|
14
|
+
const REQUIRED_BUCKET_FAILED = new Set(['fail', 'cancel', 'skipping']);
|
|
12
15
|
const MERGEABLE_STATES = new Set(['CLEAN', 'HAS_HOOKS', 'UNSTABLE']);
|
|
13
16
|
const BLOCKED_REVIEW_DECISIONS = new Set(['CHANGES_REQUESTED', 'REVIEW_REQUIRED']);
|
|
14
17
|
const DO_NOT_MERGE_LABEL = /do[\s_-]*not[\s_-]*merge/i;
|
|
@@ -67,6 +70,9 @@ query($owner:String!, $repo:String!, $number:Int!) {
|
|
|
67
70
|
function normalizeEnum(value) {
|
|
68
71
|
return typeof value === 'string' ? value.trim().toUpperCase() : '';
|
|
69
72
|
}
|
|
73
|
+
function normalizeBucket(value) {
|
|
74
|
+
return typeof value === 'string' ? value.trim().toLowerCase() : '';
|
|
75
|
+
}
|
|
70
76
|
function formatDuration(ms) {
|
|
71
77
|
if (ms <= 0) {
|
|
72
78
|
return '0s';
|
|
@@ -299,7 +305,80 @@ function summarizeChecks(nodes) {
|
|
|
299
305
|
}
|
|
300
306
|
return summary;
|
|
301
307
|
}
|
|
302
|
-
function
|
|
308
|
+
export function summarizeRequiredChecks(entries) {
|
|
309
|
+
const summary = {
|
|
310
|
+
total: 0,
|
|
311
|
+
successCount: 0,
|
|
312
|
+
pending: [],
|
|
313
|
+
failed: []
|
|
314
|
+
};
|
|
315
|
+
if (!Array.isArray(entries)) {
|
|
316
|
+
return summary;
|
|
317
|
+
}
|
|
318
|
+
for (const entry of entries) {
|
|
319
|
+
if (!entry || typeof entry !== 'object') {
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
summary.total += 1;
|
|
323
|
+
const name = typeof entry.name === 'string' && entry.name.trim() ? entry.name.trim() : 'required-check';
|
|
324
|
+
const bucket = normalizeBucket(entry.bucket);
|
|
325
|
+
const state = normalizeEnum(entry.state);
|
|
326
|
+
const detailsUrl = typeof entry.link === 'string' ? entry.link : null;
|
|
327
|
+
if (REQUIRED_BUCKET_PASS.has(bucket)) {
|
|
328
|
+
summary.successCount += 1;
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
if (REQUIRED_BUCKET_PENDING.has(bucket)) {
|
|
332
|
+
summary.pending.push(name);
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
if (REQUIRED_BUCKET_FAILED.has(bucket)) {
|
|
336
|
+
summary.failed.push({
|
|
337
|
+
name,
|
|
338
|
+
state: state || bucket.toUpperCase() || 'UNKNOWN',
|
|
339
|
+
detailsUrl
|
|
340
|
+
});
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
if (STATUS_CONTEXT_PENDING_STATES.has(state)) {
|
|
344
|
+
summary.pending.push(name);
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
if (STATUS_CONTEXT_PASS_STATES.has(state)) {
|
|
348
|
+
summary.successCount += 1;
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
summary.failed.push({
|
|
352
|
+
name,
|
|
353
|
+
state: state || 'UNKNOWN',
|
|
354
|
+
detailsUrl
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
return summary;
|
|
358
|
+
}
|
|
359
|
+
function hasRequiredChecksSummary(summary) {
|
|
360
|
+
return Boolean(summary && typeof summary === 'object' && summary.total > 0);
|
|
361
|
+
}
|
|
362
|
+
export function resolveRequiredChecksSummary(freshSummary, previousSummary, fetchError = false) {
|
|
363
|
+
if (hasRequiredChecksSummary(freshSummary)) {
|
|
364
|
+
return freshSummary;
|
|
365
|
+
}
|
|
366
|
+
if (fetchError && hasRequiredChecksSummary(previousSummary)) {
|
|
367
|
+
return previousSummary;
|
|
368
|
+
}
|
|
369
|
+
return null;
|
|
370
|
+
}
|
|
371
|
+
export function resolveCachedRequiredChecksSummary(previousCache, currentHeadOid) {
|
|
372
|
+
if (!previousCache || typeof previousCache !== 'object') {
|
|
373
|
+
return null;
|
|
374
|
+
}
|
|
375
|
+
const cachedHeadOid = typeof previousCache.headOid === 'string' ? previousCache.headOid : null;
|
|
376
|
+
if (!cachedHeadOid || !currentHeadOid || cachedHeadOid !== currentHeadOid) {
|
|
377
|
+
return null;
|
|
378
|
+
}
|
|
379
|
+
return hasRequiredChecksSummary(previousCache.summary) ? previousCache.summary : null;
|
|
380
|
+
}
|
|
381
|
+
export function buildStatusSnapshot(response, requiredChecks = null) {
|
|
303
382
|
const pr = response?.data?.repository?.pullRequest;
|
|
304
383
|
if (!pr) {
|
|
305
384
|
throw new Error('GraphQL response missing pullRequest payload.');
|
|
@@ -315,6 +394,9 @@ function buildStatusSnapshot(response) {
|
|
|
315
394
|
const contexts = pr.commits?.nodes?.[0]?.commit?.statusCheckRollup?.contexts?.nodes;
|
|
316
395
|
const checkNodes = Array.isArray(contexts) ? contexts : [];
|
|
317
396
|
const checks = summarizeChecks(checkNodes);
|
|
397
|
+
const requiredCheckSummary = requiredChecks && typeof requiredChecks === 'object' && requiredChecks.total > 0 ? requiredChecks : null;
|
|
398
|
+
const gateChecks = requiredCheckSummary ?? checks;
|
|
399
|
+
const gateChecksSource = requiredCheckSummary ? 'required' : 'rollup';
|
|
318
400
|
const reviewDecision = normalizeEnum(pr.reviewDecision);
|
|
319
401
|
const mergeStateStatus = normalizeEnum(pr.mergeStateStatus);
|
|
320
402
|
const state = normalizeEnum(pr.state);
|
|
@@ -329,8 +411,10 @@ function buildStatusSnapshot(response) {
|
|
|
329
411
|
if (hasDoNotMergeLabel) {
|
|
330
412
|
gateReasons.push('label:do-not-merge');
|
|
331
413
|
}
|
|
332
|
-
if (
|
|
333
|
-
gateReasons.push(
|
|
414
|
+
if (gateChecks.pending.length > 0) {
|
|
415
|
+
gateReasons.push(gateChecksSource === 'required'
|
|
416
|
+
? `required_checks_pending=${gateChecks.pending.length}`
|
|
417
|
+
: `checks_pending=${gateChecks.pending.length}`);
|
|
334
418
|
}
|
|
335
419
|
if (!MERGEABLE_STATES.has(mergeStateStatus)) {
|
|
336
420
|
gateReasons.push(`merge_state=${mergeStateStatus || 'UNKNOWN'}`);
|
|
@@ -354,31 +438,70 @@ function buildStatusSnapshot(response) {
|
|
|
354
438
|
hasDoNotMergeLabel,
|
|
355
439
|
unresolvedThreadCount,
|
|
356
440
|
checks,
|
|
441
|
+
requiredChecks: requiredCheckSummary,
|
|
442
|
+
gateChecksSource,
|
|
357
443
|
gateReasons,
|
|
358
444
|
readyToMerge: gateReasons.length === 0,
|
|
359
445
|
headOid: pr.commits?.nodes?.[0]?.commit?.oid || null
|
|
360
446
|
};
|
|
361
447
|
}
|
|
362
448
|
function formatStatusLine(snapshot, quietRemainingMs) {
|
|
449
|
+
const requiredChecks = snapshot.requiredChecks;
|
|
363
450
|
const failedNames = snapshot.checks.failed.map((item) => `${item.name}:${item.state}`).join(', ') || '-';
|
|
364
451
|
const pendingNames = snapshot.checks.pending.join(', ') || '-';
|
|
452
|
+
const requiredFailedNames = requiredChecks
|
|
453
|
+
? requiredChecks.failed.map((item) => `${item.name}:${item.state}`).join(', ') || '-'
|
|
454
|
+
: '-';
|
|
455
|
+
const requiredPendingNames = requiredChecks ? requiredChecks.pending.join(', ') || '-' : '-';
|
|
365
456
|
const reasons = snapshot.gateReasons.join(', ') || 'none';
|
|
366
457
|
return [
|
|
367
458
|
`PR #${snapshot.number}`,
|
|
368
459
|
`state=${snapshot.state}`,
|
|
369
460
|
`merge_state=${snapshot.mergeStateStatus}`,
|
|
370
461
|
`review=${snapshot.reviewDecision}`,
|
|
462
|
+
`gate_checks=${snapshot.gateChecksSource}`,
|
|
371
463
|
`checks_ok=${snapshot.checks.successCount}/${snapshot.checks.total}`,
|
|
372
464
|
`checks_pending=${snapshot.checks.pending.length}`,
|
|
373
465
|
`checks_failed=${snapshot.checks.failed.length}`,
|
|
466
|
+
`required_checks_ok=${requiredChecks ? `${requiredChecks.successCount}/${requiredChecks.total}` : 'n/a'}`,
|
|
467
|
+
`required_checks_pending=${requiredChecks ? requiredChecks.pending.length : 'n/a'}`,
|
|
468
|
+
`required_checks_failed=${requiredChecks ? requiredChecks.failed.length : 'n/a'}`,
|
|
374
469
|
`unresolved_threads=${snapshot.unresolvedThreadCount}`,
|
|
375
470
|
`quiet_remaining=${formatDuration(quietRemainingMs)}`,
|
|
376
471
|
`blocked_by=${reasons}`,
|
|
377
472
|
`pending=[${pendingNames}]`,
|
|
378
|
-
`failed=[${failedNames}]
|
|
473
|
+
`failed=[${failedNames}]`,
|
|
474
|
+
`required_pending=[${requiredPendingNames}]`,
|
|
475
|
+
`required_failed=[${requiredFailedNames}]`
|
|
379
476
|
].join(' | ');
|
|
380
477
|
}
|
|
381
|
-
async function
|
|
478
|
+
async function fetchRequiredChecks(owner, repo, prNumber) {
|
|
479
|
+
try {
|
|
480
|
+
const result = await runGhJson([
|
|
481
|
+
'pr',
|
|
482
|
+
'checks',
|
|
483
|
+
String(prNumber),
|
|
484
|
+
'--required',
|
|
485
|
+
'--json',
|
|
486
|
+
'name,state,link,bucket',
|
|
487
|
+
'--repo',
|
|
488
|
+
`${owner}/${repo}`
|
|
489
|
+
]);
|
|
490
|
+
const entries = Array.isArray(result) ? result : [];
|
|
491
|
+
const summary = summarizeRequiredChecks(entries);
|
|
492
|
+
return {
|
|
493
|
+
summary: summary.total > 0 ? summary : null,
|
|
494
|
+
fetchError: false
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
catch {
|
|
498
|
+
return {
|
|
499
|
+
summary: null,
|
|
500
|
+
fetchError: true
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
async function fetchSnapshot(owner, repo, prNumber, previousRequiredChecksCache = null) {
|
|
382
505
|
const response = await runGhJson([
|
|
383
506
|
'api',
|
|
384
507
|
'graphql',
|
|
@@ -391,7 +514,19 @@ async function fetchSnapshot(owner, repo, prNumber) {
|
|
|
391
514
|
'-F',
|
|
392
515
|
`number=${prNumber}`
|
|
393
516
|
]);
|
|
394
|
-
|
|
517
|
+
const currentHeadOid = response?.data?.repository?.pullRequest?.commits?.nodes?.[0]?.commit?.oid || null;
|
|
518
|
+
const previousRequiredChecks = resolveCachedRequiredChecksSummary(previousRequiredChecksCache, currentHeadOid);
|
|
519
|
+
const requiredChecksResult = await fetchRequiredChecks(owner, repo, prNumber);
|
|
520
|
+
const requiredChecks = resolveRequiredChecksSummary(requiredChecksResult.summary, previousRequiredChecks, requiredChecksResult.fetchError);
|
|
521
|
+
return {
|
|
522
|
+
snapshot: buildStatusSnapshot(response, requiredChecks),
|
|
523
|
+
requiredChecksForNextPoll: requiredChecks
|
|
524
|
+
? {
|
|
525
|
+
headOid: currentHeadOid,
|
|
526
|
+
summary: requiredChecks
|
|
527
|
+
}
|
|
528
|
+
: null
|
|
529
|
+
};
|
|
395
530
|
}
|
|
396
531
|
async function attemptMerge({ prNumber, mergeMethod, deleteBranch, headOid }) {
|
|
397
532
|
// gh pr merge has no --yes flag; rely on non-interactive stdio + explicit merge method.
|
|
@@ -472,10 +607,13 @@ async function runPrWatchMergeOrThrow(argv, options) {
|
|
|
472
607
|
let quietWindowAnchorUpdatedAt = null;
|
|
473
608
|
let quietWindowAnchorHeadOid = null;
|
|
474
609
|
let lastMergeAttemptHeadOid = null;
|
|
610
|
+
let requiredChecksForNextPollCache = null;
|
|
475
611
|
while (Date.now() <= deadline) {
|
|
476
612
|
let snapshot;
|
|
477
613
|
try {
|
|
478
|
-
|
|
614
|
+
const fetched = await fetchSnapshot(owner, repo, prNumber, requiredChecksForNextPollCache);
|
|
615
|
+
snapshot = fetched.snapshot;
|
|
616
|
+
requiredChecksForNextPollCache = fetched.requiredChecksForNextPoll;
|
|
479
617
|
}
|
|
480
618
|
catch (error) {
|
|
481
619
|
log(`Polling error: ${error instanceof Error ? error.message : String(error)} (retrying).`);
|
package/docs/README.md
CHANGED
|
@@ -155,6 +155,7 @@ Notes:
|
|
|
155
155
|
- `/prompts:review-handoff` takes `TASK=<task-id> MANIFEST=<path> NOTES=<goal + summary + risks + optional questions>`, re-exports `MCP_RUNNER_TASK_ID`, and (repo-only) runs `node scripts/delegation-guard.mjs`, `node scripts/spec-guard.mjs --dry-run`, `npm run lint`, `npm run test`, optional `npm run eval:test`, plus `npm run review` (wraps `codex review` against the current diff and includes the latest run manifest path as evidence). It also reminds you to log approvals in `$MANIFEST` and mirror the evidence to the same docs/metrics/state targets.
|
|
156
156
|
- In CI / `--no-interactive` pipelines (or when stdin is not a TTY, or `CODEX_REVIEW_NON_INTERACTIVE=1` / `CODEX_NON_INTERACTIVE=1` / `CODEX_NO_INTERACTIVE=1`), `npm run review` prints the review handoff prompt (including evidence paths) and exits successfully instead of invoking `codex review`. Set `FORCE_CODEX_REVIEW=1` to run `codex review` in those environments.
|
|
157
157
|
- When forcing non-interactive review execution, `npm run review` enforces a timeout (`CODEX_REVIEW_TIMEOUT_SECONDS`, default `900`). Set `CODEX_REVIEW_TIMEOUT_SECONDS=0` to disable the timeout.
|
|
158
|
+
- Forced non-interactive review execution also enforces a no-output stall timeout (`CODEX_REVIEW_STALL_TIMEOUT_SECONDS`, default `600`). Set `CODEX_REVIEW_STALL_TIMEOUT_SECONDS=0` to disable the stall guard.
|
|
158
159
|
- Always trigger diagnostics and review workflows through these prompts whenever you run the orchestrator so contributors consistently execute the required command sequences and capture auditable manifests.
|
|
159
160
|
|
|
160
161
|
### Identifier Guardrails
|
package/package.json
CHANGED
|
@@ -64,6 +64,7 @@ codex review "Focus on correctness, regressions, edge cases; list missing tests.
|
|
|
64
64
|
- If you need manifest evidence, use the review wrapper:
|
|
65
65
|
`TASK=<task-id> NOTES="Goal: ... | Summary: ... | Risks: ... | Questions (optional): ..." MANIFEST=<path> npm run review -- --manifest <path>`
|
|
66
66
|
- In non-interactive environments, add `FORCE_CODEX_REVIEW=1` as needed.
|
|
67
|
+
- In non-interactive environments, prefer the wrapper over raw `codex review`; it enforces `CODEX_REVIEW_TIMEOUT_SECONDS` and `CODEX_REVIEW_STALL_TIMEOUT_SECONDS` guardrails.
|
|
67
68
|
|
|
68
69
|
## Expected outputs
|
|
69
70
|
- A prioritized list of findings.
|