@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
|
@@ -0,0 +1,499 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
/**
|
|
3
|
+
* The ask-block census — how this package's own authored surfaces put decisions
|
|
4
|
+
* to the user, counted by a unit that is defined before it is counted.
|
|
5
|
+
*
|
|
6
|
+
* road-to-asked-not-parked 1.2. The source draft carried a figure of 157
|
|
7
|
+
* batch-shaped commands. It does not reproduce — the natural grep returns 129 —
|
|
8
|
+
* so NO number is carried forward from it. The definition below is this
|
|
9
|
+
* script's own, and it is published here, above any count, because a
|
|
10
|
+
* classification that counts `1.`-lines instead of decision regions produces a
|
|
11
|
+
* number that moves without the surface moving.
|
|
12
|
+
*
|
|
13
|
+
* THE UNIT
|
|
14
|
+
*
|
|
15
|
+
* A **region** is one contiguous stretch of an authored file. Every region
|
|
16
|
+
* falls into exactly one of four classes, decided in this precedence order:
|
|
17
|
+
*
|
|
18
|
+
* 1. `count-only` — a line that renders a NUMBER where a question belongs:
|
|
19
|
+
* it mentions `question(s)` and carries a `{count}`
|
|
20
|
+
* placeholder. This is the shape that prints how many
|
|
21
|
+
* decisions are outstanding instead of putting one.
|
|
22
|
+
* 2. `file-parked` — an `Open questions` SECTION heading whose body carries
|
|
23
|
+
* no ask obligation. The section is then a parking lot:
|
|
24
|
+
* a place a question is written down instead of asked.
|
|
25
|
+
* 3. `batch` — a run of non-blank lines carrying TWO OR MORE question
|
|
26
|
+
* lines: one hand-back, several decisions.
|
|
27
|
+
* 4. `single` — the same run carrying exactly one.
|
|
28
|
+
*
|
|
29
|
+
* A **question line** is a non-blank line that either ends in `?` (after
|
|
30
|
+
* trailing backticks, emphasis and whitespace are stripped) or is a list item
|
|
31
|
+
* whose content carries a `{question…}` placeholder. The placeholder half is
|
|
32
|
+
* load-bearing: `/feature:plan` Round 4 lists its questions as `- {question 1}`
|
|
33
|
+
* bullets and closes with ONE collective `?`, which is precisely the shape the
|
|
34
|
+
* owner named as the hurdle and which a `?`-only detector scores as `single`.
|
|
35
|
+
*
|
|
36
|
+
* FENCED CODE IS NOT STRIPPED, and that is deliberate. The two `{count}`
|
|
37
|
+
* surfaces this census exists to find live inside display templates
|
|
38
|
+
* (`feature/refactor/command.md`, `feature/plan/command.md`) — a fence there is
|
|
39
|
+
* the rendering the user sees, not a documentation example. Stripping fences
|
|
40
|
+
* would make the census blind to its own subject.
|
|
41
|
+
*
|
|
42
|
+
* A FENCE IS ONE REGION, whole. Outside a fence a region is a blank-line-
|
|
43
|
+
* delimited run. The asymmetry is the point: a fenced block in a command file
|
|
44
|
+
* is one rendered hand-back, so three questions separated by blank lines INSIDE
|
|
45
|
+
* it are still three decisions in one turn. Splitting on blank lines there
|
|
46
|
+
* would score `/feature:plan` Round 1 — which the roadmap describes as asking
|
|
47
|
+
* three — as three separate singles, i.e. as already fixed.
|
|
48
|
+
*
|
|
49
|
+
* THE NATIVE-ASK RATE is carried IN, not computed here. It is a transcript
|
|
50
|
+
* measurement (`probe_unblocked_ask --limit N --store …`), and a transcript
|
|
51
|
+
* store is a property of the machine, not of the commit — computing it inside
|
|
52
|
+
* this script would make the artefact non-reproducible on any other checkout
|
|
53
|
+
* and quietly break the byte-identity the pin promises. So it arrives as three
|
|
54
|
+
* explicit flags carrying the numerator, the denominator and the source they
|
|
55
|
+
* were read from, and the artefact records all three. Same flags, same bytes.
|
|
56
|
+
*
|
|
57
|
+
* WHAT IT IS NOT: a gate. It exits 0 whatever it counts. Its output is a frozen
|
|
58
|
+
* artefact pinned to a commit, and the only legitimate use of a later run is to
|
|
59
|
+
* diff two artefacts — never to quote a number in prose.
|
|
60
|
+
*
|
|
61
|
+
* Usage:
|
|
62
|
+
* ./scripts-run src/scripts/ask_block_census [--root PREFIX]... [--file PATH]
|
|
63
|
+
* ./scripts-run src/scripts/ask_block_census --write [--out PATH]
|
|
64
|
+
* [--native-asks N --unblocked-asks M --native-source TEXT]
|
|
65
|
+
* ./scripts-run src/scripts/ask_block_census --json
|
|
66
|
+
* ./scripts-run src/scripts/ask_block_census --file PATH --regions
|
|
67
|
+
* ./scripts-run src/scripts/ask_block_census --self-test
|
|
68
|
+
*
|
|
69
|
+
* Exit codes: 0 always. `--self-test` exits 1 when a fixture misclassifies.
|
|
70
|
+
*/
|
|
71
|
+
import { execFileSync } from 'node:child_process';
|
|
72
|
+
import fs from 'node:fs';
|
|
73
|
+
import path from 'node:path';
|
|
74
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
75
|
+
|
|
76
|
+
const _HERE = fileURLToPath(import.meta.url);
|
|
77
|
+
export const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
|
|
78
|
+
|
|
79
|
+
/** The census classes, in precedence order. */
|
|
80
|
+
export const ASK_CLASSES = ['single', 'batch', 'count-only', 'file-parked'] as const;
|
|
81
|
+
export type AskClass = (typeof ASK_CLASSES)[number];
|
|
82
|
+
|
|
83
|
+
/** Default scan roots. `src/domains` is narrowed to `command.md` — the ask surface. */
|
|
84
|
+
export const DEFAULT_ROOTS: readonly string[] = [
|
|
85
|
+
'src/domains',
|
|
86
|
+
'src/skills',
|
|
87
|
+
'src/agent-src/contexts',
|
|
88
|
+
];
|
|
89
|
+
|
|
90
|
+
/** A line printing a question COUNT where a question belongs. */
|
|
91
|
+
export const COUNT_ONLY_RE = /\bquestions?\b/i;
|
|
92
|
+
export const COUNT_PLACEHOLDER_RE = /\{count\}/;
|
|
93
|
+
|
|
94
|
+
/** An `Open questions` section heading, levels 2-4. */
|
|
95
|
+
export const PARK_HEADING_RE = /^#{2,4}[ \t]+open questions?\b/i;
|
|
96
|
+
|
|
97
|
+
/** A fence delimiter. A whole fenced block is ONE region — see the header. */
|
|
98
|
+
const FENCE_RE = /^[ \t]*(?:```|~~~)/;
|
|
99
|
+
|
|
100
|
+
/** Any heading — where a section body ends. */
|
|
101
|
+
const ANY_HEADING_RE = /^#{1,6}[ \t]+\S/;
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* An ask obligation stated in prose: the section is owed to the user, not
|
|
105
|
+
* stored. Matched per line so a distant sentence cannot launder a parking lot
|
|
106
|
+
* three sections away.
|
|
107
|
+
*/
|
|
108
|
+
export const ASK_OBLIGATION_RE =
|
|
109
|
+
/(ask[a-z]*\b[^\n]{0,90}\buser\b)|(\buser\b[^\n]{0,90}\bask[a-z]*\b)|one at a time|one question per|separately before/i;
|
|
110
|
+
|
|
111
|
+
/** A `{question…}` placeholder inside a list item. */
|
|
112
|
+
const QUESTION_PLACEHOLDER_RE = /^[ \t]*(?:[-*+•]|\d+[.)])[ \t]+.*\{question/i;
|
|
113
|
+
|
|
114
|
+
/** Trailing markdown noise that hides a question mark. */
|
|
115
|
+
const TRAILING_NOISE_RE = /[`*_~\s)\]]+$/;
|
|
116
|
+
|
|
117
|
+
export function isQuestionLine(line: string): boolean {
|
|
118
|
+
if (line.trim() === '') {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
if (QUESTION_PLACEHOLDER_RE.test(line)) {
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
return line.replace(TRAILING_NOISE_RE, '').endsWith('?');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export interface Region {
|
|
128
|
+
readonly cls: AskClass;
|
|
129
|
+
readonly line: number;
|
|
130
|
+
readonly questions: number;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Classify one file's regions.
|
|
135
|
+
*
|
|
136
|
+
* Count-only lines are consumed first and removed from the run-scanner's view,
|
|
137
|
+
* so a `{count}` line that happens to end in `?` cannot be double-counted.
|
|
138
|
+
*/
|
|
139
|
+
export function scanFile(text: string): Region[] {
|
|
140
|
+
const lines = text.split('\n');
|
|
141
|
+
const out: Region[] = [];
|
|
142
|
+
const consumed = new Set<number>();
|
|
143
|
+
|
|
144
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
145
|
+
const line = lines[i] as string;
|
|
146
|
+
if (COUNT_ONLY_RE.test(line) && COUNT_PLACEHOLDER_RE.test(line)) {
|
|
147
|
+
out.push({ cls: 'count-only', line: i + 1, questions: 0 });
|
|
148
|
+
consumed.add(i);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
153
|
+
if (!PARK_HEADING_RE.test(lines[i] as string)) {
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
let end = i + 1;
|
|
157
|
+
while (end < lines.length && !ANY_HEADING_RE.test(lines[end] as string)) {
|
|
158
|
+
end += 1;
|
|
159
|
+
}
|
|
160
|
+
const body = lines.slice(i, end);
|
|
161
|
+
if (!body.some((l) => ASK_OBLIGATION_RE.test(l))) {
|
|
162
|
+
out.push({ cls: 'file-parked', line: i + 1, questions: 0 });
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
let start = -1;
|
|
167
|
+
let questions = 0;
|
|
168
|
+
const flush = (): void => {
|
|
169
|
+
if (start >= 0 && questions > 0) {
|
|
170
|
+
out.push({
|
|
171
|
+
cls: questions >= 2 ? 'batch' : 'single',
|
|
172
|
+
line: start + 1,
|
|
173
|
+
questions,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
start = -1;
|
|
177
|
+
questions = 0;
|
|
178
|
+
};
|
|
179
|
+
let inFence = false;
|
|
180
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
181
|
+
const line = lines[i] as string;
|
|
182
|
+
if (FENCE_RE.test(line)) {
|
|
183
|
+
flush();
|
|
184
|
+
inFence = !inFence;
|
|
185
|
+
if (inFence) {
|
|
186
|
+
start = i;
|
|
187
|
+
}
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
if (!inFence && line.trim() === '') {
|
|
191
|
+
flush();
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
if (start < 0) {
|
|
195
|
+
start = i;
|
|
196
|
+
}
|
|
197
|
+
if (!consumed.has(i) && isQuestionLine(line)) {
|
|
198
|
+
questions += 1;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
flush();
|
|
202
|
+
|
|
203
|
+
out.sort((a, b) => a.line - b.line || ASK_CLASSES.indexOf(a.cls) - ASK_CLASSES.indexOf(b.cls));
|
|
204
|
+
return out;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** Every scannable file under one root. `src/domains` contributes `command.md` only. */
|
|
208
|
+
function filesUnder(root: string): string[] {
|
|
209
|
+
const abs = path.join(REPO_ROOT, root);
|
|
210
|
+
const out: string[] = [];
|
|
211
|
+
const domainsOnly = root === 'src/domains' || root.startsWith('src/domains/');
|
|
212
|
+
const walk = (dir: string): void => {
|
|
213
|
+
let entries: fs.Dirent[];
|
|
214
|
+
try {
|
|
215
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
216
|
+
} catch {
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
for (const e of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
220
|
+
const p = path.join(dir, e.name);
|
|
221
|
+
if (e.isDirectory()) {
|
|
222
|
+
walk(p);
|
|
223
|
+
} else if (e.name.endsWith('.md') && (!domainsOnly || e.name === 'command.md')) {
|
|
224
|
+
out.push(p);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
walk(abs);
|
|
229
|
+
return out.sort();
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export interface FileCensus {
|
|
233
|
+
readonly file: string;
|
|
234
|
+
readonly counts: Record<AskClass, number>;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** The native-ask rate, carried in from a transcript probe with its source. */
|
|
238
|
+
export interface NativeRate {
|
|
239
|
+
readonly native: number;
|
|
240
|
+
readonly unblocked: number;
|
|
241
|
+
readonly source: string;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export interface Census {
|
|
245
|
+
readonly roots: readonly string[];
|
|
246
|
+
readonly totals: Record<AskClass, number>;
|
|
247
|
+
readonly files: readonly FileCensus[];
|
|
248
|
+
readonly scanned: number;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function zero(): Record<AskClass, number> {
|
|
252
|
+
return { single: 0, batch: 0, 'count-only': 0, 'file-parked': 0 };
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export function census(roots: readonly string[], only: string | null): Census {
|
|
256
|
+
const totals = zero();
|
|
257
|
+
const files: FileCensus[] = [];
|
|
258
|
+
const targets = only
|
|
259
|
+
? [path.join(REPO_ROOT, only)]
|
|
260
|
+
: roots.flatMap((r) => filesUnder(r));
|
|
261
|
+
for (const abs of targets) {
|
|
262
|
+
let text: string;
|
|
263
|
+
try {
|
|
264
|
+
text = fs.readFileSync(abs, 'utf8');
|
|
265
|
+
} catch {
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
const counts = zero();
|
|
269
|
+
for (const r of scanFile(text)) {
|
|
270
|
+
counts[r.cls] += 1;
|
|
271
|
+
totals[r.cls] += 1;
|
|
272
|
+
}
|
|
273
|
+
if (ASK_CLASSES.some((c) => counts[c] > 0)) {
|
|
274
|
+
files.push({ file: path.relative(REPO_ROOT, abs).split(path.sep).join('/'), counts });
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return { roots, totals, files, scanned: targets.length };
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** `<sha> <ISO commit date>` — the pin. Deterministic at a given commit. */
|
|
281
|
+
function pin(): { sha: string; date: string } {
|
|
282
|
+
const git = (args: string[]): string =>
|
|
283
|
+
execFileSync('git', args, { cwd: REPO_ROOT, encoding: 'utf8' }).trim();
|
|
284
|
+
try {
|
|
285
|
+
return { sha: git(['rev-parse', 'HEAD']), date: git(['show', '-s', '--format=%cI', 'HEAD']) };
|
|
286
|
+
} catch {
|
|
287
|
+
return { sha: 'unknown', date: 'unknown' };
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export function render(c: Census, native: NativeRate | null): string {
|
|
292
|
+
const p = pin();
|
|
293
|
+
const lines: string[] = [];
|
|
294
|
+
lines.push('<!-- evidence-type: analysis -->');
|
|
295
|
+
lines.push(`<!-- ask-block-census: v1 | commit: ${p.sha} | commit-date: ${p.date} -->`);
|
|
296
|
+
lines.push('');
|
|
297
|
+
lines.push('# Ask-block census');
|
|
298
|
+
lines.push('');
|
|
299
|
+
lines.push(
|
|
300
|
+
'Emitted by `src/scripts/ask_block_census.ts`. The classification definition is',
|
|
301
|
+
);
|
|
302
|
+
lines.push(
|
|
303
|
+
'published in that script\'s module header, above any count — read it before reading',
|
|
304
|
+
);
|
|
305
|
+
lines.push('a number here. No figure from the source draft is carried forward.');
|
|
306
|
+
lines.push('');
|
|
307
|
+
lines.push(`- **Commit pin:** \`${p.sha}\` (${p.date})`);
|
|
308
|
+
lines.push(`- **Roots:** ${c.roots.map((r) => `\`${r}\``).join(', ')}`);
|
|
309
|
+
lines.push(`- **Files scanned:** ${String(c.scanned)}`);
|
|
310
|
+
lines.push('');
|
|
311
|
+
lines.push('## Totals');
|
|
312
|
+
lines.push('');
|
|
313
|
+
lines.push('| class | regions |');
|
|
314
|
+
lines.push('|---|---|');
|
|
315
|
+
for (const cls of ASK_CLASSES) {
|
|
316
|
+
lines.push(`| \`${cls}\` | ${String(c.totals[cls])} |`);
|
|
317
|
+
}
|
|
318
|
+
lines.push('');
|
|
319
|
+
lines.push('## Native-ask rate');
|
|
320
|
+
lines.push('');
|
|
321
|
+
if (native === null) {
|
|
322
|
+
lines.push(
|
|
323
|
+
'NOT MEASURED in this run. The rate is a transcript measurement carried in via',
|
|
324
|
+
);
|
|
325
|
+
lines.push(
|
|
326
|
+
'`--native-asks` / `--unblocked-asks` / `--native-source`; absent, it is recorded as',
|
|
327
|
+
);
|
|
328
|
+
lines.push('absent rather than as zero.');
|
|
329
|
+
} else {
|
|
330
|
+
const pct =
|
|
331
|
+
native.unblocked === 0
|
|
332
|
+
? 'n/a'
|
|
333
|
+
: `${((100 * native.native) / native.unblocked).toFixed(1)}%`;
|
|
334
|
+
lines.push(
|
|
335
|
+
`- **Native asks:** ${String(native.native)} of ${String(native.unblocked)} unblocked asks (${pct})`,
|
|
336
|
+
);
|
|
337
|
+
lines.push(`- **Source:** ${native.source}`);
|
|
338
|
+
lines.push('');
|
|
339
|
+
lines.push(
|
|
340
|
+
'A native ask is a hand-back carrying a structured-ask tool call. No host in the',
|
|
341
|
+
);
|
|
342
|
+
lines.push(
|
|
343
|
+
'capability registry has an observed structured-ask tool, so a zero here is a',
|
|
344
|
+
);
|
|
345
|
+
lines.push('MEASURED zero, not an uninstrumented one.');
|
|
346
|
+
}
|
|
347
|
+
lines.push('');
|
|
348
|
+
lines.push('## Per file');
|
|
349
|
+
lines.push('');
|
|
350
|
+
lines.push('| file | single | batch | count-only | file-parked |');
|
|
351
|
+
lines.push('|---|---|---|---|---|');
|
|
352
|
+
for (const f of c.files) {
|
|
353
|
+
lines.push(
|
|
354
|
+
`| \`${f.file}\` | ${String(f.counts.single)} | ${String(f.counts.batch)} | ` +
|
|
355
|
+
`${String(f.counts['count-only'])} | ${String(f.counts['file-parked'])} |`,
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
lines.push('');
|
|
359
|
+
return `${lines.join('\n')}`;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
export function selfTest(): number {
|
|
363
|
+
const cases: Array<[string, string, AskClass, number]> = [
|
|
364
|
+
[
|
|
365
|
+
'a count placeholder is count-only',
|
|
366
|
+
'OPEN QUESTIONS: {count}\n',
|
|
367
|
+
'count-only',
|
|
368
|
+
1,
|
|
369
|
+
],
|
|
370
|
+
[
|
|
371
|
+
'a bare Open questions section with no obligation is file-parked',
|
|
372
|
+
'## Open questions\n\n- something\n',
|
|
373
|
+
'file-parked',
|
|
374
|
+
1,
|
|
375
|
+
],
|
|
376
|
+
[
|
|
377
|
+
'an Open questions section carrying an ask obligation is NOT file-parked',
|
|
378
|
+
'## Open questions\n\nEach one is put to the user separately before the first work step.\n',
|
|
379
|
+
'file-parked',
|
|
380
|
+
0,
|
|
381
|
+
],
|
|
382
|
+
[
|
|
383
|
+
'two question lines in one run are a batch',
|
|
384
|
+
'What belongs in scope?\nAnd what can we leave out?\n',
|
|
385
|
+
'batch',
|
|
386
|
+
1,
|
|
387
|
+
],
|
|
388
|
+
[
|
|
389
|
+
'one question line is a single',
|
|
390
|
+
'What belongs in scope?\n',
|
|
391
|
+
'single',
|
|
392
|
+
1,
|
|
393
|
+
],
|
|
394
|
+
[
|
|
395
|
+
'question placeholders in bullets count as questions',
|
|
396
|
+
'I have some open questions:\n- {question 1}\n- {question 2}\nAny opinion?\n',
|
|
397
|
+
'batch',
|
|
398
|
+
1,
|
|
399
|
+
],
|
|
400
|
+
[
|
|
401
|
+
'a fence is one region even when blank lines split its questions',
|
|
402
|
+
'```\nDo I understand the problem?\n\nAnd the solution?\n\nWhat is missing?\n```\n',
|
|
403
|
+
'batch',
|
|
404
|
+
1,
|
|
405
|
+
],
|
|
406
|
+
[
|
|
407
|
+
'prose with no question is neither single nor batch',
|
|
408
|
+
'This paragraph states a fact.\n',
|
|
409
|
+
'single',
|
|
410
|
+
0,
|
|
411
|
+
],
|
|
412
|
+
];
|
|
413
|
+
let failed = 0;
|
|
414
|
+
for (const [name, text, cls, want] of cases) {
|
|
415
|
+
const got = scanFile(text).filter((r) => r.cls === cls).length;
|
|
416
|
+
const ok = got === want;
|
|
417
|
+
process.stdout.write(
|
|
418
|
+
`${ok ? '✅' : '❌'} ${name} — ${cls}=${String(got)}` +
|
|
419
|
+
(ok ? '\n' : ` (wanted ${String(want)})\n`),
|
|
420
|
+
);
|
|
421
|
+
if (!ok) failed += 1;
|
|
422
|
+
}
|
|
423
|
+
process.stdout.write(
|
|
424
|
+
`\nask_block_census --self-test: ${String(cases.length - failed)}/${String(cases.length)} case(s) behaved\n`,
|
|
425
|
+
);
|
|
426
|
+
return failed > 0 ? 1 : 0;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
export function main(argv: string[]): number {
|
|
430
|
+
const roots: string[] = [];
|
|
431
|
+
let only: string | null = null;
|
|
432
|
+
let out: string | null = null;
|
|
433
|
+
let write = false;
|
|
434
|
+
let json = false;
|
|
435
|
+
let regions = false;
|
|
436
|
+
let nativeAsks: number | null = null;
|
|
437
|
+
let unblockedAsks: number | null = null;
|
|
438
|
+
let nativeSource: string | null = null;
|
|
439
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
440
|
+
const a = argv[i] as string;
|
|
441
|
+
if (a === '--self-test') return selfTest();
|
|
442
|
+
else if (a === '--root') roots.push(String(argv[++i]));
|
|
443
|
+
else if (a === '--file') only = String(argv[++i]);
|
|
444
|
+
else if (a === '--out') out = String(argv[++i]);
|
|
445
|
+
else if (a === '--write') write = true;
|
|
446
|
+
else if (a === '--json') json = true;
|
|
447
|
+
else if (a === '--regions') regions = true;
|
|
448
|
+
else if (a === '--native-asks') nativeAsks = Number(argv[++i]);
|
|
449
|
+
else if (a === '--unblocked-asks') unblockedAsks = Number(argv[++i]);
|
|
450
|
+
else if (a === '--native-source') nativeSource = String(argv[++i]);
|
|
451
|
+
else {
|
|
452
|
+
process.stderr.write(`ask_block_census: unrecognized argument: ${a}\n`);
|
|
453
|
+
return 0;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
if (regions) {
|
|
457
|
+
if (only === null) {
|
|
458
|
+
process.stderr.write('ask_block_census: --regions needs --file\n');
|
|
459
|
+
return 0;
|
|
460
|
+
}
|
|
461
|
+
const text = fs.readFileSync(path.join(REPO_ROOT, only), 'utf8');
|
|
462
|
+
for (const r of scanFile(text)) {
|
|
463
|
+
process.stdout.write(
|
|
464
|
+
`${only}:${String(r.line)} ${r.cls} (question lines: ${String(r.questions)})\n`,
|
|
465
|
+
);
|
|
466
|
+
}
|
|
467
|
+
return 0;
|
|
468
|
+
}
|
|
469
|
+
const c = census(roots.length ? roots : DEFAULT_ROOTS, only);
|
|
470
|
+
if (write) {
|
|
471
|
+
const target = path.join(
|
|
472
|
+
REPO_ROOT,
|
|
473
|
+
out ?? 'agents/evidence/analysis/ask-block-census-baseline.md',
|
|
474
|
+
);
|
|
475
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
476
|
+
const native: NativeRate | null =
|
|
477
|
+
nativeAsks !== null && unblockedAsks !== null && nativeSource !== null
|
|
478
|
+
? { native: nativeAsks, unblocked: unblockedAsks, source: nativeSource }
|
|
479
|
+
: null;
|
|
480
|
+
fs.writeFileSync(target, render(c, native), 'utf8');
|
|
481
|
+
process.stdout.write(`ask_block_census: wrote ${path.relative(REPO_ROOT, target)}\n`);
|
|
482
|
+
return 0;
|
|
483
|
+
}
|
|
484
|
+
if (json) {
|
|
485
|
+
process.stdout.write(`${JSON.stringify(c, null, 2)}\n`);
|
|
486
|
+
return 0;
|
|
487
|
+
}
|
|
488
|
+
process.stdout.write(
|
|
489
|
+
`ask_block_census · ${String(c.scanned)} file(s) under ${c.roots.join(', ')}\n`,
|
|
490
|
+
);
|
|
491
|
+
for (const cls of ASK_CLASSES) {
|
|
492
|
+
process.stdout.write(` ${cls.padEnd(12)} ${String(c.totals[cls])}\n`);
|
|
493
|
+
}
|
|
494
|
+
return 0;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) {
|
|
498
|
+
process.exit(main(process.argv.slice(2)));
|
|
499
|
+
}
|