@event4u/agent-config 16.1.0 → 16.2.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 (217) 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 +237 -248
  5. package/dist/agent-src/commands/roadmap/process-full.md +1 -1
  6. package/dist/agent-src/commands/work.md +4 -0
  7. package/dist/agent-src/contexts/execution/autonomy-mechanics.md +4 -8
  8. package/dist/agent-src/rules/ui-audit-gate.md +1 -2
  9. package/dist/agent-src/skills/accessibility-auditor/data/aria-patterns.csv +1 -1
  10. package/dist/agent-src/skills/accessibility-auditor/data/manifest.json +2 -2
  11. package/dist/agent-src/skills/design-review/SKILL.md +30 -4
  12. package/dist/agent-src/skills/design-review/references/verification-automation.md +32 -0
  13. package/dist/agent-src/skills/existing-ui-audit/SKILL.md +16 -1
  14. package/dist/agent-src/skills/existing-ui-audit/references/component-taxonomy-detection.md +56 -0
  15. package/dist/agent-src/skills/markitdown/SKILL.md +4 -0
  16. package/dist/agent-src/skills/playwright-testing/SKILL.md +30 -6
  17. package/dist/agent-src/skills/ui-component-architect/SKILL.md +23 -0
  18. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  19. package/dist/agent-src/templates/roadmaps.md +30 -0
  20. package/dist/agent-src/templates/scripts/work_engine/directives/ui/app_spec.ts +10 -4
  21. package/dist/agent-src/templates/scripts/work_engine/directives/ui/apply.ts +260 -21
  22. package/dist/agent-src/templates/scripts/work_engine/directives/ui/audit.ts +22 -3
  23. package/dist/agent-src/templates/scripts/work_engine/directives/ui/scaffold.ts +6 -1
  24. package/dist/agent-src/templates/scripts/work_engine/state.ts +57 -7
  25. package/dist/agent-src/templates/scripts/work_engine/taxonomy/detect.ts +500 -0
  26. package/dist/agent-src/templates/scripts/work_engine/taxonomy/place.ts +183 -0
  27. package/dist/cli/python/knowledge_ingest.js +1 -1
  28. package/dist/cli/python/workspace_drive.js +1 -1
  29. package/dist/cli-delegate/{chunk-S3VFXMTM.js → chunk-4XFP2Y3E.js} +1 -1
  30. package/dist/cli-delegate/{chunk-HDVVGPQW.js → chunk-7XGBX6ZH.js} +471 -387
  31. package/dist/cli-delegate/{chunk-GJCBB2UG.js → chunk-FP36SWJD.js} +432 -144
  32. package/dist/cli-delegate/{chunk-UNCRJBT3.js → chunk-IGD6ERX6.js} +31 -0
  33. package/dist/cli-delegate/{chunk-FXYUYU4N.js → chunk-JNMVLWLR.js} +42 -1
  34. package/dist/cli-delegate/{chunk-QJWAUMJW.js → chunk-YJ72UIAK.js} +730 -15
  35. package/dist/cli-delegate/cmd_conformance.js +902 -782
  36. package/dist/cli-delegate/cmd_doctor.js +2 -2
  37. package/dist/cli-delegate/cmd_export.js +2 -2
  38. package/dist/cli-delegate/cmd_handoff.js +4 -4
  39. package/dist/cli-delegate/cmd_memory_get.js +1 -1
  40. package/dist/cli-delegate/cmd_prune.js +2 -2
  41. package/dist/cli-delegate/cmd_route_audit.js +1 -1
  42. package/dist/cli-delegate/cmd_session_recycle.js +2 -2
  43. package/dist/cli-delegate/cmd_settings_migrate.js +2 -2
  44. package/dist/cli-delegate/cmd_settings_set.js +1 -1
  45. package/dist/cli-delegate/cmd_sync.js +2 -2
  46. package/dist/cli-delegate/cmd_uninstall.js +2 -2
  47. package/dist/cli-delegate/cmd_validate.js +2 -2
  48. package/dist/discovery/deprecation-report.md +1 -1
  49. package/dist/discovery/discovery-manifest.json +10 -10
  50. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  51. package/dist/discovery/discovery-manifest.summary.md +1 -1
  52. package/dist/discovery/dormancy-report.md +1 -1
  53. package/dist/discovery/orphan-report.md +1 -1
  54. package/dist/discovery/packs.json +2 -2
  55. package/dist/discovery/trust-report.md +1 -1
  56. package/dist/discovery/workspaces.json +2 -2
  57. package/dist/hooks/dispatch.js +6666 -2581
  58. package/dist/hooks/dispatch.sha256 +1 -0
  59. package/dist/install/install.mjs +848 -733
  60. package/dist/mcp/registry-manifest.json +2 -2
  61. package/dist/mcp/server.json +2 -2
  62. package/dist/mcp/server.mjs +2747 -1510
  63. package/dist/server/schemas/settings.js +23 -0
  64. package/dist/ui/assets/index-Cwq4d0hX.js +23 -0
  65. package/dist/ui/assets/index-Cyjy8bWO.css +1 -0
  66. package/dist/ui/index.html +2 -2
  67. package/docs/contracts/settings-classes.md +6 -4
  68. package/package.json +4 -4
  69. package/src/agent-src/templates/scripts/work_engine/directives/ui/app_spec.ts +10 -4
  70. package/src/agent-src/templates/scripts/work_engine/directives/ui/apply.ts +260 -21
  71. package/src/agent-src/templates/scripts/work_engine/directives/ui/audit.ts +22 -3
  72. package/src/agent-src/templates/scripts/work_engine/directives/ui/scaffold.ts +6 -1
  73. package/src/agent-src/templates/scripts/work_engine/state.ts +57 -7
  74. package/src/agent-src/templates/scripts/work_engine/taxonomy/detect.ts +500 -0
  75. package/src/agent-src/templates/scripts/work_engine/taxonomy/place.ts +183 -0
  76. package/src/config/agent-settings.template.yml +29 -0
  77. package/src/config/ci-local-parity.yml +18 -0
  78. package/src/config/continuity-surface.json +2 -2
  79. package/src/config/gate-coverage.yml +234 -3
  80. package/src/config/gate-violation-baselines.json +7 -3
  81. package/src/config/hook-latency-budget.json +21 -0
  82. package/src/config/hook-token-budget.json +2 -2
  83. package/src/config/metric-registry.yml +114 -0
  84. package/src/config/placeholder-drift-budget.json +10 -0
  85. package/src/config/routing-coverage-seed.json +18 -4
  86. package/src/scripts/_cli/cmd_doctor.ts +5 -19
  87. package/src/scripts/_cli/doctor_execution.ts +100 -17
  88. package/src/scripts/_cli/doctor_network_posture.ts +226 -0
  89. package/src/scripts/_lib/body_portable.ts +102 -0
  90. package/src/scripts/_lib/cc_transcript.ts +132 -1
  91. package/src/scripts/_lib/concern_sla_window.ts +200 -0
  92. package/src/scripts/_lib/forge_protection.ts +114 -0
  93. package/src/scripts/_lib/forge_reader.ts +558 -0
  94. package/src/scripts/_lib/host_env_write.ts +111 -0
  95. package/src/scripts/_lib/llm_proposer_transport.ts +14 -4
  96. package/src/scripts/_lib/obligations.ts +119 -8
  97. package/src/scripts/_lib/probe_inputs.ts +271 -0
  98. package/src/scripts/_lib/read_surface_scan.ts +322 -0
  99. package/src/scripts/_lib/release_highlights.ts +2 -1
  100. package/src/scripts/_lib/retrieval_sanitize.ts +77 -16
  101. package/src/scripts/_lib/shell_write_shapes.ts +108 -0
  102. package/src/scripts/_lib/structural_hiding.ts +622 -0
  103. package/src/scripts/_lib/turn_end_refusals.ts +525 -4
  104. package/src/scripts/_lib/turn_end_transcript.ts +216 -0
  105. package/src/scripts/_lib/verification_command.ts +324 -0
  106. package/src/scripts/_lib/verification_evidence.ts +647 -0
  107. package/src/scripts/_lib/verify_clause.ts +280 -0
  108. package/src/scripts/ai_team/team_dispatch.ts +4 -0
  109. package/src/scripts/before_complete_hook.ts +340 -23
  110. package/src/scripts/bench_hook_latency.ts +412 -5
  111. package/src/scripts/cache_realization_report.ts +61 -1
  112. package/src/scripts/check_argument_hint.ts +287 -0
  113. package/src/scripts/check_claims.ts +125 -1
  114. package/src/scripts/check_enforcement_coverage.ts +38 -9
  115. package/src/scripts/check_enforcement_matrix.ts +245 -3
  116. package/src/scripts/check_finding_dispositions.ts +22 -3
  117. package/src/scripts/check_generator_sync.ts +71 -0
  118. package/src/scripts/check_host_docs_digest.ts +786 -0
  119. package/src/scripts/check_host_format_column.ts +559 -0
  120. package/src/scripts/check_input_references.ts +228 -0
  121. package/src/scripts/check_inputs_parity.ts +235 -0
  122. package/src/scripts/check_kernel_edit_ratified.ts +47 -5
  123. package/src/scripts/check_kill_switch_table.ts +294 -0
  124. package/src/scripts/check_placeholder_drift.ts +221 -0
  125. package/src/scripts/check_read_surface_coverage.ts +515 -0
  126. package/src/scripts/check_release_holds.ts +2 -1
  127. package/src/scripts/check_routing_coverage.ts +378 -11
  128. package/src/scripts/check_static_parity.ts +17 -0
  129. package/src/scripts/ci_settle.ts +145 -19
  130. package/src/scripts/closure_scan.ts +115 -1
  131. package/src/scripts/compile_hook_manifest.ts +60 -16
  132. package/src/scripts/external_sources_denylist.json +29 -4
  133. package/src/scripts/hook_manifest.json +1 -1
  134. package/src/scripts/hook_manifest.yaml +42 -5
  135. package/src/scripts/hooks/block_config_weakening.ts +229 -4
  136. package/src/scripts/hooks/block_kernel_rule_writes.ts +9 -78
  137. package/src/scripts/hooks/block_plumbing_writes.ts +314 -0
  138. package/src/scripts/hooks/bundle_integrity.ts +291 -0
  139. package/src/scripts/hooks/chain_nudge_hook.ts +1 -2
  140. package/src/scripts/hooks/code_graph_context_hook.ts +1 -2
  141. package/src/scripts/hooks/comment_discipline_hook.ts +1 -3
  142. package/src/scripts/hooks/concern_registry.ts +2 -0
  143. package/src/scripts/hooks/concern_timings.ts +94 -0
  144. package/src/scripts/hooks/delegation_nudge_hook.ts +1 -2
  145. package/src/scripts/hooks/design_pass_hook.ts +100 -9
  146. package/src/scripts/hooks/design_slop_hook.ts +1 -2
  147. package/src/scripts/hooks/dispatch_hook.ts +25 -19
  148. package/src/scripts/hooks/edit_shape_hook.ts +1 -2
  149. package/src/scripts/hooks/end_review_nudge_hook.ts +1 -1
  150. package/src/scripts/hooks/evidence_independence.ts +1 -2
  151. package/src/scripts/hooks/exit_codes.ts +128 -0
  152. package/src/scripts/hooks/host_lowering.json +1 -0
  153. package/src/scripts/hooks/host_lowering.ts +128 -3
  154. package/src/scripts/hooks/host_lowering.yaml +262 -46
  155. package/src/scripts/hooks/host_semantics.ts +30 -2
  156. package/src/scripts/hooks/injection_budget.ts +9 -2
  157. package/src/scripts/hooks/interruption_ledger_hook.ts +1 -2
  158. package/src/scripts/hooks/journal_record_hook.ts +1 -2
  159. package/src/scripts/hooks/memory_recall_hook.ts +1 -2
  160. package/src/scripts/hooks/obligation_settle_hook.ts +151 -5
  161. package/src/scripts/hooks/one_question_per_ask_hook.ts +25 -4
  162. package/src/scripts/hooks/orchestration_record_hook.ts +1 -2
  163. package/src/scripts/hooks/permission_gate.ts +1 -2
  164. package/src/scripts/hooks/push_settle_hook.ts +1 -3
  165. package/src/scripts/hooks/replay_hook.ts +2 -1
  166. package/src/scripts/hooks/reread_guard_hook.ts +1 -2
  167. package/src/scripts/hooks/review_baseline_hook.ts +1 -2
  168. package/src/scripts/hooks/rtk_wrap_hook.ts +1 -2
  169. package/src/scripts/hooks/rule_inject_hook.ts +8 -5
  170. package/src/scripts/hooks/run_continuation_hook.ts +13 -15
  171. package/src/scripts/hooks/skill_route_hook.ts +1 -2
  172. package/src/scripts/hooks/source_first_gate_hook.ts +1 -2
  173. package/src/scripts/hooks/spawn_guard_shadow_hook.ts +1 -2
  174. package/src/scripts/hooks/subagent_ledger_hook.ts +1 -2
  175. package/src/scripts/hooks/suggestion_capture_hook.ts +1 -1
  176. package/src/scripts/hooks/table_fingerprint.ts +49 -0
  177. package/src/scripts/hooks/telemetry_flush_hook.ts +1 -2
  178. package/src/scripts/hooks/telemetry_usage_hook.ts +1 -2
  179. package/src/scripts/hooks/tool_result_bytes_hook.ts +93 -9
  180. package/src/scripts/hooks/turn_end_gate_hook.ts +337 -324
  181. package/src/scripts/hooks/ui_route_nudge_hook.ts +1 -2
  182. package/src/scripts/install.ts +3 -4
  183. package/src/scripts/lint_deny_text.ts +419 -0
  184. package/src/scripts/lint_exit_codes.ts +262 -0
  185. package/src/scripts/lint_hook_manifest.ts +55 -0
  186. package/src/scripts/lint_roadmap_blockers.ts +78 -12
  187. package/src/scripts/lint_skill_descriptions.ts +1 -1
  188. package/src/scripts/lint_skill_scripts_readonly.ts +1 -1
  189. package/src/scripts/mcp_server/prompts.ts +142 -1
  190. package/src/scripts/measure_skill_ranker_baseline.ts +305 -51
  191. package/src/scripts/measure_turn_end_gate.ts +193 -0
  192. package/src/scripts/memory_lookup.ts +1 -1
  193. package/src/scripts/prepack-check.mjs +53 -0
  194. package/src/scripts/release_publication.ts +13 -1
  195. package/src/scripts/report_invocation_surface.ts +383 -0
  196. package/src/scripts/report_obligation_carriers.ts +699 -0
  197. package/src/scripts/report_obligation_writer_split.ts +236 -0
  198. package/src/scripts/report_skill_menu_census.ts +208 -0
  199. package/src/scripts/report_standing_payload_by_host.ts +1 -1
  200. package/src/scripts/roadmap_verify_share.ts +213 -0
  201. package/src/scripts/schemas/command.schema.json +44 -0
  202. package/src/scripts/schemas/review-findings.schema.json +4 -0
  203. package/src/scripts/schemas/skill.schema.json +44 -0
  204. package/src/scripts/self_review_gate.ts +359 -10
  205. package/src/scripts/semantic_parity_census.ts +314 -0
  206. package/src/scripts/session_eol_report.ts +54 -1
  207. package/src/scripts/sweep_dead_scan_roots.ts +1 -1
  208. package/src/scripts/trigger_eval_grandfather.json +0 -2
  209. package/src/scripts/trigger_eval_rotation.ts +122 -27
  210. package/src/scripts/ui_conformance_probe.ts +156 -4
  211. package/src/scripts/update_prices.ts +85 -3
  212. package/src/scripts/write_bundle_digest.mjs +74 -0
  213. package/src/server/schemas/settings.ts +31 -0
  214. package/src/templates/consumer-settings/ONBOARDING.md +1 -0
  215. package/dist/ui/assets/index-DH1xc62v.js +0 -29
  216. package/dist/ui/assets/index-jSfMW8_t.css +0 -1
  217. /package/dist/cli-delegate/{chunk-YUM5KURG.js → chunk-7RQP4UDH.js} +0 -0
