@miller-tech/uap 2.39.1 → 2.44.2

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 (40) hide show
  1. package/config/capacity-policy.json +25 -0
  2. package/config/model-profiles/qwen38.json +9 -8
  3. package/config/model-registry.json +670 -20
  4. package/dist/.tsbuildinfo +1 -1
  5. package/dist/cli/deliver.js +1 -1
  6. package/dist/cli/deliver.js.map +1 -1
  7. package/dist/cli/models.d.ts +6 -0
  8. package/dist/cli/models.d.ts.map +1 -1
  9. package/dist/cli/models.js +11 -2
  10. package/dist/cli/models.js.map +1 -1
  11. package/dist/cli/verify.d.ts +9 -1
  12. package/dist/cli/verify.d.ts.map +1 -1
  13. package/dist/cli/verify.js +15 -3
  14. package/dist/cli/verify.js.map +1 -1
  15. package/dist/delivery/acceptance-judge.d.ts +12 -0
  16. package/dist/delivery/acceptance-judge.d.ts.map +1 -1
  17. package/dist/delivery/acceptance-judge.js +109 -8
  18. package/dist/delivery/acceptance-judge.js.map +1 -1
  19. package/dist/inference/model-files.d.ts.map +1 -1
  20. package/dist/inference/model-files.js +6 -1
  21. package/dist/inference/model-files.js.map +1 -1
  22. package/dist/models/types.d.ts.map +1 -1
  23. package/dist/models/types.js +14 -2
  24. package/dist/models/types.js.map +1 -1
  25. package/docs/INDEX.md +1 -0
  26. package/docs/architecture/adr/0015-session-hook-model-convergence-as-standing-consent.md +68 -0
  27. package/docs/benchmarks/PROMPT_UPLIFT_BLENDER.md +439 -0
  28. package/docs/benchmarks/README.md +1 -0
  29. package/docs/guides/LOCAL_MODELS.md +50 -0
  30. package/package.json +1 -1
  31. package/scripts/lib/droid-sync-contracts.sh +114 -0
  32. package/src/policies/enforcers/__pycache__/_common.cpython-311.pyc +0 -0
  33. package/src/policies/enforcers/__pycache__/expert_review_required.cpython-311.pyc +0 -0
  34. package/src/policies/enforcers/__pycache__/quality_metrics_gate.cpython-311.pyc +0 -0
  35. package/src/policies/enforcers/delivery_enforcement.py +34 -12
  36. package/templates/hooks/__pycache__/deliver_autoroute.cpython-311.pyc +0 -0
  37. package/templates/hooks/pre-tool-use-bash.sh +34 -3
  38. package/templates/hooks/stop.sh +39 -0
  39. package/tools/agents/scripts/__pycache__/toolcall_path_normalizer.cpython-311.pyc +0 -0
  40. package/tools/agents/tests/test_delivery_enforcement_write_complexity.py +26 -0
@@ -193,16 +193,32 @@ def _trivial_edit_chars() -> int:
193
193
 
194
194
  def _changed_chars(args: dict) -> int | None:
