@event4u/agent-config 14.19.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.
Files changed (161) hide show
  1. package/.augment-plugin/marketplace.json +3 -3
  2. package/.augment-plugin/plugin.json +1 -1
  3. package/.claude-plugin/marketplace.json +1 -1
  4. package/CHANGELOG.md +351 -156
  5. package/README.md +1 -1
  6. package/agents/templates/.ai-council.yml.example +10 -0
  7. package/dist/agent-src/commands/agent-handoff.md +42 -41
  8. package/dist/agent-src/commands/analyze/inbox.md +1 -1
  9. package/dist/agent-src/commands/analyze/{reference-repo.md → repo.md} +247 -32
  10. package/dist/agent-src/commands/analyze/roadmap-repos.md +129 -0
  11. package/dist/agent-src/commands/analyze.md +12 -7
  12. package/dist/agent-src/commands/feature/plan.md +55 -16
  13. package/dist/agent-src/commands/feature/refactor.md +8 -3
  14. package/dist/agent-src/commands/humanize.md +48 -4
  15. package/dist/agent-src/contexts/execution/contract-decision-sheet.md +22 -1
  16. package/dist/agent-src/contexts/execution/host-capability-manifest.md +11 -2
  17. package/dist/agent-src/contexts/execution/roadmap-process-loop.md +35 -2
  18. package/dist/agent-src/contexts/execution/terminal-states.md +4 -1
  19. package/dist/agent-src/rules/evaluator-independence.md +4 -4
  20. package/dist/agent-src/rules/external-reference-deep-dive.md +2 -2
  21. package/dist/agent-src/rules/untrusted-input-defense.md +0 -2
  22. package/dist/agent-src/skills/competitive-positioning/SKILL.md +2 -2
  23. package/dist/agent-src/skills/evaluate-llm-feature/SKILL.md +1 -1
  24. package/dist/agent-src/skills/humanizer/references/anti-aiisms.md +27 -11
  25. package/dist/agent-src/skills/learning-to-rule-or-skill/SKILL.md +1 -1
  26. package/dist/agent-src/skills/markitdown/SKILL.md +2 -2
  27. package/dist/agent-src/skills/repomix-packer/SKILL.md +1 -1
  28. package/dist/agent-src/skills/skill-improvement-pipeline/SKILL.md +28 -6
  29. package/dist/agent-src/skills/technical-specification/SKILL.md +4 -0
  30. package/dist/agent-src/skills/test-driven-development/SKILL.md +1 -3
  31. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  32. package/dist/agent-src/templates/features.md +5 -0
  33. package/dist/agent-src/templates/roadmaps.md +29 -1
  34. package/dist/agent-src/templates/scripts/telemetry/mcp_lite.ts +213 -0
  35. package/dist/agent-src/templates/scripts/telemetry/report_renderer.ts +16 -0
  36. package/dist/agent-src/templates/scripts/telemetry_report.ts +22 -2
  37. package/dist/cli/mcp/dispatch.js +10 -0
  38. package/dist/cli/mcp/stdio.js +33 -2
  39. package/dist/cli/mcp/telemetry.js +227 -0
  40. package/dist/cli-delegate/{chunk-KPBWFOPZ.js → chunk-2FTMENJL.js} +194 -37
  41. package/dist/cli-delegate/{chunk-PMEKGDED.js → chunk-2TAQ2K4V.js} +166 -1
  42. package/dist/cli-delegate/{chunk-GZTMRYCN.js → chunk-643Z7FIC.js} +79 -12
  43. package/dist/cli-delegate/{chunk-CYH5SP6M.js → chunk-I6RHFQEG.js} +151 -16
  44. package/dist/cli-delegate/{chunk-IDHUQIXP.js → chunk-J3RFNKG4.js} +41 -5
  45. package/dist/cli-delegate/{chunk-7PRE6ZV2.js → chunk-YS6XTLEK.js} +199 -4
  46. package/dist/cli-delegate/chunk-ZVBOHSVF.js +1109 -0
  47. package/dist/cli-delegate/cmd_conformance.js +88 -110
  48. package/dist/cli-delegate/cmd_doctor.js +3 -3
  49. package/dist/cli-delegate/cmd_export.js +2 -2
  50. package/dist/cli-delegate/cmd_handoff.js +42 -12
  51. package/dist/cli-delegate/cmd_memory_get.js +1 -1
  52. package/dist/cli-delegate/cmd_prune.js +2 -2
  53. package/dist/cli-delegate/cmd_route_audit.js +1 -1
  54. package/dist/cli-delegate/cmd_session_recycle.js +59 -902
  55. package/dist/cli-delegate/cmd_settings_migrate.js +2 -2
  56. package/dist/cli-delegate/cmd_sync.js +2 -2
  57. package/dist/cli-delegate/cmd_uninstall.js +2 -2
  58. package/dist/cli-delegate/cmd_validate.js +2 -2
  59. package/dist/discovery/deprecation-report.md +1 -1
  60. package/dist/discovery/discovery-manifest.json +62 -31
  61. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  62. package/dist/discovery/discovery-manifest.summary.md +3 -3
  63. package/dist/discovery/dormancy-report.md +1 -1
  64. package/dist/discovery/orphan-report.md +1 -1
  65. package/dist/discovery/packs.json +7 -6
  66. package/dist/discovery/trust-report.md +2 -2
  67. package/dist/discovery/workspaces.json +6 -5
  68. package/dist/hooks/dispatch.js +3801 -2803
  69. package/dist/install/install.mjs +396 -168
  70. package/dist/mcp/registry-manifest.json +3 -3
  71. package/dist/mcp/server.json +2 -2
  72. package/dist/mcp/server.mjs +809 -180
  73. package/dist/scripts/ai_council/content_ceiling.js +63 -0
  74. package/dist/scripts/ai_council/seat_policy.js +106 -0
  75. package/dist/scripts/ai_council/transport_resolver.js +21 -0
  76. package/dist/scripts/mcp_server/telemetry.js +148 -0
  77. package/docs/guidelines/agent-infra/naming.md +1 -1
  78. package/docs/guidelines/agent-infra/security-lint-containment.md +46 -6
  79. package/docs/guidelines/agent-infra/self-improvement-pipeline.md +4 -4
  80. package/docs/guidelines/agent-infra/untrusted-input-spotlighting.md +1 -1
  81. package/docs/guidelines/redundancy-taxonomy.md +28 -0
  82. package/llms.txt +14 -14
  83. package/package.json +1 -1
  84. package/src/agent-src/templates/scripts/telemetry/mcp_lite.ts +213 -0
  85. package/src/agent-src/templates/scripts/telemetry/report_renderer.ts +16 -0
  86. package/src/agent-src/templates/scripts/telemetry_report.ts +22 -2
  87. package/src/config/assurance-capability-registry.json +9 -0
  88. package/src/config/gate-coverage.yml +55 -0
  89. package/src/config/gate-violation-baselines.json +18 -6
  90. package/src/config/release-gate-locality.yml +4 -3
  91. package/src/scripts/_cli/cmd_session_recycle.ts +51 -3
  92. package/src/scripts/_lib/audit_field_provenance.ts +44 -3
  93. package/src/scripts/_lib/blocked_by_marker.ts +189 -0
  94. package/src/scripts/_lib/host_capability.ts +76 -1
  95. package/src/scripts/_lib/install_reach_checks.ts +33 -1
  96. package/src/scripts/_lib/mcp_bridge.ts +222 -10
  97. package/src/scripts/_lib/mcp_consent_residual.ts +173 -0
  98. package/src/scripts/_lib/orchestration_record.ts +23 -1
  99. package/src/scripts/_lib/recycle_envelope_paths.ts +190 -0
  100. package/src/scripts/_lib/release_highlights.ts +19 -211
  101. package/src/scripts/_lib/release_material.ts +27 -77
  102. package/src/scripts/_lib/release_position.ts +96 -0
  103. package/src/scripts/_lib/review_skipped_record.ts +17 -1
  104. package/src/scripts/_lib/security_lint.ts +179 -7
  105. package/src/scripts/_lib/self_repair.ts +141 -6
  106. package/src/scripts/_lib/self_repair_store.ts +273 -7
  107. package/src/scripts/_lib/structured_ask.ts +117 -0
  108. package/src/scripts/_lib/subagent_capsule.ts +155 -3
  109. package/src/scripts/_lib/tty_prompt.ts +76 -0
  110. package/src/scripts/ai_council/api_transport.ts +163 -0
  111. package/src/scripts/ai_council/clients.ts +58 -63
  112. package/src/scripts/ai_council/config.ts +13 -0
  113. package/src/scripts/ai_council/content_ceiling.ts +79 -0
  114. package/src/scripts/ai_council/jury_aggregate.ts +178 -0
  115. package/src/scripts/ai_council/seat_policy.ts +137 -0
  116. package/src/scripts/ai_council/status_surface.ts +79 -0
  117. package/src/scripts/ai_council/transport_resolver.ts +59 -1
  118. package/src/scripts/ai_tells_rules.ts +446 -5
  119. package/src/scripts/ask_block_census.ts +499 -0
  120. package/src/scripts/bench_humanizer_eval.ts +264 -40
  121. package/src/scripts/bench_prose_tells_fp.ts +198 -0
  122. package/src/scripts/build_rule_trigger_matrix.ts +5 -0
  123. package/src/scripts/check_branch_work_committed.ts +234 -0
  124. package/src/scripts/check_mcp_doc_drift.ts +217 -0
  125. package/src/scripts/check_pack_size.ts +321 -3
  126. package/src/scripts/check_release_highlights.ts +0 -157
  127. package/src/scripts/council_cli.ts +18 -27
  128. package/src/scripts/detect_ai_tells.ts +142 -20
  129. package/src/scripts/handoff_context_hook.ts +54 -7
  130. package/src/scripts/harvest_reference_tokens.ts +698 -0
  131. package/src/scripts/hook_manifest.json +1 -1
  132. package/src/scripts/hook_manifest.yaml +31 -1
  133. package/src/scripts/hooks/concern_registry.ts +2 -0
  134. package/src/scripts/hooks/delegation_nudge_hook.ts +45 -1
  135. package/src/scripts/hooks/one_question_per_ask_hook.ts +191 -0
  136. package/src/scripts/hooks/session_eol_hook.ts +18 -7
  137. package/src/scripts/install-hooks.sh +52 -0
  138. package/src/scripts/install.ts +4 -4
  139. package/src/scripts/lint_agent_security.ts +316 -25
  140. package/src/scripts/lint_confusables.ts +42 -6
  141. package/src/scripts/lint_handoffs.ts +0 -132
  142. package/src/scripts/lint_hidden_unicode.ts +49 -7
  143. package/src/scripts/lint_instruction_smuggling.ts +42 -6
  144. package/src/scripts/lint_mcp_config_security.ts +42 -6
  145. package/src/scripts/lint_roadmap_blockers.ts +51 -1
  146. package/src/scripts/lint_roadmap_later_disposition.ts +217 -27
  147. package/src/scripts/lint_skill_frontmatter_safety.ts +43 -7
  148. package/src/scripts/mcp_server/resources.ts +41 -0
  149. package/src/scripts/mcp_server/telemetry.ts +19 -4
  150. package/src/scripts/migrate_command_suggestions.ts +1 -1
  151. package/src/scripts/orchestration_record.ts +12 -1
  152. package/src/scripts/probe_unblocked_ask.ts +135 -2
  153. package/src/scripts/release.ts +50 -95
  154. package/src/scripts/release_drill.ts +6 -10
  155. package/src/scripts/release_env.ts +1 -0
  156. package/src/scripts/release_publication.ts +40 -31
  157. package/src/scripts/self_repair_cli.ts +31 -1
  158. package/src/scripts/self_review_gate.ts +409 -15
  159. package/src/scripts/skill_scout.ts +69 -0
  160. package/dist/cli-delegate/chunk-2GNWGMAL.js +0 -172
  161. package/dist/cli-delegate/chunk-LPPWO5CE.js +0 -161
