@kbediako/codex-orchestrator 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/README.md CHANGED
@@ -176,6 +176,7 @@ codex-orchestrator doctor --usage
176
176
  ## Downstream usage cheatsheet (agent-first)
177
177
 
178
178
  - Bootstrap + wire everything: `codex-orchestrator setup --yes`
179
+ - Low-friction docs->implementation guardrails: `codex-orchestrator flow --task <task-id>`
179
180
  - Validate + measure adoption locally: `codex-orchestrator doctor --usage --format json`
180
181
  - Delegation: `codex-orchestrator doctor --apply --yes`, then enable for a Codex run with: `codex -c 'mcp_servers.delegation.enabled=true' ...`
181
182
  - Collab (symbolic RLM subagents): `codex-orchestrator rlm --collab auto "<goal>"` (requires collab feature enabled in Codex)
@@ -189,6 +190,7 @@ codex-orchestrator devtools setup
189
190
  ## Common commands
190
191
 
191
192
  - `codex-orchestrator start <pipeline>` — run a pipeline.
193
+ - `codex-orchestrator flow --task <task-id>` — run `docs-review` then `implementation-gate` in sequence.
192
194
  - `codex-orchestrator plan <pipeline>` — preview pipeline stages.
193
195
  - `codex-orchestrator exec <cmd>` — run a one-off command with the exec runtime.
194
196
  - `codex-orchestrator init codex` — install starter templates (`mcp-client.json`, `AGENTS.md`) into a repo.
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { existsSync } from 'node:fs';
3
- import { readFile } from 'node:fs/promises';
3
+ import { opendir, readFile } from 'node:fs/promises';
4
4
  import { basename, join } from 'node:path';
5
5
  import process from 'node:process';
6
6
  import { CodexOrchestrator } from '../orchestrator/src/cli/orchestrator.js';
@@ -44,6 +44,9 @@ async function main() {
44
44
  case 'frontend-test':
45
45
  await handleFrontendTest(orchestrator, args);
46
46
  break;
47
+ case 'flow':
48
+ await handleFlow(orchestrator, args);
49
+ break;
47
50
  case 'plan':
48
51
  await handlePlan(orchestrator, args);
49
52
  break;
@@ -145,6 +148,132 @@ function resolveTargetStageId(flags) {
145
148
  }
146
149
  return undefined;
147
150
  }