195
195
  """Total changed characters for an Edit/MultiEdit, or None when it can't be
196
- measured (e.g. a whole-file Write) — None means 'not trivially small'."""
196
+ measured (e.g. a whole-file Write) — None means 'not trivially small'.
197
+
198
+ Harnesses spell the anchor fields three ways: Claude Code sends
199
+ old_string/new_string, some SDKs oldString/newString, and Droid (Factory)
200
+ old_str/new_str. An unrecognized spelling measured as None — read as a
201
+ whole-file rewrite — which force-escalated every Droid Edit as complex
202
+ regardless of size (observed live 2026-11-12: a 4-line comment insert was
203
+ refused as 'a whole-file rewrite that replaces most of a substantial
204
+ file'). grab() recognizes all three spellings at both call sites."""
205
+ def grab(d: dict) -> tuple[object, object]:
206
+ old = d.get("old_string") or d.get("oldString") or d.get("old_str")
207
+ new = d.get("new_string") or d.get("newString") or d.get("new_str")
208
+ return old, new
197
209
  def pair(o: object, n: object) -> int:
198
210
  return len(str(o or "")) + len(str(n or ""))
199
211
  if "edits" in args and isinstance(args["edits"], list):
200
- return sum(
201
- pair(e.get("old_string") or e.get("oldString"), e.get("new_string") or e.get("newString"))
202
- for e in args["edits"] if isinstance(e, dict)
203
- )
204
- old = args.get("old_string") or args.get("oldString")
205
- new = args.get("new_string") or args.get("newString")
212
+ total, measured = 0, False
213
+ for e in args["edits"]:
214
+ if not isinstance(e, dict):
215
+ continue
216
+ old, new = grab(e)
217
+ if old is not None or new is not None:
218
+ measured = True
219
+ total += pair(old, new)
220
+ return total if measured else None
221
+ old, new = grab(args)
206
222
  if old is not None or new is not None:
207
223
  return pair(old, new)
208
224
  return None # Write (full content) — not a trivial edit
@@ -232,7 +248,7 @@ def _write_cost(args: dict, root: Path) -> int | None:
232
248
  """
233
249
  content = args.get("content")
234
250
  if content is None:
235
- content = args.get("new_string") or args.get("newString")
251
+ content = args.get("new_string") or args.get("newString") or args.get("new_str")
236
252
  if content is None:
237
253
  return None
238
254
  new_len = len(str(content))
@@ -460,8 +476,11 @@ def _escalation_block(rel: str, kind: str, detail: str, args: dict) -> None:
460
476
  intent = {
461
477
  k: v
462
478
  for k, v in (
463
- ("old_string", args.get("old_string", args.get("oldString"))),
464
- ("new_string", args.get("new_string", args.get("newString"))),
479
+ # Same three spellings as _changed_chars/_write_cost — an
480
+ # escalated Droid edit (old_str/new_str) must be recorded as a
481
+ # replayable intent, not silently dropped to None.
482
+ ("old_string", args.get("old_string") or args.get("oldString") or args.get("old_str")),
483
+ ("new_string", args.get("new_string") or args.get("newString") or args.get("new_str")),
465
484
  ("content", args.get("content")),
466
485
  )
467
486
  if isinstance(v, str)
@@ -868,8 +887,11 @@ def main() -> None:
868
887
  intent_payload = {
869
888
  k: v
870
889
  for k, v in (
871
- ("old_string", args.get("old_string", args.get("oldString"))),
872
- ("new_string", args.get("new_string", args.get("newString"))),
890
+ # Same three spellings as _changed_chars/_write_cost — an
891
+ # escalated Droid edit (old_str/new_str) must be recorded as a
892
+ # replayable intent, not silently dropped to None.
893
+ ("old_string", args.get("old_string") or args.get("oldString") or args.get("old_str")),
894
+ ("new_string", args.get("new_string") or args.get("newString") or args.get("new_str")),
873
895
  ("content", args.get("content")),
874
896
  )
875
897
  if isinstance(v, str)
@@ -14,8 +14,18 @@ fi
14
14
  # Read tool input from stdin (JSON)
15
15
  INPUT=$(cat)
16
16
 
17
- # Extract command from tool_input
18
- CMD=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null || true)
17
+ # Extract command from tool_input, with bounded retries. Under a full
18
+ # parallel suite (or a busy box) a jq spawn can fail transiently, and a
19
+ # fail-open on that would silently allow exactly the edits this guard
20
+ # exists to block — measured as the recurring jq '.version = "9.9.9"'
21
+ # semver-suite flake (3 strikes on 2026-11-12: two CI runs, one local).
22
+ # Fail-open stays the LAST resort for genuinely unparseable input.
23
+ CMD=""
24
+ for _attempt in 1 2 3 4 5; do
25
+ CMD=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null || true)
26
+ [ -n "$CMD" ] && break
27
+ sleep 0.05
28
+ done
19
29
 
20
30
  # If we can't determine the command, fail open
21
31
  if [ -z "$CMD" ]; then
@@ -178,7 +188,28 @@ fi
178
188
  # That is a READ. Splitting on ; && || | and newline keeps every real edit
179
189
  # (each has sed/awk/jq and package.json inside one statement) and drops the
180
190
  # cross-statement coincidence. Verified against both corpora before and after.
181
- if printf '%s' "$CMD" | tr ';|&\n' '\n\n\n\n' | grep -qE "(sed|awk).*package\.json.*(version|\"version\")|((sed|awk).*version.*package\.json)|(jq.*\.version.*package\.json)"; then
191
+ #
192
+ # Evaluated with BUILT-INS ONLY: parameter expansion for the statement split,
193
+ # [[ =~ ]] for the match. An earlier `printf | tr | grep -qE` pipeline looked
194
+ # equivalent but had a load-shaped hole: in an `if` condition a grep that fails
195
+ # to spawn (exit 2/126/127 under fork pressure) is indistinguishable from a
196
+ # no-match (exit 1), so the rule silently failed OPEN under the full parallel
197
+ # suite — the canary still refused, so it looked like a genuine allow. That was
198
+ # the recurring semver-suite flake (3 strikes on 2026-11-12: two CI runs, one
199
+ # local version-bump gate). Zero forks here means no spawn can hiccup, and no
200
+ # pipeline stage can mask a match.
201
+ _semv_split="${CMD//[;|&]/$'\n'}"
202
+ _semv_a='(sed|awk).*package\.json.*version'
203
+ _semv_b='(sed|awk).*version.*package\.json'
204
+ _semv_c='jq.*\.version.*package\.json'
205
+ _semv_hit=""
206
+ while IFS= read -r _semv_stmt; do
207
+ if [[ "$_semv_stmt" =~ $_semv_a || "$_semv_stmt" =~ $_semv_b || "$_semv_stmt" =~ $_semv_c ]]; then
208
+ _semv_hit="yes"
209
+ break
210
+ fi
211
+ done <<< "$_semv_split"
212
+ if [ -n "$_semv_hit" ]; then
182
213
  echo "BLOCKED [semver-versioning]: Manual package.json version edits are prohibited. Use: npm run version:patch, version:minor, or version:major. See policies/semver-versioning.md" >&2
183
214
  exit 2
184
215
  fi
@@ -233,10 +233,49 @@ if [ "$CODE_CHANGED" = "true" ] && [ "${UAP_VERIFY_ON_STOP:-1}" != "0" ] && comm
233
233
  fi
234
234
  fi
235
235
  set -e
236
+ # A green verify (or a gate that never ran) resets the consecutive-block
237
+ # counter below — a fresh red streak starts from zero.
238
+ if [ "$VERIFY_RC" = "0" ]; then
239
+ rm -f "${PROJECT_DIR}/.uap/stop-gate/blocks" 2>/dev/null || true
240
+ fi
236
241
  # RC 1 = a REAL gate failure (code is broken) → block. Everything else fails
237
242
  # OPEN: RC 3 = infra (gate timed out / couldn't spawn), 124 = outer timeout,
238
243
  # 127 = missing, any other = internal — none should ever wedge a session.
239
244
  if [ "$VERIFY_RC" = "1" ]; then
245
+ # Bounded consecutive-block relief. The re-entrancy guard at the top
246
+ # relents only when the harness sends stop_hook_active:true (the Claude
247
+ # Code contract). Harnesses that send no Stop payload (observed: Factory
248
+ # Droid on pay2u, 2026-10-18) never trip that guard, so a persistently-red
249
+ # gate — e.g. max-fidelity interaction fail-closed on a repo with entry
250
+ # pages but no manifest — became an unbounded Stop→continue→Stop loop:
251
+ # exactly the wedge this hook's fail-open design promises cannot happen.
252
+ # Count consecutive RC-1 blocks per project; relent (allow the stop, with
253
+ # the red verdict still on record) after UAP_STOP_BLOCK_LIMIT (default 3).
254
+ # A block older than an hour starts a fresh count: the counter tracks a
255
+ # LOOP, not history — a new session gets a fresh budget.
256
+ mkdir -p "${PROJECT_DIR}/.uap/stop-gate" 2>/dev/null || true
257
+ BLOCK_FILE="${PROJECT_DIR}/.uap/stop-gate/blocks"
258
+ BLOCK_COUNT=0
259
+ BLOCK_TS=0
260
+ if [ -f "$BLOCK_FILE" ]; then
261
+ BLOCK_COUNT="$(head -1 "$BLOCK_FILE" 2>/dev/null || echo 0)"
262
+ BLOCK_TS="$(tail -1 "$BLOCK_FILE" 2>/dev/null || echo 0)"
263
+ fi
264
+ case "$BLOCK_COUNT" in ''|*[!0-9]*) BLOCK_COUNT=0 ;; esac
265
+ case "$BLOCK_TS" in ''|*[!0-9]*) BLOCK_TS=0 ;; esac
266
+ NOW="$(date +%s 2>/dev/null || echo 0)"
267
+ case "$NOW" in ''|*[!0-9]*) NOW=0 ;; esac
268
+ if [ "$NOW" -gt 0 ] && [ "$((NOW - BLOCK_TS))" -gt 3600 ]; then
269
+ BLOCK_COUNT=0
270
+ fi
271
+ BLOCK_LIMIT="${UAP_STOP_BLOCK_LIMIT:-3}"
272
+ case "$BLOCK_LIMIT" in ''|*[!0-9]*) BLOCK_LIMIT=3 ;; esac
273
+ if [ "$BLOCK_COUNT" -ge "$BLOCK_LIMIT" ]; then
274
+ echo ""
275
+ echo "## RUNTIME/USER-PATH GATE still red after $BLOCK_COUNT consecutive blocks — allowing stop so the session cannot wedge. The verdict above stays on record; fix the failure in a follow-up session. (Bound: UAP_STOP_BLOCK_LIMIT=$BLOCK_LIMIT; full bypass: UAP_VERIFY_ON_STOP=0.)"
276
+ exit 0
277
+ fi
278
+ printf '%s\n%s\n' "$((BLOCK_COUNT + 1))" "$NOW" > "$BLOCK_FILE" 2>/dev/null || true
240
279
  echo ""
241
280
  echo "## RUNTIME/USER-PATH GATE FAILED — the code does not run or does not work for a real user"
242
281
  echo "$VERIFY_OUT"
@@ -94,6 +94,32 @@ class WriteComplexityTest(unittest.TestCase):
94
94
  self.assertEqual(small[0], 0, small[1])
95
95
  self.assertEqual(big[0], 2, big[1])
96
96
 
97
+ def test_droid_edit_spelling_is_measured_the_same(self):
98
+ # Droid (Factory) sends old_str/new_str. Before 2026-11-12 those
99
+ # spellings measured as None — "whole-file rewrite" — so EVERY Droid
100
+ # Edit escalated as complex regardless of size. Same sizes as the
101
+ # Claude-spelling case must get the same verdicts.
102
+ root = project()
103
+ target = os.path.join(root, "src", "thing.ts")
104
+ small = run(root, {"file_path": target, "old_str": "a" * 20, "new_str": "b" * 20}, op="Edit")
105
+ big = run(root, {"file_path": target, "old_str": "a" * 300, "new_str": "b" * 300}, op="Edit")
106
+ self.assertEqual(small[0], 0, small[1])
107
+ self.assertEqual(big[0], 2, big[1])
108
+
109
+ def test_multiedit_droid_spelling_sums_across_edits(self):
110
+ root = project()
111
+ target = os.path.join(root, "src", "multi.ts")
112
+ code_ok, out_ok = run(root, {"file_path": target, "edits": [
113
+ {"old_str": "a" * 20, "new_str": "b" * 20},
114
+ {"old_str": "c" * 20, "new_str": "d" * 20},
115
+ ]}, op="MultiEdit")
116
+ code_big, _ = run(root, {"file_path": target, "edits": [
117
+ {"old_str": "a" * 200, "new_str": "b" * 200},
118
+ {"old_str": "c" * 200, "new_str": "d" * 200},
119
+ ]}, op="MultiEdit")
120
+ self.assertEqual(code_ok, 0, out_ok)
121
+ self.assertEqual(code_big, 2)
122
+
97
123
  def test_nature_exemptions_are_unchanged(self):
98
124
  root = project()
99
125
  docs = run(root, {"file_path": os.path.join(root, "docs", "n.md"), "content": "x" * 9000})