@event4u/agent-config 9.15.0 → 9.17.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 (157) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/AGENTS.md +1 -1
  3. package/CHANGELOG.md +74 -0
  4. package/README.md +1 -1
  5. package/dist/agent-src/commands/agent-handoff.md +100 -48
  6. package/dist/agent-src/commands/commit.md +1 -1
  7. package/dist/agent-src/commands/condense.md +19 -64
  8. package/dist/agent-src/commands/evals/agent-handoff.json +13 -1
  9. package/dist/agent-src/commands/memory/promote.md +14 -1
  10. package/dist/agent-src/commands/mode.md +9 -5
  11. package/dist/agent-src/commands/optimize/deep.md +72 -11
  12. package/dist/agent-src/contexts/authority/commit-mechanics.md +5 -5
  13. package/dist/agent-src/contexts/communication/rules-auto/guidelines-mechanics.md +1 -0
  14. package/dist/agent-src/contexts/communication/rules-auto/skill-quality-mechanics.md +1 -1
  15. package/dist/agent-src/contexts/communication/rules-auto/think-before-action-mechanics.md +6 -1
  16. package/dist/agent-src/contexts/communication/rules-auto/token-efficiency-mechanics.md +4 -1
  17. package/dist/agent-src/contexts/execution/autonomy-mechanics.md +27 -1
  18. package/dist/agent-src/contexts/execution/roadmap-process-loop.md +5 -2
  19. package/dist/agent-src/profiles/agency.yml +3 -3
  20. package/dist/agent-src/profiles/content_creator.yml +2 -2
  21. package/dist/agent-src/profiles/developer.yml +2 -2
  22. package/dist/agent-src/profiles/finance.yml +1 -1
  23. package/dist/agent-src/profiles/founder.yml +2 -2
  24. package/dist/agent-src/profiles/ops.yml +2 -2
  25. package/dist/agent-src/rules/architecture.md +2 -2
  26. package/dist/agent-src/rules/autonomous-execution.md +1 -1
  27. package/dist/agent-src/rules/context-hygiene.md +3 -2
  28. package/dist/agent-src/rules/domain-safety-pii.md +1 -1
  29. package/dist/agent-src/rules/output-discipline.md +9 -2
  30. package/dist/agent-src/rules/token-budget-discipline.md +2 -2
  31. package/dist/agent-src/rules/user-interaction.md +3 -4
  32. package/dist/agent-src/skills/build-buy-partner/SKILL.md +1 -1
  33. package/dist/agent-src/skills/copilot-agents-optimization/SKILL.md +4 -4
  34. package/dist/agent-src/skills/copilot-config/SKILL.md +1 -1
  35. package/dist/agent-src/skills/deep-reading-analyst/SKILL.md +7 -4
  36. package/dist/agent-src/skills/description-assist/SKILL.md +1 -1
  37. package/dist/agent-src/skills/fe-design/SKILL.md +1 -1
  38. package/dist/agent-src/skills/market-entry-analysis/SKILL.md +1 -1
  39. package/dist/agent-src/skills/persona-writing/SKILL.md +2 -2
  40. package/dist/agent-src/skills/readme-reviewer/SKILL.md +2 -1
  41. package/dist/agent-src/skills/rule-writing/SKILL.md +4 -2
  42. package/dist/agent-src/skills/skill-reviewer/SKILL.md +1 -1
  43. package/dist/agent-src/skills/skill-writing/SKILL.md +2 -2
  44. package/dist/agent-src/skills/tailwind-engineer/SKILL.md +3 -1
  45. package/dist/agent-src/skills/token-optimizer/SKILL.md +2 -2
  46. package/dist/agent-src/skills/ui-component-architect/SKILL.md +6 -3
  47. package/dist/agent-src/templates/agent-settings.md +13 -15
  48. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +6 -2
  49. package/dist/agent-src/templates/persona.md +2 -2
  50. package/dist/agent-src/templates/roadmaps.md +1 -1
  51. package/dist/agent-src/templates/rule.md +1 -1
  52. package/dist/agent-src/templates/skill.md +1 -1
  53. package/dist/cli/mcp/content.js +1 -1
  54. package/dist/cli/mcp/content.js.map +1 -1
  55. package/dist/cli/registry.js +1 -0
  56. package/dist/cli/registry.js.map +1 -1
  57. package/dist/cli-delegate/{chunk-ZXXJLL5K.js → chunk-BSLUHV2F.js} +127 -1487
  58. package/dist/cli-delegate/{chunk-H574KOUH.js → chunk-IMSXPRVH.js} +24 -1
  59. package/dist/cli-delegate/chunk-VSYT7OHO.js +1382 -0
  60. package/dist/cli-delegate/chunk-XGA2NTU4.js +3727 -0
  61. package/dist/cli-delegate/cmd_conformance.js +4 -3
  62. package/dist/cli-delegate/cmd_converge.js +3 -3
  63. package/dist/cli-delegate/cmd_doctor.js +3 -2
  64. package/dist/cli-delegate/cmd_export.js +4 -4
  65. package/dist/cli-delegate/cmd_handoff.js +3194 -0
  66. package/dist/cli-delegate/cmd_memory_get.js +35 -3754
  67. package/dist/cli-delegate/cmd_migrate.js +2 -2
  68. package/dist/cli-delegate/cmd_prune.js +4 -4
  69. package/dist/cli-delegate/cmd_settings_migrate.js +4 -4
  70. package/dist/cli-delegate/cmd_sync.js +4 -4
  71. package/dist/cli-delegate/cmd_uninstall.js +4 -4
  72. package/dist/cli-delegate/cmd_update.js +3 -3
  73. package/dist/cli-delegate/cmd_upgrade.js +3 -3
  74. package/dist/cli-delegate/cmd_validate.js +4 -4
  75. package/dist/cli-delegate/cmd_versions.js +3 -3
  76. package/dist/discovery/deprecation-report.md +1 -1
  77. package/dist/discovery/discovery-manifest.json +31 -31
  78. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  79. package/dist/discovery/discovery-manifest.summary.md +1 -1
  80. package/dist/discovery/orphan-report.md +1 -1
  81. package/dist/discovery/packs.json +2 -2
  82. package/dist/discovery/trust-report.md +1 -1
  83. package/dist/discovery/workspaces.json +2 -2
  84. package/dist/hooks/dispatch.js +602 -507
  85. package/dist/install/install.mjs +23 -0
  86. package/dist/mcp/awesome-mcp-servers.row.md +1 -1
  87. package/dist/mcp/mcp-cloudflare-catalogue.json +2 -2
  88. package/dist/mcp/registry-manifest.json +4 -4
  89. package/dist/mcp/server.json +2 -2
  90. package/dist/mcp/server.mjs +25 -3
  91. package/dist/scripts/_lib/agent_src.js +39 -6
  92. package/dist/scripts/_lib/agent_src.js.map +1 -1
  93. package/dist/server/routes/workspace.js +1 -1
  94. package/dist/server/routes/workspace.js.map +1 -1
  95. package/dist/server/schemas/settings.js +13 -0
  96. package/dist/server/schemas/settings.js.map +1 -1
  97. package/dist/shared/userMd/utils.js +1 -1
  98. package/dist/shared/userMd/utils.js.map +1 -1
  99. package/dist/ui/assets/index-DmR0mbk4.js +26 -0
  100. package/dist/ui/assets/index-DmR0mbk4.js.map +1 -0
  101. package/dist/ui/index.html +1 -1
  102. package/docs/guidelines/abstraction-thresholds.md +53 -0
  103. package/docs/guidelines/agent-infra/agent-interaction-and-decision-quality.md +1 -1
  104. package/docs/guidelines/agent-infra/asking-and-brevity-examples.md +7 -0
  105. package/docs/guidelines/agent-infra/context-hygiene-mechanics.md +2 -2
  106. package/docs/guidelines/agent-infra/minimal-safe-diff-mechanics.md +5 -3
  107. package/docs/guidelines/agent-infra/size-and-scope.md +8 -2
  108. package/docs/guidelines/component-oriented-and-oop-development.md +11 -6
  109. package/docs/guidelines/docs/readme-size-and-splitting.md +5 -2
  110. package/llms.txt +1 -0
  111. package/package.json +4 -4
  112. package/src/config/agent-settings.template.yml +25 -1
  113. package/src/config/budget-routing.json +7 -0
  114. package/src/config/evaluator-budgets.json +8 -11
  115. package/src/config/gate-coverage.yml +30 -0
  116. package/src/scripts/_cli/cmd_handoff.ts +405 -0
  117. package/src/scripts/_cli/handoff_generate.ts +453 -0
  118. package/src/scripts/_cli/handoff_sessions.ts +435 -0
  119. package/src/scripts/_dispatch.bash +9 -0
  120. package/src/scripts/_lib/agent_src.ts +36 -6
  121. package/src/scripts/_lib/release_material.ts +156 -0
  122. package/src/scripts/_lib/tier_budget_routing.ts +154 -28
  123. package/src/scripts/anchor_scoring_run.ts +1 -1
  124. package/src/scripts/bench_persona_placebo.ts +1 -1
  125. package/src/scripts/bench_quality_run.ts +1 -1
  126. package/src/scripts/check_cli_registry_budget_sync.ts +168 -0
  127. package/src/scripts/check_condensation.ts +2 -1
  128. package/src/scripts/check_finding_dispositions.ts +278 -0
  129. package/src/scripts/check_gate_coverage.ts +1 -1
  130. package/src/scripts/check_pack_size.ts +4 -3
  131. package/src/scripts/check_references.ts +1 -0
  132. package/src/scripts/check_release_highlights.ts +300 -0
  133. package/src/scripts/check_release_pr_shape.ts +4 -0
  134. package/src/scripts/check_release_surface_equality.ts +132 -0
  135. package/src/scripts/check_reply_consistency.ts +187 -138
  136. package/src/scripts/check_token_quality_golden.ts +1 -1
  137. package/src/scripts/cost/budget.mjs +18 -4
  138. package/src/scripts/handoff_context_hook.ts +148 -0
  139. package/src/scripts/hook_manifest.yaml +17 -7
  140. package/src/scripts/hooks/concern_registry.ts +2 -0
  141. package/src/scripts/lint_abstraction_thresholds.ts +325 -0
  142. package/src/scripts/lint_framework_leakage_allowlist.json +2 -2
  143. package/src/scripts/lint_profile_personas.ts +153 -0
  144. package/src/scripts/lint_token_budget_discipline.ts +148 -0
  145. package/src/scripts/release.ts +264 -37
  146. package/src/scripts/release_drill.ts +491 -0
  147. package/src/scripts/routing_doctor.ts +76 -1
  148. package/src/scripts/schemas/command.schema.json +33 -0
  149. package/src/scripts/self_review_gate.ts +41 -3
  150. package/src/scripts/skill_linter.ts +3 -1
  151. package/src/scripts/smoke/router.sh +1 -1
  152. package/src/scripts/update_counts.ts +60 -12
  153. package/src/server/schemas/settings.ts +23 -0
  154. package/src/shared/userMd/utils.ts +1 -1
  155. package/dist/ui/assets/index-DJY_vQVn.js +0 -40
  156. package/dist/ui/assets/index-DJY_vQVn.js.map +0 -1
  157. package/dist/cli-delegate/{chunk-XX6TOPD7.js → chunk-WR6GTRHJ.js} +3 -3