151
+ const FLOW_TARGET_PIPELINE_SCOPES = new Set(['docs-review', 'implementation-gate']);
152
+ function isFlowTargetPipelineScope(scope) {
153
+ return FLOW_TARGET_PIPELINE_SCOPES.has(scope);
154
+ }
155
+ function normalizeFlowTargetToken(candidate) {
156
+ const trimmed = candidate.trim();
157
+ if (!trimmed) {
158
+ return null;
159
+ }
160
+ const tokens = trimmed.split(':');
161
+ if (tokens.length > 1 && !(tokens[0] ?? '').trim()) {
162
+ return null;
163
+ }
164
+ let scoped = false;
165
+ let scopeToken = null;
166
+ let suffixToken = trimmed;
167
+ if (tokens.length > 1) {
168
+ const candidateScope = (tokens[0] ?? '').trim().toLowerCase();
169
+ if (isFlowTargetPipelineScope(candidateScope)) {
170
+ scoped = true;
171
+ scopeToken = candidateScope;
172
+ suffixToken = (tokens[tokens.length - 1] ?? '').trim();
173
+ }
174
+ }
175
+ if (!suffixToken) {
176
+ return null;
177
+ }
178
+ return {
179
+ literal: trimmed,
180
+ literalLower: trimmed.toLowerCase(),
181
+ stageTokenLower: suffixToken.toLowerCase(),
182
+ scopeLower: scopeToken,
183
+ scoped
184
+ };
185
+ }
186
+ function flowPlanItemPipelineId(item) {
187
+ const metadataPipelineId = item.metadata && typeof item.metadata['pipelineId'] === 'string'
188
+ ? item.metadata['pipelineId'].trim().toLowerCase()
189
+ : '';
190
+ if (metadataPipelineId) {
191
+ return metadataPipelineId;
192
+ }
193
+ const delimiterIndex = item.id.indexOf(':');
194
+ if (delimiterIndex <= 0) {
195
+ return null;
196
+ }
197
+ return item.id.slice(0, delimiterIndex).trim().toLowerCase() || null;
198
+ }
199
+ function flowPlanItemMatchesTarget(item, candidate) {
200
+ const normalized = normalizeFlowTargetToken(candidate);
201
+ if (!normalized) {
202
+ return false;
203
+ }
204
+ if (item.id.toLowerCase() === normalized.literalLower) {
205
+ return true;
206
+ }
207
+ if (normalized.scoped && normalized.scopeLower) {
208
+ const itemPipelineId = flowPlanItemPipelineId(item);
209
+ if (itemPipelineId && itemPipelineId !== normalized.scopeLower) {
210
+ return false;
211
+ }
212
+ }
213
+ const metadataStageId = item.metadata && typeof item.metadata['stageId'] === 'string'
214
+ ? item.metadata['stageId'].toLowerCase()
215
+ : null;
216
+ const aliases = Array.isArray(item.metadata?.['aliases'])
217
+ ? item.metadata?.['aliases']
218
+ : [];
219
+ const aliasTokens = aliases.filter((alias) => typeof alias === 'string')
220
+ .map((alias) => alias.toLowerCase());
221
+ if (normalized.scoped) {
222
+ if (metadataStageId
223
+ && (metadataStageId === normalized.literalLower || metadataStageId === normalized.stageTokenLower)) {
224
+ return true;
225
+ }
226
+ return aliasTokens.some((alias) => alias === normalized.literalLower || alias === normalized.stageTokenLower);
227
+ }
228
+ if (item.id.toLowerCase().endsWith(`:${normalized.stageTokenLower}`)) {
229
+ return true;
230
+ }
231
+ if (metadataStageId
232
+ && (metadataStageId === normalized.stageTokenLower
233
+ || metadataStageId.endsWith(`:${normalized.stageTokenLower}`))) {
234
+ return true;
235
+ }
236
+ return aliasTokens.some((alias) => alias === normalized.stageTokenLower || alias.endsWith(`:${normalized.stageTokenLower}`));
237
+ }
238
+ function planIncludesStageId(plan, stageId) {
239
+ if (!stageId.trim()) {
240
+ return false;
241
+ }
242
+ return plan.plan.items.some((item) => flowPlanItemMatchesTarget(item, stageId));
243
+ }
244
+ function resolveFlowTargetScope(stageId) {
245
+ const delimiterIndex = stageId.indexOf(':');
246
+ if (delimiterIndex <= 0) {
247
+ return null;
248
+ }
249
+ const scope = stageId.slice(0, delimiterIndex).trim().toLowerCase();
250
+ if (!isFlowTargetPipelineScope(scope)) {
251
+ return null;
252
+ }
253
+ return scope;
254
+ }
255
+ async function resolveFlowTargetStageSelection(orchestrator, taskId, requestedTargetStageId) {
256
+ if (!requestedTargetStageId) {
257
+ return {};
258
+ }
259
+ const [docsPlan, implementationPlan] = (await Promise.all([
260
+ orchestrator.plan({ pipelineId: 'docs-review', taskId }),
261
+ orchestrator.plan({ pipelineId: 'implementation-gate', taskId })
262
+ ]));
263
+ const requestedScope = resolveFlowTargetScope(requestedTargetStageId);
264
+ const docsScopeMatch = !requestedScope || requestedScope === 'docs-review';
265
+ const implementationScopeMatch = !requestedScope || requestedScope === 'implementation-gate';
266
+ const docsReviewTargetStageId = docsScopeMatch && planIncludesStageId(docsPlan, requestedTargetStageId)
267
+ ? requestedTargetStageId
268
+ : undefined;
269
+ const implementationGateTargetStageId = implementationScopeMatch && planIncludesStageId(implementationPlan, requestedTargetStageId)
270
+ ? requestedTargetStageId
271
+ : undefined;
272
+ if (!docsReviewTargetStageId && !implementationGateTargetStageId) {
273
+ throw new Error(`Target stage "${requestedTargetStageId}" is not defined in docs-review or implementation-gate.`);
274
+ }
275
+ return { docsReviewTargetStageId, implementationGateTargetStageId };
276
+ }
148
277
  function readStringFlag(flags, key) {
149
278
  const value = flags[key];
150
279
  if (typeof value !== 'string') {
@@ -313,6 +442,84 @@ async function handleFrontendTest(orchestrator, rawArgs) {
313
442
  }
314
443
  }
315
444
  }
