@event4u/agent-config 9.16.0 → 9.18.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/.claude-plugin/marketplace.json +1 -1
- package/AGENTS.md +1 -1
- package/CHANGELOG.md +80 -130
- package/README.md +1 -1
- package/dist/agent-src/commands/agent-handoff.md +100 -48
- package/dist/agent-src/commands/challenge-me/vision.md +5 -0
- package/dist/agent-src/commands/commit.md +1 -1
- package/dist/agent-src/commands/condense.md +19 -64
- package/dist/agent-src/commands/evals/agent-handoff.json +13 -1
- package/dist/agent-src/commands/evals/rule-compliance-audit.json +46 -0
- package/dist/agent-src/commands/feature/plan.md +20 -0
- package/dist/agent-src/commands/feature/roadmap.md +19 -0
- package/dist/agent-src/commands/memory/promote.md +14 -1
- package/dist/agent-src/commands/mode.md +9 -5
- package/dist/agent-src/commands/optimize/deep.md +72 -11
- package/dist/agent-src/commands/pr/create.md +29 -0
- package/dist/agent-src/commands/roadmap/create.md +29 -0
- package/dist/agent-src/commands/rule-compliance-audit.md +28 -5
- package/dist/agent-src/contexts/authority/commit-mechanics.md +5 -5
- package/dist/agent-src/contexts/communication/rules-auto/guidelines-mechanics.md +1 -0
- package/dist/agent-src/contexts/communication/rules-auto/skill-quality-mechanics.md +1 -1
- package/dist/agent-src/contexts/communication/rules-auto/think-before-action-mechanics.md +6 -1
- package/dist/agent-src/contexts/communication/rules-auto/token-efficiency-mechanics.md +4 -1
- package/dist/agent-src/contexts/execution/autonomy-mechanics.md +27 -1
- package/dist/agent-src/contexts/execution/plan-confidence-gate.md +133 -0
- package/dist/agent-src/contexts/execution/roadmap-process-loop.md +5 -2
- package/dist/agent-src/profiles/agency.yml +3 -3
- package/dist/agent-src/profiles/content_creator.yml +2 -2
- package/dist/agent-src/profiles/developer.yml +2 -2
- package/dist/agent-src/profiles/finance.yml +1 -1
- package/dist/agent-src/profiles/founder.yml +2 -2
- package/dist/agent-src/profiles/ops.yml +2 -2
- package/dist/agent-src/rules/active-remediation.md +2 -0
- package/dist/agent-src/rules/architecture.md +4 -2
- package/dist/agent-src/rules/augment-edit-discipline.md +2 -0
- package/dist/agent-src/rules/autonomous-execution.md +1 -1
- package/dist/agent-src/rules/brand-consistency.md +10 -62
- package/dist/agent-src/rules/brand-source-of-truth.md +62 -11
- package/dist/agent-src/rules/broken-access-control.md +4 -0
- package/dist/agent-src/rules/cli-output-handling.md +5 -0
- package/dist/agent-src/rules/code-comment-discipline.md +3 -0
- package/dist/agent-src/rules/commit-conventions.md +2 -0
- package/dist/agent-src/rules/context-hygiene.md +3 -2
- package/dist/agent-src/rules/cross-source-consistency.md +3 -0
- package/dist/agent-src/rules/design-fidelity.md +2 -0
- package/dist/agent-src/rules/doc-screenshot-hygiene.md +2 -0
- package/dist/agent-src/rules/docker-commands.md +4 -0
- package/dist/agent-src/rules/domain-adoption-policy.md +2 -0
- package/dist/agent-src/rules/domain-safety-disclaimer.md +5 -2
- package/dist/agent-src/rules/domain-safety-pii.md +1 -1
- package/dist/agent-src/rules/engineering-safety-floor.md +2 -0
- package/dist/agent-src/rules/framework-neutrality-in-generic-skills.md +9 -0
- package/dist/agent-src/rules/image-likeness-and-rights.md +4 -0
- package/dist/agent-src/rules/improve-before-implement.md +4 -0
- package/dist/agent-src/rules/laravel-routing.md +4 -0
- package/dist/agent-src/rules/legal-safety-floor.md +3 -0
- package/dist/agent-src/rules/lethal-trifecta-guard.md +3 -0
- package/dist/agent-src/rules/media-governance-routing.md +5 -0
- package/dist/agent-src/rules/minimal-safe-diff.md +2 -0
- package/dist/agent-src/rules/no-roadmap-references.md +2 -0
- package/dist/agent-src/rules/notes-first-reasoning.md +2 -0
- package/dist/agent-src/rules/output-discipline.md +9 -2
- package/dist/agent-src/rules/php-coding.md +2 -0
- package/dist/agent-src/rules/prefer-enums-over-literals.md +3 -0
- package/dist/agent-src/rules/provider-lifecycle-discipline.md +5 -0
- package/dist/agent-src/rules/roadmap-ci-steps-policy.md +2 -0
- package/dist/agent-src/rules/roadmap-progress-sync.md +2 -0
- package/dist/agent-src/rules/rule-type-governance.md +2 -0
- package/dist/agent-src/rules/scale-discipline.md +3 -0
- package/dist/agent-src/rules/secret-vcs-guard.md +2 -0
- package/dist/agent-src/rules/security-sensitive-stop.md +9 -2
- package/dist/agent-src/rules/senior-engineering-discipline.md +6 -0
- package/dist/agent-src/rules/skill-quality.md +2 -0
- package/dist/agent-src/rules/source-confidentiality.md +4 -0
- package/dist/agent-src/rules/source-discovery-gate.md +3 -0
- package/dist/agent-src/rules/source-of-truth.md +2 -0
- package/dist/agent-src/rules/strategy-safety-floor.md +2 -0
- package/dist/agent-src/rules/think-before-action.md +5 -0
- package/dist/agent-src/rules/token-budget-discipline.md +2 -2
- package/dist/agent-src/rules/ui-audit-gate.md +2 -0
- package/dist/agent-src/rules/user-interaction.md +3 -4
- package/dist/agent-src/skills/build-buy-partner/SKILL.md +1 -1
- package/dist/agent-src/skills/copilot-agents-optimization/SKILL.md +4 -4
- package/dist/agent-src/skills/copilot-config/SKILL.md +1 -1
- package/dist/agent-src/skills/deep-reading-analyst/SKILL.md +7 -4
- package/dist/agent-src/skills/description-assist/SKILL.md +1 -1
- package/dist/agent-src/skills/design-intelligence/SKILL.md +6 -180
- package/dist/agent-src/skills/design-intelligence/references/context-and-registers.md +100 -0
- package/dist/agent-src/skills/design-intelligence/references/integration-mapping.md +91 -0
- package/dist/agent-src/skills/design-review/SKILL.md +4 -141
- package/dist/agent-src/skills/design-review/references/review-communication.md +92 -0
- package/dist/agent-src/skills/design-review/references/verification-automation.md +61 -0
- package/dist/agent-src/skills/existing-ui-audit/SKILL.md +8 -27
- package/dist/agent-src/skills/existing-ui-audit/references/anti-slop-cross-reference.md +31 -0
- package/dist/agent-src/skills/fe-design/SKILL.md +4 -289
- package/dist/agent-src/skills/fe-design/references/design-patterns.md +227 -0
- package/dist/agent-src/skills/fe-design/references/design-read-and-memory.md +75 -0
- package/dist/agent-src/skills/market-entry-analysis/SKILL.md +1 -1
- package/dist/agent-src/skills/persona-writing/SKILL.md +2 -2
- package/dist/agent-src/skills/readme-reviewer/SKILL.md +2 -1
- package/dist/agent-src/skills/roadmap-management/SKILL.md +10 -0
- package/dist/agent-src/skills/roadmap-writing/SKILL.md +12 -1
- package/dist/agent-src/skills/rule-writing/SKILL.md +4 -2
- package/dist/agent-src/skills/skill-reviewer/SKILL.md +1 -1
- package/dist/agent-src/skills/skill-writing/SKILL.md +2 -2
- package/dist/agent-src/skills/tailwind-engineer/SKILL.md +3 -1
- package/dist/agent-src/skills/token-optimizer/SKILL.md +2 -2
- package/dist/agent-src/skills/ui-component-architect/SKILL.md +6 -3
- package/dist/agent-src/templates/agent-settings.md +13 -15
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +6 -2
- package/dist/agent-src/templates/persona.md +2 -2
- package/dist/agent-src/templates/roadmaps.md +13 -1
- package/dist/agent-src/templates/rule.md +1 -1
- package/dist/agent-src/templates/scripts/work_engine/directives/ui/_fix_lane.ts +52 -0
- package/dist/agent-src/templates/scripts/work_engine/directives/ui/audit.ts +8 -0
- package/dist/agent-src/templates/scripts/work_engine/directives/ui/design.ts +6 -0
- package/dist/agent-src/templates/scripts/work_engine/intent/classify.ts +56 -4
- package/dist/agent-src/templates/skill.md +1 -1
- package/dist/cli/registry.js +3 -0
- package/dist/cli/registry.js.map +1 -1
- package/dist/cli-delegate/chunk-AIIUU73N.js +2040 -0
- package/dist/cli-delegate/{chunk-ZXXJLL5K.js → chunk-BSLUHV2F.js} +127 -1487
- package/dist/cli-delegate/{chunk-H574KOUH.js → chunk-ELMFOQPW.js} +35 -1
- package/dist/cli-delegate/chunk-LF2KKJ4Q.js +183 -0
- package/dist/cli-delegate/chunk-VSYT7OHO.js +1382 -0
- package/dist/cli-delegate/chunk-XGA2NTU4.js +3727 -0
- package/dist/cli-delegate/cmd_conformance.js +4 -3
- package/dist/cli-delegate/cmd_converge.js +3 -3
- package/dist/cli-delegate/cmd_doctor.js +3 -2
- package/dist/cli-delegate/cmd_explain.js +36 -36
- package/dist/cli-delegate/cmd_export.js +4 -4
- package/dist/cli-delegate/cmd_handoff.js +1169 -0
- package/dist/cli-delegate/cmd_memory_get.js +35 -3754
- package/dist/cli-delegate/cmd_migrate.js +2 -2
- package/dist/cli-delegate/cmd_prune.js +4 -4
- package/dist/cli-delegate/cmd_route_audit.js +249 -0
- package/dist/cli-delegate/cmd_route_explain.js +285 -0
- package/dist/cli-delegate/cmd_settings_migrate.js +4 -4
- package/dist/cli-delegate/cmd_sync.js +4 -4
- package/dist/cli-delegate/cmd_uninstall.js +4 -4
- package/dist/cli-delegate/cmd_update.js +3 -3
- package/dist/cli-delegate/cmd_upgrade.js +3 -3
- package/dist/cli-delegate/cmd_validate.js +4 -4
- package/dist/cli-delegate/cmd_versions.js +3 -3
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +84 -84
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.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 +602 -507
- package/dist/install/install.mjs +34 -0
- package/dist/mcp/awesome-mcp-servers.row.md +1 -1
- package/dist/mcp/mcp-cloudflare-catalogue.json +2 -2
- package/dist/mcp/registry-manifest.json +4 -4
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +25 -3
- package/dist/router.json +1 -1
- package/dist/scripts/_lib/agent_src.js +39 -6
- package/dist/scripts/_lib/agent_src.js.map +1 -1
- package/dist/server/schemas/settings.js +25 -0
- package/dist/server/schemas/settings.js.map +1 -1
- package/docs/guidelines/abstraction-thresholds.md +53 -0
- package/docs/guidelines/agent-infra/agent-interaction-and-decision-quality.md +1 -1
- package/docs/guidelines/agent-infra/asking-and-brevity-examples.md +7 -0
- package/docs/guidelines/agent-infra/context-hygiene-mechanics.md +2 -2
- package/docs/guidelines/agent-infra/minimal-safe-diff-mechanics.md +5 -3
- package/docs/guidelines/agent-infra/size-and-scope.md +8 -2
- package/docs/guidelines/component-oriented-and-oop-development.md +11 -6
- package/docs/guidelines/docs/readme-size-and-splitting.md +5 -2
- package/llms.txt +1 -0
- package/package.json +1 -1
- package/src/agent-src/templates/scripts/work_engine/directives/ui/_fix_lane.ts +52 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui/audit.ts +8 -0
- package/src/agent-src/templates/scripts/work_engine/directives/ui/design.ts +6 -0
- package/src/agent-src/templates/scripts/work_engine/intent/classify.ts +56 -4
- package/src/config/agent-settings.template.yml +68 -0
- package/src/config/evaluator-budgets.json +8 -11
- package/src/config/gate-coverage.yml +70 -0
- package/src/scripts/_cli/cmd_explain.ts +41 -36
- package/src/scripts/_cli/cmd_handoff.ts +405 -0
- package/src/scripts/_cli/cmd_route_audit.ts +290 -0
- package/src/scripts/_cli/cmd_route_explain.ts +269 -0
- package/src/scripts/_cli/handoff_generate.ts +453 -0
- package/src/scripts/_cli/handoff_sessions.ts +435 -0
- package/src/scripts/_dispatch.bash +32 -0
- package/src/scripts/_lib/agent_src.ts +36 -6
- package/src/scripts/_lib/git_env.ts +40 -0
- package/src/scripts/_lib/md_table.ts +27 -0
- package/src/scripts/_lib/planning_settings.ts +43 -0
- package/src/scripts/_lib/release_material.ts +156 -0
- package/src/scripts/_lib/router_match.ts +301 -0
- package/src/scripts/annotate_r1_outcomes.ts +258 -0
- package/src/scripts/check_completion_review.ts +1140 -0
- package/src/scripts/check_condensation.ts +2 -1
- package/src/scripts/check_finding_dispositions.ts +278 -0
- package/src/scripts/check_pack_size.ts +4 -3
- package/src/scripts/check_references.ts +1 -0
- package/src/scripts/check_release_highlights.ts +300 -0
- package/src/scripts/check_release_pr_shape.ts +4 -0
- package/src/scripts/check_release_surface_equality.ts +132 -0
- package/src/scripts/check_reply_consistency.ts +187 -138
- package/src/scripts/compile_router.ts +20 -3
- package/src/scripts/dispatch_r2_reviewer.ts +968 -0
- package/src/scripts/eval_ui_triviality.ts +156 -0
- package/src/scripts/handoff_context_hook.ts +148 -0
- package/src/scripts/hook_manifest.yaml +17 -7
- package/src/scripts/hooks/concern_registry.ts +2 -0
- package/src/scripts/lint_abstraction_thresholds.ts +327 -0
- package/src/scripts/lint_framework_leakage_allowlist.json +2 -2
- package/src/scripts/lint_plan_risk_register.ts +931 -0
- package/src/scripts/lint_profile_personas.ts +153 -0
- package/src/scripts/lint_token_budget_discipline.ts +148 -0
- package/src/scripts/lint_trigger_collisions.ts +257 -0
- package/src/scripts/release.ts +156 -32
- package/src/scripts/release_drill.ts +491 -0
- package/src/scripts/router_telemetry.ts +16 -290
- package/src/scripts/schemas/command.schema.json +33 -0
- package/src/scripts/schemas/rule.schema.json +10 -0
- package/src/scripts/self_review_gate.ts +41 -3
- package/src/scripts/skill_linter.ts +3 -1
- package/src/scripts/smoke/router.sh +1 -1
- package/src/scripts/update_counts.ts +60 -12
- package/src/server/schemas/settings.ts +41 -0
- package/dist/cli-delegate/{chunk-XX6TOPD7.js → chunk-WR6GTRHJ.js} +3 -3
|
@@ -0,0 +1,491 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* Release-orchestration drill — execute() against a simulated git/gh world.
|
|
4
|
+
*
|
|
5
|
+
* `task release` step machinery (checkout → bump → commit → push → PR →
|
|
6
|
+
* checks → merge → tag → GitHub Release → branch cleanup) historically ran
|
|
7
|
+
* NOWHERE except live: `--dry-run` returns before execute(), and the unit
|
|
8
|
+
* tests never call it. Every orchestration bug therefore fired mid-release —
|
|
9
|
+
* three times in one week (2026-08-03: push rejected after a remote move,
|
|
10
|
+
* merge dying on a BEHIND head, gh resolving the PR from the wrong HEAD).
|
|
11
|
+
*
|
|
12
|
+
* This drill closes that hole. It installs release.ts's exec seam
|
|
13
|
+
* (`_set_exec_override`) so every external command is answered by a scripted
|
|
14
|
+
* FakeWorld, then runs the REAL execute() through named scenarios — the happy
|
|
15
|
+
* path plus each measured failure mode. run()'s check/die/CalledProcessError
|
|
16
|
+
* semantics apply to the simulated results, so the drill exercises the real
|
|
17
|
+
* error paths, not a parallel reimplementation.
|
|
18
|
+
*
|
|
19
|
+
* Surfaces:
|
|
20
|
+
* - vitest: tests/scripts/release_drill.test.ts runs every scenario in CI.
|
|
21
|
+
* - operator: `task release-drill` (or `./scripts-run src/scripts/release_drill`)
|
|
22
|
+
* runs them all and prints one ✅/❌ line per scenario.
|
|
23
|
+
*
|
|
24
|
+
* The drill never touches git, gh, npm, or the filesystem outside reading
|
|
25
|
+
* package.json (execute()'s own resume-mode bump-skip probe).
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import * as fs from 'node:fs';
|
|
29
|
+
import * as path from 'node:path';
|
|
30
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
31
|
+
|
|
32
|
+
import {
|
|
33
|
+
_MERGE_UPDATE_ROUNDS,
|
|
34
|
+
_set_exec_override,
|
|
35
|
+
execute,
|
|
36
|
+
Plan,
|
|
37
|
+
SystemExitError,
|
|
38
|
+
} from './release.js';
|
|
39
|
+
|
|
40
|
+
const _HERE = fileURLToPath(import.meta.url);
|
|
41
|
+
const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
|
|
42
|
+
|
|
43
|
+
/** execute()'s bump-skip probe reads the real package.json — mirror it. */
|
|
44
|
+
function current_version(): string {
|
|
45
|
+
const pkg = JSON.parse(
|
|
46
|
+
fs.readFileSync(path.join(REPO_ROOT, 'package.json'), 'utf-8'),
|
|
47
|
+
) as Record<string, unknown>;
|
|
48
|
+
return String(pkg['version']);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
interface ExecResult {
|
|
52
|
+
status: number;
|
|
53
|
+
stdout: string;
|
|
54
|
+
stderr: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const OK: ExecResult = { status: 0, stdout: '', stderr: '' };
|
|
58
|
+
|
|
59
|
+
/** Knobs a scenario turns to inject the measured failure modes. */
|
|
60
|
+
interface WorldConfig {
|
|
61
|
+
/** `git push -u` is rejected this many times before succeeding (9.15.0). */
|
|
62
|
+
push_rejections?: number;
|
|
63
|
+
/** mergeStateStatus reports BEHIND this many probes in a row (9.16.0). */
|
|
64
|
+
behind_probes?: number;
|
|
65
|
+
/** `gh pr merge` fails once with the not-up-to-date error (the race). */
|
|
66
|
+
merge_races_once?: boolean;
|
|
67
|
+
/** `gh pr merge` fails hard with an unrelated error (must surface). */
|
|
68
|
+
merge_fails_hard?: boolean;
|
|
69
|
+
/** PR checks fail (watch exits non-zero) — the release must die. */
|
|
70
|
+
checks_fail?: boolean;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* A scriptable stand-in for every git/gh/task command execute() issues.
|
|
75
|
+
* Stateful where the real world is: checkouts move HEAD, `git tag` makes the
|
|
76
|
+
* tag exist, a successful merge flips the PR to MERGED and deletes the branch.
|
|
77
|
+
*/
|
|
78
|
+
class FakeWorld {
|
|
79
|
+
readonly calls: string[] = [];
|
|
80
|
+
head = 'main';
|
|
81
|
+
tag_local = false;
|
|
82
|
+
tag_remote = false;
|
|
83
|
+
pr_merged = false;
|
|
84
|
+
release_created = false;
|
|
85
|
+
branch_exists_remote = true;
|
|
86
|
+
/** Live PR body (release-truth Phase 1: refreshed from the changelog at head). */
|
|
87
|
+
pr_body = '';
|
|
88
|
+
|
|
89
|
+
private push_rejections: number;
|
|
90
|
+
private behind_probes: number;
|
|
91
|
+
private merge_races_once: boolean;
|
|
92
|
+
private merge_fails_hard: boolean;
|
|
93
|
+
private checks_fail: boolean;
|
|
94
|
+
|
|
95
|
+
constructor(
|
|
96
|
+
readonly branch: string,
|
|
97
|
+
readonly target: string,
|
|
98
|
+
cfg: WorldConfig,
|
|
99
|
+
) {
|
|
100
|
+
this.push_rejections = cfg.push_rejections ?? 0;
|
|
101
|
+
this.behind_probes = cfg.behind_probes ?? 0;
|
|
102
|
+
this.merge_races_once = cfg.merge_races_once ?? false;
|
|
103
|
+
this.merge_fails_hard = cfg.merge_fails_hard ?? false;
|
|
104
|
+
this.checks_fail = cfg.checks_fail ?? false;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
exec(args: readonly string[]): ExecResult {
|
|
108
|
+
const cmd = args.join(' ');
|
|
109
|
+
this.calls.push(cmd);
|
|
110
|
+
|
|
111
|
+
// ── git ──────────────────────────────────────────────────────────
|
|
112
|
+
if (cmd === 'git rev-parse --abbrev-ref HEAD') {
|
|
113
|
+
return { ...OK, stdout: `${this.head}\n` };
|
|
114
|
+
}
|
|
115
|
+
if (args[0] === 'git' && args[1] === 'checkout') {
|
|
116
|
+
this.head = args[2] === '-b' ? (args[3] as string) : (args[2] as string);
|
|
117
|
+
return OK;
|
|
118
|
+
}
|
|
119
|
+
if (cmd === `git rev-parse --verify --quiet refs/heads/${this.branch}`) {
|
|
120
|
+
return this.pr_merged ? { ...OK, status: 1 } : OK;
|
|
121
|
+
}
|
|
122
|
+
if (cmd === `git ls-remote --exit-code --heads origin ${this.branch}`) {
|
|
123
|
+
return this.branch_exists_remote ? OK : { ...OK, status: 1 };
|
|
124
|
+
}
|
|
125
|
+
if (cmd === `git ls-remote --exit-code --tags origin ${this.target}`) {
|
|
126
|
+
return this.tag_remote ? OK : { ...OK, status: 1 };
|
|
127
|
+
}
|
|
128
|
+
if (cmd === `git tag -l ${this.target}`) {
|
|
129
|
+
return { ...OK, stdout: this.tag_local ? `${this.target}\n` : '' };
|
|
130
|
+
}
|
|
131
|
+
if (cmd === `git tag ${this.target}` || (args[1] === 'tag' && args[2] === '-a' && args[3] === this.target)) {
|
|
132
|
+
// release-truth Phase 1: the tag is annotated, message derived
|
|
133
|
+
// from the merged changelog section.
|
|
134
|
+
this.tag_local = true;
|
|
135
|
+
return OK;
|
|
136
|
+
}
|
|
137
|
+
if (cmd === `git show ${this.target}:CHANGELOG.md`) {
|
|
138
|
+
// The drill runs in the real repo — the changelog at the tag is
|
|
139
|
+
// the real file (execute() extracts the target's section from it).
|
|
140
|
+
return { ...OK, stdout: fs.readFileSync(path.join(REPO_ROOT, 'CHANGELOG.md'), 'utf-8') };
|
|
141
|
+
}
|
|
142
|
+
if (cmd === `git push origin ${this.target}`) {
|
|
143
|
+
this.tag_remote = true;
|
|
144
|
+
return OK;
|
|
145
|
+
}
|
|
146
|
+
if (cmd === `git push -u origin ${this.branch}`) {
|
|
147
|
+
if (this.push_rejections > 0) {
|
|
148
|
+
this.push_rejections -= 1;
|
|
149
|
+
return {
|
|
150
|
+
status: 1,
|
|
151
|
+
stdout: '',
|
|
152
|
+
stderr: `! [rejected] ${this.branch} -> ${this.branch} (fetch first)`,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
return OK;
|
|
156
|
+
}
|
|
157
|
+
if (cmd === 'git log -1 --format=%s') {
|
|
158
|
+
return { ...OK, stdout: `release: ${this.target}\n` };
|
|
159
|
+
}
|
|
160
|
+
if (cmd === 'git status --porcelain') {
|
|
161
|
+
return OK; // clean tree — resume mode, everything committed
|
|
162
|
+
}
|
|
163
|
+
if (cmd === 'git diff --cached --name-only') {
|
|
164
|
+
return OK;
|
|
165
|
+
}
|
|
166
|
+
if (args[0] === 'git' && ['fetch', 'merge', 'pull', 'add', 'branch', 'push'].includes(args[1] as string)) {
|
|
167
|
+
return OK;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ── task ─────────────────────────────────────────────────────────
|
|
171
|
+
if (args[0] === 'task') {
|
|
172
|
+
return OK; // release-prepare — regeneration is out of drill scope
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ── gh ───────────────────────────────────────────────────────────
|
|
176
|
+
if (cmd.startsWith(`gh pr list --head ${this.branch}`)) {
|
|
177
|
+
const state = this.pr_merged ? 'MERGED' : 'OPEN';
|
|
178
|
+
return {
|
|
179
|
+
...OK,
|
|
180
|
+
stdout: JSON.stringify([{ number: 999, state, url: 'https://example.invalid/pr/999' }]),
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
if (cmd === `gh pr checks ${this.branch} --watch`) {
|
|
184
|
+
return this.checks_fail
|
|
185
|
+
? { status: 1, stdout: 'some-required-check\tfail\t10s\n', stderr: '' }
|
|
186
|
+
: { ...OK, stdout: 'all checks pass\n' };
|
|
187
|
+
}
|
|
188
|
+
if (cmd === `gh pr checks ${this.branch} --json name,bucket`) {
|
|
189
|
+
return {
|
|
190
|
+
...OK,
|
|
191
|
+
stdout: this.checks_fail
|
|
192
|
+
? JSON.stringify([{ name: 'some-required-check', bucket: 'fail' }])
|
|
193
|
+
: '[]',
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
if (cmd === `gh pr view ${this.branch} --json mergeStateStatus --jq .mergeStateStatus`) {
|
|
197
|
+
if (this.behind_probes > 0) {
|
|
198
|
+
this.behind_probes -= 1;
|
|
199
|
+
return { ...OK, stdout: 'BEHIND\n' };
|
|
200
|
+
}
|
|
201
|
+
return { ...OK, stdout: 'CLEAN\n' };
|
|
202
|
+
}
|
|
203
|
+
if (cmd === `gh pr merge ${this.branch} --merge --delete-branch`) {
|
|
204
|
+
if (this.merge_fails_hard) {
|
|
205
|
+
return { status: 1, stdout: '', stderr: 'GraphQL: Base branch was modified (unexpected)' };
|
|
206
|
+
}
|
|
207
|
+
if (this.merge_races_once) {
|
|
208
|
+
this.merge_races_once = false;
|
|
209
|
+
// The race: main moved between the CLEAN probe and the merge.
|
|
210
|
+
this.behind_probes += 1;
|
|
211
|
+
return {
|
|
212
|
+
status: 1,
|
|
213
|
+
stdout: '',
|
|
214
|
+
stderr: 'X Pull request #999 is not mergeable: the head branch is not up to date with the base branch.',
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
this.pr_merged = true;
|
|
218
|
+
this.branch_exists_remote = false;
|
|
219
|
+
this.head = 'main'; // gh checks out the default branch after deleting
|
|
220
|
+
return OK;
|
|
221
|
+
}
|
|
222
|
+
if (cmd === `gh pr view ${this.branch} --json body -q .body`) {
|
|
223
|
+
// release-truth Phase 1: _refresh_pr_body_from_head probes the
|
|
224
|
+
// live body before deciding whether to edit.
|
|
225
|
+
return { ...OK, stdout: this.pr_body };
|
|
226
|
+
}
|
|
227
|
+
if (args[0] === 'gh' && args[1] === 'pr' && args[2] === 'edit' && args[3] === this.branch) {
|
|
228
|
+
const bodyIdx = args.indexOf('--body');
|
|
229
|
+
this.pr_body = bodyIdx >= 0 ? String(args[bodyIdx + 1] ?? '') : this.pr_body;
|
|
230
|
+
return OK;
|
|
231
|
+
}
|
|
232
|
+
if (cmd === `gh release view ${this.target}`) {
|
|
233
|
+
return this.release_created ? OK : { ...OK, status: 1 };
|
|
234
|
+
}
|
|
235
|
+
if (cmd.startsWith(`gh release create ${this.target}`)) {
|
|
236
|
+
this.release_created = true;
|
|
237
|
+
return OK;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
throw new Error(`FakeWorld: unscripted command: ${cmd}`);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
interface ScenarioOutcome {
|
|
245
|
+
ok: boolean;
|
|
246
|
+
failures: string[];
|
|
247
|
+
world: FakeWorld;
|
|
248
|
+
error: string | null;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
interface Scenario {
|
|
252
|
+
/** What the scenario proves — printed by the CLI. */
|
|
253
|
+
summary: string;
|
|
254
|
+
config: WorldConfig;
|
|
255
|
+
/** Whether execute() is expected to complete (vs die/throw). */
|
|
256
|
+
expect_success: boolean;
|
|
257
|
+
/** Extra assertions over the finished world; return failure strings. */
|
|
258
|
+
verify: (world: FakeWorld, error: string | null) => string[];
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function _expect(cond: boolean, msg: string, failures: string[]): void {
|
|
262
|
+
if (!cond) {
|
|
263
|
+
failures.push(msg);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function _count(world: FakeWorld, needle: string): number {
|
|
268
|
+
return world.calls.filter((c) => c === needle).length;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const SCENARIOS: Record<string, Scenario> = {
|
|
272
|
+
'happy-resume': {
|
|
273
|
+
summary: 'resume with everything green runs checkout → checks → merge → tag → release',
|
|
274
|
+
config: {},
|
|
275
|
+
expect_success: true,
|
|
276
|
+
verify: (w, _err) => {
|
|
277
|
+
const f: string[] = [];
|
|
278
|
+
_expect(w.pr_merged, 'PR was never merged', f);
|
|
279
|
+
_expect(w.tag_remote, 'tag never pushed', f);
|
|
280
|
+
_expect(w.release_created, 'GitHub Release never created', f);
|
|
281
|
+
const merge = w.calls.indexOf(`gh pr merge ${w.branch} --merge --delete-branch`);
|
|
282
|
+
const watch = w.calls.indexOf(`gh pr checks ${w.branch} --watch`);
|
|
283
|
+
_expect(watch >= 0 && merge > watch, 'merge did not wait for checks', f);
|
|
284
|
+
return f;
|
|
285
|
+
},
|
|
286
|
+
},
|
|
287
|
+
'push-rejected-then-recover': {
|
|
288
|
+
summary: 'step 4: remote moved under the run — integrate + retry instead of dying (9.15.0)',
|
|
289
|
+
config: { push_rejections: 1 },
|
|
290
|
+
expect_success: true,
|
|
291
|
+
verify: (w, _err) => {
|
|
292
|
+
const f: string[] = [];
|
|
293
|
+
_expect(
|
|
294
|
+
_count(w, `git push -u origin ${w.branch}`) === 2,
|
|
295
|
+
'push was not retried exactly once',
|
|
296
|
+
f,
|
|
297
|
+
);
|
|
298
|
+
_expect(
|
|
299
|
+
w.calls.includes(`git fetch origin ${w.branch}`) &&
|
|
300
|
+
w.calls.includes(`git merge --no-edit origin/${w.branch}`),
|
|
301
|
+
'rejected push did not integrate the moved remote',
|
|
302
|
+
f,
|
|
303
|
+
);
|
|
304
|
+
_expect(w.pr_merged && w.release_created, 'release did not complete after recovery', f);
|
|
305
|
+
return f;
|
|
306
|
+
},
|
|
307
|
+
},
|
|
308
|
+
'behind-then-merge': {
|
|
309
|
+
summary: 'step 7: head BEHIND main — update branch, re-run checks, merge (9.16.0)',
|
|
310
|
+
config: { behind_probes: 1 },
|
|
311
|
+
expect_success: true,
|
|
312
|
+
verify: (w, _err) => {
|
|
313
|
+
const f: string[] = [];
|
|
314
|
+
_expect(
|
|
315
|
+
w.calls.includes('git merge --no-edit origin/main'),
|
|
316
|
+
'BEHIND head was never updated with main',
|
|
317
|
+
f,
|
|
318
|
+
);
|
|
319
|
+
_expect(
|
|
320
|
+
_count(w, `gh pr checks ${w.branch} --watch`) >= 2,
|
|
321
|
+
'checks were not re-run after the update',
|
|
322
|
+
f,
|
|
323
|
+
);
|
|
324
|
+
_expect(w.pr_merged && w.release_created, 'release did not complete after the update', f);
|
|
325
|
+
return f;
|
|
326
|
+
},
|
|
327
|
+
},
|
|
328
|
+
'merge-race-recovers': {
|
|
329
|
+
summary: 'step 7: main moves between the CLEAN probe and the merge — retry, not crash',
|
|
330
|
+
config: { merge_races_once: true },
|
|
331
|
+
expect_success: true,
|
|
332
|
+
verify: (w, _err) => {
|
|
333
|
+
const f: string[] = [];
|
|
334
|
+
_expect(
|
|
335
|
+
_count(w, `gh pr merge ${w.branch} --merge --delete-branch`) === 2,
|
|
336
|
+
'raced merge was not retried exactly once',
|
|
337
|
+
f,
|
|
338
|
+
);
|
|
339
|
+
_expect(w.pr_merged && w.release_created, 'release did not complete after the race', f);
|
|
340
|
+
return f;
|
|
341
|
+
},
|
|
342
|
+
},
|
|
343
|
+
'behind-forever-dies': {
|
|
344
|
+
summary: `step 7: BEHIND persists past ${_MERGE_UPDATE_ROUNDS} update rounds — die with the resume command`,
|
|
345
|
+
config: { behind_probes: 99 },
|
|
346
|
+
expect_success: false,
|
|
347
|
+
verify: (w, err) => {
|
|
348
|
+
const f: string[] = [];
|
|
349
|
+
_expect(err !== null && err.includes('task release -- --resume --yes'),
|
|
350
|
+
'death message does not name the resume command', f);
|
|
351
|
+
_expect(
|
|
352
|
+
_count(w, 'git merge --no-edit origin/main') === _MERGE_UPDATE_ROUNDS,
|
|
353
|
+
`expected exactly ${_MERGE_UPDATE_ROUNDS} update rounds`,
|
|
354
|
+
f,
|
|
355
|
+
);
|
|
356
|
+
_expect(!w.pr_merged, 'PR must not be merged in this scenario', f);
|
|
357
|
+
return f;
|
|
358
|
+
},
|
|
359
|
+
},
|
|
360
|
+
'merge-fails-hard-surfaces': {
|
|
361
|
+
summary: 'step 7: a non-BEHIND merge failure surfaces unchanged instead of looping',
|
|
362
|
+
config: { merge_fails_hard: true },
|
|
363
|
+
expect_success: false,
|
|
364
|
+
verify: (w, err) => {
|
|
365
|
+
const f: string[] = [];
|
|
366
|
+
_expect(err !== null && err.includes('gh pr merge'), 'error does not name the merge command', f);
|
|
367
|
+
_expect(
|
|
368
|
+
_count(w, `gh pr merge ${w.branch} --merge --delete-branch`) === 1,
|
|
369
|
+
'hard merge failure must not be retried',
|
|
370
|
+
f,
|
|
371
|
+
);
|
|
372
|
+
return f;
|
|
373
|
+
},
|
|
374
|
+
},
|
|
375
|
+
'checks-fail-dies': {
|
|
376
|
+
summary: 'step 6: failing PR checks kill the run before any merge',
|
|
377
|
+
config: { checks_fail: true },
|
|
378
|
+
expect_success: false,
|
|
379
|
+
verify: (w, err) => {
|
|
380
|
+
const f: string[] = [];
|
|
381
|
+
_expect(err !== null && err.includes('PR checks failed'), 'death does not name failing checks', f);
|
|
382
|
+
_expect(
|
|
383
|
+
!w.calls.some((c) => c.startsWith(`gh pr merge`)),
|
|
384
|
+
'merge must never run after failing checks',
|
|
385
|
+
f,
|
|
386
|
+
);
|
|
387
|
+
return f;
|
|
388
|
+
},
|
|
389
|
+
},
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
/** Run one scenario through the real execute(); never touches the world. */
|
|
393
|
+
function run_scenario(name: string): ScenarioOutcome {
|
|
394
|
+
const scenario = SCENARIOS[name];
|
|
395
|
+
if (scenario === undefined) {
|
|
396
|
+
throw new Error(`unknown scenario: ${name}`);
|
|
397
|
+
}
|
|
398
|
+
const target = current_version();
|
|
399
|
+
const branch = `release/${target}`;
|
|
400
|
+
const world = new FakeWorld(branch, target, scenario.config);
|
|
401
|
+
const plan = new Plan(target, target, 'patch', [], null, `Release ${target}.`, `## ${target}\n`, null);
|
|
402
|
+
|
|
403
|
+
let error: string | null = null;
|
|
404
|
+
// Capture the run's own output: die() writes its message to stderr before
|
|
405
|
+
// throwing a bare SystemExitError, and the step lines on stdout are noise
|
|
406
|
+
// when seven scenarios run back to back.
|
|
407
|
+
const captured: string[] = [];
|
|
408
|
+
const orig_out = process.stdout.write.bind(process.stdout);
|
|
409
|
+
const orig_err = process.stderr.write.bind(process.stderr);
|
|
410
|
+
(process.stdout as { write: unknown }).write = (s: string): boolean => {
|
|
411
|
+
captured.push(String(s));
|
|
412
|
+
return true;
|
|
413
|
+
};
|
|
414
|
+
(process.stderr as { write: unknown }).write = (s: string): boolean => {
|
|
415
|
+
captured.push(String(s));
|
|
416
|
+
return true;
|
|
417
|
+
};
|
|
418
|
+
_set_exec_override((args) => world.exec(args));
|
|
419
|
+
try {
|
|
420
|
+
execute(plan, { wait_for_checks: true, dry_run: false, resume: true });
|
|
421
|
+
} catch (err) {
|
|
422
|
+
if (err instanceof SystemExitError) {
|
|
423
|
+
error = `SystemExit(${err.code}): ${captured.join('')}`;
|
|
424
|
+
} else {
|
|
425
|
+
error = err instanceof Error ? err.message : String(err);
|
|
426
|
+
}
|
|
427
|
+
} finally {
|
|
428
|
+
_set_exec_override(null);
|
|
429
|
+
process.stdout.write = orig_out;
|
|
430
|
+
process.stderr.write = orig_err;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const failures: string[] = [];
|
|
434
|
+
if (scenario.expect_success && error !== null) {
|
|
435
|
+
failures.push(`expected success, got: ${error}`);
|
|
436
|
+
}
|
|
437
|
+
if (!scenario.expect_success && error === null) {
|
|
438
|
+
failures.push('expected the run to die, but it completed');
|
|
439
|
+
}
|
|
440
|
+
failures.push(...scenario.verify(world, error));
|
|
441
|
+
return { ok: failures.length === 0, failures, world, error };
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function main(): number {
|
|
445
|
+
let failed = 0;
|
|
446
|
+
const names = Object.keys(SCENARIOS);
|
|
447
|
+
process.stdout.write(`release drill — ${names.length} scenario(s), simulated git/gh world\n\n`);
|
|
448
|
+
for (const name of names) {
|
|
449
|
+
const outcome = run_scenario(name);
|
|
450
|
+
if (outcome.ok) {
|
|
451
|
+
process.stdout.write(`✅ ${name} — ${SCENARIOS[name]!.summary}\n`);
|
|
452
|
+
} else {
|
|
453
|
+
failed += 1;
|
|
454
|
+
process.stdout.write(`❌ ${name} — ${SCENARIOS[name]!.summary}\n`);
|
|
455
|
+
for (const f of outcome.failures) {
|
|
456
|
+
process.stdout.write(` ${f}\n`);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
process.stdout.write(`\nscanned: ${names.length}\n`);
|
|
461
|
+
process.stdout.write(
|
|
462
|
+
failed === 0
|
|
463
|
+
? '✅ release drill green — the step machinery survives every measured failure mode.\n'
|
|
464
|
+
: `❌ release drill: ${failed} scenario(s) failed.\n`,
|
|
465
|
+
);
|
|
466
|
+
return failed === 0 ? 0 : 1;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function _isCliEntry(): boolean {
|
|
470
|
+
if (process.argv[1] === undefined) {
|
|
471
|
+
return false;
|
|
472
|
+
}
|
|
473
|
+
const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
|
|
474
|
+
if (import.meta.url === argvUrl) {
|
|
475
|
+
return true;
|
|
476
|
+
}
|
|
477
|
+
try {
|
|
478
|
+
const here = fs.realpathSync(fileURLToPath(import.meta.url));
|
|
479
|
+
const argvPath = fs.realpathSync(path.resolve(process.argv[1]));
|
|
480
|
+
return here === argvPath;
|
|
481
|
+
} catch {
|
|
482
|
+
return false;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
if (_isCliEntry() || process.argv[1] === _HERE) {
|
|
487
|
+
process.exit(main());
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
export { FakeWorld, SCENARIOS, run_scenario };
|
|
491
|
+
export type { Scenario, WorldConfig };
|