@@ -0,0 +1,236 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * report_obligation_writer_split — which kind of tree wrote the obligation rows.
4
+ *
5
+ * WHAT QUESTION THIS ANSWERS, AND WHY IT NEEDED A TOOL.
6
+ * `obligation-settle-shadow-bar` is pre-registered against the shadow rows under
7
+ * `agents/runtime/state/obligations/`, and its own clause (7) says that corpus is
8
+ * one machine's runtime state. Until the rows carried a writer, that sentence was
9
+ * the ONLY place the fact lived: a reader holding the ledgers could count rows and
10
+ * could not separate them, so a reading that cleared the floor would have been
11
+ * quotable as evidence about a population nobody had measured. This prints the
12
+ * separation, from the rows themselves.
13
+ *
14
+ * IT REACHES NO VERDICT, AND THAT IS THE DESIGN. A split is an input to the corpus
15
+ * decision held in `road-to-an-obligation-row-that-names-its-writer`, not the
16
+ * decision — the three options there (accept the one-machine corpus and amend the
17
+ * bar, widen it deliberately, or file the window `resolved-null`) are owner-
18
+ * reserved, and all three name arming as owner-reserved too. So this exits 0 on
19
+ * every path, prints counts, and says nothing about whether anything may be armed.
20
+ * A reporter that graded the corpus would be handing the arming discussion a
21
+ * verdict wearing a number's clothes.
22
+ *
23
+ * `absent` IS REPORTED APART FROM `unknown`, and the distinction is load-bearing.
24
+ * `unknown` is a row whose writer WAS resolved and came back unresolvable; `absent`
25
+ * is a row written before the field existed. Folding them together would let the
26
+ * whole pre-field corpus read as "we looked and could not tell", which is a
27
+ * stronger claim than "we had not started looking". The ledger's own `parseRows`
28
+ * maps both to `unknown` — correct for a consumer that only needs a role — so this
29
+ * reads the JSON directly rather than through it, and uses `isWriterRole` for the
30
+ * vocabulary so the two cannot drift.
31
+ *
32
+ * PRIVACY, at the precision the code holds. Output is counts, plus the SCAN ROOT,
33
+ * which the caller supplied and which is the one thing a reader needs in order to
34
+ * know what was measured. Nothing else crosses: no session id, no ledger filename,
35
+ * no rule id, no timestamp. The scan reads those and keeps none of them.
36
+ *
37
+ * WRITES NOTHING. It opens files for reading and creates no directory, not even
38
+ * the ledger directory it looks for. A missing directory is a reported state, not
39
+ * a thing to fix.
40
+ *
41
+ * Exit: 0 always, except a usage error (1). Advisory — it gates nothing.
42
+ */
43
+
44
+ import * as fs from 'node:fs';
45
+ import * as path from 'node:path';
46
+ import process from 'node:process';
47
+ import { fileURLToPath } from 'node:url';
48
+
49
+ import { OBLIGATION_STATE_DIR, WRITER_ROLES, isWriterRole } from './_lib/obligations.js';
50
+
51
+ /** The reported classes: the closed vocabulary, plus the pre-field case. */
52
+ export const REPORTED_CLASSES = [...WRITER_ROLES, 'absent'] as const;
53
+ export type ReportedClass = (typeof REPORTED_CLASSES)[number];
54
+
55
+ /** The three row arrays a ledger carries, named as the file names them. */
56
+ export const ROW_ARRAYS = ['delivered', 'discharged', 'shadow'] as const;
57
+ export type RowArray = (typeof ROW_ARRAYS)[number];
58
+
59
+ export type Tally = Record<ReportedClass, number>;
60
+
61
+ export interface WriterSplit {
62
+ /** The directory scanned, echoed so a reader knows what was measured. */
63
+ readonly store: string;
64
+ /** `false` when the ledger directory does not exist at all. */
65
+ readonly storeExists: boolean;
66
+ /** Ledger files opened and parsed. */
67
+ readonly ledgers: number;
68
+ /** Files present but unreadable or unparseable — reported, never silently dropped. */
69
+ readonly unreadable: number;
70
+ /** Per row array, the split by writer class. */
71
+ readonly byArray: Record<RowArray, Tally>;
72
+ /** Every array folded together. */
73
+ readonly total: Tally;
74
+ }
75
+
76
+ function emptyTally(): Tally {
77
+ return { package: 0, consumer: 0, unknown: 0, absent: 0 };
78
+ }
79
+
80
+ /**
81
+ * Classify one row's writer field.
82
+ *
83
+ * A field that is missing entirely is `absent`. A field that is present but
84
+ * outside the closed vocabulary is `unknown`, matching `parseRows`: a corrupt
85
+ * provenance label degrades, it never deletes the record of a real row.
86
+ */
87
+ export function classifyRow(row: unknown): ReportedClass {
88
+ if (typeof row !== 'object' || row === null) return 'absent';
89
+ if (!('writer' in row)) return 'absent';
90
+ const writer = (row as { writer: unknown }).writer;
91
+ return isWriterRole(writer) ? writer : 'unknown';
92
+ }
93
+
94
+ /** Tally one ledger's arrays into an accumulator. */
95
+ function tallyLedger(doc: unknown, into: Record<RowArray, Tally>): void {
96
+ if (typeof doc !== 'object' || doc === null) return;
97
+ for (const name of ROW_ARRAYS) {
98
+ const rows = (doc as Record<string, unknown>)[name];
99
+ if (!Array.isArray(rows)) continue;
100
+ for (const row of rows) into[name][classifyRow(row)] += 1;
101
+ }
102
+ }
103
+
104
+ /**
105
+ * Read every ledger under `root` and split its rows by writer class.
106
+ *
107
+ * Opens files for reading only. A file that will not parse increments
108
+ * `unreadable` rather than being skipped in silence — a corpus with unreadable
109
+ * members is a different corpus from a clean one, and a reader deciding whether
110
+ * a sample floor is met needs to know which they have.
111
+ */
112
+ export function readWriterSplit(root: string): WriterSplit {
113
+ const store = path.join(root, OBLIGATION_STATE_DIR);
114
+ const byArray: Record<RowArray, Tally> = {
115
+ delivered: emptyTally(),
116
+ discharged: emptyTally(),
117
+ shadow: emptyTally(),
118
+ };
119
+
120
+ let entries: string[];
121
+ try {
122
+ entries = fs.readdirSync(store).filter((f) => f.endsWith('.json'));
123
+ } catch {
124
+ return { store, storeExists: false, ledgers: 0, unreadable: 0, byArray, total: emptyTally() };
125
+ }
126
+
127
+ let ledgers = 0;
128
+ let unreadable = 0;
129
+ for (const entry of entries) {
130
+ let doc: unknown;
131
+ try {
132
+ doc = JSON.parse(fs.readFileSync(path.join(store, entry), 'utf-8'));
133
+ } catch {
134
+ unreadable += 1;
135
+ continue;
136
+ }
137
+ ledgers += 1;
138
+ tallyLedger(doc, byArray);
139
+ }
140
+
141
+ const total = emptyTally();
142
+ for (const name of ROW_ARRAYS) {
143
+ for (const cls of REPORTED_CLASSES) total[cls] += byArray[name][cls];
144
+ }
145
+ return { store, storeExists: true, ledgers, unreadable, byArray, total };
146
+ }
147
+
148
+ const sum = (t: Tally): number => REPORTED_CLASSES.reduce((n, c) => n + t[c], 0);
149
+
150
+ /**
151
+ * Render the split as text.
152
+ *
153
+ * The line a reader is most likely to quote is the shadow row, so it is named
154
+ * rather than left to be inferred from a table: the pre-registered bar counts
155
+ * shadow rows and nothing else, and a split over `delivered` answers a question
156
+ * the bar did not ask.
157
+ */
158
+ export function render(split: WriterSplit): string {
159
+ const out: string[] = [];
160
+ out.push(`scanned: ${split.store}`);
161
+ if (!split.storeExists) {
162
+ out.push(' the ledger directory does not exist — nothing was measured.');
163
+ out.push(' This is not a reading of zero. It is the absence of a corpus.');
164
+ return `${out.join('\n')}\n`;
165
+ }
166
+ out.push(` ledgers: ${split.ledgers}${split.unreadable > 0 ? `, unreadable: ${split.unreadable}` : ''}`);
167
+ if (split.ledgers === 0) {
168
+ out.push(' no ledger files — the directory exists and holds nothing.');
169
+ return `${out.join('\n')}\n`;
170
+ }
171
+
172
+ const width = Math.max(...ROW_ARRAYS.map((n) => n.length));
173
+ out.push('');
174
+ out.push(`${'rows'.padEnd(width)} ${REPORTED_CLASSES.map((c) => c.padStart(9)).join('')} total`);
175
+ for (const name of ROW_ARRAYS) {
176
+ const t = split.byArray[name];
177
+ const cells = REPORTED_CLASSES.map((c) => String(t[c]).padStart(9)).join('');
178
+ out.push(`${name.padEnd(width)} ${cells} ${String(sum(t)).padStart(7)}`);
179
+ }
180
+ const cells = REPORTED_CLASSES.map((c) => String(split.total[c]).padStart(9)).join('');
181
+ out.push(`${'all'.padEnd(width)} ${cells} ${String(sum(split.total)).padStart(7)}`);
182
+
183
+ out.push('');
184
+ out.push(
185
+ `shadow rows — the only array the pre-registered bar counts: ${sum(split.byArray.shadow)}`,
186
+ );
187
+ out.push(
188
+ '`absent` is a row written before the writer field existed; `unknown` is a row whose',
189
+ );
190
+ out.push('writer was resolved and came back unresolvable. They are not the same reading.');
191
+ out.push('');
192
+ out.push('This is a split, not a verdict. Whether the corpus is fit to read the bar off,');
193
+ out.push('and what follows for the detector, are owner decisions this tool does not make.');
194
+ return `${out.join('\n')}\n`;
195
+ }
196
+
197
+ /**
198
+ * Read one flag's value, refusing a flag whose value is missing.
199
+ *
200
+ * A flag present as the last argv element must be a usage error and never a
201
+ * silent fall back to the default. The same slip in a sibling reporter measured
202
+ * the DEFAULT corpus while printing what read as a settled number — a confident
203
+ * figure from a tree nobody asked for is worse than no figure at all.
204
+ */
205
+ function argValue(argv: string[], flag: string, fallback: string): string {
206
+ const i = argv.indexOf(flag);
207
+ if (i === -1) return fallback;
208
+ const value = argv[i + 1];
209
+ if (value === undefined || value.startsWith('--')) {
210
+ process.stderr.write(`report_obligation_writer_split: ${flag} needs a value\n`);
211
+ process.exit(1);
212
+ }
213
+ return value;
214
+ }
215
+
216
+ export function main(argv: string[]): number {
217
+ if (argv.includes('--help') || argv.includes('-h')) {
218
+ process.stdout.write(
219
+ 'usage: report_obligation_writer_split [--root <dir>] [--json]\n\n' +
220
+ ' --root <dir> tree whose agents/runtime/state/obligations/ to read (default: cwd)\n' +
221
+ ' --json emit the split as JSON instead of text\n\n' +
222
+ 'Read-only. Writes nothing, gates nothing, exits 0.\n',
223
+ );
224
+ return 0;
225
+ }
226
+ const root = argValue(argv, '--root', process.cwd());
227
+ const split = readWriterSplit(root);
228
+ process.stdout.write(argv.includes('--json') ? `${JSON.stringify(split, null, 2)}\n` : render(split));
229
+ return 0;
230
+ }
231
+
232
+ const _HERE = fileURLToPath(import.meta.url);
233
+
234
+ if (process.argv[1] && path.resolve(process.argv[1]) === path.resolve(_HERE)) {
235
+ process.exit(main(process.argv.slice(2)));
236
+ }
@@ -53,8 +53,21 @@
53
53
  * `command-only` and `flow-only` name a CANDIDATE for `user-invocable: false`,
