mandrel 2.36.0 → 2.38.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.
Files changed (225) hide show
  1. package/.agents/README.md +16 -16
  2. package/.agents/agents/plan-critic.md +1 -1
  3. package/.agents/docs/SDLC.md +40 -40
  4. package/.agents/docs/configuration.md +41 -16
  5. package/.agents/docs/execution-reference.md +3 -3
  6. package/.agents/docs/quality-gates.md +2 -2
  7. package/.agents/docs/workflows.md +6 -6
  8. package/.agents/instructions.md +5 -5
  9. package/.agents/rules/changelog-style.md +3 -3
  10. package/.agents/rules/ci-remediation.md +1 -1
  11. package/.agents/rules/git-conventions-reference.md +4 -4
  12. package/.agents/rules/testing-standards.md +56 -0
  13. package/.agents/schemas/agentrc.schema.json +9 -9
  14. package/.agents/schemas/story-deliver-terminal.schema.json +2 -2
  15. package/.agents/schemas/validation-evidence.schema.json +1 -1
  16. package/.agents/scripts/acceptance-eval.js +1 -1
  17. package/.agents/scripts/apply-quality-bootstrap.js +1 -1
  18. package/.agents/scripts/audit-to-stories.js +2 -2
  19. package/.agents/scripts/boot-sweep.js +7 -1
  20. package/.agents/scripts/check-context-budget.js +195 -103
  21. package/.agents/scripts/check-cyclomatic.js +112 -42
  22. package/.agents/scripts/check-doc-links.js +32 -4
  23. package/.agents/scripts/check-generated-validator.js +202 -0
  24. package/.agents/scripts/check-lifecycle-lint.js +2 -68
  25. package/.agents/scripts/check-schema-references.js +2 -28
  26. package/.agents/scripts/check-test-temp-hygiene.js +1 -1
  27. package/.agents/scripts/check-workflow-cli-lint.js +1 -1
  28. package/.agents/scripts/deliver-light.js +10 -10
  29. package/.agents/scripts/deliver-recover.js +1 -1
  30. package/.agents/scripts/drain-pending-cleanup.js +19 -2
  31. package/.agents/scripts/evidence-gate.js +1 -1
  32. package/.agents/scripts/generate-workflows-doc.js +1 -1
  33. package/.agents/scripts/lib/audit-suite/selector.js +1 -1
  34. package/.agents/scripts/lib/audit-to-stories/audit-label-taxonomy.js +1 -1
  35. package/.agents/scripts/lib/audit-to-stories/build-story-body.js +1 -1
  36. package/.agents/scripts/lib/audit-to-stories/seed-from-findings.js +5 -5
  37. package/.agents/scripts/lib/audit-to-stories/wire-dependencies.js +1 -1
  38. package/.agents/scripts/lib/baselines/components.js +32 -2
  39. package/.agents/scripts/lib/baselines/env-overrides.js +1 -1
  40. package/.agents/scripts/lib/bdd-runner-detect.js +1 -1
  41. package/.agents/scripts/lib/bdd-scenario-budget.js +1 -1
  42. package/.agents/scripts/lib/bdd-scenario-scanner.js +3 -3
  43. package/.agents/scripts/lib/bdd-step-index.js +1 -1
  44. package/.agents/scripts/lib/bootstrap/branch-protection.js +2 -2
  45. package/.agents/scripts/lib/bootstrap/commit-push.js +2 -2
  46. package/.agents/scripts/lib/bootstrap/install-ledger.js +1 -1
  47. package/.agents/scripts/lib/bootstrap/issue-forms-template.js +1 -1
  48. package/.agents/scripts/lib/bootstrap/prompt.js +1 -1
  49. package/.agents/scripts/lib/bootstrap/quality-bootstrap.js +2 -2
  50. package/.agents/scripts/lib/checks/core-bare-clean.js +1 -1
  51. package/.agents/scripts/lib/checks/loop-health.js +1 -1
  52. package/.agents/scripts/lib/checks/story-init-not-backgrounded.js +1 -1
  53. package/.agents/scripts/lib/cli-args.js +1 -3
  54. package/.agents/scripts/lib/close-validation/gates.js +1 -1
  55. package/.agents/scripts/lib/close-validation/process.js +1 -1
  56. package/.agents/scripts/lib/command-header.js +1 -1
  57. package/.agents/scripts/lib/config/delivery-routing.js +1 -1
  58. package/.agents/scripts/lib/config/explain.js +1 -1
  59. package/.agents/scripts/lib/config/runners.js +2 -2
  60. package/.agents/scripts/lib/config/runtime.js +1 -1
  61. package/.agents/scripts/lib/config/sync-agentrc.js +1 -1
  62. package/.agents/scripts/lib/config/temp-paths.js +2 -2
  63. package/.agents/scripts/lib/config-settings-schema-delivery.js +3 -3
  64. package/.agents/scripts/lib/config-settings-schema-quality.js +6 -6
  65. package/.agents/scripts/lib/config-settings-schema.js +49 -9
  66. package/.agents/scripts/lib/coverage-baseline.js +2 -2
  67. package/.agents/scripts/lib/cpu-pool.js +90 -10
  68. package/.agents/scripts/lib/crap-utils.js +6 -2
  69. package/.agents/scripts/lib/cyclomatic-ceiling.js +28 -6
  70. package/.agents/scripts/lib/cyclomatic-scope.js +144 -0
  71. package/.agents/scripts/lib/dependency-version.js +86 -0
  72. package/.agents/scripts/lib/duplicate-search.js +2 -2
  73. package/.agents/scripts/lib/errors/index.js +1 -1
  74. package/.agents/scripts/lib/feedback-loop/graduator-core.js +2 -2
  75. package/.agents/scripts/lib/feedback-loop/prior-feedback-fetcher.js +5 -5
  76. package/.agents/scripts/lib/feedback-loop/retro-proposals-graduator.js +2 -2
  77. package/.agents/scripts/lib/findings/classify-finding.js +1 -1
  78. package/.agents/scripts/lib/findings/promote-finding.js +12 -12
  79. package/.agents/scripts/lib/findings/route-finding.js +2 -2
  80. package/.agents/scripts/lib/generated/agentrc-validator.js +16 -0
  81. package/.agents/scripts/lib/git-branch-lifecycle.js +2 -2
  82. package/.agents/scripts/lib/install-cmd-parser.js +1 -1
  83. package/.agents/scripts/lib/knip-entry-sync.js +1 -57
  84. package/.agents/scripts/lib/label-constants.js +2 -2
  85. package/.agents/scripts/lib/label-taxonomy.js +1 -1
  86. package/.agents/scripts/lib/maintainability-utils.js +108 -10
  87. package/.agents/scripts/lib/observability/source-classifier.js +1 -0
  88. package/.agents/scripts/lib/onboard/init-tail.js +4 -4
  89. package/.agents/scripts/lib/onboard/scaffold-docs.js +2 -2
  90. package/.agents/scripts/lib/orchestration/acceptance-eval-decision.js +1 -1
  91. package/.agents/scripts/lib/orchestration/code-review.js +4 -4
  92. package/.agents/scripts/lib/orchestration/complexity-gate.js +5 -5
  93. package/.agents/scripts/lib/orchestration/deliver-recover.js +3 -3
  94. package/.agents/scripts/lib/orchestration/docs-digest.js +3 -3
  95. package/.agents/scripts/lib/orchestration/lease-guard-shared.js +2 -2
  96. package/.agents/scripts/lib/orchestration/light-backstop.js +1 -1
  97. package/.agents/scripts/lib/orchestration/light-escalation.js +6 -6
  98. package/.agents/scripts/lib/orchestration/light-suitability.js +19 -19
  99. package/.agents/scripts/lib/orchestration/plan-context.js +4 -4
  100. package/.agents/scripts/lib/orchestration/plan-critic-conditions.js +2 -2
  101. package/.agents/scripts/lib/orchestration/plan-critics-evaluate.js +2 -2
  102. package/.agents/scripts/lib/orchestration/plan-metrics.js +1 -1
  103. package/.agents/scripts/lib/orchestration/plan-persist/persist-helpers.js +1 -1
  104. package/.agents/scripts/lib/orchestration/plan-persist/plan-context-source.js +3 -3
  105. package/.agents/scripts/lib/orchestration/plan-persist/run-plan-persist.js +6 -6
  106. package/.agents/scripts/lib/orchestration/plan-persist/story-ops.js +86 -25
  107. package/.agents/scripts/lib/orchestration/plan-persist/summary.js +3 -3
  108. package/.agents/scripts/lib/orchestration/plan-persist/supersede-ops.js +6 -6
  109. package/.agents/scripts/lib/orchestration/planning/authoring-context.js +7 -7
  110. package/.agents/scripts/lib/orchestration/planning/decomposer-context.js +1 -1
  111. package/.agents/scripts/lib/orchestration/planning/memory-pool-advisory.js +3 -3
  112. package/.agents/scripts/lib/orchestration/pr-watch.js +1 -30
  113. package/.agents/scripts/lib/orchestration/reassert-status-column.js +2 -2
  114. package/.agents/scripts/lib/orchestration/remote-verifier.js +2 -2
  115. package/.agents/scripts/lib/orchestration/resolve-stories.js +4 -4
  116. package/.agents/scripts/lib/orchestration/review-providers/codex.js +3 -3
  117. package/.agents/scripts/lib/orchestration/review-providers/findings-renderer.js +1 -1
  118. package/.agents/scripts/lib/orchestration/review-providers/native.js +9 -2
  119. package/.agents/scripts/lib/orchestration/review-providers/review-depth.js +2 -2
  120. package/.agents/scripts/lib/orchestration/review-providers/security-review.js +3 -3
  121. package/.agents/scripts/lib/orchestration/run-epilogue.js +2 -2
  122. package/.agents/scripts/lib/orchestration/single-story-close/phases/base-sync.js +2 -2
  123. package/.agents/scripts/lib/orchestration/single-story-close/phases/conventional-subject.js +1 -1
  124. package/.agents/scripts/lib/orchestration/single-story-close/phases/normalize-pr-title.js +5 -1
  125. package/.agents/scripts/lib/orchestration/single-story-close/phases/options.js +52 -1
  126. package/.agents/scripts/lib/orchestration/single-story-close/phases/review-block.js +1 -1
  127. package/.agents/scripts/lib/orchestration/single-story-close/phases/wrong-tree-guard.js +3 -3
  128. package/.agents/scripts/lib/orchestration/single-story-close/runner.js +2 -2
  129. package/.agents/scripts/lib/orchestration/single-story-lease-guard.js +1 -1
  130. package/.agents/scripts/lib/orchestration/story-close/format-autofix.js +2 -5
  131. package/.agents/scripts/lib/orchestration/story-deliver-terminal.js +14 -12
  132. package/.agents/scripts/lib/orchestration/story-follow-ups.js +1 -1
  133. package/.agents/scripts/lib/orchestration/ticket-lease.js +34 -9
  134. package/.agents/scripts/lib/orchestration/ticket-validator.js +1 -1
  135. package/.agents/scripts/lib/orchestration/ticketing/reads.js +6 -4
  136. package/.agents/scripts/lib/plan-phase-cleanup.js +1 -1
  137. package/.agents/scripts/lib/preflight-runner.js +3 -3
  138. package/.agents/scripts/lib/qa/qa-session.js +1 -1
  139. package/.agents/scripts/lib/runtime-deps/manifest.js +1 -7
  140. package/.agents/scripts/lib/runtime-deps/scan-imports.js +3 -58
  141. package/.agents/scripts/lib/signals/schema.js +1 -1
  142. package/.agents/scripts/lib/single-story-sweep/sweep-lock.js +284 -25
  143. package/.agents/scripts/lib/skills/walk-skill-files.js +1 -1
  144. package/.agents/scripts/lib/source-text/strip-js-comments.js +115 -0
  145. package/.agents/scripts/lib/story-adjacency.js +2 -2
  146. package/.agents/scripts/lib/templates/decomposer-prompts.js +1 -1
  147. package/.agents/scripts/lib/test-isolate/runner.js +3 -3
  148. package/.agents/scripts/lib/test-runner-contract.js +14 -6
  149. package/.agents/scripts/lib/test-tiers.js +135 -18
  150. package/.agents/scripts/lib/ticket-body-sections.js +1 -1
  151. package/.agents/scripts/lib/transpile.js +16 -3
  152. package/.agents/scripts/lib/util/concurrent-map.js +1 -1
  153. package/.agents/scripts/lib/util/parse-id-list.js +1 -1
  154. package/.agents/scripts/lib/wave-runner/live-probe.js +4 -4
  155. package/.agents/scripts/lib/wave-runner/ready-set.js +1 -1
  156. package/.agents/scripts/lib/workflow-closure.js +1 -1
  157. package/.agents/scripts/lib/worktree/lifecycle/pending-cleanup.js +170 -32
  158. package/.agents/scripts/lib/worktree/node-modules-strategy.js +2 -5
  159. package/.agents/scripts/lint-issue-body.js +3 -3
  160. package/.agents/scripts/plan-context.js +4 -4
  161. package/.agents/scripts/plan-critics.js +2 -2
  162. package/.agents/scripts/plan-persist.js +2 -2
  163. package/.agents/scripts/plan-run-epilogue.js +2 -2
  164. package/.agents/scripts/pr-watch-with-update.js +305 -137
  165. package/.agents/scripts/providers/github/board-add.js +1 -1
  166. package/.agents/scripts/providers/github/errors.js +1 -1
  167. package/.agents/scripts/providers/github/tickets.js +114 -21
  168. package/.agents/scripts/quality-preview.js +162 -70
  169. package/.agents/scripts/resolve-stories.js +3 -3
  170. package/.agents/scripts/resync-status-column.js +1 -1
  171. package/.agents/scripts/run-lint.js +1 -1
  172. package/.agents/scripts/run-test-profile.js +8 -5
  173. package/.agents/scripts/run-tests.js +79 -14
  174. package/.agents/scripts/single-story-close.js +2 -4
  175. package/.agents/scripts/single-story-init.js +10 -6
  176. package/.agents/scripts/stories-wave-tick.js +8 -8
  177. package/.agents/skills/core/idea-refinement/SKILL.md +5 -5
  178. package/.agents/skills/core/scope-triage/SKILL.md +3 -3
  179. package/.agents/skills/skills.index.json +1 -1
  180. package/.agents/skills/stack/qa/qa-harness/SKILL.md +1 -1
  181. package/.agents/workflows/audit-data-model.md +1 -1
  182. package/.agents/workflows/audit-documentation.md +2 -2
  183. package/.agents/workflows/audit-to-stories.md +11 -11
  184. package/.agents/workflows/git-deliver.md +4 -4
  185. package/.agents/workflows/helpers/_merge-conflict-template.md +1 -1
  186. package/.agents/workflows/helpers/audit-lens-core.md +1 -1
  187. package/.agents/workflows/helpers/code-review.md +4 -4
  188. package/.agents/workflows/helpers/deliver-light.md +26 -26
  189. package/.agents/workflows/helpers/deliver-reference.md +15 -7
  190. package/.agents/workflows/helpers/deliver-story-reference.md +9 -4
  191. package/.agents/workflows/helpers/deliver-story.md +6 -7
  192. package/.agents/workflows/helpers/diagnose.md +2 -2
  193. package/.agents/workflows/helpers/parallel-tooling.md +1 -1
  194. package/.agents/workflows/helpers/plan-reference.md +9 -9
  195. package/.agents/workflows/helpers/qa-core.md +8 -8
  196. package/.agents/workflows/helpers/worktree-lifecycle.md +6 -6
  197. package/.agents/workflows/{deliver.md → mandrel-deliver.md} +15 -19
  198. package/.agents/workflows/{plan.md → mandrel-plan.md} +9 -9
  199. package/.agents/workflows/memory-consolidate.md +4 -4
  200. package/.agents/workflows/prototype.md +3 -3
  201. package/.agents/workflows/qa-assist.md +12 -12
  202. package/.agents/workflows/qa-explore.md +6 -6
  203. package/.agents/workflows/qa-run.md +1 -1
  204. package/README.md +7 -7
  205. package/docs/CHANGELOG.md +43 -0
  206. package/lib/cli/doctor.js +1 -1
  207. package/lib/cli/init.js +1 -1
  208. package/lib/cli/migrate.js +100 -94
  209. package/lib/migrations/helpers/retire-agentrc-key.js +200 -0
  210. package/lib/migrations/index.js +32 -33
  211. package/lib/migrations/steps/2.1.0-retire-mi-drop-knobs.js +21 -80
  212. package/lib/migrations/steps/2.1.0-retire-verify-concurrency-cap.js +15 -87
  213. package/lib/migrations/steps/2.11.0-retire-max-seed-words.js +13 -71
  214. package/lib/migrations/steps/2.2.0-retire-epic-ac-tags.js +1 -1
  215. package/lib/migrations/steps/2.20.0-retire-codebase-snapshot.js +13 -101
  216. package/lib/migrations/steps/2.32.0-retire-lint-baseline-command.js +13 -100
  217. package/package.json +7 -3
  218. package/.agents/scripts/lib/audit-suite/frontmatter-lint.js +0 -32
  219. package/.agents/scripts/lib/baselines/maintainability-baseline-save.js +0 -37
  220. package/.agents/scripts/lib/cli/parse-numeric.js +0 -60
  221. package/.agents/scripts/lib/close-validation/telemetry.js +0 -79
  222. package/.agents/scripts/lib/orchestration/label-transitions.js +0 -44
  223. package/.agents/scripts/lib/orchestration/parked-follow-ons.js +0 -147
  224. package/.agents/scripts/lib/orchestration/phase-runner.js +0 -88
  225. package/.agents/scripts/lib/orchestration/recut.js +0 -56
