simplicio-loop 3.16.0__tar.gz → 3.20.0__tar.gz

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 (63) hide show
  1. {simplicio_loop-3.16.0/simplicio_loop.egg-info → simplicio_loop-3.20.0}/PKG-INFO +1 -1
  2. {simplicio_loop-3.16.0 → simplicio_loop-3.20.0}/README.md +20 -3
  3. {simplicio_loop-3.16.0 → simplicio_loop-3.20.0}/pyproject.toml +1 -1
  4. {simplicio_loop-3.16.0 → simplicio_loop-3.20.0}/simplicio_loop/_bundle/hooks/loop_stop.py +264 -10
  5. simplicio_loop-3.20.0/simplicio_loop/_bundle/scripts/__pycache__/cross_agent_wiki.cpython-314.pyc +0 -0
  6. simplicio_loop-3.20.0/simplicio_loop/_bundle/scripts/__pycache__/hierarchical_planner.cpython-314.pyc +0 -0
  7. simplicio_loop-3.20.0/simplicio_loop/_bundle/scripts/cross_agent_wiki.py +606 -0
  8. simplicio_loop-3.20.0/simplicio_loop/_bundle/scripts/hierarchical_planner.py +388 -0
  9. {simplicio_loop-3.16.0 → simplicio_loop-3.20.0}/simplicio_loop/_bundle/skills/simplicio-loop/SKILL.md +317 -17
  10. {simplicio_loop-3.16.0 → simplicio_loop-3.20.0}/simplicio_loop/_bundle/skills/simplicio-review/SKILL.md +5 -5
  11. {simplicio_loop-3.16.0 → simplicio_loop-3.20.0}/simplicio_loop/_bundle/skills/simplicio-tasks/SKILL.md +17 -4
  12. {simplicio_loop-3.16.0 → simplicio_loop-3.20.0}/simplicio_loop/_bundle/skills/simplicio-tasks/references/orchestration.md +38 -13
  13. {simplicio_loop-3.16.0 → simplicio_loop-3.20.0}/simplicio_loop/_bundle/skills/simplicio-tasks/references/quality-safety-delivery.md +8 -6
  14. simplicio_loop-3.20.0/simplicio_loop/_bundle/tests/__pycache__/_selfrun.cpython-314.pyc +0 -0
  15. simplicio_loop-3.20.0/simplicio_loop/_bundle/tests/__pycache__/test_loop_e2e.cpython-314-pytest-9.0.3.pyc +0 -0
  16. simplicio_loop-3.20.0/simplicio_loop/_bundle/tests/__pycache__/test_loop_e2e.cpython-314.pyc +0 -0
  17. simplicio_loop-3.20.0/simplicio_loop/_bundle/tests/_selfrun.py +46 -0
  18. simplicio_loop-3.20.0/simplicio_loop/_bundle/tests/test_cross_agent_wiki.py +106 -0
  19. simplicio_loop-3.20.0/simplicio_loop/_bundle/tests/test_loop_e2e.py +304 -0
  20. {simplicio_loop-3.16.0 → simplicio_loop-3.20.0/simplicio_loop.egg-info}/PKG-INFO +1 -1
  21. {simplicio_loop-3.16.0 → simplicio_loop-3.20.0}/simplicio_loop.egg-info/SOURCES.txt +12 -0
  22. simplicio_loop-3.20.0/tests/test_cross_agent_wiki.py +106 -0
  23. simplicio_loop-3.20.0/tests/test_hierarchical_planner.py +63 -0
  24. simplicio_loop-3.20.0/tests/test_loop_e2e.py +304 -0
  25. simplicio_loop-3.16.0/tests/test_loop_e2e.py +0 -146
  26. {simplicio_loop-3.16.0 → simplicio_loop-3.20.0}/LICENSE +0 -0
  27. {simplicio_loop-3.16.0 → simplicio_loop-3.20.0}/MANIFEST.in +0 -0
  28. {simplicio_loop-3.16.0 → simplicio_loop-3.20.0}/PYPI.md +0 -0
  29. {simplicio_loop-3.16.0 → simplicio_loop-3.20.0}/setup.cfg +0 -0
  30. {simplicio_loop-3.16.0 → simplicio_loop-3.20.0}/simplicio_loop/__init__.py +0 -0
  31. {simplicio_loop-3.16.0 → simplicio_loop-3.20.0}/simplicio_loop/_bundle/hooks/README.md +0 -0
  32. {simplicio_loop-3.16.0 → simplicio_loop-3.20.0}/simplicio_loop/_bundle/hooks/action_gate.py +0 -0
  33. {simplicio_loop-3.16.0 → simplicio_loop-3.20.0}/simplicio_loop/_bundle/hooks/hooks.claude.json +0 -0
  34. {simplicio_loop-3.16.0 → simplicio_loop-3.20.0}/simplicio_loop/_bundle/hooks/hooks.json +0 -0
  35. {simplicio_loop-3.16.0 → simplicio_loop-3.20.0}/simplicio_loop/_bundle/hooks/learn_stop.py +0 -0
  36. {simplicio_loop-3.16.0 → simplicio_loop-3.20.0}/simplicio_loop/_bundle/hooks/loop_capture.py +0 -0
  37. {simplicio_loop-3.16.0 → simplicio_loop-3.20.0}/simplicio_loop/_bundle/hooks/orient_clamp.py +0 -0
  38. {simplicio_loop-3.16.0 → simplicio_loop-3.20.0}/simplicio_loop/_bundle/hooks/orient_rewrite.py +0 -0
  39. {simplicio_loop-3.16.0 → simplicio_loop-3.20.0}/simplicio_loop/_bundle/hooks/simplicio_dashboard.py +0 -0
  40. {simplicio_loop-3.16.0 → simplicio_loop-3.20.0}/simplicio_loop/_bundle/hooks/simplicio_watch.py +0 -0
  41. {simplicio_loop-3.16.0 → simplicio_loop-3.20.0}/simplicio_loop/_bundle/skills/simplicio-compress/SKILL.md +0 -0
  42. {simplicio_loop-3.16.0 → simplicio_loop-3.20.0}/simplicio_loop/_bundle/skills/simplicio-learn/SKILL.md +0 -0
  43. {simplicio_loop-3.16.0 → simplicio_loop-3.20.0}/simplicio_loop/_bundle/skills/simplicio-orient/SKILL.md +0 -0
  44. {simplicio_loop-3.16.0 → simplicio_loop-3.20.0}/simplicio_loop/_bundle/skills/simplicio-tasks/references/agentsview-adapter.md +0 -0
  45. {simplicio_loop-3.16.0 → simplicio_loop-3.20.0}/simplicio_loop/_bundle/skills/simplicio-tasks/references/azure-devops-adapter.md +0 -0
  46. {simplicio_loop-3.16.0 → simplicio_loop-3.20.0}/simplicio_loop/_bundle/skills/simplicio-tasks/references/extension-points.md +0 -0
  47. {simplicio_loop-3.16.0 → simplicio_loop-3.20.0}/simplicio_loop/_bundle/skills/simplicio-tasks/references/lmcache-adapter.md +0 -0
  48. {simplicio_loop-3.16.0 → simplicio_loop-3.20.0}/simplicio_loop/_bundle/skills/simplicio-tasks/references/standing-loop-247.md +0 -0
  49. {simplicio_loop-3.16.0 → simplicio_loop-3.20.0}/simplicio_loop/_bundle/skills/simplicio-tasks/references/token-capture.md +0 -0
  50. {simplicio_loop-3.16.0 → simplicio_loop-3.20.0}/simplicio_loop/_bundle/skills/simplicio-tasks/references/token-economy.md +0 -0
  51. {simplicio_loop-3.16.0 → simplicio_loop-3.20.0}/simplicio_loop/_bundle/skills/simplicio-tasks/references/understand-anything-adapter.md +0 -0
  52. {simplicio_loop-3.16.0 → simplicio_loop-3.20.0}/simplicio_loop/_bundle/skills/simplicio-tasks/references/video-evidence.md +0 -0
  53. {simplicio_loop-3.16.0 → simplicio_loop-3.20.0}/simplicio_loop/_bundle/skills/simplicio-tasks/references/web-evidence.md +0 -0
  54. {simplicio_loop-3.16.0 → simplicio_loop-3.20.0}/simplicio_loop/cli.py +0 -0
  55. {simplicio_loop-3.16.0 → simplicio_loop-3.20.0}/simplicio_loop.egg-info/dependency_links.txt +0 -0
  56. {simplicio_loop-3.16.0 → simplicio_loop-3.20.0}/simplicio_loop.egg-info/entry_points.txt +0 -0
  57. {simplicio_loop-3.16.0 → simplicio_loop-3.20.0}/simplicio_loop.egg-info/requires.txt +0 -0
  58. {simplicio_loop-3.16.0 → simplicio_loop-3.20.0}/simplicio_loop.egg-info/top_level.txt +0 -0
  59. {simplicio_loop-3.16.0 → simplicio_loop-3.20.0}/tests/test_action_gate.py +0 -0
  60. {simplicio_loop-3.16.0 → simplicio_loop-3.20.0}/tests/test_flow_audit.py +0 -0
  61. {simplicio_loop-3.16.0 → simplicio_loop-3.20.0}/tests/test_impact_audit.py +0 -0
  62. {simplicio_loop-3.16.0 → simplicio_loop-3.20.0}/tests/test_worker_selftests.py +0 -0
  63. {simplicio_loop-3.16.0 → simplicio_loop-3.20.0}/tests/test_worker_smoke.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: simplicio-loop