445
+ async function handleFlow(orchestrator, rawArgs) {
446
+ const { positionals, flags } = parseArgs(rawArgs);
447
+ if (isHelpRequest(positionals, flags)) {
448
+ printFlowHelp();
449
+ return;
450
+ }
451
+ if (positionals.length > 0) {
452
+ throw new Error(`flow does not accept positional arguments: ${positionals.join(' ')}`);
453
+ }
454
+ const format = flags['format'] === 'json' ? 'json' : 'text';
455
+ const executionMode = resolveExecutionModeFlag(flags);
456
+ const taskId = typeof flags['task'] === 'string' ? flags['task'] : undefined;
457
+ const parentRunId = typeof flags['parent-run'] === 'string' ? flags['parent-run'] : undefined;
458
+ const approvalPolicy = typeof flags['approval-policy'] === 'string' ? flags['approval-policy'] : undefined;
459
+ const targetStageId = resolveTargetStageId(flags);
460
+ const { docsReviewTargetStageId, implementationGateTargetStageId } = await resolveFlowTargetStageSelection(orchestrator, taskId, targetStageId);
461
+ await withRunUi(flags, format, async (runEvents) => {
462
+ const docsReviewResult = await orchestrator.start({
463
+ pipelineId: 'docs-review',
464
+ taskId,
465
+ parentRunId,
466
+ approvalPolicy,
467
+ targetStageId: docsReviewTargetStageId,
468
+ executionMode,
469
+ runEvents
470
+ });
471
+ const docsPayload = toRunOutputPayload(docsReviewResult);
472
+ if (format === 'text') {
473
+ emitRunOutput(docsReviewResult, format, 'Docs-review run');
474
+ }
475
+ if (docsReviewResult.manifest.status !== 'succeeded') {
476
+ process.exitCode = 1;
477
+ if (format === 'json') {
478
+ const payload = {
479
+ status: docsReviewResult.manifest.status,
480
+ failed_stage: 'docs-review',
481
+ docs_review: docsPayload,
482
+ implementation_gate: null
483
+ };
484
+ console.log(JSON.stringify(payload, null, 2));
485
+ }
486
+ else {
487
+ console.log('Flow halted: docs-review failed.');
488
+ }
489
+ return;
490
+ }
491
+ const implementationGateResult = await orchestrator.start({
492
+ pipelineId: 'implementation-gate',
493
+ taskId,
494
+ parentRunId: docsReviewResult.manifest.run_id,
495
+ approvalPolicy,
496
+ targetStageId: implementationGateTargetStageId,
497
+ executionMode,
498
+ runEvents
499
+ });
500
+ const implementationPayload = toRunOutputPayload(implementationGateResult);
501
+ if (format === 'json') {
502
+ const payload = {
503
+ status: implementationGateResult.manifest.status,
504
+ failed_stage: implementationGateResult.manifest.status === 'succeeded' ? null : 'implementation-gate',
505
+ docs_review: docsPayload,
506
+ implementation_gate: implementationPayload
507
+ };
508
+ console.log(JSON.stringify(payload, null, 2));
509
+ if (implementationGateResult.manifest.status !== 'succeeded') {
510
+ process.exitCode = 1;
511
+ }
512
+ return;
513
+ }
514
+ emitRunOutput(implementationGateResult, format, 'Implementation-gate run');
515
+ if (implementationGateResult.manifest.status !== 'succeeded') {
516
+ process.exitCode = 1;
517
+ console.log('Flow halted: implementation-gate failed.');
518
+ return;
519
+ }
520
+ console.log('Flow complete: docs-review -> implementation-gate.');
521
+ });
522
+ }
316
523
  async function handlePlan(orchestrator, rawArgs) {
317
524
  const { positionals, flags } = parseArgs(rawArgs);
318
525
  const pipelineId = positionals[0];
@@ -464,14 +671,7 @@ async function withRunUi(flags, format, action) {
464
671
  }
465
672
  }
466
673
  function emitRunOutput(result, format, label) {
467
- const payload = {
468
- run_id: result.manifest.run_id,
469
- status: result.manifest.status,
470
- artifact_root: result.manifest.artifact_root,
471
- manifest: `${result.manifest.artifact_root}/manifest.json`,
472
- log_path: result.manifest.log_path,
473
- summary: result.manifest.summary ?? null
474
- };
674
+ const payload = toRunOutputPayload(result);
475
675
  if (format === 'json') {
476
676
  console.log(JSON.stringify(payload, null, 2));
477
677
  return;
@@ -487,6 +687,16 @@ function emitRunOutput(result, format, label) {
487
687
  }
488
688
  }
489
689
  }