@@ -118,6 +118,12 @@ import {
118
118
  perform_split,
119
119
  plan_split,
120
120
  } from './_lib/changelog_eras.js';
121
+ import {
122
+ extract_changelog_section,
123
+ pr_body_from_section,
124
+ release_notes_from_section,
125
+ tag_message_from_section,
126
+ } from './_lib/release_material.js';
121
127
 
122
128
  // `__doc__.splitlines()[0]` in `_parse_args` — the argparse description. Kept
123
129
  // as a referenceable constant so the first docstring line is preserved exactly.
@@ -517,6 +523,22 @@ interface RunResult {
517
523
  * stderr so release preflight failures are diagnosable without re-running with
518
524
  * a debugger.
519
525
  */
526
+ /**
527
+ * Drill/test seam — when set, every external command run() would spawn is
528
+ * answered by this function instead. The check/die/CalledProcessError
529
+ * semantics of run() still apply to the simulated result, so the drill
530
+ * exercises the REAL error paths of the orchestration (release_drill.ts).
531
+ */
532
+ type ExecOverride = (args: readonly string[]) => {
533
+ status: number;
534
+ stdout: string;
535
+ stderr: string;
536
+ };
537
+ let _exec_override: ExecOverride | null = null;
538
+ function _set_exec_override(fn: ExecOverride | null): void {
539
+ _exec_override = fn;
540
+ }
541
+
520
542
  function run(
521
543
  args: readonly string[],
522
544
  opts: { check?: boolean; capture?: boolean; cwd?: string | null } = {},
@@ -526,13 +548,15 @@ function run(
526
548
  const cwd = opts.cwd ?? REPO_ROOT;
527
549
 
528
550
  const [cmd, ...rest] = args;
529
- const res = spawnSync(cmd as string, rest, {
530
- cwd,
531
- encoding: 'utf-8',
532
- // capture_output=True → pipe; else inherit so child writes straight to
533
- // this process's stdout/stderr (text mode, matching subprocess text=True).
534
- stdio: capture ? ['ignore', 'pipe', 'pipe'] : ['inherit', 'inherit', 'inherit'],
535
- });
551
+ const res = _exec_override
552
+ ? { ..._exec_override(args), error: undefined as Error | undefined }
553
+ : spawnSync(cmd as string, rest, {
554
+ cwd,
555
+ encoding: 'utf-8',
556
+ // capture_output=True → pipe; else inherit so child writes straight to
557
+ // this process's stdout/stderr (text mode, matching subprocess text=True).
558
+ stdio: capture ? ['ignore', 'pipe', 'pipe'] : ['inherit', 'inherit', 'inherit'],
559
+ });
536
560
 
537
561
  if (res.error) {
538
562
  // FileNotFoundError analogue (ENOENT) and other spawn failures — Python
@@ -619,6 +643,31 @@ export function _failed_checks_report(names: readonly string[]): string {
619
643
  'empty (docs/contracts/release-pr-gating.md § Mid-release fixes).',
620
644
  );
621
645
  }
646
+ if (names.includes('PR body equals CHANGELOG entry')) {
647
+ lines.push(
648
+ '',
649
+ 'Surface equality: the PR body drifted from the CHANGELOG entry. A',
650
+ 'resume run refreshes it from the branch head (release-truth Phase 1);',
651
+ 'never hand-edit the PR body away from the changelog section.',
652
+ );
653
+ }
654
+ if (names.includes('Curated highlights plausible against the span')) {
655
+ lines.push(
656
+ '',
657
+ 'Highlights: a curated `_none_` field contradicts the release span.',
658
+ 'Fill the curated head in CHANGELOG.md on the release branch — the',
659
+ 'gate output lists the deriving commits (release-truth Phase 2).',
660
+ );
661
+ }
662
+ if (names.includes('Blocking review findings dispositioned')) {
663
+ lines.push(
664
+ '',
665
+ 'Dispositions: a blocking/high self-review finding has no committed',
666
+ 'disposition. Ingest + adjudicate it in',
667
+ 'agents/evidence/release-findings/<version>.json (release-truth Phase 3):',
668
+ ' ./scripts-run src/scripts/check_finding_dispositions --ingest <findings.json>',
669
+ );
670
+ }
622
671
  lines.push('', 'After fixing, resume with: task release -- --resume --yes', '');
623
672
  return lines.join('\n');
624
673
  }
@@ -634,22 +683,25 @@ export function _failed_checks_report(names: readonly string[]): string {
634
683
  * A short grace period gives GitHub time to register workflow runs on a
635
684
  * freshly-pushed branch.
636
685
  */
637
- function watch_pr_checks(): void {
638
- // time.sleep(5) — blocking grace period. Never reached on any test path.
639
- const until = Date.now() + 5000;
640
- while (Date.now() < until) {
641
- // busy-wait stand-in for time.sleep(5) without an event-loop yield.
686
+ function watch_pr_checks(branch: string): void {
687
+ if (_exec_override === null) {
688
+ // time.sleep(5) — blocking grace period for GitHub to register runs
689
+ // on a freshly-pushed branch. Skipped under the drill seam: there is
690
+ // no GitHub to wait for, and 5s per simulated wait adds up.
691
+ const until = Date.now() + 5000;
692
+ while (Date.now() < until) {
693
+ // busy-wait stand-in for time.sleep(5) without an event-loop yield.
694
+ }
642
695
  }
643
- const proc = spawnSync('gh', ['pr', 'checks', '--watch'], {
644
- cwd: REPO_ROOT,
645
- encoding: 'utf-8',
646
- stdio: ['ignore', 'pipe', 'pipe'],
696
+ // The branch is passed explicitly — `gh` inferring the PR from HEAD
697
+ // resolved to `main` mid-run once (2026-08-03, 9.16.0 resume) and the
698
+ // whole release died on "no pull requests found for branch main".
699
+ const proc = run(['gh', 'pr', 'checks', branch, '--watch'], {
700
+ check: false,
701
+ capture: true,
647
702
  });
648
- if (proc.error) {
649
- throw proc.error;
650
- }
651
703
  const output = ((proc.stdout || '') + (proc.stderr || '')).trim();
652
- const returncode = proc.status ?? 0;
704
+ const returncode = proc.returncode;
653
705
  if (returncode === 0) {
654
706
  if (output) {
655
707
  process.stdout.write(output + '\n');
@@ -667,10 +719,9 @@ function watch_pr_checks(): void {
667
719
  if (output) {
668
720
  process.stderr.write(output + '\n');
669
721
  }
670
- const summary = spawnSync('gh', ['pr', 'checks', '--json', 'name,bucket'], {
671
- cwd: REPO_ROOT,
672
- encoding: 'utf-8',
673
- stdio: ['ignore', 'pipe', 'pipe'],
722
+ const summary = run(['gh', 'pr', 'checks', branch, '--json', 'name,bucket'], {
723
+ check: false,
724
+ capture: true,
674
725
  });
675
726
  const report = _failed_checks_report(_failed_check_names(summary.stdout ?? ''));
676
727
  if (report) {
@@ -679,6 +730,139 @@ function watch_pr_checks(): void {
679
730
  die(`PR checks failed (exit ${returncode})`);
680
731
  }
681
732
 
733
+ /**
734
+ * Push the release branch, integrating a remote that moved under us.
735
+ *
736
+ * A GitHub-side "Update branch" click (or a concurrent resume run) advances
737
+ * `origin/<branch>` while the local ref stays behind; a bare `git push` is
738
+ * then rejected with "fetch first" and the whole release run dies (measured
739
+ * 2026-08-03, 9.15.0). Same-branch commits are never ours to drop, so the
740
+ * recovery is merge-and-retry, never force-push.
741
+ */
742
+ function push_release_branch(branch: string): void {
743
+ const first = run(['git', 'push', '-u', REMOTE, branch], { check: false });
744
+ if (first.returncode === 0) {
745
+ return;
746
+ }
747
+ process.stdout.write(`↻ ${REMOTE}/${branch} moved — integrating and retrying push\n`);
748
+ run(['git', 'fetch', REMOTE, branch]);
749
+ run(['git', 'merge', '--no-edit', `${REMOTE}/${branch}`]);
750
+ run(['git', 'push', '-u', REMOTE, branch]);
751
+ }
752
+
753
+ /** mergeStateStatus of the branch's PR ('' when the probe fails). */
754
+ function _pr_merge_state(branch: string): string {
755
+ const r = run(
756
+ ['gh', 'pr', 'view', branch, '--json', 'mergeStateStatus', '--jq', '.mergeStateStatus'],
757
+ {
758
+ check: false,
759
+ capture: true,
760
+ },
761
+ );
762
+ return r.returncode === 0 ? r.stdout.trim() : '';
763
+ }
764
+
765
+ /**
766
+ * Bound on update-and-retry rounds in merge_release_pr. Three is deliberate:
767
+ * main receiving unrelated merges three times inside one checks-wait window
768
+ * means the queue is hot and a human should decide, not a loop.
769
+ */
770
+ const _MERGE_UPDATE_ROUNDS = 3;
771
+
772
+ /**
773
+ * Merge the release PR, absorbing a moving base branch.
774
+ *
775
+ * Branch protection requires the head to be up to date with `main`, and
776
+ * `main` moves constantly (dependabot, unrelated merges) — so between the
777
+ * checks-wait and the merge the PR routinely flips to BEHIND and a bare
778
+ * `gh pr merge` dies with "the head branch is not up to date with the base
779
+ * branch" (measured 2026-08-03, 9.16.0). Recovery is mechanical and safe by
780
+ * construction: merging `origin/main` into `release/X.Y.Z` makes the fix
781
+ * files identical on both sides, so the release-PR diff stays inside the
782
+ * version-bump allowlist (docs/contracts/release-pr-gating.md § Mid-release
783
+ * fixes) — do it here instead of aborting the run.
784
+ */
785
+ /**
786
+ * Re-derive the release-PR body from the CHANGELOG section at the current
787
+ * release-branch head and push it to the PR (release-truth Phase 1: one
788
+ * final source — late commits and maintainer edits on the branch must reach
789
+ * the PR body instead of leaving it frozen at plan time). Best-effort sync
790
+ * of the local branch first; a no-op when the derived body is unchanged.
791
+ */
792
+ function _target_from_branch(branch: string): string {
793
+ return branch.replace(/^release\//u, '');
794
+ }
795
+
796
+ function _refresh_pr_body_from_head(branch: string, target: string): void {
797
+ run(['git', 'pull', '--ff-only', REMOTE, branch], { check: false });
798
+ const changelog = fs.readFileSync(CHANGELOG, 'utf-8');
799
+ const section = extract_changelog_section(changelog, target);
800
+ if (!section) {
801
+ die(`CHANGELOG.md at ${branch} head carries no section for ${target} — cannot derive PR body`);
802
+ }
803
+ const capped = _cap_body(section!.body, GH_PR_BODY_LIMIT - 200, '`CHANGELOG.md` in this PR');
804
+ const body = pr_body_from_section(capped, target);
805
+ const live = run(['gh', 'pr', 'view', branch, '--json', 'body', '-q', '.body'], {
806
+ check: false,
807
+ capture: true,
808
+ });
809
+ if (live.returncode === 0 && live.stdout.replace(/\r\n/gu, '\n').trim() === body.trim()) {
810
+ return;
811
+ }
812
+ run(['gh', 'pr', 'edit', branch, '--body', body]);
813
+ }
814
+
815
+ function merge_release_pr(branch: string, wait_for_checks: boolean): void {
816
+ if (git(['rev-parse', '--abbrev-ref', 'HEAD'], { capture: true }) !== branch) {
817
+ run(['git', 'checkout', branch]);
818
+ }
819
+ for (let round = 0; round <= _MERGE_UPDATE_ROUNDS; round++) {
820
+ // One final generation step at the final head: every merge attempt
821
+ // first re-derives the PR body from the branch-head CHANGELOG, so
822
+ // update rounds and maintainer edits can never desynchronize it.
823
+ _refresh_pr_body_from_head(branch, _target_from_branch(branch));
824
+ if (_pr_merge_state(branch) !== 'BEHIND') {
825
+ const merged = run(['gh', 'pr', 'merge', branch, '--merge', '--delete-branch'], {
826
+ check: false,
827
+ });
828
+ if (merged.returncode === 0) {
829
+ return;
830
+ }
831
+ if (_pr_merge_state(branch) !== 'BEHIND') {
832
+ // Not the moving-base failure — surface it unchanged.
833
+ throw new CalledProcessError(merged.returncode, [
834
+ 'gh',
835
+ 'pr',
836
+ 'merge',
837
+ branch,
838
+ '--merge',
839
+ '--delete-branch',
840
+ ]);
841
+ }
842
+ // BEHIND appeared between the probe and the merge — fall through
843
+ // to the update below.
844
+ }
845
+ if (round === _MERGE_UPDATE_ROUNDS) {
846
+ break;
847
+ }
848
+ process.stdout.write(
849
+ `↻ head behind ${MAIN_BRANCH} (update round ${round + 1}/${_MERGE_UPDATE_ROUNDS}) — ` +
850
+ `merging ${REMOTE}/${MAIN_BRANCH} into ${branch} and re-running checks\n`,
851
+ );
852
+ run(['git', 'fetch', REMOTE, MAIN_BRANCH]);
853
+ run(['git', 'merge', '--no-edit', `${REMOTE}/${MAIN_BRANCH}`]);
854
+ push_release_branch(branch);
855
+ if (wait_for_checks) {
856
+ watch_pr_checks(branch);
857
+ }
858
+ }
859
+ die(
860
+ `release PR is still behind ${MAIN_BRANCH} after ${_MERGE_UPDATE_ROUNDS} update round(s) — ` +
861
+ `${MAIN_BRANCH} is moving faster than the checks complete. ` +
862
+ 'Re-run when the queue quiets down: task release -- --resume --yes',
863
+ );
864
+ }
865
+
682
866
  function have(bin: string): boolean {
683
867
  const res = spawnSync('which', [bin], { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'] });
684
868
  if (res.error) {
@@ -1596,27 +1780,37 @@ function execute(
1596
1780
  _step(4, total, 'PR already merged — skip push');
1597
1781
  } else {
1598
1782
  // `git push -u` is naturally idempotent — it prints "Everything
1599
- // up-to-date" when remote already matches. No probe needed.
1783
+ // up-to-date" when remote already matches. push_release_branch
1784
+ // additionally absorbs a remote that moved under us.
1600
1785
  _step(4, total, `Push ${branch} to ${REMOTE}`);
1601
- run(['git', 'push', '-u', REMOTE, branch]);
1786
+ push_release_branch(branch);
1602
1787
  }
1603
1788
 
1604
1789
  // ─── 5. PR ──────────────────────────────────────────────────────────────
1605
1790
  if (pr_merged) {
1606
1791
  _step(5, total, `PR #${pr_info!['number']} already merged — skip`);
1607
1792
  } else if (resume && pr_state === 'OPEN') {
1608
- _step(5, total, `PR already open: ${pr_info!['url']}`);
1793
+ // One final source: a resumed run re-derives the body from the
1794
+ // branch-head CHANGELOG instead of leaving the plan-time snapshot —
1795
+ // this was the 9.14.0 test-count desync (resume re-rendered from a
1796
+ // later HEAD while the changelog kept the branch's entry).
1797
+ _step(5, total, `PR already open: ${pr_info!['url']} — refresh body from branch head`);
1798
+ _refresh_pr_body_from_head(branch, plan.target);
1609
1799
  } else {
1610
1800
  _step(5, total, 'Open pull request');
1801
+ // Derive the PR body from the CHANGELOG section step 2 just wrote —
1802
+ // the file is the single source; the in-memory plan is only its
1803
+ // author (release-truth Phase 1).
1804
+ const section = extract_changelog_section(fs.readFileSync(CHANGELOG, 'utf-8'), plan.target);
1805
+ if (!section) {
1806
+ die(`CHANGELOG.md carries no section for ${plan.target} — cannot derive PR body`);
1807
+ }
1611
1808
  const pr_changelog = _cap_body(
1612
- plan.changelog_body,
1809
+ section!.body,
1613
1810
  GH_PR_BODY_LIMIT - 200, // leave room for the prefix + footer
1614
1811
  '`CHANGELOG.md` in this PR',
1615
1812
  );
1616
- const pr_body =
1617
- `Release ${plan.target}.\n\n` +
1618
- `${pr_changelog}\n\n` +
1619
- 'Created by `scripts/release.py`.';
1813
+ const pr_body = pr_body_from_section(pr_changelog, plan.target);
1620
1814
  run([
1621
1815
  'gh',
1622
1816
  'pr',
@@ -1637,7 +1831,7 @@ function execute(
1637
1831
  _step(6, total, 'PR already merged — skip checks wait');
1638
1832
  } else if (wait_for_checks) {
1639
1833
  _step(6, total, 'Wait for PR checks');
1640
- watch_pr_checks();
1834
+ watch_pr_checks(branch);
1641
1835
  } else {
1642
1836
  _step(6, total, 'Skip waiting for checks (--no-wait)');
1643
1837
  }
@@ -1647,7 +1841,7 @@ function execute(
1647
1841
  _step(7, total, `PR #${pr_info!['number']} already merged — skip`);
1648
1842
  } else {
1649
1843
  _step(7, total, 'Merge pull request (merge commit) and delete branch');
1650
- run(['gh', 'pr', 'merge', '--merge', '--delete-branch']);
1844
+ merge_release_pr(branch, wait_for_checks);
1651
1845
  }
1652
1846
 
1653
1847
  // ─── 8. tag main + push tag ─────────────────────────────────────────────
@@ -1667,8 +1861,21 @@ function execute(
1667
1861
  run(['git', 'push', REMOTE, plan.target]);
1668
1862
  }
1669
1863
  } else {
1670
- _step(8, total, `Tag merge commit and push ${plan.target}`);
1671
- run(['git', 'tag', plan.target]);
1864
+ // Sequencing is load-bearing (release-truth Phase 1, council
1865
+ // 2026-08-03): merge FIRST (step 7), pull main (above), THEN derive
1866
+ // the tag message from the MERGED changelog — tagging before the
1867
+ // merge would read a section that does not exist yet. The annotated
1868
+ // tag replaces the previous lightweight one so tag metadata is a
1869
+ // fourth surface carrying the same single-source content.
1870
+ _step(8, total, `Tag merge commit (annotated, from merged CHANGELOG) and push ${plan.target}`);
1871
+ const merged = extract_changelog_section(fs.readFileSync(CHANGELOG, 'utf-8'), plan.target);
1872
+ if (!merged) {
1873
+ die(
1874
+ `CHANGELOG.md on ${MAIN_BRANCH} carries no section for ${plan.target} — ` +
1875
+ 'refusing to tag a release whose changelog entry is missing',
1876
+ );
1877
+ }
1878
+ run(['git', 'tag', '-a', plan.target, '-m', tag_message_from_section(merged!.body, plan.target)]);
1672
1879
  run(['git', 'push', REMOTE, plan.target]);
1673
1880
  }
1674
1881
 
@@ -1683,8 +1890,19 @@ function execute(
1683
1890
  ? 'Create GitHub Release (tag push under GITHUB_TOKEN triggers nothing — dispatching next)'
1684
1891
  : 'Create GitHub Release (triggers publish-npm on the tag)',
1685
1892
  );
1893
+ // One final source: the notes come from the CHANGELOG as recorded AT
1894
+ // THE TAG — never from the plan-time render (whose scope can differ
1895
+ // from the merged content; that was the 9.14.0 desync).
1896
+ const tagged_changelog = git(['show', `${plan.target}:CHANGELOG.md`], { capture: true });
1897
+ const tagged_section = extract_changelog_section(tagged_changelog, plan.target);
1898
+ if (!tagged_section) {
1899
+ die(
1900
+ `CHANGELOG.md at tag ${plan.target} carries no section for ${plan.target} — ` +
1901
+ 'refusing to publish release notes from a different source',
1902
+ );
1903
+ }
1686
1904
  const notes = _cap_body(
1687
- plan.changelog_body || `Release ${plan.target}`,
1905
+ release_notes_from_section(tagged_section!.body, plan.target),
1688
1906
  GH_RELEASE_NOTES_LIMIT,
1689
1907
  '`CHANGELOG.md`',
1690
1908
  );
@@ -2260,5 +2478,14 @@ export {
2260
2478
  SEMVER_RE,
2261
2479
  _RELEASE_BRANCH_RE,
2262
2480
  MODULE_DOC_FIRST_LINE,
2481
+ // Drill seam (release_drill.ts) — the step machinery under a simulated
2482
+ // git/gh world, so orchestration bugs surface in vitest, not mid-release.
2483
+ execute,
2484
+ _set_exec_override,
2485
+ push_release_branch,
2486
+ merge_release_pr,
2487
+ watch_pr_checks,
2488
+ _pr_merge_state,
2489
+ _MERGE_UPDATE_ROUNDS,
2263
2490
  };
2264
2491
  export type { Args };