3
- Version: 3.16.0
3
+ Version: 3.20.0
4
4
  Summary: The Universal Looping AI Orchestrator — a runtime-agnostic super-plugin (6 skills) that drains any queue of work end-to-end on any LLM/runtime.
5
5
  Author-email: Wesley Simplicio <wesleybob4@gmail.com>
6
6
  License: MIT
@@ -75,12 +75,23 @@ and keeps watching **24/7** for new work — all behind safety gates and a hard
75
75
  Three things make it different: it is a **super-plugin of focused skills**, it runs the **same
76
76
  protocol on 11 runtimes**, and it does all of this with **aggressive, honest token economy**.
77
77
 
78
+ Within the Simplicio product line, this repo is also the **current reference task flow** for
79
+ company work. `simplicio-runtime` is the unified entrypoint going forward, but it is expected to
80
+ reuse this loop's evidence-gated converge/drain discipline, durable attempt journal, and worker
81
+ coordination patterns instead of creating a separate task semantics.
82
+
78
83
  <p align="center">
79
84
  <img src="assets/simplicio-loop-infographic.png" alt="simplicio-loop — the whole system at a glance: 6 core skills, 5 satellites, 5 accelerators, 48 extension points, 11 runtimes, up to 90% fewer tokens" width="920" />
80
85
  </p>
