@event4u/agent-config 14.13.0 → 14.14.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 +116 -387
- package/dist/agent-src/commands/analyze/inbox.md +36 -0
- package/dist/agent-src/rules/code-comment-discipline.md +6 -0
- 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-delegate/cmd_conformance.js +38 -1
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +4 -4
- 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 +1438 -711
- package/dist/mcp/registry-manifest.json +2 -2
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +112 -17
- 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/estate-count-budget.json +20 -1
- package/src/config/gate-coverage.yml +150 -0
- package/src/config/gate-violation-baselines.json +8 -3
- package/src/config/preamble-payload-budget.json +19 -11
- package/src/config/rule-stub-ceilings.json +14 -7
- package/src/scripts/_dispatch.bash +0 -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/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/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/harness_evolution_guards.ts +10 -0
- 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/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 +70 -1
- package/src/scripts/_lib/release_material.ts +125 -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/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_estate_count.ts +205 -24
- package/src/scripts/check_release_highlights.ts +96 -13
- package/src/scripts/check_roadmap_trackable.ts +17 -2
- package/src/scripts/corpus_manifest.ts +235 -0
- package/src/scripts/evolution_lab.ts +181 -8
- 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/turn_end_gate_hook.ts +9 -1
- 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 +643 -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 +143 -0
- package/src/scripts/release_env.ts +35 -0
- package/src/scripts/release_publication.ts +84 -0
|
@@ -0,0 +1,495 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* Read-only probe: is the declared council-artefact TTL actually honoured?
|
|
4
|
+
*
|
|
5
|
+
* `ai_council.session_retention_days` (default 7 — `ai_council/session.ts:81`)
|
|
6
|
+
* declares a retention window for the three council artefact directories:
|
|
7
|
+
*
|
|
8
|
+
* agents/runtime/council/questions/
|
|
9
|
+
* agents/runtime/council/responses/
|
|
10
|
+
* agents/runtime/council/sessions/
|
|
11
|
+
*
|
|
12
|
+
* Every pruner in the tree that could enforce that window is reached only by an
|
|
13
|
+
* explicit human command:
|
|
14
|
+
*
|
|
15
|
+
* - `prune_all_council_artifacts` (`ai_council/session.ts:468`) has exactly
|
|
16
|
+
* one caller in `src/`, the manual CLI `council_prune.ts:131`, which is
|
|
17
|
+
* bound to `task council-prune` and to no hook, workflow or scheduler.
|
|
18
|
+
* - `save()` (`ai_council/session.ts:506`), the only function that calls
|
|
19
|
+
* `prune_old_artifacts(QUESTIONS_DIR, …)` / `(RESPONSES_DIR, …)`
|
|
20
|
+
* (`:603-604`), has no caller in `src/` at all — `council_cli.ts` writes its
|
|
21
|
+
* own `--output` payload directly (`council_cli.ts:2807`) and never imports
|
|
22
|
+
* the session module.
|
|
23
|
+
* - `janitor.ts` declares `agents/runtime/council/responses` with
|
|
24
|
+
* `ttlDays: 7` (`janitor.ts:55-61`) but defaults to a dry-run report;
|
|
25
|
+
* deletion needs `--apply` (`janitor.ts:12-14`, `:329`), and it declares no
|
|
26
|
+
* entry for `questions/` or `sessions/` at all.
|
|
27
|
+
*
|
|
28
|
+
* So the TTL is a declaration, not a mechanism, and the retained corpus is
|
|
29
|
+
* "every artefact nobody deleted by hand". This probe measures that gap instead
|
|
30
|
+
* of asserting it: it reports how much of the corpus is past its declared TTL,
|
|
31
|
+
* and how much provider-attributed response text that over-TTL slice holds.
|
|
32
|
+
*
|
|
33
|
+
* THIS SCRIPT NEVER WRITES, UNLINKS, MOVES, TRUNCATES, OR MUTATES ANYTHING.
|
|
34
|
+
* It opens files for reading only. There is no `--apply`, no `--fix`, and no
|
|
35
|
+
* code path that calls a mutating `fs` function. The corpus is the measurement
|
|
36
|
+
* subject; destroying it would destroy the instrument.
|
|
37
|
+
*
|
|
38
|
+
* Usage:
|
|
39
|
+
* ./scripts-run src/scripts/probe_council_retention
|
|
40
|
+
* ./scripts-run src/scripts/probe_council_retention --root /path/to/project
|
|
41
|
+
* ./scripts-run src/scripts/probe_council_retention --retention-days 7 --json
|
|
42
|
+
*
|
|
43
|
+
* Exit code: 0 always. This is a report, never a gate — an over-TTL corpus is
|
|
44
|
+
* the finding, not a build failure.
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
import * as fs from 'node:fs';
|
|
48
|
+
import * as path from 'node:path';
|
|
49
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
50
|
+
|
|
51
|
+
import { resolve_project_root } from './_lib/agent_settings.js';
|
|
52
|
+
|
|
53
|
+
const _HERE = fileURLToPath(import.meta.url);
|
|
54
|
+
|
|
55
|
+
const PROG = 'probe_council_retention';
|
|
56
|
+
|
|
57
|
+
/** The three canonical council dirs, relative to the project root. */
|
|
58
|
+
const COUNCIL_DIRS: readonly string[] = ['responses', 'sessions', 'questions'];
|
|
59
|
+
|
|
60
|
+
/** Mirrors `DEFAULT_RETENTION_DAYS` (`ai_council/session.ts:81`). */
|
|
61
|
+
const DEFAULT_RETENTION_DAYS = 7;
|
|
62
|
+
|
|
63
|
+
const MS_PER_DAY = 86_400_000;
|
|
64
|
+
|
|
65
|
+
const _USAGE = `usage: ${PROG} [-h] [--root PATH] [--retention-days N] [--json]`;
|
|
66
|
+
|
|
67
|
+
const _HELP =
|
|
68
|
+
`${_USAGE}\n\n` +
|
|
69
|
+
'Read-only report on whether the declared council-artefact TTL is honoured.\n' +
|
|
70
|
+
'Never deletes or modifies anything.\n\n' +
|
|
71
|
+
'options:\n' +
|
|
72
|
+
' -h, --help show this help message and exit\n' +
|
|
73
|
+
' --root PATH project root to probe (default: resolved like sibling scripts)\n' +
|
|
74
|
+
` --retention-days N TTL in days (default: ${String(DEFAULT_RETENTION_DAYS)})\n` +
|
|
75
|
+
' --json emit the report as JSON instead of text\n';
|
|
76
|
+
|
|
77
|
+
interface Args {
|
|
78
|
+
root: string | null;
|
|
79
|
+
retention_days: number;
|
|
80
|
+
json: boolean;
|
|
81
|
+
help: boolean;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** One TTL-split pair of counts. */
|
|
85
|
+
interface Split {
|
|
86
|
+
within_ttl: number;
|
|
87
|
+
over_ttl: number;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
interface DirReport {
|
|
91
|
+
/** Directory label (`responses` / `sessions` / `questions`). */
|
|
92
|
+
dir: string;
|
|
93
|
+
/** Absolute path probed. */
|
|
94
|
+
abs_path: string;
|
|
95
|
+
/** Whether the directory exists at all. */
|
|
96
|
+
exists: boolean;
|
|
97
|
+
/** Files found, recursively. */
|
|
98
|
+
files: number;
|
|
99
|
+
/** Files split by mtime against the TTL cutoff. */
|
|
100
|
+
files_split: Split;
|
|
101
|
+
/** JSON files carrying a `responses[]` array. */
|
|
102
|
+
json_with_responses: number;
|
|
103
|
+
/** Files that look like JSON but would not parse. */
|
|
104
|
+
unparseable: number;
|
|
105
|
+
/** Provider-attributed response bodies (provider + text, no error). */
|
|
106
|
+
attributed_bodies: number;
|
|
107
|
+
/** Attributed bodies split by their SOURCE FILE's mtime against the TTL. */
|
|
108
|
+
attributed_split: Split;
|
|
109
|
+
/** Attributed bodies per provider. */
|
|
110
|
+
by_provider: Record<string, number>;
|
|
111
|
+
/** Attributed bodies per provider, over-TTL only. */
|
|
112
|
+
by_provider_over_ttl: Record<string, number>;
|
|
113
|
+
/** Age in days of the oldest file found, or `null` when the dir is empty. */
|
|
114
|
+
oldest_age_days: number | null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
interface Report {
|
|
118
|
+
root: string;
|
|
119
|
+
root_origin: string;
|
|
120
|
+
retention_days: number;
|
|
121
|
+
generated_at_utc: string;
|
|
122
|
+
dirs: DirReport[];
|
|
123
|
+
totals: {
|
|
124
|
+
files: number;
|
|
125
|
+
files_over_ttl: number;
|
|
126
|
+
attributed_bodies: number;
|
|
127
|
+
attributed_bodies_over_ttl: number;
|
|
128
|
+
unparseable: number;
|
|
129
|
+
oldest_age_days: number | null;
|
|
130
|
+
};
|
|
131
|
+
/** `true` when nothing at all is past the declared window. */
|
|
132
|
+
ttl_honoured: boolean;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function _argError(message: string): never {
|
|
136
|
+
process.stderr.write(`${_USAGE}\n`);
|
|
137
|
+
process.stderr.write(`${PROG}: error: ${message}\n`);
|
|
138
|
+
// A malformed invocation is still not a gate failure for the caller's
|
|
139
|
+
// build, but it must not print a report that means nothing.
|
|
140
|
+
process.exitCode = 2;
|
|
141
|
+
throw new _ExitSignal(2);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
class _ExitSignal extends Error {
|
|
145
|
+
constructor(readonly code: number) {
|
|
146
|
+
super(`exit ${code}`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function _parseArgs(argv: readonly string[]): Args {
|
|
151
|
+
const args: Args = {
|
|
152
|
+
root: null,
|
|
153
|
+
retention_days: DEFAULT_RETENTION_DAYS,
|
|
154
|
+
json: false,
|
|
155
|
+
help: false,
|
|
156
|
+
};
|
|
157
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
158
|
+
const a = argv[i] as string;
|
|
159
|
+
if (a === '-h' || a === '--help') {
|
|
160
|
+
args.help = true;
|
|
161
|
+
} else if (a === '--json') {
|
|
162
|
+
args.json = true;
|
|
163
|
+
} else if (a === '--root') {
|
|
164
|
+
const v = argv[i + 1];
|
|
165
|
+
if (v === undefined || v.startsWith('-')) {
|
|
166
|
+
_argError('argument --root: expected one argument');
|
|
167
|
+
}
|
|
168
|
+
args.root = v;
|
|
169
|
+
i += 1;
|
|
170
|
+
} else if (a === '--retention-days') {
|
|
171
|
+
const v = argv[i + 1];
|
|
172
|
+
if (v === undefined || v.startsWith('-')) {
|
|
173
|
+
_argError('argument --retention-days: expected one argument');
|
|
174
|
+
}
|
|
175
|
+
const parsed = Number.parseInt(v, 10);
|
|
176
|
+
if (!Number.isFinite(parsed) || String(parsed) !== v.trim() || parsed < 0) {
|
|
177
|
+
_argError(`argument --retention-days: invalid non-negative int value: '${v}'`);
|
|
178
|
+
}
|
|
179
|
+
args.retention_days = parsed;
|
|
180
|
+
i += 1;
|
|
181
|
+
} else {
|
|
182
|
+
_argError(`unrecognized arguments: ${a}`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return args;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function _isDir(p: string): boolean {
|
|
189
|
+
try {
|
|
190
|
+
return fs.statSync(p).isDirectory();
|
|
191
|
+
} catch {
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Every FILE under `dir`, recursively, sorted for a stable report.
|
|
198
|
+
*
|
|
199
|
+
* Recursive rather than a flat `readdirSync`: `responses/` holds per-debate
|
|
200
|
+
* subdirectories (`<slug>.json/debate-round-N.json`), and a flat listing counts
|
|
201
|
+
* one entry where the corpus holds several artefacts.
|
|
202
|
+
*/
|
|
203
|
+
function _walkFiles(dir: string): string[] {
|
|
204
|
+
const out: string[] = [];
|
|
205
|
+
if (!_isDir(dir)) {
|
|
206
|
+
return out;
|
|
207
|
+
}
|
|
208
|
+
let entries: string[];
|
|
209
|
+
try {
|
|
210
|
+
entries = fs.readdirSync(dir).sort();
|
|
211
|
+
} catch {
|
|
212
|
+
return out;
|
|
213
|
+
}
|
|
214
|
+
for (const name of entries) {
|
|
215
|
+
const full = path.join(dir, name);
|
|
216
|
+
let st: fs.Stats;
|
|
217
|
+
try {
|
|
218
|
+
st = fs.statSync(full);
|
|
219
|
+
} catch {
|
|
220
|
+
continue;
|
|
221
|
+
}
|
|
222
|
+
if (st.isFile()) {
|
|
223
|
+
out.push(full);
|
|
224
|
+
} else if (st.isDirectory()) {
|
|
225
|
+
out.push(..._walkFiles(full));
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return out;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function _mtimeMs(p: string): number | null {
|
|
232
|
+
try {
|
|
233
|
+
return fs.statSync(p).mtimeMs;
|
|
234
|
+
} catch {
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function _bump(map: Record<string, number>, key: string): void {
|
|
240
|
+
map[key] = (map[key] ?? 0) + 1;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* A response body counts as provider-attributed when it names a provider,
|
|
245
|
+
* carries text, and records no error — the shape `recouncil_guard` and the
|
|
246
|
+
* leakage assembler both treat as real captured provider output.
|
|
247
|
+
*/
|
|
248
|
+
function _isAttributed(entry: unknown): { attributed: boolean; provider: string } {
|
|
249
|
+
if (typeof entry !== 'object' || entry === null || Array.isArray(entry)) {
|
|
250
|
+
return { attributed: false, provider: '' };
|
|
251
|
+
}
|
|
252
|
+
const rec = entry as Record<string, unknown>;
|
|
253
|
+
const provider = typeof rec['provider'] === 'string' ? rec['provider'].trim() : '';
|
|
254
|
+
const text = typeof rec['text'] === 'string' ? rec['text'] : '';
|
|
255
|
+
const error = rec['error'];
|
|
256
|
+
const hasError = error !== null && error !== undefined && error !== '' && error !== false;
|
|
257
|
+
if (provider === '' || text.trim() === '' || hasError) {
|
|
258
|
+
return { attributed: false, provider };
|
|
259
|
+
}
|
|
260
|
+
return { attributed: true, provider };
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function probeDir(root: string, label: string, cutoffMs: number, nowMs: number): DirReport {
|
|
264
|
+
const abs = path.join(root, 'agents', 'runtime', 'council', label);
|
|
265
|
+
const report: DirReport = {
|
|
266
|
+
dir: label,
|
|
267
|
+
abs_path: abs,
|
|
268
|
+
exists: _isDir(abs),
|
|
269
|
+
files: 0,
|
|
270
|
+
files_split: { within_ttl: 0, over_ttl: 0 },
|
|
271
|
+
json_with_responses: 0,
|
|
272
|
+
unparseable: 0,
|
|
273
|
+
attributed_bodies: 0,
|
|
274
|
+
attributed_split: { within_ttl: 0, over_ttl: 0 },
|
|
275
|
+
by_provider: {},
|
|
276
|
+
by_provider_over_ttl: {},
|
|
277
|
+
oldest_age_days: null,
|
|
278
|
+
};
|
|
279
|
+
if (!report.exists) {
|
|
280
|
+
return report;
|
|
281
|
+
}
|
|
282
|
+
const files = _walkFiles(abs);
|
|
283
|
+
report.files = files.length;
|
|
284
|
+
let oldestMs: number | null = null;
|
|
285
|
+
for (const f of files) {
|
|
286
|
+
const mtime = _mtimeMs(f);
|
|
287
|
+
const overTtl = mtime !== null && mtime < cutoffMs;
|
|
288
|
+
if (overTtl) {
|
|
289
|
+
report.files_split.over_ttl += 1;
|
|
290
|
+
} else {
|
|
291
|
+
report.files_split.within_ttl += 1;
|
|
292
|
+
}
|
|
293
|
+
if (mtime !== null && (oldestMs === null || mtime < oldestMs)) {
|
|
294
|
+
oldestMs = mtime;
|
|
295
|
+
}
|
|
296
|
+
// Only JSON-shaped artefacts can carry a `responses[]` array. A `.md`
|
|
297
|
+
// render is a sibling projection, counted as a file and nothing more.
|
|
298
|
+
if (!f.endsWith('.json')) {
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
let payload: unknown;
|
|
302
|
+
try {
|
|
303
|
+
payload = JSON.parse(fs.readFileSync(f, 'utf-8'));
|
|
304
|
+
} catch {
|
|
305
|
+
report.unparseable += 1;
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
if (typeof payload !== 'object' || payload === null || Array.isArray(payload)) {
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
const responses = (payload as Record<string, unknown>)['responses'];
|
|
312
|
+
if (!Array.isArray(responses)) {
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
report.json_with_responses += 1;
|
|
316
|
+
for (const entry of responses) {
|
|
317
|
+
const { attributed, provider } = _isAttributed(entry);
|
|
318
|
+
if (!attributed) {
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
report.attributed_bodies += 1;
|
|
322
|
+
_bump(report.by_provider, provider);
|
|
323
|
+
if (overTtl) {
|
|
324
|
+
report.attributed_split.over_ttl += 1;
|
|
325
|
+
_bump(report.by_provider_over_ttl, provider);
|
|
326
|
+
} else {
|
|
327
|
+
report.attributed_split.within_ttl += 1;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
if (oldestMs !== null) {
|
|
332
|
+
report.oldest_age_days = Math.floor((nowMs - oldestMs) / MS_PER_DAY);
|
|
333
|
+
}
|
|
334
|
+
return report;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
export function buildReport(opts: {
|
|
338
|
+
root: string;
|
|
339
|
+
root_origin: string;
|
|
340
|
+
retention_days: number;
|
|
341
|
+
now?: Date;
|
|
342
|
+
}): Report {
|
|
343
|
+
const now = opts.now ?? new Date();
|
|
344
|
+
const nowMs = now.getTime();
|
|
345
|
+
const cutoffMs = nowMs - opts.retention_days * MS_PER_DAY;
|
|
346
|
+
const dirs = COUNCIL_DIRS.map((label) => probeDir(opts.root, label, cutoffMs, nowMs));
|
|
347
|
+
const oldest = dirs
|
|
348
|
+
.map((d) => d.oldest_age_days)
|
|
349
|
+
.filter((v): v is number => v !== null)
|
|
350
|
+
.reduce<number | null>((acc, v) => (acc === null || v > acc ? v : acc), null);
|
|
351
|
+
const totals = {
|
|
352
|
+
files: dirs.reduce((s, d) => s + d.files, 0),
|
|
353
|
+
files_over_ttl: dirs.reduce((s, d) => s + d.files_split.over_ttl, 0),
|
|
354
|
+
attributed_bodies: dirs.reduce((s, d) => s + d.attributed_bodies, 0),
|
|
355
|
+
attributed_bodies_over_ttl: dirs.reduce((s, d) => s + d.attributed_split.over_ttl, 0),
|
|
356
|
+
unparseable: dirs.reduce((s, d) => s + d.unparseable, 0),
|
|
357
|
+
oldest_age_days: oldest,
|
|
358
|
+
};
|
|
359
|
+
return {
|
|
360
|
+
root: opts.root,
|
|
361
|
+
root_origin: opts.root_origin,
|
|
362
|
+
retention_days: opts.retention_days,
|
|
363
|
+
generated_at_utc: now.toISOString(),
|
|
364
|
+
dirs,
|
|
365
|
+
totals,
|
|
366
|
+
// A zero over-TTL count is the ONLY reading that shows the window being
|
|
367
|
+
// enforced; anything else means the declaration is not a mechanism.
|
|
368
|
+
ttl_honoured: totals.files_over_ttl === 0,
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function _providerLines(map: Record<string, number>, indent: string): string {
|
|
373
|
+
const keys = Object.keys(map).sort((a, b) => {
|
|
374
|
+
const d = (map[b] as number) - (map[a] as number);
|
|
375
|
+
return d !== 0 ? d : a.localeCompare(b);
|
|
376
|
+
});
|
|
377
|
+
if (keys.length === 0) {
|
|
378
|
+
return `${indent}(none)\n`;
|
|
379
|
+
}
|
|
380
|
+
return keys.map((k) => `${indent}${k.padEnd(14)} ${String(map[k])}\n`).join('');
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
export function renderReport(r: Report): string {
|
|
384
|
+
const lines: string[] = [];
|
|
385
|
+
lines.push(`council retention probe — read-only, nothing was deleted`);
|
|
386
|
+
lines.push(`root ${r.root} (origin: ${r.root_origin})`);
|
|
387
|
+
lines.push(`retention_days ${String(r.retention_days)}`);
|
|
388
|
+
lines.push(`generated ${r.generated_at_utc}`);
|
|
389
|
+
lines.push('');
|
|
390
|
+
for (const d of r.dirs) {
|
|
391
|
+
lines.push(`── ${d.dir} ──`);
|
|
392
|
+
if (!d.exists) {
|
|
393
|
+
lines.push(` directory absent: ${d.abs_path}`);
|
|
394
|
+
lines.push('');
|
|
395
|
+
continue;
|
|
396
|
+
}
|
|
397
|
+
lines.push(` files ${String(d.files)}`);
|
|
398
|
+
lines.push(
|
|
399
|
+
` within TTL / over TTL ${String(d.files_split.within_ttl)} / ${String(d.files_split.over_ttl)}`,
|
|
400
|
+
);
|
|
401
|
+
lines.push(` JSON records with responses[] ${String(d.json_with_responses)}`);
|
|
402
|
+
lines.push(` unparseable JSON ${String(d.unparseable)}`);
|
|
403
|
+
lines.push(` provider-attributed bodies ${String(d.attributed_bodies)}`);
|
|
404
|
+
lines.push(
|
|
405
|
+
` within TTL / over TTL ${String(d.attributed_split.within_ttl)} / ${String(d.attributed_split.over_ttl)}`,
|
|
406
|
+
);
|
|
407
|
+
lines.push(' by provider (all):');
|
|
408
|
+
lines.push(_providerLines(d.by_provider, ' ').trimEnd());
|
|
409
|
+
lines.push(' by provider (over TTL only):');
|
|
410
|
+
lines.push(_providerLines(d.by_provider_over_ttl, ' ').trimEnd());
|
|
411
|
+
lines.push(
|
|
412
|
+
` oldest file age (days) ${d.oldest_age_days === null ? 'n/a' : String(d.oldest_age_days)}`,
|
|
413
|
+
);
|
|
414
|
+
lines.push('');
|
|
415
|
+
}
|
|
416
|
+
lines.push('── totals ──');
|
|
417
|
+
lines.push(` files ${String(r.totals.files)}`);
|
|
418
|
+
lines.push(` files over TTL ${String(r.totals.files_over_ttl)}`);
|
|
419
|
+
lines.push(` attributed bodies ${String(r.totals.attributed_bodies)}`);
|
|
420
|
+
lines.push(` attributed bodies over TTL ${String(r.totals.attributed_bodies_over_ttl)}`);
|
|
421
|
+
lines.push(` unparseable JSON ${String(r.totals.unparseable)}`);
|
|
422
|
+
lines.push(
|
|
423
|
+
` oldest artefact age (days) ${r.totals.oldest_age_days === null ? 'n/a' : String(r.totals.oldest_age_days)}`,
|
|
424
|
+
);
|
|
425
|
+
lines.push('');
|
|
426
|
+
lines.push(
|
|
427
|
+
r.ttl_honoured
|
|
428
|
+
? `TTL HONOURED — 0 artefacts are older than ${String(r.retention_days)} days.`
|
|
429
|
+
: `TTL NOT HONOURED — ${String(r.totals.files_over_ttl)} of ${String(r.totals.files)} ` +
|
|
430
|
+
`artefacts are older than the declared ${String(r.retention_days)}-day window ` +
|
|
431
|
+
`(oldest ${r.totals.oldest_age_days === null ? 'n/a' : String(r.totals.oldest_age_days)} days). ` +
|
|
432
|
+
`Every pruner in the tree needs an explicit human command; nothing sweeps on its own.`,
|
|
433
|
+
);
|
|
434
|
+
return lines.join('\n') + '\n';
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
export function main(argv: readonly string[]): number {
|
|
438
|
+
let args: Args;
|
|
439
|
+
try {
|
|
440
|
+
args = _parseArgs(argv);
|
|
441
|
+
} catch (exc) {
|
|
442
|
+
if (exc instanceof _ExitSignal) {
|
|
443
|
+
// Arg errors already wrote usage + set exitCode; the report itself
|
|
444
|
+
// never fails, so the caller still sees a 0-shaped contract for the
|
|
445
|
+
// reporting path and a 2 only for a malformed invocation.
|
|
446
|
+
return exc.code;
|
|
447
|
+
}
|
|
448
|
+
throw exc;
|
|
449
|
+
}
|
|
450
|
+
if (args.help) {
|
|
451
|
+
process.stdout.write(_HELP);
|
|
452
|
+
return 0;
|
|
453
|
+
}
|
|
454
|
+
const [root, origin] = resolve_project_root(args.root);
|
|
455
|
+
const report = buildReport({
|
|
456
|
+
root,
|
|
457
|
+
root_origin: origin,
|
|
458
|
+
retention_days: args.retention_days,
|
|
459
|
+
});
|
|
460
|
+
if (args.json) {
|
|
461
|
+
process.stdout.write(JSON.stringify(report, null, 2) + '\n');
|
|
462
|
+
} else {
|
|
463
|
+
process.stdout.write(renderReport(report));
|
|
464
|
+
}
|
|
465
|
+
return 0;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
function _isCliEntry(): boolean {
|
|
469
|
+
if (process.argv[1] === undefined) {
|
|
470
|
+
return false;
|
|
471
|
+
}
|
|
472
|
+
const argvUrl = pathToFileURL(path.resolve(process.argv[1])).href;
|
|
473
|
+
if (import.meta.url === argvUrl) {
|
|
474
|
+
return true;
|
|
475
|
+
}
|
|
476
|
+
// A symlinked invocation (e.g. via an installed `.augment/` projection,
|
|
477
|
+
// or macOS /var → /private/var temp dirs) makes the raw URLs differ:
|
|
478
|
+
// import.meta.url is the resolved real path while argv[1] keeps the
|
|
479
|
+
// symlink path. Compare realpaths so the entry guard still fires
|
|
480
|
+
// (without this the CLI silently no-ops when run through a symlink).
|
|
481
|
+
try {
|
|
482
|
+
const here = fs.realpathSync(fileURLToPath(import.meta.url));
|
|
483
|
+
const argv = fs.realpathSync(path.resolve(process.argv[1]));
|
|
484
|
+
return here === argv;
|
|
485
|
+
} catch {
|
|
486
|
+
return false;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
if (_isCliEntry() || process.argv[1] === _HERE) {
|
|
491
|
+
process.exitCode = main(process.argv.slice(2));
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
export { COUNCIL_DIRS, DEFAULT_RETENTION_DAYS, PROG };
|
|
495
|
+
export type { Report, DirReport, Split };
|
package/src/scripts/release.ts
CHANGED
|
@@ -126,10 +126,10 @@ import {
|
|
|
126
126
|
} from './_lib/release_highlights.js';
|
|
127
127
|
import {
|
|
128
128
|
NEXT_SECTION_RE,
|
|
129
|
+
RELEASE_HEAD_DEFAULT,
|
|
129
130
|
extract_changelog_section,
|
|
130
131
|
pr_body_from_section,
|
|
131
132
|
release_notes_from_section,
|
|
132
|
-
tag_message_from_section,
|
|
133
133
|
} from './_lib/release_material.js';
|
|
134
134
|
|
|
135
135
|
// `__doc__.splitlines()[0]` in `_parse_args` — the argparse description. Kept
|
|
@@ -144,6 +144,8 @@ import {
|
|
|
144
144
|
AUGMENT_MARKETPLACE_JSON,
|
|
145
145
|
AUGMENT_PLUGIN_JSON,
|
|
146
146
|
CHANGELOG,
|
|
147
|
+
_set_changelog_reader,
|
|
148
|
+
read_changelog_text,
|
|
147
149
|
CalledProcessError,
|
|
148
150
|
GH_PR_BODY_LIMIT,
|
|
149
151
|
GH_RELEASE_NOTES_LIMIT,
|
|
@@ -188,6 +190,8 @@ import {
|
|
|
188
190
|
_tag_exists_local,
|
|
189
191
|
_tag_exists_remote,
|
|
190
192
|
_target_from_branch,
|
|
193
|
+
create_and_push_annotated_tag,
|
|
194
|
+
guard_publication,
|
|
191
195
|
die,
|
|
192
196
|
gh,
|
|
193
197
|
git,
|
|
@@ -269,24 +273,7 @@ const RELEASE_HEAD_SECTIONS: ReadonlyArray<string> = HEAD_LABELS;
|
|
|
269
273
|
* (`_lib/changelog_eras.ts`, 250 lines per era), so an unbounded head would
|
|
270
274
|
* quietly consume it.
|
|
271
275
|
*/
|
|
272
|
-
export
|
|
273
|
-
|
|
274
|
-
/**
|
|
275
|
-
* The fallback value of a head line the span does not substantiate.
|
|
276
|
-
*
|
|
277
|
-
* `_none_` and not a placeholder token: where nothing was derived it is the
|
|
278
|
-
* **true** answer, and a release that genuinely changed no defaults should say
|
|
279
|
-
* so rather than carry an unfilled marker.
|
|
280
|
-
*
|
|
281
|
-
* It is a fallback and no longer a blanket default. Writing `_none_` into
|
|
282
|
-
* every field made the generator assert five things it had not checked, and
|
|
283
|
-
* `check_release_highlights` rejected exactly that assertion the moment the
|
|
284
|
-
* span contradicted it — which for this package is every release, because every
|
|
285
|
-
* release touches `src/rules/` or `src/scripts/schemas/`. Substantiated labels
|
|
286
|
-
* are now pre-filled from the span (`_derive_head_prefill`), so the tool states
|
|
287
|
-
* only what it can support and `_none_` means what it says.
|
|
288
|
-
*/
|
|
289
|
-
const HEAD_DEFAULT = '_none_';
|
|
276
|
+
export { RELEASE_HEAD_CAP_LINES } from './_lib/release_material.js';
|
|
290
277
|
|
|
291
278
|
/**
|
|
292
279
|
* Pre-fill values for the curated head from the release span.
|
|
@@ -313,20 +300,21 @@ function _derive_head_prefill(prev: string | null): Record<string, string> {
|
|
|
313
300
|
/**
|
|
314
301
|
* Render the curated head. Emitted by the generator on every release so it
|
|
315
302
|
* cannot be forgotten; edited by the maintainer before merge.
|
|
303
|
+
*
|
|
304
|
+
* **The authoring instruction is NOT emitted here** (2026-09-01, roadmap
|
|
305
|
+
* `road-to-publication-integrity-hard-fail` § Phase 2, Option A). This writer
|
|
306
|
+
* used to append `CURATED_HEAD_INSTRUCTION`, nothing removed it at release
|
|
307
|
+
* time, and it published twice in `package/CHANGELOG.md`. The reminder now
|
|
308
|
+
* rides in the release-PR body's PR-only region — never published — see
|
|
309
|
+
* `pr_body_from_section` in `_lib/release_material.ts`.
|
|
316
310
|
*/
|
|
317
311
|
export function render_release_head(
|
|
318
312
|
filled: Readonly<Record<string, string>> = {},
|
|
319
313
|
): string[] {
|
|
320
|
-
const lines: string[] = [
|
|
321
|
-
'### Release highlights',
|
|
322
|
-
'',
|
|
323
|
-
'<!-- Curated head: fill before merge, keep it under ' +
|
|
324
|
-
`${RELEASE_HEAD_CAP_LINES} lines, and leave \`${HEAD_DEFAULT}\` where it is ` +
|
|
325
|
-
'genuinely the answer. The generated log below is unchanged. -->',
|
|
326
|
-
];
|
|
314
|
+
const lines: string[] = ['### Release highlights', ''];
|
|
327
315
|
for (const label of RELEASE_HEAD_SECTIONS) {
|
|
328
316
|
const value = (filled[label] ?? '').trim();
|
|
329
|
-
lines.push(`- **${label}:** ${value === '' ?
|
|
317
|
+
lines.push(`- **${label}:** ${value === '' ? RELEASE_HEAD_DEFAULT : value}`);
|
|
330
318
|
}
|
|
331
319
|
return lines;
|
|
332
320
|
}
|
|
@@ -1179,6 +1167,7 @@ function _step(n: number, total: number, msg: string): void {
|
|
|
1179
1167
|
process.stdout.write(`[${n}/${total}] ${msg}\n`);
|
|
1180
1168
|
}
|
|
1181
1169
|
|
|
1170
|
+
|
|
1182
1171
|
function execute(
|
|
1183
1172
|
plan: Plan,
|
|
1184
1173
|
opts: { wait_for_checks: boolean; dry_run: boolean; resume?: boolean; ci?: boolean },
|
|
@@ -1342,7 +1331,7 @@ function execute(
|
|
|
1342
1331
|
// Derive the PR body from the CHANGELOG section step 2 just wrote —
|
|
1343
1332
|
// the file is the single source; the in-memory plan is only its
|
|
1344
1333
|
// author (release-truth Phase 1).
|
|
1345
|
-
const section = extract_changelog_section(
|
|
1334
|
+
const section = extract_changelog_section(read_changelog_text(), plan.target);
|
|
1346
1335
|
if (!section) {
|
|
1347
1336
|
die(`CHANGELOG.md carries no section for ${plan.target} — cannot derive PR body`);
|
|
1348
1337
|
}
|
|
@@ -1388,6 +1377,7 @@ function execute(
|
|
|
1388
1377
|
_step(8, total, `Tag ${plan.target} already on ${REMOTE} — skip`);
|
|
1389
1378
|
} else {
|
|
1390
1379
|
_step(8, total, `Tag ${plan.target} exists locally — push only`);
|
|
1380
|
+
guard_publication(plan.target, 'tag push (resumed)');
|
|
1391
1381
|
_push_tag(plan.target);
|
|
1392
1382
|
}
|
|
1393
1383
|
} else {
|
|
@@ -1398,15 +1388,7 @@ function execute(
|
|
|
1398
1388
|
// tag replaces the previous lightweight one so tag metadata is a
|
|
1399
1389
|
// fourth surface carrying the same single-source content.
|
|
1400
1390
|
_step(8, total, `Tag merge commit (annotated, from merged CHANGELOG) and push ${plan.target}`);
|
|
1401
|
-
|
|
1402
|
-
if (!merged) {
|
|
1403
|
-
die(
|
|
1404
|
-
`CHANGELOG.md on ${MAIN_BRANCH} carries no section for ${plan.target} — ` +
|
|
1405
|
-
'refusing to tag a release whose changelog entry is missing',
|
|
1406
|
-
);
|
|
1407
|
-
}
|
|
1408
|
-
run(['git', 'tag', '-a', plan.target, '-m', tag_message_from_section(merged!.body, plan.target)]);
|
|
1409
|
-
_push_tag(plan.target);
|
|
1391
|
+
create_and_push_annotated_tag(plan.target);
|
|
1410
1392
|
}
|
|
1411
1393
|
|
|
1412
1394
|
// ─── 9. GitHub Release ──────────────────────────────────────────────────
|
|
@@ -1436,6 +1418,7 @@ function execute(
|
|
|
1436
1418
|
GH_RELEASE_NOTES_LIMIT,
|
|
1437
1419
|
'`CHANGELOG.md`',
|
|
1438
1420
|
);
|
|
1421
|
+
guard_publication(plan.target, 'GitHub Release notes');
|
|
1439
1422
|
gh(['release', 'create', plan.target, '--title', plan.target, '--notes', notes]);
|
|
1440
1423
|
|
|
1441
1424
|
// ─── 9b. dispatch-chain the tag-triggered workflows (--ci only) ──────
|
|
@@ -2014,6 +1997,7 @@ export {
|
|
|
2014
1997
|
// Drill seam (release_drill.ts) — the step machinery under a simulated
|
|
2015
1998
|
// git/gh world, so orchestration bugs surface in vitest, not mid-release.
|
|
2016
1999
|
execute,
|
|
2000
|
+
_set_changelog_reader,
|
|
2017
2001
|
_set_exec_override,
|
|
2018
2002
|
push_release_branch,
|
|
2019
2003
|
merge_release_pr,
|