@event4u/agent-config 14.15.0 → 14.16.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 (37) hide show
  1. package/.augment-plugin/marketplace.json +3 -3
  2. package/.augment-plugin/plugin.json +1 -1
  3. package/.claude-plugin/marketplace.json +1 -1
  4. package/CHANGELOG.md +58 -0
  5. package/dist/agent-src/rules/security-sensitive-stop.md +7 -5
  6. package/dist/agent-src/skills/accessibility-auditor/SKILL.md +20 -2
  7. package/dist/agent-src/skills/design-intelligence/data/ux-guidelines.csv +99 -99
  8. package/dist/agent-src/skills/fe-design/SKILL.md +6 -3
  9. package/dist/agent-src/skills/fe-design/references/design-patterns.md +28 -0
  10. package/dist/agent-src/skills/iconography/SKILL.md +37 -6
  11. package/dist/agent-src/skills/react-shadcn-ui/SKILL.md +1 -1
  12. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  13. package/dist/cli-delegate/cmd_conformance.js +158 -33
  14. package/dist/discovery/deprecation-report.md +1 -1
  15. package/dist/discovery/discovery-manifest.json +7 -7
  16. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  17. package/dist/discovery/discovery-manifest.summary.md +1 -1
  18. package/dist/discovery/dormancy-report.md +1 -1
  19. package/dist/discovery/orphan-report.md +1 -1
  20. package/dist/discovery/packs.json +2 -2
  21. package/dist/discovery/trust-report.md +1 -1
  22. package/dist/discovery/workspaces.json +2 -2
  23. package/dist/hooks/dispatch.js +169 -41
  24. package/dist/mcp/registry-manifest.json +2 -2
  25. package/dist/mcp/server.json +2 -2
  26. package/dist/mcp/server.mjs +160 -35
  27. package/dist/router.json +1 -1
  28. package/docs/guidelines/design-antipatterns.md +10 -2
  29. package/package.json +1 -1
  30. package/src/config/gate-violation-baselines.json +2 -2
  31. package/src/scripts/design_slop_rules.ts +19 -9
  32. package/src/scripts/git_authorization_hook.ts +257 -35
  33. package/src/scripts/hooks/block_unauthorized_git.ts +16 -4
  34. package/src/scripts/hooks/merge_impact.ts +68 -0
  35. package/src/scripts/release.ts +4 -17
  36. package/src/scripts/release_drill.ts +154 -1
  37. package/src/scripts/release_publication.ts +91 -0
@@ -38,6 +38,58 @@ import {
38
38
  SystemExitError,
39
39
  } from './release.js';
40
40
  import { DERIVED_MARKER } from './_lib/release_highlights.js';
41
+ import {
42
+ AUGMENT_MARKETPLACE_JSON,
43
+ AUGMENT_PLUGIN_JSON,
44
+ CHANGELOG,
45
+ MARKETPLACE_JSON,
46
+ PACKAGE_JSON,
47
+ PACKAGE_LOCK_JSON,
48
+ PROJECT_TEMPLATE,
49
+ } from './release_env.js';
50
+
51
+ /**
52
+ * The files step 2 mutates on disk, snapshotted around every scenario.
53
+ *
54
+ * The FakeWorld intercepts COMMANDS, not `fs` writes, and step 2's bump is
55
+ * seven direct writes. Every scenario before 2026-09-03 passed `--resume` with
56
+ * `package.json` already at the target, so step 2 short-circuited and nothing
57
+ * was ever written — the drill looked side-effect-free because no scenario had
58
+ * reached the writing arm. The first `resume: false` scenario appended twelve
59
+ * `## 14.15.0` headings to the REAL `CHANGELOG.md`.
60
+ *
61
+ * Snapshot-and-restore rather than a writer seam: a seam means threading an
62
+ * injection point through seven call sites in `release.ts`, which sits past the
63
+ * 1500-line cap where every line is charged. This is contained to the runner
64
+ * and covers any future scenario without further thought.
65
+ */
66
+ const _MUTATED_BY_STEP_2: readonly string[] = [
67
+ CHANGELOG,
68
+ PACKAGE_JSON,
69
+ PACKAGE_LOCK_JSON,
70
+ MARKETPLACE_JSON,
71
+ AUGMENT_PLUGIN_JSON,
72
+ AUGMENT_MARKETPLACE_JSON,
73
+ PROJECT_TEMPLATE,
74
+ ];
75
+
76
+ function _snapshot_step_2_files(): Map<string, string> {
77
+ const snap = new Map<string, string>();
78
+ for (const f of _MUTATED_BY_STEP_2) {
79
+ if (fs.existsSync(f)) {
80
+ snap.set(f, fs.readFileSync(f, 'utf-8'));
81
+ }
82
+ }
83
+ return snap;
84
+ }
85
+
86
+ function _restore_step_2_files(snap: ReadonlyMap<string, string>): void {
87
+ for (const [f, content] of snap) {
88
+ if (fs.readFileSync(f, 'utf-8') !== content) {
89
+ fs.writeFileSync(f, content);
90
+ }
91
+ }
92
+ }
41
93
 