81
86
 
82
87
  ---
83
88
 
89
+ ## 🤖 LLM front door
90
+
91
+ If you are an agent/runtime entering this repo cold, read `llms.txt` first for the short operational contract, then `AGENTS.md`, then `.claude/skills/simplicio-tasks/SKILL.md`.
92
+
93
+ ---
94
+
84
95
  ## 📘 Official capability record
85
96
 
86
97
  The complete, official roster of what `simplicio-tasks` ships — every capability below is **real,
@@ -90,7 +101,7 @@ deep section and its worker.
90
101
  | Capability | What it does | Proof / worker | Details |
91
102
  |---|---|---|---|
92
103
  | 🎬 **Video evidence** (`video_evidence`) | Records the **real browser session** as moving proof a UI change works (Playwright, default); renders a **deterministic captioned MP4** with [hyperframes](https://github.com/heygen-com/hyperframes) for an explicit explainer request (`/simplicio-tasks make a video of screen X`) | `scripts/video_evidence.py` · BLOCKED (never fake-pass) without the toolchain | [§ Video evidence](#-video-evidence--playwright-by-default-hyperframes-on-request) |
93
- | 🧠 **Attempt memory + stall detector** | A durable run-journal (`.orchestrator/loop/journal.jsonl`) + a stall detector so the loop **changes strategy instead of oscillating**; incremental triage (`since`) reads only the delta each turn | `scripts/loop_journal.py` · `selftest` 9/9 | [§ Anti-oscillation](#-attempt-memory--stall-detector-anti-oscillation) |
104
+ | 🧠 **Attempt memory + stall detector** | A durable run-journal (`.orchestrator/loop/journal.jsonl`) + a stall detector so the loop **changes strategy instead of oscillating**; incremental triage (`since`) reads only the delta each turn, and optional stage lineage makes retries/governance explicit | `scripts/loop_journal.py` · `selftest` 13/13 | [§ Anti-oscillation](#-attempt-memory--stall-detector-anti-oscillation) |
94
105
  | 🧭 **Repo conventions** (`repo_conventions`) | **Learns the repo's own playbook** — mines git history + merged PRs + static config into `.orchestrator/conventions.json` so every new branch/commit/PR mirrors the team's established style; worktree-per-item isolation is the default | `scripts/repo_conventions.py` · `selftest` 19/19 | [§ The full flow](#️-the-full-flow--from-demand-to-delivery) |
95
106
  | 🧩 **Scope reflection** (`dependency_graph`) | Maps local dependencies, reverse dependents, and related tests from the planned touched files; blocks task plans that ignore callers, sibling files, or proof points before the edit starts | `scripts/impact_audit.py` · `selftest` | [§ Tests & local checks](#-tests--local-checks-no-paid-ci) |
96
107
  | 🕸️ **Flow coverage** (`endpoint_compare`) | Maps mixed front/back/service workspaces: UI actions → frontend HTTP calls → backend endpoints → service calls; blocks frontend calls with no backend endpoint and stubbed endpoints, and surfaces unclassified loose ends | `scripts/flow_audit.py` · `selftest` | [§ Tests & local checks](#-tests--local-checks-no-paid-ci) |
@@ -280,7 +291,8 @@ Between turns, LMCache (when available) caches the KV state so re-feed costs nea
280
291
 
281
292
  A re-feed loop that remembers nothing oscillates — try X, fail, try X again — until the cap burns.
282
293
  simplicio-loop keeps a **durable run-journal** (`.orchestrator/loop/journal.jsonl`, append-only:
283
- `iteration · action · hypothesis · gate · error-fingerprint`) and a **stall detector**
294
+ `iteration · action · hypothesis · gate · error-fingerprint`, plus optional lineage like
295
+ `execution_state · stage_id · validator · decision · retry_count`) and a **stall detector**
284
296
  ([`scripts/loop_journal.py`](scripts/loop_journal.py), deterministic + model-free):
285
297
 
286
298
  - **Error fingerprint** — the failing gate output is reduced to a stable hash with line numbers,
@@ -292,10 +304,15 @@ simplicio-loop keeps a **durable run-journal** (`.orchestrator/loop/journal.json
292
304
  avoid, then **switches strategy** or **escalates to the human gate** with the fingerprint.
293
305
  - `loop_journal.py resume` is read at the top of every turn, so a fresh process continues without
294
306
  re-deriving prior attempts (real resume) and never retries a known dead-end.
307
+ - When the loop is doing extraction, validation, or governed retries, `record` can also stamp
308
+ `--execution-state`, `--stage-id`, `--source-artifact`, `--chunk-id`, `--validator`,
309
+ `--decision`, `--retry-count`, `--blocked-reason`, and `--next-action`, so the next turn knows
310
+ not just *what* failed, but *where in the flow* it failed.
295
311
 
296
312
  ```bash
297
313
  loop_journal.py resume # what was tried + dead-ends to avoid
298
- loop_journal.py record --iteration N --action "…" --gate fail --gate-output test.log
314
+ loop_journal.py record --iteration N --action "…" --gate fail --gate-output test.log \
315
+ --execution-state planned --stage-id validate --validator pytest --decision retry
299
316
  loop_journal.py stall --k 3 --exit-code # PROGRESS → re-feed · STALLED → switch/escalate
300
317
  ```
301
318
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "simplicio-loop"
7
- version = "3.16.0"
7
+ version = "3.20.0"
8
8
  description = "The Universal Looping AI Orchestrator — a runtime-agnostic super-plugin (6 skills) that drains any queue of work end-to-end on any LLM/runtime."
9
9
  readme = "PYPI.md"
10
10
  requires-python = ">=3.8"
@@ -21,12 +21,14 @@ the dead-end attempts. A successful (promise-fulfilled) stop needs no handoff.
21
21
  import json
22
22
  import os
23
23
  import re
24
+ import subprocess
24
25
  import sys
25
26
  import time
26
27
 
27
28
  LOOP_DIR = os.path.join(".orchestrator", "loop")
28
29
  SCRATCHPAD = os.path.join(LOOP_DIR, "scratchpad.md")
29
- DONE_FLAG = os.path.join(LOOP_DIR, "done")
30
+ DONE_FLAG = os.path.join(LOOP_DIR, "done.flag")
31
+ LEGACY_DONE_FLAG = os.path.join(LOOP_DIR, "done")
30
32
  LAST_RESP = os.path.join(LOOP_DIR, "last_response.txt")
31
33
  ANCHOR = os.path.join(LOOP_DIR, "anchor.json")
32
34
  JOURNAL = os.path.join(LOOP_DIR, "journal.jsonl")
@@ -35,6 +37,9 @@ STOP_SIGNAL = os.path.join(".orchestrator", "STOP")
35
37
  BUDGET = os.path.join(".orchestrator", "loop-budget.json")
36
38
  GATE_LOCK = os.path.join(LOOP_DIR, "gate.lock")
37
39
  GATE_TTL_SEC = 1800 # 30 min — a stale lock must NEVER permanently trap the loop (fail-open)
40
+ WATCHER_STATE = os.path.join(LOOP_DIR, "watcher_state.json")
41
+ SPINDLE_STATE = os.path.join(LOOP_DIR, "spindle_state.json")
42
+ PHASE_FILE = os.path.join(LOOP_DIR, "phase.json")
38
43
 
39
44
  EVIDENCE_RE = re.compile(
40
45
  r"(https?://\S+/pull/\d+)" # a PR URL
@@ -52,7 +57,7 @@ def allow_stop():
52
57
 
53
58
 
54
59
  def cleanup_and_stop():
55
- for p in (SCRATCHPAD, DONE_FLAG, LAST_RESP):
60
+ for p in (SCRATCHPAD, DONE_FLAG, LEGACY_DONE_FLAG, LAST_RESP, WATCHER_STATE):
56
61
  try:
57
62
  if os.path.exists(p):
58
63
  os.remove(p)
@@ -176,6 +181,29 @@ def tail_journal(n=8):
176
181
  return []
177
182
 
178
183
 
184
+ def attempt_suffix(a):
185
+ bits = []
186
+ if a.get("execution_state"):
187
+ bits.append("state=%s" % a["execution_state"])
188
+ if a.get("stage_id"):
189
+ bits.append("stage=%s" % a["stage_id"])
190
+ if a.get("decision"):
191
+ bits.append("decision=%s" % a["decision"])
192
+ if a.get("validator"):
193
+ bits.append("validator=%s" % a["validator"])
194
+ if a.get("retry_count") is not None:
195
+ bits.append("retry=%s" % a["retry_count"])
196
+ if a.get("chunk_id"):
197
+ bits.append("chunk=%s" % a["chunk_id"])
198
+ if a.get("source_artifact"):
199
+ bits.append("source=%s" % a["source_artifact"])
200
+ if a.get("next_action"):
201
+ bits.append("next=%s" % a["next_action"])
202
+ if a.get("blocked_reason"):
203
+ bits.append("blocked=%s" % a["blocked_reason"])
204
+ return (" — " + " | ".join(bits)) if bits else ""
205
+
206
+
179
207
  def write_handoff(reason, meta=None, body=None):
180
208
  """Write the cross-agent continuation artifact before an INCOMPLETE stop.
181
209
 
@@ -218,13 +246,14 @@ def write_handoff(reason, meta=None, body=None):
218
246
  lines += ["", "## Last attempts (`scripts/loop_journal.py resume` for the full read)"]
219
247
  for a in attempts:
220
248
  lines.append(
221
- "- iter %s: %s -> %s (fp %s)%s"
249
+ "- iter %s: %s -> %s (fp %s)%s%s"
222
250
  % (
223
251
  a.get("iteration", "?"),
224
252
  a.get("action", "?"),
225
253
  a.get("gate", "?"),
226
254
  (a.get("fingerprint") or "")[:12],
227
255
  (" — %s" % a["note"]) if a.get("note") else "",
256
+ attempt_suffix(a),
228
257
  )
229
258
  )
230
259
  lines += [
@@ -241,6 +270,7 @@ def write_handoff(reason, meta=None, body=None):
241
270
  with open(tmp, "w", encoding="utf-8") as f:
242
271
  f.write("\n".join(lines))
243
272
  os.replace(tmp, HANDOFF)
273
+ refresh_cross_agent_wiki(include_handoff=True)
244
274
  except Exception:
245
275
  pass # fail-open: a broken handoff write must never block the stop
246
276
 
@@ -260,6 +290,173 @@ def budget_halted():
260
290
  return False # fail-open: budget unreadable ≠ trap
261
291
 
262
292
 
293
+ def _discover_simplicio_cli():
294
+ """Probe for simplicio CLI in priority order. Returns (binary, sub) or (None, None).
295
+ Silent-fail: any probe error returns (None, None) — never blocks.
296
+ """
297
+ candidates = [
298
+ ("simplicio", "claims"),
299
+ ("simplicio-py", "claims"),
300
+ ("python3", ["-m", "simplicio.cli", "claims"]),
301
+ ]
302
+ for binary, sub in candidates:
303
+ try:
304
+ args = [binary] + (sub if isinstance(sub, list) else [sub, "--help"])
305
+ subprocess.run(args, capture_output=True, timeout=5)
306
+ return binary, sub
307
+ except (FileNotFoundError, subprocess.TimeoutExpired):
308
+ continue
309
+ return None, None
310
+
311
+
312
+ def _call_simplicio_claims():
313
+ """Run ``simplicio claims check`` silently. Fail-open."""
314
+ binary, _ = _discover_simplicio_cli()
315
+ if not binary:
316
+ return
317
+ try:
318
+ subprocess.run(
319
+ [binary, "claims", "check"],
320
+ capture_output=True, timeout=15,
321
+ )
322
+ except Exception:
323
+ pass
324
+
325
+
326
+ def _call_simplicio_nest():
327
+ """Run ``simplicio nest verify`` silently. Fail-open."""
328
+ candidates = [
329
+ ("simplicio", "nest"),
330
+ ("simplicio-py", "nest"),
331
+ ("python3", ["-m", "simplicio.cli", "nest"]),
332
+ ]
333
+ for binary, sub in candidates:
334
+ try:
335
+ args = [binary] + (sub if isinstance(sub, list) else [sub, "--help"])
336
+ subprocess.run(args, capture_output=True, timeout=5)
337
+ nest_binary = binary
338
+ try:
339
+ subprocess.run(
340
+ [nest_binary, "nest", "verify"],
341
+ capture_output=True, timeout=15,
342
+ )
343
+ except Exception:
344
+ pass
345
+ return
346
+ except (FileNotFoundError, subprocess.TimeoutExpired):
347
+ continue
348
+
349
+
350
+ def watcher_verify():
351
+ """Run pre-promise watcher verification per Asolaria N-Nest Corrective Gate pattern.
352
+
353
+ Reads `.orchestrator/loop/watcher_state.json` written by the watcher process (a separate
354
+ agent/PID that independently re-executes the work and compares results against the agent's
355
+ reported output). Gate: `reported == watcher.recomputed_truth`.
356
+
357
+ Returns (passed: bool, tag: str) where tag is "MEASURED" (verified) or "UNVERIFIED" (not
358
+ verified or mismatch). If no watcher state exists → UNVERIFIED (gate fails). Fail-open:
359
+ a corrupt or missing watcher state NEVER traps the loop — it simply gates the promise.
360
+ """
361
+ try:
362
+ if not os.path.exists(WATCHER_STATE):
363
+ return False, "UNVERIFIED"
364
+ with open(WATCHER_STATE, encoding="utf-8") as f:
365
+ state = json.load(f)
366
+ match = bool(state.get("match", False))
367
+ status = str(state.get("status", "UNVERIFIED"))
368
+ if match and status == "MEASURED":
369
+ return True, "MEASURED"
370
+ return False, "UNVERIFIED"
371
+ except Exception:
372
+ return False, "UNVERIFIED"
373
+
374
+
375
+ def spindle_latched():
376
+ """True when a spindle handoff exists with an unreleased latch.
377
+
378
+ A latched handoff means agent A handed off to agent B and is waiting for B to
379
+ confirm receipt. When latched, the loop must NOT re-feed the goal — the handoff
380
+ target will pick up. Fail-open: unreadable/missing file → False (never trap).
381
+ """
382
+ try:
383
+ if not os.path.exists(SPINDLE_STATE):
384
+ return False
385
+ with open(SPINDLE_STATE, encoding="utf-8") as f:
386
+ s = json.load(f)
387
+ return bool(s.get("latch", False))
388
+ except Exception:
389
+ return False
390
+
391
+
392
+ def spindle_active():
393
+ """True when a spindle handoff exists and IS confirmed (current agent is processing).
394
+
395
+ An active (confirmed) handoff means the receiving agent confirmed receipt and is
396
+ working. The loop can continue normally for this agent. Fail-open: unreadable/missing
397
+ file → False (never trap).
398
+ """
399
+ try:
400
+ if not os.path.exists(SPINDLE_STATE):
401
+ return False
402
+ with open(SPINDLE_STATE, encoding="utf-8") as f:
403
+ s = json.load(f)
404
+ return bool(s.get("current_agent")) and not bool(s.get("latch", False))
405
+ except Exception:
406
+ return False
407
+
408
+
409
+ def read_phase():
410
+ """Read the current hierarchical phase, if any. Fail-open."""
411
+ try:
412
+ if not os.path.exists(PHASE_FILE):
413
+ return None
414
+ with open(PHASE_FILE, encoding="utf-8") as f:
415
+ phase = json.load(f)
416
+ return phase if isinstance(phase, dict) else None
417
+ except Exception:
418
+ return None
419
+
420
+
421
+ def _call_cross_agent_wiki(command):
422
+ """Capture/refresh the shared wiki used for cross-agent continuity. Fail-open."""
423
+ try:
424
+ repo_root = os.getcwd()
425
+ script = os.path.join(repo_root, "scripts", "cross_agent_wiki.py")
426
+ if not os.path.exists(script):
427
+ return
428
+ subprocess.run(
429
+ [sys.executable, script, command],
430
+ capture_output=True, timeout=15,
431
+ cwd=repo_root,
432
+ )
433
+ except Exception:
434
+ pass
435
+
436
+
437
+ def refresh_cross_agent_wiki(include_handoff=False):
438
+ """Best-effort wiki maintenance at loop boundaries. Fail-open."""
439
+ _call_cross_agent_wiki("capture")
440
+ _call_cross_agent_wiki("summary")
441
+ if include_handoff:
442
+ _call_cross_agent_wiki("handoff")
443
+
444
+
445
+ def phase_header_hint():
446
+ """Render a short phase hint for the next iteration header. Empty when flat."""
447
+ phase = read_phase()
448
+ if not phase:
449
+ return ""
450
+ bits = [" phase=%s" % phase.get("phase", "?")]
451
+ strategy = str(phase.get("strategy", "")).strip()
452
+ guard = str(phase.get("tactical_guard", "")).strip()
453
+ if strategy:
454
+ bits.append(" strategy=%s" % strategy[:120])
455
+ if guard:
456
+ bits.append(" guard=%s" % guard[:120])
457
+ return "".join(bits)
458
+
459
+
263
460
  def emit_refeed(followup):
264
461
  """Emit the re-feed in BOTH schemas; each runtime reads its own key."""
265
462
  out = {
@@ -281,6 +478,13 @@ def main():
281
478
  except OSError:
282
479
  meta, body = None, None
283
480
 
481
+ # Fire-and-forget simplicio CLI callout: verify claims and nest tree.
482
+ # Disabled when no scratchpad exists (no active loop). Silent failure
483
+ # if the CLI is not installed — the loop proceeds either way.
484
+ if os.path.exists(SCRATCHPAD):
485
+ _call_simplicio_claims()
486
+ _call_simplicio_nest()
487
+
284
488
  # Explicit STOP signal beats everything — but still hand off if there was live state.
285
489
  if os.path.exists(STOP_SIGNAL):
286
490
  if meta is not None:
@@ -309,19 +513,31 @@ def main():
309
513
  stdin = read_stdin_json()
310
514
  resp = last_assistant_text(stdin)
311
515
 
516
+ # HRM-style hierarchical planner: re-assess phase on stall or every N iterations.
517
+ # Runs BEFORE the promise gate so the phase context is available.
518
+ _call_hierarchical_planner()
519
+
520
+ # Pre-promise: watcher-gate — independent verification before any promise is honored.
521
+ # Per Asolaria N-Nest Corrective Gate: each agent PID has a watcher PID that
522
+ # independently re-computes the truth. Gate: reported == watcher.recomputed_truth.
523
+ watcher_pass, watcher_tag = watcher_verify()
524
+
312
525
  # Completion detection (capture folded in for single-hook runtimes like Claude).
313
526
  if promise and resp:
314
527
  m = PROMISE_RE.search(resp)
315
528
  if m and m.group(1).strip() == promise.strip():
316
529
  has_evidence = bool(EVIDENCE_RE.search(resp))
317
- # The promise is honored only with evidence AND no acceptance criterion still open
318
- # in the task anchor the mechanical anti-drift gate. Pending ACs ⇒ ignore the
319
- # promise and keep looping (still bounded by max_iter), never a false "done".
320
- if ((not evidence_required) or has_evidence) and not anchor_pending():
530
+ # The promise is honored only with evidence AND watcher verification AND no
531
+ # acceptance criterion still open in the task anchor. The watcher-gate ensures
532
+ # the agent's result was independently re-executed and matched before the
533
+ # promise is accepted corrective gate per Asolaria.
534
+ if ((not evidence_required) or has_evidence) and watcher_pass and not anchor_pending():
535
+ refresh_cross_agent_wiki(include_handoff=False)
321
536
  cleanup_and_stop() # (3) promise fulfilled → stop, no handoff needed
322
- # promise without evidence, or anchor still has open ACs → ignore, keep looping
537
+ # promise without evidence, or watcher disagrees, or anchor still has open ACs
538
+ # → ignore, keep looping
323
539
  # (3') Cursor capture may have raised the flag.
324
- if os.path.exists(DONE_FLAG):
540
+ if os.path.exists(DONE_FLAG) or os.path.exists(LEGACY_DONE_FLAG):
325
541
  cleanup_and_stop()
326
542
  # (4) Iteration cap — incomplete stop, hand off.
327
543
  if max_iter > 0 and iteration >= max_iter:
@@ -332,6 +548,17 @@ def main():
332
548
  if budget_halted():
333
549
  write_handoff("budget halted", meta, body)
334
550
  cleanup_and_stop()
551
+ # (5b) Spindle handoff — latched handoff overrides re-feed.
552
+ if spindle_latched():
553
+ next_agent = "?"
554
+ try:
555
+ with open(SPINDLE_STATE, encoding="utf-8") as _f:
556
+ _s = json.load(_f)
557
+ next_agent = _s.get("next_agent", "?")
558
+ except Exception:
559
+ pass
560
+ write_handoff("spindle handoff (latched — waiting for '%s')" % next_agent, meta, body)
561
+ cleanup_and_stop()
335
562
  # (6) Continue: bump iteration in place, re-feed the goal body.
336
563
  nxt = iteration + 1
337
564
  with open(SCRATCHPAD, encoding="utf-8") as f:
@@ -361,11 +588,38 @@ def main():
361
588
  if pending
362
589
  else ""
363
590
  )
364
- header = "[simplicio-loop iteration %d.%s%s]" % (nxt, promise_hint, ac_hint)
591
+ header = "[simplicio-loop iteration %d.%s%s%s %s]" % (
592
+ nxt, promise_hint, ac_hint, phase_header_hint(), watcher_tag
593
+ )
594
+ refresh_cross_agent_wiki(include_handoff=False)
365
595
  emit_refeed(header + "\n\n" + (body or ""))
366
596
  except Exception:
367
597
  allow_stop() # fail-open, always
368
598
 
369
599
 
600
+ def _call_hierarchical_planner():
601
+ """Run the HRM-style hierarchical planner if a scratchpad exists. Fail-open.
602
+
603
+ The planner reads the journal and current phase, then MAY write a new phase
604
+ (`.orchestrator/loop/phase.json`) on stall detection or every N iterations.
605
+ The phase context is consumed by the re-feed header or the loop's decision logic.
606
+ Fail-open: any error here must never trap the loop; the loop runs in flat mode
607
+ if the planner is missing or broken.
608
+ """
609
+ try:
610
+ if os.path.exists(SCRATCHPAD):
611
+ repo_root = os.getcwd()
612
+ script = os.path.join(repo_root, "scripts", "hierarchical_planner.py")
613
+ if not os.path.exists(script):
614
+ return
615
+ subprocess.run(
616
+ [sys.executable, script, "plan"],
617
+ capture_output=True, timeout=15,
618
+ cwd=repo_root,
619
+ )
620
+ except Exception:
621
+ pass # fail-open
622
+
623
+
370
624
  if __name__ == "__main__":
371
625
  main()