@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
@@ -108,8 +108,8 @@
108
108
  },
109
109
  "tool_result_bytes": {
110
110
  "definition": "road-to-inbox-harvest-2026-08-d-context-ledger 1.1: UTF-8 bytes of every tool RESULT entering context, per call, keyed by tool name. Closes the gap `unbounded_output_advisory_rate` registered above - the injection census measures what this suite puts into context, this measures what the host's tools do, and the second is the larger term. CAPTURE ONLY: the concern emits zero bytes to the model, so it carries no per-concern injection cap; it is registered here because this file already owns the owner/review discipline for context-cost instruments.",
111
- "instrument": "the `tool-result-bytes` concern (src/scripts/hooks/tool_result_bytes_hook.ts) on post_tool_use, appending {ts, tool, bytes, measurable} to agents/runtime/state/tool-result-census.jsonl",
112
- "privacy": "census lines carry a tool NAME, a byte COUNT and a timestamp - the record type has no field able to hold file contents, command output or any free text, so there is no scrubber that can fail",
111
+ "instrument": "the `tool-result-bytes` concern (src/scripts/hooks/tool_result_bytes_hook.ts) on post_tool_use, appending {ts, tool, raw_bytes, delivered_bytes, measurable} to agents/runtime/state/tool-result-census.jsonl, and ONLY where the maintainer workspace resolves (or AGENT_CONFIG_TOOL_BYTE_CENSUS=1) - a per-call write in every consumer install would be a second unread instrument",
112
+ "privacy": "census lines carry a tool NAME, two byte COUNTS and a timestamp - the record type has no field able to hold file contents, command output or any free text, so there is no scrubber that can fail",
113
113
  "threshold": "none committed before data - baseline first, threshold at review date. A `measurable: false` line means the host's payload carried no readable result; that absence is itself a coverage finding and is never counted as zero bytes"
114
114
  },
