@event4u/agent-config 14.20.0 → 14.21.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 +243 -0
- package/README.md +1 -1
- package/agents/templates/.ai-council.yml.example +10 -0
- package/dist/agent-src/commands/analyze/inbox.md +1 -1
- package/dist/agent-src/commands/analyze/{reference-repo.md → repo.md} +247 -32
- package/dist/agent-src/commands/analyze/roadmap-repos.md +129 -0
- package/dist/agent-src/commands/analyze.md +12 -7
- package/dist/agent-src/commands/feature/plan.md +55 -16
- package/dist/agent-src/commands/feature/refactor.md +8 -3
- package/dist/agent-src/commands/humanize.md +48 -4
- package/dist/agent-src/contexts/execution/contract-decision-sheet.md +22 -1
- package/dist/agent-src/contexts/execution/host-capability-manifest.md +4 -1
- package/dist/agent-src/contexts/execution/roadmap-process-loop.md +35 -2
- package/dist/agent-src/contexts/execution/terminal-states.md +4 -1
- package/dist/agent-src/rules/evaluator-independence.md +4 -4
- package/dist/agent-src/rules/external-reference-deep-dive.md +2 -2
- package/dist/agent-src/skills/competitive-positioning/SKILL.md +2 -2
- package/dist/agent-src/skills/humanizer/references/anti-aiisms.md +27 -11
- package/dist/agent-src/skills/learning-to-rule-or-skill/SKILL.md +1 -1
- package/dist/agent-src/skills/markitdown/SKILL.md +1 -1
- package/dist/agent-src/skills/repomix-packer/SKILL.md +1 -1
- package/dist/agent-src/skills/technical-specification/SKILL.md +4 -0
- package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/dist/agent-src/templates/features.md +5 -0
- package/dist/agent-src/templates/roadmaps.md +15 -0
- package/dist/cli-delegate/{chunk-KPBWFOPZ.js → chunk-2FTMENJL.js} +194 -37
- package/dist/cli-delegate/{chunk-PI2GU3DP.js → chunk-2TAQ2K4V.js} +4 -1
- package/dist/cli-delegate/{chunk-C6IPZRDA.js → chunk-643Z7FIC.js} +1 -1
- package/dist/cli-delegate/{chunk-NUMQHLS5.js → chunk-J3RFNKG4.js} +21 -2
- package/dist/cli-delegate/cmd_conformance.js +17 -3
- package/dist/cli-delegate/cmd_doctor.js +3 -3
- package/dist/cli-delegate/cmd_export.js +2 -2
- package/dist/cli-delegate/cmd_handoff.js +1 -1
- package/dist/cli-delegate/cmd_prune.js +2 -2
- package/dist/cli-delegate/cmd_settings_migrate.js +2 -2
- package/dist/cli-delegate/cmd_sync.js +2 -2
- package/dist/cli-delegate/cmd_uninstall.js +2 -2
- package/dist/cli-delegate/cmd_validate.js +2 -2
- package/dist/discovery/deprecation-report.md +1 -1
- package/dist/discovery/discovery-manifest.json +56 -25
- package/dist/discovery/discovery-manifest.json.sha256 +1 -1
- package/dist/discovery/discovery-manifest.summary.md +3 -3
- package/dist/discovery/dormancy-report.md +1 -1
- package/dist/discovery/orphan-report.md +1 -1
- package/dist/discovery/packs.json +7 -6
- package/dist/discovery/trust-report.md +2 -2
- package/dist/discovery/workspaces.json +6 -5
- package/dist/hooks/dispatch.js +479 -241
- package/dist/install/install.mjs +4 -1
- package/dist/mcp/registry-manifest.json +2 -2
- package/dist/mcp/server.json +2 -2
- package/dist/mcp/server.mjs +313 -63
- package/dist/scripts/ai_council/content_ceiling.js +63 -0
- package/dist/scripts/ai_council/seat_policy.js +106 -0
- package/dist/scripts/ai_council/transport_resolver.js +21 -0
- package/llms.txt +14 -14
- package/package.json +1 -1
- package/src/config/gate-violation-baselines.json +3 -3
- package/src/config/release-gate-locality.yml +4 -3
- package/src/scripts/_lib/blocked_by_marker.ts +189 -0
- package/src/scripts/_lib/host_capability.ts +38 -1
- package/src/scripts/_lib/release_highlights.ts +19 -211
- package/src/scripts/_lib/release_material.ts +27 -77
- package/src/scripts/_lib/structured_ask.ts +117 -0
- package/src/scripts/_lib/tty_prompt.ts +76 -0
- package/src/scripts/ai_council/api_transport.ts +163 -0
- package/src/scripts/ai_council/clients.ts +58 -63
- package/src/scripts/ai_council/config.ts +13 -0
- package/src/scripts/ai_council/content_ceiling.ts +79 -0
- package/src/scripts/ai_council/jury_aggregate.ts +178 -0
- package/src/scripts/ai_council/seat_policy.ts +137 -0
- package/src/scripts/ai_council/status_surface.ts +79 -0
- package/src/scripts/ai_council/transport_resolver.ts +59 -1
- package/src/scripts/ai_tells_rules.ts +446 -5
- package/src/scripts/ask_block_census.ts +499 -0
- package/src/scripts/bench_humanizer_eval.ts +264 -40
- package/src/scripts/bench_prose_tells_fp.ts +198 -0
- package/src/scripts/check_branch_work_committed.ts +234 -0
- package/src/scripts/check_pack_size.ts +1 -1
- package/src/scripts/check_release_highlights.ts +0 -157
- package/src/scripts/council_cli.ts +18 -27
- package/src/scripts/detect_ai_tells.ts +142 -20
- package/src/scripts/harvest_reference_tokens.ts +698 -0
- package/src/scripts/hook_manifest.json +1 -1
- package/src/scripts/hook_manifest.yaml +31 -1
- package/src/scripts/hooks/concern_registry.ts +2 -0
- package/src/scripts/hooks/delegation_nudge_hook.ts +45 -1
- package/src/scripts/hooks/one_question_per_ask_hook.ts +191 -0
- package/src/scripts/install-hooks.sh +52 -0
- package/src/scripts/lint_roadmap_blockers.ts +51 -1
- package/src/scripts/migrate_command_suggestions.ts +1 -1
- package/src/scripts/probe_unblocked_ask.ts +135 -2
- package/src/scripts/release.ts +26 -71
- package/src/scripts/release_drill.ts +6 -10
- package/src/scripts/release_env.ts +1 -0
- package/src/scripts/release_publication.ts +34 -29
- package/src/scripts/self_review_gate.ts +409 -15
|
@@ -47,18 +47,40 @@
|
|
|
47
47
|
* Both edges are printed. A reader who cannot see the bracket cannot judge the
|
|
48
48
|
* number.
|
|
49
49
|
*
|
|
50
|
+
* The `form` dimension (road-to-asked-not-parked 1.1)
|
|
51
|
+
*
|
|
52
|
+
* Every unblocked ask is additionally partitioned by the FORM the ask took:
|
|
53
|
+
*
|
|
54
|
+
* - `native` — the same assistant turn carries a tool call whose name is a
|
|
55
|
+
* structured-ask tool (`_lib/structured_ask.ts`, `isStructuredAskTool`).
|
|
56
|
+
* - `text` — everything else, i.e. the ask was prose.
|
|
57
|
+
*
|
|
58
|
+
* The two are exhaustive and disjoint by construction, so `text + native`
|
|
59
|
+
* always equals `unblockedAsks`. That identity is asserted in `--self-test`
|
|
60
|
+
* rather than left to a reader to check.
|
|
61
|
+
*
|
|
62
|
+
* Expect `native` to be **0** on every corpus this repo can scan today: no host
|
|
63
|
+
* in the capability registry carries an observed structured-ask tool, and
|
|
64
|
+
* `structured_ask.ts` states why matching on a name-shape pattern is not a claim
|
|
65
|
+
* that any host ships one. A measured zero and an uninstrumented zero look the
|
|
66
|
+
* same in a report and are not the same fact — this axis is what makes them
|
|
67
|
+
* distinguishable later.
|
|
68
|
+
*
|
|
50
69
|
* Usage:
|
|
51
70
|
* ./scripts-run src/scripts/probe_unblocked_ask [--limit N] [--store PATH] [--json]
|
|
52
71
|
* ./scripts-run src/scripts/probe_unblocked_ask --self-test
|
|
72
|
+
* ./scripts-run src/scripts/probe_unblocked_ask --help
|
|
53
73
|
*
|
|
54
74
|
* Exit codes: 0 always — a measurement, not a gate. `--self-test` exits 1 when
|
|
55
75
|
* the detector fails to fire on its fixtures.
|
|
56
76
|
*/
|
|
57
77
|
import fs from 'node:fs';
|
|
78
|
+
import os from 'node:os';
|
|
58
79
|
import path from 'node:path';
|
|
59
80
|
import { pathToFileURL } from 'node:url';
|
|
60
81
|
|
|
61
82
|
import { defaultStore, HARNESS_TEXT, isInjectedBody } from './conformance_scan.js';
|
|
83
|
+
import { isStructuredAskTool } from './_lib/structured_ask.js';
|
|
62
84
|
import { isSyntheticPrompt } from './_lib/prompt_shape.js';
|
|
63
85
|
|
|
64
86
|
/** A numbered-options block — the surface `check_reply_consistency` already reads. */
|
|
@@ -88,6 +110,8 @@ interface Turn {
|
|
|
88
110
|
role: 'user' | 'assistant';
|
|
89
111
|
text: string;
|
|
90
112
|
at: string;
|
|
113
|
+
/** Tool names invoked in this turn — empty for a user turn. */
|
|
114
|
+
tools: string[];
|
|
91
115
|
}
|
|
92
116
|
|
|
93
117
|
function turns(lines: string[]): Turn[] {
|
|
@@ -116,10 +140,16 @@ function turns(lines: string[]): Turn[] {
|
|
|
116
140
|
.map((b: { text?: string }) => b.text ?? '')
|
|
117
141
|
.join('\n')
|
|
118
142
|
: '';
|
|
143
|
+
const tools = Array.isArray(c)
|
|
144
|
+
? c
|
|
145
|
+
.filter((b: { type?: string }) => b?.type === 'tool_use')
|
|
146
|
+
.map((b: { name?: string }) => String(b?.name ?? ''))
|
|
147
|
+
.filter((n: string) => n !== '')
|
|
148
|
+
: [];
|
|
119
149
|
if (!text.trim()) continue;
|
|
120
150
|
if (role === 'assistant' && HARNESS_TEXT.test(text.trim())) continue;
|
|
121
151
|
if (role === 'user' && (isSyntheticPrompt(text) || isInjectedBody(text))) continue;
|
|
122
|
-
out.push({ role, text, at: String(e['timestamp'] ?? '') });
|
|
152
|
+
out.push({ role, text, at: String(e['timestamp'] ?? ''), tools });
|
|
123
153
|
}
|
|
124
154
|
return out;
|
|
125
155
|
}
|
|
@@ -139,16 +169,39 @@ export function classifyTail(tail: string): AskVerdict {
|
|
|
139
169
|
return { unblockedAsk: true, malformed: !RECOMMENDATION.test(tail) };
|
|
140
170
|
}
|
|
141
171
|
|
|
172
|
+
/** The ask FORM partition. Exhaustive and disjoint: `text + native === unblockedAsks`. */
|
|
173
|
+
export interface FormBreakdown {
|
|
174
|
+
text: number;
|
|
175
|
+
native: number;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Which FORM an ask took. `native` iff the turn carried a structured-ask tool
|
|
180
|
+
* call; `text` otherwise. Exported so the partition is one function rather than
|
|
181
|
+
* an inline condition a later edit can quietly make non-exhaustive.
|
|
182
|
+
*/
|
|
183
|
+
export function askForm(tools: readonly string[]): 'text' | 'native' {
|
|
184
|
+
return tools.some((n) => isStructuredAskTool(n)) ? 'native' : 'text';
|
|
185
|
+
}
|
|
186
|
+
|
|
142
187
|
export interface Result {
|
|
143
188
|
handbacks: number;
|
|
144
189
|
withBlock: number;
|
|
145
190
|
unblockedAsks: number;
|
|
146
191
|
malformed: number;
|
|
192
|
+
form: FormBreakdown;
|
|
147
193
|
samples: { session: string; at: string; span: string }[];
|
|
148
194
|
}
|
|
149
195
|
|
|
150
196
|
export function measure(store: string, limit: number): Result {
|
|
151
|
-
const r: Result = {
|
|
197
|
+
const r: Result = {
|
|
198
|
+
handbacks: 0,
|
|
199
|
+
withBlock: 0,
|
|
200
|
+
unblockedAsks: 0,
|
|
201
|
+
malformed: 0,
|
|
202
|
+
form: { text: 0, native: 0 },
|
|
203
|
+
samples: [],
|
|
204
|
+
};
|
|
152
205
|
let files: string[];
|
|
153
206
|
try {
|
|
154
207
|
files = fs.readdirSync(store).filter((f) => f.endsWith('.jsonl'));
|
|
@@ -179,6 +232,7 @@ export function measure(store: string, limit: number): Result {
|
|
|
179
232
|
const v = classifyTail(tail);
|
|
180
233
|
if (!v.unblockedAsk) continue;
|
|
181
234
|
r.unblockedAsks += 1;
|
|
235
|
+
r.form[askForm(t.tools)] += 1;
|
|
182
236
|
if (v.malformed) {
|
|
183
237
|
r.malformed += 1;
|
|
184
238
|
if (r.samples.length < 5) {
|
|
@@ -221,6 +275,52 @@ export function selfTest(): number {
|
|
|
221
275
|
);
|
|
222
276
|
if (!ok) failed += 1;
|
|
223
277
|
}
|
|
278
|
+
for (const [tools, want] of [
|
|
279
|
+
[[], 'text'],
|
|
280
|
+
[['Bash', 'Read'], 'text'],
|
|
281
|
+
[['AskUserQuestion'], 'native'],
|
|
282
|
+
[['ask_user_question'], 'native'],
|
|
283
|
+
] as Array<[string[], 'text' | 'native']>) {
|
|
284
|
+
const got = askForm(tools);
|
|
285
|
+
const ok = got === want;
|
|
286
|
+
process.stdout.write(
|
|
287
|
+
`${ok ? '✅' : '❌'} form of [${tools.join(', ')}] — ${got}` +
|
|
288
|
+
(ok ? '\n' : ` (wanted ${want})\n`),
|
|
289
|
+
);
|
|
290
|
+
if (!ok) failed += 1;
|
|
291
|
+
}
|
|
292
|
+
const fx = fs.mkdtempSync(path.join(os.tmpdir(), 'probe-ask-form-'));
|
|
293
|
+
try {
|
|
294
|
+
const mk = (role: string, blocks: unknown): string =>
|
|
295
|
+
JSON.stringify({ type: role, timestamp: '2026-01-01T00:00:00Z', message: { role, content: blocks } });
|
|
296
|
+
fs.writeFileSync(
|
|
297
|
+
path.join(fx, 'fixture.jsonl'),
|
|
298
|
+
[
|
|
299
|
+
mk('assistant', [{ type: 'text', text: 'Done.\n\nSoll ich das so umsetzen?' }]),
|
|
300
|
+
mk('user', 'ja'),
|
|
301
|
+
mk('assistant', [
|
|
302
|
+
{ type: 'text', text: 'Done.\n\nShall I take the second half too?' },
|
|
303
|
+
{ type: 'tool_use', name: 'AskUserQuestion' },
|
|
304
|
+
]),
|
|
305
|
+
mk('user', 'ja'),
|
|
306
|
+
].join('\n'),
|
|
307
|
+
'utf8',
|
|
308
|
+
);
|
|
309
|
+
const r = measure(fx, 10);
|
|
310
|
+
const sums = r.form.text + r.form.native === r.unblockedAsks;
|
|
311
|
+
process.stdout.write(
|
|
312
|
+
`${sums ? '✅' : '❌'} form partition is exhaustive — text ${String(r.form.text)} + ` +
|
|
313
|
+
`native ${String(r.form.native)} = unblocked ${String(r.unblockedAsks)}\n`,
|
|
314
|
+
);
|
|
315
|
+
if (!sums) failed += 1;
|
|
316
|
+
const split = r.form.text === 1 && r.form.native === 1;
|
|
317
|
+
process.stdout.write(
|
|
318
|
+
`${split ? '✅' : '❌'} the fixture's two asks split one per class\n`,
|
|
319
|
+
);
|
|
320
|
+
if (!split) failed += 1;
|
|
321
|
+
} finally {
|
|
322
|
+
fs.rmSync(fx, { recursive: true, force: true });
|
|
323
|
+
}
|
|
224
324
|
const positives = cases.filter((c) => c[3]).length;
|
|
225
325
|
if (positives < 3) {
|
|
226
326
|
process.stderr.write('❌ probe_unblocked_ask --self-test: fewer than 3 positive cases — a truncated suite must fail.\n');
|
|
@@ -233,6 +333,32 @@ export function selfTest(): number {
|
|
|
233
333
|
return failed > 0 ? 1 : 0;
|
|
234
334
|
}
|
|
235
335
|
|
|
336
|
+
const HELP = `probe_unblocked_ask — the unblocked-ask rate, and the FORM each ask took.
|
|
337
|
+
|
|
338
|
+
Usage:
|
|
339
|
+
probe_unblocked_ask [--limit N] [--store PATH] [--json]
|
|
340
|
+
probe_unblocked_ask --self-test
|
|
341
|
+
probe_unblocked_ask --help
|
|
342
|
+
|
|
343
|
+
Dimensions reported:
|
|
344
|
+
hand-back turns assistant turns immediately followed by a user turn
|
|
345
|
+
unblocked asks …that hand a decision with no numbered-options block
|
|
346
|
+
malformed …of those, carrying no Recommendation:/Empfehlung: label
|
|
347
|
+
form the ask FORM partition of the unblocked asks:
|
|
348
|
+
native the turn carried a structured-ask tool call
|
|
349
|
+
text the ask was prose
|
|
350
|
+
text + native always equals the unblocked-ask total.
|
|
351
|
+
native is 0 wherever no host has an observed structured-ask
|
|
352
|
+
tool — a measured zero, not an uninstrumented one.
|
|
353
|
+
|
|
354
|
+
Options:
|
|
355
|
+
--limit N most recent N sessions (default 60)
|
|
356
|
+
--store PATH transcript store (default: the host's own)
|
|
357
|
+
--json emit the Result object instead of the report
|
|
358
|
+
--self-test run the detector fixtures, incl. the form-partition identity
|
|
359
|
+
--help this text
|
|
360
|
+
`;
|
|
361
|
+
|
|
236
362
|
export function main(argv: string[]): number {
|
|
237
363
|
let limit = 60;
|
|
238
364
|
let storeArg: string | null = null;
|
|
@@ -246,6 +372,10 @@ export function main(argv: string[]): number {
|
|
|
246
372
|
// in-process, and resolving a transcript store that may not exist here
|
|
247
373
|
// would make the sensitivity proof depend on the host having one.
|
|
248
374
|
else if (a === '--self-test') return selfTest();
|
|
375
|
+
else if (a === '--help' || a === '-h') {
|
|
376
|
+
process.stdout.write(HELP);
|
|
377
|
+
return 0;
|
|
378
|
+
}
|
|
249
379
|
else {
|
|
250
380
|
process.stderr.write(`probe_unblocked_ask: unrecognized argument: ${a}\n`);
|
|
251
381
|
return 0;
|
|
@@ -264,6 +394,9 @@ export function main(argv: string[]): number {
|
|
|
264
394
|
` …with a numbered block ${String(r.withBlock)} (excluded — check_reply_consistency reads that surface)\n` +
|
|
265
395
|
` …unblocked asks ${String(r.unblockedAsks)} ${pct(r.unblockedAsks, r.handbacks)} of hand-backs\n` +
|
|
266
396
|
` …of those, malformed ${String(r.malformed)} ${pct(r.malformed, r.unblockedAsks)} of unblocked asks\n` +
|
|
397
|
+
` form: text ${String(r.form.text)} ${pct(r.form.text, r.unblockedAsks)} of unblocked asks\n` +
|
|
398
|
+
` form: native ${String(r.form.native)} ${pct(r.form.native, r.unblockedAsks)} of unblocked asks\n` +
|
|
399
|
+
` form: text + native ${String(r.form.text + r.form.native)} (must equal unblocked asks: ${String(r.unblockedAsks)})\n` +
|
|
267
400
|
'\n CEILING, not a point estimate: "hands a decision" is a heuristic and a\n' +
|
|
268
401
|
' rhetorical question can match it. A recommendation is matched on its LABEL,\n' +
|
|
269
402
|
' which is the rule\'s own bar but a bar on form.\n',
|
package/src/scripts/release.ts
CHANGED
|
@@ -126,6 +126,7 @@ import {
|
|
|
126
126
|
derive_category_hits,
|
|
127
127
|
render_derived_head_values,
|
|
128
128
|
} from './_lib/release_highlights.js';
|
|
129
|
+
import { canPrompt, promptLine } from './_lib/tty_prompt.js';
|
|
129
130
|
import { preflightPosition } from './_lib/release_position.js';
|
|
130
131
|
import {
|
|
131
132
|
RELEASE_HEAD_DEFAULT,
|
|
@@ -554,8 +555,8 @@ function render_changelog_entry(
|
|
|
554
555
|
|
|
555
556
|
// The curated head sits above the generated log, which stays unchanged.
|
|
556
557
|
const body_lines: string[] = [...render_release_head(opts.head ?? {})];
|
|
557
|
-
// Under the head
|
|
558
|
-
if (opts.mix
|
|
558
|
+
// Under the head, outside it; any resolved reading — see `MixObligation`.
|
|
559
|
+
if (opts.mix) {
|
|
559
560
|
body_lines.push('');
|
|
560
561
|
body_lines.push(...render_mix_response(opts.mix.level));
|
|
561
562
|
}
|
|
@@ -933,74 +934,12 @@ function print_preview(plan: Plan): void {
|
|
|
933
934
|
}
|
|
934
935
|
|
|
935
936
|
function confirm(prompt: string): boolean | null {
|
|
936
|
-
const ans =
|
|
937
|
+
const ans = promptLine(`${prompt} [y/N] `);
|
|
937
938
|
if (ans === null) return null; // no usable controlling terminal
|
|
938
939
|
const norm = ans.trim().toLowerCase();
|
|
939
940
|
return norm === 'y' || norm === 'yes';
|
|
940
941
|
}
|
|
941
942
|
|
|
942
|
-
/** Can we prompt at all — is fd 0 a TTY, or is a controlling terminal openable? */
|
|
943
|
-
function _canPrompt(): boolean {
|
|
944
|
-
if (process.env.CI) return false; // CI is non-interactive by contract → require --yes
|
|
945
|
-
if (process.stdin.isTTY) return true;
|
|
946
|
-
try {
|
|
947
|
-
const fd = fs.openSync('/dev/tty', 'r');
|
|
948
|
-
fs.closeSync(fd);
|
|
949
|
-
return true;
|
|
950
|
-
} catch {
|
|
951
|
-
return false;
|
|
952
|
-
}
|
|
953
|
-
}
|
|
954
|
-
|
|
955
|
-
/**
|
|
956
|
-
* Mirror of Python `input(prompt)` — write the prompt, read one line from the
|
|
957
|
-
* controlling terminal. Returns `null` when no terminal is reachable.
|
|
958
|
-
*
|
|
959
|
-
* The answer is ALWAYS read from a freshly opened, blocking `/dev/tty`
|
|
960
|
-
* (`openSync(..., 'rs')`), NEVER from fd 0. `task release` / `./scripts-run`
|
|
961
|
-
* run this under go-task's `interactive: true`, which leaves fd 0 a TTY
|
|
962
|
-
* (`process.stdin.isTTY === true`) but one Node treats as NON-blocking — so a
|
|
963
|
-
* bare `readSync(0)` throws `EAGAIN`, the catch swallows it, and the `[y/N]`
|
|
964
|
-
* prompt "auto-aborts" without ever waiting (the exact failure this fixes;
|
|
965
|
-
* reproduced with `isTTY === true`). A fresh blocking `/dev/tty` descriptor
|
|
966
|
-
* blocks for real input regardless of how the script was invoked — the proven
|
|
967
|
-
* synchronous-prompt pattern.
|
|
968
|
-
*/
|
|
969
|
-
function _input(prompt: string): string | null {
|
|
970
|
-
process.stdout.write(prompt);
|
|
971
|
-
let fd: number;
|
|
972
|
-
try {
|
|
973
|
-
// 'rs' → O_RDONLY | O_SYNC: a fresh, blocking descriptor on the
|
|
974
|
-
// controlling terminal, unaffected by Node's non-blocking fd 0.
|
|
975
|
-
fd = fs.openSync('/dev/tty', 'rs');
|
|
976
|
-
} catch {
|
|
977
|
-
return null; // no controlling terminal (true non-interactive)
|
|
978
|
-
}
|
|
979
|
-
try {
|
|
980
|
-
const buf = Buffer.alloc(1);
|
|
981
|
-
const chars: number[] = [];
|
|
982
|
-
for (;;) {
|
|
983
|
-
let bytesRead: number;
|
|
984
|
-
try {
|
|
985
|
-
bytesRead = fs.readSync(fd, buf, 0, 1, null);
|
|
986
|
-
} catch (e) {
|
|
987
|
-
const code = (e as NodeJS.ErrnoException).code;
|
|
988
|
-
// A blocking /dev/tty should not yield these, but inherited
|
|
989
|
-
// descriptor flags can — retry rather than abort the prompt.
|
|
990
|
-
if (code === 'EAGAIN' || code === 'EINTR') continue;
|
|
991
|
-
break; // EOF / EIO → EOFError analogue; return what we have.
|
|
992
|
-
}
|
|
993
|
-
if (bytesRead === 0) break;
|
|
994
|
-
const b = buf[0] as number;
|
|
995
|
-
if (b === 0x0a) break; // newline terminates the line (stripped, like input()).
|
|
996
|
-
chars.push(b);
|
|
997
|
-
}
|
|
998
|
-
return Buffer.from(chars).toString('utf-8');
|
|
999
|
-
} finally {
|
|
1000
|
-
fs.closeSync(fd);
|
|
1001
|
-
}
|
|
1002
|
-
}
|
|
1003
|
-
|
|
1004
943
|
export interface ConfirmVerdict {
|
|
1005
944
|
proceed: boolean;
|
|
1006
945
|
/** Message to surface when not proceeding (no trailing newline). */
|
|
@@ -1024,7 +963,7 @@ export function confirmGate(target: string, yes: boolean): ConfirmVerdict {
|
|
|
1024
963
|
'No terminal available for the [y/N] confirmation (non-interactive shell). ' +
|
|
1025
964
|
'Re-run with --yes to confirm, e.g. `task release -- --yes`.',
|
|
1026
965
|
};
|
|
1027
|
-
if (!
|
|
966
|
+
if (!canPrompt()) return noTerminal;
|
|
1028
967
|
const answer = confirm(`Proceed with release ${target}?`);
|
|
1029
968
|
// null = the controlling terminal vanished between the probe and the read;
|
|
1030
969
|
// surface the actionable --yes guidance, never a bare silent "aborted.".
|
|
@@ -1684,17 +1623,33 @@ function main(argv: readonly string[] | null = null): number {
|
|
|
1684
1623
|
|
|
1685
1624
|
const bump = resolve_bump(args.bump_override, commits);
|
|
1686
1625
|
|
|
1687
|
-
//
|
|
1688
|
-
//
|
|
1689
|
-
//
|
|
1690
|
-
|
|
1626
|
+
// An in-flight release is a STATE, never a flag — so the probe runs
|
|
1627
|
+
// unconditionally and `--resume` no longer decides what is being released.
|
|
1628
|
+
//
|
|
1629
|
+
// The comment this replaces already stated the right intent ("so we don't
|
|
1630
|
+
// accidentally start a 1.16.0 release while 1.15.0 is still in flight") and
|
|
1631
|
+
// the `args.resume ?` gate narrowed it to resumed runs only. Measured on
|
|
1632
|
+
// 14.20.0: `guard_release_curation` refuses after step 2 has bumped
|
|
1633
|
+
// `package.json`, so a plain re-run computed the bump from the ALREADY
|
|
1634
|
+
// BUMPED version, landed on 14.21.0, and then refused its own branch with
|
|
1635
|
+
// `release must run from 'main' or 'release/14.21.0', currently on
|
|
1636
|
+
// 'release/14.20.0'`. The 2026-09-07 start-position fix could not help: it
|
|
1637
|
+
// was asked about the wrong target.
|
|
1638
|
+
//
|
|
1639
|
+
// Neither branch of `_detect_in_flight_target` needs the flag — HEAD
|
|
1640
|
+
// sitting on `release/X.Y.Z`, and a `package.json` version whose tag is not
|
|
1641
|
+
// PUBLISHED, are both facts about the repository. On a clean `main` after a
|
|
1642
|
+
// completed release the second branch returns null (the tag is published),
|
|
1643
|
+
// so a fresh release is unaffected; what changes is that an abandoned bump
|
|
1644
|
+
// can no longer be silently released as the NEXT version.
|
|
1645
|
+
const in_flight = _detect_in_flight_target();
|
|
1691
1646
|
let target: string;
|
|
1692
1647
|
if (args.explicit) {
|
|
1693
1648
|
target = args.explicit;
|
|
1694
1649
|
} else if (in_flight) {
|
|
1695
1650
|
target = in_flight;
|
|
1696
1651
|
process.stdout.write(
|
|
1697
|
-
`
|
|
1652
|
+
`in-flight target ${in_flight} (package.json version with no published tag yet)\n`,
|
|
1698
1653
|
);
|
|
1699
1654
|
} else {
|
|
1700
1655
|
target = bump_version(current, bump);
|
|
@@ -244,17 +244,13 @@ export function defaultChangelogFixture(target: string): string {
|
|
|
244
244
|
'',
|
|
245
245
|
'- fixture entry for the release drill',
|
|
246
246
|
'',
|
|
247
|
-
//
|
|
248
|
-
//
|
|
249
|
-
//
|
|
250
|
-
//
|
|
251
|
-
//
|
|
252
|
-
//
|
|
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.
|
|
247
|
+
// The mix line is written OUT rather than measured: measuring would
|
|
248
|
+
// couple every scenario to the live repository's commit span, which is
|
|
249
|
+
// the coupling `changelog` was introduced to remove. It is no longer a
|
|
250
|
+
// publication obligation (ADR-261 deleted the written answer it used to
|
|
251
|
+
// carry) — it stays because the drill exercises the shape the writer
|
|
252
|
+
// actually renders.
|
|
256
253
|
'> **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
254
|
'',
|
|
259
255
|
'Tests: 1000 (+0 since 0.0.0)',
|
|
260
256
|
'',
|
|
@@ -124,6 +124,7 @@ function _set_changelog_reader(fn: ((file: string) => string) | null): void {
|
|
|
124
124
|
function read_changelog_text(): string {
|
|
125
125
|
return _changelog_reader ? _changelog_reader(CHANGELOG) : fs.readFileSync(CHANGELOG, 'utf-8');
|
|
126
126
|
}
|
|
127
|
+
|
|
127
128
|
const MAIN_BRANCH = 'main';
|
|
128
129
|
const REMOTE = 'origin';
|
|
129
130
|
const REPO_SLUG = 'event4u-app/agent-config';
|
|
@@ -458,15 +458,14 @@ export function local_release_gate_argv(): string[] {
|
|
|
458
458
|
/**
|
|
459
459
|
* Measure the governance-versus-product level for the span under release.
|
|
460
460
|
*
|
|
461
|
-
*
|
|
462
|
-
*
|
|
463
|
-
*
|
|
464
|
-
*
|
|
465
|
-
* 14.17.0 red after a branch, a PR and a CI run had already been spent.
|
|
461
|
+
* REPORTING ONLY since ADR-261. It feeds exactly one consumer: the `>
|
|
462
|
+
* **Governance mix:** …` line the writer renders into the section. No guard
|
|
463
|
+
* reads it, nothing refuses over it, and `triggered` gates nothing — the
|
|
464
|
+
* written-answer obligation it used to serve is deleted.
|
|
466
465
|
*
|
|
467
466
|
* Returns `null` on any measurement failure — a shallow clone or a missing tag
|
|
468
|
-
* is an environment fact, and
|
|
469
|
-
*
|
|
467
|
+
* is an environment fact, and a release must not stop over one. `null` means
|
|
468
|
+
* the line is simply not rendered.
|
|
470
469
|
*/
|
|
471
470
|
export function measure_mix_obligation(
|
|
472
471
|
version: string,
|
|
@@ -478,19 +477,18 @@ export function measure_mix_obligation(
|
|
|
478
477
|
const reading = measureRange(from, 'HEAD', loadTaxonomy(), version, REPO_ROOT);
|
|
479
478
|
const o = reading.response_obligation;
|
|
480
479
|
return {
|
|
481
|
-
triggered: o.triggered,
|
|
482
480
|
level:
|
|
483
481
|
`governance-only ${String(o.governance_only)} vs consumer-only ` +
|
|
484
482
|
`${String(o.consumer_only)} (taxonomy ${reading.taxonomy_version})`,
|
|
485
483
|
};
|
|
486
484
|
} catch (err) {
|
|
487
|
-
//
|
|
488
|
-
//
|
|
489
|
-
//
|
|
490
|
-
//
|
|
485
|
+
// Printed rather than swallowed. Nothing refuses over the reading any
|
|
486
|
+
// more (ADR-261), so this is not a blocked release — it is a section
|
|
487
|
+
// that will publish without its `> **Governance mix:** …` line, and the
|
|
488
|
+
// operator is entitled to know why the line is missing.
|
|
491
489
|
process.stderr.write(
|
|
492
490
|
`⚠️ governance mix not measured for ${version}: ${(err as Error).message}\n` +
|
|
493
|
-
'
|
|
491
|
+
' The section will publish without the measured line. Nothing is blocked.\n',
|
|
494
492
|
);
|
|
495
493
|
return null;
|
|
496
494
|
}
|
|
@@ -539,12 +537,7 @@ export function guard_release_branch_push(branch: string): void {
|
|
|
539
537
|
if (!section) {
|
|
540
538
|
return;
|
|
541
539
|
}
|
|
542
|
-
const blockers = section_publication_blockers(
|
|
543
|
-
section.body,
|
|
544
|
-
version,
|
|
545
|
-
`\`${branch}\``,
|
|
546
|
-
measure_mix_obligation(version),
|
|
547
|
-
);
|
|
540
|
+
const blockers = section_publication_blockers(section.body, version, `\`${branch}\``);
|
|
548
541
|
if (blockers.length === 0) {
|
|
549
542
|
return;
|
|
550
543
|
}
|
|
@@ -685,6 +678,13 @@ export function integrate_main_if_behind(branch: string, say: (msg: string) => v
|
|
|
685
678
|
run(['git', 'merge', `${REMOTE}/${MAIN_BRANCH}`, '--no-edit']);
|
|
686
679
|
}
|
|
687
680
|
|
|
681
|
+
/** Everything this site refuses over: the head-level blockers, nothing else. */
|
|
682
|
+
function curation_blockers(version: string): string[] {
|
|
683
|
+
const section = extract_changelog_section(read_changelog_text(), version);
|
|
684
|
+
if (section === null) return [];
|
|
685
|
+
return publication_blockers(section.body, version, `\`release/${version}\``);
|
|
686
|
+
}
|
|
687
|
+
|
|
688
688
|
export function guard_release_curation(version: string, prMerged = false): void {
|
|
689
689
|
if (prMerged) {
|
|
690
690
|
return;
|
|
@@ -693,8 +693,7 @@ export function guard_release_curation(version: string, prMerged = false): void
|
|
|
693
693
|
if (!section) {
|
|
694
694
|
return;
|
|
695
695
|
}
|
|
696
|
-
// The head-level blockers
|
|
697
|
-
// tests footer.
|
|
696
|
+
// The head-level blockers only — deliberately NOT the tests footer.
|
|
698
697
|
//
|
|
699
698
|
// This site runs BEFORE `prepend_changelog`'s output is committed, and
|
|
700
699
|
// `--resume` skips the whole bump block once `package.json` already carries
|
|
@@ -707,17 +706,23 @@ export function guard_release_curation(version: string, prMerged = false): void
|
|
|
707
706
|
//
|
|
708
707
|
// `guard_release_branch_push` carries it instead: by then the section is
|
|
709
708
|
// committed and editable on the branch, and nothing has left the machine.
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
709
|
+
//
|
|
710
|
+
// What is NOT here any more (ADR-261): the governance-versus-product
|
|
711
|
+
// written answer, the next-cycle promise, the read-back of the previous
|
|
712
|
+
// one, the `## [Unreleased]` staging channel that fed them and the
|
|
713
|
+
// interactive prompt that asked for them. This guard refuses the
|
|
714
|
+
// generator's own unrewritten draft — a claim no human has looked at — and
|
|
715
|
+
// nothing else. It never asks the operator to write prose about a future
|
|
716
|
+
// cycle to get a version out.
|
|
717
|
+
const blockers = curation_blockers(version);
|
|
718
718
|
if (blockers.length === 0) {
|
|
719
719
|
return;
|
|
720
720
|
}
|
|
721
|
+
// Read once. This used to re-read the file here, because
|
|
722
|
+
// `consume_staged_answers` / `collect_curation_answers` could rewrite the
|
|
723
|
+
// section between the two reads; both write paths are gone (ADR-261) and
|
|
724
|
+
// `curation_blockers` is read-only, so a second read could only ever
|
|
725
|
+
// return the same bytes.
|
|
721
726
|
const marked = section.body
|
|
722
727
|
.split('\n')
|
|
723
728
|
.filter((l) => l.includes(DERIVED_MARKER))
|