@event4u/agent-config 16.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 +297 -137
- 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 +74 -27
- 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 +47 -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/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/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-H32DGUJH.js → chunk-P6ZHP5W2.js} +1 -1
- 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-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 +90 -56
- package/dist/cli-delegate/cmd_doctor.js +7 -5
- package/dist/cli-delegate/cmd_explain.js +2 -2
- 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 +4 -4
- 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 +2 -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 +2 -2
- 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 +1 -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 +85 -38
- 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 +1832 -1007
- 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 +2007 -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 +398 -39
- 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_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/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 +21 -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/loop-surfaces.yaml +215 -0
- package/src/config/platform-anchor.json +3 -2
- 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/_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/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/forge_protection.ts +267 -0
- package/src/scripts/_lib/hook_platform_keys.ts +27 -0
- 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/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/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/rides_along.ts +102 -0
- package/src/scripts/_lib/settings_renamed_keys.ts +82 -0
- package/src/scripts/_lib/test_delta.ts +166 -0
- package/src/scripts/_lib/test_provenance.ts +171 -0
- 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_enforcement_matrix.ts +624 -0
- package/src/scripts/check_gate_reachability.ts +173 -10
- package/src/scripts/check_generator_sync.ts +954 -0
- package/src/scripts/check_preamble_payload_budget.ts +75 -2
- package/src/scripts/check_release_highlights.ts +33 -1
- package/src/scripts/check_release_holds.ts +567 -0
- 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 +88 -9
- package/src/scripts/hooks/concern_registry.ts +4 -0
- package/src/scripts/hooks/design_pass_hook.ts +128 -3
- 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/rule_inject_hook.ts +49 -1
- package/src/scripts/hooks/run_continuation_hook.ts +17 -20
- package/src/scripts/hooks_doctor.ts +85 -0
- package/src/scripts/hooks_status.ts +19 -0
- package/src/scripts/install.ts +106 -206
- package/src/scripts/janitor.ts +7 -0
- 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/new_roadmap.ts +37 -0
- package/src/scripts/orchestration_record.ts +0 -5
- package/src/scripts/release.ts +14 -98
- package/src/scripts/release_drill.ts +267 -1
- package/src/scripts/release_publication.ts +51 -0
- package/src/scripts/report_enforcement_drift.ts +566 -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/trigger_eval_grandfather.json +0 -1
- package/src/scripts/ui_conformance_probe.ts +622 -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.
|
|
@@ -319,6 +349,8 @@ class FakeWorld {
|
|
|
319
349
|
private push_rejections: number;
|
|
320
350
|
private behind_probes: number;
|
|
321
351
|
private branch_behind_main: number;
|
|
352
|
+
private branch_behind_remote: number;
|
|
353
|
+
private readonly branch_ahead_remote: number;
|
|
322
354
|
private merge_races_once: boolean;
|
|
323
355
|
private merge_fails_hard: boolean;
|
|
324
356
|
private checks_fail: boolean;
|
|
@@ -341,6 +373,9 @@ class FakeWorld {
|
|
|
341
373
|
this.tag_remote = cfg.tag_already_remote ?? false;
|
|
342
374
|
this.behind_probes = cfg.behind_probes ?? 0;
|
|
343
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;
|
|
344
379
|
this.merge_races_once = cfg.merge_races_once ?? false;
|
|
345
380
|
this.merge_fails_hard = cfg.merge_fails_hard ?? false;
|
|
346
381
|
this.checks_fail = cfg.checks_fail ?? false;
|
|
@@ -375,6 +410,18 @@ class FakeWorld {
|
|
|
375
410
|
this.branch_behind_main = 0;
|
|
376
411
|
return OK;
|
|
377
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
|
+
}
|
|
378
425
|
if (cmd === `git rev-parse --verify --quiet refs/heads/${this.branch}`) {
|
|
379
426
|
return this.pr_merged ? { ...OK, status: 1 } : OK;
|
|
380
427
|
}
|
|
@@ -544,6 +591,16 @@ interface Scenario {
|
|
|
544
591
|
/** What the scenario proves — printed by the CLI. */
|
|
545
592
|
summary: string;
|
|
546
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;
|
|
547
604
|
/** Whether execute() is expected to complete (vs die/throw). */
|
|
548
605
|
expect_success: boolean;
|
|
549
606
|
/** Extra assertions over the finished world; return failure strings. */
|
|
@@ -560,7 +617,137 @@ function _count(world: FakeWorld, needle: string): number {
|
|
|
560
617
|
return world.calls.filter((c) => c === needle).length;
|
|
561
618
|
}
|
|
562
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
|
+
|
|
563
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
|
+
|
|
564
751
|
'findings-ledger-verified-on-the-remote-branch': {
|
|
565
752
|
summary:
|
|
566
753
|
'step 7: the ledger is on the remote branch and the gate passes — the release continues without producing anything',
|
|
@@ -752,6 +939,81 @@ const SCENARIOS: Record<string, Scenario> = {
|
|
|
752
939
|
return f;
|
|
753
940
|
},
|
|
754
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
|
+
},
|
|
755
1017
|
'local-gates-refuse-before-push': {
|
|
756
1018
|
summary:
|
|
757
1019
|
'step 4: a red local release gate stops the run BEFORE the branch is pushed — no branch on origin, no PR, no CI',
|
|
@@ -1087,7 +1349,11 @@ function run_scenario(name: string): ScenarioOutcome {
|
|
|
1087
1349
|
// Without this the drill's step 9 reads the repository's real CHANGELOG.md.
|
|
1088
1350
|
_set_changelog_reader(() => world.changelog_file);
|
|
1089
1351
|
try {
|
|
1090
|
-
|
|
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
|
+
}
|
|
1091
1357
|
} catch (err) {
|
|
1092
1358
|
if (err instanceof SystemExitError) {
|
|
1093
1359
|
error = `SystemExit(${err.code}): ${captured.join('')}`;
|
|
@@ -745,8 +745,59 @@ export function guard_release_curation(version: string, prMerged = false): void
|
|
|
745
745
|
);
|
|
746
746
|
}
|
|
747
747
|
|
|
748
|
+
/**
|
|
749
|
+
* What `REMOTE/<branch>` holds relative to HEAD, before anything is pushed.
|
|
750
|
+
*
|
|
751
|
+
* `push_release_branch` used to issue the push and learn the remote's position
|
|
752
|
+
* only from the rejection that came back. That reading is unavailable in two
|
|
753
|
+
* measured states, both on 16.0.0 (2026-09-12): the local pre-push gate refuses
|
|
754
|
+
* a diverged branch before git can print `[rejected]`, and a remote already AT
|
|
755
|
+
* head produces no rejection at all — git runs the pre-push hook before it
|
|
756
|
+
* discovers there is nothing to send, so the hook refuses over CI still pending
|
|
757
|
+
* on the head this run had already pushed. Asking first is the only place both
|
|
758
|
+
* are visible.
|
|
759
|
+
*/
|
|
760
|
+
type RemoteBranchState = 'absent' | 'in-sync' | 'remote-ahead' | 'local-ahead';
|
|
761
|
+
|
|
762
|
+
function _remote_branch_state(branch: string): RemoteBranchState {
|
|
763
|
+
// `--exit-code` so a never-pushed branch reads as a clean non-zero rather
|
|
764
|
+
// than an empty success the count below would then run against a missing
|
|
765
|
+
// ref — the exit-128 masking this file already carries a test for.
|
|
766
|
+
const exists = run(['git', 'ls-remote', '--exit-code', '--heads', REMOTE, branch], {
|
|
767
|
+
check: false,
|
|
768
|
+
capture: true,
|
|
769
|
+
});
|
|
770
|
+
if (exists.returncode !== 0) return 'absent';
|
|
771
|
+
run(['git', 'fetch', REMOTE, branch], { check: false, capture: true });
|
|
772
|
+
const counts = run(['git', 'rev-list', '--left-right', '--count', `HEAD...${REMOTE}/${branch}`], {
|
|
773
|
+
check: false,
|
|
774
|
+
capture: true,
|
|
775
|
+
});
|
|
776
|
+
// Unreadable → push and let git judge, which is the pre-2026-09-12 behaviour.
|
|
777
|
+
if (counts.returncode !== 0) return 'local-ahead';
|
|
778
|
+
const [ahead = '0', behind = '0'] = counts.stdout.trim().split(/\s+/);
|
|
779
|
+
if (Number(behind) > 0) return 'remote-ahead';
|
|
780
|
+
return Number(ahead) > 0 ? 'local-ahead' : 'in-sync';
|
|
781
|
+
}
|
|
782
|
+
|
|
748
783
|
function push_release_branch(branch: string): void {
|
|
784
|
+
let state = _remote_branch_state(branch);
|
|
785
|
+
if (state === 'remote-ahead') {
|
|
786
|
+
process.stdout.write(`↻ ${REMOTE}/${branch} moved under us — merging it in before the push\n`);
|
|
787
|
+
run(['git', 'merge', '--no-edit', `${REMOTE}/${branch}`]);
|
|
788
|
+
// Re-read: a fast-forward merge lands HEAD exactly on the remote, and
|
|
789
|
+
// the push that followed would then be the no-op case below.
|
|
790
|
+
state = _remote_branch_state(branch);
|
|
791
|
+
}
|
|
792
|
+
|
|
749
793
|
guard_release_branch_push(branch);
|
|
794
|
+
|
|
795
|
+
// Ahead of the skip on purpose: the guard validates the release content,
|
|
796
|
+
// which has to be right whether or not this run is the one that sends it.
|
|
797
|
+
if (state === 'in-sync') {
|
|
798
|
+
process.stdout.write(`✅ ${REMOTE}/${branch} already carries this commit — nothing to push\n`);
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
750
801
|
const first = run(['git', 'push', '-u', REMOTE, branch], { check: false, capture: true });
|
|
751
802
|
if (first.returncode === 0) {
|
|
752
803
|
process.stdout.write(first.stdout);
|