115
115
  "turns_per_task": {
@@ -26,6 +26,26 @@
26
26
  # that is a review judgement — so the honest claim is that the lint makes the
27
27
  # OMISSION impossible, not that it makes the answer true. Stated here rather
28
28
  # than discovered by someone who trusted it further than it goes.
29
+ #
30
+ # ## `unit` and `basis` — how the number was obtained, beside the number
31
+ #
32
+ # An entry MAY carry `unit:` and MUST carry `basis:` when its unit is `bytes`.
33
+ # `basis` is one of:
34
+ #
35
+ # measured — something counted it. A length, a sum, a row count.
36
+ # derived — arithmetic over a measured quantity. Never a count.
37
+ # proxy — a different quantity standing in for the one named.
38
+ # unavailable — not observable from this package. No figure is produced.
39
+ #
40
+ # The field exists because a derived byte figure and a counted one look
41
+ # identical in a cell, and this tree has already been burned by a number quoted
42
+ # onward without the conditions that produced it. `git count-objects -v` is the
43
+ # standing example of the confusion: it measures a LOCAL object store and is
44
+ # never a transfer figure, whatever the unit says.
45
+ #
46
+ # An `unavailable` entry declares a term that is out of reach, so that a total
47
+ # built without it is visibly incomplete rather than quietly wrong. It produces
48
+ # no value and must never be estimated into a sum.
29
49
 
30
50
  metrics:
31
51
  - id: attribution-shape-block-count
@@ -122,3 +142,97 @@ metrics:
122
142
  assumption that widening helped -- which is the assumption the whole
123
143
  roadmap exists to test rather than to make. Without it the estate grows
124
144
  and nothing can say whether it should have.
145
+
146
+ # ── Bytes ───────────────────────────────────────────────────────────────
147
+ #
148
+ # `road-to-a-bytes-row-that-exists`. Before these, a statement of the form
149
+ # "this change moved N bytes" could not be made at all: the tree measured
150
+ # tokens well and bytes not at all, and no row here could have held one.
151
+
152
+ - id: tool-raw-bytes
153
+ unit: bytes
154
+ basis: measured
155
+ producer: src/scripts/hooks/tool_result_bytes_hook.ts (`raw_bytes`, per post_tool_use dispatch)
156
+ consumer: >-
157
+ the `tool_result_bytes` stream registered in `src/config/hook-token-budget.json`,
158
+ and the maintainer reading `agents/runtime/state/tool-result-census.jsonl`
159
+ before a tool-result distribution is published.
160
+ decision: >-
161
+ whether a cap on tool-result size is worth introducing, and at what
162
+ value. The distribution has never been observed, so the threshold stays
163
+ uncommitted until this counter has written one.
164
+ absent: >-
165
+ every context-cost claim this package makes is computed against a
166
+ denominator that excludes its own dominant term — the hook-injection
167
+ census measures what this suite puts into context and nothing measures
168
+ what the host's tools do.
169
+
170
+ - id: tool-delivered-bytes
171
+ unit: bytes
172
+ basis: measured
173
+ producer: src/scripts/hooks/tool_result_bytes_hook.ts (`delivered_bytes`, same record)
174
+ consumer: >-
175
+ the same census reader, comparing it against `tool-raw-bytes` on the same
176
+ line. Equal by construction today, because nothing in this tree rewrites
177
+ a tool result on its way to the model.
178
+ decision: >-
179
+ whether a result-rewriting mechanism, if one ever lands, reduced anything.
180
+ A non-zero gap between the two columns is the reduction; no gap is no
181
+ reduction.
182
+ absent: >-
183
+ a later release that starts trimming tool results has no before-figure
184
+ and can only assert an improvement. The two columns exist now precisely
185
+ so that claim is a measured delta later rather than a story.
186
+
187
+ - id: transcript-bytes-per-token
188
+ unit: bytes_per_token
189
+ basis: measured
190
+ producer: src/scripts/_lib/cc_transcript.ts (`measureBytesPerToken`)
191
+ consumer: >-
192
+ `cache_realization_report` and `session_eol_report`, which multiply a
193
+ token column by it, and any reader checking that division — the two sums
194
+ it was divided from travel with it on every emitted value.
195
+ decision: >-
196
+ whether a byte figure may be derived from this corpus at all. A corpus
197
+ that supplies no ratio produces no byte figure, rather than borrowing one
198
+ measured somewhere else.
199
+ absent: >-
200
+ the only remaining way to turn a token count into bytes is a constant in
201
+ a script, which is correct for the content that produced it and silently
202
+ wrong for everything else — the third conflicting figure this tree has
203
+ already paid to learn about twice.
204
+
205
+ - id: provider-bytes-per-call
206
+ unit: bytes
207
+ basis: derived
208
+ producer: >-
209
+ src/scripts/_lib/cc_transcript.ts (`derivedBytes`), surfaced as
210
+ `provider_bytes` by `cache_realization_report` and `session_eol_report`.
211
+ consumer: >-
212
+ the maintainer sizing what a round re-sends, and `check_claims`, which
213
+ refuses a public byte figure whose metric marker resolves to no entry here.
214
+ decision: >-
215
+ whether the volume a stateless chat round re-sends is large enough to act
216
+ on. Every existing reading of that cost is a token count; this is the
217
+ first one in the unit a network is billed in.
218
+ absent: >-
219
+ the re-send cost has no byte figure at all, so any statement about it is
220
+ either a token count wearing the wrong unit or an estimate with nothing
221
+ behind it. That is the state this metric was added to end.
222
+
223
+ - id: host-fetch-and-transport-bytes
224
+ unit: bytes
225
+ basis: unavailable
226
+ producer: >-
227
+ none — the host's own fetch tool and the model transport are not
228
+ observable from inside this package, and no producer is planned.
229
+ consumer: >-
230
+ any reader of a byte total, who needs to see which terms the total does
231
+ NOT contain. The entry is the disclosure; there is no value to read.
232
+ decision: >-
233
+ whether a byte total may be published as complete. It may not: these
234
+ terms are out of reach, so every total is a partial one and says so.
235
+ absent: >-
236
+ the two unreachable terms get estimated into a sum to make it look whole,
237
+ and the estimate becomes the figure everybody quotes. Declaring them
238
+ unavailable is what stops a guess from entering a total.
@@ -0,0 +1,10 @@
1
+ {
2
+ "owner": "maintainer",
3
+ "review_by": "2027-09-29",
4
+ "seeded_at": "2026-09-29",
5
+ "note": "Shrink-only ceilings on FOREIGN placeholder syntaxes in prose, per road-to-an-invocation-contract-that-reaches-the-wire 1.2. The unit is a token outside fenced blocks, indented blocks and inline code spans — defined and argued in src/scripts/report_invocation_surface.ts's module header, with the per-file breakdown in agents/evidence/analysis/invocation-surface-census.md. Seeded at what this tree measures, not at the roadmap's figures: those counted tokens inside demonstrated code (JS template literals, HCL, a table-cell marker the prose talks about) and do not reproduce. The plurality form is deliberately ungated. Raising a ceiling is a reviewable diff, and the reason belongs in it.",
6
+ "ceilings": {
7
+ "angle-upper": 3,
8
+ "double-brace": 0
9
+ }
10
+ }
@@ -10,12 +10,12 @@
10
10
  },
11
11
  "seed": {
12
12
  "rules": 0.9623,
13
- "skills": 0.3378
13
+ "skills": 0.3445
14
14
  },
