@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.
- package/.augment-plugin/marketplace.json +3 -3
- package/.augment-plugin/plugin.json +1 -1
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +58 -0
- package/dist/agent-src/rules/security-sensitive-stop.md +7 -5
- package/dist/agent-src/skills/accessibility-auditor/SKILL.md +20 -2
- package/dist/agent-src/skills/design-intelligence/data/ux-guidelines.csv +99 -99
- package/dist/agent-src/skills/fe-design/SKILL.md +6 -3
- package/dist/agent-src/skills/fe-design/references/design-patterns.md +28 -0
- package/dist/agent-src/skills/iconography/SKILL.md +37 -6
- package/dist/agent-src/skills/react-shadcn-ui/SKILL.md +1 -1
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/cli-delegate/cmd_conformance.js +158 -33
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +7 -7
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +1 -1
- package/dist/discovery/dormancy-report.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +2 -2
- package/dist/discovery/trust-report.md +1 -1
- package/dist/discovery/workspaces.json +2 -2
- package/dist/hooks/dispatch.js +169 -41
- package/dist/mcp/registry-manifest.json +2 -2
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +160 -35
- package/dist/router.json +1 -1
- package/docs/guidelines/design-antipatterns.md +10 -2
- package/package.json +1 -1
- package/src/config/gate-violation-baselines.json +2 -2
- package/src/scripts/design_slop_rules.ts +19 -9
- package/src/scripts/git_authorization_hook.ts +257 -35
- package/src/scripts/hooks/block_unauthorized_git.ts +16 -4
- package/src/scripts/hooks/merge_impact.ts +68 -0
- package/src/scripts/release.ts +4 -17
- package/src/scripts/release_drill.ts +154 -1
- 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;
|