@event4u/agent-config 14.14.1 → 14.15.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.
@@ -303,6 +303,35 @@ function set_marketplace_version(p: string, version: string): void {
303
303
  fs.writeFileSync(p, jsonDumpsIndent(data, 2) + '\n', 'utf-8');
304
304
  }
305
305
 
306
+ /**
307
+ * Mirror of Python `str.splitlines()` — split on CR/LF/CRLF and drop a single
308
+ * trailing empty element.
309
+ *
310
+ * Lives here rather than in `release.ts` because that file is 2000+ lines and
311
+ * every line above 1500 is charged by `check_source_size_budget`, while this
312
+ * module is a third of the cap. Behaviour unchanged; it is a pure text helper
313
+ * beside the other pure text helpers in this module.
314
+ *
315
+ * Known duplication, recorded rather than fixed here: `adoption_report.ts:106`
316
+ * and `readme_linter.ts:418` carry their own copies. Collapsing all three onto
317
+ * one definition touches three modules and is not this change's subject.
318
+ */
319
+ function _splitlines(text: string): string[] {
320
+ if (text === '') {
321
+ return [];
322
+ }
323
+ const parts = text.split(/\r\n|\r|\n/);
324
+ if (parts.length > 0 && parts[parts.length - 1] === '') {
325
+ parts.pop();
326
+ }
327
+ return parts;
328
+ }
329
+
330
+ /** Mirror of Python `str.rstrip()` (trailing whitespace). */
331
+ function _rstrip(text: string): string {
332
+ return text.replace(/\s+$/u, '');
333
+ }
334
+
306
335
  export {
307
336
  ArgparseExit,
308
337
  commaGroup,
@@ -330,4 +359,6 @@ export {
330
359
  REPO_ROOT,
331
360
  REPO_SLUG,
332
361
  SystemExitError,
362
+ _rstrip,
363
+ _splitlines,
333
364
  };
@@ -16,7 +16,7 @@
16
16
  * import path changes.
17
17
  */
18
18
  import { spawnSync } from 'node:child_process';
19
- import { publication_blockers } from './_lib/release_highlights.js';
19
+ import { DERIVED_MARKER, publication_blockers } from './_lib/release_highlights.js';
20
20
  import * as fs from 'node:fs';
21
21
  import process from 'node:process';
22
22
 
@@ -479,6 +479,72 @@ export function guard_release_branch_push(branch: string): void {
479
479
  );
480
480
  }
481
481
 
482
+ /**
483
+ * Ask for the curation BEFORE the pipeline commits anything — the fifth guard
484
+ * site, and the one that closes the gap the other four only narrowed.
485
+ *
486
+ * `guard_release_branch_push` already refuses before any REMOTE state exists,
487
+ * which is why 14.14.1 cost nothing but a re-run. What it cannot do is refuse
488
+ * before LOCAL state exists: by the time the push is attempted, step 2 has
489
+ * bumped six files and regenerated the derived trees and step 3 has committed
490
+ * all of it as `release: X.Y.Z`. The operator then edits prose on top of a
491
+ * commit that already claims to be the release and continues with `--resume`.
492
+ *
493
+ * The sentence this fixes is in that guard's own docstring: the 2026-09-01
494
+ * flip's risk register named *"the releaser edits prose before merge"* as the
495
+ * mitigation, and *"nothing in the pipeline asked, so nothing did"*. Refusing
496
+ * is not asking. This is the ask, placed where the answer is cheapest: the
497
+ * changelog section has just been written, nothing is committed, and the fix is
498
+ * an edit to a file already open in the working tree.
499
+ *
500
+ * It names the labels rather than only the marker, because the operator's next
501
+ * action is to rewrite specific lines and hunting for them in a 30-line section
502
+ * is the friction that made the refusal feel like a failure rather than a step.
503
+ *
504
+ * Deliberately silent when the section does not exist — same reasoning as the
505
+ * push guard: a missing section is the publication sites' refusal to make, not
506
+ * this one's.
507
+ *
508
+ * `prMerged` is a parameter rather than a check at the call site, and it is
509
+ * load-bearing in both directions. On the merged path steps 2 and 3 do not run,
510
+ * so there is no pre-commit moment to ask about; firing anyway PREEMPTS the
511
+ * step-8 and step-9 guards, whose whole subject is the merged section. That is
512
+ * measured, not anticipated — the unconditional form red three `release_drill`
513
+ * scenarios that exist to pin exactly those later refusals. It lives here
514
+ * rather than in an `if` at the call site because `release.ts` is 2000+ lines
515
+ * and every line above 1500 is charged by the growth ratchet, while this module
516
+ * is far below it.
517
+ */
518
+ export function guard_release_curation(version: string, prMerged = false): void {
519
+ if (prMerged) {
520
+ return;
521
+ }
522
+ const section = extract_changelog_section(read_changelog_text(), version);
523
+ if (!section) {
524
+ return;
525
+ }
526
+ const blockers = publication_blockers(section.body, version, `\`release/${version}\``);
527
+ if (blockers.length === 0) {
528
+ return;
529
+ }
530
+ const marked = section.body
531
+ .split('\n')
532
+ .filter((l) => l.includes(DERIVED_MARKER))
533
+ .map((l) => ` ${l.trim()}`);
534
+ die(
535
+ `the ${version} release highlights are still the generator's draft ` +
536
+ `(${String(blockers.length)} blocker(s)):\n` +
537
+ blockers.map((b) => ` - ${b}`).join('\n') +
538
+ (marked.length > 0
539
+ ? `\n Lines to rewrite in \`CHANGELOG.md\` under \`## [${version}]\`:\n` +
540
+ marked.join('\n')
541
+ : '') +
542
+ '\n Stopped BEFORE committing — no release commit, no branch on ' +
543
+ `${REMOTE}, no pull request, no tag. Curate the ` +
544
+ '`### Release highlights` head, then re-run `task release`.',
545
+ );
546
+ }
547
+
482
548
  function push_release_branch(branch: string): void {
483
549
  guard_release_branch_push(branch);
484
550
  const first = run(['git', 'push', '-u', REMOTE, branch], { check: false, capture: true });