@event4u/agent-config 15.0.0 → 16.1.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 +301 -347
- package/CONTRIBUTING.md +9 -0
- package/README.md +2 -2
- package/dist/agent-src/commands/analyze/inbox.md +26 -0
- package/dist/agent-src/commands/analyze/roadmap-repos.md +27 -0
- package/dist/agent-src/commands/challenge-me/closure.md +178 -0
- package/dist/agent-src/commands/challenge-me.md +9 -2
- package/dist/agent-src/commands/council/default.md +25 -5
- package/dist/agent-src/commands/feature/plan.md +28 -1
- package/dist/agent-src/commands/feature/roadmap.md +28 -1
- package/dist/agent-src/commands/implement-ticket.md +27 -0
- package/dist/agent-src/commands/jira-ticket.md +27 -0
- package/dist/agent-src/commands/pr/create.md +29 -0
- package/dist/agent-src/commands/pr/merge.md +60 -9
- package/dist/agent-src/commands/roadmap/ai-council.md +7 -1
- package/dist/agent-src/commands/roadmap/create.md +77 -29
- package/dist/agent-src/commands/roadmap/materialize.md +27 -0
- package/dist/agent-src/commands/roadmap/process-full.md +123 -18
- package/dist/agent-src/commands/team.md +22 -0
- package/dist/agent-src/contexts/authority/scope-growth-ownership.md +80 -0
- package/dist/agent-src/contexts/communication/rules-auto/user-interaction-mechanics.md +97 -0
- package/dist/agent-src/contexts/execution/auto-orchestration-activation.md +44 -3
- package/dist/agent-src/contexts/execution/autonomy-mechanics.md +15 -15
- package/dist/agent-src/contexts/execution/decision-closure-execution.md +93 -0
- package/dist/agent-src/contexts/execution/fix-loop-ladder.md +108 -0
- package/dist/agent-src/contexts/execution/plan-confidence-gate.md +4 -2
- package/dist/agent-src/contexts/execution/roadmap-process-loop.md +75 -6
- package/dist/agent-src/guidelines/agent-infra/active-remediation-mechanics.md +57 -0
- package/dist/agent-src/guidelines/agent-infra/evaluator-independence-mechanics.md +102 -0
- package/dist/agent-src/guidelines/design-fidelity-routing.md +29 -0
- package/dist/agent-src/rules/autonomous-execution.md +15 -8
- package/dist/agent-src/rules/delegation-policy.md +23 -45
- package/dist/agent-src/rules/design-fidelity.md +32 -37
- package/dist/agent-src/rules/evaluator-independence.md +43 -49
- package/dist/agent-src/rules/test-first.md +41 -0
- package/dist/agent-src/rules/user-interaction.md +31 -30
- package/dist/agent-src/skills/accessibility-auditor/data/manifest.json +3 -3
- package/dist/agent-src/skills/ai-council/SKILL.md +11 -3
- package/dist/agent-src/skills/api-design/data/manifest.json +3 -3
- package/dist/agent-src/skills/database/data/manifest.json +4 -4
- package/dist/agent-src/skills/database/data/query-tuning.csv +3 -3
- package/dist/agent-src/skills/design-review/SKILL.md +50 -0
- package/dist/agent-src/skills/error-handling-patterns/SKILL.md +9 -0
- package/dist/agent-src/skills/existing-ui-audit/SKILL.md +14 -4
- package/dist/agent-src/skills/experiment-loop/SKILL.md +31 -1
- package/dist/agent-src/skills/roadmap-writing/SKILL.md +1 -1
- package/dist/agent-src/skills/storybook-workshop/SKILL.md +42 -3
- package/dist/agent-src/skills/threat-modeling/SKILL.md +9 -4
- package/dist/agent-src/skills/threat-modeling/data/manifest.json +4 -4
- package/dist/agent-src/skills/verify-repair-loop/SKILL.md +37 -3
- package/dist/agent-src/templates/agent-settings.md +1 -1
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/roadmaps.md +188 -0
- package/dist/agent-src/templates/scripts/work_engine/directives/backend/_self_fix.ts +62 -2
- package/dist/agent-src/templates/scripts/work_engine/directives/backend/test.ts +1 -0
- package/dist/agent-src/templates/scripts/work_engine/directives/backend/verify.ts +1 -0
- package/dist/cli-delegate/check_update_banner.js +40 -2
- package/dist/cli-delegate/chunk-4E2W63DA.js +335 -0
- package/dist/cli-delegate/chunk-5R7HOLAF.js +99 -0
- package/dist/cli-delegate/{chunk-IVBPRCQO.js → chunk-6KFEC3G7.js} +1 -1
- package/dist/cli-delegate/{chunk-2TAQ2K4V.js → chunk-6KM6ORRY.js} +55 -386
- package/dist/cli-delegate/{chunk-2XY7ILBT.js → chunk-FC75PGJE.js} +42 -2
- package/dist/cli-delegate/{chunk-GYC3VRYN.js → chunk-FXYUYU4N.js} +57 -13
- package/dist/cli-delegate/{chunk-55ACLDUE.js → chunk-GJCBB2UG.js} +168 -10
- package/dist/cli-delegate/{chunk-AK6HODNQ.js → chunk-HDVVGPQW.js} +873 -539
- package/dist/cli-delegate/chunk-NUHLI2RX.js +80 -0
- package/dist/cli-delegate/{chunk-HZ52BRGG.js → chunk-P6ZHP5W2.js} +15 -90
- package/dist/cli-delegate/{chunk-TYSY33MD.js → chunk-QJWAUMJW.js} +12 -2
- package/dist/cli-delegate/{chunk-57PN4GYJ.js → chunk-RGNIWTEA.js} +1 -1
- package/dist/cli-delegate/chunk-ROYYTEZU.js +39 -0
- package/dist/cli-delegate/{chunk-2KKXJW25.js → chunk-S3VFXMTM.js} +1 -1
- package/dist/cli-delegate/{chunk-3FKR64R5.js → chunk-UNCRJBT3.js} +31 -7
- package/dist/cli-delegate/{chunk-2DK2VJWG.js → chunk-WGXTVKCQ.js} +20 -6
- package/dist/cli-delegate/chunk-YDPILCOL.js +152 -0
- package/dist/cli-delegate/cmd_analyze_session.js +1 -1
- package/dist/cli-delegate/cmd_brand_status.js +1 -1
- package/dist/cli-delegate/cmd_conformance.js +918 -325
- package/dist/cli-delegate/cmd_doctor.js +7 -5
- package/dist/cli-delegate/cmd_explain.js +5 -3
- package/dist/cli-delegate/cmd_export.js +9 -7
- package/dist/cli-delegate/cmd_fleet.js +4 -2
- package/dist/cli-delegate/cmd_handoff.js +6 -6
- package/dist/cli-delegate/cmd_mcp_available.js +1 -1
- package/dist/cli-delegate/cmd_migrate.js +2 -2
- package/dist/cli-delegate/cmd_packs_active.js +3 -2
- package/dist/cli-delegate/cmd_preflight.js +4 -2
- package/dist/cli-delegate/cmd_prune.js +10 -8
- package/dist/cli-delegate/cmd_route_audit.js +2 -2
- package/dist/cli-delegate/cmd_route_explain.js +1 -1
- package/dist/cli-delegate/cmd_session_recycle.js +4 -4
- package/dist/cli-delegate/cmd_settings_get.js +28 -2
- package/dist/cli-delegate/cmd_settings_migrate.js +9 -7
- package/dist/cli-delegate/cmd_settings_set.js +14 -1
- package/dist/cli-delegate/cmd_sync.js +10 -8
- package/dist/cli-delegate/cmd_uninstall.js +16 -13
- package/dist/cli-delegate/cmd_update.js +1 -1
- package/dist/cli-delegate/cmd_upgrade.js +1 -1
- package/dist/cli-delegate/cmd_validate.js +12 -10
- package/dist/cli-delegate/cmd_versions.js +1 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +87 -40
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +4 -4
- package/dist/discovery/dormancy-report.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +10 -8
- package/dist/discovery/trust-report.md +3 -3
- package/dist/discovery/workspaces.json +8 -6
- package/dist/hooks/dispatch.js +2601 -1073
- package/dist/install/claudeRuleRewrite.js +14 -3
- package/dist/install/conflict.js +60 -10
- package/dist/install/conflictTracker.js +218 -0
- package/dist/install/fsPrimitives.js +54 -0
- package/dist/install/install.mjs +2008 -1481
- package/dist/install/manifestFiles.js +259 -0
- package/dist/install/preflight.js +14 -2
- package/dist/install/preserve.js +157 -0
- package/dist/install/recordedOwnership.js +132 -0
- package/dist/install/txlog.js +11 -2
- package/dist/mcp/registry-manifest.json +3 -3
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +1612 -667
- package/dist/router.json +1 -1
- package/dist/scripts/_lib/agent_settings.js +3 -2
- package/dist/scripts/_lib/fs_atomic.js +167 -0
- package/dist/scripts/_lib/installed_lock.js +1 -1
- package/dist/scripts/_lib/installed_tools.js +460 -0
- package/dist/scripts/_lib/lean_projection_mode.js +3 -2
- package/dist/scripts/_lib/settings_renamed_keys.js +78 -0
- package/dist/server/io/yamlIO.js +50 -0
- package/dist/server/routes/install.js +8 -0
- package/dist/server/routes/wizard.js +9 -3
- package/dist/server/schemas/settings.js +17 -5
- package/dist/shared/missionExecution.js +97 -0
- package/dist/ui/assets/{index-D0jaCRPM.js → index-DH1xc62v.js} +17 -17
- package/dist/ui/index.html +1 -1
- package/docs/MIGRATION.md +103 -0
- package/docs/contracts/settings-classes.md +26 -7
- package/docs/guidelines/agent-infra/active-remediation-mechanics.md +57 -0
- package/docs/guidelines/agent-infra/evaluator-independence-mechanics.md +102 -0
- package/docs/guidelines/design-fidelity-routing.md +29 -0
- package/package.json +1 -1
- package/src/agent-src/templates/scripts/work_engine/directives/backend/_self_fix.ts +62 -2
- package/src/agent-src/templates/scripts/work_engine/directives/backend/test.ts +1 -0
- package/src/agent-src/templates/scripts/work_engine/directives/backend/verify.ts +1 -0
- package/src/config/agent-settings.template.yml +139 -27
- package/src/config/auto-rule-budget.json +16 -0
- package/src/config/ci-local-parity.yml +18 -5
- package/src/config/continuity-surface.json +28 -0
- package/src/config/gate-coverage.yml +146 -13
- package/src/config/gate-violation-baselines.json +6 -6
- package/src/config/hook-token-budget.json +2 -0
- package/src/config/host-injection-effect.json +1 -1
- package/src/config/loop-surfaces.yaml +215 -0
- package/src/config/platform-anchor.json +3 -2
- package/src/config/preamble-payload-budget.json +12 -7
- package/src/config/preamble-payload-exceptions.json +28 -0
- package/src/config/release-gate-locality.yml +11 -0
- package/src/config/routing-coverage-seed.json +18 -4
- package/src/config/rule-enforcement-baseline.json +0 -1
- package/src/install/claudeRuleRewrite.ts +16 -2
- package/src/install/conflict.ts +65 -9
- package/src/install/conflictTracker.ts +299 -0
- package/src/install/fsPrimitives.ts +54 -0
- package/src/install/manifestFiles.ts +312 -0
- package/src/install/preflight.ts +16 -2
- package/src/install/preserve.ts +205 -0
- package/src/install/recordedOwnership.ts +149 -0
- package/src/install/txlog.ts +11 -2
- package/src/install/types.ts +19 -4
- package/src/scripts/_cli/cmd_conformance.ts +88 -15
- package/src/scripts/_cli/cmd_doctor.ts +4 -3
- package/src/scripts/_cli/cmd_route_explain.ts +9 -2
- package/src/scripts/_cli/cmd_settings_get.ts +44 -0
- package/src/scripts/_cli/doctor_execution.ts +90 -0
- package/src/scripts/_dispatch.bash +3 -1
- package/src/scripts/_lib/activation_payload.ts +247 -0
- package/src/scripts/_lib/agent_settings.ts +4 -2
- package/src/scripts/_lib/authority_path.ts +102 -0
- package/src/scripts/_lib/base_ref_payload.ts +315 -0
- package/src/scripts/_lib/cascade_base.ts +94 -0
- package/src/scripts/_lib/changelog_eras.ts +7 -2
- package/src/scripts/_lib/continuation_ladder.ts +149 -2
- package/src/scripts/_lib/council_transport.ts +129 -0
- package/src/scripts/_lib/delivery_ready.ts +112 -0
- package/src/scripts/_lib/dropped_decision.ts +142 -0
- package/src/scripts/_lib/forge_protection.ts +267 -0
- package/src/scripts/_lib/hook_platform_keys.ts +27 -0
- package/src/scripts/_lib/installed_lock.ts +1 -1
- package/src/scripts/_lib/json_python_parity.ts +127 -0
- package/src/scripts/_lib/lean_projection_mode.ts +14 -3
- package/src/scripts/_lib/loop_surfaces.ts +382 -0
- package/src/scripts/_lib/measured_payload_ceiling.ts +424 -0
- package/src/scripts/_lib/mission_record.ts +190 -0
- package/src/scripts/_lib/obligation_frequency.ts +217 -0
- package/src/scripts/_lib/obligations.ts +402 -0
- package/src/scripts/_lib/orchestration_record.ts +11 -20
- package/src/scripts/_lib/payload_catalogue_completeness.ts +211 -0
- package/src/scripts/_lib/release_findings_ingest.ts +324 -0
- package/src/scripts/_lib/release_highlights.ts +199 -10
- package/src/scripts/_lib/release_holds.ts +493 -0
- package/src/scripts/_lib/release_migration_gate.ts +184 -0
- package/src/scripts/_lib/release_tag_dispatch.ts +63 -0
- package/src/scripts/_lib/review_baseline.ts +254 -0
- package/src/scripts/_lib/review_skipped_record.ts +61 -1
- package/src/scripts/_lib/rides_along.ts +102 -0
- package/src/scripts/_lib/settings_renamed_keys.ts +82 -0
- package/src/scripts/_lib/standing_bound_ratchet.ts +216 -103
- package/src/scripts/_lib/test_delta.ts +166 -0
- package/src/scripts/_lib/test_provenance.ts +171 -0
- package/src/scripts/_lib/turn_end_refusals.ts +51 -4
- package/src/scripts/_lib/typed_op_grant.ts +246 -0
- package/src/scripts/_lib/typed_op_watch.ts +185 -0
- package/src/scripts/ai_council/config.ts +8 -11
- package/src/scripts/ai_council/fallback_config.ts +5 -3
- package/src/scripts/ai_council/ownership_classes.ts +118 -0
- package/src/scripts/ask_block_census.ts +285 -15
- package/src/scripts/check_beta_review_markers.ts +35 -4
- package/src/scripts/check_enforcement_matrix.ts +624 -0
- package/src/scripts/check_finding_dispositions.ts +167 -16
- package/src/scripts/check_gate_reachability.ts +173 -10
- package/src/scripts/check_generator_sync.ts +954 -0
- package/src/scripts/check_held_object_arrivals.ts +381 -0
- package/src/scripts/check_preamble_payload_budget.ts +474 -124
- package/src/scripts/check_release_highlights.ts +33 -1
- package/src/scripts/check_release_holds.ts +567 -0
- package/src/scripts/check_release_published.ts +1 -1
- package/src/scripts/check_reply_consistency.ts +86 -17
- package/src/scripts/check_rule_projection_integrity.ts +22 -12
- package/src/scripts/check_test_delta.ts +294 -0
- package/src/scripts/check_test_weakening.ts +299 -0
- package/src/scripts/closure_scan.ts +333 -0
- package/src/scripts/compile_hook_manifest.ts +43 -3
- package/src/scripts/council_record_shape.ts +209 -0
- package/src/scripts/hook_manifest.json +1 -1
- package/src/scripts/hook_manifest.yaml +127 -12
- package/src/scripts/hooks/block_kernel_rule_writes.ts +12 -2
- package/src/scripts/hooks/concern_registry.ts +6 -0
- package/src/scripts/hooks/design_pass_hook.ts +128 -3
- package/src/scripts/hooks/end_review_nudge_hook.ts +101 -21
- package/src/scripts/hooks/evidence_independence.ts +96 -0
- package/src/scripts/hooks/memory_recall_hook.ts +540 -0
- package/src/scripts/hooks/obligation_settle_hook.ts +182 -0
- package/src/scripts/hooks/review_baseline_hook.ts +130 -0
- package/src/scripts/hooks/rule_inject_hook.ts +49 -1
- package/src/scripts/hooks/run_continuation_hook.ts +17 -20
- package/src/scripts/hooks/turn_end_gate_hook.ts +232 -14
- package/src/scripts/hooks_doctor.ts +89 -1
- package/src/scripts/hooks_status.ts +19 -0
- package/src/scripts/install.ts +107 -207
- package/src/scripts/interruption_report.ts +8 -2
- package/src/scripts/janitor.ts +15 -1
- package/src/scripts/lint_decision_classes.ts +464 -0
- package/src/scripts/lint_hook_manifest.ts +11 -0
- package/src/scripts/lint_major_migration_sections.ts +450 -0
- package/src/scripts/lint_plan_risk_register.ts +47 -3
- package/src/scripts/lint_roadmap_blockers.ts +61 -0
- package/src/scripts/lint_roadmap_producers.ts +202 -0
- package/src/scripts/measure_rule_budget.ts +101 -5
- package/src/scripts/measure_turn_end_gate.ts +302 -5
- package/src/scripts/new_roadmap.ts +37 -0
- package/src/scripts/orchestration_record.ts +0 -5
- package/src/scripts/release.ts +66 -162
- package/src/scripts/release_drill.ts +460 -15
- package/src/scripts/release_env.ts +1 -1
- package/src/scripts/release_publication.ts +52 -1
- package/src/scripts/report_enforcement_drift.ts +566 -0
- package/src/scripts/report_held_object_arrivals.ts +456 -0
- package/src/scripts/report_host_injection_effect.ts +3 -11
- package/src/scripts/report_standing_payload_by_host.ts +26 -152
- package/src/scripts/schemas/agent-settings.schema.json +23 -0
- package/src/scripts/schemas/command.schema.json +4 -0
- package/src/scripts/scope_growth.ts +219 -0
- package/src/scripts/sync_agent_settings.ts +140 -18
- package/src/scripts/trigger_eval_grandfather.json +0 -1
- package/src/scripts/ui_conformance_probe.ts +622 -0
- package/src/server/io/yamlIO.ts +48 -0
- package/src/server/schemas/settings.ts +31 -7
- package/src/shared/missionExecution.ts +126 -0
- package/dist/cli-delegate/chunk-523HCFYO.js +0 -90
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
*/
|
|
27
27
|
|
|
28
28
|
import * as fs from 'node:fs';
|
|
29
|
+
import * as os from 'node:os';
|
|
29
30
|
import * as path from 'node:path';
|
|
30
31
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
31
32
|
|
|
@@ -37,6 +38,8 @@ import {
|
|
|
37
38
|
Plan,
|
|
38
39
|
SystemExitError,
|
|
39
40
|
} from './release.js';
|
|
41
|
+
import { releaseHoldPreflight } from './_lib/release_holds.js';
|
|
42
|
+
import { die } from './release_publication.js';
|
|
40
43
|
import { DERIVED_MARKER } from './_lib/release_highlights.js';
|
|
41
44
|
import {
|
|
42
45
|
AUGMENT_MARKETPLACE_JSON,
|
|
@@ -126,6 +129,33 @@ interface WorldConfig {
|
|
|
126
129
|
* existed.
|
|
127
130
|
*/
|
|
128
131
|
branch_behind_main?: number;
|
|
132
|
+
/**
|
|
133
|
+
* Commits `origin/<release-branch>` carries that this checkout does not.
|
|
134
|
+
*
|
|
135
|
+
* Sibling of `branch_behind_main`, one ref over: that one fakes `git
|
|
136
|
+
* rev-list --count HEAD..origin/main`, this one the distance to the
|
|
137
|
+
* release branch's OWN remote — the divergence a GitHub *Update branch*
|
|
138
|
+
* press or a concurrent resume run produces.
|
|
139
|
+
*/
|
|
140
|
+
branch_behind_remote?: number;
|
|
141
|
+
/**
|
|
142
|
+
* Commits this checkout carries that `origin/<release-branch>` does not.
|
|
143
|
+
*
|
|
144
|
+
* Defaults to 1 so every pre-existing scenario keeps the local-ahead shape
|
|
145
|
+
* it was written against. Set to 0 together with `branch_behind_remote: 0`
|
|
146
|
+
* to reach the state where the remote is already AT head and the push must
|
|
147
|
+
* therefore not be issued at all.
|
|
148
|
+
*/
|
|
149
|
+
branch_ahead_remote?: number;
|
|
150
|
+
/**
|
|
151
|
+
* Whether `origin/<release-branch>` exists at the start of the run.
|
|
152
|
+
*
|
|
153
|
+
* Internal state until 2026-09-12 — a scenario could only reach `false`
|
|
154
|
+
* indirectly, by having the run delete the branch. The remote-position
|
|
155
|
+
* probe added with `_remote_branch_state` has a never-pushed arm that no
|
|
156
|
+
* scenario could otherwise enter, so the knob is now declared.
|
|
157
|
+
*/
|
|
158
|
+
branch_exists_remote?: boolean;
|
|
129
159
|
/**
|
|
130
160
|
* Whether `execute()` is invoked with `--resume`. Defaults to TRUE so every
|
|
131
161
|
* pre-existing scenario keeps the shape it was written against.
|
|
@@ -143,6 +173,27 @@ interface WorldConfig {
|
|
|
143
173
|
merge_fails_hard?: boolean;
|
|
144
174
|
/** PR checks fail (watch exits non-zero) — the release must die. */
|
|
145
175
|
checks_fail?: boolean;
|
|
176
|
+
/**
|
|
177
|
+
* Step 7's world — two knobs, because the step asks two questions.
|
|
178
|
+
*
|
|
179
|
+
* It VERIFIES: the ledger is produced by the self-review workflow, so the
|
|
180
|
+
* only things a scenario can vary are whether the file is on the remote
|
|
181
|
+
* branch and what the disposition gate answers. An earlier version of this
|
|
182
|
+
* step ingested, and carried four more knobs for run discovery and artifact
|
|
183
|
+
* download; those went with it when the owner moved the ingest into CI.
|
|
184
|
+
*/
|
|
185
|
+
ledger_on_branch?: boolean;
|
|
186
|
+
/**
|
|
187
|
+
* Whether the ledger is on the TRUNK — the ref the merged path asks.
|
|
188
|
+
*
|
|
189
|
+
* Separate from `ledger_on_branch` because the two states worth testing are
|
|
190
|
+
* exactly the ones a single boolean cannot express: present on the branch
|
|
191
|
+
* and not on the trunk, and the reverse. Defaults to `ledger_on_branch`, so
|
|
192
|
+
* every scenario written before this knob keeps its answer.
|
|
193
|
+
*/
|
|
194
|
+
ledger_on_trunk?: boolean;
|
|
195
|
+
/** Exit code + streams the disposition gate answers with. */
|
|
196
|
+
disposition_verdict?: { status: number; stdout: string; stderr: string };
|
|
146
197
|
/**
|
|
147
198
|
* Content `git show <target>:CHANGELOG.md` returns.
|
|
148
199
|
*
|
|
@@ -169,7 +220,7 @@ interface WorldConfig {
|
|
|
169
220
|
changelog?: string;
|
|
170
221
|
/**
|
|
171
222
|
* Content the WORKING-TREE read returns — `read_changelog_text()`, used by
|
|
172
|
-
* step 5 (PR body) and step
|
|
223
|
+
* step 5 (PR body) and step 9 (annotated-tag message).
|
|
173
224
|
*
|
|
174
225
|
* Added 2026-09-01 with the publication guard (roadmap § 3.1). `changelog`
|
|
175
226
|
* above only covers `git show <tag>:CHANGELOG.md`; the working tree was
|
|
@@ -189,19 +240,20 @@ interface WorldConfig {
|
|
|
189
240
|
*/
|
|
190
241
|
tag_created_unpushed?: boolean;
|
|
191
242
|
/**
|
|
192
|
-
* The tag is already on the remote, so step
|
|
243
|
+
* The tag is already on the remote, so step 9 skips entirely and step 10 —
|
|
193
244
|
* the GitHub Release body — is the FIRST irreversible transition the run
|
|
194
245
|
* reaches. The only state in which the Release guard can be exercised on
|
|
195
|
-
* its own, because a marker otherwise stops the run at step
|
|
246
|
+
* its own, because a marker otherwise stops the run at step 9.
|
|
196
247
|
*/
|
|
197
248
|
tag_already_remote?: boolean;
|
|
198
249
|
/**
|
|
199
250
|
* The release PR is ALREADY merged when the run starts — the real
|
|
200
|
-
* resume-after-merge state, in which steps 1 through
|
|
201
|
-
* the
|
|
251
|
+
* resume-after-merge state, in which steps 1 through 6 skip, step 7 still
|
|
252
|
+
* verifies the ledger (on the trunk, since the branch is gone) and step 8
|
|
253
|
+
* is the first step that does work.
|
|
202
254
|
*
|
|
203
255
|
* Added 2026-09-02 with `guard_release_branch_push`. Before that guard, a
|
|
204
|
-
* marker scenario could start from scratch and still reach step
|
|
256
|
+
* marker scenario could start from scratch and still reach step 9, because
|
|
205
257
|
* nothing between the changelog write and the tag looked at the section.
|
|
206
258
|
* The push guard now stops such a run at step 4 — correctly, and that is
|
|
207
259
|
* the point of it — which makes this knob the only way left to exercise
|
|
@@ -297,9 +349,14 @@ class FakeWorld {
|
|
|
297
349
|
private push_rejections: number;
|
|
298
350
|
private behind_probes: number;
|
|
299
351
|
private branch_behind_main: number;
|
|
352
|
+
private branch_behind_remote: number;
|
|
353
|
+
private readonly branch_ahead_remote: number;
|
|
300
354
|
private merge_races_once: boolean;
|
|
301
355
|
private merge_fails_hard: boolean;
|
|
302
356
|
private checks_fail: boolean;
|
|
357
|
+
private readonly ledger_on_branch: boolean;
|
|
358
|
+
private readonly ledger_on_trunk: boolean | undefined;
|
|
359
|
+
private readonly disposition_verdict: { status: number; stdout: string; stderr: string };
|
|
303
360
|
private readonly changelog: string;
|
|
304
361
|
/** Working-tree content; see `WorldConfig.changelog_file`. */
|
|
305
362
|
readonly changelog_file: string;
|
|
@@ -316,11 +373,20 @@ class FakeWorld {
|
|
|
316
373
|
this.tag_remote = cfg.tag_already_remote ?? false;
|
|
317
374
|
this.behind_probes = cfg.behind_probes ?? 0;
|
|
318
375
|
this.branch_behind_main = cfg.branch_behind_main ?? 0;
|
|
376
|
+
this.branch_exists_remote = cfg.branch_exists_remote ?? true;
|
|
377
|
+
this.branch_behind_remote = cfg.branch_behind_remote ?? 0;
|
|
378
|
+
this.branch_ahead_remote = cfg.branch_ahead_remote ?? 1;
|
|
319
379
|
this.merge_races_once = cfg.merge_races_once ?? false;
|
|
320
380
|
this.merge_fails_hard = cfg.merge_fails_hard ?? false;
|
|
321
381
|
this.checks_fail = cfg.checks_fail ?? false;
|
|
322
382
|
this.pr_merged = cfg.pr_already_merged ?? false;
|
|
323
383
|
this.release_verify_fails = cfg.release_verify_fails ?? false;
|
|
384
|
+
// Defaults keep every pre-2026-09-11 scenario green: the ledger is
|
|
385
|
+
// already on the branch and the gate is happy, so step 7 is a two-call
|
|
386
|
+
// no-op and the sequencing scenarios stay about sequencing.
|
|
387
|
+
this.ledger_on_branch = cfg.ledger_on_branch ?? true;
|
|
388
|
+
this.ledger_on_trunk = cfg.ledger_on_trunk;
|
|
389
|
+
this.disposition_verdict = cfg.disposition_verdict ?? { status: 0, stdout: '', stderr: '' };
|
|
324
390
|
}
|
|
325
391
|
|
|
326
392
|
exec(args: readonly string[]): ExecResult {
|
|
@@ -344,6 +410,18 @@ class FakeWorld {
|
|
|
344
410
|
this.branch_behind_main = 0;
|
|
345
411
|
return OK;
|
|
346
412
|
}
|
|
413
|
+
if (cmd === `git rev-list --left-right --count HEAD...origin/${this.branch}`) {
|
|
414
|
+
return {
|
|
415
|
+
...OK,
|
|
416
|
+
stdout: `${String(this.branch_ahead_remote)}\t${String(this.branch_behind_remote)}\n`,
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
if (cmd === `git merge --no-edit origin/${this.branch}`) {
|
|
420
|
+
// A real merge closes the gap, so the re-read sees 0 — the same
|
|
421
|
+
// reason the origin/main handler above zeroes its own counter.
|
|
422
|
+
this.branch_behind_remote = 0;
|
|
423
|
+
return OK;
|
|
424
|
+
}
|
|
347
425
|
if (cmd === `git rev-parse --verify --quiet refs/heads/${this.branch}`) {
|
|
348
426
|
return this.pr_merged ? { ...OK, status: 1 } : OK;
|
|
349
427
|
}
|
|
@@ -483,6 +561,21 @@ class FakeWorld {
|
|
|
483
561
|
return OK;
|
|
484
562
|
}
|
|
485
563
|
|
|
564
|
+
// Step 7 — the findings ledger.
|
|
565
|
+
if (cmd.startsWith('git cat-file -e ')) {
|
|
566
|
+
// REF-AWARE, because one boolean for every ref made the two states
|
|
567
|
+
// that distinguish right from wrong inexpressible: on the branch
|
|
568
|
+
// but not the trunk, and the reverse. A fourth review named the
|
|
569
|
+
// merged-path scenario as asserting a string in the argv rather
|
|
570
|
+
// than the ref that was read.
|
|
571
|
+
const onTrunk = this.ledger_on_trunk ?? this.ledger_on_branch;
|
|
572
|
+
const present = cmd.includes('/main:') ? onTrunk : this.ledger_on_branch;
|
|
573
|
+
return present ? OK : { ...OK, status: 1 };
|
|
574
|
+
}
|
|
575
|
+
if (cmd.includes('check_finding_dispositions --release')) {
|
|
576
|
+
return { ...this.disposition_verdict };
|
|
577
|
+
}
|
|
578
|
+
|
|
486
579
|
throw new Error(`FakeWorld: unscripted command: ${cmd}`);
|
|
487
580
|
}
|
|
488
581
|
}
|
|
@@ -498,6 +591,16 @@ interface Scenario {
|
|
|
498
591
|
/** What the scenario proves — printed by the CLI. */
|
|
499
592
|
summary: string;
|
|
500
593
|
config: WorldConfig;
|
|
594
|
+
/**
|
|
595
|
+
* Run THIS instead of `execute()`.
|
|
596
|
+
*
|
|
597
|
+
* Added for the release-holds scenarios, whose boundary is `preflight()` —
|
|
598
|
+
* it runs before step 1 and therefore before `execute()`, so a scenario
|
|
599
|
+
* driving `execute()` could never reach it. Every pre-existing scenario
|
|
600
|
+
* omits the field and keeps `execute()` byte-identically; the override is
|
|
601
|
+
* additive and changes nothing it does not set.
|
|
602
|
+
*/
|
|
603
|
+
run?: () => void;
|
|
501
604
|
/** Whether execute() is expected to complete (vs die/throw). */
|
|
502
605
|
expect_success: boolean;
|
|
503
606
|
/** Extra assertions over the finished world; return failure strings. */
|
|
@@ -514,7 +617,270 @@ function _count(world: FakeWorld, needle: string): number {
|
|
|
514
617
|
return world.calls.filter((c) => c === needle).length;
|
|
515
618
|
}
|
|
516
619
|
|
|
620
|
+
|
|
621
|
+
/* ------------------------------------------------------------------ *
|
|
622
|
+
* Release-holds fixtures (template rule 28, roadmap step 4.3).
|
|
623
|
+
*
|
|
624
|
+
* A throwaway tree per scenario, because the assertion is about what the
|
|
625
|
+
* evaluator READS — a mocked return would assert the mock.
|
|
626
|
+
* ------------------------------------------------------------------ */
|
|
627
|
+
|
|
628
|
+
function _holdFixtureRoot(name: string, body: string): string {
|
|
629
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), `release-holds-${name}-`));
|
|
630
|
+
const dir = path.join(root, 'agents', 'roadmaps');
|
|
631
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
632
|
+
fs.writeFileSync(path.join(dir, 'fixture.md'), body, 'utf8');
|
|
633
|
+
return root;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
function _openHoldBody(channel: string): string {
|
|
637
|
+
return [
|
|
638
|
+
'## Phase 1',
|
|
639
|
+
'- [x] **1.1 opener** <!-- opens-hold: half-wired -->',
|
|
640
|
+
' verify: the opener\'s own check',
|
|
641
|
+
'- [ ] **1.2 clearer** <!-- clears-hold: half-wired -->',
|
|
642
|
+
' verify: ./scripts-run src/scripts/check_surface_whole',
|
|
643
|
+
'',
|
|
644
|
+
'## Release holds',
|
|
645
|
+
'',
|
|
646
|
+
'### hold: half-wired',
|
|
647
|
+
`- **Channel:** ${channel}`,
|
|
648
|
+
'- **Opened by:** 1.1',
|
|
649
|
+
'- **Cleared by:** 1.2',
|
|
650
|
+
'- **State:** the surface is half wired while this is open.',
|
|
651
|
+
'- **Why not a guard:** the entry point is reachable and cannot be made inert.',
|
|
652
|
+
'',
|
|
653
|
+
].join('\n');
|
|
654
|
+
}
|
|
655
|
+
|
|
517
656
|
const SCENARIOS: Record<string, Scenario> = {
|
|
657
|
+
'release-hold-open-all-refuses-the-cut': {
|
|
658
|
+
summary:
|
|
659
|
+
'pre-flight: an open hold with `Channel: all` refuses the cut, naming the roadmap, the hold, its opener, its closer and the closer\'s verify command',
|
|
660
|
+
config: {},
|
|
661
|
+
expect_success: false,
|
|
662
|
+
run: () => {
|
|
663
|
+
releaseHoldPreflight(_holdFixtureRoot('all', _openHoldBody('all')), die);
|
|
664
|
+
},
|
|
665
|
+
verify: (_w, error) => {
|
|
666
|
+
const f: string[] = [];
|
|
667
|
+
const msg = error ?? '';
|
|
668
|
+
// All five fields, asserted individually. A refusal missing any one
|
|
669
|
+
// of them tells the operator they are stuck rather than what to do,
|
|
670
|
+
// which is the failure step 4.1 names.
|
|
671
|
+
for (const field of [
|
|
672
|
+
'fixture.md',
|
|
673
|
+
'half-wired',
|
|
674
|
+
'opened by: 1.1',
|
|
675
|
+
'cleared by: 1.2',
|
|
676
|
+
'check_surface_whole',
|
|
677
|
+
]) {
|
|
678
|
+
_expect(msg.includes(field), `the refusal never named \`${field}\`: ${msg}`, f);
|
|
679
|
+
}
|
|
680
|
+
for (const way of ['finish the clearing step', '-next.N', 'release line']) {
|
|
681
|
+
_expect(msg.includes(way), `the refusal never offered \`${way}\`: ${msg}`, f);
|
|
682
|
+
}
|
|
683
|
+
return f;
|
|
684
|
+
},
|
|
685
|
+
},
|
|
686
|
+
'release-hold-open-latest-refuses-the-stable-cut': {
|
|
687
|
+
summary:
|
|
688
|
+
'pre-flight: an open hold with `Channel: latest` still refuses, because release.ts cuts a bare X.Y.Z and never silently redirects it to a prerelease',
|
|
689
|
+
config: {},
|
|
690
|
+
expect_success: false,
|
|
691
|
+
run: () => {
|
|
692
|
+
releaseHoldPreflight(_holdFixtureRoot('latest', _openHoldBody('latest')), die);
|
|
693
|
+
},
|
|
694
|
+
verify: (_w, error) => {
|
|
695
|
+
const f: string[] = [];
|
|
696
|
+
const msg = error ?? '';
|
|
697
|
+
_expect(msg.includes('half-wired'), `the refusal never named the hold: ${msg}`, f);
|
|
698
|
+
_expect(
|
|
699
|
+
msg.includes('channel latest'),
|
|
700
|
+
`the refusal never showed the hold's own channel: ${msg}`,
|
|
701
|
+
f,
|
|
702
|
+
);
|
|
703
|
+
// The silent-redirect prohibition, asserted as a behaviour rather
|
|
704
|
+
// than as a grep: the run DIED. A release.ts that quietly converted
|
|
705
|
+
// this into `-next.N` would have completed.
|
|
706
|
+
_expect(
|
|
707
|
+
msg.startsWith('SystemExit('),
|
|
708
|
+
`the stable cut was not refused — a silent channel redirect: ${msg}`,
|
|
709
|
+
f,
|
|
710
|
+
);
|
|
711
|
+
return f;
|
|
712
|
+
},
|
|
713
|
+
},
|
|
714
|
+
'release-hold-not-evaluable-refuses-the-cut': {
|
|
715
|
+
summary:
|
|
716
|
+
'pre-flight: a malformed declaration the evaluator cannot read refuses the cut — there is no path on which "could not evaluate" reads as safe',
|
|
717
|
+
config: {},
|
|
718
|
+
expect_success: false,
|
|
719
|
+
run: () => {
|
|
720
|
+
// Missing `Why not a guard:`, which rule 28 makes mandatory. The
|
|
721
|
+
// evaluator can parse the file but not trust the declaration, which
|
|
722
|
+
// is exactly the not-evaluable row.
|
|
723
|
+
const body = _openHoldBody('all').replace(
|
|
724
|
+
/- \*\*Why not a guard:\*\*.*\n/,
|
|
725
|
+
'',
|
|
726
|
+
);
|
|
727
|
+
releaseHoldPreflight(_holdFixtureRoot('bad', body), die);
|
|
728
|
+
},
|
|
729
|
+
verify: (_w, error) => {
|
|
730
|
+
const f: string[] = [];
|
|
731
|
+
const msg = error ?? '';
|
|
732
|
+
_expect(
|
|
733
|
+
msg.startsWith('SystemExit('),
|
|
734
|
+
`an unreadable declaration did not refuse — it failed OPEN: ${msg}`,
|
|
735
|
+
f,
|
|
736
|
+
);
|
|
737
|
+
_expect(
|
|
738
|
+
msg.includes('not evaluable'),
|
|
739
|
+
`the refusal did not say the declaration was unreadable: ${msg}`,
|
|
740
|
+
f,
|
|
741
|
+
);
|
|
742
|
+
_expect(
|
|
743
|
+
msg.includes('Why not a guard'),
|
|
744
|
+
`the refusal did not name what was wrong with it: ${msg}`,
|
|
745
|
+
f,
|
|
746
|
+
);
|
|
747
|
+
return f;
|
|
748
|
+
},
|
|
749
|
+
},
|
|
750
|
+
|
|
751
|
+
'findings-ledger-verified-on-the-remote-branch': {
|
|
752
|
+
summary:
|
|
753
|
+
'step 7: the ledger is on the remote branch and the gate passes — the release continues without producing anything',
|
|
754
|
+
// Step 7 VERIFIES. The ingest lives in the self-review workflow, which
|
|
755
|
+
// commits the ledger to the release branch; two review rounds showed no
|
|
756
|
+
// placement inside this script can produce it, because the gate it
|
|
757
|
+
// anticipates already reds the check step 6 waits on.
|
|
758
|
+
// `resume: false` is load-bearing, not tidiness. `resume` defaults to
|
|
759
|
+
// TRUE here, and on a resumed run the PR number is already in hand — so
|
|
760
|
+
// a scenario that kept the default could not see the `--pr` flag being
|
|
761
|
+
// dropped, which is exactly the hole this scenario asserts against.
|
|
762
|
+
config: { ledger_on_branch: true, resume: false },
|
|
763
|
+
expect_success: true,
|
|
764
|
+
verify: (w) => {
|
|
765
|
+
const f: string[] = [];
|
|
766
|
+
const probe = w.calls.find((c) => c.startsWith('git cat-file -e '));
|
|
767
|
+
_expect(probe !== undefined, 'the ledger presence was never probed', f);
|
|
768
|
+
_expect(
|
|
769
|
+
(probe ?? '').includes('origin/'),
|
|
770
|
+
`the probe read a local ref, which a failed push makes true: ${probe ?? '(none)'}`,
|
|
771
|
+
f,
|
|
772
|
+
);
|
|
773
|
+
_expect(
|
|
774
|
+
!w.calls.some((c) => c.includes('--ingest')),
|
|
775
|
+
'step 7 ingested — that belongs to the workflow now, not the release',
|
|
776
|
+
f,
|
|
777
|
+
);
|
|
778
|
+
_expect(
|
|
779
|
+
!w.calls.some((c) => c.startsWith('gh run download')),
|
|
780
|
+
'step 7 downloaded an artifact — the release no longer produces the ledger',
|
|
781
|
+
f,
|
|
782
|
+
);
|
|
783
|
+
// `--pr` is what makes the gate compare the review's own report
|
|
784
|
+
// against the committed ledger. The PR number used to come only
|
|
785
|
+
// from the resume-time probe, so on a FIRST run — this scenario —
|
|
786
|
+
// the flag was silently dropped and the release passed a strictly
|
|
787
|
+
// weaker check than the one it was trying to pre-satisfy.
|
|
788
|
+
_expect(
|
|
789
|
+
w.calls.some(
|
|
790
|
+
(c) => c.includes('check_finding_dispositions --release') && c.includes('--pr '),
|
|
791
|
+
),
|
|
792
|
+
'the disposition gate ran without --pr, so an un-ingested finding would pass here',
|
|
793
|
+
f,
|
|
794
|
+
);
|
|
795
|
+
_expect(
|
|
796
|
+
w.calls.includes(`gh pr merge ${w.branch} --merge --delete-branch`),
|
|
797
|
+
'the release never reached the merge',
|
|
798
|
+
f,
|
|
799
|
+
);
|
|
800
|
+
return f;
|
|
801
|
+
},
|
|
802
|
+
},
|
|
803
|
+
'findings-ledger-absent-stops-the-release': {
|
|
804
|
+
summary:
|
|
805
|
+
'step 7: no ledger on the remote branch — the release STOPS rather than merging and tagging without one',
|
|
806
|
+
config: { ledger_on_branch: false },
|
|
807
|
+
expect_success: false,
|
|
808
|
+
verify: (w, error) => {
|
|
809
|
+
const f: string[] = [];
|
|
810
|
+
_expect(
|
|
811
|
+
(error ?? '').includes('is not on origin/'),
|
|
812
|
+
`the stop did not name the ref it checked: ${error ?? '(no error)'}`,
|
|
813
|
+
f,
|
|
814
|
+
);
|
|
815
|
+
_expect(
|
|
816
|
+
!w.calls.includes(`gh pr merge ${w.branch} --merge --delete-branch`),
|
|
817
|
+
'merged with no ledger — the failure this step exists to end',
|
|
818
|
+
f,
|
|
819
|
+
);
|
|
820
|
+
_expect(!w.tag_remote, 'tagged with no ledger', f);
|
|
821
|
+
return f;
|
|
822
|
+
},
|
|
823
|
+
},
|
|
824
|
+
'findings-ledger-absent-after-a-hand-merge-still-stops': {
|
|
825
|
+
summary:
|
|
826
|
+
'step 7: the PR is already merged and no ledger reached the trunk — the release STOPS instead of tagging a version whose ledger does not exist',
|
|
827
|
+
// The reachable path to a shipped version with no ledger, and the one
|
|
828
|
+
// the old code asserted away: the ingest produces nothing, step 7
|
|
829
|
+
// stops, a human merges the PR, and the resumed run used to print "the
|
|
830
|
+
// ledger rode in with it" without reading anything.
|
|
831
|
+
// The two knobs differ on purpose: the ledger IS on the release branch
|
|
832
|
+
// and is NOT on the trunk. A run that read the branch ref would pass
|
|
833
|
+
// here, which is the wrong answer — the branch is deleted and the merge
|
|
834
|
+
// is what decides whether the ledger shipped.
|
|
835
|
+
config: { pr_already_merged: true, ledger_on_branch: true, ledger_on_trunk: false },
|
|
836
|
+
expect_success: false,
|
|
837
|
+
verify: (w, error) => {
|
|
838
|
+
const f: string[] = [];
|
|
839
|
+
_expect(
|
|
840
|
+
(error ?? '').includes('already merged'),
|
|
841
|
+
`the stop did not name the merged state: ${error ?? '(no error)'}`,
|
|
842
|
+
f,
|
|
843
|
+
);
|
|
844
|
+
_expect(
|
|
845
|
+
w.calls.some((c) => c.startsWith('git cat-file -e ') && c.includes('/main:')),
|
|
846
|
+
'the merged path never asked the trunk whether the ledger is there',
|
|
847
|
+
f,
|
|
848
|
+
);
|
|
849
|
+
_expect(!w.tag_remote, 'tagged a version whose findings ledger does not exist', f);
|
|
850
|
+
return f;
|
|
851
|
+
},
|
|
852
|
+
},
|
|
853
|
+
'findings-ledger-undispositioned-stops-and-shows-why': {
|
|
854
|
+
summary:
|
|
855
|
+
'step 7: the disposition gate refuses — the release stops and the operator is shown the gate report, not just its scan line',
|
|
856
|
+
config: {
|
|
857
|
+
ledger_on_branch: true,
|
|
858
|
+
disposition_verdict: {
|
|
859
|
+
status: 1,
|
|
860
|
+
stdout: 'scanned: 1',
|
|
861
|
+
stderr: '- `a1` (critical security: x): no disposition status',
|
|
862
|
+
},
|
|
863
|
+
},
|
|
864
|
+
expect_success: false,
|
|
865
|
+
verify: (w, error) => {
|
|
866
|
+
const f: string[] = [];
|
|
867
|
+
// The defect an independent review reproduced: the stop message
|
|
868
|
+
// preferred stdout, so the operator saw `scanned: 1` and the entire
|
|
869
|
+
// per-finding diagnosis — which the gate writes to stderr — was
|
|
870
|
+
// discarded on the one path where the message has to carry content.
|
|
871
|
+
_expect(
|
|
872
|
+
(error ?? '').includes('no disposition status'),
|
|
873
|
+
`the gate's own diagnosis was swallowed: ${error ?? '(no error)'}`,
|
|
874
|
+
f,
|
|
875
|
+
);
|
|
876
|
+
_expect(
|
|
877
|
+
!w.calls.includes(`gh pr merge ${w.branch} --merge --delete-branch`),
|
|
878
|
+
'merged over an undispositioned ledger',
|
|
879
|
+
f,
|
|
880
|
+
);
|
|
881
|
+
return f;
|
|
882
|
+
},
|
|
883
|
+
},
|
|
518
884
|
'plain-run-reuses-an-existing-branch': {
|
|
519
885
|
summary:
|
|
520
886
|
'step 1: a PLAIN run (no --resume) over an existing release branch checks it out instead of dying at `git checkout -b`',
|
|
@@ -573,6 +939,81 @@ const SCENARIOS: Record<string, Scenario> = {
|
|
|
573
939
|
return f;
|
|
574
940
|
},
|
|
575
941
|
},
|
|
942
|
+
'remote-branch-moved-merges-before-push': {
|
|
943
|
+
summary:
|
|
944
|
+
'step 4: a release branch that diverged on the remote is merged in BEFORE the push — not after a rejection the local preflight never lets git produce',
|
|
945
|
+
// Measured 2026-09-12 on 16.0.0. `origin/release/16.0.0` carried five
|
|
946
|
+
// commits this checkout did not have. `push_release_branch` has a
|
|
947
|
+
// merge-and-retry for exactly that, keyed on git's rejection wording —
|
|
948
|
+
// but the local pre-push preflight refused first, with
|
|
949
|
+
// `check_pr_ci_current`'s own message naming the divergence. git never
|
|
950
|
+
// printed `[rejected]`, `_is_non_fast_forward` correctly answered
|
|
951
|
+
// "not the fetch-and-retry case" on the text it was given, and the
|
|
952
|
+
// recovery was unreachable from the state it was written for.
|
|
953
|
+
config: { branch_behind_remote: 5 },
|
|
954
|
+
expect_success: true,
|
|
955
|
+
verify: (w) => {
|
|
956
|
+
const f: string[] = [];
|
|
957
|
+
const merge = w.calls.indexOf(`git merge --no-edit origin/${w.branch}`);
|
|
958
|
+
const push = w.calls.indexOf(`git push -u origin ${w.branch}`);
|
|
959
|
+
_expect(merge >= 0, 'the diverged remote branch was never merged in', f);
|
|
960
|
+
_expect(push >= 0, 'the branch was never pushed', f);
|
|
961
|
+
// Ordering is the whole point: a merge only AFTER the push means
|
|
962
|
+
// the reactive path ran, and that path is what the gate pre-empts.
|
|
963
|
+
_expect(
|
|
964
|
+
merge >= 0 && push >= 0 && merge < push,
|
|
965
|
+
'the remote branch was merged after the push, not before it',
|
|
966
|
+
f,
|
|
967
|
+
);
|
|
968
|
+
_expect(
|
|
969
|
+
_count(w, `git merge --no-edit origin/${w.branch}`) === 1,
|
|
970
|
+
'merged the remote branch more than once — the probe is not re-read after the merge',
|
|
971
|
+
f,
|
|
972
|
+
);
|
|
973
|
+
return f;
|
|
974
|
+
},
|
|
975
|
+
},
|
|
976
|
+
'remote-already-at-head-skips-the-push': {
|
|
977
|
+
summary:
|
|
978
|
+
'step 4: a remote already carrying this commit is not pushed to again — the no-op push still runs the pre-push hook, and the hook refuses',
|
|
979
|
+
// The second half of the same 16.0.0 failure, and the one no rejection
|
|
980
|
+
// text can reach. The push had succeeded; a resumed run re-entered
|
|
981
|
+
// step 4 and pushed again. `git push -u` is idempotent on the network
|
|
982
|
+
// side, but git runs the pre-push hook BEFORE discovering there is
|
|
983
|
+
// nothing to send — so the preflight was handed the head this run had
|
|
984
|
+
// just pushed, found its CI still pending, and killed the release.
|
|
985
|
+
config: { branch_ahead_remote: 0, branch_behind_remote: 0 },
|
|
986
|
+
expect_success: true,
|
|
987
|
+
verify: (w) => {
|
|
988
|
+
const f: string[] = [];
|
|
989
|
+
_expect(
|
|
990
|
+
!w.calls.includes(`git push -u origin ${w.branch}`),
|
|
991
|
+
'pushed a branch the remote already carries — that push runs the pre-push hook for nothing',
|
|
992
|
+
f,
|
|
993
|
+
);
|
|
994
|
+
return f;
|
|
995
|
+
},
|
|
996
|
+
},
|
|
997
|
+
'unpushed-branch-still-pushes': {
|
|
998
|
+
summary:
|
|
999
|
+
'step 4: a branch that was never pushed is pushed, and no distance is probed against a remote ref that does not exist',
|
|
1000
|
+
// The near-miss for the direction the new probe opens: `git rev-list
|
|
1001
|
+
// HEAD...origin/<branch>` against a missing ref is the 9.26.0 shape all
|
|
1002
|
+
// over again — an exit from the recovery masking the real state.
|
|
1003
|
+
// `--exit-code` on the ls-remote probe is what keeps it closed.
|
|
1004
|
+
config: { branch_exists_remote: false },
|
|
1005
|
+
expect_success: true,
|
|
1006
|
+
verify: (w) => {
|
|
1007
|
+
const f: string[] = [];
|
|
1008
|
+
_expect(w.calls.includes(`git push -u origin ${w.branch}`), 'the branch was never pushed', f);
|
|
1009
|
+
_expect(
|
|
1010
|
+
!w.calls.includes(`git rev-list --left-right --count HEAD...origin/${w.branch}`),
|
|
1011
|
+
'probed the distance to a remote ref that does not exist',
|
|
1012
|
+
f,
|
|
1013
|
+
);
|
|
1014
|
+
return f;
|
|
1015
|
+
},
|
|
1016
|
+
},
|
|
576
1017
|
'local-gates-refuse-before-push': {
|
|
577
1018
|
summary:
|
|
578
1019
|
'step 4: a red local release gate stops the run BEFORE the branch is pushed — no branch on origin, no PR, no CI',
|
|
@@ -664,7 +1105,7 @@ const SCENARIOS: Record<string, Scenario> = {
|
|
|
664
1105
|
},
|
|
665
1106
|
'marker-refuses-before-tag': {
|
|
666
1107
|
summary:
|
|
667
|
-
'step
|
|
1108
|
+
'step 9: a draft marker in the merged section refuses BEFORE the annotated tag is created',
|
|
668
1109
|
config: {
|
|
669
1110
|
pr_already_merged: true,
|
|
670
1111
|
changelog_file: markedChangelogFixture(current_version()),
|
|
@@ -691,7 +1132,7 @@ const SCENARIOS: Record<string, Scenario> = {
|
|
|
691
1132
|
},
|
|
692
1133
|
'marker-refuses-resumed-tag-push': {
|
|
693
1134
|
summary:
|
|
694
|
-
'step
|
|
1135
|
+
'step 9 (§ 3.2): a tag created but never pushed is NOT pushed when its section carries the marker',
|
|
695
1136
|
config: {
|
|
696
1137
|
pr_already_merged: true,
|
|
697
1138
|
tag_created_unpushed: true,
|
|
@@ -720,7 +1161,7 @@ const SCENARIOS: Record<string, Scenario> = {
|
|
|
720
1161
|
},
|
|
721
1162
|
'marker-refuses-github-release': {
|
|
722
1163
|
summary:
|
|
723
|
-
'step
|
|
1164
|
+
'step 10: with the tag already pushed, a marker in the TAGGED section refuses before the Release is created',
|
|
724
1165
|
config: {
|
|
725
1166
|
pr_already_merged: true,
|
|
726
1167
|
tag_already_remote: true,
|
|
@@ -793,7 +1234,7 @@ const SCENARIOS: Record<string, Scenario> = {
|
|
|
793
1234
|
},
|
|
794
1235
|
},
|
|
795
1236
|
'behind-then-merge': {
|
|
796
|
-
summary: 'step
|
|
1237
|
+
summary: 'step 8: head BEHIND main — update branch, re-run checks, merge (9.16.0)',
|
|
797
1238
|
config: { behind_probes: 1 },
|
|
798
1239
|
expect_success: true,
|
|
799
1240
|
verify: (w, _err) => {
|
|
@@ -813,7 +1254,7 @@ const SCENARIOS: Record<string, Scenario> = {
|
|
|
813
1254
|
},
|
|
814
1255
|
},
|
|
815
1256
|
'merge-race-recovers': {
|
|
816
|
-
summary: 'step
|
|
1257
|
+
summary: 'step 8: main moves between the CLEAN probe and the merge — retry, not crash',
|
|
817
1258
|
config: { merge_races_once: true },
|
|
818
1259
|
expect_success: true,
|
|
819
1260
|
verify: (w, _err) => {
|
|
@@ -828,7 +1269,7 @@ const SCENARIOS: Record<string, Scenario> = {
|
|
|
828
1269
|
},
|
|
829
1270
|
},
|
|
830
1271
|
'behind-forever-dies': {
|
|
831
|
-
summary: `step
|
|
1272
|
+
summary: `step 8: BEHIND persists past ${_MERGE_UPDATE_ROUNDS} update rounds — die with the resume command`,
|
|
832
1273
|
config: { behind_probes: 99 },
|
|
833
1274
|
expect_success: false,
|
|
834
1275
|
verify: (w, err) => {
|
|
@@ -845,7 +1286,7 @@ const SCENARIOS: Record<string, Scenario> = {
|
|
|
845
1286
|
},
|
|
846
1287
|
},
|
|
847
1288
|
'merge-fails-hard-surfaces': {
|
|
848
|
-
summary: 'step
|
|
1289
|
+
summary: 'step 8: a non-BEHIND merge failure surfaces unchanged instead of looping',
|
|
849
1290
|
config: { merge_fails_hard: true },
|
|
850
1291
|
expect_success: false,
|
|
851
1292
|
verify: (w, err) => {
|
|
@@ -905,10 +1346,14 @@ function run_scenario(name: string): ScenarioOutcome {
|
|
|
905
1346
|
const tree_snapshot = _snapshot_step_2_files();
|
|
906
1347
|
_set_exec_override((args) => world.exec(args));
|
|
907
1348
|
// The working-tree read, faked for the same reason the command layer is.
|
|
908
|
-
// Without this the drill's step
|
|
1349
|
+
// Without this the drill's step 9 reads the repository's real CHANGELOG.md.
|
|
909
1350
|
_set_changelog_reader(() => world.changelog_file);
|
|
910
1351
|
try {
|
|
911
|
-
|
|
1352
|
+
if (scenario.run !== undefined) {
|
|
1353
|
+
scenario.run();
|
|
1354
|
+
} else {
|
|
1355
|
+
execute(plan, { wait_for_checks: true, dry_run: false, resume: scenario.config.resume ?? true });
|
|
1356
|
+
}
|
|
912
1357
|
} catch (err) {
|
|
913
1358
|
if (err instanceof SystemExitError) {
|
|
914
1359
|
error = `SystemExit(${err.code}): ${captured.join('')}`;
|
|
@@ -98,7 +98,7 @@ const CHANGELOG = path.join(REPO_ROOT, 'CHANGELOG.md');
|
|
|
98
98
|
* Mirrors `_set_exec_override` in `release_publication.ts`, and exists for the
|
|
99
99
|
* same reason: `execute()` reads `CHANGELOG.md` off disk at two points (the PR
|
|
100
100
|
* body and the annotated-tag message), and the release drill fakes commands
|
|
101
|
-
* but not the filesystem. Before this seam the drill's step
|
|
101
|
+
* but not the filesystem. Before this seam the drill's step 9 read the
|
|
102
102
|
* repository's REAL changelog — whose current section carries four
|
|
103
103
|
* `_auto-derived, rewrite before merge:_` lines at `14.13.0` — so a
|
|
104
104
|
* publication guard added at that call site refused every drill scenario for a
|