@miller-tech/uap 1.158.0 → 1.160.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/cli/fidelity.d.ts.map +1 -1
  3. package/dist/cli/fidelity.js +15 -0
  4. package/dist/cli/fidelity.js.map +1 -1
  5. package/dist/cli/policy-select.d.ts +10 -0
  6. package/dist/cli/policy-select.d.ts.map +1 -1
  7. package/dist/cli/policy-select.js +30 -0
  8. package/dist/cli/policy-select.js.map +1 -1
  9. package/dist/cli/policy.d.ts.map +1 -1
  10. package/dist/cli/policy.js +20 -1
  11. package/dist/cli/policy.js.map +1 -1
  12. package/dist/cli/setup.d.ts.map +1 -1
  13. package/dist/cli/setup.js +20 -0
  14. package/dist/cli/setup.js.map +1 -1
  15. package/dist/delivery/convergence-loop.d.ts.map +1 -1
  16. package/dist/delivery/convergence-loop.js +11 -1
  17. package/dist/delivery/convergence-loop.js.map +1 -1
  18. package/dist/delivery/epic-controller.d.ts.map +1 -1
  19. package/dist/delivery/epic-controller.js +25 -0
  20. package/dist/delivery/epic-controller.js.map +1 -1
  21. package/dist/delivery/user-validation.d.ts.map +1 -1
  22. package/dist/delivery/user-validation.js +14 -2
  23. package/dist/delivery/user-validation.js.map +1 -1
  24. package/package.json +2 -2
  25. package/src/policies/enforcers/__pycache__/_common.cpython-312.pyc +0 -0
  26. package/src/policies/enforcers/commitment_reserve.py +341 -0
  27. package/src/policies/enforcers/enforcement_infra_protect.py +33 -0
  28. package/src/policies/schemas/policies/commitment-reserve.md +67 -0
  29. package/src/policies/schemas/policies/proportional-commitment.md +39 -0
  30. package/templates/hooks/__pycache__/deliver_autoroute.cpython-312.pyc +0 -0
  31. package/templates/hooks/deliver_autoroute.py +234 -15
  32. package/tools/agents/scripts/__pycache__/toolcall_path_normalizer.cpython-312.pyc +0 -0
  33. package/tools/agents/scripts/anthropic_proxy.py +158 -5
  34. package/tools/agents/tests/test_deliver_autoroute.py +116 -1
  35. package/tools/agents/tests/test_doubling_break.py +239 -0
@@ -7,11 +7,25 @@ proxy), `kill $(lsof -t -i:8080)` (killed llama-server), and then
7
7
  `python3 -m http.server 8080` (stole the inference port, putting llama-server
8
8
  into a systemd bind-failure crash loop and 529-ing its own session).
9
9
 
10
+ Observed again (2026-07-16, octopus_invaders_v3): a deliver cleanup loop ran
11
+ `pkill -9 -f "uap"` — a BROAD -f pattern that never names llama-server, yet
12
+ matched it anyway because the server's own argv carries the token via
13
+ `--slot-save-path ~/.cache/uap/llama-slots`. That killed llama-server 5x in
14
+ 4 minutes (systemd restart counter 3->6) until the model narrowed the pattern.
15
+ Rule 6 closes that gap: any -f/--full kill whose pattern is a substring of the
16
+ inference stack's argv (uap/llama/qwen/mmproj/nomic/anthropic) is refused.
17
+ Note the stack that a bare interpreter can hit is the PROXY only (python3);
18
+ llama-server and embeddings are native binaries, so `node.*` globs are inert
19
+ against infra and are left to loop-protection, not this enforcer.
20
+
10
21
  Scope (Bash/bash/run_bash commands only):
11
22
  - pkill/killall with a bare-interpreter pattern (matches EVERY python/node).
12
23
  - kill/pkill/killall aimed at llama-server / anthropic_proxy / nomic by name.
13
24
  - kill-by-port or fuser -k on the infra ports (8080 llama, 4000 proxy,
14
25
  8081 embeddings).
26
+ - broad `-f`/`--full` kill whose pattern is a substring of the stack's argv
27
+ (uap/llama/qwen/mmproj/nomic/anthropic) or a glob over the python
28
+ interpreter that runs the proxy.
15
29
  - systemctl stop/restart/kill/disable of the inference services.
16
30
  - Starting a server that BINDS an infra port (http.server 8080 etc.) —