42
94
  const _HERE = fileURLToPath(import.meta.url);
43
95
  const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
@@ -64,6 +116,27 @@ interface WorldConfig {
64
116
  push_rejections?: number;
65
117
  /** mergeStateStatus reports BEHIND this many probes in a row (9.16.0). */
66
118
  behind_probes?: number;
119
+ /**
120
+ * Commits the release branch is behind `origin/main` at step 1.
121
+ *
122
+ * Distinct from `behind_probes`, which fakes the PR's own
123
+ * `mergeStateStatus` at step 6. This one fakes `git rev-list --count
124
+ * HEAD..origin/main`, the probe `integrate_main_if_behind` uses — the
125
+ * defect it covers surfaced at the PUSH, six steps before that PR ever
126
+ * existed.
127
+ */
128
+ branch_behind_main?: number;
129
+ /**
130
+ * Whether `execute()` is invoked with `--resume`. Defaults to TRUE so every
131
+ * pre-existing scenario keeps the shape it was written against.
132
+ *
133
+ * Configurable since 2026-09-03 because the drill could not otherwise
134
+ * reach the defect: branch reuse used to be gated on `resume`, so a plain
135
+ * `task release` over an existing branch died at `git checkout -b` with
136
+ * exit 128 — and every scenario here passed `resume: true`, which is
137
+ * exactly the arm that worked.
138
+ */
139
+ resume?: boolean;
67
140
  /** `gh pr merge` fails once with the not-up-to-date error (the race). */
68
141
  merge_races_once?: boolean;
69
142
  /** `gh pr merge` fails hard with an unrelated error (must surface). */
@@ -199,6 +272,7 @@ class FakeWorld {
199
272
 
200
273
  private push_rejections: number;
201
274
  private behind_probes: number;
275
+ private branch_behind_main: number;
202
276
  private merge_races_once: boolean;
203
277
  private merge_fails_hard: boolean;
204
278
  private checks_fail: boolean;
@@ -217,6 +291,7 @@ class FakeWorld {
217
291
  this.tag_local = (cfg.tag_created_unpushed ?? false) || (cfg.tag_already_remote ?? false);
218
292
  this.tag_remote = cfg.tag_already_remote ?? false;
219
293
  this.behind_probes = cfg.behind_probes ?? 0;
294
+ this.branch_behind_main = cfg.branch_behind_main ?? 0;
220
295
  this.merge_races_once = cfg.merge_races_once ?? false;
221
296
  this.merge_fails_hard = cfg.merge_fails_hard ?? false;
222
297
  this.checks_fail = cfg.checks_fail ?? false;
@@ -235,6 +310,15 @@ class FakeWorld {
235
310
  this.head = args[2] === '-b' ? (args[3] as string) : (args[2] as string);
236
311
  return OK;
237
312
  }
313
+ if (cmd === 'git rev-list --count HEAD..origin/main') {
314
+ return { ...OK, stdout: `${String(this.branch_behind_main)}\n` };
315
+ }
316
+ if (cmd === 'git merge origin/main --no-edit') {
317
+ // A real merge closes the gap, so a second probe reads 0. Without
318
+ // this the scenario could not tell one merge from a loop.
319
+ this.branch_behind_main = 0;
320
+ return OK;
321
+ }
238
322
  if (cmd === `git rev-parse --verify --quiet refs/heads/${this.branch}`) {
239
323
  return this.pr_merged ? { ...OK, status: 1 } : OK;
240
324
  }
@@ -345,6 +429,15 @@ class FakeWorld {
345
429
  // live body before deciding whether to edit.
346
430
  return { ...OK, stdout: this.pr_body };
347
431
  }
432
+ if (args[0] === 'gh' && args[1] === 'pr' && args[2] === 'create') {
433
+ // Only reachable on the NON-resume path: a resumed run refreshes an
434
+ // existing PR body instead (step 5's other arm), which is why every
435
+ // scenario before 2026-09-03 could leave this unscripted.
436
+ const bodyIdx = args.indexOf('--body');
437
+ this.pr_body = bodyIdx >= 0 ? String(args[bodyIdx + 1] ?? '') : '';
438
+ this.branch_exists_remote = true;
439
+ return { ...OK, stdout: `https://github.com/event4u-app/agent-config/pull/999\n` };
440
+ }
348
441
  if (args[0] === 'gh' && args[1] === 'pr' && args[2] === 'edit' && args[3] === this.branch) {
349
442
  const bodyIdx = args.indexOf('--body');
350
443
  this.pr_body = bodyIdx >= 0 ? String(args[bodyIdx + 1] ?? '') : this.pr_body;
@@ -390,6 +483,64 @@ function _count(world: FakeWorld, needle: string): number {
390
483
  }
391
484
 
392
485
  const SCENARIOS: Record<string, Scenario> = {
486
+ 'plain-run-reuses-an-existing-branch': {
487
+ summary:
488
+ 'step 1: a PLAIN run (no --resume) over an existing release branch checks it out instead of dying at `git checkout -b`',
489
+ // The defect, measured on 14.15.0: branch reuse read
490
+ // `resume && _branch_exists_local(branch)`, so without --resume the
491
+ // run fell through to the create arm and died with exit 128. That is
492
+ // precisely the state `guard_release_curation` leaves behind — it stops
493
+ // BEFORE the commit, on a branch step 1 already created — while its own
494
+ // message says to re-run `task release`. The code refused the remedy its
495
+ // own message prescribed.
496
+ config: { resume: false },
497
+ expect_success: true,
498
+ verify: (w) => {
499
+ const f: string[] = [];
500
+ _expect(
501
+ w.calls.includes(`git checkout ${w.branch}`),
502
+ 'the existing branch was not checked out',
503
+ f,
504
+ );
505
+ _expect(
506
+ !w.calls.includes(`git checkout -b ${w.branch}`),
507
+ 'took the create arm over an existing branch — this is the exit-128 path',
508
+ f,
509
+ );
510
+ return f;
511
+ },
512
+ },
513
+ 'stale-branch-merges-main-at-step-1': {
514
+ summary:
515
+ 'step 1: a branch behind origin/main merges it in there — NOT at the push, where the preflight used to be the first to notice',
516
+ // Second half of the same defect. On 14.15.0 the branch had been cut
517
+ // from an older main during an earlier aborted run; steps 2 and 3
518
+ // bumped and committed, and only the pre-push preflight said `branch is
519
+ // BEHIND origin/main`. The cheapest moment to integrate main is the
520
+ // moment the branch is checked out.
521
+ config: { resume: false, branch_behind_main: 3 },
522
+ expect_success: true,
523
+ verify: (w) => {
524
+ const f: string[] = [];
525
+ const merge = w.calls.indexOf('git merge origin/main --no-edit');
526
+ const push = w.calls.indexOf(`git push -u origin ${w.branch}`);
527
+ _expect(merge >= 0, 'main was never merged into the stale branch', f);
528
+ _expect(push >= 0, 'the branch was never pushed', f);
529
+ // Ordering is the whole point: a merge AFTER the push would mean the
530
+ // preflight had already refused.
531
+ _expect(
532
+ merge >= 0 && push >= 0 && merge < push,
533
+ 'main was merged after the push, not before it',
534
+ f,
535
+ );
536
+ _expect(
537
+ _count(w, 'git merge origin/main --no-edit') === 1,
538
+ 'merged main more than once — the probe is not being re-read after the merge',
539
+ f,
540
+ );
541
+ return f;
542
+ },
543
+ },
393
544
  'marker-refuses-before-commit': {
394
545
  summary:
395
546
  'step 3: a draft marker refuses BEFORE the release commit — no local release state, and therefore no remote state either',
@@ -685,12 +836,13 @@ function run_scenario(name: string): ScenarioOutcome {
685
836
  captured.push(String(s));
686
837
  return true;
687
838
  };
839
+ const tree_snapshot = _snapshot_step_2_files();
688
840
  _set_exec_override((args) => world.exec(args));
689
841
  // The working-tree read, faked for the same reason the command layer is.
690
842
  // Without this the drill's step 8 reads the repository's real CHANGELOG.md.
691
843
  _set_changelog_reader(() => world.changelog_file);
692
844
  try {
693
- execute(plan, { wait_for_checks: true, dry_run: false, resume: true });
845
+ execute(plan, { wait_for_checks: true, dry_run: false, resume: scenario.config.resume ?? true });
694
846
  } catch (err) {
695
847
  if (err instanceof SystemExitError) {
696
848
  error = `SystemExit(${err.code}): ${captured.join('')}`;
@@ -702,6 +854,7 @@ function run_scenario(name: string): ScenarioOutcome {
702
854
  _set_changelog_reader(null);
703
855
  process.stdout.write = orig_out;
704
856
  process.stderr.write = orig_err;
857
+ _restore_step_2_files(tree_snapshot);
705
858
  }
706
859
 
707
860
  const failures: string[] = [];
@@ -515,6 +515,97 @@ export function guard_release_branch_push(branch: string): void {
515
515
  * and every line above 1500 is charged by the growth ratchet, while this module
516
516
  * is far below it.
517
517
  */
518
+ /**
519
+ * Own the whole step-1 branch decision: reuse an existing release branch, and
520
+ * make sure whatever we end up on carries current `main`.
521
+ *
522
+ * Three defects it closes, all measured on 14.15.0 (2026-09-03) in one sitting,
523
+ * and all the same underlying mistake — a release branch that is not on current
524
+ * `main`, discovered later than it could have been.
525
+ *
526
+ * 1. **Reuse was gated on `--resume`.** The two "branch exists" arms read
527
+ * `resume && _branch_exists_local(branch)`, so a PLAIN `task release` over an
528
+ * existing branch fell through to `git checkout -b` and died with exit 128.
529
+ * That is exactly the state `guard_release_curation` leaves behind — it stops
530
+ * before the commit, and the branch it stops on already exists — while its
531
+ * own message says to re-run `task release`. The message was right about
532
+ * what to do and the code refused to do it. Reuse no longer consults
533
+ * `resume`: an existing release branch is a fact about the repository, not
534
+ * about the flag.
535
+ * 2. **A branch cut from an older `main` failed at the PUSH.** The pre-push
536
+ * preflight reported `branch is BEHIND origin/main` after steps 2 and 3 had
537
+ * already bumped and committed — the cheapest moment to integrate `main` is
538
+ * the moment we check the branch out, not eight steps later.
539
+ * 3. **A fresh branch was cut from whatever local `main` happened to be.** The
540
+ * create arm never pulled, so a stale local `main` produced a stale release
541
+ * branch and defect 2 followed on the next run. Fixed on the same terms
542
+ * rather than left as the half that still bites: leaving it would reproduce
543
+ * the "fix that does not fix it" pattern the other two came from.
544
+ *
545
+ * `say` is a callback rather than a direct write because the `[N/total]` step
546
+ * prefix belongs to the orchestrator, which knows the total; this module does
547
+ * not. Keeping the printing there and the decision here is also what lets
548
+ * `release.ts` shed twenty lines — it sits ~500 lines past the 1500-line cap,
549
+ * where every line is charged by the growth ratchet, and this module is under
550
+ * it.
551
+ *
552
+ * The merge is `git merge`, never a rebase and never a force: a release branch
553
+ * can already carry a pushed commit and somebody else's merge, and neither is
554
+ * ours to rewrite.
555
+ */
556
+ export function checkout_release_branch(
557
+ branch: string,
558
+ prMerged: boolean,
559
+ say: (msg: string) => void,
560
+ ): void {
561
+ if (prMerged) {
562
+ say(`PR for ${branch} already merged — staying on ${MAIN_BRANCH}`);
563
+ if (git(['rev-parse', '--abbrev-ref', 'HEAD'], { capture: true }) !== MAIN_BRANCH) {
564
+ run(['git', 'checkout', MAIN_BRANCH]);
565
+ }
566
+ run(['git', 'pull', '--ff-only', REMOTE, MAIN_BRANCH]);
567
+ return;
568
+ }
569
+ if (_branch_exists_local(branch)) {
570
+ say(`Branch ${branch} exists locally — checkout`);
571
+ run(['git', 'checkout', branch]);
572
+ } else if (_branch_exists_remote(branch)) {
573
+ say(`Branch ${branch} exists on ${REMOTE} — fetch + checkout`);
574
+ run(['git', 'fetch', REMOTE, branch]);
575
+ run(['git', 'checkout', '-b', branch, `${REMOTE}/${branch}`]);
576
+ } else {
577
+ // Cut from CURRENT main, not from whatever the local ref happens to be.
578
+ say(`Create branch ${branch} from current ${MAIN_BRANCH}`);
579
+ if (git(['rev-parse', '--abbrev-ref', 'HEAD'], { capture: true }) === MAIN_BRANCH) {
580
+ run(['git', 'pull', '--ff-only', REMOTE, MAIN_BRANCH]);
581
+ }
582
+ run(['git', 'checkout', '-b', branch]);
583
+ return;
584
+ }
585
+ integrate_main_if_behind(branch, say);
586
+ }
587
+
588
+ /**
589
+ * Merge `origin/<default>` into the checked-out release branch when it is
590
+ * behind, so the pre-push preflight is never the first place that surfaces.
591
+ *
592
+ * Silent when the branch is already current — a release run should not print a
593
+ * merge it did not make. Counting with `rev-list --count` against the fetched
594
+ * remote ref rather than the local one: the local `main` is exactly what went
595
+ * stale in the case this exists for.
596
+ */
597
+ export function integrate_main_if_behind(branch: string, say: (msg: string) => void): void {
598
+ run(['git', 'fetch', REMOTE, MAIN_BRANCH], { check: false });
599
+ const behind = git(['rev-list', '--count', `HEAD..${REMOTE}/${MAIN_BRANCH}`], {
600
+ capture: true,
601
+ }).trim();
602
+ if (behind === '' || behind === '0') {
603
+ return;
604
+ }
605
+ say(`Branch ${branch} is ${behind} commit(s) behind ${REMOTE}/${MAIN_BRANCH} — merging it in`);
606
+ run(['git', 'merge', `${REMOTE}/${MAIN_BRANCH}`, '--no-edit']);
607
+ }
608
+
518
609
  export function guard_release_curation(version: string, prMerged = false): void {
519
610
  if (prMerged) {
520
611
  return;