@miller-tech/uap 1.183.3 → 1.184.1
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/dist/.tsbuildinfo +1 -1
- package/dist/bin/cli.js +14 -0
- package/dist/bin/cli.js.map +1 -1
- package/dist/cli/principles.d.ts +10 -0
- package/dist/cli/principles.d.ts.map +1 -0
- package/dist/cli/principles.js +138 -0
- package/dist/cli/principles.js.map +1 -0
- package/dist/config/policy-recommendations.d.ts.map +1 -1
- package/dist/config/policy-recommendations.js +17 -0
- package/dist/config/policy-recommendations.js.map +1 -1
- package/dist/config/settings-registry.d.ts +1 -1
- package/dist/config/settings-registry.d.ts.map +1 -1
- package/dist/config/settings-registry.js +24 -0
- package/dist/config/settings-registry.js.map +1 -1
- package/dist/coordination/reactor.d.ts.map +1 -1
- package/dist/coordination/reactor.js +15 -0
- package/dist/coordination/reactor.js.map +1 -1
- package/dist/delivery/convergence-loop.d.ts +7 -1
- package/dist/delivery/convergence-loop.d.ts.map +1 -1
- package/dist/delivery/convergence-loop.js +32 -4
- package/dist/delivery/convergence-loop.js.map +1 -1
- package/dist/delivery/judge.d.ts.map +1 -1
- package/dist/delivery/judge.js +6 -0
- package/dist/delivery/judge.js.map +1 -1
- package/dist/delivery/task-orchestrator.d.ts.map +1 -1
- package/dist/delivery/task-orchestrator.js +8 -1
- package/dist/delivery/task-orchestrator.js.map +1 -1
- package/dist/principles/config.d.ts +10 -0
- package/dist/principles/config.d.ts.map +1 -0
- package/dist/principles/config.js +32 -0
- package/dist/principles/config.js.map +1 -0
- package/dist/principles/index.d.ts +16 -0
- package/dist/principles/index.d.ts.map +1 -0
- package/dist/principles/index.js +34 -0
- package/dist/principles/index.js.map +1 -0
- package/dist/principles/reactor-inject.d.ts +12 -0
- package/dist/principles/reactor-inject.d.ts.map +1 -0
- package/dist/principles/reactor-inject.js +69 -0
- package/dist/principles/reactor-inject.js.map +1 -0
- package/dist/principles/render.d.ts +14 -0
- package/dist/principles/render.d.ts.map +1 -0
- package/dist/principles/render.js +64 -0
- package/dist/principles/render.js.map +1 -0
- package/dist/principles/rules.d.ts +53 -0
- package/dist/principles/rules.d.ts.map +1 -0
- package/dist/principles/rules.js +101 -0
- package/dist/principles/rules.js.map +1 -0
- package/dist/principles/stance.d.ts +54 -0
- package/dist/principles/stance.d.ts.map +1 -0
- package/dist/principles/stance.js +154 -0
- package/dist/principles/stance.js.map +1 -0
- package/dist/types/config.d.ts +46 -0
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/config.js +17 -0
- package/dist/types/config.js.map +1 -1
- package/docs/guides/POLICIES.md +43 -0
- package/docs/reference/CONFIGURATION_REFERENCE.md +221 -0
- package/package.json +2 -2
- package/src/policies/enforcers/__pycache__/_common.cpython-312.pyc +0 -0
- package/src/policies/enforcers/codebase_read_before_plan.py +33 -1
- package/src/policies/enforcers/expert_review_required.py +90 -5
- package/src/policies/enforcers/memory_before_plan.py +24 -5
- package/src/policies/enforcers/workdir_scope.py +150 -6
- package/src/policies/schemas/policies/engineering-principles.md +93 -0
- package/templates/hooks/__pycache__/deliver_autoroute.cpython-312.pyc +0 -0
- package/tools/agents/scripts/__pycache__/toolcall_path_normalizer.cpython-312.pyc +0 -0
- package/tools/agents/tests/test_expert_review_pr_scope.py +201 -0
- package/tools/agents/tests/test_workdir_scope_enforcer.py +152 -0
|
@@ -26,6 +26,7 @@ uap config wizard # interactive expert configurator (also: uap set
|
|
|
26
26
|
- [Orchestrator & hands-free](#orchestration) — Long-task autonomy: decompose, resume, and loop-to-100%.
|
|
27
27
|
- [Reactor (auto-apply)](#reactor) — Per-prompt injection of the matching experts, skills, and patterns.
|
|
28
28
|
- [Design system](#design) — DESIGN.md interrogation and the hard token gate for UI work.
|
|
29
|
+
- [Engineering principles](#principles) — How code should be written, and the backward-compatibility stance.
|
|
29
30
|
- [Worktree workflow](#worktree) — Branch-per-feature isolation and auto-cleanup.
|
|
30
31
|
- [Inference proxy tuning](#proxy) — Guardrails and context limits for a local model behind the proxy.
|
|
31
32
|
- [Dashboard](#dashboard) — The live analytics server and its mutation token.
|
|
@@ -60,6 +61,18 @@ How local-model sessions are routed through delivery. `deliver` runs builds thro
|
|
|
60
61
|
|
|
61
62
|
**Recommendation:** `deliver` when a local model does the writing (routes it through the verified loop); `advisory` for exploratory work.
|
|
62
63
|
|
|
64
|
+
### `deliver.escalateModel`
|
|
65
|
+
|
|
66
|
+
| | |
|
|
67
|
+
|---|---|
|
|
68
|
+
| **Where** | `.uap.json` |
|
|
69
|
+
| **Type** | string |
|
|
70
|
+
| **Default** | `null` |
|
|
71
|
+
|
|
72
|
+
Stronger model id for deliver escalation ladders: repair passes, the phase-5 escalation tier, and the evaluator fallback. Same role as $UAP_ESCALATE_MODEL but persisted in .uap.json (reproducible).
|
|
73
|
+
|
|
74
|
+
**Recommendation:** Point at your strongest available preset/model (e.g. an Opus cloud id) when the executor is a local model — stuck epics then escalate instead of re-splitting into the same wall.
|
|
75
|
+
|
|
63
76
|
### `UAP_ENFORCE_DELIVERY`
|
|
64
77
|
|
|
65
78
|
| | |
|
|
@@ -100,6 +113,30 @@ Installs the runtime-verify Stop-hook: at end of turn it actually runs the chang
|
|
|
100
113
|
|
|
101
114
|
**Recommendation:** Enable for any project with a runnable artifact — it catches "declared done but never ran". Safe on empty projects (it skips when nothing is runnable).
|
|
102
115
|
|
|
116
|
+
### `delivery.userValidation`
|
|
117
|
+
|
|
118
|
+
| | |
|
|
119
|
+
|---|---|
|
|
120
|
+
| **Where** | `.uap.json` |
|
|
121
|
+
| **Type** | string |
|
|
122
|
+
| **Default** | `block` |
|
|
123
|
+
|
|
124
|
+
User-path validation gate: deliver runs the .uap/user-paths.json critical journeys through the real client (headless browser / HTTP / built CLI) as the terminal gate rung. block = DELIVERED requires them green; advisory = report only; off = disabled.
|
|
125
|
+
|
|
126
|
+
**Recommendation:** Leave on block — it is the only gate that proves the artifact works for a real user, not just that tests pass.
|
|
127
|
+
|
|
128
|
+
### `UAP_USER_VALIDATION`
|
|
129
|
+
|
|
130
|
+
| | |
|
|
131
|
+
|---|---|
|
|
132
|
+
| **Where** | shell env |
|
|
133
|
+
| **Type** | boolean |
|
|
134
|
+
| **Default** | `true` |
|
|
135
|
+
|
|
136
|
+
Runtime downgrade for the user-validation gate: `0` demotes block to advisory for this run only. Persisting `0` is blocked by the self-protect enforcer.
|
|
137
|
+
|
|
138
|
+
**Recommendation:** Leave unset. Use inline `UAP_USER_VALIDATION=0 uap deliver ...` only to unblock a run where the gate itself misfires.
|
|
139
|
+
|
|
103
140
|
### `UAP_VERIFY_ON_STOP`
|
|
104
141
|
|
|
105
142
|
| | |
|
|
@@ -112,6 +149,78 @@ Master switch for the runtime execution gate in the Stop hook. `0` bypasses it.
|
|
|
112
149
|
|
|
113
150
|
**Recommendation:** Leave on (default). Set `0` only to unblock a session where the runtime gate misfires.
|
|
114
151
|
|
|
152
|
+
### `fidelity.mode`
|
|
153
|
+
|
|
154
|
+
| | |
|
|
155
|
+
|---|---|
|
|
156
|
+
| **Where** | `.uap.json` |
|
|
157
|
+
| **Type** | enum (standard \| max) |
|
|
158
|
+
| **Default** | `standard` |
|
|
159
|
+
|
|
160
|
+
Maximum-fidelity mode. `max` flips every verification default to its strongest: raised verifier floor (runtime+integration), acceptance judge required, blocking vision review, and a fail-CLOSED visual gate — a delivery is accepted only when it builds, runs, looks right, and matches the spec. (`UAP_FIDELITY` overrides at runtime.)
|
|
161
|
+
|
|
162
|
+
**Recommendation:** `max` when correctness matters more than speed (UI work, releases, hands-free autonomy). `standard` for fast exploratory iteration.
|
|
163
|
+
|
|
164
|
+
### `UAP_FIDELITY`
|
|
165
|
+
|
|
166
|
+
| | |
|
|
167
|
+
|---|---|
|
|
168
|
+
| **Where** | shell env |
|
|
169
|
+
| **Type** | enum (standard \| max) |
|
|
170
|
+
| **Default** | `standard` |
|
|
171
|
+
|
|
172
|
+
Runtime override of `fidelity.mode`, read from the shell env by verify/deliver and the Python enforcers. Takes precedence over the config value.
|
|
173
|
+
|
|
174
|
+
**Recommendation:** Set inline (`UAP_FIDELITY=max <cmd>`) to force max fidelity for one command without editing config.
|
|
175
|
+
|
|
176
|
+
### `fidelity.visionMinScore`
|
|
177
|
+
|
|
178
|
+
| | |
|
|
179
|
+
|---|---|
|
|
180
|
+
| **Where** | `.uap.json` |
|
|
181
|
+
| **Type** | number |
|
|
182
|
+
| **Default** | `6` |
|
|
183
|
+
|
|
184
|
+
Minimum aesthetic score (0–10) the vision judge must give a rendered UI before it passes under `max` fidelity.
|
|
185
|
+
|
|
186
|
+
**Recommendation:** 6 is a reasonable "looks like a real, polished app" bar. Raise toward 8 for design-critical surfaces; lower to 4 to only catch broken/blank UIs.
|
|
187
|
+
|
|
188
|
+
### `fidelity.visualBaselines`
|
|
189
|
+
|
|
190
|
+
| | |
|
|
191
|
+
|---|---|
|
|
192
|
+
| **Where** | `.uap.json` |
|
|
193
|
+
| **Type** | boolean |
|
|
194
|
+
| **Default** | `true` |
|
|
195
|
+
|
|
196
|
+
Keep approved UI screenshots as regression baselines under `.uap/visual/baseline/` and block on visual drift beyond threshold on later runs.
|
|
197
|
+
|
|
198
|
+
**Recommendation:** Leave on so accepted UIs are pinned against regressions. Disable for throwaway prototypes where every render legitimately differs.
|
|
199
|
+
|
|
200
|
+
### `UAP_VISION_ENDPOINT`
|
|
201
|
+
|
|
202
|
+
| | |
|
|
203
|
+
|---|---|
|
|
204
|
+
| **Where** | `.uap/proxy.env` |
|
|
205
|
+
| **Type** | string |
|
|
206
|
+
| **Default** | `` |
|
|
207
|
+
|
|
208
|
+
Base URL of an OpenAI-compatible, image_url-capable endpoint used for aesthetic screenshot review (e.g. http://127.0.0.1:8080/v1). Defaults to the local model when set by setup.
|
|
209
|
+
|
|
210
|
+
**Recommendation:** Point at your local vision-capable model so aesthetic review runs offline with no per-image cost.
|
|
211
|
+
|
|
212
|
+
### `UAP_VISION_MODEL`
|
|
213
|
+
|
|
214
|
+
| | |
|
|
215
|
+
|---|---|
|
|
216
|
+
| **Where** | `.uap/proxy.env` |
|
|
217
|
+
| **Type** | string |
|
|
218
|
+
| **Default** | `` |
|
|
219
|
+
|
|
220
|
+
Model id sent to the vision endpoint for aesthetic review (e.g. qwen36-35b-a3b-iq4xs).
|
|
221
|
+
|
|
222
|
+
**Recommendation:** Set by `uap setup` to your local vision model. Required for blocking vision review under `max` fidelity.
|
|
223
|
+
|
|
115
224
|
## Model routing
|
|
116
225
|
|
|
117
226
|
<a id="routing"></a>Which model handles planning, execution, and review.
|
|
@@ -482,6 +591,58 @@ Hard-blocks UI edits that hardcode off-token colors or off-scale spacing.
|
|
|
482
591
|
|
|
483
592
|
**Recommendation:** Enable once your DESIGN.md tokens are stable — it keeps the UI on-system automatically.
|
|
484
593
|
|
|
594
|
+
## Engineering principles
|
|
595
|
+
|
|
596
|
+
<a id="principles"></a>How code should be written, and the backward-compatibility stance.
|
|
597
|
+
|
|
598
|
+
### `principles.enabled`
|
|
599
|
+
|
|
600
|
+
| | |
|
|
601
|
+
|---|---|
|
|
602
|
+
| **Where** | `.uap.json` |
|
|
603
|
+
| **Type** | boolean |
|
|
604
|
+
| **Default** | `true` |
|
|
605
|
+
|
|
606
|
+
Applies the engineering principles (simplest sufficient implementation, reuse over reinvention, no stopgaps, prior art first) to generated code.
|
|
607
|
+
|
|
608
|
+
**Recommendation:** Leave on. Turn it off only if your project has its own conflicting house style.
|
|
609
|
+
|
|
610
|
+
### `principles.compat`
|
|
611
|
+
|
|
612
|
+
| | |
|
|
613
|
+
|---|---|
|
|
614
|
+
| **Where** | `.uap.json` |
|
|
615
|
+
| **Type** | enum (ask \| preserve \| remove) |
|
|
616
|
+
| **Default** | `ask` |
|
|
617
|
+
|
|
618
|
+
Backward-compatibility stance. `remove` deletes obsolete paths outright; `preserve` keeps them working and migrates callers; `ask` (default) prompts once per session instead of guessing.
|
|
619
|
+
|
|
620
|
+
**Recommendation:** Leave on `ask` unless the project's answer is settled. `remove` is for side projects — on anything published it tells the agent to delete migration paths.
|
|
621
|
+
|
|
622
|
+
### `principles.maturity`
|
|
623
|
+
|
|
624
|
+
| | |
|
|
625
|
+
|---|---|
|
|
626
|
+
| **Where** | `.uap.json` |
|
|
627
|
+
| **Type** | enum (ask \| greenfield \| production) |
|
|
628
|
+
| **Default** | `ask` |
|
|
629
|
+
|
|
630
|
+
What breaking a caller costs. `production` adds caveats about existing callers and dependency cost; `greenfield` states the rules absolutely.
|
|
631
|
+
|
|
632
|
+
**Recommendation:** Set `production` for anything with real users; `greenfield` for a fresh side project.
|
|
633
|
+
|
|
634
|
+
### `principles.injectDeliver`
|
|
635
|
+
|
|
636
|
+
| | |
|
|
637
|
+
|---|---|
|
|
638
|
+
| **Where** | `.uap.json` |
|
|
639
|
+
| **Type** | boolean |
|
|
640
|
+
| **Default** | `true` |
|
|
641
|
+
|
|
642
|
+
Injects the compact principles block into deliver prompts, so generated code follows them rather than only the agent preamble.
|
|
643
|
+
|
|
644
|
+
**Recommendation:** Leave on. Disable only if you are tight on prompt budget with a small-context model.
|
|
645
|
+
|
|
485
646
|
## Worktree workflow
|
|
486
647
|
|
|
487
648
|
<a id="worktree"></a>Branch-per-feature isolation and auto-cleanup.
|
|
@@ -651,6 +812,66 @@ Dashboard data refresh interval in milliseconds (floor 250).
|
|
|
651
812
|
|
|
652
813
|
**Recommendation:** 2000 is fine; lower for a more live feel at higher CPU cost.
|
|
653
814
|
|
|
815
|
+
### `proxy.dashboard`
|
|
816
|
+
|
|
817
|
+
| | |
|
|
818
|
+
|---|---|
|
|
819
|
+
| **Where** | `.uap.json` |
|
|
820
|
+
| **Type** | boolean |
|
|
821
|
+
| **Default** | `true` |
|
|
822
|
+
|
|
823
|
+
Ride-along dashboard: `uap proxy ensure|start` also starts (or adopts) `uap dashboard serve`, and release/stop tears it down under the same ownership rules — so a session gets monitoring without running a second command. Only takes effect where the proxy itself runs (hooks gate on proxy.autostart).
|
|
824
|
+
|
|
825
|
+
**Recommendation:** Leave on. Turn off if you prefer to run `uap dash serve` yourself, or the port is spoken for.
|
|
826
|
+
|
|
827
|
+
### `UAP_PROXY_DASHBOARD`
|
|
828
|
+
|
|
829
|
+
| | |
|
|
830
|
+
|---|---|
|
|
831
|
+
| **Where** | shell env |
|
|
832
|
+
| **Type** | boolean |
|
|
833
|
+
| **Default** | `true` |
|
|
834
|
+
|
|
835
|
+
Force the ride-along dashboard on (1/on/true) or off (0/off/false). Wins over .uap.json proxy.dashboard.
|
|
836
|
+
|
|
837
|
+
**Recommendation:** Use for one-off overrides; prefer `uap proxy dashboard on|off` for a durable project setting.
|
|
838
|
+
|
|
839
|
+
### `UAP_DASH_PORT`
|
|
840
|
+
|
|
841
|
+
| | |
|
|
842
|
+
|---|---|
|
|
843
|
+
| **Where** | shell env |
|
|
844
|
+
| **Type** | number |
|
|
845
|
+
| **Default** | `3847` |
|
|
846
|
+
|
|
847
|
+
Port the ride-along dashboard binds and is probed on (1-65535). Does not change `uap dash serve --port`.
|
|
848
|
+
|
|
849
|
+
**Recommendation:** Change only on a port clash; keep it consistent across sessions so adoption works.
|
|
850
|
+
|
|
851
|
+
### `UAP_DASH_HOST`
|
|
852
|
+
|
|
853
|
+
| | |
|
|
854
|
+
|---|---|
|
|
855
|
+
| **Where** | shell env |
|
|
856
|
+
| **Type** | string |
|
|
857
|
+
| **Default** | `localhost` |
|
|
858
|
+
|
|
859
|
+
Interface the ride-along dashboard binds. `0.0.0.0` exposes it beyond this machine.
|
|
860
|
+
|
|
861
|
+
**Recommendation:** Keep localhost. Bind wider only behind a trusted network, and set UAP_DASHBOARD_TOKEN when you do.
|
|
862
|
+
|
|
863
|
+
### `UAP_DASH_HEALTH_WAIT_MS`
|
|
864
|
+
|
|
865
|
+
| | |
|
|
866
|
+
|---|---|
|
|
867
|
+
| **Where** | shell env |
|
|
868
|
+
| **Type** | number |
|
|
869
|
+
| **Default** | `10000` |
|
|
870
|
+
|
|
871
|
+
How long `uap proxy ensure` waits for the ride-along dashboard to serve before giving up.
|
|
872
|
+
|
|
873
|
+
**Recommendation:** Lower it if session start feels slow on a cold dashboard; the proxy is unaffected either way.
|
|
874
|
+
|
|
654
875
|
## Token & time optimization
|
|
655
876
|
|
|
656
877
|
<a id="optimization"></a>Context budgets, caching, batching, and parallelism.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@miller-tech/uap",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.184.1",
|
|
4
4
|
"description": "Autonomous AI agent memory system with CLAUDE.md protocol enforcement",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"start": "node dist/bin/cli.js",
|
|
22
22
|
"test": "vitest",
|
|
23
23
|
"test:ci": "vitest run",
|
|
24
|
-
"test:enforcers": "UAP_PROXY_ENV_AUTOLOAD=0 python3 -m unittest tools.agents.tests.test_enforcement_self_protect tools.agents.tests.test_schema_diff_gate tools.agents.tests.test_stream_telemetry tools.agents.tests.test_project_telemetry_events tools.agents.tests.test_workdir_scope_enforcer tools.agents.tests.test_path_containment tools.agents.tests.test_path_normalizer_hardened tools.agents.tests.test_sandbox_tool_strip tools.agents.tests.test_proxy_env_loader tools.agents.tests.test_stream_required_tool tools.agents.tests.test_tool_call_wrapper_profiles tools.agents.tests.test_tool_convert_cache tools.agents.tests.test_doubling_break tools.agents.tests.test_error_loop_ignores_correctives tools.agents.tests.test_attractor_detection tools.agents.tests.test_client_disconnect tools.agents.tests.test_confidence_escalation tools.agents.tests.test_coordination_ban tools.agents.tests.test_coordination_early_ban tools.agents.tests.test_cycle_break_exploration tools.agents.tests.test_deferral_break tools.agents.tests.test_deliver_autoroute tools.agents.tests.test_delivery_enforcement_all_langs tools.agents.tests.test_delivery_enforcement_exemptions tools.agents.tests.test_delivery_enforcement_filepath tools.agents.tests.test_delivery_enforcement_web_and_bash tools.agents.tests.test_disconnect_watcher tools.agents.tests.test_empty_maxtokens_recovery tools.agents.tests.test_empty_tool_loop_break tools.agents.tests.test_enforcer_escape_hatches tools.agents.tests.test_error_loop_break tools.agents.tests.test_finalize_suppression tools.agents.tests.test_malformed_unclosed_think tools.agents.tests.test_mandate_beats_recon tools.agents.tests.test_mandate_deliver tools.agents.tests.test_overflow_truncate_count_tokens tools.agents.tests.test_passthrough_oauth tools.agents.tests.test_project_telemetry tools.agents.tests.test_proxy_auth_headers tools.agents.tests.test_prune_preserve_force_write tools.agents.tests.test_recon_deliver_gate tools.agents.tests.test_session_admission tools.agents.tests.test_stream_heartbeat tools.agents.tests.test_stuck_break_reattach tools.agents.tests.test_turn_count_breaker_periodic tools.agents.tests.test_upstream_chokepoint tools.agents.tests.test_vision_passthrough tools.agents.tests.test_worktree_required tools.agents.tests.test_enforcer_suite_coverage tools.agents.tests.test_validate_plan_gate tools.agents.tests.test_validate_plan_inside_project tools.agents.tests.test_anthropic_proxy_streaming tools.agents.tests.test_delivery_enforcement_worktree tools.agents.tests.test_output_token_ceilings tools.agents.tests.test_tool_narrowing_core",
|
|
24
|
+
"test:enforcers": "UAP_PROXY_ENV_AUTOLOAD=0 python3 -m unittest tools.agents.tests.test_enforcement_self_protect tools.agents.tests.test_schema_diff_gate tools.agents.tests.test_stream_telemetry tools.agents.tests.test_project_telemetry_events tools.agents.tests.test_workdir_scope_enforcer tools.agents.tests.test_expert_review_pr_scope tools.agents.tests.test_path_containment tools.agents.tests.test_path_normalizer_hardened tools.agents.tests.test_sandbox_tool_strip tools.agents.tests.test_proxy_env_loader tools.agents.tests.test_stream_required_tool tools.agents.tests.test_tool_call_wrapper_profiles tools.agents.tests.test_tool_convert_cache tools.agents.tests.test_doubling_break tools.agents.tests.test_error_loop_ignores_correctives tools.agents.tests.test_attractor_detection tools.agents.tests.test_client_disconnect tools.agents.tests.test_confidence_escalation tools.agents.tests.test_coordination_ban tools.agents.tests.test_coordination_early_ban tools.agents.tests.test_cycle_break_exploration tools.agents.tests.test_deferral_break tools.agents.tests.test_deliver_autoroute tools.agents.tests.test_delivery_enforcement_all_langs tools.agents.tests.test_delivery_enforcement_exemptions tools.agents.tests.test_delivery_enforcement_filepath tools.agents.tests.test_delivery_enforcement_web_and_bash tools.agents.tests.test_disconnect_watcher tools.agents.tests.test_empty_maxtokens_recovery tools.agents.tests.test_empty_tool_loop_break tools.agents.tests.test_enforcer_escape_hatches tools.agents.tests.test_error_loop_break tools.agents.tests.test_finalize_suppression tools.agents.tests.test_malformed_unclosed_think tools.agents.tests.test_mandate_beats_recon tools.agents.tests.test_mandate_deliver tools.agents.tests.test_overflow_truncate_count_tokens tools.agents.tests.test_passthrough_oauth tools.agents.tests.test_project_telemetry tools.agents.tests.test_proxy_auth_headers tools.agents.tests.test_prune_preserve_force_write tools.agents.tests.test_recon_deliver_gate tools.agents.tests.test_session_admission tools.agents.tests.test_stream_heartbeat tools.agents.tests.test_stuck_break_reattach tools.agents.tests.test_turn_count_breaker_periodic tools.agents.tests.test_upstream_chokepoint tools.agents.tests.test_vision_passthrough tools.agents.tests.test_worktree_required tools.agents.tests.test_enforcer_suite_coverage tools.agents.tests.test_validate_plan_gate tools.agents.tests.test_validate_plan_inside_project tools.agents.tests.test_anthropic_proxy_streaming tools.agents.tests.test_delivery_enforcement_worktree tools.agents.tests.test_output_token_ceilings tools.agents.tests.test_tool_narrowing_core",
|
|
25
25
|
"test:coverage": "vitest --coverage",
|
|
26
26
|
"bench": "vitest --config vitest.bench.config.ts",
|
|
27
27
|
"lint": "eslint src --ext .ts",
|
|
Binary file
|
|
@@ -8,12 +8,41 @@ import time
|
|
|
8
8
|
from pathlib import Path
|
|
9
9
|
|
|
10
10
|
sys.path.insert(0, str(Path(__file__).parent))
|
|
11
|
-
from _common import arg_str, emit, parse_cli # noqa: E402
|
|
11
|
+
from _common import arg_str, emit, parse_cli, repo_root # noqa: E402
|
|
12
12
|
|
|
13
13
|
PLAN_OPS = {"ExitPlanMode", "Plan", "TodoWrite"}
|
|
14
14
|
PLAN_WORD_RE = re.compile(r"(?<![-\w/])(plan the|design the|architect the|propose a plan|spec the)", re.I)
|
|
15
15
|
READ_LOG = Path(os.environ.get("UAP_STATE_DIR", ".uap")) / "read_log.state"
|
|
16
16
|
RECENT_SEC = 1800
|
|
17
|
+
# The PostToolUse hook that writes READ_LOG. This gate accepts evidence that
|
|
18
|
+
# ONLY that hook produces, so without it the evidence can never appear.
|
|
19
|
+
#
|
|
20
|
+
# Checked per platform, not just under .claude/: copyHookScripts() drops the
|
|
21
|
+
# script into EVERY platform's hook dir, but each platform needs its own
|
|
22
|
+
# PostToolUse wiring. A hardcoded .claude/ probe would report "writer present"
|
|
23
|
+
# for a Factory or Cursor session whose settings never invoke it — enforcing
|
|
24
|
+
# strictly against evidence that platform cannot produce, which is the same
|
|
25
|
+
# permanent block this fail-open exists to prevent.
|
|
26
|
+
WRITER_HOOK_DIRS = (".claude", ".factory", ".cursor", ".codex", ".forge", ".opencode")
|
|
27
|
+
WRITER_HOOK_NAME = "post-tool-use-read.sh"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def writer_installed() -> bool:
|
|
31
|
+
"""True when the hook that populates READ_LOG is installed for this platform.
|
|
32
|
+
|
|
33
|
+
Load-bearing. For a long time nothing wrote read_log.state at all: the
|
|
34
|
+
matcher was never added to settings.json, the last entries aged past
|
|
35
|
+
RECENT_SEC, and this gate then refused every ExitPlanMode with a remedy
|
|
36
|
+
("read the codebase first") that no amount of reading could clear. A gate
|
|
37
|
+
whose writer is missing silently escalates from advisory to a wall, so when
|
|
38
|
+
the writer is absent we degrade to advisory instead of bricking planning.
|
|
39
|
+
"""
|
|
40
|
+
roots = (repo_root(), Path.cwd())
|
|
41
|
+
for root in roots:
|
|
42
|
+
for hook_dir in WRITER_HOOK_DIRS:
|
|
43
|
+
if (root / hook_dir / "hooks" / WRITER_HOOK_NAME).exists():
|
|
44
|
+
return True
|
|
45
|
+
return False
|
|
17
46
|
|
|
18
47
|
|
|
19
48
|
def recent_reads() -> set[str]:
|
|
@@ -41,6 +70,9 @@ def main() -> None:
|
|
|
41
70
|
if reads:
|
|
42
71
|
emit(True, f"{len(reads)} recent codebase reads on record")
|
|
43
72
|
|
|
73
|
+
if not writer_installed():
|
|
74
|
+
emit(True, "read-log writer hook not installed — gate advisory (run `uap hooks install`)")
|
|
75
|
+
|
|
44
76
|
emit(
|
|
45
77
|
False,
|
|
46
78
|
"codebase-read-before-plan: no Read/Grep/Glob within the last 30 min. "
|
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
"""expert-review-required enforcer: a parallel expert review must precede ship.
|
|
3
3
|
|
|
4
4
|
Blocks ship actions (git commit / git push / gh pr create / merge / pr-ready /
|
|
5
|
-
signoff) unless a review artifact exists for the
|
|
6
|
-
|
|
5
|
+
signoff) unless a review artifact exists for the branch being shipped AND covers
|
|
6
|
+
its HEAD. For `gh pr merge <N>` the branch being shipped is the PR's head
|
|
7
|
+
branch, resolved via gh — not whatever branch the invoking shell is on. This makes the `parallel-expert-review` skill's "REQUIRED by
|
|
7
8
|
policy" claim real rather than advisory.
|
|
8
9
|
|
|
9
10
|
Review artifact: .uap/reviews/<branch-slug>.json, written by the
|
|
@@ -20,6 +21,7 @@ from __future__ import annotations
|
|
|
20
21
|
import json
|
|
21
22
|
import os
|
|
22
23
|
import re
|
|
24
|
+
import shlex
|
|
23
25
|
import sys
|
|
24
26
|
from pathlib import Path
|
|
25
27
|
|
|
@@ -37,6 +39,19 @@ SHIP_PATTERNS = (
|
|
|
37
39
|
re.compile(r"\b(pr[-_ ]?ready|sign[-_ ]?off|ready[-_ ]for[-_ ]review)\b", re.I),
|
|
38
40
|
)
|
|
39
41
|
|
|
42
|
+
# A ship action that NAMES a pull request. The review that matters is the one
|
|
43
|
+
# for that PR's head branch, which is usually not the branch the shell is on.
|
|
44
|
+
PR_SHIP_VERBS = ("merge", "ready")
|
|
45
|
+
|
|
46
|
+
# Flags on those verbs that consume the NEXT token as their value. Without this,
|
|
47
|
+
# `gh pr merge -b 1 900` reads "1" as the PR — so the review for PR 1 authorises
|
|
48
|
+
# shipping PR 900. `gh pr merge --body Merging 123` misfires the same way by
|
|
49
|
+
# accident, which is the more likely path to it happening.
|
|
50
|
+
PR_VALUE_FLAGS = frozenset({
|
|
51
|
+
"-b", "--body", "-F", "--body-file", "-t", "--subject",
|
|
52
|
+
"-R", "--repo", "--match-head-commit", "-c", "--comment",
|
|
53
|
+
})
|
|
54
|
+
|
|
40
55
|
# Risk-scope: a parallel expert review is required only for *substantive* diffs.
|
|
41
56
|
# A diff that touches ONLY low-risk surfaces (frontend/styles, docs, config,
|
|
42
57
|
# tests, assets) ships freely — trivial/frontend PRs aren't gated. High-risk
|
|
@@ -115,6 +130,64 @@ def slug_for(branch: str) -> str:
|
|
|
115
130
|
return branch.replace("%", "%25").replace("/", "%2F")
|
|
116
131
|
|
|
117
132
|
|
|
133
|
+
def pr_reference(cmd: str) -> str | None:
|
|
134
|
+
"""The pull request a `gh pr merge|ready` command names, or None.
|
|
135
|
+
|
|
136
|
+
Tokenized rather than pattern-matched on "digits right after the verb":
|
|
137
|
+
flags may come first (`gh pr merge --squash 645`), and gh accepts a number,
|
|
138
|
+
a URL, or a branch name interchangeably. The narrow form missed all of
|
|
139
|
+
those and fell back to the local branch — silently reinstating the very bug
|
|
140
|
+
this resolution exists to fix.
|
|
141
|
+
|
|
142
|
+
A bare `gh pr merge` (the current branch's PR) returns None, which is
|
|
143
|
+
correct: the local branch IS the right thing to check then.
|
|
144
|
+
"""
|
|
145
|
+
try:
|
|
146
|
+
tokens = shlex.split(cmd, comments=True)
|
|
147
|
+
except ValueError:
|
|
148
|
+
return None
|
|
149
|
+
for i in range(len(tokens) - 2):
|
|
150
|
+
if (
|
|
151
|
+
os.path.basename(tokens[i]) == "gh"
|
|
152
|
+
and tokens[i + 1] == "pr"
|
|
153
|
+
and tokens[i + 2] in PR_SHIP_VERBS
|
|
154
|
+
):
|
|
155
|
+
skip_value = False
|
|
156
|
+
for tok in tokens[i + 3:]:
|
|
157
|
+
if skip_value:
|
|
158
|
+
skip_value = False
|
|
159
|
+
continue
|
|
160
|
+
if tok.startswith("-"):
|
|
161
|
+
# `--body=x` carries its value inline; `--body x` does not,
|
|
162
|
+
# and that value can look exactly like a PR reference.
|
|
163
|
+
if tok in PR_VALUE_FLAGS:
|
|
164
|
+
skip_value = True
|
|
165
|
+
continue
|
|
166
|
+
return tok
|
|
167
|
+
return None
|
|
168
|
+
return None
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def pr_target(root: Path, ref: str) -> tuple[str | None, str | None]:
|
|
172
|
+
"""(head branch, head sha) of the PR being shipped, resolved via `gh`.
|
|
173
|
+
|
|
174
|
+
Returns (None, None) on any failure — no gh, no network, no auth, unknown
|
|
175
|
+
PR — so the caller falls back to the local branch and this stays fail-open.
|
|
176
|
+
`run` already bounds the call at 5s, and PR_SHIP_RE only matches an explicit
|
|
177
|
+
`gh pr merge/ready <N>`, so the cost is paid on ship actions, not per Bash.
|
|
178
|
+
"""
|
|
179
|
+
rc, out, _ = run(
|
|
180
|
+
["gh", "pr", "view", ref, "--json", "headRefName,headRefOid"], cwd=root
|
|
181
|
+
)
|
|
182
|
+
if rc != 0 or not out.strip():
|
|
183
|
+
return None, None
|
|
184
|
+
try:
|
|
185
|
+
data = json.loads(out)
|
|
186
|
+
except Exception: # noqa: BLE001
|
|
187
|
+
return None, None
|
|
188
|
+
return (data.get("headRefName") or None), (data.get("headRefOid") or None)
|
|
189
|
+
|
|
190
|
+
|
|
118
191
|
def head_sha(root: Path) -> str | None:
|
|
119
192
|
rc, out, _ = run(["git", "rev-parse", "HEAD"], cwd=root)
|
|
120
193
|
return out.strip() if rc == 0 and out.strip() else None
|
|
@@ -158,7 +231,16 @@ def main() -> None:
|
|
|
158
231
|
# to MAIN_ROOT by the gate, so it always read the main checkout's branch and
|
|
159
232
|
# demanded a review for the wrong branch on every worktree commit/push.
|
|
160
233
|
root = worktree_root()
|
|
161
|
-
|
|
234
|
+
|
|
235
|
+
# `gh pr merge 645` ships PR 645's branch. Reading the LOCAL branch here
|
|
236
|
+
# meant a merge run from the main checkout looked for .uap/reviews/master
|
|
237
|
+
# .json — an artifact for a branch that is not being shipped — and refused
|
|
238
|
+
# a PR whose own branch was reviewed and approved. Resolve the PR's head
|
|
239
|
+
# instead; fall back to the local branch when gh cannot answer.
|
|
240
|
+
pr_ref = pr_reference(cmd)
|
|
241
|
+
pr_branch, pr_sha = pr_target(root, pr_ref) if pr_ref else (None, None)
|
|
242
|
+
|
|
243
|
+
branch = pr_branch or current_branch(root)
|
|
162
244
|
if branch is None:
|
|
163
245
|
emit(True, "branch not resolvable (detached/non-git) — fail-open")
|
|
164
246
|
slug = slug_for(branch)
|
|
@@ -172,7 +254,10 @@ def main() -> None:
|
|
|
172
254
|
# migrations, or policy code — the change ships without a parallel review.
|
|
173
255
|
# When the base diff is not resolvable (None) we do NOT skip: we can't prove
|
|
174
256
|
# the change is low-risk, so the review requirement below still applies.
|
|
175
|
-
|
|
257
|
+
# Skipped for a PR ship: this diffs the LOCAL working tree, which on a
|
|
258
|
+
# `gh pr merge` from the main checkout is not the PR's contents at all —
|
|
259
|
+
# it would grade the wrong change as low-risk.
|
|
260
|
+
changed = None if pr_branch else _changed_files(root)
|
|
176
261
|
if changed and all(_is_low_risk(f) for f in changed):
|
|
177
262
|
emit(
|
|
178
263
|
True,
|
|
@@ -191,7 +276,7 @@ def main() -> None:
|
|
|
191
276
|
"(no longer honoured inline), or a waiver file.",
|
|
192
277
|
)
|
|
193
278
|
|
|
194
|
-
head = head_sha(root)
|
|
279
|
+
head = pr_sha or head_sha(root)
|
|
195
280
|
try:
|
|
196
281
|
data = json.loads(review.read_text())
|
|
197
282
|
except Exception: # noqa: BLE001
|
|
@@ -8,16 +8,34 @@ import time
|
|
|
8
8
|
from pathlib import Path
|
|
9
9
|
|
|
10
10
|
sys.path.insert(0, str(Path(__file__).parent))
|
|
11
|
-
from _common import arg_str, emit, parse_cli, repo_root # noqa: E402
|
|
11
|
+
from _common import arg_str, emit, parse_cli, repo_root, worktree_root # noqa: E402
|
|
12
12
|
|
|
13
13
|
PLAN_OPS = {"ExitPlanMode", "Plan", "TodoWrite", "plan", "design"}
|
|
14
14
|
# Only match standalone words, not compounds like 'validate-plan-on-change'
|
|
15
15
|
PLAN_WORD_RE = re.compile(r"(?<![-\w/])(plan the|design the|architect the|propose a plan|roadmap for)", re.I)
|
|
16
16
|
RECENT_SEC = 300
|
|
17
|
+
DB_REL = Path("agents") / "data" / "memory" / "short_term.db"
|
|
17
18
|
|
|
18
19
|
|
|
19
|
-
def
|
|
20
|
-
|
|
20
|
+
def candidate_dbs() -> list[Path]:
|
|
21
|
+
"""Every short-term DB that could hold the evidence, worktree first.
|
|
22
|
+
|
|
23
|
+
`uap memory query` writes to the DB under the CWD, which inside a worktree
|
|
24
|
+
is the WORKTREE's DB — but this gate used to read only repo_root()'s. An
|
|
25
|
+
agent doing the required query while working in a worktree (which the
|
|
26
|
+
worktree policy mandates) therefore produced evidence the gate never saw,
|
|
27
|
+
and the remedy could not clear it. Same repo_root()-vs-worktree_root() bug
|
|
28
|
+
already fixed in expert-review-required and local-build-before-push.
|
|
29
|
+
"""
|
|
30
|
+
seen: list[Path] = []
|
|
31
|
+
for root in (worktree_root(), repo_root()):
|
|
32
|
+
db = root / DB_REL
|
|
33
|
+
if db not in seen:
|
|
34
|
+
seen.append(db)
|
|
35
|
+
return seen
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def recent_memory_query(db: Path) -> bool:
|
|
21
39
|
if not db.exists():
|
|
22
40
|
return False
|
|
23
41
|
try:
|
|
@@ -48,8 +66,9 @@ def main() -> None:
|
|
|
48
66
|
if op not in PLAN_OPS and not PLAN_WORD_RE.search(blob):
|
|
49
67
|
emit(True, "not a plan operation")
|
|
50
68
|
|
|
51
|
-
|
|
52
|
-
|
|
69
|
+
for db in candidate_dbs():
|
|
70
|
+
if recent_memory_query(db):
|
|
71
|
+
emit(True, "recent uap memory query on record")
|
|
53
72
|
|
|
54
73
|
emit(
|
|
55
74
|
False,
|