17
31
  this is how the port got stolen even with kills blocked (the model
@@ -54,6 +68,25 @@ RULES = (
54
68
  r"\b(serve|live-server|http-server|vite|webpack-dev-server|next\s+dev|python3?\s+-m\s+flask\s+run)"
55
69
  r"\b[^|;&\n]*(--port[= ]|-p[= ]?|:)" + INFRA_PORTS + r"\b"
56
70
  ),
71
+ # 6) Broad kill by FULL-cmdline match (-f/--full/-e) whose pattern is a
72
+ # substring of the inference stack's own argv. The stack carries these
73
+ # tokens (e.g. --slot-save-path ~/.cache/uap/llama-slots, model 'qwen',
74
+ # 'mmproj', anthropic_proxy). `pkill -9 -f uap` killed llama-server 5x on
75
+ # 2026-07-16 via the 'uap' in its slot path — this is NOT a specific
76
+ # pattern even though it never names the server.
77
+ re.compile(
78
+ r"\b(pkill|kill|killall)\b[^|;&\n]*"
79
+ r"(\s-(?:-full|[A-Za-z0-9]*f)\b|\s-e\b)"
80
+ r"[^|;&\n]*\b(uap|llama|anthropic|nomic|mmproj|qwen[0-9.]*|llama-slots|slots?[_-]?save)\b"
81
+ ),
82
+ # 7) Same, but a GLOB/regex over the python interpreter that runs the proxy
83
+ # (`python3 .../anthropic_proxy.py`): `pkill -f "python.*"` sweeps it even
84
+ # without an explicit infra token. Bare `pkill -f python3` -> rule 1.
85
+ re.compile(
86
+ r"\b(pkill|kill|killall)\b[^|;&\n]*"
87
+ r"(\s-(?:-full|[A-Za-z0-9]*f)\b)"
88
+ r"[^|;&\n]*[\"']?python[0-9.]*[.*\[\]\\]"
89
+ ),
57
90
  )
58
91
 
