@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.
- package/config/capacity-policy.json +25 -0
- package/config/model-profiles/qwen38.json +9 -8
- package/config/model-registry.json +670 -20
- package/dist/.tsbuildinfo +1 -1
- package/dist/cli/deliver.js +1 -1
- package/dist/cli/deliver.js.map +1 -1
- package/dist/cli/models.d.ts +6 -0
- package/dist/cli/models.d.ts.map +1 -1
- package/dist/cli/models.js +11 -2
- package/dist/cli/models.js.map +1 -1
- package/dist/cli/verify.d.ts +9 -1
- package/dist/cli/verify.d.ts.map +1 -1
- package/dist/cli/verify.js +15 -3
- package/dist/cli/verify.js.map +1 -1
- package/dist/delivery/acceptance-judge.d.ts +12 -0
- package/dist/delivery/acceptance-judge.d.ts.map +1 -1
- package/dist/delivery/acceptance-judge.js +109 -8
- package/dist/delivery/acceptance-judge.js.map +1 -1
- package/dist/inference/model-files.d.ts.map +1 -1
- package/dist/inference/model-files.js +6 -1
- package/dist/inference/model-files.js.map +1 -1
- package/dist/models/types.d.ts.map +1 -1
- package/dist/models/types.js +14 -2
- package/dist/models/types.js.map +1 -1
- package/docs/INDEX.md +1 -0
- package/docs/architecture/adr/0015-session-hook-model-convergence-as-standing-consent.md +68 -0
- package/docs/benchmarks/PROMPT_UPLIFT_BLENDER.md +439 -0
- package/docs/benchmarks/README.md +1 -0
- package/docs/guides/LOCAL_MODELS.md +50 -0
- package/package.json +1 -1
- package/scripts/lib/droid-sync-contracts.sh +114 -0
- package/src/policies/enforcers/__pycache__/_common.cpython-311.pyc +0 -0
- package/src/policies/enforcers/__pycache__/expert_review_required.cpython-311.pyc +0 -0
- package/src/policies/enforcers/__pycache__/quality_metrics_gate.cpython-311.pyc +0 -0
- package/src/policies/enforcers/delivery_enforcement.py +34 -12
- package/templates/hooks/__pycache__/deliver_autoroute.cpython-311.pyc +0 -0
- package/templates/hooks/pre-tool-use-bash.sh +34 -3
- package/templates/hooks/stop.sh +39 -0
- package/tools/agents/scripts/__pycache__/toolcall_path_normalizer.cpython-311.pyc +0 -0
- 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
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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
|
-
|
|
464
|
-
|
|
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
|
-
|
|
872
|
-
|
|
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)
|
|
Binary file
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
package/templates/hooks/stop.sh
CHANGED
|
@@ -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"
|
|
Binary file
|
|
@@ -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})
|