@miller-tech/uap 1.221.0 → 2.0.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 (238) hide show
  1. package/README.md +19 -9
  2. package/config/exl3-profiles/qwen38-27b-exl3.env +68 -0
  3. package/config/llama-profiles/qwen38-27b-dflash2.env +6 -1
  4. package/dist/.tsbuildinfo +1 -1
  5. package/dist/benchmarks/paired/adapter.d.ts +15 -0
  6. package/dist/benchmarks/paired/adapter.d.ts.map +1 -1
  7. package/dist/benchmarks/paired/adapter.js +22 -1
  8. package/dist/benchmarks/paired/adapter.js.map +1 -1
  9. package/dist/bin/cli.js +20 -24
  10. package/dist/bin/cli.js.map +1 -1
  11. package/dist/cli/deliver.d.ts +34 -0
  12. package/dist/cli/deliver.d.ts.map +1 -1
  13. package/dist/cli/deliver.js +147 -20
  14. package/dist/cli/deliver.js.map +1 -1
  15. package/dist/cli/guided-setup.d.ts.map +1 -1
  16. package/dist/cli/guided-setup.js +15 -0
  17. package/dist/cli/guided-setup.js.map +1 -1
  18. package/dist/cli/init.d.ts.map +1 -1
  19. package/dist/cli/init.js +3 -0
  20. package/dist/cli/init.js.map +1 -1
  21. package/dist/cli/plan.d.ts.map +1 -1
  22. package/dist/cli/plan.js +8 -2
  23. package/dist/cli/plan.js.map +1 -1
  24. package/dist/cli/policy.d.ts.map +1 -1
  25. package/dist/cli/policy.js +104 -2
  26. package/dist/cli/policy.js.map +1 -1
  27. package/dist/cli/quality.d.ts +11 -0
  28. package/dist/cli/quality.d.ts.map +1 -0
  29. package/dist/cli/quality.js +213 -0
  30. package/dist/cli/quality.js.map +1 -0
  31. package/dist/cli/wizard-config.d.ts.map +1 -1
  32. package/dist/cli/wizard-config.js +19 -9
  33. package/dist/cli/wizard-config.js.map +1 -1
  34. package/dist/cli/worktree.d.ts.map +1 -1
  35. package/dist/cli/worktree.js +8 -1
  36. package/dist/cli/worktree.js.map +1 -1
  37. package/dist/dashboard/savings.d.ts +1 -4
  38. package/dist/dashboard/savings.d.ts.map +1 -1
  39. package/dist/dashboard/savings.js +2 -52
  40. package/dist/dashboard/savings.js.map +1 -1
  41. package/dist/delivery/acceptance-judge.d.ts +10 -0
  42. package/dist/delivery/acceptance-judge.d.ts.map +1 -1
  43. package/dist/delivery/acceptance-judge.js +45 -3
  44. package/dist/delivery/acceptance-judge.js.map +1 -1
  45. package/dist/delivery/agentic-executor.d.ts +12 -1
  46. package/dist/delivery/agentic-executor.d.ts.map +1 -1
  47. package/dist/delivery/agentic-executor.js +25 -5
  48. package/dist/delivery/agentic-executor.js.map +1 -1
  49. package/dist/delivery/bash-sweep.d.ts +12 -0
  50. package/dist/delivery/bash-sweep.d.ts.map +1 -1
  51. package/dist/delivery/bash-sweep.js +8 -1
  52. package/dist/delivery/bash-sweep.js.map +1 -1
  53. package/dist/delivery/convergence-loop.d.ts.map +1 -1
  54. package/dist/delivery/convergence-loop.js +6 -4
  55. package/dist/delivery/convergence-loop.js.map +1 -1
  56. package/dist/delivery/execution-gate.d.ts +1 -1
  57. package/dist/delivery/execution-gate.d.ts.map +1 -1
  58. package/dist/delivery/execution-gate.js +248 -5
  59. package/dist/delivery/execution-gate.js.map +1 -1
  60. package/dist/delivery/snapshot.d.ts +16 -0
  61. package/dist/delivery/snapshot.d.ts.map +1 -1
  62. package/dist/delivery/snapshot.js +113 -2
  63. package/dist/delivery/snapshot.js.map +1 -1
  64. package/dist/delivery/user-validation.d.ts.map +1 -1
  65. package/dist/delivery/user-validation.js +55 -10
  66. package/dist/delivery/user-validation.js.map +1 -1
  67. package/dist/delivery/verifier-ladder.d.ts +24 -0
  68. package/dist/delivery/verifier-ladder.d.ts.map +1 -1
  69. package/dist/delivery/verifier-ladder.js +119 -2
  70. package/dist/delivery/verifier-ladder.js.map +1 -1
  71. package/dist/delivery/visual-gate.d.ts +24 -0
  72. package/dist/delivery/visual-gate.d.ts.map +1 -1
  73. package/dist/delivery/visual-gate.js +70 -9
  74. package/dist/delivery/visual-gate.js.map +1 -1
  75. package/dist/generators/claude-md.js +17 -4
  76. package/dist/generators/claude-md.js.map +1 -1
  77. package/dist/policies/liveness.d.ts +105 -0
  78. package/dist/policies/liveness.d.ts.map +1 -0
  79. package/dist/policies/liveness.js +251 -0
  80. package/dist/policies/liveness.js.map +1 -0
  81. package/dist/policies/policy-memory.d.ts.map +1 -1
  82. package/dist/policies/policy-memory.js +3 -4
  83. package/dist/policies/policy-memory.js.map +1 -1
  84. package/dist/quality/baseline.d.ts +29 -0
  85. package/dist/quality/baseline.d.ts.map +1 -0
  86. package/dist/quality/baseline.js +79 -0
  87. package/dist/quality/baseline.js.map +1 -0
  88. package/dist/quality/complexity.d.ts +38 -0
  89. package/dist/quality/complexity.d.ts.map +1 -0
  90. package/dist/quality/complexity.js +239 -0
  91. package/dist/quality/complexity.js.map +1 -0
  92. package/dist/quality/config.d.ts +40 -0
  93. package/dist/quality/config.d.ts.map +1 -0
  94. package/dist/quality/config.js +77 -0
  95. package/dist/quality/config.js.map +1 -0
  96. package/dist/quality/crap.d.ts +11 -0
  97. package/dist/quality/crap.d.ts.map +1 -0
  98. package/dist/quality/crap.js +54 -0
  99. package/dist/quality/crap.js.map +1 -0
  100. package/dist/quality/languages.d.ts +25 -0
  101. package/dist/quality/languages.d.ts.map +1 -0
  102. package/dist/quality/languages.js +150 -0
  103. package/dist/quality/languages.js.map +1 -0
  104. package/dist/quality/report.d.ts +38 -0
  105. package/dist/quality/report.d.ts.map +1 -0
  106. package/dist/quality/report.js +251 -0
  107. package/dist/quality/report.js.map +1 -0
  108. package/dist/quality/scanner.d.ts +32 -0
  109. package/dist/quality/scanner.d.ts.map +1 -0
  110. package/dist/quality/scanner.js +127 -0
  111. package/dist/quality/scanner.js.map +1 -0
  112. package/dist/quality/tools.d.ts +28 -0
  113. package/dist/quality/tools.d.ts.map +1 -0
  114. package/dist/quality/tools.js +189 -0
  115. package/dist/quality/tools.js.map +1 -0
  116. package/dist/types/config.d.ts +148 -0
  117. package/dist/types/config.d.ts.map +1 -1
  118. package/dist/types/config.js +35 -0
  119. package/dist/types/config.js.map +1 -1
  120. package/docs/ATTRIBUTION.md +0 -1
  121. package/docs/INDEX.md +38 -4
  122. package/docs/analysis/MARKET_POSITION.md +179 -0
  123. package/docs/architecture/OVERVIEW.md +82 -18
  124. package/docs/architecture/PROTOCOL.md +1 -1
  125. package/docs/benchmarks/PAIRED_FINDINGS.md +106 -0
  126. package/docs/benchmarks/PAIRED_HARNESS.md +16 -0
  127. package/docs/benchmarks/README.md +59 -0
  128. package/docs/design/UAP_REACTOR.md +0 -1
  129. package/docs/getting-started/INSTALLATION.md +1 -1
  130. package/docs/guides/AUTOMATIC.md +0 -14
  131. package/docs/guides/AUTOMATIC_FEATURES.md +1 -9
  132. package/docs/guides/COORDINATION.md +1 -1
  133. package/docs/guides/DELIVERY_PIPELINE.md +5 -0
  134. package/docs/guides/DEPLOY_BATCHING.md +1 -1
  135. package/docs/guides/DROIDS_AND_SKILLS.md +1 -1
  136. package/docs/guides/LOCAL_MODELS.md +6 -1
  137. package/docs/guides/MCP_ROUTER.md +1 -1
  138. package/docs/guides/MEMORY.md +1 -1
  139. package/docs/guides/MULTI_MODEL.md +1 -1
  140. package/docs/guides/ORCHESTRATOR.md +1 -1
  141. package/docs/guides/POLICIES.md +19 -3
  142. package/docs/guides/QWEN38_EXL3.md +114 -0
  143. package/docs/guides/WORKTREE_WORKFLOW.md +45 -1
  144. package/docs/integrations/MCP_ROUTER.md +1 -4
  145. package/docs/plans/deliver-hardening-plan-2026-07-13.md +173 -0
  146. package/docs/plans/deliver-hardening-review-response-2026-07-13.md +180 -0
  147. package/docs/plans/k3-uap-uplift-validation.md +216 -0
  148. package/docs/plans/qwen38-uap-retest-plan.md +196 -0
  149. package/docs/reference/API.md +1 -1
  150. package/docs/reference/CLI.md +49 -14
  151. package/docs/reference/COMPARISON.md +248 -0
  152. package/docs/reference/CONFIGURATION.md +1 -3
  153. package/docs/reference/DATABASE_SCHEMA.md +1 -1
  154. package/docs/reference/FEATURES.md +3 -3
  155. package/docs/reference/PATTERNS.md +1 -1
  156. package/docs/reference/PLATFORMS.md +1 -1
  157. package/package.json +2 -2
  158. package/scripts/maintenance/update-skills.py +0 -22
  159. package/scripts/maintenance/verify-compliance.sh +3 -3
  160. package/src/policies/enforcers/__pycache__/_common.cpython-312.pyc +0 -0
  161. package/src/policies/enforcers/__pycache__/quality_metrics_gate.cpython-312.pyc +0 -0
  162. package/src/policies/enforcers/adr_guard.py +0 -0
  163. package/src/policies/enforcers/bearer_lockdown.py +0 -0
  164. package/src/policies/enforcers/branch_freshness.py +0 -0
  165. package/src/policies/enforcers/design_token_gate.py +0 -0
  166. package/src/policies/enforcers/enforcement_self_protect.py +70 -0
  167. package/src/policies/enforcers/expert_review_required.py +22 -0
  168. package/src/policies/enforcers/iac_plan_destruction_check.py +0 -0
  169. package/src/policies/enforcers/quality_metrics_gate.py +447 -0
  170. package/src/policies/enforcers/ship_loop_gate.py +0 -0
  171. package/src/policies/enforcers/validate_plan_on_change.py +0 -0
  172. package/src/policies/enforcers/visual_verification.py +0 -0
  173. package/src/policies/enforcers/workdir_scope.py +0 -0
  174. package/src/policies/schemas/policies/expert-review-required.md +9 -1
  175. package/src/policies/schemas/policies/quality-metrics-gate.md +74 -0
  176. package/templates/CLAUDE.template.md +1 -1
  177. package/templates/hooks/__pycache__/deliver_autoroute.cpython-312.pyc +0 -0
  178. package/templates/hooks/pre-tool-use-bash.sh +50 -3
  179. package/templates/hooks/pre-tool-use-edit-write.sh +31 -0
  180. package/templates/hooks/session-start.sh +15 -0
  181. package/templates/hooks/uap-policy-gate.sh +187 -5
  182. package/templates/stryker.config.json +14 -0
  183. package/tools/agents/scripts/__pycache__/toolcall_path_normalizer.cpython-312.pyc +0 -0
  184. package/tools/agents/scripts/anthropic_proxy.py +254 -28
  185. package/tools/agents/tests/run_acceptance.py +0 -0
  186. package/tools/agents/tests/test_client_disconnect.py +0 -0
  187. package/tools/agents/tests/test_cycle_break_exploration.py +0 -0
  188. package/tools/agents/tests/test_deferral_break.py +0 -0
  189. package/tools/agents/tests/test_delivery_enforcement_all_langs.py +0 -0
  190. package/tools/agents/tests/test_delivery_enforcement_exemptions.py +0 -0
  191. package/tools/agents/tests/test_delivery_enforcement_filepath.py +0 -0
  192. package/tools/agents/tests/test_delivery_enforcement_web_and_bash.py +0 -0
  193. package/tools/agents/tests/test_delivery_enforcement_worktree.py +0 -0
  194. package/tools/agents/tests/test_delivery_enforcement_write_complexity.py +0 -0
  195. package/tools/agents/tests/test_disconnect_watcher.py +0 -0
  196. package/tools/agents/tests/test_doubling_break.py +0 -0
  197. package/tools/agents/tests/test_empty_maxtokens_recovery.py +0 -0
  198. package/tools/agents/tests/test_empty_tool_loop_break.py +0 -0
  199. package/tools/agents/tests/test_enforcement_self_protect.py +90 -0
  200. package/tools/agents/tests/test_enforcer_escape_hatches.py +0 -0
  201. package/tools/agents/tests/test_error_loop_break.py +0 -0
  202. package/tools/agents/tests/test_expert_review_pr_scope.py +13 -5
  203. package/tools/agents/tests/test_fastpath_config.py +0 -0
  204. package/tools/agents/tests/test_finalize_suppression.py +0 -0
  205. package/tools/agents/tests/test_gate_evidence.py +0 -0
  206. package/tools/agents/tests/test_gate_failclosed_indirection.py +0 -0
  207. package/tools/agents/tests/test_gate_integrity.py +0 -0
  208. package/tools/agents/tests/test_live_deliver_kill.py +0 -0
  209. package/tools/agents/tests/test_malformed_unclosed_think.py +0 -0
  210. package/tools/agents/tests/test_mandate_beats_recon.py +0 -0
  211. package/tools/agents/tests/test_mandate_deliver.py +0 -0
  212. package/tools/agents/tests/test_non_stream_resp_log.py +0 -0
  213. package/tools/agents/tests/test_output_token_ceilings.py +0 -0
  214. package/tools/agents/tests/test_overflow_truncate_count_tokens.py +0 -0
  215. package/tools/agents/tests/test_path_containment.py +0 -0
  216. package/tools/agents/tests/test_path_normalizer_hardened.py +0 -0
  217. package/tools/agents/tests/test_repeat_call_guard.py +0 -0
  218. package/tools/agents/tests/test_repeat_guard.py +0 -0
  219. package/tools/agents/tests/test_session_admission.py +0 -0
  220. package/tools/agents/tests/test_stream_heartbeat.py +0 -0
  221. package/tools/agents/tests/test_stream_thinking_deltas.py +405 -0
  222. package/tools/agents/tests/test_stuck_break_hard.py +0 -0
  223. package/tools/agents/tests/test_stuck_break_reattach.py +0 -0
  224. package/tools/agents/tests/test_turn_count_breaker_periodic.py +0 -0
  225. package/tools/agents/tests/test_upstream_chokepoint.py +0 -0
  226. package/tools/agents/tests/test_upstream_model_aliases.py +0 -0
  227. package/tools/agents/tests/test_validate_plan_gate.py +0 -0
  228. package/tools/agents/tests/test_vision_passthrough.py +0 -0
  229. package/tools/agents/tests/test_workdir_scope_enforcer.py +0 -0
  230. package/dist/cli/rtk.d.ts +0 -34
  231. package/dist/cli/rtk.d.ts.map +0 -1
  232. package/dist/cli/rtk.js +0 -401
  233. package/dist/cli/rtk.js.map +0 -1
  234. package/docs/integrations/RTK.md +0 -113
  235. package/scripts/setup/install-rtk.sh +0 -184
  236. package/src/policies/enforcers/rtk_wrap.py +0 -205
  237. package/src/policies/schemas/policies/rtk-wrap.md +0 -50
  238. package/tools/agents/tests/test_rtk_wrap.py +0 -151