@@ -487,6 +487,272 @@ async function evaluateGreenWatch({
487
487
  };
488
488
  }
489
489
 
490
+ /**
491
+ * Resolve the knobs, temp root and working directory one watch run needs.
492
+ *
493
+ * Split out of `runPrWatch` because config resolution must not abort the
494
+ * watch: a broken `.agentrc` should degrade to defaults, not turn a CI probe
495
+ * into a crash.
496
+ *
497
+ * @param {{ config?: object, tempRoot?: string, logger: object, flags: object }} params
498
+ * @returns {{ knobs: object, effectiveTempRoot: string, cwd: string }}
499
+ */
500
+ function resolveWatchContext({ config, tempRoot, logger, flags }) {
501
+ const resolvedConfig =
502
+ config !== undefined ? config : safeResolveConfig(logger);
503
+ const knobs = resolveWatchKnobs({ config: resolvedConfig, flags });
504
+ const effectiveTempRoot =
505
+ tempRoot ?? resolvedConfig?.project?.paths?.tempRoot ?? 'temp';
506
+ return { knobs, effectiveTempRoot, cwd: process.cwd() };
507
+ }
508
+
509
+ /**
510
+ * Build the argument bag for the underlying watch port.
511
+ *
512
+ * Each injectable port is spread in only when supplied so the port keeps its
513
+ * own default — passing `undefined` explicitly would override a default with
514
+ * nothing and break every caller that relies on it.
515
+ *
516
+ * @param {object} params
517
+ * @returns {object}
518
+ */
519
+ function buildWatchArgs({
520
+ prRef,
521
+ repo,
522
+ cwd,
523
+ knobs,
524
+ ghPrChecksFn,
525
+ ghPrViewFn,
526
+ ghPrUpdateBranchFn,
527
+ sleepFn,
528
+ logger,
529
+ }) {
530
+ return {
531
+ prUrl: prRef,
532
+ repo,
533
+ cwd,
534
+ maxPolls: knobs.maxPolls,
535
+ maxUpdates: knobs.maxUpdates,
536
+ maxResumes: knobs.maxResumes,
537
+ pollIntervalMs: knobs.pollIntervalMs,
538
+ ...(ghPrChecksFn ? { ghPrChecksFn } : {}),
539
+ ...(ghPrViewFn ? { ghPrViewFn } : {}),
540
+ ...(ghPrUpdateBranchFn ? { ghPrUpdateBranchFn } : {}),
541
+ ...(sleepFn ? { sleepFn } : {}),
542
+ logger,
543
+ };
544
+ }
545
+
546
+ /**
547
+ * One terminal outcome of the watch: the attach window is spent and no
548
+ * required check ever attached, or the pull request could not be read at all.
549
+ *
550
+ * The two are distinguished structurally — never against `gh`'s stderr prose
551
+ * (Story #4890) — because they route oppositely: CI that has not started is
552
+ * slow (exit 2, keep watching), while an unreadable PR is a `gh` / access
553
+ * fault (exit 1).
554
+ *
555
+ * @param {object} params
556
+ * @returns {number} exit code
557
+ */
558
+ function reportUnattachedOrError({
559
+ result,
560
+ envelope,
561
+ prNumber,
562
+ knobs,
563
+ logger,
564
+ print,
565
+ }) {
566
+ const notYetStarted = Boolean(
567
+ result.requiredChecksEmpty && result.prResolvable,
568
+ );
569
+ print(
570
+ JSON.stringify({
571
+ ...envelope,
572
+ requiredChecksEmpty: Boolean(result.requiredChecksEmpty),
573
+ notYetStarted,
574
+ }),
575
+ );
576
+ if (notYetStarted) {
577
+ logger.warn?.(
578
+ `[pr-watch] no required check has attached to PR #${prNumber} within the ` +
579
+ `${Math.round(knobs.attachWindowMs / 1000)}s attach window (${result.attachRetries} re-resolutions), ` +
580
+ 'and the pull request still reads back fine — this is CI that has not started, NOT a red check. ' +
581
+ 'Keep polling natively:',
582
+ );
583
+ logger.warn?.('[pr-watch] gh pr checks <pr> --watch');
584
+ return STILL_RUNNING_EXIT_CODE;
585
+ }
586
+ logger.error?.(
587
+ `[pr-watch] could not resolve required checks: ${result.error} — the pull request itself could ` +
588
+ 'not be read, so this is a `gh` / access fault rather than CI that has not started.',
589
+ );
590
+ return 1;
591
+ }
592
+
593
+ /**
594
+ * Settle a watch whose observed required checks all came back green.
595
+ *
596
+ * Never green on an undercount (Story #4873): the observed set is whatever
597
+ * `gh pr checks --required` returned on the FIRST probe, so a context a
598
+ * ruleset attached later can leave every check we knew about green while
599
+ * GitHub still refuses the merge. An unreconcilable set reports unresolved
600
+ * (exit 2, keep watching) rather than a false green or a false red.
601
+ *
602
+ * @param {object} params
603
+ * @returns {Promise<number>} exit code
604
+ */
605
+ async function settleGreenWatch({
606
+ result,
607
+ envelope,
608
+ readMergeState,
609
+ storyId,
610
+ prNumber,
611
+ guardPrRef,
612
+ effectiveTempRoot,
613
+ cwd,
614
+ readDigestFn,
615
+ retireDigestFn,
616
+ headShaFn,
617
+ reArmAutoMergeFn,
618
+ blockDeliveryFn,
619
+ logger,
620
+ print,
621
+ }) {
622
+ const reconciliation = reconcileGreenVerdict({
623
+ observedRequired: result.requiredChecks,
624
+ mergeStateStatus: await readMergeState(),
625
+ });
626
+ if (!reconciliation.reconciled) {
627
+ print(JSON.stringify({ ...envelope, reconciliation }));
628
+ logger.warn?.(
629
+ `[pr-watch] withholding the green verdict: ${reconciliation.reason}. ` +
630
+ 'Re-run the watch once the repository settles, or inspect branch protection for a context this watch never saw.',
631
+ );
632
+ return STILL_RUNNING_EXIT_CODE;
633
+ }
634
+ const guard = await evaluateGreenWatch({
635
+ storyId,
636
+ prNumber,
637
+ prRef: guardPrRef,
638
+ tempRoot: effectiveTempRoot,
639
+ cwd,
640
+ readDigestFn,
641
+ retireDigestFn,
642
+ headShaFn,
643
+ reArmFn: reArmAutoMergeFn,
644
+ blockFn: blockDeliveryFn,
645
+ logger,
646
+ });
647
+ print(JSON.stringify({ ...envelope, reconciliation, rerunGuard: guard }));
648
+ if (guard.exitCode === 0) {
649
+ logger.info?.('[pr-watch] all required checks green.');
650
+ }
651
+ return guard.exitCode;
652
+ }
653
+
654
+ /**
655
+ * Slow-but-not-red: the poll cap AND the resume budget are exhausted with
656
+ * checks still pending and none failed. Never exit 1, never `timed_out` —
657
+ * hand off to the host's interval loop and exit 2.
658
+ *
659
+ * @param {object} params
660
+ * @returns {number} exit code
661
+ */
662
+ function reportStillRunning({ result, envelope, logger, print }) {
663
+ print(JSON.stringify(envelope));
664
+ const stillPending = Object.entries(result.outcomes)
665
+ .filter(([, v]) => v === 'still-running')
666
+ .map(([k]) => k)
667
+ .join(', ');
668
+ logger.warn?.(
669
+ `[pr-watch] required check(s) still running after ${result.polls} polls + ${result.resumesApplied} resumes: ${stillPending}. Keep polling natively:`,
670
+ );
671
+ logger.warn?.('[pr-watch] gh pr checks <pr> --watch');
672
+ return STILL_RUNNING_EXIT_CODE;
673
+ }
674
+
675
+ /**
676
+ * The failing checks, excluding every non-failing state **and**
677
+ * `still-running`: when the cap fires with a mixed failed+pending map,
678
+ * `promotePendingToStillRunning` has rewritten the pending entries, and a
679
+ * still-running check is slow, not red — including it here would let it become
680
+ * the digest's "primary" failing check and mispoint the diagnosis.
681
+ *
682
+ * @param {Record<string, string>} outcomes
683
+ * @returns {Array<{ name: string, outcome: string }>}
684
+ */
685
+ function collectFailures(outcomes) {
686
+ return Object.entries(outcomes)
687
+ .filter(
688
+ ([, v]) =>
689
+ v !== 'success' &&
690
+ v !== 'neutral' &&
691
+ v !== 'skipped' &&
692
+ v !== 'still-running',
693
+ )
694
+ .map(([name, outcome]) => ({ name, outcome }));
695
+ }
696
+
697
+ /**
698
+ * Genuine red check — exit 1 immediately, disarm auto-merge, write the digest,
699
+ * and surface the fix-loop handoff.
700
+ *
701
+ * @param {object} params
702
+ * @returns {Promise<number>} exit code
703
+ */
704
+ async function settleRedWatch({
705
+ result,
706
+ envelope,
707
+ storyId,
708
+ prNumber,
709
+ guardPrRef,
710
+ effectiveTempRoot,
711
+ cwd,
712
+ writeDigestFn,
713
+ headShaFn,
714
+ disarmAutoMergeFn,
715
+ blockDeliveryFn,
716
+ logger,
717
+ print,
718
+ }) {
719
+ const failures = collectFailures(result.outcomes);
720
+ const red = failures.map((f) => `${f.name}=${f.outcome}`).join(', ');
721
+ logger.error?.(`[pr-watch] required check(s) not green: ${red}`);
722
+ const redOutcome = await handleRedWatch({
723
+ storyId,
724
+ prNumber,
725
+ prRef: guardPrRef,
726
+ failures,
727
+ tempRoot: effectiveTempRoot,
728
+ cwd,
729
+ writeDigestFn,
730
+ headShaFn,
731
+ disarmFn: disarmAutoMergeFn,
732
+ blockFn: blockDeliveryFn,
733
+ logger,
734
+ });
735
+ print(
736
+ JSON.stringify({
737
+ ...envelope,
738
+ classification: classifyFailure(failures[0]?.name),
739
+ rerunGuard: {
740
+ verdict: 'red',
741
+ headSha: redOutcome.headSha,
742
+ autoMergeDisarmed: redOutcome.disarm.disarmed,
743
+ disarmDetail: redOutcome.disarm.detail,
744
+ digestPath: redOutcome.digestPaths?.jsonPath ?? null,
745
+ blocked: redOutcome.blocked,
746
+ },
747
+ }),
748
+ );
749
+ logger.error?.(
750
+ '[pr-watch] a required check failed. Read the digest, reproduce the failure, and apply the smallest fix at source, ' +
751
+ 'then push a new commit — re-running the failed job is forbidden (`.agents/rules/ci-remediation.md` § Verifier).',
752
+ );
753
+ return 1;
754
+ }
755
+
490
756
  /**
491
757
  * Run the watch loop and resolve to the exit code. Exported for tests so
492
758
  * the green / red / still-running / BEHIND paths can be exercised with
@@ -557,21 +823,12 @@ export async function runPrWatch({
557
823
  if (!Number.isInteger(prNumber) || prNumber < 1)
558
824
  throw new TypeError('runPrWatch: --pr requires a positive integer');
559
825
 
560
- const resolvedConfig =
561
- config !== undefined ? config : safeResolveConfig(logger);
562
- const knobs = resolveWatchKnobs({
563
- config: resolvedConfig,
564
- flags: {
565
- pollIntervalMs,
566
- maxPolls,
567
- maxResumes,
568
- maxUpdates,
569
- attachWindowMs,
570
- },
826
+ const { knobs, effectiveTempRoot, cwd } = resolveWatchContext({
827
+ config,
828
+ tempRoot,
829
+ logger,
830
+ flags: { pollIntervalMs, maxPolls, maxResumes, maxUpdates, attachWindowMs },
571
831
  });
572
- const effectiveTempRoot =
573
- tempRoot ?? resolvedConfig?.project?.paths?.tempRoot ?? 'temp';
574
- const cwd = process.cwd();
575
832
 
576
833
  // `gh` has NO `<owner/repo>#<number>` argument form — it parses that string
577
834
  // as a BRANCH NAME, which is why every `--repo` invocation used to fail at
@@ -596,20 +853,17 @@ export async function runPrWatch({
596
853
  probeMergeState({ prRef: guardPrRef, prUrl: prRef, repo, cwd, prNumber });
597
854
 
598
855
  const result = await watchWithAttachWindow({
599
- watchArgs: {
600
- prUrl: prRef,
856
+ watchArgs: buildWatchArgs({
857
+ prRef,
601
858
  repo,
602
859
  cwd,
603
- maxPolls: knobs.maxPolls,
604
- maxUpdates: knobs.maxUpdates,
605
- maxResumes: knobs.maxResumes,
606
- pollIntervalMs: knobs.pollIntervalMs,
607
- ...(ghPrChecksFn ? { ghPrChecksFn } : {}),
608
- ...(ghPrViewFn ? { ghPrViewFn } : {}),
609
- ...(ghPrUpdateBranchFn ? { ghPrUpdateBranchFn } : {}),
610
- ...(sleepFn ? { sleepFn } : {}),
860
+ knobs,
861
+ ghPrChecksFn,
862
+ ghPrViewFn,
863
+ ghPrUpdateBranchFn,
864
+ sleepFn,
611
865
  logger,
612
- },
866
+ }),
613
867
  attachWindowMs: knobs.attachWindowMs,
614
868
  retryIntervalMs: knobs.pollIntervalMs,
615
869
  sleepFn: sleepFn ?? defaultSleep,
@@ -637,141 +891,55 @@ export async function runPrWatch({
637
891
  };
638
892
 
639
893
  if (result.requiredChecksEmpty || result.error) {
640
- // The attach window is spent and the required set is still empty. Classify
641
- // it structurally — never against `gh`'s stderr prose (Story #4890).
642
- const notYetStarted = Boolean(
643
- result.requiredChecksEmpty && result.prResolvable,
644
- );
645
- print(
646
- JSON.stringify({
647
- ...envelope,
648
- requiredChecksEmpty: Boolean(result.requiredChecksEmpty),
649
- notYetStarted,
650
- }),
651
- );
652
- if (notYetStarted) {
653
- logger.warn?.(
654
- `[pr-watch] no required check has attached to PR #${prNumber} within the ` +
655
- `${Math.round(knobs.attachWindowMs / 1000)}s attach window (${result.attachRetries} re-resolutions), ` +
656
- 'and the pull request still reads back fine — this is CI that has not started, NOT a red check. ' +
657
- 'Keep polling natively:',
658
- );
659
- logger.warn?.('[pr-watch] gh pr checks <pr> --watch');
660
- return STILL_RUNNING_EXIT_CODE;
661
- }
662
- logger.error?.(
663
- `[pr-watch] could not resolve required checks: ${result.error} — the pull request itself could ` +
664
- 'not be read, so this is a `gh` / access fault rather than CI that has not started.',
665
- );
666
- return 1;
894
+ return reportUnattachedOrError({
895
+ result,
896
+ envelope,
897
+ prNumber,
898
+ knobs,
899
+ logger,
900
+ print,
901
+ });
667
902
  }
668
903
 
669
904
  if (result.green) {
670
- // Never green on an undercount (Story #4873). The observed required set is
671
- // whatever `gh pr checks --required` returned on the FIRST probe; when a
672
- // ruleset attached a context after that, every check we knew about can be
673
- // green while GitHub still refuses the merge. Reconcile against the
674
- // repository's own verdict before any green is issued — an unreconcilable
675
- // set reports unresolved (exit 2, keep watching) rather than a false green
676
- // or a false red.
677
- const reconciliation = reconcileGreenVerdict({
678
- observedRequired: result.requiredChecks,
679
- mergeStateStatus: await readMergeState(),
680
- });
681
- if (!reconciliation.reconciled) {
682
- print(JSON.stringify({ ...envelope, reconciliation }));
683
- logger.warn?.(
684
- `[pr-watch] withholding the green verdict: ${reconciliation.reason}. ` +
685
- 'Re-run the watch once the repository settles, or inspect branch protection for a context this watch never saw.',
686
- );
687
- return STILL_RUNNING_EXIT_CODE;
688
- }
689
- const guard = await evaluateGreenWatch({
905
+ return await settleGreenWatch({
906
+ result,
907
+ envelope,
908
+ readMergeState,
690
909
  storyId,
691
910
  prNumber,
692
- prRef: guardPrRef,
693
- tempRoot: effectiveTempRoot,
911
+ guardPrRef,
912
+ effectiveTempRoot,
694
913
  cwd,
695
914
  readDigestFn,
696
915
  retireDigestFn,
697
916
  headShaFn,
698
- reArmFn: reArmAutoMergeFn,
699
- blockFn: blockDeliveryFn,
917
+ reArmAutoMergeFn,
918
+ blockDeliveryFn,
700
919
  logger,
920
+ print,
701
921
  });
702
- print(JSON.stringify({ ...envelope, reconciliation, rerunGuard: guard }));
703
- if (guard.exitCode === 0) {
704
- logger.info?.('[pr-watch] all required checks green.');
705
- }
706
- return guard.exitCode;
707
922
  }
708
923
 
709
- // Slow-but-not-red: the cap AND resume budget are exhausted with checks
710
- // still pending and none failed. Never exit 1, never `timed_out` — hand
711
- // off to the host's interval loop and exit 2.
712
924
  if (result.stillRunning) {
713
- print(JSON.stringify(envelope));
714
- const stillPending = Object.entries(result.outcomes)
715
- .filter(([, v]) => v === 'still-running')
716
- .map(([k]) => k)
717
- .join(', ');
718
- logger.warn?.(
719
- `[pr-watch] required check(s) still running after ${result.polls} polls + ${result.resumesApplied} resumes: ${stillPending}. Keep polling natively:`,
720
- );
721
- logger.warn?.('[pr-watch] gh pr checks <pr> --watch');
722
- return STILL_RUNNING_EXIT_CODE;
925
+ return reportStillRunning({ result, envelope, logger, print });
723
926
  }
724
927
 
725
- // Genuine red check — exit 1 immediately, disarm auto-merge, write the
726
- // digest, and surface the fix-loop handoff.
727
- // Exclude 'still-running' as well as the non-failing states: when the cap
728
- // fires with a mixed failed+pending map, promotePendingToStillRunning has
729
- // rewritten the pending entries, and a still-running check is slow, not
730
- // red — including it here would let it become the digest's "primary"
731
- // failing check and mispoint the diagnosis at a slow check.
732
- const failures = Object.entries(result.outcomes)
733
- .filter(
734
- ([, v]) =>
735
- v !== 'success' &&
736
- v !== 'neutral' &&
737
- v !== 'skipped' &&
738
- v !== 'still-running',
739
- )
740
- .map(([name, outcome]) => ({ name, outcome }));
741
- const red = failures.map((f) => `${f.name}=${f.outcome}`).join(', ');
742
- logger.error?.(`[pr-watch] required check(s) not green: ${red}`);
743
- const redOutcome = await handleRedWatch({
928
+ return await settleRedWatch({
929
+ result,
930
+ envelope,
744
931
  storyId,
745
932
  prNumber,
746
- prRef: guardPrRef,
747
- failures,
748
- tempRoot: effectiveTempRoot,
933
+ guardPrRef,
934
+ effectiveTempRoot,
749
935
  cwd,
750
936
  writeDigestFn,
751
937
  headShaFn,
752
- disarmFn: disarmAutoMergeFn,
753
- blockFn: blockDeliveryFn,
938
+ disarmAutoMergeFn,
939
+ blockDeliveryFn,
754
940
  logger,
941
+ print,
755
942
  });
756
- print(
757
- JSON.stringify({
758
- ...envelope,
759
- classification: classifyFailure(failures[0]?.name),
760
- rerunGuard: {
761
- verdict: 'red',
762
- headSha: redOutcome.headSha,
763
- autoMergeDisarmed: redOutcome.disarm.disarmed,
764
- disarmDetail: redOutcome.disarm.detail,
765
- digestPath: redOutcome.digestPaths?.jsonPath ?? null,
766
- blocked: redOutcome.blocked,
767
- },
768
- }),
769
- );
770
- logger.error?.(
771
- '[pr-watch] a required check failed. Read the digest, reproduce the failure, and apply the smallest fix at source, ' +
772
- 'then push a new commit — re-running the failed job is forbidden (`.agents/rules/ci-remediation.md` § Verifier).',
773
- );
774
- return 1;
775
943
  }
776
944
 
777
945
  /** Resolve config without letting a config error abort the watch. */
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Story #3822 — single source of truth for the post-create board-add
5
5
  * step. Issues created through any create path (`createTicket`,
6
- * `createIssue` — which backs `/plan` persist and the `/plan`
6
+ * `createIssue` — which backs `/mandrel-plan` persist and the `/mandrel-plan`
7
7
  * Phase 4 Epic open) must land on the configured Projects V2 board
8
8
  * without relying on GitHub's "Auto-add to project" built-in workflow,
9
9
  * which is off by default on fresh boards and cannot be enabled via API.
@@ -156,7 +156,7 @@ export function classifyGithubError(err) {
156
156
  // Read-path callers (paginateRest, getTicket, getNativeSubIssues, …) all
157
157
  // absorb the same jittered exponential backoff on transient GitHub errors
158
158
  // through this one primitive, instead of bubbling a one-shot
159
- // 502/429/ECONNRESET that kills a longer pipeline (e.g. the /deliver
159
+ // 502/429/ECONNRESET that kills a longer pipeline (e.g. the /mandrel-deliver
160
160
  // Phase E retro). The network consumers repointed here
161
161
  // (branch-protection, labels, projects-v2-graphql) call with no opts, so
162
162
  // they adopt these defaults; their retry *classes* (the network blips) are