54
54
  * never a decision — see the sole-entry limit above.
55
55
  *
56
+ * THE PER-PROFILE MENU, added by `road-to-a-menu-whose-precision-is-measured`
57
+ * 4.1. `--profile minimal|balanced|full|all` prints the menu byte count each
58
+ * shipped install profile delivers. Each profile's preset is read from its own
59
+ * file, and the answer is that all three are equal: no shipped preset declares
60
+ * a key that selects skills, so each one ships the whole menu.
61
+ *
62
+ * The equality note is WITHHELD when any profile carries a key listed in
63
+ * {@link SKILL_SELECTING_INI_KEYS}, and the key is named instead. That is the
64
+ * whole falsifiability claim, stated no wider than it holds: the byte count
65
+ * would not move — nothing here knows what such a key means — but the report
66
+ * stops asserting a conclusion whose premise has changed.
67
+ *
56
68
  * Usage:
57
69
  * ./scripts-run src/scripts/report_skill_menu_census # stdout summary
70
+ * ./scripts-run src/scripts/report_skill_menu_census --profile all
58
71
  * ./scripts-run src/scripts/report_skill_menu_census --emit # write the artifact
59
72
  * ./scripts-run src/scripts/report_skill_menu_census --emit --pin <sha>
60
73
  *
@@ -313,8 +326,203 @@ function headSha(root: string): string {
313
326
  return execFileSync('git', ['-C', root, 'rev-parse', 'HEAD'], { encoding: 'utf-8' }).trim();
314
327
  }