@@ -1172,6 +1172,40 @@ PROXY_DISABLE_THINKING_ON_TOOL_TURNS = os.environ.get(
1172
1172
  "off",
1173
1173
  "no",
1174
1174
  }
1175
+ # Stream upstream `reasoning_content` deltas to the client as Anthropic
1176
+ # `thinking` blocks on the TRUE streaming path. Default off: reasoning is
1177
+ # chain-of-thought and the standing policy is not to leak it unless the
1178
+ # client opted in. Enable for clients whose stream watchdog disconnects on
1179
+ # silence (Factory Droid ~300 s): a slow-thinking model otherwise produces
1180
+ # zero client-visible events for minutes, the client aborts and retries, and
1181
+ # each retry regenerates the turn from scratch (observed 2026-09-05 with
1182
+ # Qwen3.8-27B on exllamav3: >4-min reasoning runs on tool turns, one
1183
+ # watchdog kill + full retry per long turn).
1184
+ PROXY_STREAM_THINKING_DELTAS = os.environ.get(
1185
+ "PROXY_STREAM_THINKING_DELTAS", "off"
1186
+ ).lower() not in {
1187
+ "0",
1188
+ "false",
1189
+ "off",
1190
+ "no",
1191
+ }
1192
+ # Emit a one-space thinking_delta every N seconds while the upstream stream
1193
+ # has produced no content yet (prompt prefill) or is idle before the first
1194
+ # text/tool block. Motivation (2026-09-09, Qwen3.8-Flash-Next 125B-A6B on a
1195
+ # single RTX 3090): a 64k-token session turn prefills at ~160-390 tok/s =
1196
+ # 170-400 s, past the Factory Droid ~242 s stream watchdog, and message_start
1197
+ # / ping events do NOT reset that watchdog — only content deltas do (verified
1198
+ # 2026-09-05). Each kill landed mid-prefill and the retry started from token
1199
+ # 0: a livelock. The space-only deltas keep the client alive without altering
1200
+ # visible output; the thinking block they open is the same block any real
1201
+ # leading reasoning then streams into. Requires PROXY_STREAM_THINKING_DELTAS
1202
+ # (the lazy block layout); 0 disables.
1203
+ try:
1204
+ PROXY_PREFILL_HEARTBEAT_SECS = float(
1205
+ os.environ.get("PROXY_PREFILL_HEARTBEAT_SECS", "0")
1206
+ )
1207
+ except (TypeError, ValueError):
1208
+ PROXY_PREFILL_HEARTBEAT_SECS = 0.0
1175
1209
  # Disable thinking on EVERY turn (not just tool turns). For models like Gemma 4