15
15
  "seed_provenance": {
16
16
  "rules": "102 covered of 106 routed rules on 2026-09-13; was 101 of 105 on 2026-09-08 and 94 of 105 (tier-1 21/24, tier-2 73/81) on 2026-08-25.",
17
- "skills": "101 of 299 skills carry evals/triggers.json on 2026-09-19; was 100 on 2026-08-30.",
18
- "finding": "The gap between the two seeds IS the roadmap defect D1, quantified. The rules surface is ~90% covered by a deterministic corpus that can fail a PR; the skills surface -- the one production routes on -- is 25%, covered only by a harness that is advisory only, never gating (rule_trigger_eval.ts:4)."
17
+ "skills": "103 of 299 skills carry evals/triggers.json on 2026-10-01; was 102 on 2026-09-30, 101 on 2026-09-19 and 100 on 2026-08-30.",
18
+ "finding": "The gap between the two seeds IS the roadmap defect D1, quantified. The rules surface is ~90% covered by a deterministic corpus that can fail a PR; the skills surface -- the one production routes on -- is far lower and is covered only by a harness that is advisory only, never gating (rule_trigger_eval.ts:4). The figure is deliberately not restated here (corrected 2026-10-01): it read `25%` against a seed that had since moved to 0.3445, which is the same count-drift this file's own history entries keep repairing one field away. The live value is `seed.skills`, two keys above."
19
19
  },
20
20
  "history": [
21
21
  {
@@ -51,7 +51,7 @@
51
51
  "scope": "rules",
52
52
  "from": 0.9619,
53
53
  "to": 0.9623,
54
- "reason": "road-to-adversarial-verification-and-long-runs Phase 1.1 added `test-first` as a routed rule, which moved the DENOMINATOR from 105 to 106 -- the ratio-falls-two-ways case this file's own _comment names, and the one that is a defect to record rather than repair. The repair is the corpus file, not the seed: `tests/eval/routing-matrix/test-first.yaml` landed in the same change (101 -> 102 of 106), so the ratio ROSE across a denominator increase rather than merely holding. The rule is trigger-less by design, so its positives describe the situations it governs rather than strings that summon it, and its near-misses are its own \u00a7 Does NOT fire set. The seed follows the gain because this file's test asserts seed == live measurement in both directions; it may never be LOWERED."
54
+ "reason": "road-to-adversarial-verification-and-long-runs Phase 1.1 added `test-first` as a routed rule, which moved the DENOMINATOR from 105 to 106 -- the ratio-falls-two-ways case this file's own _comment names, and the one that is a defect to record rather than repair. The repair is the corpus file, not the seed: `tests/eval/routing-matrix/test-first.yaml` landed in the same change (101 -> 102 of 106), so the ratio ROSE across a denominator increase rather than merely holding. The rule is trigger-less by design, so its positives describe the situations it governs rather than strings that summon it, and its near-misses are its own § Does NOT fire set. The seed follows the gain because this file's test asserts seed == live measurement in both directions; it may never be LOWERED."
55
55
  },
56
56
  {
57
57
  "date": "2026-09-19",
@@ -59,6 +59,20 @@
59
59
  "from": 0.3344,
60
60
  "to": 0.3378,
61
61
  "reason": "road-to-release-holds-that-refuse step 1.4 authored one corpus file for `roadmap-writing` (100 -> 101 of 299) and removed its now-false entry from trigger_eval_grandfather.json. ONE file rather than a wave: the step asked for a single positive case whose expected output is a re-sequenced phase rather than a hold, and the eval set is where that expectation is pinned. The seed rises so the gain cannot be given back silently; it may never be LOWERED. Two published measurements moved with it and were re-taken in the same change -- routing-body-signal-verdict.json and the measure_delivery_sets record -- because the trigger corpus is their input, so any skill gaining an eval set re-takes both."
62
+ },
63
+ {
64
+ "date": "2026-09-30",
65
+ "scope": "skills",
66
+ "from": 0.3378,
67
+ "to": 0.3411,
68
+ "reason": "road-to-a-component-taxonomy-we-follow-but-never-force authored one corpus file for `ui-component-architect` (101 -> 102 of 299). Not a sought gain: the branch edits that skill's placement section, and check_routing_coverage requires a corpus for every skill a diff touches, so the file is the gate's own remedy rather than a coverage push. Five should-trigger cases, one per shape the skill decides, and five near misses whose vocabulary overlaps hardest -- styling an existing component, inventorying what the project owns (existing-ui-audit, which runs first and feeds this one), a backend composition question, a prop rename, and general React knowledge with no component to shape. The seed rises so the gain cannot be given back silently; it may never be LOWERED. The two published measurements the 2026-09-19 entry names -- routing-body-signal-verdict.json and the measure_delivery_sets record -- are re-taken in the same change, for the reason that entry gives: the trigger corpus is their input."
69
+ },
70
+ {
71
+ "date": "2026-10-01",
72
+ "scope": "skills",
73
+ "from": 0.3411,
74
+ "to": 0.3445,
75
+ "reason": "road-to-corpus-refresh-cadence-shape step 1.2a authored one corpus file for `accessibility-auditor` (102 -> 103 of 299). Same shape as the 2026-09-30 entry and for the same reason: that step re-checked the skill's grounding corpus against WCAG 2.2 and the APG, which edited src/skills/accessibility-auditor/data/, and check_routing_coverage requires a corpus for every skill a diff touches. Ten cases -- five exemplars including the German phrasing the skill's own description names, three near-misses -- tailwind-engineer for token-level contrast and ui-component-architect for landmarks and heading order, both named in that skill's SKILL.md, plus existing-ui-audit for component inventory, which it does not name and which is included because it runs first over the same surface -- and two counterexamples, one guarding the 'accessible' homonym. The seed rises so the gain cannot be given back silently; it may never be LOWERED. The trigger-corpus holdout pin is re-taken in the same change -- and this growth moves it for the first time on the HOLDOUT side (18 -> 19), because sha256('accessibility-auditor')[0:2] is 0x2f = 47, below the ceiling of 51. That is recorded in the holdout artefact with its ordering claim explicitly scoped to the 18 originally sealed rows. The two published measurements the 2026-09-19 and 2026-09-30 entries name -- routing-body-signal-verdict.json and the measure_delivery_sets record -- are deliberately NOT re-taken here, and the departure from those two entries is stated rather than left to be noticed. The reason is the partition, not the date: this growth is HOLDOUT-side, so the train input both measurements read is byte-identical to what it was before this change -- all 84 train rows reproduce -- and re-taking them would publish the same measurement under a newer date, which is a freshness claim nothing earned. The two artefacts are NOT in the same state, and conflating them would repeat the error this sentence replaces: routing-body-signal-verdict.json carries `corpus.partition: train` and `train_corpora: 84` and is current for the train partition; delivery-set-result-2026-08-31.md still reads `82 train corpora, 764 distinct prompts` and carries no re-take section for either the 2026-09-19 or the 2026-09-30 growth, so it is two corpora stale and those two entries' claim that it `is re-taken in the same change` is not backed by the tree. Repairing it is a train-side measurement re-run, out of scope for a holdout-side growth, and is named here so the gap is attributed rather than inherited silently. A FUTURE train-side growth owes both re-takes. Separately, TWO entries were removed from trigger_eval_grandfather.json, and the two reds are of different kinds. `ui-component-architect` was red ON MAIN: it gained a corpus on 2026-09-30 and its now-false allowlist entry was not removed then, which is the convention the 2026-09-19 entry sets and the 2026-09-30 entry omitted. `accessibility-auditor` was NOT red on main -- on main it ships no corpus at all, and check_trigger_eval_presence fires only on an allowlist entry whose skill HAS one, so its entry went stale the moment this change added the corpus. That red is created here and removed here, in the same change. Stated separately because a reader auditing why two entries were dropped would otherwise be told one pre-existing condition explains both."
62
76
  }
63
77
  ]
