@event4u/agent-config 14.13.0 → 14.14.1
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 +138 -376
- package/README.md +1 -1
- package/dist/agent-src/commands/analyze/inbox.md +36 -0
- package/dist/agent-src/commands/pr/create.md +16 -1
- package/dist/agent-src/contexts/execution/terminal-states.md +25 -4
- package/dist/agent-src/rules/code-comment-discipline.md +6 -0
- package/dist/agent-src/rules/missing-skill-recovery.md +2 -2
- package/dist/agent-src/rules/token-budget-discipline.md +2 -2
- package/dist/agent-src/scripts/archive_completed_roadmaps.ts +6 -6
- package/dist/agent-src/scripts/guarded_baseline.ts +22 -7
- package/dist/agent-src/scripts/stubs_due.ts +118 -2
- package/dist/agent-src/scripts/update_roadmap_progress.ts +8 -8
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/cli/registry.js +1 -0
- package/dist/cli-delegate/{chunk-ILMUPLGW.js → chunk-7RXIKF62.js} +159 -86
- package/dist/cli-delegate/cmd_conformance.js +39 -2
- package/dist/cli-delegate/cmd_doctor.js +1 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +7 -7
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +1 -1
- package/dist/discovery/dormancy-report.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +2 -2
- package/dist/discovery/trust-report.md +1 -1
- package/dist/discovery/workspaces.json +2 -2
- package/dist/hooks/dispatch.js +1696 -812
- package/dist/mcp/registry-manifest.json +2 -2
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +297 -115
- package/docs/guidelines/agent-infra/guarded-baseline.md +1 -1
- package/docs/guidelines/code-clarity.md +18 -0
- package/package.json +1 -1
- package/src/agent-src/scripts/archive_completed_roadmaps.ts +6 -6
- package/src/agent-src/scripts/guarded_baseline.ts +22 -7
- package/src/agent-src/scripts/stubs_due.ts +118 -2
- package/src/agent-src/scripts/update_roadmap_progress.ts +8 -8
- package/src/config/assurance-capability-registry.json +1 -1
- package/src/config/estate-count-budget.json +20 -1
- package/src/config/evaluator-budgets.json +4 -4
- package/src/config/gate-coverage.yml +211 -1
- package/src/config/gate-reachability-exemptions.json +1 -0
- package/src/config/gate-violation-baselines.json +9 -3
- package/src/config/preamble-payload-budget.json +19 -11
- package/src/config/rule-stub-ceilings.json +14 -7
- package/src/config/web-launch-readiness.json +4 -2
- package/src/scripts/_cli/_doctor_emit.ts +71 -0
- package/src/scripts/_cli/_doctor_strict.ts +163 -0
- package/src/scripts/_cli/cmd_doctor.ts +16 -37
- package/src/scripts/_dispatch.bash +11 -4
- package/src/scripts/_lib/activation_ladder.ts +47 -3
- package/src/scripts/_lib/activation_receipt_producer.ts +341 -0
- package/src/scripts/_lib/adr_frontmatter.ts +26 -0
- package/src/scripts/_lib/branch_convergence.ts +186 -0
- package/src/scripts/_lib/candidate_pair_delta.ts +349 -0
- package/src/scripts/_lib/carrier_status.ts +118 -0
- package/src/scripts/_lib/cascade_stage_enumeration.ts +92 -0
- package/src/scripts/_lib/context_observation.ts +132 -0
- package/src/scripts/_lib/continuation_ladder.ts +188 -0
- package/src/scripts/_lib/corpus_manifest.ts +726 -0
- package/src/scripts/_lib/delivery_arm_experiment.ts +18 -2
- package/src/scripts/_lib/estate_carrier_cases.ts +72 -0
- package/src/scripts/_lib/evaluation_cascade.ts +452 -0
- package/src/scripts/_lib/evolution_roi.ts +562 -0
- package/src/scripts/_lib/experiment_binding.ts +95 -0
- package/src/scripts/_lib/harness_evolution_guards.ts +10 -0
- package/src/scripts/_lib/ignored_blocker.ts +8 -1
- package/src/scripts/_lib/lexical_shortlist.ts +215 -0
- package/src/scripts/_lib/llm_candidate_proposer.ts +461 -0
- package/src/scripts/_lib/llm_proposer_transport.ts +153 -0
- package/src/scripts/_lib/outcome_envelope.ts +6 -1
- package/src/scripts/_lib/outcome_vocabularies.ts +135 -5
- package/src/scripts/_lib/pathology_archive.ts +393 -0
- package/src/scripts/_lib/promotion_capability.ts +315 -0
- package/src/scripts/_lib/promotion_evidence.ts +414 -0
- package/src/scripts/_lib/promotion_review.ts +294 -0
- package/src/scripts/_lib/prompt_shape.ts +92 -0
- package/src/scripts/_lib/release_highlights.ts +79 -1
- package/src/scripts/_lib/release_material.ts +125 -2
- package/src/scripts/_lib/repeated_failure.ts +19 -2
- package/src/scripts/_lib/routing_corpus.ts +307 -0
- package/src/scripts/_lib/routing_index_input.ts +104 -0
- package/src/scripts/_lib/rule_injection.ts +22 -1
- package/src/scripts/_lib/runtime_journal.ts +11 -4
- package/src/scripts/_lib/semantic_noop.ts +167 -0
- package/src/scripts/activation_receipt.ts +258 -0
- package/src/scripts/ai_council/cross_exam.ts +128 -0
- package/src/scripts/ai_council/explain_route.ts +157 -0
- package/src/scripts/ai_council/information_gain.ts +293 -0
- package/src/scripts/ai_council/leakage_corpus.ts +385 -0
- package/src/scripts/ai_council/leakage_patterns.ts +453 -0
- package/src/scripts/ai_council/minority_retention.ts +171 -0
- package/src/scripts/ai_council/recouncil_savings.ts +299 -0
- package/src/scripts/ai_council/replay_route.ts +223 -0
- package/src/scripts/ai_council/reviewer_assignment.ts +279 -0
- package/src/scripts/ai_council/routing_training_row.ts +197 -0
- package/src/scripts/ai_council/synthesis_strategy.ts +159 -0
- package/src/scripts/check_adr_frontmatter.ts +4 -9
- package/src/scripts/check_agent_artifact_location.ts +337 -0
- package/src/scripts/check_artefact_count_messaging.ts +74 -8
- package/src/scripts/check_enforcement_coverage.ts +40 -1
- package/src/scripts/check_estate_count.ts +205 -24
- package/src/scripts/check_release_highlights.ts +113 -14
- package/src/scripts/check_roadmap_trackable.ts +17 -2
- package/src/scripts/check_web_launch_readiness.ts +310 -20
- package/src/scripts/corpus_manifest.ts +235 -0
- package/src/scripts/evolution_lab.ts +234 -9
- package/src/scripts/generate_capability_matrix.ts +16 -9
- package/src/scripts/grade_target_readiness.ts +221 -2
- package/src/scripts/hook_effect_doctor.ts +47 -4
- package/src/scripts/hook_manifest.json +1 -1
- package/src/scripts/hook_manifest.yaml +28 -6
- package/src/scripts/hooks/comment_discipline_hook.ts +146 -0
- package/src/scripts/hooks/concern_registry.ts +2 -0
- package/src/scripts/hooks/run_continuation_hook.ts +78 -88
- package/src/scripts/hooks/session_eol_hook.ts +12 -1
- package/src/scripts/hooks/turn_end_gate_hook.ts +9 -1
- package/src/scripts/install-hooks.sh +18 -0
- package/src/scripts/language_mirror_hook.ts +14 -2
- package/src/scripts/lint_carrier_integrity.ts +820 -0
- package/src/scripts/lint_code_comments.ts +748 -0
- package/src/scripts/lint_plan_risk_register.ts +39 -19
- package/src/scripts/lint_promotion_paths.ts +779 -0
- package/src/scripts/llm_propose.ts +256 -0
- package/src/scripts/measure_delivery_sets.ts +361 -0
- package/src/scripts/measure_routing_signal.ts +307 -0
- package/src/scripts/model_rule_injection.ts +19 -3
- package/src/scripts/probe_council_retention.ts +495 -0
- package/src/scripts/release.ts +21 -37
- package/src/scripts/release_drill.ts +199 -0
- package/src/scripts/release_env.ts +35 -0
- package/src/scripts/release_publication.ts +142 -0
- package/src/scripts/roadmap_context.ts +15 -0
- package/src/scripts/skill_scout.ts +633 -0
- package/src/scripts/sync_pr_branch.ts +521 -42
|
@@ -31,11 +31,13 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
|
31
31
|
|
|
32
32
|
import {
|
|
33
33
|
_MERGE_UPDATE_ROUNDS,
|
|
34
|
+
_set_changelog_reader,
|
|
34
35
|
_set_exec_override,
|
|
35
36
|
execute,
|
|
36
37
|
Plan,
|
|
37
38
|
SystemExitError,
|
|
38
39
|
} from './release.js';
|
|
40
|
+
import { DERIVED_MARKER } from './_lib/release_highlights.js';
|
|
39
41
|
|
|
40
42
|
const _HERE = fileURLToPath(import.meta.url);
|
|
41
43
|
const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
|
|
@@ -92,6 +94,48 @@ interface WorldConfig {
|
|
|
92
94
|
* removes.
|
|
93
95
|
*/
|
|
94
96
|
changelog?: string;
|
|
97
|
+
/**
|
|
98
|
+
* Content the WORKING-TREE read returns — `read_changelog_text()`, used by
|
|
99
|
+
* step 5 (PR body) and step 8 (annotated-tag message).
|
|
100
|
+
*
|
|
101
|
+
* Added 2026-09-01 with the publication guard (roadmap § 3.1). `changelog`
|
|
102
|
+
* above only covers `git show <tag>:CHANGELOG.md`; the working tree was
|
|
103
|
+
* still the REAL file, whose `14.13.0` section carries four
|
|
104
|
+
* `_auto-derived, rewrite before merge:_` lines. A guard at the tag call
|
|
105
|
+
* site therefore refused every scenario for a reason unrelated to what the
|
|
106
|
+
* scenario tested — the same coupling `changelog` was introduced to
|
|
107
|
+
* remove, on the other read.
|
|
108
|
+
*
|
|
109
|
+
* Defaults to `defaultChangelogFixture(target)`, which is policy-valid by
|
|
110
|
+
* construction.
|
|
111
|
+
*/
|
|
112
|
+
changelog_file?: string;
|
|
113
|
+
/**
|
|
114
|
+
* The tag already exists locally and was never pushed — the § 3.2 resume
|
|
115
|
+
* state, which reached `_push_tag` having read no changelog at all.
|
|
116
|
+
*/
|
|
117
|
+
tag_created_unpushed?: boolean;
|
|
118
|
+
/**
|
|
119
|
+
* The tag is already on the remote, so step 8 skips entirely and step 9 —
|
|
120
|
+
* the GitHub Release body — is the FIRST irreversible transition the run
|
|
121
|
+
* reaches. The only state in which the Release guard can be exercised on
|
|
122
|
+
* its own, because a marker otherwise stops the run at step 8.
|
|
123
|
+
*/
|
|
124
|
+
tag_already_remote?: boolean;
|
|
125
|
+
/**
|
|
126
|
+
* The release PR is ALREADY merged when the run starts — the real
|
|
127
|
+
* resume-after-merge state, in which steps 1 through 7 skip and step 8 is
|
|
128
|
+
* the first step that does work.
|
|
129
|
+
*
|
|
130
|
+
* Added 2026-09-02 with `guard_release_branch_push`. Before that guard, a
|
|
131
|
+
* marker scenario could start from scratch and still reach step 8, because
|
|
132
|
+
* nothing between the changelog write and the tag looked at the section.
|
|
133
|
+
* The push guard now stops such a run at step 4 — correctly, and that is
|
|
134
|
+
* the point of it — which makes this knob the only way left to exercise
|
|
135
|
+
* the tag and Release guards on their own. It is not a bypass: the state
|
|
136
|
+
* it fakes is one every resumed release actually passes through.
|
|
137
|
+
*/
|
|
138
|
+
pr_already_merged?: boolean;
|
|
95
139
|
}
|
|
96
140
|
|
|
97
141
|
/**
|
|
@@ -122,6 +166,21 @@ export function defaultChangelogFixture(target: string): string {
|
|
|
122
166
|
].join('\n');
|
|
123
167
|
}
|
|
124
168
|
|
|
169
|
+
/**
|
|
170
|
+
* `defaultChangelogFixture` with one head line still carrying the generator's
|
|
171
|
+
* draft marker — the exact state five released sections shipped in.
|
|
172
|
+
*
|
|
173
|
+
* Built from `DERIVED_MARKER` rather than from a literal string: a fixture
|
|
174
|
+
* that hardcodes the marker text keeps passing after the marker is renamed,
|
|
175
|
+
* which is a guard policing a population of zero.
|
|
176
|
+
*/
|
|
177
|
+
export function markedChangelogFixture(target: string): string {
|
|
178
|
+
return defaultChangelogFixture(target).replace(
|
|
179
|
+
'- fixture entry for the release drill',
|
|
180
|
+
`### Release highlights\n\n- **Behaviour changes:** ${DERIVED_MARKER} rule/schema diffs in abc1234.\n\n- fixture entry for the release drill`,
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
125
184
|
/**
|
|
126
185
|
* A scriptable stand-in for every git/gh/task command execute() issues.
|
|
127
186
|
* Stateful where the real world is: checkouts move HEAD, `git tag` makes the
|
|
@@ -144,6 +203,8 @@ class FakeWorld {
|
|
|
144
203
|
private merge_fails_hard: boolean;
|
|
145
204
|
private checks_fail: boolean;
|
|
146
205
|
private readonly changelog: string;
|
|
206
|
+
/** Working-tree content; see `WorldConfig.changelog_file`. */
|
|
207
|
+
readonly changelog_file: string;
|
|
147
208
|
|
|
148
209
|
constructor(
|
|
149
210
|
readonly branch: string,
|
|
@@ -152,10 +213,14 @@ class FakeWorld {
|
|
|
152
213
|
) {
|
|
153
214
|
this.push_rejections = cfg.push_rejections ?? 0;
|
|
154
215
|
this.changelog = cfg.changelog ?? defaultChangelogFixture(target);
|
|
216
|
+
this.changelog_file = cfg.changelog_file ?? defaultChangelogFixture(target);
|
|
217
|
+
this.tag_local = (cfg.tag_created_unpushed ?? false) || (cfg.tag_already_remote ?? false);
|
|
218
|
+
this.tag_remote = cfg.tag_already_remote ?? false;
|
|
155
219
|
this.behind_probes = cfg.behind_probes ?? 0;
|
|
156
220
|
this.merge_races_once = cfg.merge_races_once ?? false;
|
|
157
221
|
this.merge_fails_hard = cfg.merge_fails_hard ?? false;
|
|
158
222
|
this.checks_fail = cfg.checks_fail ?? false;
|
|
223
|
+
this.pr_merged = cfg.pr_already_merged ?? false;
|
|
159
224
|
}
|
|
160
225
|
|
|
161
226
|
exec(args: readonly string[]): ExecResult {
|
|
@@ -325,6 +390,136 @@ function _count(world: FakeWorld, needle: string): number {
|
|
|
325
390
|
}
|
|
326
391
|
|
|
327
392
|
const SCENARIOS: Record<string, Scenario> = {
|
|
393
|
+
'marker-refuses-before-branch-push': {
|
|
394
|
+
summary:
|
|
395
|
+
'step 4: a draft marker refuses the branch push — the first remote state — so no PR is ever opened',
|
|
396
|
+
config: { changelog_file: markedChangelogFixture(current_version()) },
|
|
397
|
+
expect_success: false,
|
|
398
|
+
verify: (w, err) => {
|
|
399
|
+
const f: string[] = [];
|
|
400
|
+
_expect(
|
|
401
|
+
(err ?? '').includes(`refusing to push ${w.branch}`),
|
|
402
|
+
`died for the wrong reason: ${err ?? '(no error)'}`,
|
|
403
|
+
f,
|
|
404
|
+
);
|
|
405
|
+
// The load-bearing assertions are about what did NOT happen. Each
|
|
406
|
+
// one is a remote side effect the 14.14.0 run actually produced
|
|
407
|
+
// before this guard existed: a pushed branch, an open PR, and a
|
|
408
|
+
// check watch on a check that could not pass.
|
|
409
|
+
_expect(
|
|
410
|
+
!w.calls.includes(`git push -u origin ${w.branch}`),
|
|
411
|
+
'the branch was pushed after the refusal',
|
|
412
|
+
f,
|
|
413
|
+
);
|
|
414
|
+
_expect(
|
|
415
|
+
!w.calls.some((c) => c.startsWith('gh pr create')),
|
|
416
|
+
'a pull request was opened after the refusal',
|
|
417
|
+
f,
|
|
418
|
+
);
|
|
419
|
+
_expect(
|
|
420
|
+
!w.calls.some((c) => c.includes('--watch')),
|
|
421
|
+
'CI was waited on after the refusal',
|
|
422
|
+
f,
|
|
423
|
+
);
|
|
424
|
+
_expect(!w.tag_local && !w.tag_remote, 'a tag exists after the refusal', f);
|
|
425
|
+
_expect(!w.release_created, 'a GitHub Release was created after the refusal', f);
|
|
426
|
+
return f;
|
|
427
|
+
},
|
|
428
|
+
},
|
|
429
|
+
'marker-refuses-before-tag': {
|
|
430
|
+
summary:
|
|
431
|
+
'step 8: a draft marker in the merged section refuses BEFORE the annotated tag is created',
|
|
432
|
+
config: {
|
|
433
|
+
pr_already_merged: true,
|
|
434
|
+
changelog_file: markedChangelogFixture(current_version()),
|
|
435
|
+
},
|
|
436
|
+
expect_success: false,
|
|
437
|
+
verify: (w, err) => {
|
|
438
|
+
const f: string[] = [];
|
|
439
|
+
_expect(
|
|
440
|
+
(err ?? '').includes('refusing to publish the annotated tag'),
|
|
441
|
+
`died for the wrong reason: ${err ?? '(no error)'}`,
|
|
442
|
+
f,
|
|
443
|
+
);
|
|
444
|
+
// The load-bearing assertion, and it is about ORDER, not about the
|
|
445
|
+
// message: nothing irreversible may run after the refusal.
|
|
446
|
+
_expect(
|
|
447
|
+
!w.calls.some((c) => c.startsWith('git tag -a ')),
|
|
448
|
+
'an annotated tag was created after the refusal',
|
|
449
|
+
f,
|
|
450
|
+
);
|
|
451
|
+
_expect(!w.tag_local && !w.tag_remote, 'a tag exists after the refusal', f);
|
|
452
|
+
_expect(!w.release_created, 'a GitHub Release was created after the refusal', f);
|
|
453
|
+
return f;
|
|
454
|
+
},
|
|
455
|
+
},
|
|
456
|
+
'marker-refuses-resumed-tag-push': {
|
|
457
|
+
summary:
|
|
458
|
+
'step 8 (§ 3.2): a tag created but never pushed is NOT pushed when its section carries the marker',
|
|
459
|
+
config: {
|
|
460
|
+
pr_already_merged: true,
|
|
461
|
+
tag_created_unpushed: true,
|
|
462
|
+
changelog_file: markedChangelogFixture(current_version()),
|
|
463
|
+
},
|
|
464
|
+
expect_success: false,
|
|
465
|
+
verify: (w, err) => {
|
|
466
|
+
const f: string[] = [];
|
|
467
|
+
_expect(
|
|
468
|
+
(err ?? '').includes('refusing to publish the tag push (resumed)'),
|
|
469
|
+
`died for the wrong reason: ${err ?? '(no error)'}`,
|
|
470
|
+
f,
|
|
471
|
+
);
|
|
472
|
+
// The recorded bypass: this path read no changelog at all, so the
|
|
473
|
+
// push was the FIRST thing that happened. It must now be the thing
|
|
474
|
+
// that does not happen.
|
|
475
|
+
_expect(!w.tag_remote, 'the tag was pushed after the refusal', f);
|
|
476
|
+
_expect(
|
|
477
|
+
!w.calls.includes(`git push origin ${w.target}`),
|
|
478
|
+
'a tag push ran after the refusal',
|
|
479
|
+
f,
|
|
480
|
+
);
|
|
481
|
+
_expect(!w.release_created, 'a GitHub Release was created after the refusal', f);
|
|
482
|
+
return f;
|
|
483
|
+
},
|
|
484
|
+
},
|
|
485
|
+
'marker-refuses-github-release': {
|
|
486
|
+
summary:
|
|
487
|
+
'step 9: with the tag already pushed, a marker in the TAGGED section refuses before the Release is created',
|
|
488
|
+
config: {
|
|
489
|
+
pr_already_merged: true,
|
|
490
|
+
tag_already_remote: true,
|
|
491
|
+
changelog: markedChangelogFixture(current_version()),
|
|
492
|
+
changelog_file: markedChangelogFixture(current_version()),
|
|
493
|
+
},
|
|
494
|
+
expect_success: false,
|
|
495
|
+
verify: (w, err) => {
|
|
496
|
+
const f: string[] = [];
|
|
497
|
+
_expect(
|
|
498
|
+
(err ?? '').includes('refusing to publish the GitHub Release notes'),
|
|
499
|
+
`died for the wrong reason: ${err ?? '(no error)'}`,
|
|
500
|
+
f,
|
|
501
|
+
);
|
|
502
|
+
_expect(!w.release_created, 'the GitHub Release was created after the refusal', f);
|
|
503
|
+
_expect(
|
|
504
|
+
!w.calls.some((c) => c.startsWith(`gh release create ${w.target}`)),
|
|
505
|
+
'gh release create ran after the refusal',
|
|
506
|
+
f,
|
|
507
|
+
);
|
|
508
|
+
return f;
|
|
509
|
+
},
|
|
510
|
+
},
|
|
511
|
+
'clean-section-still-publishes': {
|
|
512
|
+
summary:
|
|
513
|
+
'the guard is scoped to a real defect: a policy-valid section tags and releases as before',
|
|
514
|
+
config: {},
|
|
515
|
+
expect_success: true,
|
|
516
|
+
verify: (w, _err) => {
|
|
517
|
+
const f: string[] = [];
|
|
518
|
+
_expect(w.tag_remote, 'tag never pushed', f);
|
|
519
|
+
_expect(w.release_created, 'GitHub Release never created', f);
|
|
520
|
+
return f;
|
|
521
|
+
},
|
|
522
|
+
},
|
|
328
523
|
'happy-resume': {
|
|
329
524
|
summary: 'resume with everything green runs checkout → checks → merge → tag → release',
|
|
330
525
|
config: {},
|
|
@@ -472,6 +667,9 @@ function run_scenario(name: string): ScenarioOutcome {
|
|
|
472
667
|
return true;
|
|
473
668
|
};
|
|
474
669
|
_set_exec_override((args) => world.exec(args));
|
|
670
|
+
// The working-tree read, faked for the same reason the command layer is.
|
|
671
|
+
// Without this the drill's step 8 reads the repository's real CHANGELOG.md.
|
|
672
|
+
_set_changelog_reader(() => world.changelog_file);
|
|
475
673
|
try {
|
|
476
674
|
execute(plan, { wait_for_checks: true, dry_run: false, resume: true });
|
|
477
675
|
} catch (err) {
|
|
@@ -482,6 +680,7 @@ function run_scenario(name: string): ScenarioOutcome {
|
|
|
482
680
|
}
|
|
483
681
|
} finally {
|
|
484
682
|
_set_exec_override(null);
|
|
683
|
+
_set_changelog_reader(null);
|
|
485
684
|
process.stdout.write = orig_out;
|
|
486
685
|
process.stderr.write = orig_err;
|
|
487
686
|
}
|
|
@@ -91,6 +91,39 @@ const PROJECT_TEMPLATE = path.join(
|
|
|
91
91
|
'agent-project-settings.example.yml',
|
|
92
92
|
);
|
|
93
93
|
const CHANGELOG = path.join(REPO_ROOT, 'CHANGELOG.md');
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Overridable reader for the WORKING-TREE changelog.
|
|
97
|
+
*
|
|
98
|
+
* Mirrors `_set_exec_override` in `release_publication.ts`, and exists for the
|
|
99
|
+
* same reason: `execute()` reads `CHANGELOG.md` off disk at two points (the PR
|
|
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 8 read the
|
|
102
|
+
* repository's REAL changelog — whose current section carries four
|
|
103
|
+
* `_auto-derived, rewrite before merge:_` lines at `14.13.0` — so a
|
|
104
|
+
* publication guard added at that call site refused every drill scenario for a
|
|
105
|
+
* reason that had nothing to do with what the scenario was testing.
|
|
106
|
+
*
|
|
107
|
+
* That is the failure mode the `WorldConfig.changelog` fixture already removed
|
|
108
|
+
* for `git show`, and the AI council of 2026-08-23 (2/2 convergent) chose a
|
|
109
|
+
* controlled fixture over a policy exemption when it removed it. This is the
|
|
110
|
+
* same choice applied to the other read: the drill supplies policy-valid
|
|
111
|
+
* content and exercises the real parsing and guarding path, rather than being
|
|
112
|
+
* exempted from it.
|
|
113
|
+
*
|
|
114
|
+
* `null` restores the real filesystem read. Test-only; nothing in production
|
|
115
|
+
* sets it.
|
|
116
|
+
*/
|
|
117
|
+
let _changelog_reader: ((file: string) => string) | null = null;
|
|
118
|
+
|
|
119
|
+
function _set_changelog_reader(fn: ((file: string) => string) | null): void {
|
|
120
|
+
_changelog_reader = fn;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** The working-tree changelog text, through the seam above. */
|
|
124
|
+
function read_changelog_text(): string {
|
|
125
|
+
return _changelog_reader ? _changelog_reader(CHANGELOG) : fs.readFileSync(CHANGELOG, 'utf-8');
|
|
126
|
+
}
|
|
94
127
|
const MAIN_BRANCH = 'main';
|
|
95
128
|
const REMOTE = 'origin';
|
|
96
129
|
const REPO_SLUG = 'event4u-app/agent-config';
|
|
@@ -282,6 +315,8 @@ export {
|
|
|
282
315
|
_cap_body,
|
|
283
316
|
CHANGELOG,
|
|
284
317
|
CalledProcessError,
|
|
318
|
+
_set_changelog_reader,
|
|
319
|
+
read_changelog_text,
|
|
285
320
|
GH_PR_BODY_LIMIT,
|
|
286
321
|
GH_RELEASE_NOTES_LIMIT,
|
|
287
322
|
MAIN_BRANCH,
|
|
@@ -16,12 +16,14 @@
|
|
|
16
16
|
* import path changes.
|
|
17
17
|
*/
|
|
18
18
|
import { spawnSync } from 'node:child_process';
|
|
19
|
+
import { publication_blockers } from './_lib/release_highlights.js';
|
|
19
20
|
import * as fs from 'node:fs';
|
|
20
21
|
import process from 'node:process';
|
|
21
22
|
|
|
22
23
|
import { gh_argv_label, gh_retry } from './_lib/gh_transient.js';
|
|
23
24
|
import {
|
|
24
25
|
extract_changelog_section,
|
|
26
|
+
tag_message_from_section,
|
|
25
27
|
pr_body_from_section,
|
|
26
28
|
} from './_lib/release_material.js';
|
|
27
29
|
|
|
@@ -34,6 +36,7 @@ import {
|
|
|
34
36
|
REPO_ROOT,
|
|
35
37
|
SystemExitError,
|
|
36
38
|
_cap_body,
|
|
39
|
+
read_changelog_text,
|
|
37
40
|
} from './release_env.js';
|
|
38
41
|
|
|
39
42
|
function die(msg: string, code = 2): never {
|
|
@@ -419,7 +422,65 @@ export function _is_non_fast_forward(stderr: string, stdout: string): boolean {
|
|
|
419
422
|
return /(non-fast-forward|fetch first|behind its remote counterpart)/i.test(text);
|
|
420
423
|
}
|
|
421
424
|
|
|
425
|
+
/**
|
|
426
|
+
* Refuse the FIRST remote state of a release whose section is not publishable.
|
|
427
|
+
*
|
|
428
|
+
* The fourth guard site, and the one the other three structurally cannot cover.
|
|
429
|
+
* They sit at the irreversible transitions — annotated tag, resumed tag push,
|
|
430
|
+
* GitHub Release body — and every one of them is DOWNSTREAM of `gh pr create`
|
|
431
|
+
* and of `gh pr checks --watch`. `check_release_highlights` refuses the same
|
|
432
|
+
* section from the other side, on the release PR, so a run whose head is still
|
|
433
|
+
* the generator's draft pushed a branch, opened a PR, burned CI, waited on a
|
|
434
|
+
* check that could not pass, and died there — leaving a half-open release to
|
|
435
|
+
* clean up by hand.
|
|
436
|
+
*
|
|
437
|
+
* That is not a hypothetical: it is PR #1812 (14.14.0, 2026-09-02), and the
|
|
438
|
+
* 2026-09-01 flip's own risk register pre-registered it as Risk 1 — *"the flip
|
|
439
|
+
* reds the next release PR on its first run"*. Its stated mitigation was that
|
|
440
|
+
* the releaser edits prose before merge; nothing in the pipeline asked, so
|
|
441
|
+
* nothing did.
|
|
442
|
+
*
|
|
443
|
+
* The branch push is the first thing in the pipeline that leaves the machine,
|
|
444
|
+
* so a refusal here costs one local edit and no remote state at all: no branch
|
|
445
|
+
* on the remote, no pull request, no CI minutes, no tag. It sits at the push
|
|
446
|
+
* CALL SITE rather than in a formatter for the reason the publication guards
|
|
447
|
+
* already state — a formatter has no notion of whether it is publishing, and
|
|
448
|
+
* the call site is the one that can still stop. Deriving the version from
|
|
449
|
+
* `release/X.Y.Z` mirrors what `release-validation.yml` already does with
|
|
450
|
+
* `head_ref`.
|
|
451
|
+
*
|
|
452
|
+
* Two states pass unguarded, deliberately: a branch that is not
|
|
453
|
+
* `release/X.Y.Z` (nothing about a release to read), and a version with no
|
|
454
|
+
* changelog section (the missing-section refusal belongs to the publication
|
|
455
|
+
* sites, which need a section to publish; refusing here would break every
|
|
456
|
+
* non-release push through this helper).
|
|
457
|
+
*/
|
|
458
|
+
export function guard_release_branch_push(branch: string): void {
|
|
459
|
+
const prefix = 'release/';
|
|
460
|
+
if (!branch.startsWith(prefix)) {
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
const version = branch.slice(prefix.length);
|
|
464
|
+
const section = extract_changelog_section(read_changelog_text(), version);
|
|
465
|
+
if (!section) {
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
const blockers = publication_blockers(section.body, version, `\`${branch}\``);
|
|
469
|
+
if (blockers.length === 0) {
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
die(
|
|
473
|
+
`refusing to push ${branch} — the ${version} changelog section is not publishable ` +
|
|
474
|
+
`(${String(blockers.length)} blocker(s)):\n` +
|
|
475
|
+
blockers.map((b) => ` - ${b}`).join('\n') +
|
|
476
|
+
`\n Nothing has left this machine: no branch on ${REMOTE}, no pull request, no tag.\n` +
|
|
477
|
+
' Curate the `### Release highlights` head in CHANGELOG.md under ' +
|
|
478
|
+
`\`## [${version}]\`, then re-run \`task release -- --resume\`.`,
|
|
479
|
+
);
|
|
480
|
+
}
|
|
481
|
+
|
|
422
482
|
function push_release_branch(branch: string): void {
|
|
483
|
+
guard_release_branch_push(branch);
|
|
423
484
|
const first = run(['git', 'push', '-u', REMOTE, branch], { check: false, capture: true });
|
|
424
485
|
if (first.returncode === 0) {
|
|
425
486
|
process.stdout.write(first.stdout);
|
|
@@ -718,3 +779,84 @@ export {
|
|
|
718
779
|
run,
|
|
719
780
|
watch_pr_checks,
|
|
720
781
|
};
|
|
782
|
+
|
|
783
|
+
/**
|
|
784
|
+
* Refuse an irreversible publication whose section is not publishable.
|
|
785
|
+
*
|
|
786
|
+
* Called at the THREE call sites that publish, never inside a formatter:
|
|
787
|
+
* annotated-tag creation, the resumed push of a tag created but never pushed,
|
|
788
|
+
* and the GitHub Release body. A pure formatter has no notion of whether it is
|
|
789
|
+
* actually publishing, which is why an earlier attempt at that placement was
|
|
790
|
+
* refused; the call site does, and it is the one that can still stop.
|
|
791
|
+
*
|
|
792
|
+
* **The resumed push is the bypass worth naming.** The changelog is read only
|
|
793
|
+
* in the tag-CREATION branch, so a resume over a tag that was created but never
|
|
794
|
+
* pushed reached `_push_tag` having read nothing. A guard covering creation
|
|
795
|
+
* alone misses exactly that path, and pushing a tag is as irreversible as
|
|
796
|
+
* creating it.
|
|
797
|
+
*
|
|
798
|
+
* `die` throws before the command runs, so nothing irreversible fires after a
|
|
799
|
+
* refusal — an ordering asserted against the drill's recorded command list
|
|
800
|
+
* rather than by reading this comment.
|
|
801
|
+
*
|
|
802
|
+
* Lives here rather than in `release.ts` because that file is 2000+ lines and
|
|
803
|
+
* every line above 1500 is charged by the source-size growth ratchet, while
|
|
804
|
+
* this module is far below it and already owns `die`. Behaviour unchanged.
|
|
805
|
+
*/
|
|
806
|
+
export function _refuse_unpublishable(sectionBody: string, version: string, surface: string): void {
|
|
807
|
+
const blockers = publication_blockers(sectionBody, version);
|
|
808
|
+
if (blockers.length === 0) {
|
|
809
|
+
return;
|
|
810
|
+
}
|
|
811
|
+
die(
|
|
812
|
+
`refusing to publish the ${surface} for ${version} — ` +
|
|
813
|
+
`${String(blockers.length)} publication blocker(s):\n` +
|
|
814
|
+
blockers.map((b) => ` - ${b}`).join('\n'),
|
|
815
|
+
);
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
/**
|
|
819
|
+
* Read the section under release and refuse if it is not publishable.
|
|
820
|
+
*
|
|
821
|
+
* One helper for all three publish sites so the read, the missing-section
|
|
822
|
+
* refusal and the blocker refusal cannot drift apart between them — the
|
|
823
|
+
* resumed-push path drifted exactly that way before this existed, reaching
|
|
824
|
+
* `_push_tag` having read nothing.
|
|
825
|
+
*/
|
|
826
|
+
export function guard_publication(version: string, surface: string): void {
|
|
827
|
+
const section = extract_changelog_section(read_changelog_text(), version);
|
|
828
|
+
if (!section) {
|
|
829
|
+
die(
|
|
830
|
+
`CHANGELOG.md on ${MAIN_BRANCH} carries no section for ${version} — ` +
|
|
831
|
+
`refusing to publish the ${surface}`,
|
|
832
|
+
);
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
835
|
+
_refuse_unpublishable(section.body, version, surface);
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
/**
|
|
839
|
+
* Derive the tag message from the MERGED changelog, refuse if unpublishable,
|
|
840
|
+
* create the annotated tag and push it.
|
|
841
|
+
*
|
|
842
|
+
* The three steps are one function because they are one irreversible act with
|
|
843
|
+
* one precondition, and splitting them across call sites is what let the
|
|
844
|
+
* resumed-push path reach `_push_tag` having read nothing. Sequencing is
|
|
845
|
+
* load-bearing (release-truth Phase 1, council 2026-08-03): merge FIRST, pull
|
|
846
|
+
* main, THEN derive the message — tagging before the merge reads a section that
|
|
847
|
+
* does not exist yet. The annotated tag replaces the previous lightweight one so
|
|
848
|
+
* tag metadata is a fourth surface carrying the same single-source content.
|
|
849
|
+
*/
|
|
850
|
+
export function create_and_push_annotated_tag(version: string): void {
|
|
851
|
+
const merged = extract_changelog_section(read_changelog_text(), version);
|
|
852
|
+
if (!merged) {
|
|
853
|
+
die(
|
|
854
|
+
`CHANGELOG.md on ${MAIN_BRANCH} carries no section for ${version} — ` +
|
|
855
|
+
'refusing to tag a release whose changelog entry is missing',
|
|
856
|
+
);
|
|
857
|
+
return;
|
|
858
|
+
}
|
|
859
|
+
_refuse_unpublishable(merged.body, version, 'annotated tag');
|
|
860
|
+
run(['git', 'tag', '-a', version, '-m', tag_message_from_section(merged.body, version)]);
|
|
861
|
+
_push_tag(version);
|
|
862
|
+
}
|
|
@@ -49,6 +49,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
49
49
|
import { gitEnv } from './_lib/git_env.js';
|
|
50
50
|
import { read_live_records, register_dir } from './_lib/session_register.js';
|
|
51
51
|
import { other_worktree_branches_detailed } from './sessions_cli.js';
|
|
52
|
+
import { recordContextObservation } from './_lib/context_observation.js';
|
|
52
53
|
|
|
53
54
|
/** One open pull request, with the file set it changes. */
|
|
54
55
|
export interface PullRequestInfo {
|
|
@@ -746,6 +747,20 @@ export function main(argv: string[] = process.argv.slice(2)): number {
|
|
|
746
747
|
roadmap: at('--roadmap'),
|
|
747
748
|
...(ownedFile !== null ? { ownedPaths: readOwnedPathsFile(ownedFile) } : {}),
|
|
748
749
|
});
|
|
750
|
+
// `road-to-wired-instruments` 2.1 — this probe is the only thing in the tree
|
|
751
|
+
// that can OBSERVE the world (it costs a `gh` call), so it records what it
|
|
752
|
+
// saw where a Stop-path concern can read it without spawning anything. The
|
|
753
|
+
// continuation ladder's premise rung is the consumer; the resume checkpoint
|
|
754
|
+
// is the other.
|
|
755
|
+
//
|
|
756
|
+
// Only a LIVE reading is recorded. Offline, `contextFingerprint` still
|
|
757
|
+
// returns a digest — over a null base and an empty PR list — and that digest
|
|
758
|
+
// is a statement about the network, not about the repository. Recording it
|
|
759
|
+
// would make every offline probe look like a moved premise, which is a false
|
|
760
|
+
// halt manufactured out of a dropped connection.
|
|
761
|
+
if (ctx.network === 'live') {
|
|
762
|
+
recordContextObservation(root, ctx.roadmap ?? '', ctx.fingerprint);
|
|
763
|
+
}
|
|
749
764
|
if (argv.includes('--fingerprint')) {
|
|
750
765
|
// Just the digest, for the loop's phase-boundary comparison and for the
|
|
751
766
|
// resume checkpoint's `context_fingerprint` (§ 5.6 — same value).
|