690
+ function toRunOutputPayload(result) {
691
+ return {
692
+ run_id: result.manifest.run_id,
693
+ status: result.manifest.status,
694
+ artifact_root: result.manifest.artifact_root,
695
+ manifest: `${result.manifest.artifact_root}/manifest.json`,
696
+ log_path: result.manifest.log_path,
697
+ summary: result.manifest.summary ?? null
698
+ };
699
+ }
490
700
  async function handleExec(rawArgs) {
491
701
  const parsed = parseExecArgs(rawArgs);
492
702
  if (parsed.commandTokens.length === 0) {
@@ -519,6 +729,55 @@ async function handleExec(rawArgs) {
519
729
  else if (result.status !== 'succeeded') {
520
730
  process.exitCode = 1;
521
731
  }
732
+ if (outputMode === 'interactive') {
733
+ await maybeEmitExecAdoptionHint(env.taskId);
734
+ }
735
+ }
736
+ async function shouldScanExecAdoptionHint(taskFilter) {
737
+ if (!taskFilter) {
738
+ return false;
739
+ }
740
+ const env = resolveEnvironmentPaths();
741
+ const taskCliRunsRoot = join(env.runsRoot, taskFilter, 'cli');
742
+ let handle = null;
743
+ try {
744
+ handle = await opendir(taskCliRunsRoot);
745
+ let runCount = 0;
746
+ for await (const entry of handle) {
747
+ if (!entry.isDirectory()) {
748
+ continue;
749
+ }
750
+ runCount += 1;
751
+ if (runCount > 150) {
752
+ return false;
753
+ }
754
+ }
755
+ return true;
756
+ }
757
+ catch {
758
+ return false;
759
+ }
760
+ finally {
761
+ if (handle) {
762
+ await handle.close().catch(() => undefined);
763
+ }
764
+ }
765
+ }
766
+ async function maybeEmitExecAdoptionHint(taskFilter) {
767
+ try {
768
+ if (!(await shouldScanExecAdoptionHint(taskFilter))) {
769
+ return;
770
+ }
771
+ const usage = await runDoctorUsage({ windowDays: 7, taskFilter });
772
+ const recommendation = usage.adoption.recommendations[0];
773
+ if (!recommendation) {
774
+ return;
775
+ }
776
+ console.log(`Adoption hint: ${recommendation}`);
777
+ }
778
+ catch {
779
+ // Exec command behavior should not fail when usage telemetry cannot be read.
780
+ }
522
781
  }
523
782
  async function handleSelfCheck(rawArgs) {
524
783
  const { flags } = parseArgs(rawArgs);
@@ -599,6 +858,7 @@ Options:
599
858
  throw new Error('No bundled skills detected; cannot run setup.');
600
859
  }
601
860
  const forceSkills = bundledSkills.filter((skill) => skill !== 'chrome-devtools');
861
+ const guidance = buildSetupGuidance();
602
862
  if (!apply) {
603
863
  const forceOnly = forceSkills.join(',');
604
864
  const forceCommand = forceOnly ? `codex-orchestrator skills install --force --only ${forceOnly}` : null;
@@ -615,7 +875,8 @@ Options:
615
875
  note: 'Installs bundled skills into $CODEX_HOME/skills (setup avoids overwriting chrome-devtools when already present).'
616
876
  },
617
877
  delegation,
618
- devtools
878
+ devtools,
879
+ guidance
619
880
  }
620
881
  };
621
882
  if (format === 'json') {
@@ -629,6 +890,9 @@ Options:
629
890
  }
630
891
  console.log(`- Delegation: codex-orchestrator delegation setup --yes${delegationRepoArg}`);
631
892
  console.log('- DevTools: codex-orchestrator devtools setup --yes');
893
+ for (const line of formatSetupGuidanceSummary(guidance)) {
894
+ console.log(line);
895
+ }
632
896
  console.log('Run with --yes to apply this setup.');
633
897
  return;
634
898
  }
@@ -659,8 +923,41 @@ Options:
659
923
  for (const line of formatDevtoolsSetupSummary(devtools)) {
660
924
  console.log(line);
661
925
  }