64
78
  }
@@ -114,7 +114,8 @@ import {
114
114
  resolve_project_root,
115
115
  type TraceRecord,
116
116
  } from '../_lib/agent_settings.js';
117
- import { executionJson, forgeProtectionJson, UNREAD_FORGE } from './doctor_execution.js';
117
+ import { executionJson, forgeProtectionJsonFor } from './doctor_execution.js';
118
+ import { checkOfflineReadiness, trafficEnvironmentJson } from './doctor_network_posture.js';
118
119
  import * as ai_council_clients from '../ai_council/clients.js';
119
120
  import * as ai_council_config from '../ai_council/config.js';
120
121
  import {
@@ -1450,23 +1451,7 @@ function _check_mcp_mode(project_root: string): Dict {
1450
1451
  };
1451
1452
  }
1452
1453
 
1453
- function _check_offline_readiness(): Dict {
1454
- const script = path.join(_package_root(), 'src', 'scripts', 'hermetic-install.sh');
1455
- if (!pathExists(script)) {
1456
- return {
1457
- id: 'offline-readiness',
1458
- status: 'warn',
1459
- message: 'src/scripts/hermetic-install.sh not found in package',
1460
- remedy: 'reinstall @event4u/agent-config or pull missing files',
1461
- };
1462
- }
1463
- return {
1464
- id: 'offline-readiness',
1465
- status: 'ok',
1466
- message: 'verified-offline install entrypoint present',
1467
- remedy: '',
1468
- };
1469
- }
1454
+ const _check_offline_readiness = (): Dict => checkOfflineReadiness(_package_root());
1470
1455
 