59
92
  REASON = (
@@ -0,0 +1,67 @@
1
+ # commitment-reserve
2
+
3
+ **Category**: safety
4
+ **Level**: REQUIRED
5
+ **Enforcement Stage**: pre-exec
6
+ **Tags**: safety, reversibility, backup, restraint, never-go-full
7
+
8
+ ## Rule
9
+
10
+ An **all-in move with no way back** is blocked until a reserve exists. Commit
11
+ hard to an approach — but never commit everything ("never go full": the ones
12
+ who hold something back are the ones who can still recover).
13
+
14
+ Matching is segment-anchored and quote-aware: a rule fires only when the
15
+ segment's leading verb is `git`/`rm`, so a `grep`, an `echo`, or a commit
16
+ message that merely *mentions* a destructive pattern never trips the gate.
17
+
18
+ Blocked without a reserve (`Bash`/`run_bash`):
19
+
20
+ - `git reset --hard` — reserve: `git stash` first (an inline
21
+ `git stash && …` in the same command also counts).
22
+ - Forced push (`--force`, `-f`, `+refspec`) — `--force-with-lease` is allowed
23
+ by this gate; if the project's git-safety hook blocks that too, the
24
+ sanctioned fallback is a merge commit, not a bigger hammer.
25
+ - `git clean -f…` / `--force` — reserve: `git stash -u` first. Dry runs
26
+ (`-n` / `--dry-run`) stay allowed.
27
+ - `git checkout`/`restore` wholesale discards — final arg `.`/`*`, including
28
+ `checkout HEAD -- .`, `restore --worktree .`, and bare `checkout -f`.
29
+ - Recursive **and** forced delete (`rm -rf` and flag-order variants) of a
30
+ protected source root (`src`, `test`, `tests`, `lib`, `tools`, `scripts`,
31
+ the policy-definition dir), of the whole tree (`.`, `*`, `/`), or of a
32
+ parent/home escape (`..`, `~`). Scratch/derived dirs stay allowed at any
33
+ depth (`node_modules`, `dist`, `src/__pycache__`, `src/generated`, …) —
34
+ they are reconstructible; source is not.
35
+
36
+ Blocked stub overwrites (`Write`):
37
+
38
+ - Overwriting an existing source file ≥ 4 KiB with content under 20% of its
39
+ size — the signature of a real implementation being gutted into a stub.
40
+ Reserve: back the file up to `.uap-backups/<today>/` first. The backup is
41
+ **verified**: a real regular file (not a symlink) of at least half the
42
+ original's size — an empty or unrelated same-named file does not count.
43
+ Incremental `Edit` operations are never touched.
44
+
45
+ Unlocks:
46
+
47
+ - An inline reserve-**creating** stash (`git stash`, `git stash push`,
48
+ `-u`/`--include-untracked`). Reserve-destroying or inert stash subcommands
49
+ (`drop`, `clear`, `pop`, `list`, `show`, `apply`, `branch`) do **not**
50
+ unlock.
51
+ - `UAP_RESERVE_OK=1` **only as a leading env assignment of the destructive
52
+ segment itself** (set it only after creating the backup/stash). A mention
53
+ elsewhere in the command does not count.
54
+ - A verified same-day `.uap-backups/` backup, for stub overwrites.
55
+
56
+ Operator overrides: `UAP_RESERVE_OK=1` / `UAP_COMMITMENT_RESERVE_OFF=1` in
57
+ the environment; `DELIVER_ACTIVE=1` sessions are exempt (the deliver harness
58
+ keeps its own snapshot reserve).
59
+
60
+ ## Why
61
+
62
+ Every catastrophic agent incident on this project was a full-commitment
63
+ failure: real implementation files deleted into stubs to satisfy gates,
64
+ destructive resets that vaporized uncommitted work, force-pushes with nothing
65
+ held back. The rule is not "never do destructive things" — it is "destructive
66
+ moves are allowed only once something is held in reserve", so the recovery
67
+ path always stays open.
@@ -0,0 +1,39 @@
1
+ # proportional-commitment
2
+
3
+ **Category**: custom
4
+ **Level**: RECOMMENDED
5
+ **Enforcement Stage**: pre-exec
6
+ **Tags**: judgment, proportionality, restraint
7
+
8
+ ## Rule
9
+
10
+ Effort and blast radius must scale with the task. Commit hard to an approach,
11
+ but never commit **everything** -- always hold a reserve of judgment and a way
12
+ back:
13
+
14
+ 1. **Smallest viable intervention first.** Do not rewrite a whole file when an
15
+ edit suffices; do not restructure a module to fix a line; do not regenerate
16
+ what can be patched.
17
+ 2. **No orchestration for small work.** Do not spawn subagents, decompose into
18
+ epics, or fan out parallel workers for a task whose diff is small. The
19
+ overhead must be justified by the work, not by enthusiasm.
20
+ 3. **No destructive reset as a first resort.** `git reset --hard`, `rm -rf`,
21
+ force-push, and wholesale overwrites are last resorts taken only after the
22
+ incremental path has demonstrably failed -- and only with a backup or branch
23
+ point held in reserve.
24
+ 4. **Keep one alternative alive.** Before committing to any expensive path
25
+ (long build, large refactor, multi-agent run), be able to state the fallback
26
+ you will use if it fails. If there is no fallback, the commitment is too big.
27
+ 5. **Stop doubling down.** If the same approach has failed twice, the third
28
+ attempt must be a different approach, not a louder version of the same one.
29
+
30
+ ## Why
31
+
32
+ Total commitment is a trap: an agent that goes all-in on one approach, one
33
+ rewrite, or one orchestration pattern surrenders the judgment needed to step
34
+ back out ("hold something back" -- the Tropic Thunder rule). Every major
35
+ incident in this project's history is a full-commitment failure: thousands of
36
+ identical subagent spawns, real implementation files deleted into stubs,
37
+ monolithic sessions overflowing their context window, gates saturated until
38
+ the remedy for a blocked action was itself blocked. Proportional commitment
39
+ keeps the recovery path open.
@@ -6,10 +6,16 @@ The policy-gate hook pipes a blocked enforcer's JSON output to this helper. When
6
6
  the block carries route == "deliver", the helper:
7
7
  1. Logs the blocked intent to the project's .uap pending-deliver log (so the
8
8
  intent is never lost).
