@event4u/agent-config 14.17.0 → 14.19.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 +207 -77
- package/README.md +72 -150
- package/dist/agent-src/README.md +1 -1
- package/dist/agent-src/commands/agent-handoff.md +2 -2
- package/dist/agent-src/commands/pr/merge.md +35 -44
- package/dist/agent-src/commands/review/changes.md +19 -19
- package/dist/agent-src/commands/review.md +2 -2
- package/dist/agent-src/contexts/execution/host-capability-manifest.md +30 -3
- package/dist/agent-src/rules/autonomous-execution.md +1 -1
- package/dist/agent-src/rules/git-history-discipline.md +2 -2
- package/dist/agent-src/rules/guidelines.md +1 -1
- package/dist/agent-src/rules/session-canary.md +2 -2
- package/dist/agent-src/rules/token-efficiency.md +1 -1
- package/dist/agent-src/skills/accessibility-auditor/SKILL.md +2 -0
- package/dist/agent-src/skills/adversarial-review/SKILL.md +0 -8
- package/dist/agent-src/skills/ai-code-blindspots/SKILL.md +27 -1
- package/dist/agent-src/skills/canvas-design/SKILL.md +1 -1
- package/dist/agent-src/skills/code-intelligence/SKILL.md +60 -12
- package/dist/agent-src/skills/complexity-first-planning/SKILL.md +27 -0
- package/dist/agent-src/skills/design-intelligence/SKILL.md +1 -1
- package/dist/agent-src/skills/design-intelligence/data/app-interface.csv +1 -1
- package/dist/agent-src/skills/design-intelligence/data/motion.csv +17 -17
- package/dist/agent-src/skills/design-intelligence/data/ux-guidelines.csv +2 -2
- package/dist/agent-src/skills/design-intelligence/references/context-and-registers.md +17 -0
- package/dist/agent-src/skills/design-intelligence/references/design-rules-checklist.md +4 -4
- package/dist/agent-src/skills/design-review/SKILL.md +3 -1
- package/dist/agent-src/skills/design-variations/SKILL.md +1 -1
- package/dist/agent-src/skills/existing-ui-audit/SKILL.md +1 -1
- package/dist/agent-src/skills/fe-design/SKILL.md +32 -5
- package/dist/agent-src/skills/fe-design/references/craft-floor.md +7 -0
- package/dist/agent-src/skills/fe-design/references/design-patterns.md +47 -2
- package/dist/agent-src/skills/feature-planning/SKILL.md +34 -0
- package/dist/agent-src/skills/git-workflow/SKILL.md +37 -12
- package/dist/agent-src/skills/git-workflow/references/push-closes-its-loop.md +176 -0
- package/dist/agent-src/skills/laravel-mail/SKILL.md +44 -0
- package/dist/agent-src/skills/laravel-scheduling/SKILL.md +3 -1
- package/dist/agent-src/skills/project-analyzer/SKILL.md +0 -6
- package/dist/agent-src/skills/react-native-setup/SKILL.md +3 -3
- package/dist/agent-src/skills/react-shadcn-ui/SKILL.md +1 -1
- package/dist/agent-src/skills/sequential-thinking/SKILL.md +18 -8
- package/dist/agent-src/skills/tailwind-engineer/SKILL.md +1 -1
- package/dist/agent-src/skills/testing-anti-patterns/SKILL.md +40 -0
- package/dist/agent-src/skills/ui-apply-generic/SKILL.md +1 -1
- package/dist/agent-src/skills/ui-component-architect/SKILL.md +1 -1
- package/dist/agent-src/skills/using-git-worktrees/SKILL.md +29 -2
- package/dist/agent-src/skills/verify-repair-loop/SKILL.md +53 -8
- package/dist/agent-src/skills/worktree-lifecycle/SKILL.md +3 -1
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/cli/commands/recordTriggerEval.js +2 -2
- package/dist/cli/main.js +1 -1
- package/dist/cli-delegate/{chunk-AKJGUQNO.js → chunk-GZTMRYCN.js} +405 -341
- package/dist/cli-delegate/{chunk-7RXIKF62.js → chunk-IDHUQIXP.js} +283 -132
- package/dist/cli-delegate/cmd_adr_effective.js +318 -81
- package/dist/cli-delegate/cmd_conformance.js +749 -669
- package/dist/cli-delegate/cmd_doctor.js +1 -1
- package/dist/cli-delegate/cmd_export.js +1 -1
- package/dist/cli-delegate/cmd_handoff.js +1 -1
- package/dist/cli-delegate/cmd_prune.js +1 -1
- package/dist/cli-delegate/cmd_route_audit.js +1 -1
- package/dist/cli-delegate/cmd_settings_migrate.js +1 -1
- package/dist/cli-delegate/cmd_sync.js +1 -1
- package/dist/cli-delegate/cmd_uninstall.js +1 -1
- package/dist/cli-delegate/cmd_validate.js +1 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +39 -38
- 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 +2764 -2439
- package/dist/install/install.mjs +588 -527
- package/dist/mcp/registry-manifest.json +2 -2
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +1381 -1138
- package/dist/scripts/_lib/model_tier.js +1 -1
- package/dist/shared/settingsConsent.js +1 -1
- package/docs/guidelines/agent-infra/evaluator-independence-mechanics.md +1 -1
- package/docs/guidelines/agent-infra/roadmap-progress-mechanics.md +1 -1
- package/docs/guidelines/design-antipatterns.md +87 -64
- package/package.json +1 -1
- package/src/agent-src/README.md +1 -1
- package/src/config/ci-local-parity.yml +6 -0
- package/src/config/conformance-claim-baseline.json +9 -0
- package/src/config/gate-coverage.yml +170 -0
- package/src/config/gate-violation-baselines.json +4 -4
- package/src/config/release-gate-locality.yml +141 -0
- package/src/scripts/_cli/cmd_doctor.ts +2 -2
- package/src/scripts/_cli/cmd_handoff.ts +1 -1
- package/src/scripts/_cli/cmd_update.ts +1 -1
- package/src/scripts/_cli/cmd_versions.ts +1 -1
- package/src/scripts/_lib/adr_frontmatter.ts +44 -0
- package/src/scripts/_lib/consequence_objects.ts +152 -0
- package/src/scripts/_lib/exec_evidence.ts +7 -0
- package/src/scripts/_lib/global_deploy_inventory.ts +2 -2
- package/src/scripts/_lib/host_permission_checks.ts +251 -0
- package/src/scripts/_lib/model_tier.ts +1 -1
- package/src/scripts/_lib/obligation_frequency.ts +2 -2
- package/src/scripts/_lib/release_highlights.ts +265 -1
- package/src/scripts/_lib/release_material.ts +129 -0
- package/src/scripts/_lib/repeated_run.ts +77 -0
- package/src/scripts/_lib/runtime_journal.ts +26 -3
- package/src/scripts/_lib/runtime_wiring_checks.ts +12 -1
- package/src/scripts/_lib/session_register.ts +11 -6
- package/src/scripts/_lib/sqlite_schema.ts +22 -0
- package/src/scripts/_lib/surface.ts +86 -0
- package/src/scripts/_lib/tamper_vocabulary.ts +242 -0
- package/src/scripts/_lib/ui_authority.ts +116 -0
- package/src/scripts/_lib/unified_diff.ts +121 -0
- package/src/scripts/adr/regenerate_index.ts +54 -1
- package/src/scripts/adr_cite_check.ts +71 -15
- package/src/scripts/ai_council/clients.ts +1 -1
- package/src/scripts/autonomy_friction_corpus.ts +229 -0
- package/src/scripts/build_proof.ts +15 -2
- package/src/scripts/check_beta_review_markers.ts +141 -17
- package/src/scripts/check_enforcement_coverage.ts +1 -1
- package/src/scripts/check_enforcement_denominator.ts +121 -11
- package/src/scripts/check_finding_dispositions.ts +239 -6
- package/src/scripts/check_gate_coverage.ts +96 -0
- package/src/scripts/check_installed_hooks_fresh.ts +381 -0
- package/src/scripts/check_release_highlights.ts +99 -14
- package/src/scripts/check_skill_activation_claim.ts +179 -0
- package/src/scripts/check_suppression_hygiene.ts +11 -0
- package/src/scripts/check_trigger_evals.ts +1 -1
- package/src/scripts/code_graph/build.ts +230 -33
- package/src/scripts/code_graph/extract.ts +168 -33
- package/src/scripts/code_graph/types.ts +37 -2
- package/src/scripts/code_graph/validate.ts +12 -1
- package/src/scripts/conformance_scan.ts +45 -4
- package/src/scripts/detect_silent_catch.ts +403 -0
- package/src/scripts/detect_verification_tampering.ts +538 -0
- package/src/scripts/explain_run.ts +1 -1
- package/src/scripts/generate_capability_matrix.ts +1 -1
- package/src/scripts/git_authorization_hook.ts +228 -18
- package/src/scripts/hook_manifest.json +1 -1
- package/src/scripts/hook_manifest.yaml +47 -30
- package/src/scripts/hooks/augment-dispatcher.sh +1 -1
- package/src/scripts/hooks/block_no_verify.ts +3 -3
- package/src/scripts/hooks/category_a.ts +291 -0
- package/src/scripts/hooks/cline-dispatcher.sh +1 -1
- package/src/scripts/hooks/concern_registry.ts +2 -2
- package/src/scripts/hooks/cursor-dispatcher.sh +11 -5
- package/src/scripts/hooks/dispatch_hook.ts +17 -22
- package/src/scripts/hooks/evidence_independence.ts +21 -2
- package/src/scripts/hooks/{block_unauthorized_git.ts → git_command_classifier.ts} +175 -364
- package/src/scripts/hooks/host_lowering.ts +215 -0
- package/src/scripts/hooks/host_lowering.yaml +185 -0
- package/src/scripts/hooks/host_semantics.ts +105 -37
- package/src/scripts/hooks/journal_record_hook.ts +10 -0
- package/src/scripts/hooks/permission_gate.ts +117 -0
- package/src/scripts/hooks/push_settle_hook.ts +250 -0
- package/src/scripts/hooks/ship_diff_volume_hook.ts +1 -1
- package/src/scripts/hooks_status.ts +12 -10
- package/src/scripts/install-hooks.sh +134 -1
- package/src/scripts/install.ts +53 -82
- package/src/scripts/install_anthropic_key.sh +2 -2
- package/src/scripts/language_mirror_hook.ts +27 -5
- package/src/scripts/lint_design_antipattern_parity.ts +80 -0
- package/src/scripts/lint_evidence_artifacts.ts +45 -1
- package/src/scripts/lint_hook_manifest.ts +87 -3
- package/src/scripts/lint_motion_authority_drift.ts +731 -0
- package/src/scripts/lint_readme_size.ts +158 -12
- package/src/scripts/lint_reasoning_fixtures.ts +216 -0
- package/src/scripts/lint_skill_trigger_contradiction.ts +161 -0
- package/src/scripts/measure_sibling_naming.ts +150 -0
- package/src/scripts/redact_hook_capture.ts +1 -1
- package/src/scripts/release.ts +31 -143
- package/src/scripts/release_drill.ts +71 -1
- package/src/scripts/release_publication.ts +243 -3
- package/src/scripts/release_verify.ts +220 -0
- package/src/scripts/report_skill_activation.ts +319 -7
- package/src/scripts/schemas/review-findings.schema.json +6 -1
- package/src/scripts/schemas/skill.schema.json +12 -2
- package/src/scripts/self_review_gate.ts +10 -1
- package/src/scripts/session_canary_hook.ts +1 -1
- package/src/scripts/session_register_hook.ts +4 -3
- package/src/scripts/trigger_eval_grandfather.json +5 -1
- package/src/scripts/validate_agent_settings.ts +1 -1
- package/src/shared/settingsConsent.ts +1 -1
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* measure_sibling_naming — how many skill descriptions name another skill.
|
|
3
|
+
*
|
|
4
|
+
* `road-to-one-motion-authority` step 5.2. An inbox census claimed "44/299
|
|
5
|
+
* descriptions name a sibling" and shipped no definition of *naming* and no
|
|
6
|
+
* script, so the figure could be neither reproduced nor refuted. A companion
|
|
7
|
+
* claim of "218/299 bodies" reproduced under no reading of the tree at all.
|
|
8
|
+
* This script exists so the next such number is falsifiable.
|
|
9
|
+
*
|
|
10
|
+
* WHAT "NAMES A SIBLING" MEANS HERE, stated because the census did not.
|
|
11
|
+
*
|
|
12
|
+
* A description names a sibling when it contains the exact `name:` value of a
|
|
13
|
+
* DIFFERENT shipped skill, delimited so it is not a fragment of a longer
|
|
14
|
+
* identifier. `\b` is not enough: it puts a boundary between `brand` and the
|
|
15
|
+
* hyphen in `brand-identity`, so a bare `\b`-matched `brand` would score every
|
|
16
|
+
* mention of `brand-identity` as naming `brand` too. The delimiter here is
|
|
17
|
+
* "not `[A-Za-z0-9-]`" on both sides, which is the smallest rule that keeps
|
|
18
|
+
* `mcp` out of `mcp-builder` and `laravel` out of `laravel-dto`.
|
|
19
|
+
*
|
|
20
|
+
* A skill naming ITSELF does not count — that is a description, not a pointer.
|
|
21
|
+
*
|
|
22
|
+
* WHAT THE NUMBER IS AND IS NOT.
|
|
23
|
+
*
|
|
24
|
+
* A KNOWN AND UNFIXABLE FALSE POSITIVE, named rather than left for a reader to
|
|
25
|
+
* find: some skill names are ordinary English words, so a description can score
|
|
26
|
+
* a match by accident. `canvas-design` scores `brand` on the phrase "brand
|
|
27
|
+
* assets", which is not a pointer to the `brand` skill. Excluding short or
|
|
28
|
+
* common names would trade this for a false negative on the skills whose names
|
|
29
|
+
* really are those words, and there is no rule that gets both. The count is
|
|
30
|
+
* therefore an upper bound; the per-skill list is printed so a reader can see
|
|
31
|
+
* which matches are real.
|
|
32
|
+
*
|
|
33
|
+
* It is a count of descriptions that mention a sibling by name. It is NOT a
|
|
34
|
+
* measure of routing quality: a description can name a sibling uselessly, and a
|
|
35
|
+
* description that names none can be perfectly routable on its own keywords.
|
|
36
|
+
* The reason the count is worth having is narrower — the router reads
|
|
37
|
+
* descriptions and nothing else, so a family whose members never point at each
|
|
38
|
+
* other is a family the router cannot traverse.
|
|
39
|
+
*
|
|
40
|
+
* Exit code is 0 unless `--min <n>` is given and the count falls below it.
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
import fs from 'node:fs';
|
|
44
|
+
import path from 'node:path';
|
|
45
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
46
|
+
|
|
47
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
48
|
+
const REPO_ROOT = path.resolve(HERE, '..', '..');
|
|
49
|
+
const SKILLS_REL = path.join('src', 'skills');
|
|
50
|
+
|
|
51
|
+
export interface SkillRecord { name: string; description: string }
|
|
52
|
+
|
|
53
|
+
/** Read `name:` and `description:` out of a SKILL.md frontmatter block. */
|
|
54
|
+
export function parseSkill(text: string): SkillRecord | null {
|
|
55
|
+
if (!text.startsWith('---')) return null;
|
|
56
|
+
const end = text.indexOf('\n---', 3);
|
|
57
|
+
if (end < 0) return null;
|
|
58
|
+
const fm = text.slice(3, end);
|
|
59
|
+
const name = /^name:\s*(.+)$/m.exec(fm)?.[1]?.trim() ?? '';
|
|
60
|
+
let description = /^description:\s*(.+)$/m.exec(fm)?.[1]?.trim() ?? '';
|
|
61
|
+
if ((description.startsWith('"') && description.endsWith('"'))
|
|
62
|
+
|| (description.startsWith("'") && description.endsWith("'"))) {
|
|
63
|
+
description = description.slice(1, -1);
|
|
64
|
+
}
|
|
65
|
+
description = description.replace(/\\"/g, '"');
|
|
66
|
+
if (name === '') return null;
|
|
67
|
+
return { name, description };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function loadSkills(root: string): SkillRecord[] {
|
|
71
|
+
const dir = path.join(root, SKILLS_REL);
|
|
72
|
+
if (!fs.existsSync(dir)) return [];
|
|
73
|
+
const out: SkillRecord[] = [];
|
|
74
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
75
|
+
if (!entry.isDirectory()) continue;
|
|
76
|
+
const file = path.join(dir, entry.name, 'SKILL.md');
|
|
77
|
+
if (!fs.existsSync(file)) continue;
|
|
78
|
+
const rec = parseSkill(fs.readFileSync(file, 'utf-8'));
|
|
79
|
+
if (rec !== null) out.push(rec);
|
|
80
|
+
}
|
|
81
|
+
return out.sort((a, b) => a.name.localeCompare(b.name));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Identifier-boundary containment: `mcp` is not found inside `mcp-builder`. */
|
|
85
|
+
export function namesIdentifier(haystack: string, id: string): boolean {
|
|
86
|
+
let from = 0;
|
|
87
|
+
for (;;) {
|
|
88
|
+
const i = haystack.indexOf(id, from);
|
|
89
|
+
if (i < 0) return false;
|
|
90
|
+
const before = i === 0 ? '' : haystack[i - 1] ?? '';
|
|
91
|
+
const after = haystack[i + id.length] ?? '';
|
|
92
|
+
const isPart = (c: string): boolean => c !== '' && /[A-Za-z0-9-]/.test(c);
|
|
93
|
+
if (!isPart(before) && !isPart(after)) return true;
|
|
94
|
+
from = i + 1;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** For each skill, the OTHER skills its description names. Sorted, deterministic. */
|
|
99
|
+
export function siblingsNamed(skills: readonly SkillRecord[]): Map<string, string[]> {
|
|
100
|
+
const names = skills.map((s) => s.name);
|
|
101
|
+
const out = new Map<string, string[]>();
|
|
102
|
+
for (const s of skills) {
|
|
103
|
+
const hits = names
|
|
104
|
+
.filter((n) => n !== s.name && namesIdentifier(s.description, n))
|
|
105
|
+
.sort();
|
|
106
|
+
out.set(s.name, hits);
|
|
107
|
+
}
|
|
108
|
+
return out;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function main(argv: readonly string[] = process.argv.slice(2)): number {
|
|
112
|
+
const rootFlag = argv.indexOf('--root');
|
|
113
|
+
const root = rootFlag >= 0 ? (argv[rootFlag + 1] ?? REPO_ROOT) : REPO_ROOT;
|
|
114
|
+
const minFlag = argv.indexOf('--min');
|
|
115
|
+
const min = minFlag >= 0 ? Number(argv[minFlag + 1]) : null;
|
|
116
|
+
const json = argv.includes('--json');
|
|
117
|
+
|
|
118
|
+
const skills = loadSkills(root);
|
|
119
|
+
const map = siblingsNamed(skills);
|
|
120
|
+
const naming = [...map].filter(([, v]) => v.length > 0);
|
|
121
|
+
|
|
122
|
+
if (json) {
|
|
123
|
+
process.stdout.write(`${JSON.stringify({
|
|
124
|
+
total: skills.length,
|
|
125
|
+
naming: naming.length,
|
|
126
|
+
entries: Object.fromEntries(naming),
|
|
127
|
+
}, null, 1)}\n`);
|
|
128
|
+
} else {
|
|
129
|
+
process.stdout.write(`scanned: ${String(skills.length)}\n`);
|
|
130
|
+
process.stdout.write(
|
|
131
|
+
`sibling-naming: ${String(naming.length)}/${String(skills.length)} description(s) name at least one other shipped skill\n\n`,
|
|
132
|
+
);
|
|
133
|
+
for (const [name, hits] of naming) {
|
|
134
|
+
process.stdout.write(` ${name} → ${hits.join(', ')}\n`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (min !== null && Number.isFinite(min) && naming.length < min) {
|
|
139
|
+
process.stderr.write(
|
|
140
|
+
`\n❌ measure_sibling_naming: ${String(naming.length)} below the declared floor of ${String(min)}\n`,
|
|
141
|
+
);
|
|
142
|
+
return 1;
|
|
143
|
+
}
|
|
144
|
+
return 0;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (process.argv[1] !== undefined) {
|
|
148
|
+
const invoked = pathToFileURL(path.resolve(process.argv[1])).href;
|
|
149
|
+
if (invoked === import.meta.url) process.exitCode = main();
|
|
150
|
+
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Ported from the retired Python `redact_hook_capture.py` (Phase 8 / Wave 8g).
|
|
6
6
|
*
|
|
7
|
-
* Reads JSON capture files written by `dispatch_hook.
|
|
7
|
+
* Reads JSON capture files written by `dispatch_hook.ts` (when
|
|
8
8
|
* `AGENT_HOOK_CAPTURE_DIR` is set) and produces a redacted version
|
|
9
9
|
* suitable for pasting into the verified-platforms roadmap.
|
|
10
10
|
*
|
package/src/scripts/release.ts
CHANGED
|
@@ -102,7 +102,6 @@
|
|
|
102
102
|
* - `_lib.changelog_eras` imports resolve to the `.ts` twin, never a `.py`.
|
|
103
103
|
*/
|
|
104
104
|
|
|
105
|
-
import { spawnSync } from 'node:child_process';
|
|
106
105
|
import * as fs from 'node:fs';
|
|
107
106
|
import * as path from 'node:path';
|
|
108
107
|
import process from 'node:process';
|
|
@@ -120,16 +119,17 @@ import {
|
|
|
120
119
|
} from './_lib/changelog_eras.js';
|
|
121
120
|
import {
|
|
122
121
|
HEAD_LABELS,
|
|
122
|
+
type MixObligation,
|
|
123
123
|
collect_span_commits,
|
|
124
124
|
derive_category_hits,
|
|
125
125
|
render_derived_head_values,
|
|
126
126
|
} from './_lib/release_highlights.js';
|
|
127
127
|
import {
|
|
128
|
-
NEXT_SECTION_RE,
|
|
129
128
|
RELEASE_HEAD_DEFAULT,
|
|
130
129
|
extract_changelog_section,
|
|
131
130
|
pr_body_from_section,
|
|
132
131
|
release_notes_from_section,
|
|
132
|
+
render_mix_response,
|
|
133
133
|
} from './_lib/release_material.js';
|
|
134
134
|
|
|
135
135
|
// `__doc__.splitlines()[0]` in `_parse_args` — the argparse description. Kept
|
|
@@ -164,11 +164,16 @@ import {
|
|
|
164
164
|
_splitlines,
|
|
165
165
|
} from './release_env.js';
|
|
166
166
|
import type { RunResult } from './release_publication.js';
|
|
167
|
-
// Re-export surface for the
|
|
168
|
-
//
|
|
167
|
+
// Re-export surface for the names tests import from `release.js` and that moved
|
|
168
|
+
// into the publication unit — eleven since the test-count-trend unit followed. `export ... from` rather than a bare import,
|
|
169
169
|
// because these are not USED here — an unused import would be dropped and the
|
|
170
170
|
// test import path would break silently.
|
|
171
171
|
export {
|
|
172
|
+
_TEST_LIST_MAX_BUFFER,
|
|
173
|
+
_count_from_list_result,
|
|
174
|
+
_count_tests_current,
|
|
175
|
+
_previous_test_count_from_changelog,
|
|
176
|
+
_render_test_trend_line,
|
|
172
177
|
_failed_check_names,
|
|
173
178
|
_failed_checks_report,
|
|
174
179
|
_is_non_fast_forward,
|
|
@@ -176,7 +181,7 @@ export {
|
|
|
176
181
|
_no_checks_action,
|
|
177
182
|
_required_contexts_from_rules,
|
|
178
183
|
} from './release_publication.js';
|
|
179
|
-
import { _cap_body, jsonDumpsIndent
|
|
184
|
+
import { _cap_body, jsonDumpsIndent } from './release_env.js';
|
|
180
185
|
import {
|
|
181
186
|
_MERGE_UPDATE_ROUNDS,
|
|
182
187
|
_branch_exists_local,
|
|
@@ -195,7 +200,10 @@ import {
|
|
|
195
200
|
create_and_push_annotated_tag,
|
|
196
201
|
guard_publication,
|
|
197
202
|
checkout_release_branch,
|
|
203
|
+
_render_test_trend_line,
|
|
198
204
|
guard_release_curation,
|
|
205
|
+
local_release_gate_argv,
|
|
206
|
+
measure_mix_obligation,
|
|
199
207
|
die,
|
|
200
208
|
gh,
|
|
201
209
|
git,
|
|
@@ -500,7 +508,11 @@ function render_changelog_entry(
|
|
|
500
508
|
prev: string | null,
|
|
501
509
|
commits: readonly Commit[],
|
|
502
510
|
today: string,
|
|
503
|
-
opts: {
|
|
511
|
+
opts: {
|
|
512
|
+
test_trend_line?: string | null;
|
|
513
|
+
head?: Readonly<Record<string, string>>;
|
|
514
|
+
mix?: MixObligation | null;
|
|
515
|
+
} = {},
|
|
504
516
|
): [string, string] {
|
|
505
517
|
const test_trend_line = opts.test_trend_line ?? null;
|
|
506
518
|
let heading: string;
|
|
@@ -539,6 +551,11 @@ function render_changelog_entry(
|
|
|
539
551
|
|
|
540
552
|
// The curated head sits above the generated log, which stays unchanged.
|
|
541
553
|
const body_lines: string[] = [...render_release_head(opts.head ?? {})];
|
|
554
|
+
// Under the head and outside it — rationale on `MIX_RESPONSE_PLACEHOLDER`.
|
|
555
|
+
if (opts.mix?.triggered) {
|
|
556
|
+
body_lines.push('');
|
|
557
|
+
body_lines.push(...render_mix_response(opts.mix.level));
|
|
558
|
+
}
|
|
542
559
|
const ordered_labels = ['BREAKING CHANGES', ...SECTIONS.map(([label]) => label)];
|
|
543
560
|
for (const label of ordered_labels) {
|
|
544
561
|
const bucket = grouped[label] ?? [];
|
|
@@ -581,139 +598,6 @@ function _changelog_line(c: Commit): string {
|
|
|
581
598
|
return `* ${scope}${c.subject} ([${short}](${link}))`;
|
|
582
599
|
}
|
|
583
600
|
|
|
584
|
-
// ─── test-count trend (road-to-feedback-followups P3.2) ───────────────────────
|
|
585
|
-
|
|
586
|
-
const _TEST_COUNT_LINE_RE = /^Tests:\s+(\d+)/m;
|
|
587
|
-
|
|
588
|
-
/**
|
|
589
|
-
* Buffer ceiling for the `vitest list` probe, in bytes.
|
|
590
|
-
*
|
|
591
|
-
* `spawnSync` buffers the child's whole stdout in memory and fails with
|
|
592
|
-
* ENOBUFS past `maxBuffer`, whose default is 1 MiB. The probe emits one line
|
|
593
|
-
* per test case, so its output grows with the suite: at 9470 cases the listing
|
|
594
|
-
* is ~1.25 MB, i.e. already past the default. That is what silently dropped
|
|
595
|
-
* the footer from the 9.10.0 notes and turned the `changelog-entry` gate red.
|
|
596
|
-
* 64 MiB is ~50× the current listing — headroom for a suite many times this
|
|
597
|
-
* size, at no cost when the output is small.
|
|
598
|
-
*/
|
|
599
|
-
const _TEST_LIST_MAX_BUFFER = 64 * 1024 * 1024;
|
|
600
|
-
|
|
601
|
-
/**
|
|
602
|
-
* Count test cases from a finished `vitest list` spawn result, or return null
|
|
603
|
-
* when the probe did not produce a usable listing.
|
|
604
|
-
*
|
|
605
|
-
* Split out from `_count_tests_current` so the failure modes are testable
|
|
606
|
-
* without spawning the real (~14s, >1 MB) collection. `warn` is injected for
|
|
607
|
-
* the same reason; it defaults to stderr.
|
|
608
|
-
*/
|
|
609
|
-
function _count_from_list_result(
|
|
610
|
-
res: {
|
|
611
|
-
error?: (Error & { code?: string }) | undefined;
|
|
612
|
-
status: number | null;
|
|
613
|
-
stdout: string | null;
|
|
614
|
-
},
|
|
615
|
-
warn: (msg: string) => void = (msg) => void process.stderr.write(msg),
|
|
616
|
-
): number | null {
|
|
617
|
-
// The trend line is informational and never blocks a release — but a
|
|
618
|
-
// SILENT drop is what shipped 9.8.0 and 9.10.0 without a footer, so every
|
|
619
|
-
// degradation says why. The `changelog-entry` CI gate treats a missing
|
|
620
|
-
// footer as fatal, so this warning is the difference between fixing the
|
|
621
|
-
// probe now and finding out from a red release PR.
|
|
622
|
-
if (res.error) {
|
|
623
|
-
// ENOENT (no npx) · ETIMEDOUT · ENOBUFS (listing past maxBuffer).
|
|
624
|
-
const code = res.error.code ?? res.error.message;
|
|
625
|
-
warn(
|
|
626
|
-
`⚠️ test-count probe failed (${code}) — the \`Tests:\` footer will ` +
|
|
627
|
-
`be omitted from the release notes, which fails the ` +
|
|
628
|
-
`\`CHANGELOG entry exists for head version\` gate.\n`,
|
|
629
|
-
);
|
|
630
|
-
return null;
|
|
631
|
-
}
|
|
632
|
-
if ((res.status ?? 1) !== 0) {
|
|
633
|
-
warn(
|
|
634
|
-
`⚠️ test-count probe exited ${res.status ?? '(null)'} — the ` +
|
|
635
|
-
`\`Tests:\` footer will be omitted from the release notes.\n`,
|
|
636
|
-
);
|
|
637
|
-
return null;
|
|
638
|
-
}
|
|
639
|
-
const lines = (res.stdout ?? '').split('\n').filter((l) => l.trim().length > 0);
|
|
640
|
-
if (lines.length === 0) {
|
|
641
|
-
warn('⚠️ test-count probe listed 0 cases — the `Tests:` footer will be omitted.\n');
|
|
642
|
-
return null;
|
|
643
|
-
}
|
|
644
|
-
return lines.length;
|
|
645
|
-
}
|
|
646
|
-
|
|
647
|
-
/**
|
|
648
|
-
* Return the collected vitest test-case count on the current tree
|
|
649
|
-
* (`npx vitest list`, one line per case; ~14s wall). Returns null when
|
|
650
|
-
* collection fails — the trend line is informational, never a release
|
|
651
|
-
* blocker, but every failure warns (see `_count_from_list_result`).
|
|
652
|
-
* (Replaced the dead `pytest --collect-only` probe on 2026-07-08,
|
|
653
|
-
* road-to-truth-and-reference-hygiene Phase 3: the Python suite was retired
|
|
654
|
-
* with ADR-200, so the old probe always degraded to null and the `Tests:`
|
|
655
|
-
* footer silently vanished from release notes.)
|
|
656
|
-
*/
|
|
657
|
-
function _count_tests_current(): number | null {
|
|
658
|
-
// Recursion/cost guard: `npx vitest list` inside a vitest-driven release
|
|
659
|
-
// test would collect the whole suite from within the suite (the child
|
|
660
|
-
// inherits VITEST=… from the runner) — the exact recursion the caller
|
|
661
|
-
// comment warns about. Degrade to null there, same as any other
|
|
662
|
-
// collection failure.
|
|
663
|
-
if (process.env['VITEST'] !== undefined) {
|
|
664
|
-
return null;
|
|
665
|
-
}
|
|
666
|
-
const res = spawnSync('npx', ['vitest', 'list'], {
|
|
667
|
-
cwd: REPO_ROOT,
|
|
668
|
-
encoding: 'utf-8',
|
|
669
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
670
|
-
timeout: 180_000,
|
|
671
|
-
maxBuffer: _TEST_LIST_MAX_BUFFER,
|
|
672
|
-
});
|
|
673
|
-
return _count_from_list_result(res);
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
/**
|
|
677
|
-
* Read CHANGELOG.md and return the most recent `Tests: N` footer under the
|
|
678
|
-
* `prev_tag` heading, or null when not found.
|
|
679
|
-
*/
|
|
680
|
-
function _previous_test_count_from_changelog(prev_tag: string | null): number | null {
|
|
681
|
-
if (!prev_tag || !fs.existsSync(CHANGELOG)) {
|
|
682
|
-
return null;
|
|
683
|
-
}
|
|
684
|
-
const text = fs.readFileSync(CHANGELOG, 'utf-8');
|
|
685
|
-
const heading_re = new RegExp(`^##\\s+\\[?${reEscape(prev_tag)}\\b`, 'm');
|
|
686
|
-
const m = heading_re.exec(text);
|
|
687
|
-
if (!m) {
|
|
688
|
-
return null;
|
|
689
|
-
}
|
|
690
|
-
const headEnd = m.index + m[0].length;
|
|
691
|
-
const rest = text.slice(headEnd);
|
|
692
|
-
const next_heading = NEXT_SECTION_RE.exec(rest);
|
|
693
|
-
const sectionEnd = headEnd + (next_heading ? next_heading.index : rest.length);
|
|
694
|
-
const section = text.slice(headEnd, sectionEnd);
|
|
695
|
-
const count_match = _TEST_COUNT_LINE_RE.exec(section);
|
|
696
|
-
return count_match ? Number.parseInt(count_match[1] as string, 10) : null;
|
|
697
|
-
}
|
|
698
|
-
|
|
699
|
-
/**
|
|
700
|
-
* Return the `Tests: N (+M since X.Y.Z)` footer line, or null when the current
|
|
701
|
-
* count cannot be determined. Silent on collection errors.
|
|
702
|
-
*/
|
|
703
|
-
function _render_test_trend_line(prev_tag: string | null): string | null {
|
|
704
|
-
const current = _count_tests_current();
|
|
705
|
-
if (current === null) {
|
|
706
|
-
return null;
|
|
707
|
-
}
|
|
708
|
-
const previous = _previous_test_count_from_changelog(prev_tag);
|
|
709
|
-
if (previous === null || !prev_tag) {
|
|
710
|
-
return `Tests: ${current}`;
|
|
711
|
-
}
|
|
712
|
-
const delta = current - previous;
|
|
713
|
-
const sign = delta >= 0 ? '+' : '';
|
|
714
|
-
return `Tests: ${current} (${sign}${delta} since ${prev_tag})`;
|
|
715
|
-
}
|
|
716
|
-
|
|
717
601
|
/**
|
|
718
602
|
* Insert `entry` inside the current era block.
|
|
719
603
|
*
|
|
@@ -1287,6 +1171,10 @@ function execute(
|
|
|
1287
1171
|
// `git push -u` is naturally idempotent — it prints "Everything
|
|
1288
1172
|
// up-to-date" when remote already matches. push_release_branch
|
|
1289
1173
|
// additionally absorbs a remote that moved under us.
|
|
1174
|
+
// Not a `_step`: a second `[4/10]` makes the cited evidence anchors ambiguous.
|
|
1175
|
+
process.stdout.write(' · verifying release gates locally (`task release:verify -- --cheap`)\n');
|
|
1176
|
+
run(local_release_gate_argv());
|
|
1177
|
+
|
|
1290
1178
|
_step(4, total, `Push ${branch} to ${REMOTE}`);
|
|
1291
1179
|
push_release_branch(branch);
|
|
1292
1180
|
}
|
|
@@ -1834,9 +1722,13 @@ function main(argv: readonly string[] | null = null): number {
|
|
|
1834
1722
|
// made the preview print the `_none_` skeleton while the real run wrote
|
|
1835
1723
|
// pre-filled lines — a preview that contradicts its own output, which is
|
|
1836
1724
|
// the exact class of surface disagreement this pre-fill exists to end.
|
|
1725
|
+
// Unconditional for the same preview-parity reason as `_derive_head_prefill`,
|
|
1726
|
+
// at a different cost: one `git show` per commit — measured 3.2 s over a
|
|
1727
|
+
// 120-commit span, paid 3-4× per release. Not the ~25 ms of the line above.
|
|
1837
1728
|
const [full, body] = render_changelog_entry(target, prev, commits, today, {
|
|
1838
1729
|
test_trend_line,
|
|
1839
1730
|
head: _derive_head_prefill(prev),
|
|
1731
|
+
mix: measure_mix_obligation(target, prev),
|
|
1840
1732
|
});
|
|
1841
1733
|
|
|
1842
1734
|
// Era-split planning — gate on the POST-release view (2026-07-07 fix).
|
|
@@ -1954,10 +1846,6 @@ export {
|
|
|
1954
1846
|
_detect_in_flight_target,
|
|
1955
1847
|
print_preview,
|
|
1956
1848
|
latest_tag,
|
|
1957
|
-
_render_test_trend_line,
|
|
1958
|
-
_previous_test_count_from_changelog,
|
|
1959
|
-
_count_from_list_result,
|
|
1960
|
-
_TEST_LIST_MAX_BUFFER,
|
|
1961
1849
|
Commit,
|
|
1962
1850
|
Plan,
|
|
1963
1851
|
SystemExitError,
|
|
@@ -209,6 +209,18 @@ interface WorldConfig {
|
|
|
209
209
|
* it fakes is one every resumed release actually passes through.
|
|
210
210
|
*/
|
|
211
211
|
pr_already_merged?: boolean;
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Make `task release:verify` fail, as a red local release gate would.
|
|
215
|
+
*
|
|
216
|
+
* Without this the drill returns OK for EVERY `task` invocation, so the
|
|
217
|
+
* pre-push gate run added 2026-09-05 would be exercised only on its happy
|
|
218
|
+
* path — wired but never proven to stop anything. The whole point of that
|
|
219
|
+
* step is that a red local gate costs one edit instead of a branch, a PR
|
|
220
|
+
* and a CI run, and that property is only real if a scenario watches it
|
|
221
|
+
* hold.
|
|
222
|
+
*/
|
|
223
|
+
release_verify_fails?: boolean;
|
|
212
224
|
}
|
|
213
225
|
|
|
214
226
|
/**
|
|
@@ -232,6 +244,20 @@ export function defaultChangelogFixture(target: string): string {
|
|
|
232
244
|
'',
|
|
233
245
|
'- fixture entry for the release drill',
|
|
234
246
|
'',
|
|
247
|
+
// Both lines are section-level publication obligations
|
|
248
|
+
// (`section_publication_blockers`), so a fixture without them refuses
|
|
249
|
+
// at the curation guard and every sequencing scenario dies at step 2 —
|
|
250
|
+
// the same "fixture trips the guard the drill exists to sequence"
|
|
251
|
+
// failure this builder's docstring already warns about, for the two
|
|
252
|
+
// obligations added 2026-09-05. The mix answer is written OUT rather
|
|
253
|
+
// than measured: measuring would couple every scenario to the live
|
|
254
|
+
// repository's commit span, which is the coupling `changelog` was
|
|
255
|
+
// introduced to remove.
|
|
256
|
+
'> **Governance mix:** governance-only 2 vs consumer-only 1 (taxonomy 1.0.0).',
|
|
257
|
+
'> Next cycle ships the fixture consumer work, tracked in the drill.',
|
|
258
|
+
'',
|
|
259
|
+
'Tests: 1000 (+0 since 0.0.0)',
|
|
260
|
+
'',
|
|
235
261
|
'## [0.0.0]',
|
|
236
262
|
'',
|
|
237
263
|
'- earlier release, present so the section boundary is exercised',
|
|
@@ -267,6 +293,8 @@ class FakeWorld {
|
|
|
267
293
|
pr_merged = false;
|
|
268
294
|
release_created = false;
|
|
269
295
|
branch_exists_remote = true;
|
|
296
|
+
/** Whether the simulated `task release:verify` reports a red local gate. */
|
|
297
|
+
private readonly release_verify_fails: boolean;
|
|
270
298
|
/** Live PR body (release-truth Phase 1: refreshed from the changelog at head). */
|
|
271
299
|
pr_body = '';
|
|
272
300
|
|
|
@@ -296,6 +324,7 @@ class FakeWorld {
|
|
|
296
324
|
this.merge_fails_hard = cfg.merge_fails_hard ?? false;
|
|
297
325
|
this.checks_fail = cfg.checks_fail ?? false;
|
|
298
326
|
this.pr_merged = cfg.pr_already_merged ?? false;
|
|
327
|
+
this.release_verify_fails = cfg.release_verify_fails ?? false;
|
|
299
328
|
}
|
|
300
329
|
|
|
301
330
|
exec(args: readonly string[]): ExecResult {
|
|
@@ -374,7 +403,14 @@ class FakeWorld {
|
|
|
374
403
|
|
|
375
404
|
// ── task ─────────────────────────────────────────────────────────
|
|
376
405
|
if (args[0] === 'task') {
|
|
377
|
-
|
|
406
|
+
// `release-prepare` — regeneration is out of drill scope.
|
|
407
|
+
// `release:verify` — the pre-push local gate run; simulated
|
|
408
|
+
// red only when a scenario asks for it, so the step's stopping
|
|
409
|
+
// power is exercised rather than assumed.
|
|
410
|
+
if (args[1] === 'release:verify' && this.release_verify_fails) {
|
|
411
|
+
return { status: 1, stdout: '', stderr: '❌ 1 release gate(s) would fail the release PR: release-shape.\n' };
|
|
412
|
+
}
|
|
413
|
+
return OK;
|
|
378
414
|
}
|
|
379
415
|
|
|
380
416
|
// ── gh ───────────────────────────────────────────────────────────
|
|
@@ -541,6 +577,40 @@ const SCENARIOS: Record<string, Scenario> = {
|
|
|
541
577
|
return f;
|
|
542
578
|
},
|
|
543
579
|
},
|
|
580
|
+
'local-gates-refuse-before-push': {
|
|
581
|
+
summary:
|
|
582
|
+
'step 4: a red local release gate stops the run BEFORE the branch is pushed — no branch on origin, no PR, no CI',
|
|
583
|
+
config: { release_verify_fails: true },
|
|
584
|
+
expect_success: false,
|
|
585
|
+
verify: (w, err) => {
|
|
586
|
+
const f: string[] = [];
|
|
587
|
+
_expect(
|
|
588
|
+
(err ?? '').includes('release:verify'),
|
|
589
|
+
`died for the wrong reason: ${err ?? '(no error)'}`,
|
|
590
|
+
f,
|
|
591
|
+
);
|
|
592
|
+
// The three remote side effects the 14.17.0 run actually produced
|
|
593
|
+
// before `check_release_highlights` refused it on the PR. Each is
|
|
594
|
+
// a separate assertion because each was a separate cost.
|
|
595
|
+
_expect(
|
|
596
|
+
!w.calls.some((c) => c.startsWith('git push')),
|
|
597
|
+
'the release branch was pushed after the local gates refused',
|
|
598
|
+
f,
|
|
599
|
+
);
|
|
600
|
+
_expect(
|
|
601
|
+
!w.calls.some((c) => c.startsWith('gh pr create')),
|
|
602
|
+
'a pull request was opened after the local gates refused',
|
|
603
|
+
f,
|
|
604
|
+
);
|
|
605
|
+
_expect(
|
|
606
|
+
!w.calls.some((c) => c.includes('pr checks')),
|
|
607
|
+
'CI was watched after the local gates refused',
|
|
608
|
+
f,
|
|
609
|
+
);
|
|
610
|
+
return f;
|
|
611
|
+
},
|
|
612
|
+
},
|
|
613
|
+
|
|
544
614
|
'marker-refuses-before-commit': {
|
|
545
615
|
summary:
|
|
546
616
|
'step 3: a draft marker refuses BEFORE the release commit — no local release state, and therefore no remote state either',
|