315
328
 
329
+ // ── per-profile menu bytes (road-to-a-menu-whose-precision-is-measured 4.1) ──
330
+
331
+ /** The shipped install presets, mirroring `install.ts` SUPPORTED_PROFILES. */
332
+ export const PROFILES = ['minimal', 'balanced', 'full'] as const;
333
+ export type Profile = (typeof PROFILES)[number];
334
+
335
+ /**
336
+ * Preset keys that would narrow the skill menu.
337
+ *
338
+ * EMPTY, and the emptiness is the finding rather than a stub. A preset selects
339
+ * `rule_loading_tier`, `discipline_profile` and `chat_history_frequency`; which
340
+ * SKILLS are written is decided by `packs:` frontmatter against the active pack
341
+ * set, which no preset names.
342
+ *
343
+ * It is a TRIPWIRE, not a selector, and the distinction is worth stating
344
+ * plainly because the first version of this comment overclaimed it. Nothing
345
+ * here knows what such a key would MEAN, so nothing here can apply one: the
346
+ * byte count would not move. What the list buys is that a preset carrying a
347
+ * listed key is NAMED in the output and the equality note is withheld — so the
348
+ * report stops asserting a conclusion whose premise has changed, instead of
349
+ * printing the same prose over a different tree.
350
+ */
351
+ export const SKILL_SELECTING_INI_KEYS: readonly string[] = [];
352
+
353
+ /** Flat `key=value` read of a profile preset; `;` comments and blanks dropped. */
354
+ export function readProfileIni(root: string, profile: Profile): Record<string, string> {
355
+ const p = path.join(root, 'src', 'config', 'profiles', `${profile}.ini`);
356
+ const out: Record<string, string> = {};
357
+ for (const raw of fs.readFileSync(p, 'utf-8').split('\n')) {
358
+ const line = raw.trim();
359
+ if (line === '' || line.startsWith(';') || line.startsWith('#') || line.startsWith('[')) continue;
360
+ const eq = line.indexOf('=');
361
+ if (eq === -1) continue;
362
+ out[line.slice(0, eq).trim()] = line.slice(eq + 1).trim();
363
+ }
364
+ return out;
365
+ }
366
+
367
+ export interface ProfileMenu {
368
+ readonly profile: Profile;
369
+ /** Skills this profile writes onto the model menu. */
370
+ readonly menuSkills: number;
371
+ /** Bytes of `- <name>: <description>\n`, the shape `censusSkillsCatalog` sums. */
372
+ readonly menuBytes: number;
373
+ /** chars/4, the estimator the payload budget uses. */
374
+ readonly tokensEstimate: number;
375
+ /** `declared_skill_catalogue_tokens` from the preset, or null when absent. */
376
+ readonly declaredCatalogueTokens: number | null;
377
+ /** Preset keys found that would narrow the menu. Empty today, by measurement. */
378
+ readonly selectingKeys: readonly string[];
379
+ }
380
+
381
+ /**
382
+ * One profile's menu, computed from its preset plus the skill tree.
383
+ *
384
+ * The catalogue LINE is the same shape `preamble_byte_census.censusSkillsCatalog`
385
+ * sums — `- <name>: <description>\n`. The POPULATION deliberately is not, and the
386
+ * difference is stated rather than smoothed over: that census sums every
387
+ * `SKILL.md`, because the payload bucket it feeds is about what the host lists;
388
+ * this one drops a skill carrying `user-invocable: false` or
389
+ * `disable-model-invocation: true`, because a menu is what the MODEL may pick
390
+ * from. Two skills carry such a flag today, so over the same tree and the same
391
+ * line shape this report reads 311 B below the full-population sum. Expect the
392
+ * two to differ; a reader who finds them equal has found a defect in one.
393
+ */
394
+ export function menuForProfile(root: string, profile: Profile): ProfileMenu {
395
+ const ini = readProfileIni(root, profile);
396
+ const selectingKeys = SKILL_SELECTING_INI_KEYS.filter((k) => k in ini);
397
+ const skillsDir = path.join(root, 'src', 'skills');
398
+ let menuSkills = 0;
399
+ let menuBytes = 0;
400
+ for (const entry of fs.readdirSync(skillsDir, { withFileTypes: true })) {
401
+ if (!entry.isDirectory()) continue;
402
+ const skillMd = path.join(skillsDir, entry.name, 'SKILL.md');
403
+ if (!fs.existsSync(skillMd)) continue;
404
+ const text = fs.readFileSync(skillMd, 'utf-8');
405
+ const flags = menuFlags(text);
406
+ if (flags.userInvocable === false || flags.disableModel === true) continue;
407
+ const name = /^name:\s*(\S.*?)\s*$/mu.exec(text.slice(0, 4000))?.[1] ?? entry.name;
408
+ const description = frontmatterDescription(text);
409
+ menuSkills += 1;
410
+ menuBytes += `- ${name}: ${description}\n`.length;
411
+ }
412
+ const declared = ini['declared_skill_catalogue_tokens'];
413
+ return {
414
+ profile,
415
+ menuSkills,
416
+ menuBytes,
417
+ tokensEstimate: Math.round(menuBytes / 4),
418
+ declaredCatalogueTokens: declared === undefined ? null : Number(declared),
419
+ selectingKeys,
420
+ };
421
+ }
422
+
423
+ /**
424
+ * The `description:` value, folded lines included.
425
+ *
426
+ * A `[^\n]+` capture was measured 17.1 % low elsewhere in this tree because it
427
+ * truncates every folded multi-line description; this reads to the next
428
+ * top-level key instead.
429
+ *
430
+ * A double-quoted value has its escapes DECODED. Leaving them encoded counted
431
+ * `\"` as two characters and over-reported the menu by 56 B across 13 skills —
432
+ * small, but the same class of defect as the 17.1 % one above, and a function
433
+ * whose docstring cites that fix should not carry a second instance of it.
434
+ */
435
+ export function frontmatterDescription(text: string): string {
436
+ if (!text.startsWith('---')) return '';
437
+ const end = text.indexOf('\n---', 3);
438
+ if (end === -1) return '';
439
+ const lines = text.slice(3, end).split('\n');
440
+ const at = lines.findIndex((l) => /^description:/u.test(l));
441
+ if (at === -1) return '';
442
+ const parts = [(lines[at] as string).replace(/^description:\s*/u, '')];
443
+ for (let i = at + 1; i < lines.length; i += 1) {
444
+ const l = lines[i] as string;
445
+ if (/^\S/u.test(l)) break;
446
+ parts.push(l.trim());
447
+ }
448
+ const joined = parts.join(' ').trim();
449
+ if (joined.length >= 2 && joined.startsWith('"') && joined.endsWith('"')) {
450
+ return joined.slice(1, -1).replace(/\\(["\\])/gu, '$1');
451
+ }
452
+ if (joined.length >= 2 && joined.startsWith("'") && joined.endsWith("'")) {
453
+ return joined.slice(1, -1).replace(/''/gu, "'");
454
+ }
455
+ return joined;
456
+ }
457
+
458
+ /** Render the per-profile menu table. Returns the lines, so tests read them. */
459
+ export function renderProfileMenus(menus: readonly ProfileMenu[]): string[] {
460
+ const L: string[] = [];
461
+ L.push('menu bytes per install profile (name + description, the catalogue shape):');
462
+ L.push(' profile skills menu_bytes ~tokens declared');
463
+ for (const m of menus) {
464
+ L.push(
465
+ ` ${m.profile.padEnd(10)} ${String(m.menuSkills).padStart(6)} ` +
466
+ `${String(m.menuBytes).padStart(12)} ${String(m.tokensEstimate).padStart(9)} ` +
467
+ `${(m.declaredCatalogueTokens === null ? '—' : String(m.declaredCatalogueTokens)).padStart(10)}`,
468
+ );
469
+ }
470
+ const flagged = menus.filter((m) => m.selectingKeys.length > 0);
471
+ if (flagged.length > 0) {
472
+ L.push('');
473
+ L.push(' A preset declares a key that could select skills, so the equality below is NOT');
474
+ L.push(' asserted — the premise it rested on has changed. Keys found:');
475
+ for (const m of flagged) L.push(` ${m.profile}: ${m.selectingKeys.join(', ')}`);
476
+ return L;
477
+ }
478
+ const distinct = new Set(menus.map((m) => m.menuBytes));
479
+ if (menus.length > 1 && distinct.size === 1) {
480
+ L.push('');
481
+ L.push(
482
+ ' All profiles report the SAME menu_bytes, and that is a measurement, not a',
483
+ );
484
+ L.push(
485
+ ' placeholder: no preset declares a key that selects skills, so each ships the',
486
+ );
487
+ L.push(
488
+ ' whole menu. The preset chooses which RULES activate; which SKILLS are written',
489
+ );
490
+ L.push(
491
+ ' is decided by `packs:` frontmatter against the active pack set. A menu-bytes',
492
+ );
493
+ L.push(
494
+ ' lever therefore has to move packs or the menu flags — changing profile does',
495
+ );
496
+ L.push(' nothing. The three presets already record this in their own comments.');
497
+ }
498
+ return L;
499
+ }
500
+
316
501
  export function main(argv: readonly string[]): number {
317
502
  const root = REPO_ROOT;
503
+ const profIdx = argv.indexOf('--profile');
504
+ if (profIdx !== -1) {
505
+ const want = argv[profIdx + 1] ?? 'all';
506
+ const chosen: Profile[] =
507
+ want === 'all' ? [...PROFILES] : PROFILES.includes(want as Profile) ? [want as Profile] : [];
508
+ if (chosen.length === 0) {
509
+ process.stderr.write(
510
+ `report_skill_menu_census: --profile expects ${PROFILES.join(' | ')} | all, got ${want}\n`,
511
+ );
512
+ return 2;
513
+ }
514
+ if (argv.includes('--emit')) {
515
+ // Refused rather than ignored: `--emit` writes the census artifact,
516
+ // `--profile` prints a table and writes nothing, and silently doing
517
+ // the second when the caller asked for both is how an artifact goes
518
+ // un-refreshed without anyone noticing.
519
+ process.stderr.write('report_skill_menu_census: --profile and --emit are separate runs\n');
520
+ return 2;
521
+ }
522
+ const menus = chosen.map((p) => menuForProfile(root, p));
523
+ process.stdout.write(`${renderProfileMenus(menus).join('\n')}\n`);
524
+ return 0;
525
+ }
318
526
  const emit = argv.includes('--emit');
319
527
  const pinIdx = argv.indexOf('--pin');
320
528
  const pin = pinIdx !== -1 && pinIdx + 1 < argv.length ? (argv[pinIdx + 1] as string) : headSha(root);
@@ -156,7 +156,7 @@ export const HOST_SURFACES: readonly HostSurface[] = [
156
156
  },
157
157
  {
158
158
  host: 'codex', surface: '.codex/agent-config.md', perRuleTree: false,
159
- writer: 'src/scripts/install.ts:1432',
159
+ writer: 'src/scripts/install.ts:1431',
160
160
  // Scoped to `.codex` rather than the bare filename: nine other lines in
161
161
  // `install.ts` write an `agent-config.md` for some other host, so the
162
162
  // unscoped anchor would have accepted a drift onto any of them — the
@@ -0,0 +1,213 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * The runnable share of `verify:` clauses, per roadmap.
4
+ *
5
+ * A `verify:` clause names a command and never names what the command must
6
+ * produce, so a step can be flipped on a command that cannot fail. Two numbers
7
+ * say how bad that is, and neither had a producing command before this script:
8
+ * how many clauses name a COMMAND at all (the runnable share), and how many
9
+ * name an EXPECTATION (the falsifiable share).
10
+ *
11
+ * THIS IS A READER, NOT A GATE. It exits 0 on every corpus, including one where
12
+ * both shares are zero. That is a property of the reader, not a description of
13
+ * the tree: measured 2026-09-29, the tree carried 230 clauses, 93 naming a
14
+ * command and 6 naming an expectation — neither share is zero, and the
15
+ * commissioning roadmap's claim that the second one was is the first thing this
16
+ * script refuted. A ratchet needs a false-positive rate and two readings a
17
+ * quarter apart; it has neither, and a gate that reds all but a handful of
18
+ * roadmaps at once gets weakened until it finds nothing. What it buys instead
19
+ * is that the NEXT reading is a delta rather than a re-derivation.
20
+ *
21
+ * THE UNIT, stated because the figure is meaningless without it. One unit is
22
+ * one STEP BLOCK (`- [ ]`, `- [x]`, `- [~]`, `- [-]`, plus its continuation
23
+ * lines) that carries a `verify:` clause. Not one line: a clause wraps, and a
24
+ * line count would double-count a wrapped clause and count prose that merely
25
+ * mentions the token. Step blocks come from `closure_scan`'s own `units()`, so
26
+ * the two instruments agree on what a step is; that also inherits its
27
+ * exclusions — fenced code, and the `## Decisions` / `## Blockers` /
28
+ * `## Kill register` / `## Risk Register` discharge sections, where a mention
29
+ * of `verify:` is commentary rather than a step's oracle.
30
+ *
31
+ * Closed steps are counted. A `[x]` flipped on a clause that could not fail is
32
+ * the defect in its completed form, and excluding them would report the
33
+ * backlog as smaller every time someone closed one.
34
+ *
35
+ * CLI:
36
+ * ./scripts-run src/scripts/roadmap_verify_share
37
+ * ./scripts-run src/scripts/roadmap_verify_share --json
38
+ * ./scripts-run src/scripts/roadmap_verify_share --root <dir> [--recursive]
39
+ *
40
+ * Exit codes: 0 always, except 2 for bad argv or a dead scan root.
41
+ */
42
+ import * as fs from 'node:fs';
43
+ import * as path from 'node:path';
44
+ import process from 'node:process';
45
+ import { fileURLToPath } from 'node:url';
46
+
47
+ import { units } from './closure_scan.js';
48
+ import { DeadScopeError, assertScanned } from './_lib/scan_scope.js';
49
+ import { parseVerifyClause } from './_lib/verify_clause.js';
50
+
51
+ const _HERE = fileURLToPath(import.meta.url);
52
+ const _PROG = 'roadmap_verify_share';
53
+ const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
54
+ const DEFAULT_ROOT = 'agents/roadmaps';
55
+
56
+ export interface FileShare {
57
+ readonly file: string;
58
+ /** Step blocks carrying a `verify:` clause. */
59
+ readonly clauses: number;
60
+ /** Of those, the ones naming a command — the runnable share. */
61
+ readonly withCommand: number;
62
+ /** Of those, the ones naming an expectation — the falsifiable share. */
63
+ readonly withExpectation: number;
64
+ }
65
+
66
+ export interface ShareReport {
67
+ readonly root: string;
68
+ readonly files: number;
69
+ readonly clauses: number;
70
+ readonly withCommand: number;
71
+ readonly withExpectation: number;
72
+ readonly perFile: readonly FileShare[];
73
+ }
74
+
75
+ /** Count the clauses in one roadmap's text. */
76
+ export function shareOf(file: string, text: string): FileShare {
77
+ let clauses = 0;
78
+ let withCommand = 0;
79
+ let withExpectation = 0;
80
+ for (const u of units(text.split('\n'))) {
81
+ if (!u.hasVerify) continue;
82
+ const clause = parseVerifyClause(u.blockText);
83
+ if (clause === null) continue;
84
+ clauses += 1;
85
+ if (clause.command !== null) withCommand += 1;
86
+ if (clause.expect !== null) withExpectation += 1;
87
+ }
88
+ return { file, clauses, withCommand, withExpectation };
89
+ }
90
+
91
+ function markdownFiles(root: string, recursive: boolean): string[] {
92
+ const out: string[] = [];
93
+ const walk = (dir: string): void => {
94
+ let entries: fs.Dirent[];
95
+ try {
96
+ entries = fs.readdirSync(dir, { withFileTypes: true });
97
+ } catch {
98
+ return;
99
+ }
100
+ for (const e of entries) {
101
+ const full = path.join(dir, e.name);
102
+ if (e.isDirectory()) {
103
+ if (recursive) walk(full);
104
+ continue;
105
+ }
106
+ if (e.name.endsWith('.md')) out.push(full);
107
+ }
108
+ };
109
+ walk(root);
110
+ return out.sort();
111
+ }
112
+
113
+ export function report(rootAbs: string, rootLabel: string, recursive: boolean): ShareReport {
114
+ const files = markdownFiles(rootAbs, recursive);
115
+ // A root that resolved to nothing is blindness, not cleanliness: the share
116
+ // would read as a clean 0/0 and the reader would believe it.
117
+ assertScanned({ gate: _PROG, scanned: files.length, units: 'roadmap(s)', roots: [rootLabel] });
118
+ const perFile = files
119
+ .map((f) => shareOf(path.relative(REPO_ROOT, f), fs.readFileSync(f, 'utf-8')))
120
+ .filter((s) => s.clauses > 0);
121
+ return {
122
+ root: rootLabel,
123
+ files: files.length,
124
+ clauses: perFile.reduce((n, s) => n + s.clauses, 0),
125
+ withCommand: perFile.reduce((n, s) => n + s.withCommand, 0),
126
+ withExpectation: perFile.reduce((n, s) => n + s.withExpectation, 0),
127
+ perFile,
128
+ };
129
+ }
130
+
131
+ function pct(n: number, of: number): string {
132
+ return of === 0 ? 'n/a' : `${((n / of) * 100).toFixed(1)}%`;
133
+ }
134
+
135
+ export function render(r: ShareReport): string {
136
+ const lines: string[] = [];
137
+ lines.push(`root: ${r.root}`);
138
+ lines.push(`roadmaps scanned: ${String(r.files)}`);
139
+ lines.push(`verify clauses: ${String(r.clauses)}`);
140
+ lines.push(` naming a command: ${String(r.withCommand)} (${pct(r.withCommand, r.clauses)})`);
141
+ lines.push(` naming an expectation: ${String(r.withExpectation)} (${pct(r.withExpectation, r.clauses)})`);
142
+ lines.push('');
143
+ lines.push('| roadmap | clauses | command | expectation |');
144
+ lines.push('|---|---:|---:|---:|');
145
+ for (const s of r.perFile) {
146
+ lines.push(
147
+ `| \`${path.basename(s.file)}\` | ${String(s.clauses)} | ${String(s.withCommand)} | ${String(s.withExpectation)} |`,
148
+ );
149
+ }
150
+ return lines.join('\n') + '\n';
151
+ }
152
+
153
+ function _usage(): string {
154
+ return (
155
+ `usage: ${_PROG} [-h] [--json] [--root <dir>] [--recursive]\n\n` +
156
+ `Report the runnable and falsifiable share of \`verify:\` clauses. Gates nothing.\n`
157
+ );
158
+ }
159
+
160
+ export function main(argv?: readonly string[]): number {
161
+ const args = argv ?? process.argv.slice(2);
162
+ let json = false;
163
+ let recursive = false;
164
+ let root = DEFAULT_ROOT;
165
+ for (let i = 0; i < args.length; i += 1) {
166
+ const a = args[i] as string;
167
+ if (a === '-h' || a === '--help') {
168
+ process.stdout.write(_usage());
169
+ return 0;
170
+ } else if (a === '--json') json = true;
171
+ else if (a === '--recursive') recursive = true;
172
+ else if (a === '--root') {
173
+ const next = args[i + 1];
174
+ if (next === undefined) {
175
+ process.stderr.write(`${_usage()}${_PROG}: error: --root needs a directory\n`);
176
+ return 2;
177
+ }
178
+ root = next;
179
+ i += 1;
180
+ } else {
181
+ process.stderr.write(`${_usage()}${_PROG}: error: unrecognized arguments: ${a}\n`);
182
+ return 2;
183
+ }
184
+ }
185
+
186
+ const rootAbs = path.isAbsolute(root) ? root : path.join(REPO_ROOT, root);
187
+ let r: ShareReport;
188
+ try {
189
+ r = report(rootAbs, root, recursive);
190
+ } catch (e) {
191
+ if (e instanceof DeadScopeError) {
192
+ process.stderr.write(`${e.message}\n`);
193
+ return 2;
194
+ }
195
+ throw e;
196
+ }
197
+
198
+ process.stdout.write(json ? JSON.stringify(r, null, 2) + '\n' : render(r));
199
+ return 0;
200
+ }
201
+
202
+ function _isCliEntry(): boolean {
203
+ if (!process.argv[1]) return false;
204
+ try {
205
+ return fs.realpathSync(_HERE) === fs.realpathSync(path.resolve(process.argv[1]));
206
+ } catch {
207
+ return false;
208
+ }
209
+ }
210
+
211
+ if (_isCliEntry() || process.argv[1] === _HERE) {
212
+ process.exitCode = main();
213
+ }