926
+ for (const line of formatSetupGuidanceSummary(guidance)) {
927
+ console.log(line);
928
+ }
662
929
  console.log('Next: codex-orchestrator doctor --usage');
663
930
  }
931
+ function buildSetupGuidance() {
932
+ return {
933
+ note: 'Agent-first default: run docs-review before implementation and implementation-gate before handoff.',
934
+ references: [
935
+ 'https://github.com/Kbediako/CO#downstream-usage-cheatsheet-agent-first',
936
+ 'https://github.com/Kbediako/CO/blob/main/docs/AGENTS.md',
937
+ 'https://github.com/Kbediako/CO/blob/main/docs/guides/collab-vs-mcp.md'
938
+ ],
939
+ recommended_commands: [
940
+ 'codex-orchestrator flow --task <task-id>',
941
+ 'codex-orchestrator doctor --usage'
942
+ ]
943
+ };
944
+ }
945
+ function formatSetupGuidanceSummary(guidance) {
946
+ const lines = ['Setup guidance:', `- ${guidance.note}`];
947
+ if (guidance.recommended_commands.length > 0) {
948
+ lines.push('- Recommended commands:');
949
+ for (const command of guidance.recommended_commands) {
950
+ lines.push(` - ${command}`);
951
+ }
952
+ }
953
+ if (guidance.references.length > 0) {
954
+ lines.push('- References:');
955
+ for (const reference of guidance.references) {
956
+ lines.push(` - ${reference}`);
957
+ }
958
+ }
959
+ return lines;
960
+ }
664
961
  async function handleDoctor(rawArgs) {
665
962
  const { flags } = parseArgs(rawArgs);
666
963
  const format = flags['format'] === 'json' ? 'json' : 'text';
@@ -1159,6 +1456,17 @@ Commands:
1159
1456
  --interactive | --ui Enable read-only HUD when running in a TTY.
1160
1457
  --no-interactive Force disable HUD (default is off unless requested).
1161
1458
 
1459
+ flow Run docs-review then implementation-gate sequentially.
1460
+ --task <id> Override task identifier (defaults to MCP_RUNNER_TASK_ID).
1461
+ --parent-run <id> Link docs-review run to parent run id.
1462
+ --approval-policy <p> Record approval policy metadata.
1463
+ --format json Emit machine-readable output summary for both runs.
1464
+ --execution-mode <mcp|cloud> Force execution mode for both runs.
1465
+ --cloud Shortcut for --execution-mode cloud.
1466
+ --target <stage-id> Focus plan/build metadata on a specific stage (alias: --target-stage).
1467
+ --interactive | --ui Enable read-only HUD when running in a TTY.
1468
+ --no-interactive Force disable HUD (default is off unless requested).
1469
+
1162
1470
  plan [pipeline] Preview pipeline stages without executing.
1163
1471
  --task <id> Override task identifier.
1164
1472
  --format json Emit machine-readable output.
@@ -1324,3 +1632,20 @@ Options:
1324
1632
  --help Show this message.
1325
1633
  `);
1326
1634
  }
1635
+ function printFlowHelp() {
1636
+ console.log(`Usage: codex-orchestrator flow [options]
1637
+
1638
+ Runs docs-review first, then implementation-gate. Stops on the first failure.
1639
+
1640
+ Options:
1641
+ --task <id> Override task identifier.
1642
+ --parent-run <id> Link docs-review run to parent run id.
1643
+ --approval-policy <p> Record approval policy metadata.
1644
+ --format json Emit machine-readable output for both runs.
1645
+ --execution-mode <mcp|cloud> Force execution mode for both runs.
1646
+ --cloud Shortcut for --execution-mode cloud.
1647
+ --target <stage-id> Focus plan/build metadata (applies where the stage exists).
1648
+ --interactive | --ui Enable read-only HUD when running in a TTY.
1649
+ --no-interactive Force disable HUD.
1650
+ `);
1651
+ }
@@ -15,6 +15,12 @@ const REQUIRED_BUCKET_FAILED = new Set(['fail', 'cancel', 'skipping']);
15
15
  const MERGEABLE_STATES = new Set(['CLEAN', 'HAS_HOOKS', 'UNSTABLE']);
16
16
  const BLOCKED_REVIEW_DECISIONS = new Set(['CHANGES_REQUESTED', 'REVIEW_REQUIRED']);
17
17
  const DO_NOT_MERGE_LABEL = /do[\s_-]*not[\s_-]*merge/i;
18
+ const ACTIONABLE_BOT_LOGINS = new Set([
19
+ 'chatgpt-codex-connector',
20
+ 'chatgpt-codex-connector[bot]',
21
+ 'coderabbitai',
22
+ 'coderabbitai[bot]'
23
+ ]);
18
24
  const PR_QUERY = `
19
25
  query($owner:String!, $repo:String!, $number:Int!) {
20
26
  repository(owner:$owner, name:$repo) {
@@ -73,6 +79,29 @@ function normalizeEnum(value) {
73
79
  function normalizeBucket(value) {
74
80
  return typeof value === 'string' ? value.trim().toLowerCase() : '';
75
81
  }
82
+ function normalizeLogin(value) {
83
+ return typeof value === 'string' ? value.trim().toLowerCase() : '';
84
+ }
85
+ function isActionableBot(login) {
86
+ return ACTIONABLE_BOT_LOGINS.has(normalizeLogin(login));
87
+ }
88
+ export function isHumanReviewActor(user) {
89
+ if (!user || typeof user !== 'object') {
90
+ return false;
91
+ }
92
+ const login = normalizeLogin(user.login);
93
+ if (!login) {
94
+ return false;
95
+ }
96
+ if (isActionableBot(login)) {
97
+ return false;
98
+ }
99
+ const accountType = typeof user.type === 'string' ? user.type.trim().toUpperCase() : '';
100
+ if (accountType) {
101
+ return accountType === 'USER';
102
+ }
103
+ return !login.endsWith('[bot]');
104
+ }
76
105
  function formatDuration(ms) {
77
106
  if (ms <= 0) {
78
107
  return '0s';
@@ -218,6 +247,15 @@ async function runGhJson(args) {
218
247
  throw new Error(`Failed to parse JSON from gh ${args.join(' ')}: ${error instanceof Error ? error.message : String(error)}`);
219
248
  }
220
249
  }
250
+ async function runGhJsonSlurped(args) {
251
+ const result = await runGh([...args, '--paginate', '--slurp']);
252
+ try {
253
+ return JSON.parse(result.stdout);
254
+ }
255
+ catch (error) {
256
+ throw new Error(`Failed to parse paginated JSON from gh ${args.join(' ')}: ${error instanceof Error ? error.message : String(error)}`);
257
+ }
258
+ }
221
259
  async function ensureGhAuth() {
222
260
  const result = await runGh(['auth', 'status', '-h', 'github.com'], { allowFailure: true });
223
261
  if (result.exitCode !== 0) {
@@ -378,7 +416,7 @@ export function resolveCachedRequiredChecksSummary(previousCache, currentHeadOid
378
416
  }
379
417
  return hasRequiredChecksSummary(previousCache.summary) ? previousCache.summary : null;
380
418
  }
381
- export function buildStatusSnapshot(response, requiredChecks = null) {
419
+ export function buildStatusSnapshot(response, requiredChecks = null, inlineBotFeedback = null) {
382
420
  const pr = response?.data?.repository?.pullRequest;
383
421
  if (!pr) {
384
422
  throw new Error('GraphQL response missing pullRequest payload.');
@@ -391,10 +429,15 @@ export function buildStatusSnapshot(response, requiredChecks = null) {
391
429
  const hasDoNotMergeLabel = labels.some((label) => DO_NOT_MERGE_LABEL.test(label));
392
430
  const threads = Array.isArray(pr.reviewThreads?.nodes) ? pr.reviewThreads.nodes : [];
393
431
  const unresolvedThreadCount = threads.filter((thread) => thread && !thread.isResolved && !thread.isOutdated).length;
432
+ const hasUnresolvedThread = unresolvedThreadCount > 0;
394
433
  const contexts = pr.commits?.nodes?.[0]?.commit?.statusCheckRollup?.contexts?.nodes;
395
434
  const checkNodes = Array.isArray(contexts) ? contexts : [];
396
435
  const checks = summarizeChecks(checkNodes);
397
436
  const requiredCheckSummary = requiredChecks && typeof requiredChecks === 'object' && requiredChecks.total > 0 ? requiredChecks : null;
437
+ const unacknowledgedBotFeedbackCount = inlineBotFeedback && typeof inlineBotFeedback.unacknowledgedCount === 'number'
438
+ ? inlineBotFeedback.unacknowledgedCount
439
+ : 0;
440
+ const botFeedbackFetchError = inlineBotFeedback?.fetchError === true;
398
441
  const gateChecks = requiredCheckSummary ?? checks;
399
442
  const gateChecksSource = requiredCheckSummary ? 'required' : 'rollup';
400
443
  const reviewDecision = normalizeEnum(pr.reviewDecision);
@@ -422,9 +465,15 @@ export function buildStatusSnapshot(response, requiredChecks = null) {
422
465
  if (BLOCKED_REVIEW_DECISIONS.has(reviewDecision)) {
423
466
  gateReasons.push(`review=${reviewDecision}`);
424
467
  }
425
- if (unresolvedThreadCount > 0) {
468
+ if (hasUnresolvedThread) {
426
469
  gateReasons.push(`unresolved_threads=${unresolvedThreadCount}`);
427
470
  }
471
+ if (botFeedbackFetchError) {
472
+ gateReasons.push('bot_feedback=unknown');
473
+ }
474
+ else if (unacknowledgedBotFeedbackCount > 0) {
475
+ gateReasons.push(`unacknowledged_bot_feedback=${unacknowledgedBotFeedbackCount}`);
476
+ }
428
477
  return {
429
478
  number: Number(pr.number),
430
479
  url: typeof pr.url === 'string' ? pr.url : null,
@@ -437,6 +486,8 @@ export function buildStatusSnapshot(response, requiredChecks = null) {
437
486
  labels,
438
487
  hasDoNotMergeLabel,
439
488
  unresolvedThreadCount,
489
+ unacknowledgedBotFeedbackCount,
490
+ botFeedbackFetchError,
440
491
  checks,
441
492
  requiredChecks: requiredCheckSummary,
442
493
  gateChecksSource,
@@ -467,6 +518,8 @@ function formatStatusLine(snapshot, quietRemainingMs) {
467
518
  `required_checks_pending=${requiredChecks ? requiredChecks.pending.length : 'n/a'}`,
468
519
  `required_checks_failed=${requiredChecks ? requiredChecks.failed.length : 'n/a'}`,
469
520
  `unresolved_threads=${snapshot.unresolvedThreadCount}`,
521
+ `unack_bot_feedback=${snapshot.unacknowledgedBotFeedbackCount}`,
522
+ `bot_feedback_fetch_error=${snapshot.botFeedbackFetchError ? 'yes' : 'no'}`,
470
523
  `quiet_remaining=${formatDuration(quietRemainingMs)}`,
471
524
  `blocked_by=${reasons}`,
472
525
  `pending=[${pendingNames}]`,
@@ -501,6 +554,77 @@ async function fetchRequiredChecks(owner, repo, prNumber) {
501
554
  };
502
555
  }
503
556
  }
557
+ function flattenReviewCommentPages(pagesPayload) {
558
+ if (!Array.isArray(pagesPayload)) {
559
+ return [];
560
+ }
561
+ const comments = [];
562
+ for (const page of pagesPayload) {
563
+ if (Array.isArray(page)) {
564
+ comments.push(...page);
565
+ continue;
566
+ }
567
+ if (page && typeof page === 'object') {
568
+ comments.push(page);
569
+ }
570
+ }
571
+ return comments;
572
+ }
573
+ async function fetchInlineBotFeedback(owner, repo, prNumber, headOid) {
574
+ if (!headOid) {
575
+ return { fetchError: false, unacknowledgedCount: 0 };
576
+ }
577
+ try {
578
+ const pagedPayload = await runGhJsonSlurped([
579
+ 'api',
580
+ `repos/${owner}/${repo}/pulls/${prNumber}/comments`
581
+ ]);
582
+ const comments = flattenReviewCommentPages(pagedPayload);
583
+ const repliesByParentId = new Map();
584
+ for (const comment of comments) {
585
+ if (!comment || typeof comment !== 'object') {
586
+ continue;
587
+ }
588
+ const parentId = Number(comment.in_reply_to_id);
589
+ if (!Number.isInteger(parentId) || parentId <= 0) {
590
+ continue;
591
+ }
592
+ const bucket = repliesByParentId.get(parentId) ?? [];
593
+ bucket.push(comment);
594
+ repliesByParentId.set(parentId, bucket);
595
+ }
596
+ let unacknowledgedCount = 0;
597
+ for (const comment of comments) {
598
+ if (!comment || typeof comment !== 'object') {
599
+ continue;
600
+ }
601
+ const commentId = Number(comment.id);
602
+ if (!Number.isInteger(commentId) || commentId <= 0) {
603
+ continue;
604
+ }
605
+ if (comment.in_reply_to_id !== null && comment.in_reply_to_id !== undefined) {
606
+ continue;
607
+ }
608
+ if (!isActionableBot(comment.user?.login)) {
609
+ continue;
610
+ }
611
+ const commitId = typeof comment.commit_id === 'string' ? comment.commit_id : null;
612
+ const originalCommitId = typeof comment.original_commit_id === 'string' ? comment.original_commit_id : null;
613
+ if (commitId !== headOid && originalCommitId !== headOid) {
614
+ continue;
615
+ }
616
+ const replies = repliesByParentId.get(commentId) ?? [];
617
+ const hasHumanReply = replies.some((reply) => isHumanReviewActor(reply?.user));
618
+ if (!hasHumanReply) {
619
+ unacknowledgedCount += 1;
620
+ }
621
+ }
622
+ return { fetchError: false, unacknowledgedCount };
623
+ }
624
+ catch {
625
+ return { fetchError: true, unacknowledgedCount: 0 };
626
+ }
627
+ }
504
628
  async function fetchSnapshot(owner, repo, prNumber, previousRequiredChecksCache = null) {
505
629
  const response = await runGhJson([
506
630
  'api',
@@ -518,8 +642,9 @@ async function fetchSnapshot(owner, repo, prNumber, previousRequiredChecksCache
518
642
  const previousRequiredChecks = resolveCachedRequiredChecksSummary(previousRequiredChecksCache, currentHeadOid);
519
643
  const requiredChecksResult = await fetchRequiredChecks(owner, repo, prNumber);
520
644
  const requiredChecks = resolveRequiredChecksSummary(requiredChecksResult.summary, previousRequiredChecks, requiredChecksResult.fetchError);
645
+ const inlineBotFeedback = await fetchInlineBotFeedback(owner, repo, prNumber, currentHeadOid);
521
646
  return {
522
- snapshot: buildStatusSnapshot(response, requiredChecks),
647
+ snapshot: buildStatusSnapshot(response, requiredChecks, inlineBotFeedback),
523
648
  requiredChecksForNextPoll: requiredChecks
524
649
  ? {
525
650
  headOid: currentHeadOid,
package/docs/README.md CHANGED
@@ -101,7 +101,8 @@ Use `npx @kbediako/codex-orchestrator resume --run <run-id>` to continue interru
101
101
  ## Companion Package Commands
102
102
  - `codex-orchestrator mcp serve [--repo <path>] [--dry-run] [-- <extra args>]`: launch the MCP stdio server (delegates to `codex mcp-server`; stdout guard keeps protocol-only output, logs to stderr).
103
103
  - `codex-orchestrator init codex [--cwd <path>] [--force]`: copy starter templates into a repo (includes `mcp-client.json` and `AGENTS.md`; no overwrite unless `--force`).
104
- - `codex-orchestrator setup [--yes]`: one-shot bootstrap for downstream users (installs bundled skills and configures delegation + DevTools wiring).
104
+ - `codex-orchestrator setup [--yes]`: one-shot bootstrap for downstream users (installs bundled skills, configures delegation + DevTools wiring, and prints policy/usage guidance).
105
+ - `codex-orchestrator flow [--task <task-id>]`: runs `docs-review` then `implementation-gate` in sequence; stops on the first failure.
105
106
  - `codex-orchestrator doctor [--format json] [--usage] [--apply]`: check optional tooling dependencies plus collab/cloud/delegation readiness and print enablement commands. `--usage` appends a local usage snapshot (scans `.runs/`). `--apply` plans/applies quick fixes (use with `--yes`).
106
107
  - `codex-orchestrator devtools setup [--yes]`: print DevTools MCP setup instructions (`--yes` applies `codex mcp add ...`).
107
108
  - `codex-orchestrator delegation setup [--yes]`: configure delegation MCP wiring (`--yes` applies `codex mcp add ...`).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kbediako/codex-orchestrator",
3
- "version": "0.1.31",
3
+ "version": "0.1.32",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",