9
- 2. If UAP_DELIVER_AUTOROUTE is on, spawns `uap deliver "<hint>"` detached in
10
- the background (deduped per file so a retrying model does not fan out
9
+ 2. If the intent is REPLAYABLE (plan D1 recorded the blocked edit's exact
10
+ content) and UAP_DELIVER_PENDING_REPLAY is on (default), spawns
11
+ `uap deliver --pending <file>` detached — a DETERMINISTIC replay that
12
+ writes the content to disk (no model) and runs the gates once. This is what
13
+ actually lands the blocked write; without it the recorded intent is never
14
+ applied and the model re-emits it forever (0 files change).
15
+ 3. Else, if UAP_DELIVER_AUTOROUTE is on, spawns `uap deliver "<hint>"` detached
16
+ in the background (deduped per file so a retrying model does not fan out
11
17
  dozens of runs), and annotates the message.
12
- 3. Prints the (possibly annotated) block message on stdout for the hook to
18
+ 4. Prints the (possibly annotated) block message on stdout for the hook to
13
19
  surface to the agent.
14
20
 
15
21
  Autoroute is ON by default (UAP_DELIVER_AUTOROUTE=0 to disable): the blocked
@@ -27,6 +33,30 @@ from pathlib import Path
27
33
  PENDING_LOG = "pending-deliver.jsonl"
28
34
  SEEN_FILE = "autoroute-seen"
29
35
  UAP_DIR = ".uap"
36
+ BASH_TOOLS = {"bash", "run_bash", "shell", "execute_command"}
37
+
38
+ # `cat > FILE << DELIM\n...body...\nDELIM` — the heredoc source-write a model
39
+ # reaches for when its Write tool is gated. Captures path + body so the write is
40
+ # REPLAYABLE (applied deterministically) rather than dead-ending. Overwrite form
41
+ # only; append (`>>`) and non-heredoc writers fall through to the model-spawn
42
+ # autoroute. Tolerates a leading env-prefix / `bash -c '...'` wrapper and a
43
+ # quoted or unquoted path/delimiter.
44
+ _BASH_WRITE_RE = re.compile(
45
+ r"cat\s*>\s*(?P<pq>['\"]?)(?P<path>[^\s'\";|&>]+)(?P=pq)\s*"
46
+ r"<<-?\s*(?P<dq>['\"]?)(?P<delim>\w+)(?P=dq)\s*\n"
47
+ r"(?P<body>.*?)\n(?P=delim)\b",
48
+ re.DOTALL,
49
+ )
50
+
51
+
52
+ def _parse_bash_write(command: str):
53
+ """Recover (path, content) from a heredoc file write, else (None, None)."""
54
+ if not command or "cat" not in command:
55
+ return None, None
56
+ m = _BASH_WRITE_RE.search(command)
57
+ if not m:
58
+ return None, None
59
+ return m.group("path"), m.group("body")
30
60
 
31
61
 
32
62
  def _autoroute_enabled() -> bool:
@@ -41,7 +71,8 @@ def _autoroute_enabled() -> bool:
41
71
  return v in {"1", "on", "true", "yes"}
42
72
 
43
73
 
44
- def decide(out: dict, tool: str, args: dict, autoroute_on: bool, seen_files: set) -> dict:
74
+ def decide(out: dict, tool: str, args: dict, autoroute_on: bool, seen_files: set,
75
+ replay_on: bool = True) -> dict:
45
76
  """Pure decision: what message to show, whether to spawn, and the intent."""
46
77
  reason = out.get("reason", "")
47
78
  route = out.get("route")
@@ -61,9 +92,20 @@ def decide(out: dict, tool: str, args: dict, autoroute_on: bool, seen_files: set
61
92
  args.get("file_path") or args.get("filePath") or args.get("path")
62
93
  or args.get("target") or args.get("filename") or args.get("file") or ""
63
94
  )
95
+ # A bash-routed source write carries its path AND content inside the command
96
+ # (`cat > FILE << EOF`), not as tool args — recover both so the write becomes
97
+ # REPLAYABLE instead of dead-ending. Without this the model's `cat >` rewrites
98
+ # are blocked forever (octopus_invaders_v3, 2026-07-16: 35 min of blocked
99
+ # heredoc rewrites, 0 landed).
100
+ bash_content = None
101
+ if not file_path and tool.lower() in BASH_TOOLS:
102
+ bp, bc = _parse_bash_write(str(args.get("command") or ""))
103
+ if bp and bc is not None:
104
+ file_path = bp
105
+ bash_content = bc
64
106
 
65
107
  if route != "deliver":
66
- return {"message": reason, "route": route, "spawn": False,
108
+ return {"message": reason, "route": route, "spawn": False, "replay": False,
67
109
  "file_path": file_path, "hint": hint, "dedup_key": "", "intent": None}
68
110
 
69
111
  intent = {"ts": int(time.time()), "tool": tool, "file_path": file_path, "hint": hint}
@@ -85,20 +127,46 @@ def decide(out: dict, tool: str, args: dict, autoroute_on: bool, seen_files: set
85
127
  )
86
128
  if isinstance(v, str)
87
129
  }
130
+ # Recovered heredoc body overwrites the whole file — exactly what the blocked
131
+ # `cat > FILE` would have done — so record it as a content-intent.
132
+ if not edit_intent and bash_content is not None:
133
+ edit_intent = {"content": bash_content}
88
134
  if edit_intent:
89
135
  intent["edit"] = edit_intent
90
- spawn = bool(autoroute_on and hint and dedup_key and dedup_key not in seen_files)
136
+ # A REPLAYABLE intent (plan D1) carries the blocked edit's exact content, so
137
+ # it can be applied to disk DETERMINISTICALLY via `uap deliver --pending`
138
+ # (writeFileSync of the captured content — no model, no blind fan-out). This
139
+ # is what actually LANDS the blocked write. Without it the intent is recorded
140
+ # but never applied: the model re-emits the same write forever, the gate
141
+ # re-blocks it every time, and 0 files change (observed 2026-07-16,
142
+ # octopus_invaders_v3 — every deliver run frozen at phase 0 with an empty
143
+ # project). Prefer replay over the model-spawn autoroute whenever available.
144
+ replayable = bool(
145
+ file_path
146
+ and edit_intent
147
+ and (isinstance(edit_intent.get("content"), str)
148
+ or isinstance(edit_intent.get("old_string"), str))
149
+ )
150
+ # Deduped like a spawn (per change), so a retrying model does not re-run
151
+ # `uap deliver --pending` for an identical edit — the lock only guards
152
+ # CONCURRENT runs, the seen-set guards repeats. `replay_on` lets an operator
153
+ # fall back to the model-spawn autoroute (UAP_DELIVER_PENDING_REPLAY=off).
154
+ unseen = bool(dedup_key and dedup_key not in seen_files)
155
+ replay = bool(replayable and replay_on and unseen)
156
+ spawn = bool(autoroute_on and not replay and hint and unseen)
91
157
  message = reason
92
- if spawn:
158
+ if replay:
159
+ message = reason + " [intent recorded to .uap/pending-deliver.jsonl — auto-applying to disk via deterministic `uap deliver --pending` replay]"
160
+ elif spawn:
93
161
  message = reason + " [auto-routed to `uap deliver` — running in the background]"
94
- elif autoroute_on and dedup_key and dedup_key in seen_files:
95
- message = reason + " [already auto-routed to `uap deliver` for this change — see .uap/autoroute.log / pending-deliver.jsonl]"
162
+ elif dedup_key and dedup_key in seen_files and (replayable or autoroute_on):
163
+ message = reason + " [already auto-routed/applied for this change — see .uap/autoroute.log / pending-deliver.jsonl]"
96
164
  elif file_path:
97
165
  message = reason + (
98
166
  " [intent recorded to .uap/pending-deliver.jsonl — apply it yourself by running"
99
167
  " `uap deliver` with the exact intended change as the instruction]"
100
168
  )
101
- return {"message": message, "route": route, "spawn": spawn,
169
+ return {"message": message, "route": route, "spawn": spawn, "replay": replay,
102
170
  "file_path": file_path, "hint": hint, "dedup_key": dedup_key, "intent": intent}
103
171
 
104
172
 
@@ -115,6 +183,29 @@ def _load_seen(root: Path) -> set:
115
183
 
116
184
  LOCK_FILE = "autoroute.lock"
117
185
  LOG_FILE = "autoroute.log"
186
+ COHERENT_LOCK = "coherent-mission.lock"
187
+
188
+
189
+ def _coherent_enabled() -> bool:
190
+ # PHASE 1 (coherent-mission routing): route the WHOLE mission to ONE agentic
191
+ # `uap deliver --epics` run (contracts -> scaffold -> fill, writes in-band)
192
+ # instead of landing files one at a time via the per-file replay/model-spawn
193
+ # side-channel — which produces syntactically-valid but NON-integrating output
194
+ # (index.html loading 2 of 9 scripts, mismatched module APIs; octopus, 07-16).
195
+ # Default OFF until epic-run phase-0 convergence is hardened; enable with
196
+ # UAP_DELIVER_COHERENT_MISSION=1/on/true/yes.
197
+ v = os.environ.get("UAP_DELIVER_COHERENT_MISSION", "off").lower()
198
+ return v in {"1", "on", "true", "yes"}
199
+
200
+
201
+ def coherent_route(coherent_on: bool, route: str, has_write_intent: bool,
202
+ mission: str, inflight: bool) -> str:
203
+ """Whether to route the whole mission to one coherent epic run. Returns
204
+ 'spawn' (start it), 'wait' (one already in flight — suppress the per-file
205
+ side-channel), or '' (not applicable; fall through to replay/spawn)."""
206
+ if not (coherent_on and route == "deliver" and has_write_intent and mission):
207
+ return ""
208
+ return "wait" if inflight else "spawn"
118
209
 
119
210
 
120
211
  def _pid_alive(pid: int) -> bool:
@@ -176,6 +267,111 @@ def _spawn_deliver(root: Path, hint: str) -> None:
176
267
  pass
177
268
 
178
269
 
270
+ def _replay_enabled() -> bool:
271
+ # Deterministic pending-intent replay is safe — it writes the recorded
272
+ # content to disk and runs the gate ladder once, with no model and no blind
273
+ # background fan-out (the exact hazard that keeps model autoroute OFF by
274
+ # default). ON by default; UAP_DELIVER_PENDING_REPLAY=0/off/false to disable.
275
+ v = os.environ.get("UAP_DELIVER_PENDING_REPLAY", "on").lower()
276
+ return v in {"1", "on", "true", "yes"}
277
+
278
+
279
+ def _spawn_pending_replay(root: Path, file_arg: str) -> None:
280
+ """Spawn `uap deliver --pending <file>` detached: DETERMINISTICALLY replay
281
+ the recorded edit intent(s) for this file to disk (writeFileSync of the
282
+ captured content — no model), then run the required gates once. This is the
283
+ plan-D1 path that actually lands the blocked write. Serialized behind the
284
+ same one-in-flight lock as _spawn_deliver so concurrent blocked writes do
285
+ not pile up runs; the model's retry of any still-unwritten file re-triggers
286
+ a replay once the lock frees. Best-effort; never raises."""
287
+ import subprocess
288
+ if not file_arg or file_arg.startswith("-"):
289
+ return # need a path, and never let one be parsed as a flag
290
+ if not _acquire_slot(root):
291
+ return # a replay/deliver is already converging in this repo
292
+ try:
293
+ log = (root / UAP_DIR / LOG_FILE).open("a")
294
+ except Exception:
295
+ log = subprocess.DEVNULL
296
+ try:
297
+ proc = subprocess.Popen(
298
+ ["uap", "deliver", "--pending", file_arg],
299
+ cwd=str(root),
300
+ stdout=log, stderr=log, stdin=subprocess.DEVNULL,
301
+ start_new_session=True,
302
+ )
303
+ try:
304
+ (root / UAP_DIR / LOCK_FILE).write_text(str(proc.pid))
305
+ except Exception:
306
+ pass
307
+ except Exception:
308
+ pass
309
+
310
+
311
+ def _recover_mission(root: Path) -> str:
312
+ """Best-available mission text for a coherent epic run: the completion
313
+ ledger's `mission`, else the newest deliver run's `instruction`. '' if none
314
+ (then coherent routing is skipped and the per-file paths handle the write)."""
315
+ try:
316
+ p = root / UAP_DIR / "completion-ledger.json"
317
+ if p.exists():
318
+ m = json.loads(p.read_text()).get("mission")
319
+ if isinstance(m, str) and m.strip():
320
+ return m.strip()
321
+ except Exception:
322
+ pass
323
+ try:
324
+ runs = sorted((root / UAP_DIR / "deliver-runs").glob("run-*"),
325
+ key=lambda x: x.stat().st_mtime, reverse=True)
326
+ for r in runs:
327
+ try:
328
+ m = json.loads((r / "state.json").read_text()).get("instruction")
329
+ if isinstance(m, str) and m.strip():
330
+ return m.strip()
331
+ except Exception:
332
+ continue
333
+ except Exception:
334
+ pass
335
+ return ""
336
+
337
+
338
+ def _coherent_inflight(root: Path) -> bool:
339
+ """One coherent epic run per repo; a live lock pid means one is building."""
340
+ lock = root / UAP_DIR / COHERENT_LOCK
341
+ try:
342
+ if lock.exists():
343
+ pid = int(lock.read_text().strip() or "0")
344
+ return bool(pid and _pid_alive(pid))
345
+ except Exception:
346
+ pass
347
+ return False
348
+
349
+
350
+ def _spawn_coherent_epic(root: Path, mission: str) -> None:
351
+ """Spawn ONE `uap deliver --epics -- "<mission>"` (agentic, in-process) that
352
+ builds the whole mission COHERENTLY through the phase flow. Detached, one per
353
+ repo. Best-effort; never raises."""
354
+ import subprocess
355
+ if not mission or mission.startswith("-") or _coherent_inflight(root):
356
+ return
357
+ try:
358
+ log = (root / UAP_DIR / LOG_FILE).open("a")
359
+ except Exception:
360
+ log = subprocess.DEVNULL
361
+ try:
362
+ proc = subprocess.Popen(
363
+ ["uap", "deliver", "--epics", "--", mission],
364
+ cwd=str(root), stdout=log, stderr=log, stdin=subprocess.DEVNULL,
365
+ start_new_session=True,
366
+ )
367
+ try:
368
+ (root / UAP_DIR / COHERENT_LOCK).write_text(str(proc.pid))
369
+ except Exception:
370
+ pass
371
+ except Exception:
372
+ pass
373
+
374
+
179
375
  def main() -> None:
180
376
  ap = argparse.ArgumentParser()
181
377
  ap.add_argument("--tool", default="")
@@ -194,7 +390,7 @@ def main() -> None:
194
390
  args = {}
195
391
 
196
392
  root = Path(ns.root)
197
- d = decide(out, ns.tool, args, _autoroute_enabled(), _load_seen(root))
393
+ d = decide(out, ns.tool, args, _autoroute_enabled(), _load_seen(root), _replay_enabled())
198
394
 
199
395
  if d["intent"] is not None:
200
396
  try:
@@ -205,20 +401,43 @@ def main() -> None:
205
401
  except Exception:
206
402
  pass
207
403
 
208
- if d["spawn"]:
404
+ # PHASE 1 (opt-in): route the whole mission to ONE coherent agentic epic run
405
+ # instead of the per-file side-channel. When it takes over, the single
406
+ # `uap deliver --epics` run owns the build, so we do NOT also fire per-file
407
+ # replay/spawn for this write (that is what produced non-integrating output).
408
+ coherent = ""
409
+ message = d["message"]
410
+ if _coherent_enabled() and d["route"] == "deliver" and d["file_path"]:
411
+ mission = _recover_mission(root)
412
+ coherent = coherent_route(True, d["route"], True, mission, _coherent_inflight(root))
413
+ if coherent == "spawn":
414
+ _spawn_coherent_epic(root, mission)
415
+ message = d["message"] + " [routed to a single coherent `uap deliver --epics` run — building the whole mission in-band]"
416
+ elif coherent == "wait":
417
+ message = d["message"] + " [a coherent `uap deliver --epics` run is already building this mission — intent recorded, not re-routed]"
418
+
419
+ if not coherent and (d["replay"] or d["spawn"]):
209
420
  try:
210
421
  # Dedup on the SAME key `decide` gated on (file when present, else the
211
422
  # hint) — writing file_path here would record "" for a bash-routed
212
- # intent and never dedup it.
423
+ # intent and never dedup it. Both paths (deterministic replay and the
424
+ # model-spawn autoroute) mark the change seen so a retry does not
425
+ # double-run.
213
426
  key = d.get("dedup_key") or d["file_path"] or d["hint"]
214
427
  with _seen_path(root).open("a") as f:
215
428
  f.write(key.replace("\n", " ").replace("\r", " ") + "\n")
216
429
  except Exception:
217
430
  pass
218
- _spawn_deliver(root, d["hint"])
431
+ if d["replay"]:
432
+ # Deterministic path: the blocked write's content is recorded, so
433
+ # replay it to disk exactly (no model). This is what makes the block
434
+ # productive instead of an infinite record-and-re-block loop.
435
+ _spawn_pending_replay(root, d["file_path"])
436
+ else:
437
+ _spawn_deliver(root, d["hint"])
219
438
 
220
439
  prefix = ("[UAP policy blocked: " + ns.policy + "] ") if ns.policy else ""
221
- sys.stdout.write(prefix + d["message"])
440
+ sys.stdout.write(prefix + message)
222
441
 
223
442
 
224
443
  if __name__ == "__main__":