1176
1210
  # that emit ~100 thinking tokens for trivial replies, this halves output cost.
1177
1211
  PROXY_DISABLE_THINKING_ALWAYS = os.environ.get(
@@ -11815,6 +11849,16 @@ async def _heartbeat_then_buffered(produce_coro, model: str, input_tokens: int =
11815
11849
  a Starlette ``Response`` (the guarded path's error returns). Since the stream
11816
11850
  has already committed to HTTP 200, an error Response is re-emitted as an SSE
11817
11851
  ``error`` event rather than an HTTP status.
11852
+
11853
+ When PROXY_STREAM_THINKING_DELTAS and PROXY_PREFILL_HEARTBEAT_SECS are both
11854
+ on, the keep-alive is a ``thinking`` block at index 0 fed with one-space
11855
+ thinking_deltas instead of bare pings: pings and message_start do NOT reset
11856
+ the Factory Droid ~242 s stream watchdog, only content deltas do
11857
+ (2026-09-09: no-tool turns like session summarization — 51k-token prefill
11858
+ plus up to 8000 buffered output tokens ≈ 470 s — livelocked on this path
11859
+ while tool turns on the streaming relay stayed alive). The heartbeat block
11860
+ is closed before the buffered content streams, which is then re-indexed
11861
+ +1 via ``stream_anthropic_message(index_offset=1)``.
11818
11862
  """
11819
11863
  msg_id = f"msg_{uuid.uuid4().hex[:24]}"
11820
11864
  yield (
@@ -11822,6 +11866,8 @@ async def _heartbeat_then_buffered(produce_coro, model: str, input_tokens: int =
11822
11866
  f"data: {json.dumps({'type': 'message_start', 'message': {'id': msg_id, 'type': 'message', 'role': 'assistant', 'content': [], 'model': model, 'stop_reason': None, 'stop_sequence': None, 'usage': {'input_tokens': input_tokens, 'output_tokens': 0}}})}\n\n"
11823
11867
  )
11824
11868
  interval = PROXY_STREAM_HEARTBEAT_SECS if PROXY_STREAM_HEARTBEAT_SECS > 0 else 15.0
11869
+ thinking_heartbeat = PROXY_STREAM_THINKING_DELTAS and PROXY_PREFILL_HEARTBEAT_SECS > 0
11870
+ hb_thinking_open = False
11825
11871
  task = asyncio.ensure_future(produce_coro)
11826
11872
  try:
11827
11873
  while True:
@@ -11831,7 +11877,19 @@ async def _heartbeat_then_buffered(produce_coro, model: str, input_tokens: int =
11831
11877
  produced = await asyncio.wait_for(asyncio.shield(task), timeout=interval)
11832
11878
  break
11833
11879
  except asyncio.TimeoutError:
11834
- yield 'event: ping\ndata: {"type": "ping"}\n\n'
11880
+ if thinking_heartbeat:
11881
+ if not hb_thinking_open:
11882
+ hb_thinking_open = True
11883
+ yield (
11884
+ f"event: content_block_start\n"
11885
+ f"data: {json.dumps({'type': 'content_block_start', 'index': 0, 'content_block': {'type': 'thinking', 'thinking': '', 'signature': ''}})}\n\n"
11886
+ )
11887
+ yield (
11888
+ f"event: content_block_delta\n"
11889
+ f"data: {json.dumps({'type': 'content_block_delta', 'index': 0, 'delta': {'type': 'thinking_delta', 'thinking': ' '}})}\n\n"
11890
+ )
11891
+ else:
11892
+ yield 'event: ping\ndata: {"type": "ping"}\n\n'
11835
11893
  except asyncio.CancelledError:
11836
11894
  # Client disconnected — cancel the in-flight produce AND await its
11837
11895
  # cleanup so httpx returns/closes the upstream connection before we
@@ -11853,6 +11911,14 @@ async def _heartbeat_then_buffered(produce_coro, model: str, input_tokens: int =
11853
11911
  )
11854
11912
  return
11855
11913
 
11914
+ # Close the heartbeat thinking block (if opened) before any terminal
11915
+ # event so the stream stays well-formed.
11916
+ if hb_thinking_open:
11917
+ yield (
11918
+ f"event: content_block_stop\n"
11919
+ f"data: {json.dumps({'type': 'content_block_stop', 'index': 0})}\n\n"
11920
+ )
11921
+
11856
11922
  if isinstance(produced, Response):
11857
11923
  # Guarded path returned an error Response; re-emit as an SSE error event.
11858
11924
  try:
@@ -11866,16 +11932,23 @@ async def _heartbeat_then_buffered(produce_coro, model: str, input_tokens: int =
11866
11932
  return
11867
11933
 
11868
11934
  # produced is the finalized Anthropic response dict — stream its content
11869
- # without a second message_start (already sent above).
11870
- async for chunk in stream_anthropic_message(produced, emit_message_start=False):
11935
+ # without a second message_start (already sent above). Blocks shift +1
11936
+ # when the heartbeat thinking block occupies index 0.
11937
+ async for chunk in stream_anthropic_message(
11938
+ produced, emit_message_start=False, index_offset=1 if hb_thinking_open else 0
11939
+ ):
11871
11940
  yield chunk
11872
11941
 
11873
11942
 
11874
- async def stream_anthropic_message(anthropic_resp: dict, emit_message_start: bool = True):
11943
+ async def stream_anthropic_message(
11944
+ anthropic_resp: dict, emit_message_start: bool = True, index_offset: int = 0
11945
+ ):
11875
11946
  """Stream a finalized Anthropic message as SSE events.
11876
11947
 
11877
11948
  emit_message_start=False skips the leading message_start event for callers
11878
11949
  (the heartbeat wrapper) that have already emitted one to start the stream.
11950
+ index_offset shifts every content block index — used when the caller has
11951
+ already occupied index 0 (the prefill-heartbeat thinking block).
11879
11952
  """
11880
11953
  if emit_message_start:
11881
11954
  message = {
@@ -11897,7 +11970,7 @@ async def stream_anthropic_message(anthropic_resp: dict, emit_message_start: boo
11897
11970
  yield f"event: message_start\ndata: {json.dumps({'type': 'message_start', 'message': message})}\n\n"
11898
11971
 
11899
11972
  content_blocks = anthropic_resp.get("content", []) or [{"type": "text", "text": ""}]
11900
- block_index = 0
11973
+ block_index = index_offset
11901
11974
  for block in content_blocks:
11902
11975
  btype = block.get("type", "text")
11903
11976
  if btype == "tool_use":
@@ -11976,11 +12049,24 @@ async def stream_anthropic_response(
11976
12049
  f"data: {json.dumps({'type': 'message_start', 'message': {'id': msg_id, 'type': 'message', 'role': 'assistant', 'content': [], 'model': model, 'stop_reason': None, 'stop_sequence': None, 'usage': {'input_tokens': _client_input_tokens(monitor), 'output_tokens': 0}}})}\n\n"
11977
12050
  )
11978
12051
 
11979
- # content_block_start for text (index 0)
11980
- yield (
11981
- f"event: content_block_start\n"
11982
- f"data: {json.dumps({'type': 'content_block_start', 'index': 0, 'content_block': {'type': 'text', 'text': ''}})}\n\n"
11983
- )
12052
+ # Block bookkeeping. Two layouts:
12053
+ # - legacy (PROXY_STREAM_THINKING_DELTAS off): text block pre-opened at
12054
+ # index 0, tool blocks from 1. Reasoning deltas are collected but never
12055
+ # streamed, so a slow-thinking model is client-silent for minutes.
12056
+ # - lazy (flag on): blocks open in arrival order. A leading reasoning run
12057
+ # opens a `thinking` block and streams thinking_deltas, so the client
12058
+ # sees continuous activity instead of a silence its watchdog will kill.
12059
+ stream_thinking = PROXY_STREAM_THINKING_DELTAS
12060
+ next_block_index = 0 # lazy layout only; legacy uses fixed 0/1+
12061
+ open_block: int | None = None # currently open block (lazy layout)
12062
+ text_block_index: int | None = None # lazy layout; legacy hardcodes 0
12063
+
12064
+ if not stream_thinking:
12065
+ # content_block_start for text (index 0)
12066
+ yield (
12067
+ f"event: content_block_start\n"
12068
+ f"data: {json.dumps({'type': 'content_block_start', 'index': 0, 'content_block': {'type': 'text', 'text': ''}})}\n\n"
12069
+ )
11984
12070
 
11985
12071
  yield 'event: ping\ndata: {"type": "ping"}\n\n'
11986
12072
 
@@ -11989,7 +12075,7 @@ async def stream_anthropic_response(
11989
12075
 
11990
12076
  # Track tool call state for streaming tool_calls
11991
12077
  tool_calls_by_index: dict[int, dict] = {}
11992
- tool_block_index = 1 # anthropic block index (0 = text)
12078
+ tool_block_index = 1 # anthropic block index (0 = text) — legacy layout
11993
12079
  text_chunks: list[str] = [] # accumulate text for logging
11994
12080
  reasoning_chunks: list[str] = [] # accumulate reasoning for fallback
11995
12081
 
@@ -12007,8 +12093,42 @@ async def stream_anthropic_response(
12007
12093
  # upstream truth for telemetry and the session monitor.
12008
12094
  upstream_usage: dict = {}
12009
12095
 
12096
+ # Heartbeat during upstream silence (prefill). aiter_lines() blocks with
12097
+ # no idle callback, so each line read is a persistent Task raced against
12098
+ # asyncio.wait — NOT wait_for, which cancels the inner read on timeout and
12099
+ # would destroy the line iterator's state mid-stream. On timeout, emit a
12100
+ # one-space thinking_delta (the only event class verified to reset the
12101
+ # Droid stream watchdog) in the leading phase, else a ping.
12102
+ _hb_secs = PROXY_PREFILL_HEARTBEAT_SECS if stream_thinking else 0.0
12103
+ _line_iter = openai_stream.aiter_lines()
12104
+ _pending_line: asyncio.Task | None = None
12010
12105
  try:
12011
- async for line in openai_stream.aiter_lines():
12106
+ while True:
12107
+ if _pending_line is None:
12108
+ _pending_line = asyncio.ensure_future(_line_iter.__anext__())
12109
+ if _hb_secs > 0:
12110
+ _ready, _ = await asyncio.wait({_pending_line}, timeout=_hb_secs)
12111
+ if not _ready:
12112
+ if text_block_index is None and not tool_calls_by_index:
12113
+ if open_block is None:
12114
+ open_block = next_block_index
12115
+ next_block_index += 1
12116
+ yield (
12117
+ f"event: content_block_start\n"
12118
+ f"data: {json.dumps({'type': 'content_block_start', 'index': open_block, 'content_block': {'type': 'thinking', 'thinking': '', 'signature': ''}})}\n\n"
12119
+ )
12120
+ yield (
12121
+ f"event: content_block_delta\n"
12122
+ f"data: {json.dumps({'type': 'content_block_delta', 'index': open_block, 'delta': {'type': 'thinking_delta', 'thinking': ' '}})}\n\n"
12123
+ )
12124
+ else:
12125
+ yield 'event: ping\ndata: {"type": "ping"}\n\n'
12126
+ continue
12127
+ try:
12128
+ line = await _pending_line
12129
+ except StopAsyncIteration:
12130
+ break
12131
+ _pending_line = None
12012
12132
  if not line.startswith("data: "):
12013
12133
  continue
12014
12134
  data = line[6:].strip()
@@ -12030,14 +12150,55 @@ async def stream_anthropic_response(
12030
12150
  reasoning = delta.get("reasoning_content", "")
12031
12151
  if reasoning:
12032
12152
  reasoning_chunks.append(reasoning)
12153
+ # Stream leading reasoning as a thinking block so the client's
12154
+ # stream watchdog sees activity during long thinking runs.
12155
+ # Only when it LEADS the turn (no text/tool output yet): late
12156
+ # interleaved reasoning keeps the legacy collect-only path
12157
+ # rather than churning block open/close per chunk.
12158
+ thinking_open = (
12159
+ stream_thinking
12160
+ and text_block_index is None
12161
+ and not tool_calls_by_index
12162
+ )
12163
+ if thinking_open and open_block is None:
12164
+ open_block = next_block_index
12165
+ next_block_index += 1
12166
+ yield (
12167
+ f"event: content_block_start\n"
12168
+ f"data: {json.dumps({'type': 'content_block_start', 'index': open_block, 'content_block': {'type': 'thinking', 'thinking': '', 'signature': ''}})}\n\n"
12169
+ )
12170
+ if thinking_open and open_block is not None:
12171
+ yield (
12172
+ f"event: content_block_delta\n"
12173
+ f"data: {json.dumps({'type': 'content_block_delta', 'index': open_block, 'delta': {'type': 'thinking_delta', 'thinking': reasoning}})}\n\n"
12174
+ )
12033
12175
 
12034
12176
  # Handle text content deltas
12035
12177
  if delta.get("content"):
12036
12178
  output_tokens += 1 # rough token estimate
12037
12179
  text_chunks.append(delta["content"])
12180
+ text_idx = 0
12181
+ if stream_thinking:
12182
+ # Close a leading thinking block before the text block.
12183
+ if open_block is not None and open_block != text_block_index:
12184
+ yield (
12185
+ f"event: content_block_stop\n"
12186
+ f"data: {json.dumps({'type': 'content_block_stop', 'index': open_block})}\n\n"
12187
+ )
12188
+ open_block = None
12189
+ if text_block_index is None:
12190
+ text_block_index = next_block_index
12191
+ next_block_index += 1
12192
+ if open_block is None:
12193
+ open_block = text_block_index
12194
+ yield (
12195
+ f"event: content_block_start\n"
12196
+ f"data: {json.dumps({'type': 'content_block_start', 'index': text_block_index, 'content_block': {'type': 'text', 'text': ''}})}\n\n"
12197
+ )
12198
+ text_idx = text_block_index
12038
12199
  yield (
12039
12200
  f"event: content_block_delta\n"
12040
- f"data: {json.dumps({'type': 'content_block_delta', 'index': 0, 'delta': {'type': 'text_delta', 'text': delta['content']}})}\n\n"
12201
+ f"data: {json.dumps({'type': 'content_block_delta', 'index': text_idx, 'delta': {'type': 'text_delta', 'text': delta['content']}})}\n\n"
12041
12202
  )
12042
12203
 
12043
12204
  # Degenerate-repetition guard: stop a runaway while it is still
@@ -12077,15 +12238,28 @@ async def stream_anthropic_response(
12077
12238
  tc_id = tc_delta.get("id", f"toolu_{uuid.uuid4().hex[:12]}")
12078
12239
  fn = tc_delta.get("function", {})
12079
12240
  initial_args = fn.get("arguments", "")
12241
+ if stream_thinking:
12242
+ this_block_index = next_block_index
12243
+ next_block_index += 1
12244
+ else:
12245
+ this_block_index = tool_block_index
12080
12246
  tool_calls_by_index[tc_idx] = {
12081
12247
  "id": tc_id,
12082
12248
  "name": _restore_tool_name(fn.get("name", "")),
12083
12249
  "arguments": initial_args,
12084
- "block_index": tool_block_index,
12250
+ "block_index": this_block_index,
12085
12251
  }
12086
12252
 
12087
- # Close text block before first tool block
12088
- if tool_block_index == 1:
12253
+ # Close the open text/thinking block before the first
12254
+ # tool block.
12255
+ if stream_thinking:
12256
+ if open_block is not None:
12257
+ yield (
12258
+ f"event: content_block_stop\n"
12259
+ f"data: {json.dumps({'type': 'content_block_stop', 'index': open_block})}\n\n"
12260
+ )
12261
+ open_block = None
12262
+ elif tool_block_index == 1:
12089
12263
  yield (
12090
12264
  f"event: content_block_stop\n"
12091
12265
  f"data: {json.dumps({'type': 'content_block_stop', 'index': 0})}\n\n"
@@ -12094,7 +12268,7 @@ async def stream_anthropic_response(
12094
12268
  # Emit content_block_start for this tool_use
12095
12269
  yield (
12096
12270
  f"event: content_block_start\n"
12097
- f"data: {json.dumps({'type': 'content_block_start', 'index': tool_block_index, 'content_block': {'type': 'tool_use', 'id': tc_id, 'name': fn.get('name', '')}})}\n\n"
12271
+ f"data: {json.dumps({'type': 'content_block_start', 'index': this_block_index, 'content_block': {'type': 'tool_use', 'id': tc_id, 'name': fn.get('name', '')}})}\n\n"
12098
12272
  )
12099
12273
 
12100
12274
  # Emit initial arguments fragment (e.g. "{") that
@@ -12105,7 +12279,7 @@ async def stream_anthropic_response(
12105
12279
  if initial_args:
12106
12280
  yield (
12107
12281
  f"event: content_block_delta\n"
12108
- f"data: {json.dumps({'type': 'content_block_delta', 'index': tool_block_index, 'delta': {'type': 'input_json_delta', 'partial_json': initial_args}})}\n\n"
12282
+ f"data: {json.dumps({'type': 'content_block_delta', 'index': this_block_index, 'delta': {'type': 'input_json_delta', 'partial_json': initial_args}})}\n\n"
12109
12283
  )
12110
12284
 
12111
12285
  tool_block_index += 1
@@ -12144,6 +12318,8 @@ async def stream_anthropic_response(
12144
12318
  logger.error("Unexpected stream error: %s: %s", type(exc).__name__, exc)
12145
12319
  finish_reason = "end_turn"
12146
12320
  finally:
12321
+ if _pending_line is not None and not _pending_line.done():
12322
+ _pending_line.cancel()
12147
12323
  if _sr_client is not None:
12148
12324
  _inflight_dec(_sr_client)
12149
12325
  # Detached close: a bare 'await aclose()' here is itself cancellable
@@ -12199,9 +12375,29 @@ async def stream_anthropic_response(
12199
12375
  PROXY_STREAM_REASONING_FALLBACK,
12200
12376
  )
12201
12377
  text_chunks.append(fallback_text)
12378
+ fallback_idx = 0
12379
+ if stream_thinking:
12380
+ # The thinking block (if any) is still open: close it and
12381
+ # open the text block the fallback lands in.
12382
+ if open_block is not None and open_block != text_block_index:
12383
+ yield (
12384
+ f"event: content_block_stop\n"
12385
+ f"data: {json.dumps({'type': 'content_block_stop', 'index': open_block})}\n\n"
12386
+ )
12387
+ open_block = None
12388
+ if text_block_index is None:
12389
+ text_block_index = next_block_index
12390
+ next_block_index += 1
12391
+ if open_block is None:
12392
+ open_block = text_block_index
12393
+ yield (
12394
+ f"event: content_block_start\n"
12395
+ f"data: {json.dumps({'type': 'content_block_start', 'index': text_block_index, 'content_block': {'type': 'text', 'text': ''}})}\n\n"
12396
+ )
12397
+ fallback_idx = text_block_index
12202
12398
  yield (
12203
12399
  f"event: content_block_delta\n"
12204
- f"data: {json.dumps({'type': 'content_block_delta', 'index': 0, 'delta': {'type': 'text_delta', 'text': fallback_text}})}\n\n"
12400
+ f"data: {json.dumps({'type': 'content_block_delta', 'index': fallback_idx, 'delta': {'type': 'text_delta', 'text': fallback_text}})}\n\n"
12205
12401
  )
12206
12402
  else:
12207
12403
  logger.warning(
@@ -12210,10 +12406,30 @@ async def stream_anthropic_response(
12210
12406
  PROXY_STREAM_REASONING_FALLBACK,
12211
12407
  )
12212
12408
 
12213
- yield (
12214
- f"event: content_block_stop\n"
12215
- f"data: {json.dumps({'type': 'content_block_stop', 'index': 0})}\n\n"
12216
- )
12409
+ if stream_thinking:
12410
+ if open_block is not None:
12411
+ yield (
12412
+ f"event: content_block_stop\n"
12413
+ f"data: {json.dumps({'type': 'content_block_stop', 'index': open_block})}\n\n"
12414
+ )
12415
+ open_block = None
12416
+ elif text_block_index is None and next_block_index == 0:
12417
+ # Completely empty turn: keep the legacy guarantee of at least
12418
+ # one (empty) text block so clients always see a well-formed
12419
+ # message body.
12420
+ yield (
12421
+ f"event: content_block_start\n"
12422
+ f"data: {json.dumps({'type': 'content_block_start', 'index': 0, 'content_block': {'type': 'text', 'text': ''}})}\n\n"
12423
+ )
12424
+ yield (
12425
+ f"event: content_block_stop\n"
12426
+ f"data: {json.dumps({'type': 'content_block_stop', 'index': 0})}\n\n"
12427
+ )
12428
+ else:
12429
+ yield (
12430
+ f"event: content_block_stop\n"
12431
+ f"data: {json.dumps({'type': 'content_block_stop', 'index': 0})}\n\n"
12432
+ )
12217
12433
 
12218
12434
  # Log response summary
12219
12435
  accumulated_text = "".join(text_chunks)
@@ -12267,11 +12483,21 @@ async def stream_anthropic_response(
12267
12483
  # We already streamed the text as-is. We cannot un-stream it,
12268
12484
  # but we CAN close the text block, emit the recovered tool_use
12269
12485
  # blocks, and fix the finish_reason so Claude Code sees them.
12270
- yield (
12271
- f"event: content_block_stop\n"
12272
- f"data: {json.dumps({'type': 'content_block_stop', 'index': 0})}\n\n"
12273
- )
12274
- for idx, xtc in enumerate(xml_extracted, start=1):
12486
+ if stream_thinking:
12487
+ if open_block is not None:
12488
+ yield (
12489
+ f"event: content_block_stop\n"
12490
+ f"data: {json.dumps({'type': 'content_block_stop', 'index': open_block})}\n\n"
12491
+ )
12492
+ open_block = None
12493
+ xml_base = next_block_index
12494
+ else:
12495
+ yield (
12496
+ f"event: content_block_stop\n"
12497
+ f"data: {json.dumps({'type': 'content_block_stop', 'index': 0})}\n\n"
12498
+ )
12499
+ xml_base = 1
12500
+ for idx, xtc in enumerate(xml_extracted, start=xml_base):
12275
12501
  fn = xtc.get("function", {})
12276
12502
  tc_id = xtc.get("id", f"toolu_{uuid.uuid4().hex[:12]}")
12277
12503
  tc_name = _restore_tool_name(fn.get("name", ""))
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -41,6 +41,96 @@ class SelfProtectTest(unittest.TestCase):
41
41
  rc, out = run("Write", {"file_path": str(REPO / ".policy-tools/abc_delivery_enforcement.py")})
42
42
  self.assertEqual(rc, 2)
43
43
 
44
+ def test_blocks_edit_to_policies_db(self):
45
+ # The inversion fix (F2): the enforcement registry the gates READ must
46
+ # be at least as protected as the artifact paths the gates demand.
47
+ rc, out = run("Edit", {"file_path": str(REPO / "agents/data/memory/policies.db")})
48
+ self.assertEqual(rc, 2)
49
+ self.assertFalse(out["allowed"])
50
+
51
+ def test_blocks_bash_rm_policies_db(self):
52
+ rc, out = run("Bash", {"command": "rm -f agents/data/memory/policies.db"})
53
+ self.assertEqual(rc, 2)
54
+
55
+ def test_allows_uap_policy_cli_bash(self):
56
+ # Policy management goes through the CLI process, not direct file ops;
57
+ # a command that merely RUNS `uap policy` must not trip the DB guard.
58
+ rc, out = run("Bash", {"command": "uap policy list"})
59
+ self.assertEqual(rc, 0)
60
+
61
+ def test_blocks_edit_to_operator_overrides(self):
62
+ # F3: the signed override file is operator-only; the "/.uap/" marker
63
+ # already refuses Edit/Write. Pin it so a future carve-out cannot
64
+ # silently reopen the self-mint path.
65
+ rc, out = run("Write", {"file_path": str(REPO / ".uap/operator-overrides.json")})
66
+ self.assertEqual(rc, 2)
67
+
68
+ def test_blocks_bash_redirect_creating_operator_overrides(self):
69
+ # F3: the file-op marker does not cover shell redirects — without this
70
+ # target entry `echo …> .uap/operator-overrides.json` self-mints the
71
+ # bypass the file exists to restrict to the operator.
72
+ rc, out = run("Bash", {"command": 'echo "{}" > .uap/operator-overrides.json'})
73
+ self.assertEqual(rc, 2)
74
+
75
+ def test_blocks_bash_rm_operator_overrides(self):
76
+ rc, out = run("Bash", {"command": "rm -f .uap/operator-overrides.json"})
77
+ self.assertEqual(rc, 2)
78
+
79
+ def test_blocks_bash_forging_liveness_cache(self):
80
+ # F1: the degrade consult trusts this file — a forged entry downgrades
81
+ # a blocking policy to advisory, so writing it directly must be refused.
82
+ # The legitimate writer is `uap policy liveness`, whose command text
83
+ # never names the file.
84
+ rc, out = run("Bash", {"command": 'echo "{}" > .uap/policy-liveness.json'})
85
+ self.assertEqual(rc, 2)
86
+
87
+ def test_allows_uap_policy_liveness_cli(self):
88
+ rc, out = run("Bash", {"command": "uap policy liveness --quiet"})
89
+ self.assertEqual(rc, 0)
90
+
91
+ def test_allows_reading_operator_overrides(self):
92
+ rc, out = run("Bash", {"command": "cat .uap/operator-overrides.json"})
93
+ self.assertEqual(rc, 0)
94
+
95
+ def test_blocks_python_minting_operator_overrides(self):
96
+ # Security review (deliver-hardening, 2026-07-13): an interpreter-
97
+ # mediated write carries no destructive verb and no redirect, so the
98
+ # redirect/verb scans miss it — and before the root-ownership check
99
+ # this one command minted a full bypass. Blocked on sight now.
100
+ rc, out = run("Bash", {"command":
101
+ 'python3 -c \'import json; json.dump({"deliverBypass": True}, open(".uap/operator-overrides.json", "w"))\''})
102
+ self.assertEqual(rc, 2)
103
+
104
+ def test_blocks_python_forging_liveness_cache(self):
105
+ rc, out = run("Bash", {"command":
106
+ 'python3 -c \'open(".uap/policy-liveness.json", "w").write("{}")\''})
107
+ self.assertEqual(rc, 2)
108
+
109
+ def test_blocks_node_writing_trust_anchor(self):
110
+ rc, out = run("Bash", {"command":
111
+ 'node -e \'require("fs").writeFileSync(".uap/operator-overrides.json", "{}")\''})
112
+ self.assertEqual(rc, 2)
113
+
114
+ def test_blocks_bash_c_wrapped_python_mint(self):
115
+ # The rule fires on the full command text, so a shell wrapper does not
116
+ # hide the write.
117
+ rc, out = run("Bash", {"command":
118
+ 'bash -c "python3 -c \'open(".uap/operator-overrides.json", "w")\'"'})
119
+ self.assertEqual(rc, 2)
120
+
121
+ def test_allows_python_READING_a_trust_anchor(self):
122
+ # Reads must stay cheap: no write primitive, no interpreter-write rule.
123
+ rc, out = run("Bash", {"command":
124
+ 'python3 -c \'import json; print(json.load(open(".uap/policy-liveness.json")))\''})
125
+ self.assertEqual(rc, 0)
126
+
127
+ def test_allows_python_write_to_ordinary_uap_state(self):
128
+ # The narrow list is deliberate: runtime state under .uap/ stays
129
+ # writable, only the two trust anchors get the interpreter rule.
130
+ rc, out = run("Bash", {"command":
131
+ 'python3 -c \'open(".uap/verify-cadence", "w").write("0")\''})
132
+ self.assertEqual(rc, 0)
133
+
44
134
  def test_blocks_bash_setting_bypass(self):
45
135
  rc, out = run("Bash", {"command": "export UAP_DELIVER_BYPASS=1 && echo hi"})
46
136
  self.assertEqual(rc, 2)
File without changes
@@ -207,9 +207,9 @@ class TestShipDetectionIsCommandPosition(unittest.TestCase):
207
207
  review artifact was refused because the notes described the bug being fixed.
208
208
 
209
209
  The risk in the fix is UNDER-detection, so the ship cases below matter more
210
- than the prose ones. `rtk git push` especially: this repo requires git to be
211
- invoked through rtk, so a verb check that stopped at the wrapper would miss
212
- every real ship command in the codebase.
210
+ than the prose ones. The wrapper cases especially: rtk is no longer
211
+ integrated in UAP, but the binary can still exist on operator machines, so
212
+ a verb check that stopped at the wrapper would miss wrapped ship commands.
213
213
  """
214
214
 
215
215
  def setUp(self):
@@ -247,10 +247,18 @@ class TestShipDetectionIsCommandPosition(unittest.TestCase):
247
247
  self.assert_ship("git push origin master")
248
248
 
249
249
  def test_rtk_wrapped_git_is_a_ship_action(self):
250
- # The mandated form in this repo. Missing it would silently ungate
251
- # every push and commit made here.
250
+ # rtk is no longer a UAP integration, but the wrapper unwrap stays as
251
+ # defense-in-depth for machines where the binary remains installed.
252
252
  self.assert_ship("rtk git push origin master")
253
253
 
254
+ def test_rtk_wrapped_git_with_value_option_is_a_ship_action(self):
255
+ # The `-C <path>` form needs the wrapper unwrap — the raw prose
256
+ # patterns only catch adjacent `git push`. Regression guard.
257
+ self.assert_ship("rtk git -C /repo push origin master")
258
+
259
+ def test_rtk_wrapped_plumbing_is_a_ship_action(self):
260
+ self.assert_ship("rtk git send-pack origin master")
261
+
254
262
  def test_ship_action_after_a_chained_command(self):
255
263
  self.assert_ship("cd sub && git commit -m x")
256
264
 
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes