@miller-tech/uap 1.158.0 → 1.159.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.
@@ -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__":
@@ -30,9 +30,13 @@ class DecideTest(unittest.TestCase):
30
30
  def test_deliver_route_logs_intent_no_spawn_when_off(self):
31
31
  d = mod.decide(BLOCK_OUT, "Write", ARGS, False, set())
32
32
  self.assertFalse(d["spawn"])
33
+ self.assertFalse(d["replay"]) # no recorded content -> not replayable
33
34
  self.assertIsNotNone(d["intent"])
34
35
  self.assertEqual(d["intent"]["file_path"], "/repo/src/foo.ts")
35
- self.assertEqual(d["message"], BLOCK_OUT["reason"]) # unchanged
36
+ # autoroute off + no recorded content: message is annotated to tell the
37
+ # agent to run deliver itself; the block reason is preserved as prefix.
38
+ self.assertTrue(d["message"].startswith(BLOCK_OUT["reason"]))
39
+ self.assertIn("apply it yourself", d["message"])
36
40
 
37
41
  def test_deliver_route_spawns_when_on_and_unseen(self):
38
42
  d = mod.decide(BLOCK_OUT, "Write", ARGS, True, set())
@@ -117,5 +121,116 @@ class BashRoutedIntentTest(unittest.TestCase):
117
121
  self.assertFalse(d["spawn"])
118
122
 
119
123
 
124
+ class BashHeredocReplayTest(unittest.TestCase):
125
+ """A model whose Write tool is gated reaches for `cat > FILE << EOF ... EOF`.
126
+ That heredoc carries the path AND body in the command, so autoroute recovers
127
+ both and the write becomes REPLAYABLE — else the model's `cat >` rewrites are
128
+ blocked forever (octopus_invaders_v3, 2026-07-16: 35 min, 0 landed)."""
129
+
130
+ HEREDOC = 'cat > src/app.js <<EOF\nconsole.log(1)\nEOF'
131
+
132
+ def test_parse_bash_write_extracts_path_and_body(self):
133
+ p, b = mod._parse_bash_write(self.HEREDOC)
134
+ self.assertEqual(p, 'src/app.js')
135
+ self.assertEqual(b, 'console.log(1)')
136
+
137
+ def test_parse_handles_wrapper_and_quoted_delim(self):
138
+ cmd = "UAP_DELIVER_BYPASS=1 bash -c 'cat > /a/game.js << \"GAMEJS\"\nX=1\nGAMEJS'"
139
+ p, b = mod._parse_bash_write(cmd)
140
+ self.assertEqual(p, '/a/game.js')
141
+ self.assertEqual(b, 'X=1')
142
+
143
+ def test_parse_returns_none_for_non_heredoc(self):
144
+ self.assertEqual(mod._parse_bash_write('cat > a.js'), (None, None))
145
+ self.assertEqual(mod._parse_bash_write('echo hi > a.js'), (None, None))
146
+
147
+ def test_bash_heredoc_is_replayable(self):
148
+ d = mod.decide(BLOCK_OUT, "Bash", {"command": self.HEREDOC}, True, set())
149
+ self.assertTrue(d["replay"]) # recovered content -> deterministic replay
150
+ self.assertFalse(d["spawn"])
151
+ self.assertEqual(d["file_path"], 'src/app.js') # path recovered from the command
152
+ self.assertEqual(d["intent"]["edit"]["content"], 'console.log(1)')
153
+
154
+ def test_bash_without_body_still_model_spawns(self):
155
+ # No heredoc body to recover -> not replayable -> model-spawn on the hint.
156
+ d = mod.decide(BLOCK_OUT, "Bash", {"command": "cat > a.js <<EOF"}, True, set())
157
+ self.assertFalse(d["replay"])
158
+ self.assertTrue(d["spawn"])
159
+ self.assertEqual(d["file_path"], "")
160
+
161
+
162
+ class ReplayIntentTest(unittest.TestCase):
163
+ """A REPLAYABLE intent (the blocked edit's exact content was recorded) must
164
+ be applied DETERMINISTICALLY via `uap deliver --pending` — not re-routed
165
+ through a fresh model deliver that just re-blocks. Without this the blocked
166
+ write is recorded but never lands: the model re-emits it forever and 0 files
167
+ change (octopus_invaders_v3, 2026-07-16 — every run frozen at phase 0)."""
168
+
169
+ REPLAY_ARGS = {"file_path": "/repo/src/foo.ts", "content": "const X = 1;\n"}
170
+
171
+ def test_content_intent_is_replayable_not_model_spawn(self):
172
+ d = mod.decide(BLOCK_OUT, "Write", self.REPLAY_ARGS, True, set())
173
+ self.assertTrue(d["replay"])
174
+ self.assertFalse(d["spawn"]) # deterministic replay, not a model run
175
+ self.assertIn("--pending", d["message"])
176
+ self.assertEqual(d["intent"]["edit"]["content"], "const X = 1;\n")
177
+
178
+ def test_replay_not_gated_on_autoroute(self):
179
+ d = mod.decide(BLOCK_OUT, "Write", self.REPLAY_ARGS, False, set())
180
+ self.assertTrue(d["replay"]) # safe deterministic path runs even when autoroute is off
181
+ self.assertFalse(d["spawn"])
182
+
183
+ def test_enforcer_editIntent_old_new_is_replayable(self):
184
+ out = dict(BLOCK_OUT); out["editIntent"] = {"old_string": "a", "new_string": "b"}
185
+ d = mod.decide(out, "Edit", {"file_path": "/repo/src/foo.ts"}, True, set())
186
+ self.assertTrue(d["replay"])
187
+ self.assertFalse(d["spawn"])
188
+
189
+ def test_non_replayable_still_model_spawns(self):
190
+ d = mod.decide(BLOCK_OUT, "Write", ARGS, True, set()) # no content recorded
191
+ self.assertFalse(d["replay"])
192
+ self.assertTrue(d["spawn"])
193
+
194
+
195
+ class CoherentMissionRouteTest(unittest.TestCase):
196
+ """Phase 1: route the whole mission to ONE agentic `uap deliver --epics` run
197
+ instead of landing files one at a time via the per-file replay/model-spawn
198
+ side-channel — which produced syntactically-valid but NON-integrating output
199
+ (octopus_invaders_v3, 2026-07-16). Opt-in: UAP_DELIVER_COHERENT_MISSION."""
200
+
201
+ def test_off_by_default(self):
202
+ self.assertEqual(mod.coherent_route(False, "deliver", True, "build X", False), "")
203
+
204
+ def test_spawn_when_on_mission_and_free(self):
205
+ self.assertEqual(mod.coherent_route(True, "deliver", True, "build X", False), "spawn")
206
+
207
+ def test_wait_when_run_already_inflight(self):
208
+ self.assertEqual(mod.coherent_route(True, "deliver", True, "build X", True), "wait")
209
+
210
+ def test_no_route_without_mission(self):
211
+ self.assertEqual(mod.coherent_route(True, "deliver", True, "", False), "")
212
+
213
+ def test_no_route_without_write_intent(self):
214
+ self.assertEqual(mod.coherent_route(True, "deliver", False, "build X", False), "")
215
+
216
+ def test_no_route_for_non_deliver(self):
217
+ self.assertEqual(mod.coherent_route(True, "worktree", True, "build X", False), "")
218
+
219
+ def test_recover_mission_from_ledger(self):
220
+ import tempfile, os, json as _j
221
+ from pathlib import Path
222
+ with tempfile.TemporaryDirectory() as td:
223
+ os.makedirs(os.path.join(td, ".uap"))
224
+ _j.dump({"mission": "Build the thing"},
225
+ open(os.path.join(td, ".uap", "completion-ledger.json"), "w"))
226
+ self.assertEqual(mod._recover_mission(Path(td)), "Build the thing")
227
+
228
+ def test_recover_mission_empty_when_absent(self):
229
+ import tempfile
230
+ from pathlib import Path
231
+ with tempfile.TemporaryDirectory() as td:
232
+ self.assertEqual(mod._recover_mission(Path(td)), "")
233
+
234
+
120
235
  if __name__ == "__main__":
121
236
  unittest.main()