@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
@@ -48,9 +48,8 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
48
48
  import { hookSectionEnabled } from '../_lib/hook_settings.js';
49
49
  import { isUiPath, isUiTreePath } from '../_lib/ui_surface.js';
50
50
  import { readHookStdin } from './hook_stdin.js';
51
+ import { EXIT_ALLOW, EXIT_WARN } from './exit_codes.js';
51
52
 
52
- const EXIT_ALLOW = 0;
53
- const EXIT_WARN = 2;
54
53
  /** Nudges per session before the valve goes silent. */
55
54
  export const MAX_NUDGES = 2;
56
55
 
@@ -76,6 +76,7 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
76
76
  import type * as YamlModule from 'yaml';
77
77
 
78
78
  import { build_merge_entries } from './_lib/json_pointers.js';
79
+ import { withHostEnv } from './_lib/host_env_write.js';
79
80
  import { jsonDumpsCompact, jsonDumpsIndent } from './_lib/json_python_parity.js';
80
81
  import { is_claude_builtin_name } from './_lib/claude_builtin_names.js';
81
82
  import * as installed_lock from './_lib/installed_lock.js';
@@ -996,10 +997,8 @@ function _heal_legacy_claude_plugin_ids(p: string): string[] {
996
997
  function ensure_claude_bridge(project_root: string, force: boolean): Record<string, unknown>[] {
997
998
  const target = path.join(project_root, '.claude', 'settings.json');
998
999
  const healed = _heal_legacy_claude_plugin_ids(target);
999
- for (const pid of healed) {
1000
- success(`.claude/settings.json: removed stale plugin id \`${pid}\``);
1001
- }
1002
- const bridge = { enabledPlugins: { [CLAUDE_PLUGIN_ID]: true } };
1000
+ for (const pid of healed) success(`.claude/settings.json: removed stale plugin id \`${pid}\``);
1001
+ const bridge = withHostEnv({ enabledPlugins: { [CLAUDE_PLUGIN_ID]: true } }, project_root, success);
1003
1002
  return merge_json_file(target, bridge, force || healed.length > 0, '.claude/settings.json');
1004
1003
  }
1005
1004
 
@@ -0,0 +1,419 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * A deny never names its own kill switch.
4
+ *
5
+ * road-to-a-kernel-that-guards-its-plumbing 1.3.
6
+ *
7
+ * A blocking concern refuses a tool call and then prints a message to the one
8
+ * party that wanted the call to succeed. If that message names the environment
9
+ * variable, the `export` line or the settings key that turns the guard off, the
10
+ * refusal is a speed bump with instructions attached. The reader most motivated
11
+ * to follow them is the agent that was just refused.
12
+ *
13
+ * The rule is therefore about the OUTPUT, not about the mechanism: a kill
14
+ * switch may exist, may be documented in the concern's header, may be listed in
15
+ * the manifest and in the contract. It may not appear in the text the refusal
16
+ * prints. Two of the nine blocking concerns carry a kill switch today and both
17
+ * name it only in a comment — this gate is what keeps that true, rather than
18
+ * true-so-far.
19
+ *
20
+ * WHAT IS SCANNED, stated precisely because the boundary is the whole design.
21
+ *
22
+ * The corpus is the `severity: blocking` concerns in `hook_manifest.yaml`, read
23
+ * from the manifest rather than listed here, so a guard added there joins the
24
+ * corpus without an edit. Within each, the unit is a MESSAGE-SHAPED string
25
+ * literal — comments stripped first, then any literal containing whitespace.
26
+ *
27
+ * The prose test is the discriminator, and it is what makes the settings-key
28
+ * rule usable at all: `'hooks.enabled'` as a lookup key is how a concern reads
29
+ * its own configuration and is not a message, while
30
+ * `'set hooks.enabled: false to skip'` is. A rule that fired on the bare key
31
+ * would fire on every concern that reads a setting, which is the false-positive
32
+ * shape that gets a young gate disabled.
33
+ *
34
+ * WHAT IT DOES NOT SEE, so nobody reads more into a green than is there:
35
+ * a message assembled at runtime from non-literal parts, a message in a module
36
+ * the concern imports, and a kill switch named in a way none of the three
37
+ * patterns matches. This is a pattern gate over source text, not a proof about
38
+ * emitted bytes.
39
+ *
40
+ * Exit codes: 0 clean · 1 a deny names a bypass · 2 the corpus could not be read.
41
+ */
42
+
43
+ import fs from 'node:fs';
44
+ import os from 'node:os';
45
+ import path from 'node:path';
46
+ import { fileURLToPath, pathToFileURL } from 'node:url';
47
+
48
+ import * as yaml from 'js-yaml';
49
+
50
+ import { GateLedger } from './_lib/gate_ledger.js';
51
+ import { runGateCli, runSelfTest } from './_lib/gate_self_test.js';
52
+ import { assertScanned, reportScanned } from './_lib/scan_scope.js';
53
+
54
+ const _HERE = fileURLToPath(import.meta.url);
55
+ const ROOT = path.resolve(path.dirname(_HERE), '..', '..');
56
+
57
+ export const GATE = 'lint_deny_text';
58
+
59
+ const MANIFEST_REL = 'src/scripts/hook_manifest.yaml';
60
+
61
+ /**
62
+ * Top-level settings keys, so `hooks.enabled` reads as a settings path and
63
+ * `payload.tool_input` does not.
64
+ *
65
+ * A fixed list rather than a parse of the template: the template is a large
66
+ * file with placeholder substitutions, and the gate needs the discriminator to
67
+ * be stable and reviewable in a diff. Adding a top-level settings section is
68
+ * the moment to add it here; a missing entry costs a miss, never a false
69
+ * refusal.
70
+ */
71
+ export const SETTINGS_ROOTS: readonly string[] = [
72
+ 'ai_team',
73
+ 'augment',
74
+ 'chat_history',
75
+ 'code_style',
76
+ 'commands',
77
+ 'consistency',
78
+ 'continuity',
79
+ 'cost',
80
+ 'decision_engine',
81
+ 'delivery',
82
+ 'design',
83
+ 'emergency',
84
+ 'execution',
85
+ 'github',
86
+ 'hooks',
87
+ 'knowledge',
88
+ 'memory',
89
+ 'model',
90
+ 'onboarding',
91
+ 'personal',
92
+ 'pipelines',
93
+ 'planning',
94
+ 'profile',
95
+ 'project',
96
+ 'projection',
97
+ 'quality',
98
+ 'reasoning',
99
+ 'roadmap',
100
+ 'screenshots',
101
+ 'subagents',
102
+ 'telegraph',
103
+ 'tokens',
104
+ 'update_check',
105
+ 'verbosity',
106
+ ];
107
+
108
+ export interface DenyFinding {
109
+ readonly concern: string;
110
+ readonly file: string;
111
+ readonly kind: 'env-assignment' | 'export-line' | 'settings-key';
112
+ readonly text: string;
113
+ }
114
+
115
+ /**
116
+ * Characters after which a `/` begins a REGEX literal rather than a division.
117
+ *
118
+ * Regex handling is not a nicety here, it is the difference between a working
119
+ * gate and a nonsense one — measured on the first run against the real tree.
120
+ * `turn_end_gate_hook.ts` carries `/\bI (wi)ll\b/i`; without this the
121
+ * apostrophe inside a pattern like `/\bI'll\b/` opens a phantom string literal
122
+ * that swallows the next few hundred characters of real code and comments, and
123
+ * the gate then reports `export function …` as deny text. Three such
124
+ * false positives, all in one file, all from one unhandled token class.
125
+ */
126
+ const REGEX_PRECEDERS = new Set('(,=:[!&|?{};+-*%~^<>'.split(''));
127
+
128
+ /** Strip `//` and block comments, preserving string literals and regexes. */
129
+ export function stripComments(src: string): string {
130
+ let out = '';
131
+ let i = 0;
132
+ let quote: string | null = null;
133
+ let lastSignificant = '';
134
+ while (i < src.length) {
135
+ const c = src[i] as string;
136
+ const next = src[i + 1];
137
+ if (quote !== null) {
138
+ out += c;
139
+ if (c === '\\') {
140
+ out += src[i + 1] ?? '';
141
+ i += 2;
142
+ continue;
143
+ }
144
+ if (c === quote) {
145
+ quote = null;
146
+ }
147
+ i += 1;
148
+ continue;
149
+ }
150
+ if (c === "'" || c === '"' || c === '`') {
151
+ quote = c;
152
+ out += c;
153
+ i += 1;
154
+ lastSignificant = c;
155
+ continue;
156
+ }
157
+ if (c === '/' && next === '/') {
158
+ while (i < src.length && src[i] !== '\n') i += 1;
159
+ continue;
160
+ }
161
+ if (c === '/' && next === '*') {
162
+ i += 2;
163
+ while (i < src.length && !(src[i] === '*' && src[i + 1] === '/')) i += 1;
164
+ i += 2;
165
+ continue;
166
+ }
167
+ if (c === '/' && (lastSignificant === '' || REGEX_PRECEDERS.has(lastSignificant))) {
168
+ // Regex literal — consumed whole, emitted as a placeholder so the
169
+ // scanner below never sees its contents as a message.
170
+ i += 1;
171
+ let inClass = false;
172
+ while (i < src.length) {
173
+ const r = src[i] as string;
174
+ if (r === '\\') {
175
+ i += 2;
176
+ continue;
177
+ }
178
+ if (r === '[') inClass = true;
179
+ else if (r === ']') inClass = false;
180
+ else if (r === '/' && !inClass) break;
181
+ else if (r === '\n') break;
182
+ i += 1;
183
+ }
184
+ i += 1;
185
+ out += '0';
186
+ lastSignificant = '0';
187
+ continue;
188
+ }
189
+ out += c;
190
+ i += 1;
191
+ if (!/\s/.test(c)) {
192
+ lastSignificant = c;
193
+ }
194
+ }
195
+ return out;
196
+ }
197
+
198
+ /** Message-shaped string literals — comments already stripped. */
199
+ export function messageLiterals(src: string): string[] {
200
+ const out: string[] = [];
201
+ const re = /'((?:[^'\\]|\\.)*)'|"((?:[^"\\]|\\.)*)"|`((?:[^`\\]|\\.)*)`/g;
202
+ for (const m of src.matchAll(re)) {
203
+ const body = m[1] ?? m[2] ?? m[3] ?? '';
204
+ if (/\s/.test(body)) {
205
+ out.push(body);
206
+ }
207
+ }
208
+ return out;
209
+ }
210
+
211
+ const ENV_ASSIGNMENT_RE = /AGENT_CONFIG_[A-Z0-9_]+\s*=/;
212
+ const EXPORT_RE = /(^|[\s`'"(])export\s/;
213
+
214
+ function settingsKeyIn(text: string): boolean {
215
+ for (const root of SETTINGS_ROOTS) {
216
+ const re = new RegExp(`(^|[^A-Za-z0-9_.])${root}\\.[a-z][a-z0-9_]*`);
217
+ if (re.test(text)) {
218
+ return true;
219
+ }
220
+ }
221
+ return false;
222
+ }
223
+
224
+ /** Findings for one concern's source text. */
225
+ export function scanSource(concern: string, file: string, src: string): DenyFinding[] {
226
+ const out: DenyFinding[] = [];
227
+ for (const text of messageLiterals(stripComments(src))) {
228
+ if (ENV_ASSIGNMENT_RE.test(text)) {
229
+ out.push({ concern, file, kind: 'env-assignment', text });
230
+ continue;
231
+ }
232
+ if (EXPORT_RE.test(text)) {
233
+ out.push({ concern, file, kind: 'export-line', text });
234
+ continue;
235
+ }
236
+ if (settingsKeyIn(text)) {
237
+ out.push({ concern, file, kind: 'settings-key', text });
238
+ }
239
+ }
240
+ return out;
241
+ }
242
+
243
+ /** `severity: blocking` concerns, as `[name, script]`, read from the manifest. */
244
+ export function blockingConcerns(manifestText: string): Array<[string, string]> {
245
+ const doc = yaml.load(manifestText) as { concerns?: Record<string, Record<string, unknown>> };
246
+ const concerns = doc?.concerns ?? {};
247
+ const out: Array<[string, string]> = [];
248
+ for (const [name, body] of Object.entries(concerns)) {
249
+ if (String(body['severity'] ?? '') !== 'blocking') continue;
250
+ const script = body['script'];
251
+ if (typeof script === 'string') out.push([name, script]);
252
+ }
253
+ return out.sort((a, b) => a[0].localeCompare(b[0]));
254
+ }
255
+
256
+ export function scan(root: string): { findings: DenyFinding[]; scanned: number } {
257
+ const manifest = path.join(root, MANIFEST_REL);
258
+ if (!fs.existsSync(manifest)) {
259
+ return { findings: [], scanned: 0 };
260
+ }
261
+ const findings: DenyFinding[] = [];
262
+ let scanned = 0;
263
+ for (const [name, script] of blockingConcerns(fs.readFileSync(manifest, 'utf8'))) {
264
+ const abs = path.join(root, script);
265
+ if (!fs.existsSync(abs)) continue;
266
+ scanned += 1;
267
+ findings.push(...scanSource(name, script, fs.readFileSync(abs, 'utf8')));
268
+ }
269
+ return { findings, scanned };
270
+ }
271
+
272
+ const REASON: Readonly<Record<DenyFinding['kind'], string>> = {
273
+ 'env-assignment': 'names an environment kill switch, with its value',
274
+ 'export-line': 'carries an `export` line the reader can paste',
275
+ 'settings-key': 'names a settings key path',
276
+ };
277
+
278
+ export function main(argv: readonly string[]): number {
279
+ if (argv.includes('--self-test')) return selfTest();
280
+ const rootIdx = argv.indexOf('--root');
281
+ const root = rootIdx === -1 ? ROOT : (argv[rootIdx + 1] ?? ROOT);
282
+
283
+ const { findings, scanned } = scan(root);
284
+
285
+ const ledger = new GateLedger(GATE);
286
+ ledger.plan(findings.map((f) => `${f.concern}:${f.kind}:${f.text.slice(0, 40)}`));
287
+ for (const f of findings) {
288
+ ledger.fail(`${f.concern}:${f.kind}:${f.text.slice(0, 40)}`, REASON[f.kind]);
289
+ }
290
+
291
+ // A gate that read nothing has not passed. BEFORE the verdict: a manifest
292
+ // that moved, or a `severity:` key that was renamed, would otherwise
293
+ // certify an empty corpus clean.
294
+ assertScanned({
295
+ gate: GATE,
296
+ scanned,
297
+ units: 'blocking concern(s)',
298
+ roots: [MANIFEST_REL],
299
+ });
300
+ reportScanned({
301
+ gate: GATE,
302
+ scanned,
303
+ units: 'blocking concern(s)',
304
+ roots: [MANIFEST_REL],
305
+ });
306
+ ledger.report();
307
+
308
+ if (findings.length > 0) {
309
+ for (const f of findings) {
310
+ process.stdout.write(
311
+ `❌ ${GATE}: ${f.file} (${f.concern}) ${REASON[f.kind]}:\n ${f.text.slice(0, 200)}\n`,
312
+ );
313
+ }
314
+ process.stdout.write(
315
+ '\nA blocking concern may HAVE a kill switch. Its refusal message may not name it.\n' +
316
+ "Move the sentence to the concern's header comment, the manifest entry, or\n" +
317
+ 'docs/contracts/hook-architecture-v1.md — all three are read by people deciding\n' +
318
+ 'whether the guard should exist, and none is read by the agent it just refused.\n',
319
+ );
320
+ return 1;
321
+ }
322
+
323
+ process.stdout.write(
324
+ `✅ ${GATE}: no blocking concern names its own bypass ` +
325
+ `(${String(scanned)} concern(s) scanned).\n`,
326
+ );
327
+ return 0;
328
+ }
329
+
330
+ function selfTest(): number {
331
+ const plant = (name: string, body: string): string => {
332
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), `deny-text-${name}-`));
333
+ fs.mkdirSync(path.join(dir, 'src', 'scripts', 'hooks'), { recursive: true });
334
+ fs.writeFileSync(
335
+ path.join(dir, 'src', 'scripts', 'hook_manifest.yaml'),
336
+ [
337
+ 'schema_version: 1',
338
+ 'concerns:',
339
+ ' probe-guard:',
340
+ ' script: src/scripts/hooks/probe_guard.ts',
341
+ ' severity: blocking',
342
+ ' probe-advisory:',
343
+ ' script: src/scripts/hooks/probe_advisory.ts',
344
+ ' severity: advisory',
345
+ '',
346
+ ].join('\n'),
347
+ 'utf-8',
348
+ );
349
+ fs.writeFileSync(path.join(dir, 'src', 'scripts', 'hooks', 'probe_guard.ts'), body, 'utf-8');
350
+ fs.writeFileSync(
351
+ path.join(dir, 'src', 'scripts', 'hooks', 'probe_advisory.ts'),
352
+ "process.stderr.write('set AGENT_CONFIG_X=1 to skip this advice');\n",
353
+ 'utf-8',
354
+ );
355
+ return dir;
356
+ };
357
+ const emit = (msg: string): string => `process.stderr.write(${JSON.stringify(msg)});\n`;
358
+ const run = (root: string): number =>
359
+ runGateCli(ROOT, path.join('src', 'scripts', 'lint_deny_text.ts'), ['--root', root], ROOT);
360
+
361
+ return runSelfTest({
362
+ gate: GATE,
363
+ minCases: 7,
364
+ minRejectCases: 3,
365
+ cases: [
366
+ {
367
+ name: 'an empty corpus is refused, not certified green',
368
+ expect: 'reject',
369
+ run: () => run(fs.mkdtempSync(path.join(os.tmpdir(), 'deny-text-empty-'))),
370
+ },
371
+ {
372
+ name: 'a deny naming an env kill switch with its value fails',
373
+ expect: 'reject',
374
+ run: () => run(plant('env', emit('BLOCKED — set AGENT_CONFIG_X=1 to skip'))),
375
+ },
376
+ {
377
+ name: 'a deny carrying an export line fails',
378
+ expect: 'reject',
379
+ run: () => run(plant('export', emit('BLOCKED — run export AC_OFF=1 first'))),
380
+ },
381
+ {
382
+ name: 'a deny naming a settings key path fails',
383
+ expect: 'reject',
384
+ run: () => run(plant('settings', emit('BLOCKED — flip hooks.enabled to false'))),
385
+ },
386
+ {
387
+ name: 'a deny that names no bypass passes',
388
+ expect: 'accept',
389
+ run: () => run(plant('clean', emit('BLOCKED — rebuild it from its source'))),
390
+ },
391
+ {
392
+ name: 'the kill switch named in a COMMENT passes — the rule is about output',
393
+ expect: 'accept',
394
+ run: () =>
395
+ run(
396
+ plant(
397
+ 'comment',
398
+ '// Kill switch: AGENT_CONFIG_X=1.\n/* or AGENT_CONFIG_Y=1 */\n' +
399
+ emit('BLOCKED — rebuild it from its source'),
400
+ ),
401
+ ),
402
+ },
403
+ {
404
+ name: 'a bare settings key used as a lookup passes — it is not a message',
405
+ expect: 'accept',
406
+ run: () => run(plant('lookup', "const k = 'hooks.enabled';\nvoid k;\n")),
407
+ },
408
+ {
409
+ name: 'an ADVISORY concern naming its switch passes — only denials are scanned',
410
+ expect: 'accept',
411
+ run: () => run(plant('advisory-only', emit('BLOCKED — rebuild it'))),
412
+ },
413
+ ],
414
+ });
415
+ }
416
+
417
+ if (process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) {
418
+ process.exit(main(process.argv.slice(2)));
419
+ }