@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,322 @@
1
+ /**
2
+ * Read-surface scanner — derive the sanitize floor's coverage list from the
3
+ * imports, not from intent
4
+ * (road-to-a-sanitize-list-that-is-generated, Phase 1).
5
+ *
6
+ * WHY A SCANNER AND NOT A LIST
7
+ * ----------------------------
8
+ * `retrieval_sanitize.ts` used to carry a hand-maintained prose list of the
9
+ * read surfaces it covers, with its own header stating the rule for anything
10
+ * not on it: uncovered. That list went wrong in both directions — a path that
11
+ * reaches the outside world and IS sanitized was absent from it, and two paths
12
+ * carrying fetched bytes toward a model-facing surface were neither listed nor
13
+ * covered. The header already recorded the same failure happening once before,
14
+ * for the same reason: a list maintained by hand records what its author meant,
15
+ * and the tree records what the code does.
16
+ *
17
+ * This module derives the list. A row cannot disagree with the tree, because
18
+ * the tree is the only thing it reads.
19
+ *
20
+ * WHAT A ROW MEANS, AND WHAT IT DOES NOT
21
+ * --------------------------------------
22
+ * A row says a module matched an INBOUND shape — a recognised way outside bytes
23
+ * enter this process — and whether that module imports the sanitize floor. It
24
+ * does not say the floor is applied to the right string, at the right point, or
25
+ * at all: an import is evidence of intent, and only a read of the module
26
+ * establishes the rest. `covered` here means "the floor is in scope in this
27
+ * module", never "this module is safe".
28
+ *
29
+ * THE MISSING-ROW FAILURE, AND WHY `unclassified` EXISTS
30
+ * -----------------------------------------------------
31
+ * A generated table carries an authority a hand-written one never claimed, so
32
+ * its worst failure is not a wrong row — it is an ABSENT one. A fetch shape the
33
+ * scanner does not recognise produces no row, and a missing row reads as "no
34
+ * surface here".
35
+ *
36
+ * Two things push back. Inbound shapes are plural and named (a `fetch`, a
37
+ * `node:http(s)` request, a subprocess of a remote-fetching tool, a parse of
38
+ * markup arriving on stdin) rather than a single pattern. And a WEAK shape —
39
+ * a network call reached through an injected or computed callee, which the
40
+ * strong detectors cannot see — still emits a row, marked `unclassified`, so an
41
+ * unrecognised shape appears in the table as a visible gap instead of as
42
+ * silence. The residual is stated in the table's own header rather than left
43
+ * for a reader to discover: a module whose network access is reached through a
44
+ * shape matching neither set is still absent.
45
+ */
46
+ import * as fs from 'node:fs';
47
+ import * as path from 'node:path';
48
+
49
+ /** Coverage verdict for one read surface. */
50
+ export type SurfaceCoverage = 'covered' | 'uncovered' | 'unclassified';
51
+
52
+ /** One module that brings outside bytes in and hands a string onward. */
53
+ export interface ReadSurfaceRow {
54
+ /** Path relative to `src/scripts/`, POSIX separators. */
55
+ readonly module: string;
56
+ /** Inbound shape id that matched. */
57
+ readonly inbound: string;
58
+ /** Emit shape id that matched, or `undecided`. */
59
+ readonly emit: string;
60
+ readonly coverage: SurfaceCoverage;
61
+ /** Why the row reads the way it does — one clause, no paraphrase. */
62
+ readonly evidence: string;
63
+ }
64
+
65
+ interface ShapeDetector {
66
+ readonly id: string;
67
+ /** True when this module's source carries the shape. */
68
+ readonly match: (src: string) => boolean;
69
+ }
70
+
71
+ /** True when the module imports node's HTTP client at all. */
72
+ function _importsNodeHttp(src: string): boolean {
73
+ return /from\s+['"]node:https?['"]/.test(src);
74
+ }
75
+
76
+ /**
77
+ * Strong inbound shapes — a recognised way outside bytes enter this process.
78
+ *
79
+ * Each is deliberately narrow. `await fetch(` rather than `fetch(` keeps a
80
+ * fetch call named inside a string literal (a spawned one-liner, a doc example)
81
+ * out of the table; the node-HTTP shape requires the import as well as the call
82
+ * so a `.get(` on an unrelated object cannot produce a row.
83
+ */
84
+ export const STRONG_INBOUND: readonly ShapeDetector[] = [
85
+ {
86
+ id: 'network-fetch',
87
+ match: (s) => /\bawait\s+fetch\s*\(/.test(s),
88
+ },
89
+ {
90
+ id: 'node-http-request',
91
+ match: (s) => _importsNodeHttp(s) && /\bhttps?\s*\.\s*(?:get|request)\s*\(/.test(s),
92
+ },
93
+ {
94
+ id: 'remote-subprocess',
95
+ match: (s) =>
96
+ /\b(?:spawnSync|spawn|execSync|execFileSync|execFile)\s*\(\s*\[?\s*['"`](?:curl|wget|gh)['"`]/.test(s),
97
+ },
98
+ {
99
+ id: 'stdin-markup-parse',
100
+ match: (s) =>
101
+ /\bprocess\s*\.\s*stdin\b/.test(s) && /&(?:amp|lt|gt|quot|nbsp|#x?[0-9a-f]+);/i.test(s),
102
+ },
103
+ {
104
+ id: 'declared',
105
+ match: (s) => _DECLARATION_RE.test(s),
106
+ },
107
+ ];
108
+
109
+ /**
110
+ * A module may declare its own inbound shape when no syntactic one captures it.
111
+ *
112
+ * The shapes above are all things the scanner can SEE. A host primitive cannot
113
+ * be seen: a subagent reply, an MCP tool result and a host file-read arrive
114
+ * through a channel this package does not own, so no call site in this tree
115
+ * spells them. Risk 1 of the roadmap that produced this scanner names exactly
116
+ * that class, and the answer it demands is a visible row rather than silence.
117
+ *
118
+ * The marker is a comment at the site — `// read-surface: <shape-id>` — not a
119
+ * line in a central list, which is the arrangement this whole module exists to
120
+ * retire. A declaration says only "outside bytes arrive here"; the coverage
121
+ * verdict is still read from the imports, so declaring a surface cannot make it
122
+ * read `covered`.
123
+ */
124
+ const _DECLARATION_RE = /\/\/\s*read-surface:\s*([a-z0-9-]+)/;
125
+
126
+ /** `git` is deliberately NOT a remote command above. */
127
+ export const LOCAL_VCS_EXCLUSION =
128
+ 'a `git` subprocess reads local repository state, not outside bytes, so it is not an ' +
129
+ 'inbound shape. `git fetch` and `git ls-remote` move refs rather than content and are ' +
130
+ 'excluded with the rest — a branch name is attacker-influenceable in principle, and if ' +
131
+ 'that ever reaches a model-facing surface it needs its own row, not a widened matcher ' +
132
+ 'that would re-add every local `git status` wrapper in the tree.';
133
+
134
+ /**
135
+ * Weak inbound shapes — a network call the strong detectors cannot resolve.
136
+ *
137
+ * `await this._fetch(url, …)` is a real HTTP request behind an injected
138
+ * dependency, which is exactly the shape a strong matcher on `await fetch(`
139
+ * misses. It produces a row rather than silence; the verdict is
140
+ * `unclassified`, because the scanner cannot tell an injected fetch from an
141
+ * injected stub without executing it.
142
+ */
143
+ export const WEAK_INBOUND: readonly ShapeDetector[] = [
144
+ {
145
+ id: 'indirect-fetch',
146
+ match: (s) => /\bawait\s+(?:this|self|[a-z_$][\w$]*)\s*\.\s*_?fetch\s*\(/i.test(s),
147
+ },
148
+ ];
149
+
150
+ /** Recognised ways a module hands a string onward. */
151
+ export const EMIT_SHAPES: readonly ShapeDetector[] = [
152
+ { id: 'file-write', match: (s) => /\b(?:writeFileSync|appendFileSync|writeFile)\s*\(/.test(s) },
153
+ { id: 'stdout-write', match: (s) => /\bprocess\s*\.\s*stdout\s*\.\s*write\s*\(/.test(s) },
154
+ { id: 'exported-api', match: (s) => /^export\s+(?:async\s+)?function\s/m.test(s) },
155
+ ];
156
+
157
+ /** The sanitize floor, as an import. One hop; a transitive import is not counted. */
158
+ const _SANITIZE_IMPORT_RE =
159
+ /import\s*\{[^}]*\b(?:sanitize_text|sanitize_entry|sanitize_markup)\b[^}]*\}\s*from\s*['"][^'"]*retrieval_sanitize(?:\.js)?['"]/;
160
+
161
+ /** Test files under `src/scripts/` — fixtures, not surfaces that ship bytes. */
162
+ const _EXCLUDE_RE = /(?:^|\/)(?:__tests__|node_modules)\/|\.test\.ts$/;
163
+
164
+ /**
165
+ * Modules that define or read the coverage rather than being a surface.
166
+ *
167
+ * The scanner and its gate quote every detector pattern above as a literal, so
168
+ * they match their own matchers. Excluding them by name is the honest fix: a
169
+ * row for the scanner would be an artefact of the scanner reading itself, and
170
+ * carrying it would train a reader to skip rows.
171
+ */
172
+ const _SELF_MODULES: ReadonlySet<string> = new Set([
173
+ '_lib/retrieval_sanitize.ts',
174
+ '_lib/read_surface_scan.ts',
175
+ 'check_read_surface_coverage.ts',
176
+ ]);
177
+
178
+ /**
179
+ * Raised when a directory under the scan root cannot be read.
180
+ *
181
+ * The walk used to swallow the error and return, which made an INCOMPLETE scan
182
+ * indistinguishable from a complete one: a permission error on one subtree
183
+ * silently removed every module under it from the table, and the gate then
184
+ * reported the shortened list as matching the tree. An independent review named
185
+ * it; the direction is the same one `assertScanned` enforces at the corpus
186
+ * level, applied to the traversal that builds the corpus.
187
+ */
188
+ export class ScanTraversalError extends Error {
189
+ constructor(readonly dir: string, cause: unknown) {
190
+ super(`read_surface_scan: cannot read ${dir} — the scan is incomplete, not clean (${String(cause)})`);
191
+ this.name = 'ScanTraversalError';
192
+ }
193
+ }
194
+
195
+ /**
196
+ * Directory reader, injectable for one reason: to test the failure.
197
+ *
198
+ * The fail-closed branch below can only be exercised by a `readdirSync` that
199
+ * throws, and on ESM `vi.spyOn(fs, 'readdirSync')` cannot redefine the
200
+ * namespace property. The alternative — chmod-000 on a real directory — is not
201
+ * portable, because under root the mode is no barrier and the test then fails
202
+ * in exactly the environment it was written to survive. A narrow seam is the
203
+ * honest shape: production passes nothing and gets `fs.readdirSync`.
204
+ */
205
+ export type ReadDir = (dir: string) => fs.Dirent[];
206
+
207
+ const _defaultReadDir: ReadDir = (dir) => fs.readdirSync(dir, { withFileTypes: true });
208
+
209
+ function _walk(dir: string, base: string, out: string[], readDir: ReadDir): void {
210
+ let entries: fs.Dirent[];
211
+ try {
212
+ entries = readDir(dir);
213
+ } catch (exc) {
214
+ throw new ScanTraversalError(dir, exc);
215
+ }
216
+ for (const e of entries) {
217
+ const abs = path.join(dir, e.name);
218
+ const rel = path.relative(base, abs).split(path.sep).join('/');
219
+ if (e.isDirectory()) {
220
+ _walk(abs, base, out, readDir);
221
+ } else if (e.isFile() && rel.endsWith('.ts') && !rel.endsWith('.d.ts') && !_EXCLUDE_RE.test(rel)) {
222
+ out.push(rel);
223
+ }
224
+ }
225
+ }
226
+
227
+ /** The scan root, repo-relative. Published so the gate can name it in an error. */
228
+ export const SCAN_ROOT = 'src/scripts';
229
+
230
+ /**
231
+ * Scan every TypeScript module under `src/scripts/` and return one row per
232
+ * module that both brings outside bytes in and hands a string onward.
233
+ *
234
+ * Rows are sorted by module path so the rendered table is stable across
235
+ * filesystems.
236
+ */
237
+ export function scanReadSurfaces(repoRoot: string, readDir: ReadDir = _defaultReadDir): ReadSurfaceRow[] {
238
+ const base = path.join(repoRoot, SCAN_ROOT);
239
+ const files: string[] = [];
240
+ _walk(base, base, files, readDir);
241
+ files.sort();
242
+
243
+ const rows: ReadSurfaceRow[] = [];
244
+ for (const rel of files) {
245
+ if (_SELF_MODULES.has(rel)) continue;
246
+ let src: string;
247
+ try {
248
+ src = fs.readFileSync(path.join(base, rel), 'utf-8');
249
+ } catch {
250
+ continue;
251
+ }
252
+ const strong = STRONG_INBOUND.find((d) => d.match(src));
253
+ const weak = strong === undefined ? WEAK_INBOUND.find((d) => d.match(src)) : undefined;
254
+ if (strong === undefined && weak === undefined) continue;
255
+ // A declared surface reports the shape its own marker names, so the
256
+ // table says `ingested-corpus` rather than the detector's own id.
257
+ const strongId =
258
+ strong?.id === 'declared' ? (_DECLARATION_RE.exec(src)?.[1] ?? 'declared') : strong?.id;
259
+
260
+ const emit = EMIT_SHAPES.find((d) => d.match(src));
261
+ const sanitized = _SANITIZE_IMPORT_RE.test(src);
262
+
263
+ if (weak !== undefined) {
264
+ rows.push({
265
+ module: rel,
266
+ inbound: weak.id,
267
+ emit: emit?.id ?? 'undecided',
268
+ coverage: 'unclassified',
269
+ evidence: 'network access through a callee the scanner cannot resolve — read the module',
270
+ });
271
+ continue;
272
+ }
273
+ if (emit === undefined) {
274
+ rows.push({
275
+ module: rel,
276
+ inbound: strongId ?? 'unknown',
277
+ emit: 'undecided',
278
+ coverage: 'unclassified',
279
+ evidence: 'inbound bytes with no recognised emit — the onward path is undecided',
280
+ });
281
+ continue;
282
+ }
283
+ rows.push({
284
+ module: rel,
285
+ inbound: strongId ?? 'unknown',
286
+ emit: emit.id,
287
+ coverage: sanitized ? 'covered' : 'uncovered',
288
+ evidence: sanitized
289
+ ? 'imports the sanitize floor'
290
+ : 'no sanitize import — fetched bytes reach the emit unfiltered',
291
+ });
292
+ }
293
+ return rows;
294
+ }
295
+
296
+ /** Counts by verdict, for the gate's scanned line and for the table header. */
297
+ export function tally(rows: readonly ReadSurfaceRow[]): Record<SurfaceCoverage, number> {
298
+ const out: Record<SurfaceCoverage, number> = { covered: 0, uncovered: 0, unclassified: 0 };
299
+ for (const r of rows) out[r.coverage] += 1;
300
+ return out;
301
+ }
302
+
303
+ /** Render the rows as the markdown table body committed to the contract doc. */
304
+ export function renderTable(rows: readonly ReadSurfaceRow[]): string {
305
+ const lines = [
306
+ '| module (under `src/scripts/`) | inbound shape | emit shape | coverage | evidence |',
307
+ '|---|---|---|---|---|',
308
+ ];
309
+ for (const r of rows) {
310
+ lines.push(
311
+ `| \`${r.module}\` | \`${r.inbound}\` | \`${r.emit}\` | **${r.coverage}** | ${r.evidence} |`,
312
+ );
313
+ }
314
+ const t = tally(rows);
315
+ lines.push('');
316
+ lines.push(
317
+ `${String(rows.length)} read surface(s): ` +
318
+ `${String(t.covered)} covered, ${String(t.uncovered)} uncovered, ` +
319
+ `${String(t.unclassified)} unclassified.`,
320
+ );
321
+ return lines.join('\n');
322
+ }
@@ -574,7 +574,8 @@ export function section_publication_blockers(
574
574
  'fails, so a missing line means the collection failed during `task release`. ' +
575
575
  'Fix the collection, then render the number and put it on the section — ' +
576
576
  `the release commit already exists on ${where}, so amend it there. Do not ` +
577
- 'invent the count: `npx vitest list | wc -l` is the same probe the writer runs.',
577
+ 'invent the count: `npx vitest list --staticParse=false | grep -c .` is the same ' +
578
+ 'probe the writer runs.',
578
579
  );
579
580
  }
580
581
  return out;
@@ -16,26 +16,58 @@
16
16
  * Codepoint classes are shared with `lint_hidden_unicode` (one source of truth
17
17
  * for what counts as a hidden-instruction vector).
18
18
  *
19
- * WHERE IT ACTUALLY RUNS — measured end-to-end, not asserted. Keep this list
20
- * honest: an earlier version of this header named surfaces by intent, and the
21
- * legacy-envelope gap below went unnoticed for exactly that reason. Every
22
- * claim here is backed by a probe row in
23
- * `agents/evidence/reports/sanitize-floor-wiring.md`:
19
+ * WHERE IT ACTUALLY RUNS — read the table, which is GENERATED:
24
20
  *
25
- * - `retrieve_v1()` — yes (`memory_lookup.ts` calls `sanitize_entry`)
26
- * - `memory_get_v1()` — yes (same)
27
- * - MCP `memory_lookup` / `memory_get` tools — inherited from the two above
28
- * - `retrieve()` / `retrieve_with_meta()` — yes, since the S0.0 wiring fix;
29
- * before it they emitted every vector intact, and so did the CLI default
30
- * (`--envelope legacy`), which is the path the rules document
31
- * - inter-agent channels (`ai_team` / `ai_council` model replies) — the
32
- * inbound parse choke point only; the subagent boundary itself is a HOST
33
- * primitive this package cannot reach (see the report, § S0.0b)
21
+ * docs/contracts/retrieval-read-surfaces.md
34
22
  *
35
- * Anything not on that list is uncovered. Do not widen the list without a
36
- * probe row to back it.
23
+ * This header used to carry that list inline, and the list was wrong in BOTH
24
+ * directions: a path that reaches the outside world and IS sanitized was
25
+ * absent from it, and two paths carrying fetched bytes toward a model-facing
26
+ * surface were neither listed nor covered. An earlier version had already
27
+ * failed the same way — it named surfaces by intent, and the legacy-envelope
28
+ * gap went unnoticed for exactly that reason. Twice is a property of the
29
+ * mechanism, not of the entries, which is why the list is now derived from the
30
+ * import graph by `check_read_surface_coverage` and this header points at it
31
+ * instead of copying it. A divergence between the two FAILS that gate; two
32
+ * copies is how it went stale.
33
+ *
34
+ * Anything absent from the table is uncovered, and a module the generator
35
+ * cannot classify appears there as an `unclassified` ROW rather than being
36
+ * omitted — a missing row would read as no surface at all.
37
+ *
38
+ * One boundary the table cannot move: the subagent boundary itself is a HOST
39
+ * primitive this package cannot reach, so the inter-agent channels are covered
40
+ * at the inbound parse choke point and nowhere else.
41
+ *
42
+ * loss_class: ephemeral-lossy
43
+ *
44
+ * `sanitize_text` drops hidden-instruction codepoints, control-char noise and
45
+ * invisible fillers, and caps the result at MAX_FIELD_CHARS. None of that is
46
+ * reconstructible from the output, and for the dropped vectors the
47
+ * unrecoverability IS the guarantee — a retrieval path that could hand back
48
+ * the vector it stripped would not be a floor. So the class is
49
+ * `ephemeral-lossy` for the module, even though `sanitize_markup` below
50
+ * returns structured removals for the layer it composes: a class names what a
51
+ * caller can recover from the output, and `sanitize_text` returns a string.
52
+ *
53
+ * THE STRUCTURAL LAYER IS A SEPARATE MODULE. Everything below is a CODEPOINT
54
+ * floor. Fetched markup also hides content STRUCTURALLY — an HTML comment, a
55
+ * `<template>` block, a `style="display:none"` span — which no codepoint
56
+ * predicate can see. That class is covered by `_lib/structural_hiding.ts`,
57
+ * composed with this floor by `sanitize_markup` below, and it is covered for
58
+ * fetched MARKUP only: a caller holding plain text needs nothing from it.
59
+ *
60
+ * WHAT THAT LAYER STILL DOES NOT SEE is a register in its own file —
61
+ * `STRUCTURAL_HIDING_GAPS` — and every surface claiming the coverage cites the
62
+ * register alongside the claim, because a detector cited without its gaps is
63
+ * how the claim becomes a false green. No recall or coverage percentage is
64
+ * published for it anywhere; a rate needs a frozen corpus and none exists.
65
+ * `aria-hidden` is deliberately NOT one of its channels
66
+ * (`STRUCTURAL_HIDING_NON_CHANNELS` says why).
37
67
  */
38
68
  import { _classify } from '../lint_hidden_unicode.js';
69
+ import { strip_structural_hiding } from './structural_hiding.js';
70
+ import type { StructuralRemoval } from './structural_hiding.js';
39
71
  import { TOKEN_RE, classifyToken } from './confusables.js';
40
72
 
41
73
  /** Hard per-field length cap — bounds a runaway/adversarial body. */
@@ -85,6 +117,35 @@ export function sanitize_text(s: string): string {
85
117
  return out.length > MAX_FIELD_CHARS ? out.slice(0, MAX_FIELD_CHARS) : out;
86
118
  }
87
119
 
120
+ /**
121
+ * The fetched-markup floor: structural pre-pass, then the codepoint floor.
122
+ *
123
+ * ORDER IS LOAD-BEARING. The structural pass runs FIRST, because a codepoint
124
+ * floor applied first would sanitize text that is about to be discarded and,
125
+ * worse, would leave a hidden subtree looking clean. Running it second means
126
+ * every byte that survives the structural pass still clears the codepoint
127
+ * floor — including the text that was only reachable once markup was removed.
128
+ *
129
+ * Use this for fetched MARKUP. For plain text `sanitize_text` is the whole
130
+ * floor and this adds nothing but a parse.
131
+ *
132
+ * The removals are RETURNED, not logged and not swallowed: a caller debugging
133
+ * a corrupted result needs to know what went and where it was. See the removal
134
+ * policy in `structural_hiding.ts`.
135
+ */
136
+ export function sanitize_markup(markup: string): {
137
+ text: string;
138
+ removals: readonly StructuralRemoval[];
139
+ truncated: boolean;
140
+ } {
141
+ const stripped = strip_structural_hiding(markup);
142
+ return {
143
+ text: sanitize_text(stripped.text),
144
+ removals: stripped.removals,
145
+ truncated: stripped.truncated,
146
+ };
147
+ }
148
+
88
149
  /**
89
150
  * Report the INVISIBLE layer without removing it.
90
151
  *
@@ -0,0 +1,108 @@
1
+ /**
2
+ * The shell shapes that mean "this path is being WRITTEN", shared by the
3
+ * PreToolUse guards that refuse a write to a governed file.
4
+ *
5
+ * WHY IT IS SHARED. `block_kernel_rule_writes.ts` grew this parser after the
6
+ * S0.2 spike measured a real gap: a guard keyed on the TOOL NAME let
7
+ * `Bash sed -i … src/rules/commit-policy.md` reach the outcome the Write
8
+ * branch refuses, and a two-step `Write` → `Bash mv` sequence did the same with
9
+ * every individual step allowed. `block_plumbing_writes.ts`
10
+ * (road-to-a-kernel-that-guards-its-plumbing 1.2) refuses a different file set
11
+ * against the same shapes. Two copies of a shell-shape parser is two places a
12
+ * verb can be added to one and forgotten in the other, and the guard that
13
+ * silently stopped recognising `tee` would look exactly like the guard that
14
+ * never had to.
15
+ *
16
+ * DELIBERATELY NARROW (council 2026-08-02 cut, option ii, recorded on the
17
+ * kernel guard and inherited here unchanged): only redirection into the path,
18
+ * an in-place `sed`, a `tee`/`truncate`/`rm`/`shred` naming it, or a
19
+ * `mv`/`cp`/`install`/`rsync` whose DESTINATION it is. Reads stay allowed —
20
+ * `cat`, `grep`, `head`, `diff`, `git show` carry none of these shapes — and no
21
+ * attempt is made to understand arbitrary shell. Recognising every conceivable
22
+ * write verb would make this a shell sandbox, which is the failure mode the
23
+ * council named.
24
+ *
25
+ * KNOWN-OPEN, stated rather than implied: a write routed through an
26
+ * interpreter (`python -c`, a heredoc fed to `sh`) carries none of these
27
+ * shapes and is not detected. This is a guard, not write mediation.
28
+ */
29
+ import * as path from 'node:path';
30
+
31
+ /** In-place mutators that name their target as a plain argument. */
32
+ const MUTATOR_VERBS: ReadonlySet<string> = new Set(['sed', 'tee', 'truncate', 'rm', 'shred']);
33
+
34
+ /** Verbs whose LAST positional argument is the destination. */
35
+ const DEST_LAST_VERBS: ReadonlySet<string> = new Set(['mv', 'cp', 'install', 'rsync']);
36
+
37
+ /**
38
+ * The first governed target a command WRITES, or `null`.
39
+ *
40
+ * @param command the raw shell string off the tool envelope
41
+ * @param match classifies one token: a non-null return names the governed
42
+ * target that token resolves to, and is what this returns
43
+ */
44
+ export function shellWriteTarget<T>(command: string, match: (token: string) => T | null): T | null {
45
+ const tokens = command.split(/\s+/).filter((t) => t.length > 0);
46
+ const stripped = tokens.map((t) => t.replace(/^['"]|['"]$/g, ''));
47
+
48
+ // Cheap reject first: no governed path in the string at all.
49
+ if (!stripped.some((t) => match(t) !== null)) {
50
+ return null;
51
+ }
52
+
53
+ // 1. Redirection into the path: `> p`, `>> p`, `>p`, `>>p`.
54
+ for (let i = 0; i < stripped.length; i += 1) {
55
+ const tok = stripped[i] as string;
56
+ const inline = /^>{1,2}(.+)$/.exec(tok);
57
+ if (inline) {
58
+ const r = match(inline[1] as string);
59
+ if (r !== null) {
60
+ return r;
61
+ }
62
+ }
63
+ if (tok === '>' || tok === '>>') {
64
+ const next = stripped[i + 1];
65
+ if (typeof next === 'string') {
66
+ const r = match(next);
67
+ if (r !== null) {
68
+ return r;
69
+ }
70
+ }
71
+ }
72
+ }
73
+
74
+ // 2. A mutator verb anywhere in the pipeline, with the path as an argument.
75
+ // `sed` only counts in its in-place form — `sed 's/x/y/' file` prints.
76
+ const verbs = new Set<string>();
77
+ let prevWasSeparator = true;
78
+ for (const tok of stripped) {
79
+ if (['&&', '||', ';', '|'].includes(tok)) {
80
+ prevWasSeparator = true;
81
+ continue;
82
+ }
83
+ if (prevWasSeparator && !tok.startsWith('-')) {
84
+ verbs.add(path.basename(tok));
85
+ prevWasSeparator = false;
86
+ }
87
+ }
88
+ const sedInPlace = verbs.has('sed') && stripped.some((t) => /^-i/.test(t));
89
+ for (const verb of verbs) {
90
+ if (verb === 'sed' && !sedInPlace) {
91
+ continue;
92
+ }
93
+ if (MUTATOR_VERBS.has(verb)) {
94
+ const hit = stripped.map(match).find((r) => r !== null);
95
+ if (hit !== undefined && hit !== null) {
96
+ return hit;
97
+ }
98
+ }
99
+ if (DEST_LAST_VERBS.has(verb)) {
100
+ const last = stripped[stripped.length - 1] as string;
101
+ const r = match(last);
102
+ if (r !== null) {
103
+ return r;
104
+ }
105
+ }
106
+ }
107
+ return null;
108
+ }