@@ -23,6 +23,7 @@
23
23
  */
24
24
 
25
25
  import { execFileSync } from 'node:child_process';
26
+ import { createHash } from 'node:crypto';
26
27
  import * as fs from 'node:fs';
27
28
  import * as path from 'node:path';
28
29
  import { fileURLToPath } from 'node:url';
@@ -206,6 +207,220 @@ export function classifyPayload(files: readonly PackFile[]): string[] {
206
207
  return out;
207
208
  }
208
209
 
210
+ /**
211
+ * Payload TYPE classification — what an entry IS, not where it sits.
212
+ *
213
+ * `CONTENT_CLASSES` above matches PATHS. That catches the shapes with a naming
214
+ * convention (a `.pem`, a `.test.js`, a `.vscode/` directory) and is blind to
215
+ * everything else: a compiled binary named `helper`, an archive named
216
+ * `data.md`, an extensionless script nobody looked at. Extension is the
217
+ * attacker's choice; the first bytes are not.
218
+ *
219
+ * Priority is archive > binary > dotfile > no-extension > text, so each entry
220
+ * lands in exactly one class and a count is a partition of the payload.
221
+ */
222
+ export type PayloadType = 'text' | 'dotfile' | 'no-extension' | 'binary' | 'archive';
223
+
224
+ export const PAYLOAD_TYPES: readonly PayloadType[] = [
225
+ 'archive',
226
+ 'binary',
227
+ 'dotfile',
228
+ 'no-extension',
229
+ 'text',
230
+ ];
231
+
232
+ /** Leading magic bytes for the archive container formats worth naming. */
233
+ const ARCHIVE_MAGIC: ReadonlyArray<readonly number[]> = [
234
+ [0x50, 0x4b, 0x03, 0x04], // zip / jar / docx / xlsx
235
+ [0x1f, 0x8b], // gzip
236
+ [0x42, 0x5a, 0x68], // bzip2
237
+ [0xfd, 0x37, 0x7a, 0x58, 0x5a, 0x00], // xz
238
+ [0x04, 0x22, 0x4d, 0x18], // lz4
239
+ [0x28, 0xb5, 0x2f, 0xfd], // zstd
240
+ [0x37, 0x7a, 0xbc, 0xaf, 0x27, 0x1c], // 7z
241
+ [0x52, 0x61, 0x72, 0x21], // rar
242
+ ];
243
+
244
+ /** How many leading bytes are read to decide. */
245
+ export const MAGIC_BYTES_READ = 512;
246
+
247
+ function startsWith(buf: Uint8Array, sig: readonly number[]): boolean {
248
+ if (buf.length < sig.length) return false;
249
+ return sig.every((b, i) => buf[i] === b);
250
+ }
251
+
252
+ /**
253
+ * Classify one entry from its name and its first bytes.
254
+ *
255
+ * `head` is null when the file could not be read. That is NOT text: an entry
256
+ * the check could not open is an entry it did not classify, and the caller
257
+ * treats it as a failure rather than letting it fall through to the class that
258
+ * needs no exception.
259
+ */
260
+ export function classifyEntry(entryPath: string, head: Uint8Array | null): PayloadType | 'unreadable' {
261
+ if (head === null) return 'unreadable';
262
+ if (ARCHIVE_MAGIC.some((sig) => startsWith(head, sig))) return 'archive';
263
+ if (head.length > 0) {
264
+ // A NUL byte is decisive. Short of that, a high proportion of C0
265
+ // control bytes outside tab/newline/CR is what separates a compiled
266
+ // object from prose that happens to contain an unusual character.
267
+ if (head.includes(0)) return 'binary';
268
+ let control = 0;
269
+ for (const b of head) {
270
+ if (b < 0x09 || (b > 0x0d && b < 0x20)) control += 1;
271
+ }
272
+ if (control / head.length > 0.1) return 'binary';
273
+ }
274
+ const base = entryPath.split('/').pop() ?? entryPath;
275
+ if (base.startsWith('.')) return 'dotfile';
276
+ if (!base.includes('.')) return 'no-extension';
277
+ return 'text';
278
+ }
279
+
280
+ /**
281
+ * An entry admitted into a class that would otherwise be empty.
282
+ *
283
+ * BOUND, not allow-listed, and the distinction is the whole point. A count
284
+ * ratchet ("8 extensionless entries are fine") is satisfied by ANY eight
285
+ * extensionless entries, so swapping one for something else passes silently —
286
+ * the same key-lookup weakness the Phase 5 pragma work removed one layer up.
287
+ * Here the binding is `sha256(path + "\n" + size)`: change the file and the
288
+ * exception stops applying, which re-fires the check on the entry that changed.
289
+ *
290
+ * NOT a `*_allowlist.json`. There is no file to grow, each entry states its own
291
+ * reason next to the class it excepts, and adding one is a reviewed code change
292
+ * in the gate — the same shape `CONTENT_CLASSES` already uses for its ratchets.
293
+ *
294
+ * Recompute a fingerprint with:
295
+ * ./scripts-run src/scripts/check_pack_size (the failure names path and size)
296
+ */
297
+ export interface BoundPayloadException {
298
+ readonly fingerprint: string;
299
+ readonly path: string;
300
+ readonly size: number;
301
+ readonly why: string;
302
+ }
303
+
304
+ export function payloadFingerprint(entryPath: string, size: number): string {
305
+ return createHash('sha256').update(`${entryPath}\n${String(size)}`).digest('hex');
306
+ }
307
+
308
+ /**
309
+ * Measured on the BUILT payload, 2026-09-07: 3041 entries, of which 3026 are
310
+ * text and these 15 are not. `archive` is empty and stays a hard zero.
311
+ *
312
+ * The roadmap step that added this expected `binary` to be zero too. It is not:
313
+ * three 69-byte PNG placeholders ship as media-adapter fixtures. Recorded as a
314
+ * correction rather than resolved by redefining the class — they are genuinely
315
+ * binary, they genuinely ship, and dropping them from the published surface is
316
+ * a consumer-visible change that belongs to whoever owns those adapters.
317
+ */
318
+ const BOUND_PAYLOAD_EXCEPTIONS: readonly BoundPayloadException[] = [
319
+ { fingerprint: '955b5da399e14fb39cfe1f3279044061043e64e2bd1576871514db927e56d701', path: 'src/scripts/media/lib/fixtures/flux/asset-0001.png', size: 69, why: 'media-adapter response fixture — a 69-byte placeholder PNG the flux adapter test decodes; binary by construction' },
320
+ { fingerprint: 'c2dd9743367ece8ffe0deef5e798d39967c22245749fd7f798edd8aa78b70f59', path: 'src/scripts/media/lib/fixtures/gemini-image/asset-0001.png', size: 69, why: 'media-adapter response fixture — same class as the flux placeholder' },
321
+ { fingerprint: 'cbac44d021a1dc41b938e798eb9be4147fc9d35194c348dfe9acaa0596bc83a5', path: 'src/scripts/media/lib/fixtures/ideogram/asset-0001.png', size: 69, why: 'media-adapter response fixture — same class as the flux placeholder' },
322
+ { fingerprint: '47e4328e71652a0375ade7eb4dac8a3e4b6df1c6e4e74caa75287266676cc9a6', path: 'agents/templates/.ai-council.yml.example', size: 30561, why: 'shipped council config TEMPLATE — a dotfile because the file it is copied to is one. RE-PINNED 2026-09-07, 29,872 -> 30,561 bytes: the exception is path-AND-size-bound by design, so this branch adding a `disabled_reason` honest-null block to the gemini seat in the template invalidated it and the gate refused the entry as unaccounted. The re-pin excepts the SAME path for the SAME reason at its measured new size — it does not widen the class, raise a cap, or except anything the gate was not already carrying.' },
323
+ { fingerprint: 'd36a53951c184db7481139235056e90b53a853714b3a68b31ba1349ecb95ac1d', path: 'agents/templates/.ai-video.xml.example', size: 9209, why: 'shipped video-provider config template — same reason as the council template' },
324
+ { fingerprint: 'f83229412cf8cceaadd0cb1934140a0c7c8277df43edc590f370572f6df6da98', path: 'dist/agent-src/templates/agents/.gitattributes.fragment', size: 2140, why: 'gitattributes fragment the installer appends to a consumer repo' },
325
+ { fingerprint: 'a4f51dba3a66c0f2c3548e77961271ed4d7b5ec8a6661ea359ff044758f0d921', path: 'src/templates/minimal/.agent-settings.yml', size: 1042, why: 'the minimal-install settings template; the dot is the consumer-side filename' },
326
+ { fingerprint: 'a9cb828c7f3e6ac62d3c55c2fd215779307bf695a065498b1d1e1188210ebea8', path: 'LICENSE', size: 1064, why: 'the licence npm requires in the tarball; extensionless by convention' },
327
+ { fingerprint: 'e89a3d025a838e295838db68d0d832c701eeca01bb7e6c3983d3a48950f80017', path: 'NOTICE', size: 472, why: 'attribution notice shipped with the licence; extensionless by convention' },
328
+ { fingerprint: 'c9da3c6e2476a6d2b9610a1fbf638ba9bb78ddd76fc0f799631d601774c32cab', path: 'dist/agent-src/templates/hooks/pre-commit-frontmatter', size: 2537, why: 'git hook template — git requires a hook file to carry no extension' },
329
+ { fingerprint: 'aeffa474df3f805de07cdc9c6960c4918aa8e51e317fa1c30906d2f0267b7a16', path: 'dist/agent-src/templates/hooks/pre-commit-roadmap-progress', size: 3988, why: 'git hook template — same reason' },
330
+ { fingerprint: '914f9c34551c5f4c42a70cd8bb0cd7b73078b0c4e7815b551833a3c64c8476d3', path: 'src/scripts/agent-config', size: 2391, why: 'the CLI entry script; an executable entry point carries no extension by convention' },
331
+ { fingerprint: '0bb1cdd48ac2e9b6a2a56cde25de9fc221d75d42683680284c43118cff796e92', path: 'src/scripts/hooks/shims/php', size: 4439, why: 'shim invoked as the command name `php`; an extension would break the lookup' },
332
+ { fingerprint: 'b14bdd3cd589848ad8e6d7e5f4ee633dc9005a2a97abba327a9426ce556714da', path: 'src/scripts/install', size: 22049, why: 'the installer entry script; same reason as the CLI entry' },
333
+ { fingerprint: '1fb564019de468368e34941dffd5eb8bc994364313e02f476ff354ac0da464d7', path: 'src/templates/minimal/overrides-gitkeep', size: 164, why: 'placeholder that becomes .gitkeep on install; extensionless on purpose' },
334
+ ];
335
+
336
+ /** Read the first bytes of a payload entry from the tree it was packed from. */
337
+ function readHead(entryPath: string, root: string): Uint8Array | null {
338
+ try {
339
+ const fd = fs.openSync(path.join(root, entryPath), 'r');
340
+ try {
341
+ const buf = Buffer.alloc(MAGIC_BYTES_READ);
342
+ const read = fs.readSync(fd, buf, 0, MAGIC_BYTES_READ, 0);
343
+ return buf.subarray(0, read);
344
+ } finally {
345
+ fs.closeSync(fd);
346
+ }
347
+ } catch {
348
+ return null;
349
+ }
350
+ }
351
+
352
+ export interface TypeClassification {
353
+ readonly counts: Record<PayloadType | 'unreadable', number>;
354
+ /** Entries in a non-`text` class with no bound exception. */
355
+ readonly unbound: Array<{ path: string; size: number; type: PayloadType | 'unreadable'; fingerprint: string }>;
356
+ /** Bound exceptions matching no entry in this payload. */
357
+ readonly unusedExceptions: string[];
358
+ }
359
+
360
+ /**
361
+ * Classify the whole payload by type and report what is not accounted for.
362
+ *
363
+ * Every entry outside `text` needs a bound exception. `archive` has none and is
364
+ * meant to stay that way; `binary` has three, named; the dotfile and
365
+ * extensionless entries are each bound to a path AND a size, so replacing one
366
+ * with different content re-fires the check on that entry rather than passing
367
+ * inside a count.
368
+ */
369
+ export function classifyPayloadTypes(
370
+ files: readonly PackFile[],
371
+ root = REPO_ROOT,
372
+ head: (p: string) => Uint8Array | null = (p) => readHead(p, root),
373
+ ): TypeClassification {
374
+ const counts: Record<PayloadType | 'unreadable', number> = {
375
+ archive: 0,
376
+ binary: 0,
377
+ dotfile: 0,
378
+ 'no-extension': 0,
379
+ text: 0,
380
+ unreadable: 0,
381
+ };
382
+ const byFingerprint = new Map(BOUND_PAYLOAD_EXCEPTIONS.map((e) => [e.fingerprint, e]));
383
+ const used = new Set<string>();
384
+ const unbound: TypeClassification['unbound'] = [];
385
+ for (const f of files) {
386
+ const type = classifyEntry(f.path, head(f.path));
387
+ counts[type] += 1;
388
+ if (type === 'text') continue;
389
+ const fingerprint = payloadFingerprint(f.path, f.size);
390
+ if (byFingerprint.has(fingerprint)) {
391
+ used.add(fingerprint);
392
+ continue;
393
+ }
394
+ unbound.push({ path: f.path, size: f.size, type, fingerprint });
395
+ }
396
+ return {
397
+ counts,
398
+ unbound,
399
+ unusedExceptions: BOUND_PAYLOAD_EXCEPTIONS.filter((e) => !used.has(e.fingerprint)).map((e) => e.path),
400
+ };
401
+ }
402
+
403
+ /** One line naming every type class and its count. */
404
+ export function payloadTypeLine(c: TypeClassification): string {
405
+ const parts = [...PAYLOAD_TYPES, 'unreadable' as const]
406
+ .filter((t) => t !== 'unreadable' || c.counts.unreadable > 0)
407
+ .map((t) => `${t} ${String(c.counts[t])}`);
408
+ return `${parts.join(' · ')} — ${String(c.unbound.length)} unaccounted`;
409
+ }
410
+
411
+ /** Violations from the type classification, as human-readable lines. */
412
+ export function typeClassViolations(c: TypeClassification): string[] {
413
+ return c.unbound.map(
414
+ (u) =>
415
+ `payload type \`${u.type}\`: ${u.path} (${String(u.size)} bytes) is not accounted for. ` +
416
+ (u.type === 'archive' || u.type === 'binary'
417
+ ? 'This class is meant to be empty. '
418
+ : 'Every dotfile and extensionless entry is carried by a path-and-size-bound exception. ') +
419
+ `If it belongs in the published surface, add a BoundPayloadException with ` +
420
+ `fingerprint sha256:${u.fingerprint} and a reason; otherwise exclude it from package.json files[].`,
421
+ );
422
+ }
423
+
209
424
  /** Per-class counts, for the green-path report. */