1471
1456
  function _check_stale_orphans(): Dict {
1472
1457
  const gdi = global_deploy_inventory;
@@ -3077,7 +3062,8 @@ function _emit_json(
3077
3062
  };
3078
3063
  if (origin !== null) payload['project_root_origin'] = origin;
3079
3064
  payload['execution'] = executionJson(() => iter_setting_overrides({ cwd: project_root }));
3080
- payload['forge_protection'] = forgeProtectionJson(UNREAD_FORGE); // 3.2
3065
+ payload['forge_protection'] = forgeProtectionJsonFor(String(project_root)); // 3.2 · AC-5
3066
+ payload['traffic_environment'] = trafficEnvironmentJson(process.env);
3081
3067
  if (checks !== null) {
3082
3068
  payload['checks'] = checks;
3083
3069
  const drift = missing.length + modified.length + foreign.length + tag_drift.length;
@@ -23,6 +23,15 @@ import {
23
23
  protectionActions,
24
24
  type ForgeReading,
25
25
  } from '../_lib/forge_protection.js';
26
+ import {
27
+ FORGE_TOTAL_BUDGET_MS,
28
+ budgetOf,
29
+ forgeReadingFor,
30
+ liveForgeApi,
31
+ originUrl,
32
+ resolveForgeRepo,
33
+ type Runner,
34
+ } from '../_lib/forge_reader.js';
26
35
 
27
36
  type Dict = Record<string, unknown>;
28
37
 
@@ -52,12 +61,17 @@ export function executionJson(overrides: () => OverrideStream): Dict {
52
61
  /**
53
62
  * The `forge_protection` block of `doctor --json` — Phase 3.2.
54
63
  *
55
- * **`doctor` does NOT reach the network on its own.** A diagnostic that makes a
56
- * `gh` call on every invocation is one nobody runs offline, and the sibling
57
- * anchor gate already declined that cost for the same reason. So the reading is
58
- * INJECTED: a caller that has queried the forge passes it, and a caller that has
59
- * not passes the all-null reading, which produces five `unread` rows that still
60
- * name the call each value would come from.
64
+ * **THIS function does not reach the network; `doctor` now does.** The mapper
65
+ * stays pure and takes an INJECTED reading — a caller that queried the forge
66
+ * passes it, one that did not passes the all-null reading, which produces five
67
+ * `unread` rows that still name the call each value would come from. What
68
+ * changed is the caller: `forgeProtectionJsonFor` below acquires that reading,
69
+ * so `doctor --json` does make `gh` calls. Phase 3.2's "`doctor` does not reach
70
+ * the network" is SUPERSEDED, and this header said otherwise for three review
71
+ * rounds while the function forty lines down called it a reversal — the third
72
+ * copy of a refuted argument in this diff, each found by a different round.
73
+ * Corrected here for the reason the others were: a header is where a dead
74
+ * argument gets reused.
61
75
  *
62
76
  * That is the honest shape for *read, never guessed*: the block is always
63
77
  * present, a value appears only when a named call produced it, and `unread` is a
@@ -67,24 +81,93 @@ export function executionJson(overrides: () => OverrideStream): Dict {
67
81
  * `actions` carries the human ACTION lines — the step's own words are that a
68
82
  * missing row is a blocker entry and NOT a halt, so this reports and returns.
69
83
  */
70
- export function forgeProtectionJson(reading: ForgeReading): Dict {
84
+ export function forgeProtectionJson(reading: ForgeReading, repo: string | null = null): Dict {
71
85
  const rows = forgeProtectionRows(reading);
86
+ const readFromForge = rows.some((r) => r.state !== 'unread');
87
+ // **The slug is reported only when something was actually read, and that
88
+ // invariant is structural rather than a caller's promise.** The first
89
+ // version reported whatever the git remote resolved to, which is a LOCAL
90
+ // read that succeeds with no network — so an ordinary offline run (GitHub
91
+ // remote present, no connectivity, no opt-out) emitted a named repository
92
+ // and substituted sources while every row was `unread`. That made the
93
+ // claim this change rests on — offline output is what Phase 3.2 shipped —
94
+ // false on exactly the failed-live path the council condition names. The
95
+ // earlier verification only ever exercised the opt-out switch and read it
96
+ // as offline; the two are not the same path.
97
+ const anonymous = repo === null || !readFromForge;
98
+ const name = (s: string): string =>
99
+ anonymous ? s : s.replace(/\{owner\}\/\{repo\}/g, () => repo);
72
100
  return {
101
+ repository: readFromForge ? repo : null,
73
102
  rows: rows.map((r) => ({
74
103
  id: r.id,
75
104
  state: r.state,
76
- source: r.source,
105
+ source: name(r.source),
77
106
  detail: r.detail,
78
107
  })),
79
- actions: protectionActions(rows),
80
- read_from_forge: rows.some((r) => r.state !== 'unread'),
108
+ actions: protectionActions(rows).map(name),
109
+ read_from_forge: readFromForge,
81
110
  };
82
111
  }
83
112
 
84
- /** The all-null reading: nothing was queried. */
85
- export const UNREAD_FORGE: ForgeReading = {
86
- rulesets: null,
87
- defaultBranch: null,
88
- allowAutoMerge: null,
89
- deployRestricted: null,
90
- };
113
+ /**
114
+ * The block as `doctor` itself builds it — AC-5.
115
+ *
116
+ * Phase 3.2 left `forgeProtectionJson` with no production caller, so the
117
+ * command the criterion names reported five `unread` rows while the forge
118
+ * satisfied all five, and the rows were only ever established by a human
119
+ * running `gh api` and reading the mapper in a test. This is the wiring that
120
+ * makes the named command answer for itself.
121
+ *
122
+ * **This IS a reversal of Phase 3.2's module boundary, and the roadmap records
123
+ * the supersession.** An earlier draft of this comment argued the opposite —
124
+ * "an addition rather than a reversal" — which a 2/2 convergent council pass
125
+ * refuted: that note recorded two boundaries, `doctor` does not reach the
126
+ * network AND the reading is injected, and this reverses both. The refuted
127
+ * argument is corrected here rather than left standing, because a comment is
128
+ * what the next reader of this module finds first and is where a refuted
129
+ * reasoning would get reused.
130
+ *
131
+ * What survives is the AVAILABILITY property, which is narrower than the
132
+ * earlier claim and is the one the supersession rests on: every failure path in
133
+ * `forgeReadingFor` — opt-out switch, no GitHub remote, no `gh`, no
134
+ * credentials, timeout, unparseable payload, a throw — returns the same
135
+ * `UNREAD_FORGE` reading, so offline the ROWS, their `source` templates and the
136
+ * action lines are what Phase 3.2 printed. Not the whole document: the block
137
+ * gained a top-level `repository` key, which is `null` on every such path. A
138
+ * consumer validating `forge_protection` against a closed schema sees that one
139
+ * added key; "prints exactly what it printed before" was too strong and is not
140
+ * claimed.
141
+ *
142
+ * `process.env` and the git remote are read HERE rather than inside the reader,
143
+ * so the reader stays injectable and every branch above is reachable from a
144
+ * test without spawning anything.
145
+ */
146
+ export interface ForgeDeps {
147
+ readonly env?: Readonly<Record<string, string | undefined>>;
148
+ /** The subprocess runner for both `git` and `gh`. */
149
+ readonly run?: Runner;
150
+ }
151
+
152
+ export function forgeProtectionJsonFor(root: string, deps: ForgeDeps = {}): Dict {
153
+ // The composition root — budget, thunk, host and cwd wired together — had
154
+ // no seam and therefore no test, so the opt-out-before-spawn and host
155
+ // binding were asserted one layer down against fakes while a regression in
156
+ // the real wiring stayed invisible. `deps` is that seam; production passes
157
+ // nothing and gets `process.env` plus the real spawn.
158
+ const env = deps.env ?? process.env;
159
+ const run = deps.run;
160
+ const remaining = budgetOf(FORGE_TOTAL_BUDGET_MS);
161
+ const read = forgeReadingFor({
162
+ env,
163
+ // A thunk, so the opt-out short-circuits before `git` spawns.
164
+ resolveRepo: () => resolveForgeRepo(originUrl(root, run)),
165
+ // Built from the RESOLVED target, so `gh` is addressed at the host and
166
+ // directory the slug came from. The per-call ceiling is whatever is
167
+ // left of the whole-read budget, so a call admitted near the deadline
168
+ // cannot run past it.
169
+ apiFor: (t) => liveForgeApi(remaining, run, { host: t.host, cwd: root }),
170
+ remaining,
171
+ });
172
+ return forgeProtectionJson(read.reading, read.repo);
173
+ }
@@ -0,0 +1,226 @@
1
+ /**
2
+ * What this install does on the network — the two `doctor` surfaces that
3
+ * answer that question, in one module under the source-size cap.
4
+ *
5
+ * `road-to-host-traffic-knobs-that-ship` Phases 2.1 and 2.2. Its own module
6
+ * rather than a function inside `cmd_doctor.ts` for the reason
7
+ * `_lib/doctor_runtime_checks.ts` states for itself: that file is ~3,600 lines
8
+ * and `check_source_size_budget` counts every line above 1,500, so a change
9
+ * that adds lines there pays for them by taking lines away. Wiring the traffic
10
+ * section in costs two lines; `checkOfflineReadiness` moving here returns more
11
+ * than that, and lands beside a cohesive sibling rather than in an arbitrary
12
+ * bin — "can this install work with no registry" and "which host traffic is
13
+ * switched off" are the same question asked from two ends.
14
+ *
15
+ * `checkOfflineReadiness` is a PURE MOVE: same id, same statuses, same message
16
+ * and remedy strings. Its `pathExists` is `fs.statSync` in a try, matching the
17
+ * helper it used in `cmd_doctor.ts` — `existsSync` would differ on a path whose
18
+ * parent denies traversal, and this is a move, not a repair.
19
+ *
20
+ * Why the traffic section reports rather than decides: the variables below belong to the HOST, not to this package. Writing one into
21
+ * a consumer environment changes behaviour the consumer never asked to change,
22
+ * and — per the mapping measured in `docs/setup/host-traffic-environment.md` —
23
+ * could stop their background security updates as a side effect of a telemetry
24
+ * preference. So this module reads `env` and returns; it writes nothing, opens
25
+ * no socket, and touches no settings file. Two consecutive calls return equal
26
+ * payloads and leave the tree byte-identical.
27
+ *
28
+ * Why an inapplicable row is PRESENT and says so: the obvious implementation lists only the variables that resolve on the
29
+ * current host, which produces a short, clean section on a host that documents
30
+ * none of them. An operator reading that concludes their traffic is governed,
31
+ * when in fact the package simply had nothing to say. An omission that reads as
32
+ * a clean bill of health is the precise failure this surface exists to prevent,
33
+ * so a row that does not apply is emitted with the literal state
34
+ * {@link NOT_APPLICABLE} instead of being dropped.
35
+ *
36
+ * A variable that is SET on a host that does not document it still reports its
37
+ * value beside that state. The state says "this host is not known to read it";
38
+ * dropping the value as well would hide a fact the reader has.
39
+ */
40
+ import * as fs from 'node:fs';
41
+ import * as path from 'node:path';
42
+
43
+ /** The doctor's structured check row. Mirrors `cmd_doctor`'s internal `Dict`. */
44
+ export interface NetworkCheck {
45
+ id: string;
46
+ status: 'ok' | 'warn' | 'fail' | 'skipped';
47
+ message: string;
48
+ remedy: string;
49
+ /** Structural compatibility with `cmd_doctor`'s `Dict` row type. */
50
+ [k: string]: string;
51
+ }
52
+
53
+ /** The literal a row carries where the host has no documented meaning for it. */
54
+ export const NOT_APPLICABLE = 'not applicable on this host';
55
+
56
+ /** The documentation surface every row points at. One copy of the mapping. */
57
+ export const TRAFFIC_DOC = 'docs/setup/host-traffic-environment.md';
58
+
59
+ /** Host id used when nothing in the environment identifies the host. */
60
+ export const UNKNOWN_HOST = 'unknown';
61
+
62
+ /**
63
+ * One documented host traffic variable.
64
+ *
65
+ * `hosts` lists the host ids whose shipped build was actually READ for this
66
+ * variable — never a host somebody expects to behave the same way. A host
67
+ * absent from the list gets {@link NOT_APPLICABLE}, which is the honest answer
68
+ * and also the safe one.
69
+ */
70
+ export interface TrafficVariable {
71
+ readonly variable: string;
72
+ readonly hosts: readonly string[];
73
+ /** Host build and date the row was read from, e.g. `claude-code 2.1.284 · 2026-09-29`. */
74
+ readonly checked_against: string;
75
+ /**
76
+ * May THIS PACKAGE write the variable into the host env block?
77
+ *
78
+ * `false` on both traffic variables and it is not a policy setting — it is a
79
+ * structural fact: `install.ts`'s `WRITABLE_HOST_ENV` is a two-row allow
80
+ * table and the settings schema admits only those two keys, so neither
81
+ * traffic variable is expressible anywhere in the write path. Reported
82
+ * because a consumer reading `set` on a row deserves to know whether this
83
+ * package could have been the one that set it.
84
+ */
85
+ readonly writable_by_this_package: boolean;
86
+ }
87
+
88
+ /** The host build every row below was read from. One place to bump on a re-check. */
89
+ const CHECKED = 'claude-code 2.1.284 · 2026-09-29';
90
+
91
+ /**
92
+ * The documented set, in the order `docs/setup/host-traffic-environment.md`
93
+ * lists it. What each one governs — and does not — lives in that page and is
94
+ * deliberately not duplicated here: a second copy is a second thing to keep
95
+ * true, and this one would be the copy nobody re-reads.
96
+ */
97
+ export const TRAFFIC_VARIABLES: readonly TrafficVariable[] = [
98
+ // Both traffic variables reach the host's update-disabled resolver, the
99
+ // blanket one as its third rung, so neither is writable by this package —
100
+ // owner-decided 2026-09-30.
101
+ { variable: 'CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC', hosts: ['claude-code'], checked_against: CHECKED, writable_by_this_package: false },
102
+ { variable: 'DISABLE_AUTOUPDATER', hosts: ['claude-code'], checked_against: CHECKED, writable_by_this_package: false },
103
+ // Size caps. Neither reaches the updater resolver, and neither suppresses a
104
+ // call, an update check or a download.
105
+ { variable: 'BASH_MAX_OUTPUT_LENGTH', hosts: ['claude-code'], checked_against: CHECKED, writable_by_this_package: true },
106
+ { variable: 'MAX_MCP_OUTPUT_TOKENS', hosts: ['claude-code'], checked_against: CHECKED, writable_by_this_package: true },
107
+ ];
108
+
109
+ /** An environment map, in the shape `process.env` has. */
110
+ export type EnvMap = Readonly<Record<string, string | undefined>>;
111
+
112
+ /** Which host this process is running under, and whether that was observed. */
113
+ export interface TrafficHost {
114
+ host: string;
115
+ observed: boolean;
116
+ }
117
+
118
+ /**
119
+ * Resolve the host from the environment it exported.
120
+ *
121
+ * `observed` carries the same distinction `routing:doctor` draws for its own
122
+ * platform field: a host that identified itself is a fact, and the absence of
123
+ * any marker is NOT evidence of a different host — only evidence that nobody
124
+ * said. An unidentified host resolves to {@link UNKNOWN_HOST}, which documents
125
+ * no variable, so every row reads {@link NOT_APPLICABLE} rather than reporting
126
+ * against a host this code guessed at.
127
+ */
128
+ export function resolveTrafficHost(env: EnvMap): TrafficHost {
129
+ const claude =
130
+ (env['CLAUDECODE'] ?? '').trim() !== '' || (env['CLAUDE_CODE_SESSION_ID'] ?? '').trim() !== '';
131
+ if (claude) return { host: 'claude-code', observed: true };
132
+ return { host: UNKNOWN_HOST, observed: false };
133
+ }
134
+
135
+ /** One emitted row: the variable, its state on this host, and its value if set. */
136
+ export interface TrafficRow {
137
+ variable: string;
138
+ state: string;
139
+ value: string | null;
140
+ checked_against: string;
141
+ writable_by_this_package: boolean;
142
+ }
143
+
144
+ /**
145
+ * Build one row. A variable the host does not document reports
146
+ * {@link NOT_APPLICABLE} whether or not it happens to be exported.
147
+ */
148
+ function trafficRow(spec: TrafficVariable, env: EnvMap, host: string): TrafficRow {
149
+ const raw = env[spec.variable];
150
+ const present = raw !== undefined && raw !== '';
151
+ const applicable = spec.hosts.includes(host);
152
+ return {
153
+ variable: spec.variable,
154
+ state: applicable ? (present ? 'set' : 'unset') : NOT_APPLICABLE,
155
+ value: present ? String(raw) : null,
156
+ checked_against: spec.checked_against,
157
+ writable_by_this_package: spec.writable_by_this_package,
158
+ };
159
+ }
160
+
161
+ /**
162
+ * The `traffic_environment` block of `agent-config doctor --json`.
163
+ *
164
+ * Read-only by construction: the only input is the injected `env` map, and the
165
+ * only output is a fresh object. `hostOverride` is the test seam Phase 2.2's
166
+ * non-Claude fixture needs, and is never passed by the production call site.
167
+ *
168
+ * `reporting_read_only` replaced a flat `read_only: true` on 2026-09-30, and the
169
+ * rename is the point rather than cosmetics: this REPORT is still read-only, but
170
+ * the PACKAGE is no longer, so the old key had become a true statement about the
171
+ * wrong subject. Two of the four rows are now writable by the installer when the
172
+ * consumer sets a cap, and `writable_by_this_package` says which — a consumer
173
+ * reading `set` deserves to know whether we could have been the one who set it.
174
+ */
175
+ export function trafficEnvironmentJson(
176
+ env: EnvMap,
177
+ hostOverride?: TrafficHost,
178
+ ): Record<string, unknown> {
179
+ const resolved = hostOverride ?? resolveTrafficHost(env);
180
+ return {
181
+ host: resolved.host,
182
+ host_observed: resolved.observed,
183
+ doc: TRAFFIC_DOC,
184
+ reporting_read_only: true,
185
+ writable_by_this_package: TRAFFIC_VARIABLES.filter((v) => v.writable_by_this_package).map(
186
+ (v) => v.variable,
187
+ ),
188
+ rows: TRAFFIC_VARIABLES.map((spec) => trafficRow(spec, env, resolved.host)),
189
+ };
190
+ }
191
+
192
+ /** `fs.statSync` in a try — the `cmd_doctor.ts` helper this move preserves. */
193
+ function pathExists(p: string): boolean {
194
+ try {
195
+ fs.statSync(p);
196
+ return true;
197
+ } catch {
198
+ return false;
199
+ }
200
+ }
201
+
202
+ /**
203
+ * The `offline-readiness` check — a pure move out of `cmd_doctor.ts`.
204
+ *
205
+ * `packageRoot` is passed IN rather than resolved here for the reason
206
+ * `doctor_runtime_checks.ts` gives for its `which` parameter: `_package_root`
207
+ * lives in `cmd_doctor.ts` and importing it back would make the two modules
208
+ * circular.
209
+ */
210
+ export function checkOfflineReadiness(packageRoot: string): NetworkCheck {
211
+ const script = path.join(packageRoot, 'src', 'scripts', 'hermetic-install.sh');
212
+ if (!pathExists(script)) {
213
+ return {
214
+ id: 'offline-readiness',
215
+ status: 'warn',
216
+ message: 'src/scripts/hermetic-install.sh not found in package',
217
+ remedy: 'reinstall @event4u/agent-config or pull missing files',
218
+ };
219
+ }
220
+ return {
221
+ id: 'offline-readiness',
222
+ status: 'ok',
223
+ message: 'verified-offline install entrypoint present',
224
+ remedy: '',
225
+ };
226
+ }