@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
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
# Prompt Uplift Field Study: a 2-bit Model Passes a Pixel Gate (2026-11)
|
|
2
|
+
|
|
3
|
+
**Date:** 2026-11-12, revised 2026-11-13 · **Subject:** `abliterated-iq2_xs` on a
|
|
4
|
+
frozen Blender build contract, driven by bespoke harness tooling (the repo's UAP
|
|
5
|
+
install is 2.39.1; UAP supplies the memory store and worktree workflow here, not the
|
|
6
|
+
harness — there is no uplift subsystem to attribute a result to)
|
|
7
|
+
**Scope:** one measured prompt-uplift run against a frozen multi-gate acceptance
|
|
8
|
+
contract, and the four transferable lessons it produced. This is a dated study, not
|
|
9
|
+
a feature doc: the harness lives in the experiment repo, not here. It is an
|
|
10
|
+
*uncontrolled field run* in the sense defined by
|
|
11
|
+
[benchmarks/README.md](README.md) — one model, no control arm, prompt changed at every
|
|
12
|
+
step — so it shows magnitude and mechanism, not causation; quote the paired harness for
|
|
13
|
+
causal claims. For the evergreen mechanism the study informs, see
|
|
14
|
+
[guides/DELIVER.md](../guides/DELIVER.md) (iterate against real completion gates) and
|
|
15
|
+
[guides/LOCAL_MODELS.md](../guides/LOCAL_MODELS.md).
|
|
16
|
+
|
|
17
|
+
The revision quarantines 15 of the 124 scored attempts: the `B-visual-v4` and
|
|
18
|
+
`B-visual-v5` window was served by a different model than the study recorded. See
|
|
19
|
+
"An arm switch went unnoticed for two stages".
|
|
20
|
+
|
|
21
|
+
## The experiment
|
|
22
|
+
|
|
23
|
+
Task: write a Blender Python script that builds a rigged, animated, game-ready
|
|
24
|
+
robot. Acceptance is two independent gates, and the model is asked to produce exactly
|
|
25
|
+
one artifact, its build script:
|
|
26
|
+
|
|
27
|
+
- **Structural gate** — 12 checks: exactly 19 bones, 4 clips, under 12k tris, feet
|
|
28
|
+
on the floor, weapon grip at the palm, every mesh bound to the rig, FBX
|
|
29
|
+
round-trip, and so on. Hash-pinned in `.uap/gate-lock.json` (`a519af41c595…`),
|
|
30
|
+
along with the human-readable contract `gates.md`.
|
|
31
|
+
- **Visual gate** — 8 pixel-level checks over 36 rendered PNGs (4 clips × 3 keyframe
|
|
32
|
+
positions × 3 ortho cameras). Pixels are classified by HSV hue, so the gate reads
|
|
33
|
+
the render the way a reviewer would: is the weapon at the hand, are the feet on the
|
|
34
|
+
floor line, is any part floating off in space. Added after the lock file was
|
|
35
|
+
written, so it is pinned by recorded hash (`7672fece1d0e…`) rather than by
|
|
36
|
+
`gate-lock.json` — see the caveat under lesson 1.
|
|
37
|
+
|
|
38
|
+
Model: `abliterated-iq2_xs` (a ~2-bit quantized small model), temp 0, **first attempt
|
|
39
|
+
only, no repair turns**. Each stage is one prompt revision, scored by `prompt_sha`, so
|
|
40
|
+
a score is attributable to an exact prompt.
|
|
41
|
+
|
|
42
|
+
`n` counts scored attempts — 124 of the 130 rows in `.uap/uplift/results.jsonl`; the
|
|
43
|
+
six `--dry-prompt` rows print the prompt and record no score. A build that crashes
|
|
44
|
+
is scored 0, not excluded. `12/12` is the structural gate; `both gates` means 12/12
|
|
45
|
+
*and* 8/8 on the same first attempt. The `arm` column is the model that actually
|
|
46
|
+
served the attempt, established after the fact (see the quarantine section); the
|
|
47
|
+
runner now records `served_model` and scores 0 on `arm_mismatch`.
|
|
48
|
+
|
|
49
|
+
| stage | prompt change | n | arm | 12/12 | both gates | `sys` crashes |
|
|
50
|
+
|-------|---------------|---|-----|-------|------------|---------------|
|
|
51
|
+
| bare | raw prompt | 5 | IQ2_XS | 0 | n/a (no visual gate) | 0 |
|
|
52
|
+
| A-acceptance | 12-check contract + API crib sheet | 38 | IQ2_XS | 23 | n/a (no visual gate) | 0 |
|
|
53
|
+
| B-visual | + render/visual gate | 3 | IQ2_XS | 3 | 0 (visual 3/8–4/8) | 0 |
|
|
54
|
+
| B-visual-v2 | visual rules rewritten | 3 | IQ2_XS | 0 (all three builds crashed) | 0 | 0 |
|
|
55
|
+
| B-visual-v3 | tightened geometry rules | 3 | IQ2_XS | 2 | 0 (visual 6/8 twice) | 0 |
|
|
56
|
+
| B-visual-v4 | vertical limb chains, blade inside hand | 9 | **IQ4_XS v31** | 1 | 1 | 4 |
|
|
57
|
+
| B-visual-v4 | *same prompt*, re-run on the restored IQ2_XS arm (`--tag arm-rerun`) | 9 | IQ2_XS | 8 | 3 | 0 |
|
|
58
|
+
| B-visual-v5 | verbatim import header + copied fix loop | 6 | **IQ4_XS v31** | 6 | 4 (67%) | 0 |
|
|
59
|
+
| B-visual-v5 | *same prompt*, re-run on IQ2_XS | 6 | IQ2_XS | 5 | 2 | 0 |
|
|
60
|
+
| B-visual-v6 | read back the keyed fcurves before saving | 9 | IQ2_XS | 0 (all nine died in the build) | 0 | 0 |
|
|
61
|
+
| B-visual-v6 | `channelbag(create=)` → positional `ensure` | 3 | IQ2_XS | 0 (all three died on that kwarg) | 0 | 0 |
|
|
62
|
+
| B-visual-v6 | pose tables keyed to the 19 `bone_defs` names, appended self-check block at column 0, animation-amplitude rungs | 6 | IQ2_XS | 5 | 5 (83%) | 0 |
|
|
63
|
+
| **B-visual-v6** | *same prompt*, plus one rule: every pose-table row has exactly three elements | 6 | IQ2_XS | **5** | **5 (83%)** | 0 |
|
|
64
|
+
| **B-visual-v6** | *same prompt*, plus a rule banning assignment expressions inside pose tables | 6 | IQ2_XS | **4** | **4 (67%)** | 0 |
|
|
65
|
+
| **B-visual-v6** | *same prompt*, plus a named angle block, a 3-tuple row rule, and a row-shape self-check | 6 | IQ2_XS | **5** | **1 (17%)** | 0 |
|
|
66
|
+
| **B-visual-v6** | *same prompt*, plus the first-slot rule and a `SELFCHECK slot0=` self-check | 6 | IQ2_XS | **5** | **5 (83%)** | 0 |
|
|
67
|
+
|
|
68
|
+
First-attempt rate at *both* gates, measured on IQ2_XS alone, went 0/3 → 3/9 → 2/6 →
|
|
69
|
+
5/6 → 5/6 → 4/6 → 1/6 → 5/6 across the prompt revisions that survive the quarantine.
|
|
70
|
+
The 5/6 rate held across two consecutive prompt revisions; the revision after them
|
|
71
|
+
regressed to 4/6 with two failure modes that neither of its predecessors had shown. The
|
|
72
|
+
revision after *that* closed both of those modes and regressed further, to 1/6, on a
|
|
73
|
+
mode no earlier revision had produced. The revision after *that* — the same prompt plus
|
|
74
|
+
one rule about which Euler slot carries the animated angle — restored 5/6, so three
|
|
75
|
+
prompts now measure 5/6 and only the last of them closes the failure family its
|
|
76
|
+
predecessor exposed. The series reads as a record of which rung falls next rather than
|
|
77
|
+
of monotone improvement: each revision closed the family that was visible and left a
|
|
78
|
+
different one open. v4's nine IQ4-arm runs split four ways: 4 `sys` crashes, 3
|
|
79
|
+
reasoning truncations, 1 built-but-incomplete (11/12, 6/8), 1 clean pass.
|
|
80
|
+
|
|
81
|
+
## An arm switch went unnoticed for two stages
|
|
82
|
+
|
|
83
|
+
`B-visual-v4` and `B-visual-v5` — 15 attempts, the two stages that produced every
|
|
84
|
+
recorded both-gates pass in the original write-up — were not served by the model the
|
|
85
|
+
stage rows say they were. Both arms declare port 8080, so only one could own the
|
|
86
|
+
endpoint, and the runner recorded the *configured* model (`MODEL`,
|
|
87
|
+
`abliterated-iq2_xs`) rather than the served one; `served_model` and the
|
|
88
|
+
`arm_mismatch` guard were added afterwards.
|
|
89
|
+
|
|
90
|
+
Prompt-token counts are a fingerprint, but a weak one inside this window: the same
|
|
91
|
+
prompt file produces the same prompt for either arm, so `13868` and `14828` appear in
|
|
92
|
+
both engine logs. The completion-token count is per-attempt and separates them
|
|
93
|
+
cleanly — each window's counts appear in one log, in attempt order, and not in the
|
|
94
|
+
other at all.
|
|
95
|
+
|
|
96
|
+
| window | prompt tokens | `usage.completion_tokens` | IQ2_XS log | IQ4_XS v31 log |
|
|
97
|
+
|--------|--------------:|---------------------------|-----------:|---------------:|
|
|
98
|
+
| B-visual (n=3) | 8851 | — | 3 lines | 0 |
|
|
99
|
+
| B-visual-v2 (n=3) | 10389 | — | 3 | 0 |
|
|
100
|
+
| B-visual-v3 (n=3) | 10075 | — | 3 | 0 |
|
|
101
|
+
| B-visual-v4 originals (n=9) | 13908 ×3, 13868 ×6 | 5798, 5706, 5680, 5356, 5945, 5597 (6 untruncated) | 2 unrelated lines\* | 3 × 13908 + 6 × 13868 |
|
|
102
|
+
| B-visual-v4 reruns (n=9) | 13868 | 7311, 6741, 6830, 7126, 6163, 7482, 7471, 6927, 7085 | 9 | 0 |
|
|
103
|
+
| B-visual-v5 originals (n=6) | 14828 | 6597, 6585, 6472, 5225, 5189, 6211 | 0 | 6 |
|
|
104
|
+
| B-visual-v5 reruns (n=6) | 14828 | 6858, 5932, 7096, 6064, 6102, 6490 | 6 | 0 |
|
|
105
|
+
| B-visual-v6 (n=42, seven prompt revisions) | 15962 ×9, 16140 ×3, 16517 ×6, 16762 ×6, 16961 ×6, 17629 ×6, 18163 ×6 | 6496, 6208, 5844, 6397, 6791, 6385 (pose-table prompt); 7194, 6738, 6460, 6589, 6764, 6664 (pose-table + row-arity prompt); 6814, 6566, 5774, 6420, 6430, 6316 (pose-table + no-assignment-expression prompt); 6900, 6513, 6415, 7181, 6486, 6832 (pose-table + named angle block); 7363, 6683, 6636, 6743, 7075, 7003 (pose-table + first-slot rule) | 9, 3, 6\*, 6, 6, 6, 6 | 0 |
|
|
106
|
+
|
|
107
|
+
\* Two `13868` lines and one `16517` line in the IQ2_XS log belong to unrelated work:
|
|
108
|
+
they generated 634, 178 and 110 tokens rather than any of the attempt counts above,
|
|
109
|
+
and one of them reused 16,512 of 16,517 tokens. Excluding them, every prompt-token
|
|
110
|
+
line in the window belongs to the arm the completion-token column says it does. The
|
|
111
|
+
last four windows' twenty-four completion counts each appear only in the IQ2_XS log, in
|
|
112
|
+
attempt order: the `16762` window at lines 272807–272924, the `16961` window at lines
|
|
113
|
+
276731–276971, the `17629` window at lines 283670–283950, the `18163` window at lines
|
|
114
|
+
284394, 284446, 284452, 284478, 284512 and 284562, every one of them at its window's
|
|
115
|
+
prompt-token count. The `18163` prompt count appears in neither the IQ4_XS v31 log nor
|
|
116
|
+
the swift-1.5-iq3_s log at all, and that window's six completion counts appear in
|
|
117
|
+
neither. The `16961` window has exactly seven such lines in the
|
|
118
|
+
IQ2_XS log — the six attempts plus one unrelated 19-token generation at line 104791 —
|
|
119
|
+
and one line in the IQ4_XS log at an unrelated 1,378-token generation. The `16762`
|
|
120
|
+
window's `6738` also shows up once in that log at an unrelated 42,964-token prompt, and
|
|
121
|
+
in the IQ4_XS log not at all. The `17629` prompt count appears once in the IQ4_XS log,
|
|
122
|
+
at line 17123 with 237 generated tokens; none of that window's six completion counts
|
|
123
|
+
appear in that log at all.
|
|
124
|
+
|
|
125
|
+
The same log lines carry generation speed, which corroborates: ~150–170 tok/s on
|
|
126
|
+
IQ2_XS against ~80 tok/s on IQ4_XS v31, and the reruns' recorded `gen_elapsed`
|
|
127
|
+
implies IQ2_XS speed. Port 8080 declared `abliterated-iq2_xs` (`GET /v1/models`)
|
|
128
|
+
throughout the rerun windows — it has since been restored to the fleet default
|
|
129
|
+
`strata` — and every `B-visual-v6` row carries
|
|
130
|
+
`served_model=abliterated-iq2_xs` with no `arm_mismatch`.
|
|
131
|
+
|
|
132
|
+
Consequences:
|
|
133
|
+
|
|
134
|
+
- The published `0/3 → 1/9 → 4/6` headline spanned two arms and is replaced above.
|
|
135
|
+
- Every both-gates pass in the original table came from the IQ4_XS arm, and so did
|
|
136
|
+
v4's four `sys` `NameError` crashes, its three reasoning truncations, and the
|
|
137
|
+
17 px walk-cycle failure. Those are not 2-bit-model behaviour.
|
|
138
|
+
- Re-running the same two prompts on the restored IQ2_XS arm still clears the pixel
|
|
139
|
+
gate: 3/9 at v4 and 2/6 at v5, so the claim survives — at a lower rate, and with
|
|
140
|
+
the improvement continuing to 5/6 at v6 for two consecutive prompt revisions, each
|
|
141
|
+
of which cleared both gates five of six attempts. The third revision in that series
|
|
142
|
+
regressed to 4/6 and the fourth to 1/6, so the step to 5/6 is real but neither later
|
|
143
|
+
revision is the one to keep. The fifth revision — the same prompt plus a rule naming
|
|
144
|
+
which Euler slot carries the animated angle — restored 5/6 and closed the family the
|
|
145
|
+
fourth had exposed, so it is the one to keep.
|
|
146
|
+
- The mechanism is lesson 1 with the verifier swapped for the model: integrity
|
|
147
|
+
established after the fact instead of enforced at run time. One line in the
|
|
148
|
+
runner — compare `served_model` to `MODEL` before scoring — would have caught the
|
|
149
|
+
switch on the first affected attempt.
|
|
150
|
+
|
|
151
|
+
## Four lessons
|
|
152
|
+
|
|
153
|
+
### 1. A gate the model can edit is not a gate
|
|
154
|
+
|
|
155
|
+
The lock file states the rule in its own header: the model "may NOT edit these files.
|
|
156
|
+
A hash mismatch fails the run." The runner copies the validator, the visual gate, the
|
|
157
|
+
render script and `gates.md` fresh into each attempt workspace before the build runs,
|
|
158
|
+
then executes the model's script there. That keeps the frozen originals in `tooling/`
|
|
159
|
+
out of the way, but nothing enforces the header's sentence at runtime: the model's
|
|
160
|
+
script runs as ordinary Blender Python with write access to its workspace, and the
|
|
161
|
+
workspace sits inside the same repo as the originals. Integrity was therefore
|
|
162
|
+
established *after* the fact: pinned hash matches live, and every workspace carried
|
|
163
|
+
byte-identical gate copies — one distinct hash across all 117 validator copies, all 78
|
|
164
|
+
visual-gate copies, all 78 render-script copies and all 117 `gates.md` copies, covering
|
|
165
|
+
the 124 scored runs.
|
|
166
|
+
|
|
167
|
+
That audit has one weakness worth naming. `make_workspace` builds
|
|
168
|
+
`runs/<stage>/attempt-N` and `--tag` is **not** in the path, so a rerun `rmtree`s and
|
|
169
|
+
rewrites the directory its predecessor used. The 124 scored attempts land in 102 attempt
|
|
170
|
+
directories, 19 of which were written more than once — 18 pairs (the nine
|
|
171
|
+
`B-visual-v4` and six `B-visual-v5` originals plus their `arm-rerun` twins, and the
|
|
172
|
+
first three `B-visual-v6` attempts plus their `arm-probe` twins) and `bare` attempt 1
|
|
173
|
+
five times, 22 extra writes in total — and the hash audit can only see the **last
|
|
174
|
+
writer** of each. The 15 IQ4-arm workspaces survive only because the quarantine copied
|
|
175
|
+
them to
|
|
176
|
+
`runs/B-visual-v{4,5}-arm-iq4_xs-v31/` before the rerun; those 15 archived directories
|
|
177
|
+
have no rows in `results.jsonl`, so the 117 directories on disk outnumber the 102 that
|
|
178
|
+
carry scores. So "no tampering was found" is a statement about the surviving directory
|
|
179
|
+
contents, not about every attempt that ever ran: the same-hash-per-copy result holds
|
|
180
|
+
for every directory on disk, and the attempt that matters for a pass claim is the one
|
|
181
|
+
whose score row and workspace agree on the same writer.
|
|
182
|
+
|
|
183
|
+
Be precise about the strength of that. The runner copies the gates but does not
|
|
184
|
+
re-verify the hashes *inside* the run, and the workspace copy is writable, so the
|
|
185
|
+
guarantee here is audit-after-the-fact rather than runtime enforcement. That is enough
|
|
186
|
+
to make the pass rate falsifiable — a tampered gate would show up as a hash mismatch
|
|
187
|
+
when checked — but it is not the same as a read-only mount or a pre-flight assertion.
|
|
188
|
+
Closing that gap is cheap: recompute the workspace gate hashes after the model's
|
|
189
|
+
script has written and before the gates execute, and score the attempt 0 on mismatch.
|
|
190
|
+
Four `hashlib` calls in the runner turn "no tampering was found afterwards" into
|
|
191
|
+
"tampering could not have helped". The transferable part stands either way: without a
|
|
192
|
+
pinned verifier, a pass rate is unfalsifiable, because the cheapest path to 12/12
|
|
193
|
+
is editing the checker. This is the
|
|
194
|
+
same `enforce, don't suggest` stance as everywhere else in UAP, applied to the verifier
|
|
195
|
+
rather than the producer.
|
|
196
|
+
|
|
197
|
+
### 2. Score the prompt, not the vibe
|
|
198
|
+
|
|
199
|
+
Every run records `prompt_sha` alongside its score, and prompt files compose with
|
|
200
|
+
`#extends <stage>` so a revision is a delta, not a rewrite. That is what made v4
|
|
201
|
+
diagnosable: the geometry rules demonstrably worked — attempt 5 hit both gates, and
|
|
202
|
+
its static numbers matched the offline geometry test that had been run before the
|
|
203
|
+
stage shipped: 842 tris, feet 0.0000 m, grip 0.0000 m, predicted island ratio 0.088
|
|
204
|
+
against a 0.30 limit and measured 0.087. Its clip diffs came out smaller than the
|
|
205
|
+
offline prediction (predicted 0.020/0.080/0.029/0.161, measured 0.021/0.068/0.027/
|
|
206
|
+
0.084) but every one cleared the movement threshold, so the pass was not a fluke.
|
|
207
|
+
Without per-revision attribution, "v4 is worse than v3" would have buried a working
|
|
208
|
+
fix under an unrelated crash.
|
|
209
|
+
|
|
210
|
+
That pass was an IQ4_XS-served attempt, so the attribution needed the arm column too:
|
|
211
|
+
the same prompt on IQ2_XS cleared both gates 3 of 9 times, which confirms the geometry
|
|
212
|
+
rules rather than the arm. The isolation came from the appended `sys` crash and the
|
|
213
|
+
reasoning truncations, both of which the quarantine section shows were IQ4-side.
|
|
214
|
+
|
|
215
|
+
The same attribution is what made the arm switch recoverable rather than fatal: the
|
|
216
|
+
rerun rows carry the identical `prompt_sha` as the originals and differ only by
|
|
217
|
+
`--tag`, so the two arms can be separated attempt by attempt instead of discarded.
|
|
218
|
+
It also paid for itself forward: v6's five pose-table revisions are a demonstrable
|
|
219
|
+
uplift step, not a lucky streak. The three rungs that failed in the v4 and v5 IQ2_XS
|
|
220
|
+
reruns — the attack clip losing all its weapon pixels (`gap 999`, four times at v4,
|
|
221
|
+
twice at v5), a walk cycle lifting feet past the limit, and jump endpoints leaving the
|
|
222
|
+
root off the floor — each got a named rule, and the next six attempts cleared all eight
|
|
223
|
+
rungs five times. The revision after that, which added one rule and changed the prompt
|
|
224
|
+
hash, cleared them five times again in a second six-attempt window, so the step did not
|
|
225
|
+
rest on one prompt's luck. A third revision, banning assignment expressions inside pose
|
|
226
|
+
tables, cleared them only four times in its own six-attempt window and introduced two
|
|
227
|
+
failure modes its predecessors had not seen; a fourth, which closed both of those,
|
|
228
|
+
cleared them once; and a fifth, which added one rule about which Euler slot carries the
|
|
229
|
+
animated angle, cleared them five times again and closed the family the fourth had
|
|
230
|
+
exposed — which is why the honest headline is 5/6 three times, not 5/6 and rising, and
|
|
231
|
+
why the improvement claim belongs to the prompts that measured 5/6 rather than to the
|
|
232
|
+
series of edits.
|
|
233
|
+
|
|
234
|
+
### 3. A regression can be a missing word, not a wrong idea
|
|
235
|
+
|
|
236
|
+
4 of 9 v4 runs died identically:
|
|
237
|
+
|
|
238
|
+
```
|
|
239
|
+
NameError: name 'sys' is not defined. Did you forget to import 'sys'?
|
|
240
|
+
argv = sys.argv
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
The composed prompt asks for an `argv` guard (`argv` appears 7 times) and never
|
|
244
|
+
contains the words `import sys` — 0 occurrences in 43,382 characters. So the import is
|
|
245
|
+
purely the model's own guess. Reading the generated headers confirms it: v3's three
|
|
246
|
+
runs each happened to write `import bpy, sys, os`, while v4's four crashing runs wrote
|
|
247
|
+
`import bpy` / `import math` / `from mathutils import …` and omitted `sys` — same
|
|
248
|
+
instruction, different coin flip. v5 states the header as four lines to copy verbatim,
|
|
249
|
+
`import sys` included, and the crash count went to 0/6 with the validator at 6/6.
|
|
250
|
+
|
|
251
|
+
State the arm caveat: those four crashes were IQ4_XS runs. On IQ2_XS the v4 rerun had
|
|
252
|
+
0 `sys` crashes and 8/9 at 12/12, so the crash lesson cannot be separated from the
|
|
253
|
+
arm switch by this log alone — but v5's IQ2_XS rerun also had 0 `sys` crashes, and
|
|
254
|
+
its one build failure was an `IndentationError`, a different mode. The instruction
|
|
255
|
+
that matters is the one the failure names: when a prompt asks for a construct, state
|
|
256
|
+
the import or declaration that construct needs, as text to copy rather than prose to
|
|
257
|
+
understand. Small models rewrite headers; they do not reliably infer them.
|
|
258
|
+
|
|
259
|
+
The same shape showed up twice more in v6, both as prompt-text defects rather than
|
|
260
|
+
model defects. The appended post-save self-check block sits indented 4 spaces in the
|
|
261
|
+
prompt file for display, and one v6 run pasted it verbatim into a top-level script and
|
|
262
|
+
died on `IndentationError: unexpected indent` — fixed by a rule that says to paste it
|
|
263
|
+
at column 0; an `IndentationError` also ended one v5 IQ2_XS rerun. And `Action.fcurves`
|
|
264
|
+
does not exist in Blender 5.2: seven v6 runs died on that `AttributeError` — four in
|
|
265
|
+
the original window, three in the arm probe — *after* printing correct self-check
|
|
266
|
+
values and *after* saving the file, so the build exited 0 while the attempt scored 0.
|
|
267
|
+
Every scored row in the log reports `build_rc` 0; a crash is caught from the build
|
|
268
|
+
tail, never from the return code. That is why the prompt
|
|
269
|
+
now ships a measured, verified read path (`act.layers → strips → channelbags → fcurves`)
|
|
270
|
+
and an explicit ban list instead of letting the model invent one.
|
|
271
|
+
|
|
272
|
+
### 4. Reasoning budget is a hard constraint, not a tuning knob
|
|
273
|
+
|
|
274
|
+
Three v4 runs left reasoning on: the model spent 17k–27k reasoning characters before
|
|
275
|
+
writing any code and hit `finish_reason=length` mid-table, at both 8k and 12k max
|
|
276
|
+
tokens, producing an unterminated tuple and a `SyntaxError`. All three scored 0. Those
|
|
277
|
+
three were IQ4_XS runs.
|
|
278
|
+
|
|
279
|
+
The pattern holds across the whole log, not just v4. Of 124 scored runs, 15 had
|
|
280
|
+
reasoning on and 109 had it off. Every one of the 109 reasoning-off runs finished with
|
|
281
|
+
`finish=stop` — none truncated. All 8 truncated runs in the log had reasoning on, and
|
|
282
|
+
all 8 scored 0. Reasoning on did not become safe at a bigger budget either: the 7
|
|
283
|
+
reasoning-on runs that were given 32k–64k tokens did finish, and their best score was
|
|
284
|
+
11/12 — no reasoning-on run in the log ever cleared the structural gate. 12 of the 15
|
|
285
|
+
reasoning-on runs predate the arm switch (the `bare` and `A-acceptance` stages, recorded
|
|
286
|
+
as IQ2_XS), and the conclusion is unchanged on that subset alone: 5 truncated, best
|
|
287
|
+
score 11/12, no 12/12. Meanwhile v5, run with
|
|
288
|
+
`--reasoning-effort none --max-tokens 12000`, scored 12/12 six of six on the IQ4 arm
|
|
289
|
+
and five of six on IQ2_XS, and v6's five pose-table revisions scored 12/12 five, five,
|
|
290
|
+
four, five and five of six on IQ2_XS.
|
|
291
|
+
A truncated run is a budget misconfiguration, not a model failure, and the budget
|
|
292
|
+
must be pinned in the invocation for scores to stay comparable across stages.
|
|
293
|
+
|
|
294
|
+
## Where the remaining failures sit
|
|
295
|
+
|
|
296
|
+
On the IQ2_XS arm the pixel gate's two animation rungs were the last thing standing for
|
|
297
|
+
most of the study, and both were amplitude, not geometry:
|
|
298
|
+
|
|
299
|
+
- **`vis-weapon-at-hand`** — 4 of 9 v4 reruns and 2 of 6 v5 reruns. Every failure
|
|
300
|
+
reported `gaps {'attack.mid': 999}` or `{'attack.end': 999}`: `999` means *no
|
|
301
|
+
weapon pixels found in that frame*, not "drifted a little". The attack pose rotated
|
|
302
|
+
the blade edge-on out of the camera plane.
|
|
303
|
+
- **`vis-feet-on-floor`** — 1 v4 rerun (13 px at `jump.end`, limit 12) and 1 v5 rerun
|
|
304
|
+
(26 px at `walk.end`).
|
|
305
|
+
|
|
306
|
+
The structural gate measures the rest pose, so both builds pass 12/12 and fail the
|
|
307
|
+
pixel gate. Its `feet-on-floor` rung measures the lowest world-Z of any `Foot*` mesh
|
|
308
|
+
vertex in the rest pose against a 0.005 m tolerance — a static measurement — while the
|
|
309
|
+
visual gate measures the lift in every rendered frame. That gap, a structural check
|
|
310
|
+
that never exercises the animated state, is the argument for the visual gate existing
|
|
311
|
+
at all.
|
|
312
|
+
|
|
313
|
+
For scale, the logged margins say the improvement was in headroom, not only in the
|
|
314
|
+
pass rate. All 45 runs that cleared the island rung measured 0.083–0.089 of the bbox
|
|
315
|
+
diagonal against a 0.30 limit except `rule-f` attempt 16 at 0.113, still well under the
|
|
316
|
+
limit, while the five runs that failed it measured 3.142–3.338
|
|
317
|
+
at `B-visual` and 4.504 and 6.393 at `B-visual-v3`; all five `rule-i` clears measured
|
|
318
|
+
0.087. On the floor rung, eight of the
|
|
319
|
+
eleven IQ2_XS runs that cleared it at v4 or v5 reported a max lift of exactly 12 px —
|
|
320
|
+
the limit itself, at `jump.start` — so "cleared" there was a coin flip against the
|
|
321
|
+
next frame. v6's twenty-four clearing runs reported 1–9 px, none at the limit, and all
|
|
322
|
+
five `rule-i` clears reported 9 px at `walk.start` with the feet touching the floor line
|
|
323
|
+
at 1 px on `idle.start`. The rules moved the margin.
|
|
324
|
+
|
|
325
|
+
v6's pose-table prompt is what closed the two rungs: the attack clip keys `upper_arm_R`
|
|
326
|
+
only (0 → −70° → 0, forearm and hand never keyed, so the blade stays face-on to the
|
|
327
|
+
camera), the walk cycle keys both thighs ±20° with shins capped at ±12°, and the jump
|
|
328
|
+
holds root-z 0 at both endpoints with endpoint thigh keys at 0. Its failures after that
|
|
329
|
+
came in two families: names and expressions written inside pose-table rows, and, once
|
|
330
|
+
those were gone, which Euler slot carries the animation angle. Each revision closed one
|
|
331
|
+
family while the model found the other:
|
|
332
|
+
|
|
333
|
+
- attempt 10 wrote a pose-table row with two tuple elements where `build_clip`
|
|
334
|
+
unpacked three (`ValueError: not enough values to unpack (expected 3, got 2)`), and
|
|
335
|
+
the prompt's existing "one tuple shape for every row" rule covers the *parts* table,
|
|
336
|
+
not the pose tables.
|
|
337
|
+
- That rule shipped in the revision after it, which scored 5/6 again. Its one failure
|
|
338
|
+
was a second member: attempt 19 put a walrus target that is not a name inside a
|
|
339
|
+
pose-table row (`SyntaxError: cannot use assignment expressions with expression`, at
|
|
340
|
+
`(-d30 := math.radians(-30.0), d0, d0)`), so the arity rule fixed the shape of the
|
|
341
|
+
row without constraining the expressions inside it.
|
|
342
|
+
- The revision after that banned assignment expressions inside pose tables outright.
|
|
343
|
+
It scored 4/6, and its two failures were two more members of the same family rather
|
|
344
|
+
than a repeat of either: attempt 24 mixed types in the rotation slot — `d0` is the
|
|
345
|
+
3-tuple `(0.0, 0.0, 0.0)` while `d10, d20, dm10, dm20, dm70` are scalar
|
|
346
|
+
`math.radians(…)` values, and rows pass those bare names straight into
|
|
347
|
+
`Euler(rot, 'XYZ')` — so `Euler()` received a scalar where it wanted a triple and
|
|
348
|
+
died with the bare message `TypeError: mathutils.Euler()`; re-executing its tables in
|
|
349
|
+
order, the first failing call is `Idle / spine_03 / frame 24` with `rot = 0.1745`,
|
|
350
|
+
five rows *upstream* of the walrus row that first drew attention to this attempt.
|
|
351
|
+
Attempt 25 referenced `dm30` in a row without ever assigning it (`NameError: name
|
|
352
|
+
'dm30' is not defined. Did you mean: 'dm10'?`) — it obeyed the ban on `:=` and the
|
|
353
|
+
arity rule, then used a name the prompt's own crib sheet does not define.
|
|
354
|
+
|
|
355
|
+
Banning one syntax did not make the rows plain. The next revision shipped exactly that
|
|
356
|
+
stronger rule: a named angle block above the tables (`d0, d10, dm10, d20, dm20, dm30,
|
|
357
|
+
dm70`, all scalar `math.radians(…)`), a requirement that every row be `(int frame,
|
|
358
|
+
3-tuple location, 3-tuple rotation)`, an explicit ban on bare scalars and inline
|
|
359
|
+
`math.radians()` in the rotation slot, and a pre-keyframe self-check printing
|
|
360
|
+
`SELFCHECK pose_rows=<count> all_rows_3=True rot_all_triples=True`. It closed the
|
|
361
|
+
family: all six attempts used only the seven listed angle names, none used `:=`, five
|
|
362
|
+
printed the self-check line, and the structural rate rose to 5/6. It also regressed the
|
|
363
|
+
both-gates rate to 1/6, on a failure no earlier revision had produced.
|
|
364
|
+
|
|
365
|
+
Four of those five structural passes failed `vis-clips-move` with identical numbers:
|
|
366
|
+
`walk` diff 0.0037, `attack` diff 0.0007, only `idle` and `jump` moving against a
|
|
367
|
+
"3 of 4" requirement. The cause is which Euler slot carries the angle. All four keyed
|
|
368
|
+
the attack as `(d0, dm70, d0)`; the one pass keyed it `(dm70, d0, d0)`. Rebuilding the
|
|
369
|
+
same script in a scratch workspace with nothing changed but that slot ordering moved the
|
|
370
|
+
attack diff from 0.0007 to 0.0296 and the visual score from 7/8 to 8/8; moving the walk
|
|
371
|
+
thigh angles to the first slot as well took walk from 0.0037 to 0.0768. The limb bones
|
|
372
|
+
run along their local −Z, so a Y-slot rotation mostly foreshortens a limb instead of
|
|
373
|
+
sweeping it across the camera planes, and the gate's pixel-difference test never sees it.
|
|
374
|
+
|
|
375
|
+
That rule shipped in the fifth revision, which scored 5/6 again. It changed RULE H's
|
|
376
|
+
worked example from `(d0, dm20, d0)` to `(dm20, d0, d0)` and added RULE I: the measured
|
|
377
|
+
failure, the mechanism, the requirement that the animated angle go in the *first* slot
|
|
378
|
+
of the rotation triple, and a self-check line `SELFCHECK slot0=<count>`. All six
|
|
379
|
+
attempts reported the same 18,163-token prompt, five cleared both gates, and an
|
|
380
|
+
independent scan of the scripts' syntax trees found non-zero rotation slots only in the
|
|
381
|
+
first position — eight or ten animated rows per script, and zero second-slot patterns,
|
|
382
|
+
where every rule-h failure had *only* second-slot patterns. RULE H's family stayed
|
|
383
|
+
closed: no `Euler()` type errors, no undefined angle names, no `:=`. The one failure was
|
|
384
|
+
a returning family, not a new one: attempt 39 wrote a **7-element** row into the
|
|
385
|
+
parts table at line 96 (`Forearm_R`, with the location tuple duplicated into the size
|
|
386
|
+
slot) against the 6-name unpack at line 148, and the build died on `ValueError: too many
|
|
387
|
+
values to unpack (expected 6)` before it reached keyframing or the save at lines 377 and
|
|
388
|
+
401, so `robot.blend` was never written and the runner scored 0 before validating or
|
|
389
|
+
rendering. `build_rc` is 0 because Blender 5.2 exits 0 on an uncaught script exception
|
|
390
|
+
under `-b -P`; the traceback is in the row's build tail, and the workspace holds the
|
|
391
|
+
18,155-character script but no `.blend`. Every self-check the earlier revisions added
|
|
392
|
+
lives *below* the crash (line 263 onward), so none of them ran for that attempt. It is
|
|
393
|
+
the fourth unpack-arity failure in the log — `A-acceptance` attempt 9 died on `bone_defs`
|
|
394
|
+
(`expected 4, got 3`), `B-visual-v2` attempt 1 on `parts` (`too many values … expected
|
|
395
|
+
5`), `prompt-fix-rerun` attempt 10 on the pose keys (`expected 3, got 2`) — and the
|
|
396
|
+
second on the parts table. RULES F, G, H and I pin the *pose* tables' row shape; the
|
|
397
|
+
prompt shows a parts table to copy but never states its field count or asks for a
|
|
398
|
+
self-check on it.
|
|
399
|
+
|
|
400
|
+
So the same lesson as the structural rungs, one level down: the rung that falls next is
|
|
401
|
+
the one the prompt has not described yet, and a rule that fixes types can unmask a rule
|
|
402
|
+
about axes. The slot rule is now measured rather than recommended, and it restored 5/6
|
|
403
|
+
while closing the family its predecessor exposed. Two rungs remain open, both below the
|
|
404
|
+
tables the prompt already pins. First, state the parts table's field count and order and
|
|
405
|
+
require a printed count of rows that match it, before any mesh is built. Second, the
|
|
406
|
+
artifact itself: require the script to save, then assert the saved file exists and is
|
|
407
|
+
non-empty, and print its path and size, so a clean exit that wrote nothing cannot score
|
|
408
|
+
as a build.
|
|
409
|
+
|
|
410
|
+
## Reproduce
|
|
411
|
+
|
|
412
|
+
Harness, prompts, findings, and per-attempt workspaces live in the experiment repo
|
|
413
|
+
(`blender-test-1`): `tooling/uplift_runner.py`, `.uap/uplift/prompts/`,
|
|
414
|
+
`.uap/uplift/FINDINGS-B-visual-v{4,5,6}.md`,
|
|
415
|
+
`.uap/uplift/FINDINGS-arm-contamination.md`, `.uap/uplift/runs/`.
|
|
416
|
+
|
|
417
|
+
```
|
|
418
|
+
python3 tooling/uplift_runner.py --stage B-visual-v6 --attempt N \
|
|
419
|
+
--reasoning-effort none --max-tokens 12000 --tag rule-i-rerun
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
Pin `--reasoning-effort none` and `--max-tokens 12000` (lesson 4), keep `--tag` so an
|
|
423
|
+
attempt served by a different arm stays separable in `results.jsonl` (the quarantine
|
|
424
|
+
section) — the tagged rerun windows used `--tag arm-rerun`, `api-fix-rerun`,
|
|
425
|
+
`prompt-fix-rerun`, `rule-f-rerun`, `rule-g-rerun`, `rule-h-rerun` and `rule-i-rerun`,
|
|
426
|
+
and `.uap/uplift/rerun-arm.sh`, `.uap/uplift/rerun-rule-g.sh`,
|
|
427
|
+
`.uap/uplift/rerun-rule-h.sh` and `.uap/uplift/rerun-rule-i.sh` drive whole windows —
|
|
428
|
+
and confirm the endpoint before scoring: `GET /v1/models` on port 8080 must return
|
|
429
|
+
`abliterated-iq2_xs`. Note that
|
|
430
|
+
`--tag` does **not** change the workspace path, so a rerun overwrites the attempt
|
|
431
|
+
directory of any earlier run with the same `--stage` and `--attempt`; archive the
|
|
432
|
+
directory first if the original run's artifacts still matter, as the quarantine did.
|
|
433
|
+
The rule windows therefore used fresh attempt numbers — 10–15, 16–21, 22–27, 28–33 and
|
|
434
|
+
34–39 — so no earlier workspace was overwritten. As of this revision the on-disk
|
|
435
|
+
`B-visual-v6` prompt carries the first-slot rule (RULE I) on top of the named-angle
|
|
436
|
+
block (RULE H): composed prompt sha `e6733cb043d58a8f`, 18,163 prompt tokens, attempts
|
|
437
|
+
34–39.
|
|
438
|
+
`tooling/uplift_runner.py` also now hard-fails on a fifth chained `#extends` link or a
|
|
439
|
+
cycle (`EXTENDS_MAX_DEPTH`), where it used to silently truncate the composition.
|
|
@@ -61,6 +61,7 @@ authoritative.
|
|
|
61
61
|
| [Token Optimization](TOKEN_OPTIMIZATION.md) | Where the token savings come from |
|
|
62
62
|
| [Accuracy Analysis](ACCURACY_ANALYSIS.md) | Success-rate and error analysis |
|
|
63
63
|
| [Comprehensive Benchmarks](COMPREHENSIVE_BENCHMARKS.md) | Extended measurements |
|
|
64
|
+
| [Prompt Uplift Field Study](PROMPT_UPLIFT_BLENDER.md) | Uncontrolled field run: prompt revisions only, one 2-bit model, frozen structural + pixel gates (0/3 → 4/6). Descriptive, no control arm |
|
|
64
65
|
|
|
65
66
|
## Task suites and raw runs
|
|
66
67
|
|
|
@@ -171,6 +171,56 @@ For the full picture — the `uap proxy` lifecycle, the reliability guardrails
|
|
|
171
171
|
that keep a small model from wedging, security, and serving recipes — see the
|
|
172
172
|
**[Inference Proxy guide](PROXY.md)**.
|
|
173
173
|
|
|
174
|
+
## Droid (Factory) custom models — auto-populated from the live arm
|
|
175
|
+
|
|
176
|
+
Droid reads its model list from `customModels` in `~/.factory/settings.json`
|
|
177
|
+
(user) and `<project>/.factory/settings.json` (project). Those entries are
|
|
178
|
+
static per file — Droid does not discover local servers — so a backend swap
|
|
179
|
+
would otherwise leave every Droid instance pinning a retired model id.
|
|
180
|
+
|
|
181
|
+
[`scripts/sync-local-agent-configs.sh`](../../scripts/sync-local-agent-configs.sh)
|
|
182
|
+
closes that gap, and never transcribes the alias: it reads the model id and
|
|
183
|
+
geometry from `config/model-profiles/qwen38.json` (the same single source of
|
|
184
|
+
truth the rails suite drift-checks against the live `/props` on :8080), then:
|
|
185
|
+
|
|
186
|
+
- migrates every retired local id (the registry's `retired_local_aliases`
|
|
187
|
+
list — the single source, never transcribed into the script) onto the
|
|
188
|
+
alias the current arm serves — proxy entries to `:4000`, direct-Responses
|
|
189
|
+
entries stay on `:8080`;
|
|
190
|
+
- **adds** the entry when a settings file has no local qwen model at all, so
|
|
191
|
+
every Droid instance (user-level settings cover all projects) can call it;
|
|
192
|
+
- sizes entries to the profile's per-session cap (114688 via the proxy, the
|
|
193
|
+
131072 pool on the direct path) and the proxy tool-turn cap (32768);
|
|
194
|
+
- pins `baseModelId` on Anthropic-protocol entries (default
|
|
195
|
+
`claude-sonnet-4-6`, override `DROID_ANTHROPIC_BASE_MODEL`) so the entry
|
|
196
|
+
inherits model-specific prompts, tools, and Script-mode eligibility instead
|
|
197
|
+
of the "custom model does not map to a built-in" fallback behavior;
|
|
198
|
+
- collapses duplicate picker lines: entries sharing `(model, baseUrl,
|
|
199
|
+
displayName)` are one line rendered twice — Droid re-copies legacy
|
|
200
|
+
`config.json` entries into `settings.json` under fresh ids each copy era,
|
|
201
|
+
so each era leaves a full extra set; a group keeps the canonical entry,
|
|
202
|
+
else its newest member;
|
|
203
|
+
- repoints dead `modelFavorites` pins from the local-entry id family onto the
|
|
204
|
+
surviving local entry, so the favorites bar keeps selecting the local stack
|
|
205
|
+
across arm swaps instead of rendering a dead line.
|
|
206
|
+
|
|
207
|
+
For "at any given time" freshness, run it with `MODELS_ONLY=1`: that mode
|
|
208
|
+
converges **only** the `.factory` custom-model lists, takes the alias from the
|
|
209
|
+
LIVE backend (`/props` on :8080, profile fallback when unreachable), and
|
|
210
|
+
touches nothing else. The wiring is machine-local, not shipped:
|
|
211
|
+
`.factory/` is gitignored, so the repo cannot carry the hook — wire it once
|
|
212
|
+
per machine (project or user `.factory/settings.json`) as a `SessionStart`
|
|
213
|
+
hook running exactly that, per [ADR
|
|
214
|
+
0015](../architecture/adr/0015-session-hook-model-convergence-as-standing-consent.md),
|
|
215
|
+
and every Droid session starts with the qwen list pointed at whatever arm is
|
|
216
|
+
actually serving. `DRY_RUN=1` shows the plan; a run backs up only what it
|
|
217
|
+
changes.
|
|
218
|
+
|
|
219
|
+
```bash
|
|
220
|
+
bash scripts/sync-local-agent-configs.sh # full operator sync
|
|
221
|
+
MODELS_ONLY=1 bash scripts/sync-local-agent-configs.sh # droid lists only
|
|
222
|
+
```
|
|
223
|
+
|
|
174
224
|
## Related
|
|
175
225
|
|
|
176
226
|
- [Deploy Batching](./DEPLOY_BATCHING.md) — what `uap deliver --deploy` queues.
|
package/package.json
CHANGED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
#
|
|
3
|
+
# droid-sync-contracts.sh — SHARED CONTRACT for Droid (Factory) model sync.
|
|
4
|
+
# -----------------------------------------------------------------------------
|
|
5
|
+
# Every Droid/Factory instance — the user-level ~/.factory/config.json and
|
|
6
|
+
# ~/.factory/settings.json AND the repo-level .factory/settings.json — must
|
|
7
|
+
# auto-converge its customModels onto the LIVE local qwen3.8 backend instead
|
|
8
|
+
# of drifting onto retired ids and stale hosts.
|
|
9
|
+
#
|
|
10
|
+
# This file is the single place the installer and the sandbox test agree on:
|
|
11
|
+
# the variable names and their defaults. It is SOURCED (never executed) by
|
|
12
|
+
# scripts/sync-local-agent-configs.sh and by scripts/test-sync-sandbox.sh, so
|
|
13
|
+
# the installer and the assertions read ONE contract instead of two
|
|
14
|
+
# transcriptions of it — a number or path transcribed in both places is
|
|
15
|
+
# exactly the failure class that left the sandbox suite red through the
|
|
16
|
+
# 1-rail era. The FUNCTION bodies live in sync-local-agent-configs.sh
|
|
17
|
+
# (sync_droid_models / droid_sync_lock); the sandbox asserts them textually
|
|
18
|
+
# against that file, so this contract stays free of dead stub code.
|
|
19
|
+
#
|
|
20
|
+
# CONTRACT RULES (this file is frozen once accepted):
|
|
21
|
+
# * Sourcing is SIDE-EFFECT FREE: no I/O, no mkdir, no network, no writes.
|
|
22
|
+
# Every value is a `: "${VAR:=default}"` assignment, so a caller may
|
|
23
|
+
# pre-set any of them (the sandbox does, to point at a throwaway $HOME)
|
|
24
|
+
# and the caller's value always wins.
|
|
25
|
+
# * Nothing here depends on bash features beyond 4.2.
|
|
26
|
+
#
|
|
27
|
+
# shellcheck shell=bash
|
|
28
|
+
#
|
|
29
|
+
|
|
30
|
+
# Double-source guard: the installer and the sandbox may both source this.
|
|
31
|
+
if [ -n "${_UAP_DROID_SYNC_CONTRACTS_LOADED:-}" ]; then
|
|
32
|
+
return 0 2>/dev/null || true
|
|
33
|
+
fi
|
|
34
|
+
_UAP_DROID_SYNC_CONTRACTS_LOADED=1
|
|
35
|
+
|
|
36
|
+
# ===========================================================================
|
|
37
|
+
# SHARED VARIABLES — all overridable from the environment.
|
|
38
|
+
# ===========================================================================
|
|
39
|
+
|
|
40
|
+
# MODELS_ONLY — when "1", the sync touches ONLY the customModels/custom_models
|
|
41
|
+
# arrays of the Droid config files. No service operations (systemctl/curl),
|
|
42
|
+
# no proxy env requirement, no other agent configs. This is the mode the
|
|
43
|
+
# auto-convergence session hook runs in: it must be safe to fire on every
|
|
44
|
+
# session start, on any machine state.
|
|
45
|
+
# "0" (default) = full sync "1" = customModels only
|
|
46
|
+
: "${MODELS_ONLY:=0}"
|
|
47
|
+
|
|
48
|
+
# REPO_ROOT — absolute path of the checkout the sync is running against.
|
|
49
|
+
# Derived from this file's own location (scripts/lib/ -> repo root), never
|
|
50
|
+
# from $PWD, so a hook fired from a subdirectory still resolves the repo
|
|
51
|
+
# .factory/settings.json correctly. A caller (the installer, the sandbox)
|
|
52
|
+
# may pre-set it to redirect the whole sync at a throwaway tree.
|
|
53
|
+
: "${REPO_ROOT:=$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
|
|
54
|
+
|
|
55
|
+
# PROJECT_FACTORY_SET — the repo-level Droid settings file whose customModels
|
|
56
|
+
# must converge alongside the user-level ones. camelCase spelling
|
|
57
|
+
# ("customModels"), same shape as ~/.factory/settings.json.
|
|
58
|
+
: "${PROJECT_FACTORY_SET:=$REPO_ROOT/.factory/settings.json}"
|
|
59
|
+
|
|
60
|
+
# DROID_ANTHROPIC_BASE_MODEL — the Factory BUILT-IN catalog id written into
|
|
61
|
+
# `baseModelId` of anthropic-provider entries. A custom Anthropic-provider
|
|
62
|
+
# model inherits that built-in's prompt/tool-call behavior; without it Droid
|
|
63
|
+
# warns the entry "does not map to a built-in model". claude-sonnet-4-6 is the
|
|
64
|
+
# choice because it accepts reasoning-effort "off" (the proxy sends none) and
|
|
65
|
+
# speaks the Anthropic Messages class the :4000 proxy serves. This is NOT a
|
|
66
|
+
# local model id — it must never carry a qwen id.
|
|
67
|
+
: "${DROID_ANTHROPIC_BASE_MODEL:=claude-sonnet-4-6}"
|
|
68
|
+
|
|
69
|
+
# DROID_SYNC_LOCK — lock file serialising concurrent Droid config writes
|
|
70
|
+
# (session-start hook + manual installer run + a second Droid window).
|
|
71
|
+
# Without it two writers read-modify-write the same JSON and one set of
|
|
72
|
+
# customModels silently loses the other's update. Defaults under
|
|
73
|
+
# $XDG_RUNTIME_DIR so it is per-user and per-boot; /tmp is the fallback for
|
|
74
|
+
# environments without XDG_RUNTIME_DIR (containers, CI sandboxes).
|
|
75
|
+
# $XDG_RUNTIME_DIR is per-user (0700 tmpfs); the /tmp fallback is
|
|
76
|
+
# world-writable, so key it per-uid there or any local user could hold the
|
|
77
|
+
# lock forever and silently disable convergence.
|
|
78
|
+
if [ -n "${XDG_RUNTIME_DIR:-}" ]; then
|
|
79
|
+
: "${DROID_SYNC_LOCK:=$XDG_RUNTIME_DIR/uap-droid-sync.lock}"
|
|
80
|
+
else
|
|
81
|
+
: "${DROID_SYNC_LOCK:=/tmp/uap-droid-sync-$UID.lock}"
|
|
82
|
+
fi
|
|
83
|
+
|
|
84
|
+
# ===========================================================================
|
|
85
|
+
# FUNCTION CONTRACT (bodies live in sync-local-agent-configs.sh)
|
|
86
|
+
# ===========================================================================
|
|
87
|
+
# droid_sync_lock — acquire fd 9 on $DROID_SYNC_LOCK with `flock -n`.
|
|
88
|
+
# returns 0 = lock held for this script; 1 = another sync holds it.
|
|
89
|
+
#
|
|
90
|
+
# sync_droid_models <FILE> <CREATE> — converge ONE Droid config file.
|
|
91
|
+
# FILE : a Droid settings.json (camelCase customModels) or legacy
|
|
92
|
+
# config.json (snake_case custom_models).
|
|
93
|
+
# CREATE : "1" = create the file (with the canonical entry) when absent —
|
|
94
|
+
# only settings targets use this; the legacy file is patched
|
|
95
|
+
# in place or left alone. "0" = patch only when present.
|
|
96
|
+
# Reads from the caller's scope: MODEL_ALIAS, PROXY_URL, DIRECT_URL,
|
|
97
|
+
# CTX_SESSION, CTX_POOL, MAX_OUTPUT, DROID_ANTHROPIC_BASE_MODEL,
|
|
98
|
+
# PROXY_TOKEN (via UAP_SYNC_TOKEN env), DRY_RUN.
|
|
99
|
+
# Migration rules (idempotent; no change => file left byte-identical):
|
|
100
|
+
# * entries whose model is a RETIRED local id — or already the live
|
|
101
|
+
# alias, or model-less but pointed at :4000/:8080 — converge onto
|
|
102
|
+
# MODEL_ALIAS;
|
|
103
|
+
# * route by existing shape: baseUrl with :8080 -> direct strata route
|
|
104
|
+
# (pool-sized); :4000 WITHOUT /v1 -> Anthropic Messages root (proxy
|
|
105
|
+
# root + baseModelId + apiKey only if absent, session-sized); anything
|
|
106
|
+
# else local -> proxy chat route ($PROXY_URL/v1 + token);
|
|
107
|
+
# * cloud models (anything else, including the :8317 router entries) are
|
|
108
|
+
# deliberate user choices and are never repointed;
|
|
109
|
+
# * a settings.json with NO local entry after migration gains the
|
|
110
|
+
# canonical proxy entry (add-if-missing);
|
|
111
|
+
# * the 192.168.1.165:8317 split-brain host is fixed everywhere;
|
|
112
|
+
# * writes are atomic (tmp file in the same directory + os.replace),
|
|
113
|
+
# under the flock, with backups taken by the caller's backup().
|
|
114
|
+
# returns 0 always (a per-file failure is a printed WARN, not an abort).
|
|
Binary file
|
|
Binary file
|
|
Binary file
|