210
425
  export function payloadClassCounts(
211
426
  files: readonly PackFile[],
@@ -377,8 +592,8 @@ function selfTest(): number {
377
592
  Array.from({ length: n }, (_, i) => `dist/hooks/chunk-${String(i)}.js.map`);
378
593
  return runSelfTest({
379
594
  gate: 'check_pack_size',
380
- minCases: 9,
381
- minRejectCases: 5,
595
+ minCases: 14,
596
+ minRejectCases: 9,
382
597
  cases: [
383
598
  {
384
599
  name: 'credential-shaped: a .pem in the payload is refused',
@@ -410,6 +625,95 @@ function selfTest(): number {
410
625
  expect: 'accept',
411
626
  run: () => (classifyPayload(builtPayload(...maps(120))).length > 0 ? 1 : 0),
412
627
  },
628
+ {
629
+ // MAGIC BYTES, NOT THE EXTENSION. This is the whole reason the
630
+ // type pass exists beside the path pass above: an archive named
631
+ // `.md` is invisible to every path pattern in CONTENT_CLASSES.
632
+ name: 'type/archive: a zip planted under a .md name is classified archive, not text',
633
+ expect: 'reject',
634
+ run: () =>
635
+ typeClassViolations(
636
+ classifyPayloadTypes(packFiles('src/skills/x/SKILL.md'), REPO_ROOT, () =>
637
+ Uint8Array.from([0x50, 0x4b, 0x03, 0x04, 0x14, 0x00]),
638
+ ),
639
+ ).length > 0
640
+ ? 1
641
+ : 0,
642
+ },
643
+ {
644
+ name: 'type/archive: the SAME path with ordinary text is classified text and passes',
645
+ expect: 'accept',
646
+ run: () =>
647
+ typeClassViolations(
648
+ classifyPayloadTypes(packFiles('src/skills/x/SKILL.md'), REPO_ROOT, () =>
649
+ new TextEncoder().encode('# An ordinary skill\n'),
650
+ ),
651
+ ).length > 0
652
+ ? 1
653
+ : 0,
654
+ },
655
+ {
656
+ name: 'type/binary: a NUL-bearing entry is refused however it is named',
657
+ expect: 'reject',
658
+ run: () =>
659
+ typeClassViolations(
660
+ classifyPayloadTypes(packFiles('src/scripts/helper.ts'), REPO_ROOT, () =>
661
+ Uint8Array.from([0x7f, 0x45, 0x4c, 0x46, 0x02, 0x00, 0x00]),
662
+ ),
663
+ ).length > 0
664
+ ? 1
665
+ : 0,
666
+ },
667
+ {
668
+ name: 'type/extensionless: an unbound extensionless entry is refused',
669
+ expect: 'reject',
670
+ run: () =>
671
+ typeClassViolations(
672
+ classifyPayloadTypes(packFiles('src/scripts/newtool'), REPO_ROOT, () =>
673
+ new TextEncoder().encode('#!/bin/sh\n'),
674
+ ),
675
+ ).length > 0
676
+ ? 1
677
+ : 0,
678
+ },
679
+ {
680
+ // The property a COUNT ratchet cannot have. LICENSE is bound at
681
+ // its measured size; the same path at a different size is a
682
+ // different artifact and the exception stops applying.
683
+ name: 'type/binding: a bound entry whose SIZE changed is refused again',
684
+ expect: 'reject',
685
+ run: () =>
686
+ typeClassViolations(
687
+ classifyPayloadTypes([{ path: 'LICENSE', size: 999999 }], REPO_ROOT, () =>
688
+ new TextEncoder().encode('MIT\n'),
689
+ ),
690
+ ).length > 0
691
+ ? 1
692
+ : 0,
693
+ },
694
+ {
695
+ name: 'type/binding: that same entry at its recorded size passes',
696
+ expect: 'accept',
697
+ run: () =>
698
+ typeClassViolations(
699
+ classifyPayloadTypes([{ path: 'LICENSE', size: 1064 }], REPO_ROOT, () =>
700
+ new TextEncoder().encode('MIT\n'),
701
+ ),
702
+ ).length > 0
703
+ ? 1
704
+ : 0,
705
+ },
706
+ {
707
+ // An entry the check could not open is one it did not classify.
708
+ name: 'type/unreadable: an entry that cannot be read is refused, never assumed text',
709
+ expect: 'reject',
710
+ run: () =>
711
+ typeClassViolations(
712
+ classifyPayloadTypes(packFiles('src/scripts/gone.ts'), REPO_ROOT, () => null),
713
+ ).length > 0
714
+ ? 1
715
+ : 0,
716
+ },
413
717
  {
414
718
  name: 'a plain payload passes — the classes are not firing on everything',
415
719
  expect: 'accept',
@@ -477,7 +781,12 @@ export function main(argv: readonly string[] = process.argv.slice(2)): number {
477
781
  // still knows the population it was red over.
478
782
  process.stdout.write(`scanned: ${String(pack.files.length)}\n`);
479
783
 
480
- const errors = [...evaluate(budget, pack), ...classifyPayload(pack.files)];
784
+ const types = classifyPayloadTypes(pack.files);
785
+ const errors = [
786
+ ...evaluate(budget, pack),
787
+ ...classifyPayload(pack.files),
788
+ ...typeClassViolations(types),
789
+ ];
481
790
  const { perSkill, total } = skillBytes(pack.files);
482
791
  if (argv.includes('--json')) {
483
792
  process.stdout.write(
@@ -485,6 +794,15 @@ export function main(argv: readonly string[] = process.argv.slice(2)): number {
485
794
  );
486
795
  return errors.length > 0 ? 1 : 0;
487
796
  }
797
+ // Printed BEFORE the verdict and on EVERY path, red included. The counts are
798
+ // what a reader needs to judge a violation: "one unaccounted binary" reads
799
+ // very differently against a payload of 3 binaries than against one of 0.
800
+ process.stdout.write(` payload types: ${payloadTypeLine(types)}\n`);
801
+ for (const unused of types.unusedExceptions) {
802
+ // Not an error: an unbuilt tree legitimately packs fewer files. Named
803
+ // so a stale exception is visible rather than accumulating unread.
804
+ process.stdout.write(` bound exception matched no entry: ${unused}\n`);
805
+ }
488
806
  if (errors.length > 0) {
489
807
  for (const e of errors) process.stderr.write(`❌ pack size: ${e}\n`);
490
808
  return 1;
@@ -71,18 +71,12 @@ import {
71
71
  collect_span_commits,
72
72
  derive_categories,
73
73
  parse_git_log,
74
- mix_response_blockers,
75
- previous_promise,
76
74
  previous_release_tag,
77
- promise_readback_blockers,
78
75
  stale_draft_labels,
79
76
  } from './_lib/release_highlights.js';
80
- import { loadTaxonomy, measureRange } from './measure_release_mix.js';
81
77
  import {
82
78
  CURATED_HEAD_INSTRUCTION,
83
- PROMISE_READBACK_MARKER,
84
79
  extract_changelog_section,
85
- previous_changelog_version,
86
80
  } from './_lib/release_material.js';
87
81
  import { assertScanned, assertWatchlistResolves, DeadScopeError } from './_lib/scan_scope.js';
88
82
 
@@ -244,23 +238,6 @@ export function main(argv: readonly string[]): number {
244
238
  );
245
239
  return 1;
246
240
  }
247
- // Hoisted above the curated-head branches on purpose. It used to sit inside
248
- // `contradictions.length === 0`, so CI reached it only for a section that
249
- // HAS a head, carries no unrewritten draft and contradicts nothing — while
250
- // the local guards call the same predicate unconditionally. A section
251
- // without a curated head was therefore refused locally and waved through by
252
- // CI: one predicate, two different gatekeepers, which is the drift this
253
- // change exists to remove.
254
- const mix = check_governance_mix_response(section.body, from, to, version);
255
- if (mix !== 0) {
256
- return mix;
257
- }
258
-
259
- const readback = check_previous_promise_readback(changelogText, section.body, version);
260
- if (readback !== 0) {
261
- return readback;
262
- }
263
-
264
241
  const curated = parse_curated_head(section.body);
265
242
  if (!curated) {
266
243
  process.stdout.write(`ℹ️ no curated head in the ${version} section — nothing to check\n`);
@@ -356,140 +333,6 @@ export function main(argv: readonly string[]): number {
356
333
  return 1;
357
334
  }
358
335
 
359
- /**
360
- * The governance-versus-product response, checked for PRESENCE, never for the
361
- * ratio (ADR-253).
362
- *
363
- * `measure_release_mix` publishes a level. This is the one place that level has
364
- * a consequence, and the consequence is deliberately about completeness of the
365
- * written answer: when governance-only commits strictly outnumber consumer-only
366
- * commits over the release span, the section under release must carry a written
367
- * response naming either the next cycle's consumer work or a maintainer
368
- * justification. No number is enforced, and no number can be — both council
369
- * seats refused a threshold on fewer than two readings, and a gate with no
370
- * threshold that blocks on the ratio is a smuggled threshold.
371
- *
372
- * The response lives OUTSIDE the curated head, as its own `> **Governance
373
- * mix:**` line, so it does not consume the head's ten-line cap and does not
374
- * become a sixth label every historical section would suddenly lack.
375
- *
376
- * Scope matches every other check here: only the section under release is read.
377
- *
378
- * A measurement failure DEGRADES to a printed warning rather than to a refusal.
379
- * This is a governance signal, not a correctness or security control, and a
380
- * shallow clone or a missing tag on the release path would otherwise turn an
381
- * unrelated environment fact into a blocked release.
382
- */
383
- export function check_governance_mix_response(
384
- body: string,
385
- from: string,
386
- to: string,
387
- version: string,
388
- ): number {
389
- let reading;
390
- try {
391
- reading = measureRange(from, to, loadTaxonomy(), version, REPO_ROOT);
392
- } catch (err) {
393
- process.stdout.write(
394
- `⚠️ governance mix not measured for ${version}: ${(err as Error).message}\n`,
395
- );
396
- return 0;
397
- }
398
- const o = reading.response_obligation;
399
- const level = `governance-only ${o.governance_only} vs consumer-only ${o.consumer_only} (taxonomy ${reading.taxonomy_version})`;
400
- if (!o.triggered) {
401
- process.stdout.write(`✅ governance mix for ${version}: ${level} — no response owed\n`);
402
- return 0;
403
- }
404
- // ONE predicate, shared with `guard_release_branch_push` — see
405
- // `mix_response_blockers`. This side prints and exits; the local side dies
406
- // before anything leaves the machine. Neither owns the rule.
407
- const blockers = mix_response_blockers(body, version, `\`release/${version}\``, {
408
- triggered: true,
409
- level,
410
- });
411
- if (blockers.length === 0) {
412
- process.stdout.write(`✅ governance mix for ${version}: ${level} — response present\n`);
413
- return 0;
414
- }
415
- process.stderr.write(
416
- `❌ the ${version} section owes a governance-versus-product response: ${level}.\n` +
417
- blockers.map((b) => ` - ${b}\n`).join('') +
418
- ' Governance-only commits outnumber consumer-only commits over the release span, so\n' +
419
- ' the section carries a written response naming either the next cycle\'s consumer work\n' +
420
- ' or a maintainer justification (docs/contracts/CHANGELOG-conventions.md § Governance-\n' +
421
- ' versus-product response; the decline this replaces is ADR-253).\n' +
422
- ' Only that section is read; historical sections are out of scope. No ratio is\n' +
423
- ' enforced — this blocks a MISSING answer, never a particular number.\n' +
424
- ' Reproduce locally, before any push:\n' +
425
- ` ./scripts-run src/scripts/check_release_highlights --version ${version}\n`,
426
- );
427
- return 1;
428
- }
429
-
430
- /**
431
- * The previous release's promise, read back at the next release.
432
- *
433
- * `_lib/release_material.ts` generates `Next cycle ships …` and, until this
434
- * check existed, nothing ever asked what became of it: 14.18.0 promised the MCP
435
- * bridge repair, and no gate in the tree would have noticed the same promise
436
- * being reprinted in 14.19.0 and 14.20.0. A commitment nothing reads back is
437
- * free to restate, and one that is free to restate is not a commitment.
438
- *
439
- * Only the section under release is read, and only the previous section's
440
- * governance-response block — the same scope every other check here uses, so a
441
- * historical section cannot red a future release.
442
- *
443
- * The check is about ANSWERING, never about having kept the promise. A gate
444
- * that refused an unkept promise would price honesty above silence and teach
445
- * the next author to promise nothing.
446
- */
447
- export function check_previous_promise_readback(
448
- changelogText: string,
449
- body: string,
450
- version: string,
451
- ): number {
452
- const previousVersion = previous_changelog_version(changelogText, version);
453
- if (previousVersion === null) {
454
- process.stdout.write(`✅ no previous CHANGELOG section before ${version} — no promise to read back\n`);
455
- return 0;
456
- }
457
- const promise = previous_promise(changelogText, previousVersion);
458
- if (promise === null) {
459
- process.stdout.write(
460
- `✅ the ${previousVersion} section made no next-cycle promise — nothing owed by ${version}\n`,
461
- );
462
- return 0;
463
- }
464
- const blockers = promise_readback_blockers(
465
- body,
466
- version,
467
- previousVersion,
468
- promise,
469
- `\`release/${version}\``,
470
- );
471
- if (blockers.length === 0) {
472
- process.stdout.write(`✅ ${version} answers the ${previousVersion} next-cycle promise\n`);
473
- return 0;
474
- }
475
- process.stderr.write(
476
- `❌ the ${version} section owes an answer to the ${previousVersion} next-cycle promise.\n` +
477
- blockers.map((b) => ` - ${b}\n`).join('') +
478
- ` The ${previousVersion} promise reads:\n` +
479
- promise
480
- .split('\n')
481
- .map((l) => ` ${l}\n`)
482
- .join('') +
483
- ` Answering it is one blockquote line under the curated head, e.g.\n` +
484
- ` > ${PROMISE_READBACK_MARKER} the <promise> did not ship; <what happened>.\n` +
485
- ' Shipped, did not ship, or withdrawn with a reason — the check refuses a MISSING\n' +
486
- ' answer, never a particular outcome. An unkept promise answered honestly passes.\n' +
487
- ' Reproduce locally, before any push:\n' +
488
- ` ./scripts-run src/scripts/check_release_highlights --version ${version}\n`,
489
- );
490
- return 1;
491
- }
492
-
493
336
  const _isMain = (() => {
494
337
  const entry = process.argv[1];
495
338
  if (!entry) return false;
@@ -78,8 +78,11 @@ import {
78
78
  } from './ai_council/config.js';
79
79
  import { AuthCache, select_solo_member } from './ai_council/solo_dispatch.js';
80
80
  import { InvalidModeError, resolve_global_mode } from './ai_council/modes.js';
81
- import { resolveMemberTransport } from './ai_council/transport_resolver.js';
82
- import { classifyCliFailure, type AbsentReason } from './ai_council/transport_resolver.js';
81
+ import { classifyCliFailure, resolveMemberTransport, type AbsentReason } from './ai_council/transport_resolver.js';
82
+ import {
83
+ disabledSeats as _disabledSeats,
84
+ resolvedTransportFor as _resolvedTransportFor,
85
+ } from './ai_council/status_surface.js';
83
86
  import { evaluateQuorum, type QuorumResult } from './ai_council/quorum.js';
84
87
  import {
85
88
  _emitQuorumEvent,
@@ -2163,31 +2166,6 @@ function _emit_shadow_slo_banner(): void {
2163
2166
  * leaves nothing to infer. Exit is always 0 — this reports a state, it does not
2164
2167
  * gate — and the verdict line is machine-greppable in both directions.
2165
2168
  */
2166
- /**
2167
- * Resolve the concrete transport a member would use on THIS machine right now.
2168
- *
2169
- * `cmd_status` exists to answer "is the council reachable, and will it bill me".
2170
- * Since the transport-mode setting was removed, the second half is not a value
2171
- * anyone can look up in the config — it is a per-machine resolution. Printing a
2172
- * configured mode would be the same class of stale answer the removed setting
2173
- * produced. `mode` is pinned to `auto` because the loader no longer emits
2174
- * anything else.
2175
- */
2176
- function _resolvedTransportFor(
2177
- name: string,
2178
- member: { readonly binary: string | null; readonly api_key_ref: string | null },
2179
- report?: EnvironmentReport,
2180
- ): ReturnType<typeof resolveMemberTransport> {
2181
- return resolveMemberTransport({
2182
- provider: name,
2183
- report: report ?? detectEnvironment(),
2184
- invocationMode: null,
2185
- memberSettings: null,
2186
- globalMode: 'auto',
2187
- binaryOverride: member.binary,
2188
- });
2189
- }
2190
-
2191
2169
  /** Gather what `fallbackPostureFor` needs; the decision itself is pure. */
2192
2170
  function _posture(
2193
2171
  name: string,
@@ -2263,6 +2241,9 @@ export function cmd_status(args: Args, opts: { env?: Record<string, string | und
2263
2241
  transport: t.available ? t.transport : null,
2264
2242
  billing: t.available ? t.billing : null,
2265
2243
  reason: t.reason,
2244
+ absent_reason: t.absentReason,
2245
+ policy_exclusion: m.policy_exclusion,
2246
+ content_ceiling: m.content_ceiling,
2266
2247
  },
2267
2248
  ];
2268
2249
  }),
@@ -2273,6 +2254,7 @@ export function cmd_status(args: Args, opts: { env?: Record<string, string | und
2273
2254
  enabledMembers.map(([n, m]) => [n, _posture(n, m)]),
2274
2255
  ),
2275
2256
  },
2257
+ disabled_seats: _disabledSeats(cfg),
2276
2258
  ignored_transport_keys: cfg?.ignored_transport_keys ?? [],
2277
2259
  qualification: qualificationJson(qualifications),
2278
2260
  qualified_members: countableSeats(qualifications),
@@ -2310,6 +2292,15 @@ export function cmd_status(args: Args, opts: { env?: Record<string, string | und
2310
2292
  const billing = t.available ? ` · ${t.billing}` : '';
2311
2293
  const detail = t.available ? t.transport : `unavailable — ${t.reason ?? 'no usable transport'}`;
2312
2294
  _stdout(` transport ${name}: ${detail}${billing}\n`);
2295
+ // 2.1: the machine-readable half of the refusal.
2296
+ if (!t.available && t.absentReason !== null && t.absentReason.startsWith('policy_')) {
2297
+ _stdout(` policy ${name}: ${t.absentReason}\n`);
2298
+ }
2299
+ _stdout(` content ceiling ${name}: ${member.content_ceiling}\n`);
2300
+ }
2301
+ // 1.2: a disabled seat was invisible here — `enabled` members only.
2302
+ for (const [name, reason] of Object.entries(_disabledSeats(cfg))) {
2303
+ _stdout(` disabled seat ${name}: ${reason}\n`);
2313
2304
  }
2314
2305
  for (const line of renderPostureLines(
2315
2306
  enabledMembers.map(([n, m]) => [n, _posture(n, m)] as const),