@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
|
@@ -70,6 +70,7 @@ import {
|
|
|
70
70
|
type SpanCommit,
|
|
71
71
|
collect_span_commits,
|
|
72
72
|
derive_categories,
|
|
73
|
+
make_span_file_reader,
|
|
73
74
|
parse_git_log,
|
|
74
75
|
previous_release_tag,
|
|
75
76
|
stale_draft_labels,
|
|
@@ -126,6 +127,24 @@ export interface Contradiction {
|
|
|
126
127
|
evidence: string[];
|
|
127
128
|
}
|
|
128
129
|
|
|
130
|
+
/** The fifth label, named once so the note below and the gate cannot drift. */
|
|
131
|
+
export const KNOWN_LIMITATIONS = 'Known limitations';
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* What a passing `Known limitations: _none_` actually claims.
|
|
135
|
+
*
|
|
136
|
+
* Exported so it is a fixture rather than a sentence somebody can soften: the
|
|
137
|
+
* derivation reads self-declared residuals, so it is blind to a limitation
|
|
138
|
+
* nobody documented, and printing `_none_` without saying so would turn an
|
|
139
|
+
* undefended field into a defended-looking one. That inversion is the whole
|
|
140
|
+
* risk the roadmap registered against making this label derivable.
|
|
141
|
+
*/
|
|
142
|
+
export const UNDERIVED_LIMITATIONS_NOTE =
|
|
143
|
+
'ℹ️ `Known limitations: _none_` means NO CANDIDATE WAS DERIVED from the span, ' +
|
|
144
|
+
'not that no limitations exist —\n' +
|
|
145
|
+
' the derivation reads residuals a commit or a touched executable file ' +
|
|
146
|
+
'DECLARES, so an undocumented one is invisible to it.';
|
|
147
|
+
|
|
129
148
|
/**
|
|
130
149
|
* A populated generated category meeting a `_none_` curated field is a
|
|
131
150
|
* contradiction. Filled fields and underived labels never fail.
|
|
@@ -281,7 +300,12 @@ export function main(argv: readonly string[]): number {
|
|
|
281
300
|
// reaches another section, another era file, or `docs/archive/`. That is
|
|
282
301
|
// what keeps eighteen historical marker lines from turning this into a
|
|
283
302
|
// permanent red on every future release.
|
|
284
|
-
|
|
303
|
+
// `--to` and not the worktree: the gate must answer about the ref it was
|
|
304
|
+
// pointed at, and on a release branch the checkout and `--to` are the same
|
|
305
|
+
// commit only by accident of how the gate happens to be invoked.
|
|
306
|
+
const derived = derive_categories(span, {
|
|
307
|
+
readTouchedFile: make_span_file_reader(to, REPO_ROOT),
|
|
308
|
+
});
|
|
285
309
|
const drafts = stale_draft_labels(curated);
|
|
286
310
|
if (drafts.length > 0) {
|
|
287
311
|
process.stderr.write(
|
|
@@ -315,6 +339,14 @@ export function main(argv: readonly string[]): number {
|
|
|
315
339
|
const contradictions = highlight_contradictions(curated, derived);
|
|
316
340
|
if (contradictions.length === 0) {
|
|
317
341
|
process.stdout.write(`✅ curated head plausible for ${version} (span ${from}..${to})\n`);
|
|
342
|
+
// Risk 2 of the roadmap that made `Known limitations` derivable, paid
|
|
343
|
+
// where it lands rather than in a comment: a self-declared residual is
|
|
344
|
+
// only detectable when somebody WROTE IT DOWN, so a derived `_none_`
|
|
345
|
+
// here reads stronger than the undefended one it replaced unless the
|
|
346
|
+
// gate says which of the two it means. It means the first.
|
|
347
|
+
if ((curated[KNOWN_LIMITATIONS] ?? '').trim() === HEAD_NONE) {
|
|
348
|
+
process.stdout.write(`${UNDERIVED_LIMITATIONS_NOTE}\n`);
|
|
349
|
+
}
|
|
318
350
|
return 0;
|
|
319
351
|
}
|
|
320
352
|
process.stderr.write(
|
|
@@ -0,0 +1,567 @@
|
|
|
1
|
+
#!/usr/bin/env -S npx tsx
|
|
2
|
+
/**
|
|
3
|
+
* check_release_holds — the one evaluator every release boundary reads.
|
|
4
|
+
*
|
|
5
|
+
* Template rule 28. Four modes, and the split matters because two of them run
|
|
6
|
+
* on completely different paths:
|
|
7
|
+
*
|
|
8
|
+
* --lint a malformed declaration reddens NORMAL CI, so a broken hold
|
|
9
|
+
* cannot fail open by being unparseable.
|
|
10
|
+
* --status list every declared hold and its state. Read-only.
|
|
11
|
+
* --require-safe the RELEASE-path question: may a cut be published now?
|
|
12
|
+
* --selftest every row of rule 28's state table, including the
|
|
13
|
+
* not-evaluable row, asserted against fixtures.
|
|
14
|
+
*
|
|
15
|
+
* Exit codes: 0 = clean / safe · 1 = a violation or a refusal · 2 = usage.
|
|
16
|
+
*
|
|
17
|
+
* The load-bearing behaviour, and the one worth re-reading before changing
|
|
18
|
+
* anything here: `not-evaluable` REFUSES, on every channel. An evaluator that
|
|
19
|
+
* cannot read a declaration has learned nothing about whether the tree is safe,
|
|
20
|
+
* and "learned nothing" is not "safe". There is deliberately no flag, label or
|
|
21
|
+
* trailer anywhere in this file that turns a refusal into a pass.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import * as fs from 'node:fs';
|
|
25
|
+
import * as os from 'node:os';
|
|
26
|
+
import * as path from 'node:path';
|
|
27
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
28
|
+
|
|
29
|
+
import {
|
|
30
|
+
evaluateFile,
|
|
31
|
+
evaluateHolds,
|
|
32
|
+
lifecycleViolations,
|
|
33
|
+
refuses,
|
|
34
|
+
refusalReport,
|
|
35
|
+
REFUSING_DIRS,
|
|
36
|
+
type CutChannel,
|
|
37
|
+
type Hold,
|
|
38
|
+
} from './_lib/release_holds.js';
|
|
39
|
+
import { REPO_ROOT } from './lint_roadmap_blockers.js';
|
|
40
|
+
import { assertScanned, DeadScopeError } from './_lib/scan_scope.js';
|
|
41
|
+
import { runGateCli, runSelfTest, type SelfTestCase } from './_lib/gate_self_test.js';
|
|
42
|
+
|
|
43
|
+
const _HERE = fileURLToPath(import.meta.url);
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The folders a release question must read.
|
|
47
|
+
*
|
|
48
|
+
* Wider than `lint_roadmap_blockers`' active-only glob on purpose: rule 28's
|
|
49
|
+
* per-folder lifecycle says a window does not disappear by moving the file, and
|
|
50
|
+
* a `later/` roadmap still refuses. `archive/` and `skipped/` are included
|
|
51
|
+
* because the move INTO them is refused while a window is open — a file that
|
|
52
|
+
* reached them with an open window is a defect this must still see, not one it
|
|
53
|
+
* should be blind to.
|
|
54
|
+
*/
|
|
55
|
+
const HOLD_DIRS = ['', 'later', 'archive', 'skipped', 'stubs'] as const;
|
|
56
|
+
|
|
57
|
+
function globRoadmaps(root = REPO_ROOT): string[] {
|
|
58
|
+
const out: string[] = [];
|
|
59
|
+
for (const sub of HOLD_DIRS) {
|
|
60
|
+
const dir = path.join(root, 'agents', 'roadmaps', sub);
|
|
61
|
+
let entries: fs.Dirent[];
|
|
62
|
+
try {
|
|
63
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
64
|
+
} catch {
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
for (const e of entries) {
|
|
68
|
+
if (e.isFile() && e.name.endsWith('.md')) {
|
|
69
|
+
out.push(path.join(dir, e.name));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return out.sort();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function collect(root = REPO_ROOT): Hold[] {
|
|
77
|
+
return globRoadmaps(root).flatMap((f) => evaluateFile(f));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function rel(p: string): string {
|
|
81
|
+
return path.relative(REPO_ROOT, p).split(path.sep).join('/');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function cmdLint(root: string): number {
|
|
85
|
+
// A broken glob is this gate's one silent failure mode: with zero files it
|
|
86
|
+
// would print "0 declared hold(s), all well-formed" and exit 0, certifying
|
|
87
|
+
// a coverage that does not exist. "Zero holds" is a real and normal state;
|
|
88
|
+
// "zero FILES under agents/roadmaps" never is.
|
|
89
|
+
try {
|
|
90
|
+
assertScanned({
|
|
91
|
+
gate: 'check_release_holds',
|
|
92
|
+
scanned: globRoadmaps(root).length,
|
|
93
|
+
units: 'roadmap file(s)',
|
|
94
|
+
roots: ['agents/roadmaps'],
|
|
95
|
+
});
|
|
96
|
+
} catch (e) {
|
|
97
|
+
if (e instanceof DeadScopeError) {
|
|
98
|
+
process.stderr.write(`❌ ${e.message}\n`);
|
|
99
|
+
return 1;
|
|
100
|
+
}
|
|
101
|
+
throw e;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const files = globRoadmaps(root);
|
|
105
|
+
const holds = collect(root);
|
|
106
|
+
const bad = holds.filter((h) => h.state === 'not-evaluable');
|
|
107
|
+
|
|
108
|
+
// The lifecycle half. A move already made is what a gate can see: if a file
|
|
109
|
+
// with a live window is sitting in `archive/` or `skipped/`, the refused
|
|
110
|
+
// move happened, and reddening CI is how the move is refused in practice.
|
|
111
|
+
const lifecycle = files.flatMap((f) => {
|
|
112
|
+
const hs = evaluateFile(f);
|
|
113
|
+
if (hs.length === 0) {
|
|
114
|
+
return [];
|
|
115
|
+
}
|
|
116
|
+
let text: string;
|
|
117
|
+
try {
|
|
118
|
+
text = fs.readFileSync(f, 'utf8');
|
|
119
|
+
} catch {
|
|
120
|
+
return [];
|
|
121
|
+
}
|
|
122
|
+
return lifecycleViolations(f, hs, text);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
process.stdout.write(`scanned: ${files.length}\n`);
|
|
126
|
+
if (bad.length === 0 && lifecycle.length === 0) {
|
|
127
|
+
process.stdout.write(
|
|
128
|
+
`✅ check-release-holds: ${holds.length} declared hold(s), all well-formed, ` +
|
|
129
|
+
`lifecycle clean\n`,
|
|
130
|
+
);
|
|
131
|
+
return 0;
|
|
132
|
+
}
|
|
133
|
+
if (bad.length > 0) {
|
|
134
|
+
process.stderr.write('❌ check-release-holds: malformed declaration(s):\n');
|
|
135
|
+
for (const h of bad) {
|
|
136
|
+
for (const why of h.malformed) {
|
|
137
|
+
process.stderr.write(` - ${rel(h.file)} · hold \`${h.id}\`: ${why}\n`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (lifecycle.length > 0) {
|
|
142
|
+
process.stderr.write('❌ check-release-holds: lifecycle violation(s):\n');
|
|
143
|
+
for (const v of lifecycle) {
|
|
144
|
+
process.stderr.write(` - ${rel(v.file)} · hold \`${v.holdId}\`: ${v.reason}\n`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return 1;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Answer the move BEFORE it happens, so the loop asks rather than discovers.
|
|
152
|
+
*
|
|
153
|
+
* `--can-move <file> <archive|skipped|later>`; exit 0 permits, 1 refuses. The
|
|
154
|
+
* `--lint` gate above is the backstop that catches a move made without asking.
|
|
155
|
+
*/
|
|
156
|
+
function cmdCanMove(file: string, dest: string): number {
|
|
157
|
+
if (!(REFUSING_DIRS as readonly string[]).includes(dest) && dest !== 'later') {
|
|
158
|
+
process.stderr.write(`❌ --can-move destination must be archive, skipped or later\n`);
|
|
159
|
+
return 2;
|
|
160
|
+
}
|
|
161
|
+
const holds = evaluateFile(file);
|
|
162
|
+
const live = holds.filter((h) => h.state === 'open' || h.state === 'not-evaluable');
|
|
163
|
+
if (live.length === 0) {
|
|
164
|
+
process.stdout.write(`✅ no live window — the move to \`${dest}/\` is permitted\n`);
|
|
165
|
+
return 0;
|
|
166
|
+
}
|
|
167
|
+
if (dest === 'later') {
|
|
168
|
+
let text = '';
|
|
169
|
+
try {
|
|
170
|
+
text = fs.readFileSync(file, 'utf8');
|
|
171
|
+
} catch {
|
|
172
|
+
/* an unreadable file is already not-evaluable above */
|
|
173
|
+
}
|
|
174
|
+
const problems = lifecycleViolations(
|
|
175
|
+
path.join(REPO_ROOT, 'agents', 'roadmaps', 'later', path.basename(file)),
|
|
176
|
+
holds,
|
|
177
|
+
text,
|
|
178
|
+
);
|
|
179
|
+
if (problems.length === 0) {
|
|
180
|
+
process.stdout.write(
|
|
181
|
+
`✅ the move to \`later/\` is permitted — the window stays listed and still refuses\n`,
|
|
182
|
+
);
|
|
183
|
+
return 0;
|
|
184
|
+
}
|
|
185
|
+
for (const v of problems) {
|
|
186
|
+
process.stderr.write(`❌ hold \`${v.holdId}\`: ${v.reason}\n`);
|
|
187
|
+
}
|
|
188
|
+
return 1;
|
|
189
|
+
}
|
|
190
|
+
for (const h of live) {
|
|
191
|
+
process.stderr.write(
|
|
192
|
+
`❌ REFUSED: hold \`${h.id}\` is ${h.state} — a roadmap with a live window may not ` +
|
|
193
|
+
`move to \`${dest}/\`. Finish \`${h.clearedBy || '?'}\`, or move it to \`later/\`.\n`,
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
return 1;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function cmdStatus(root: string): number {
|
|
200
|
+
const holds = collect(root);
|
|
201
|
+
if (holds.length === 0) {
|
|
202
|
+
process.stdout.write('No release holds declared. Every cut is permitted.\n');
|
|
203
|
+
return 0;
|
|
204
|
+
}
|
|
205
|
+
for (const h of holds) {
|
|
206
|
+
process.stdout.write(
|
|
207
|
+
`${h.state.padEnd(14)} ${h.id.padEnd(28)} channel=${h.channel.padEnd(6)} ` +
|
|
208
|
+
`opens=${h.openedBy || '-'} clears=${h.clearedBy || '-'} ${rel(h.file)}\n`,
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
return 0;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function cmdRequireSafe(root: string, cut: CutChannel): number {
|
|
215
|
+
const report = refusalReport(collect(root), cut);
|
|
216
|
+
if (report === null) {
|
|
217
|
+
process.stdout.write(`✅ release-holds: safe to cut (channel ${cut})\n`);
|
|
218
|
+
return 0;
|
|
219
|
+
}
|
|
220
|
+
process.stderr.write(`❌ ${report}\n`);
|
|
221
|
+
return 1;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** Rule 28's state table, one fixture per row, plus the two channel cases. */
|
|
225
|
+
function cmdSelftest(): number {
|
|
226
|
+
// Built line-by-line rather than as one template literal on purpose: a
|
|
227
|
+
// heredoc whose lines begin with `##` reads to `lint_code_comments` as a
|
|
228
|
+
// markdown heading left in source, and the fixture would redden a gate it
|
|
229
|
+
// has nothing to do with.
|
|
230
|
+
const base = (open: string, clear: string, channel = 'all', extra = '') =>
|
|
231
|
+
[
|
|
232
|
+
'',
|
|
233
|
+
'## Phase 1',
|
|
234
|
+
`- [${open}] **1.1 opener** <!-- opens-hold: probe -->`,
|
|
235
|
+
" verify: the opener's own check",
|
|
236
|
+
`- [${clear}] **1.2 clearer** <!-- clears-hold: probe -->`,
|
|
237
|
+
' verify: the check that proves the state is repaired',
|
|
238
|
+
'',
|
|
239
|
+
'## Release holds',
|
|
240
|
+
'',
|
|
241
|
+
'### hold: probe',
|
|
242
|
+
`- **Channel:** ${channel}`,
|
|
243
|
+
'- **Opened by:** 1.1',
|
|
244
|
+
'- **Cleared by:** 1.2',
|
|
245
|
+
'- **State:** the surface is half wired while this is open.',
|
|
246
|
+
'- **Why not a guard:** the entry point is reachable and cannot be made inert.',
|
|
247
|
+
extra,
|
|
248
|
+
].join('\n');
|
|
249
|
+
|
|
250
|
+
type Case = { name: string; text: string; state: string; all: boolean; latest: boolean };
|
|
251
|
+
const cases: Case[] = [
|
|
252
|
+
{ name: 'unopened permits', text: base(' ', ' '), state: 'unopened', all: false, latest: false },
|
|
253
|
+
{ name: 'open/all refuses every cut', text: base('x', ' '), state: 'open', all: true, latest: true },
|
|
254
|
+
{ name: 'cleared permits', text: base('x', 'x'), state: 'cleared', all: false, latest: false },
|
|
255
|
+
{
|
|
256
|
+
name: 'open/latest refuses stable, permits prerelease',
|
|
257
|
+
text: base('x', ' ', 'latest'),
|
|
258
|
+
state: 'open',
|
|
259
|
+
all: true,
|
|
260
|
+
latest: false,
|
|
261
|
+
},
|
|
262
|
+
// The load-bearing row. Each of these is a DIFFERENT way to be
|
|
263
|
+
// unreadable, and every one of them must refuse on BOTH channels.
|
|
264
|
+
{
|
|
265
|
+
name: 'not-evaluable: missing `Why not a guard:`',
|
|
266
|
+
text: base('x', ' ').replace(/- \*\*Why not a guard:\*\*.*\n/, ''),
|
|
267
|
+
state: 'not-evaluable',
|
|
268
|
+
all: true,
|
|
269
|
+
latest: true,
|
|
270
|
+
},
|
|
271
|
+
{
|
|
272
|
+
name: 'not-evaluable: unknown channel',
|
|
273
|
+
text: base('x', ' ', 'someday'),
|
|
274
|
+
state: 'not-evaluable',
|
|
275
|
+
all: true,
|
|
276
|
+
latest: true,
|
|
277
|
+
},
|
|
278
|
+
{
|
|
279
|
+
name: 'not-evaluable: clearer carries no `verify:`',
|
|
280
|
+
text: base('x', ' ').replace(
|
|
281
|
+
' verify: the check that proves the state is repaired\n',
|
|
282
|
+
'',
|
|
283
|
+
),
|
|
284
|
+
state: 'not-evaluable',
|
|
285
|
+
all: true,
|
|
286
|
+
latest: true,
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
name: 'not-evaluable: marker is not on a checkbox',
|
|
290
|
+
text: base('x', ' ').replace(
|
|
291
|
+
'- [ ] **1.2 clearer** <!-- clears-hold: probe -->',
|
|
292
|
+
'The clearer is step 1.2 <!-- clears-hold: probe -->',
|
|
293
|
+
),
|
|
294
|
+
state: 'not-evaluable',
|
|
295
|
+
all: true,
|
|
296
|
+
latest: true,
|
|
297
|
+
},
|
|
298
|
+
{
|
|
299
|
+
name: 'not-evaluable: hold entry names a version (rule 13)',
|
|
300
|
+
text: base('x', ' ', 'all', '- **Note:** ships in 15.1.0\n'),
|
|
301
|
+
state: 'not-evaluable',
|
|
302
|
+
all: true,
|
|
303
|
+
latest: true,
|
|
304
|
+
},
|
|
305
|
+
{
|
|
306
|
+
name: 'not-evaluable: duplicate hold id',
|
|
307
|
+
text: `${base('x', ' ')}\n### hold: probe\n- **Channel:** all\n`,
|
|
308
|
+
state: 'not-evaluable',
|
|
309
|
+
all: true,
|
|
310
|
+
latest: true,
|
|
311
|
+
},
|
|
312
|
+
{
|
|
313
|
+
name: 'not-evaluable: marker with no entry',
|
|
314
|
+
text: '## Phase 1\n- [x] **1.1 opener** <!-- opens-hold: orphan -->\n',
|
|
315
|
+
state: 'not-evaluable',
|
|
316
|
+
all: true,
|
|
317
|
+
latest: true,
|
|
318
|
+
},
|
|
319
|
+
];
|
|
320
|
+
|
|
321
|
+
// The two NEGATIVE cases. Without them a check that refused everything
|
|
322
|
+
// would pass every row above, which is the tautology this guards against.
|
|
323
|
+
const negatives: { name: string; text: string }[] = [
|
|
324
|
+
{ name: 'a roadmap with no holds section declares nothing', text: '## Phase 1\n- [ ] **1.1 x**\n' },
|
|
325
|
+
{
|
|
326
|
+
name: 'a fenced documentation example is not a declaration',
|
|
327
|
+
text: '```markdown\n## Release holds\n\n### hold: doc-example\n- **Channel:** all\n```\n',
|
|
328
|
+
},
|
|
329
|
+
];
|
|
330
|
+
|
|
331
|
+
// Lifecycle, rule 28's per-folder half. Asserted on the SAME fixture text
|
|
332
|
+
// under three different paths, so the only variable is the folder.
|
|
333
|
+
const openText = base('x', ' ');
|
|
334
|
+
const lifecycle: { name: string; file: string; text: string; want: number }[] = [
|
|
335
|
+
{
|
|
336
|
+
name: 'archive/ refuses a live window',
|
|
337
|
+
file: 'agents/roadmaps/archive/probe.md',
|
|
338
|
+
text: openText,
|
|
339
|
+
want: 1,
|
|
340
|
+
},
|
|
341
|
+
{
|
|
342
|
+
name: 'skipped/ refuses a live window',
|
|
343
|
+
file: 'agents/roadmaps/skipped/probe.md',
|
|
344
|
+
text: openText,
|
|
345
|
+
want: 1,
|
|
346
|
+
},
|
|
347
|
+
{
|
|
348
|
+
name: 'later/ refuses when entry_condition.what does not name the hold',
|
|
349
|
+
file: 'agents/roadmaps/later/probe.md',
|
|
350
|
+
text: `---\nstatus: later\nentry_condition:\n what: something else entirely\n when: later\n who: maintainer\n---\n${openText}`,
|
|
351
|
+
want: 1,
|
|
352
|
+
},
|
|
353
|
+
{
|
|
354
|
+
name: 'later/ permits when entry_condition.what names the hold',
|
|
355
|
+
file: 'agents/roadmaps/later/probe.md',
|
|
356
|
+
text: `---\nstatus: later\nentry_condition:\n what: the probe hold stays open until 1.2 lands\n when: later\n who: maintainer\n---\n${openText}`,
|
|
357
|
+
want: 0,
|
|
358
|
+
},
|
|
359
|
+
{
|
|
360
|
+
name: 'the active root permits a live window',
|
|
361
|
+
file: 'agents/roadmaps/probe.md',
|
|
362
|
+
text: openText,
|
|
363
|
+
want: 0,
|
|
364
|
+
},
|
|
365
|
+
{
|
|
366
|
+
name: 'archive/ permits a CLEARED window',
|
|
367
|
+
file: 'agents/roadmaps/archive/probe.md',
|
|
368
|
+
text: base('x', 'x'),
|
|
369
|
+
want: 0,
|
|
370
|
+
},
|
|
371
|
+
];
|
|
372
|
+
|
|
373
|
+
let failed = 0;
|
|
374
|
+
for (const c of cases) {
|
|
375
|
+
const holds = evaluateHolds(c.text, 'selftest.md');
|
|
376
|
+
const h = holds[0];
|
|
377
|
+
const ok =
|
|
378
|
+
h !== undefined &&
|
|
379
|
+
h.state === c.state &&
|
|
380
|
+
refuses(h, 'all') === c.all &&
|
|
381
|
+
refuses(h, 'latest') === c.latest;
|
|
382
|
+
if (!ok) {
|
|
383
|
+
failed += 1;
|
|
384
|
+
process.stderr.write(
|
|
385
|
+
` ❌ ${c.name}: got state=${h?.state ?? '<none>'} ` +
|
|
386
|
+
`all=${h ? refuses(h, 'all') : '-'} latest=${h ? refuses(h, 'latest') : '-'}; ` +
|
|
387
|
+
`want state=${c.state} all=${c.all} latest=${c.latest}\n`,
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
for (const n of negatives) {
|
|
392
|
+
const holds = evaluateHolds(n.text, 'selftest.md');
|
|
393
|
+
if (holds.length !== 0) {
|
|
394
|
+
failed += 1;
|
|
395
|
+
process.stderr.write(` ❌ ${n.name}: got ${holds.length} hold(s), want 0\n`);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
for (const l of lifecycle) {
|
|
400
|
+
const got = lifecycleViolations(l.file, evaluateHolds(l.text, l.file), l.text).length;
|
|
401
|
+
if (got !== l.want) {
|
|
402
|
+
failed += 1;
|
|
403
|
+
process.stderr.write(` ❌ ${l.name}: got ${got} violation(s), want ${l.want}\n`);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const total = cases.length + negatives.length + lifecycle.length;
|
|
408
|
+
if (failed > 0) {
|
|
409
|
+
process.stderr.write(`❌ check-release-holds --selftest: ${failed}/${total} failed\n`);
|
|
410
|
+
return 1;
|
|
411
|
+
}
|
|
412
|
+
process.stdout.write(
|
|
413
|
+
`✅ check-release-holds --selftest: ${total}/${total} — every state-table row ` +
|
|
414
|
+
`(including not-evaluable, 7 ways), both negatives, and all six lifecycle cases\n`,
|
|
415
|
+
);
|
|
416
|
+
return 0;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* The house self-test: fixture trees on disk, this gate's real CLI, exit codes.
|
|
421
|
+
*
|
|
422
|
+
* Two accept cases are not padding. A gate that refused everything would pass
|
|
423
|
+
* every reject case, and the one that matters most is the VALID OPEN WINDOW:
|
|
424
|
+
* normal CI must stay green on exactly the tree state the release path refuses,
|
|
425
|
+
* and nothing else in this suite would notice if `--lint` started reddening it.
|
|
426
|
+
*/
|
|
427
|
+
function cmdConventionSelfTest(): number {
|
|
428
|
+
const script = 'src/scripts/check_release_holds.ts';
|
|
429
|
+
const mk = (name: string, files: Record<string, string>): string => {
|
|
430
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), `crh-${name}-`));
|
|
431
|
+
for (const [rel, body] of Object.entries(files)) {
|
|
432
|
+
const abs = path.join(root, rel);
|
|
433
|
+
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
434
|
+
fs.writeFileSync(abs, body, 'utf8');
|
|
435
|
+
}
|
|
436
|
+
return root;
|
|
437
|
+
};
|
|
438
|
+
const hold = (open: string, clear: string, extra = ''): string =>
|
|
439
|
+
[
|
|
440
|
+
'## Phase 1',
|
|
441
|
+
`- [${open}] **1.1 opener** <!-- opens-hold: probe -->`,
|
|
442
|
+
' verify: the opener check',
|
|
443
|
+
`- [${clear}] **1.2 clearer** <!-- clears-hold: probe -->`,
|
|
444
|
+
' verify: ./scripts-run src/scripts/check_surface_whole',
|
|
445
|
+
'',
|
|
446
|
+
'## Release holds',
|
|
447
|
+
'',
|
|
448
|
+
'### hold: probe',
|
|
449
|
+
'- **Channel:** all',
|
|
450
|
+
'- **Opened by:** 1.1',
|
|
451
|
+
'- **Cleared by:** 1.2',
|
|
452
|
+
'- **State:** the surface is half wired while this is open.',
|
|
453
|
+
`- **Why not a guard:** the entry point cannot be made inert.${extra}`,
|
|
454
|
+
'',
|
|
455
|
+
].join('\n');
|
|
456
|
+
const at = (root: string, args: string[]): number =>
|
|
457
|
+
runGateCli(REPO_ROOT, script, [...args, '--root', root], REPO_ROOT);
|
|
458
|
+
|
|
459
|
+
const cases: SelfTestCase[] = [
|
|
460
|
+
{
|
|
461
|
+
name: 'a malformed declaration reddens --lint',
|
|
462
|
+
expect: 'reject',
|
|
463
|
+
run: () =>
|
|
464
|
+
at(
|
|
465
|
+
mk('bad', {
|
|
466
|
+
'agents/roadmaps/r.md': hold('x', ' ').replace(
|
|
467
|
+
/- \*\*Why not a guard:\*\*.*\n/,
|
|
468
|
+
'',
|
|
469
|
+
),
|
|
470
|
+
}),
|
|
471
|
+
['--lint'],
|
|
472
|
+
),
|
|
473
|
+
},
|
|
474
|
+
{
|
|
475
|
+
name: 'an open window refuses --require-safe',
|
|
476
|
+
expect: 'reject',
|
|
477
|
+
run: () => at(mk('open', { 'agents/roadmaps/r.md': hold('x', ' ') }), ['--require-safe']),
|
|
478
|
+
},
|
|
479
|
+
{
|
|
480
|
+
name: 'a live window sitting in archive/ reddens --lint',
|
|
481
|
+
expect: 'reject',
|
|
482
|
+
run: () => at(mk('arch', { 'agents/roadmaps/archive/r.md': hold('x', ' ') }), ['--lint']),
|
|
483
|
+
},
|
|
484
|
+
{
|
|
485
|
+
name: 'a dead scan scope reddens rather than certifying an empty corpus',
|
|
486
|
+
expect: 'reject',
|
|
487
|
+
run: () => at(mk('dead', { 'README.md': 'no roadmaps here\n' }), ['--lint']),
|
|
488
|
+
},
|
|
489
|
+
{
|
|
490
|
+
name: 'a clean corpus passes --lint',
|
|
491
|
+
expect: 'accept',
|
|
492
|
+
run: () =>
|
|
493
|
+
at(mk('clean', { 'agents/roadmaps/r.md': '## Phase 1\n- [ ] **1.1 x**\n' }), ['--lint']),
|
|
494
|
+
},
|
|
495
|
+
{
|
|
496
|
+
name: 'a VALID OPEN window passes --lint — normal CI stays green on it',
|
|
497
|
+
expect: 'accept',
|
|
498
|
+
run: () => at(mk('valid', { 'agents/roadmaps/r.md': hold('x', ' ') }), ['--lint']),
|
|
499
|
+
},
|
|
500
|
+
];
|
|
501
|
+
return runSelfTest({ gate: 'check_release_holds', cases, minCases: 6, minRejectCases: 4 });
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function usage(): number {
|
|
505
|
+
process.stderr.write(
|
|
506
|
+
'usage: check_release_holds (--lint | --status | --require-safe [--channel latest|all]\n' +
|
|
507
|
+
' | --can-move <file> <archive|skipped|later>\n' +
|
|
508
|
+
' | --selftest | --self-test) [--root <dir>]\n' +
|
|
509
|
+
' --channel all (default) a stable X.Y.Z cut — every open hold refuses\n' +
|
|
510
|
+
' --channel latest a -next.N prerelease — only `Channel: all` holds refuse\n',
|
|
511
|
+
);
|
|
512
|
+
return 2;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function main(argv: string[] = process.argv.slice(2)): number {
|
|
516
|
+
const ri = argv.indexOf('--root');
|
|
517
|
+
const root = ri === -1 ? REPO_ROOT : path.resolve(argv[ri + 1] ?? '.');
|
|
518
|
+
// `--self-test` is the house convention (`_lib/gate_self_test`): it drives
|
|
519
|
+
// this CLI against fixture trees on disk and asserts the exit code, so what
|
|
520
|
+
// it proves is that the BINARY still rejects. `--selftest` is the unit-level
|
|
521
|
+
// state-table suite. They are complementary, not duplicates -- one can pass
|
|
522
|
+
// while the other fails, which is the whole reason both exist.
|
|
523
|
+
if (argv.includes('--self-test') && process.env['GATE_SELF_TEST_CHILD'] !== '1') {
|
|
524
|
+
return cmdConventionSelfTest();
|
|
525
|
+
}
|
|
526
|
+
if (argv.includes('--selftest')) {
|
|
527
|
+
return cmdSelftest();
|
|
528
|
+
}
|
|
529
|
+
if (argv.includes('--lint')) {
|
|
530
|
+
return cmdLint(root);
|
|
531
|
+
}
|
|
532
|
+
if (argv.includes('--status')) {
|
|
533
|
+
return cmdStatus(root);
|
|
534
|
+
}
|
|
535
|
+
if (argv.includes('--require-safe')) {
|
|
536
|
+
const i = argv.indexOf('--channel');
|
|
537
|
+
const raw = i === -1 ? 'all' : argv[i + 1];
|
|
538
|
+
if (raw !== 'all' && raw !== 'latest') {
|
|
539
|
+
process.stderr.write(`❌ --channel must be \`all\` or \`latest\`, got \`${raw ?? ''}\`\n`);
|
|
540
|
+
return usage();
|
|
541
|
+
}
|
|
542
|
+
return cmdRequireSafe(root, raw);
|
|
543
|
+
}
|
|
544
|
+
const mv = argv.indexOf('--can-move');
|
|
545
|
+
if (mv !== -1) {
|
|
546
|
+
const file = argv[mv + 1];
|
|
547
|
+
const dest = argv[mv + 2];
|
|
548
|
+
if (file === undefined || dest === undefined) {
|
|
549
|
+
return usage();
|
|
550
|
+
}
|
|
551
|
+
return cmdCanMove(path.resolve(file), dest);
|
|
552
|
+
}
|
|
553
|
+
return usage();
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
function _isCliEntry(): boolean {
|
|
557
|
+
if (process.argv[1] === undefined) {
|
|
558
|
+
return false;
|
|
559
|
+
}
|
|
560
|
+
return import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
if (_isCliEntry() || process.argv[1] === _HERE) {
|
|
564
|
+
process.exit(main());
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
export { collect, globRoadmaps, main, HOLD_DIRS };
|
|
@@ -57,7 +57,7 @@ function _package_name(): string {
|
|
|
57
57
|
*
|
|
58
58
|
* This probe used to answer local-OR-remote, and that made the gate reply
|
|
59
59
|
* "tagged, therefore published" to the exact state it exists to detect. A tag
|
|
60
|
-
* created by `release.ts` step
|
|
60
|
+
* created by `release.ts` step 9 whose push then failed sits in the local
|
|
61
61
|
* repository while nothing has shipped: `publish-npm.yml` triggers on
|
|
62
62
|
* `push: tags:`, so a tag that never reached the remote published nothing.
|
|
63
63
|
* Measured 2026-08-20 on 14.6.0 — main carried `package.json` 14.6.0, the tag
|